hexo-theme-stellar 1.40.0 → 1.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/_config.yml +33 -10
- package/_data/icons.yml +250 -37
- package/languages/en.yml +6 -0
- package/languages/zh-CN.yml +6 -0
- package/languages/zh-TW.yml +6 -0
- package/layout/_partial/comments/artalk/script.ejs +25 -8
- package/layout/_partial/comments/beaudar/script.ejs +1 -0
- package/layout/_partial/comments/twikoo/script.ejs +1 -0
- package/layout/_partial/comments/utterances/script.ejs +1 -0
- package/layout/_partial/comments/waline/script.ejs +1 -0
- package/layout/_partial/head.ejs +43 -7
- package/layout/_partial/main/article/article_tags.ejs +16 -0
- package/layout/_partial/main/navbar/article_banner.ejs +11 -1
- package/layout/_partial/main/notebook/note_card.ejs +1 -1
- package/layout/_partial/main/notebook/paginator.ejs +2 -2
- package/layout/_partial/main/pin_slider.ejs +2 -2
- package/layout/_partial/main/post_list/paginator.ejs +2 -2
- package/layout/_partial/menubtn.ejs +2 -2
- package/layout/_partial/scripts/defines.ejs +26 -2
- package/layout/_partial/scripts.ejs +30 -7
- package/layout/_partial/sidebar/logo.ejs +1 -1
- package/layout/_partial/sidebar/menu.ejs +1 -1
- package/layout/_partial/sidebar/search.ejs +1 -1
- package/layout/_partial/widgets/author.ejs +1 -1
- package/layout/_partial/widgets/components/link.ejs +2 -2
- package/layout/_partial/widgets/ghuser.ejs +1 -1
- package/layout/_partial/widgets/related.ejs +1 -1
- package/layout/_partial/widgets/toc.ejs +1 -1
- package/layout/_partial/widgets/tree.ejs +1 -1
- package/layout/_plugins/fancybox.ejs +4 -2
- package/layout/_plugins/mermaid.ejs +2 -1
- package/layout/_plugins/search/local_search.ejs +4 -3
- package/layout/_plugins/swiper.ejs +1 -0
- package/layout/categories.ejs +1 -1
- package/layout/page.ejs +3 -0
- package/layout/tags.ejs +1 -0
- package/package.json +1 -1
- package/scripts/events/lib/doc_tree.js +12 -208
- package/scripts/events/lib/notebooks.js +8 -148
- package/scripts/events/lib/utils.js +10 -1
- package/scripts/filters/index.js +1 -0
- package/scripts/filters/lib/img_lazyload.js +4 -0
- package/scripts/filters/lib/img_onerror.js +4 -0
- package/scripts/filters/lib/md_table.js +46 -0
- package/scripts/generators/search.js +12 -18
- package/scripts/generators/stellar-icons.js +47 -0
- package/scripts/helpers/ai_label.js +29 -0
- package/scripts/helpers/icon.js +2 -2
- package/scripts/helpers/json_ld.js +10 -13
- package/scripts/helpers/related_posts.js +0 -7
- package/scripts/helpers/seo.js +9 -0
- package/scripts/lib/ai_label.js +55 -0
- package/scripts/lib/doc_tree.js +267 -0
- package/scripts/lib/notebooks.js +181 -0
- package/scripts/lib/search_index.js +82 -0
- package/scripts/lib/seo.js +79 -0
- package/scripts/tags/lib/about.js +2 -2
- package/scripts/tags/lib/albums.js +1 -1
- package/scripts/tags/lib/banner.js +1 -1
- package/scripts/tags/lib/chat.js +49 -49
- package/scripts/tags/lib/copy.js +1 -1
- package/scripts/tags/lib/friends.js +1 -1
- package/scripts/tags/lib/gallery.js +1 -1
- package/scripts/tags/lib/hashtag.js +1 -1
- package/scripts/tags/lib/image.js +3 -3
- package/scripts/tags/lib/posters.js +1 -1
- package/scripts/tags/lib/sites.js +2 -2
- package/source/css/_common/base.styl +5 -9
- package/source/css/_common/device.styl +3 -3
- package/source/css/_components/md-table.styl +23 -0
- package/source/css/_components/pages/archives.styl +2 -22
- package/source/css/_components/pages/article-story.styl +4 -4
- package/source/css/_components/partial/article-tags.styl +10 -0
- package/source/css/_components/partial/bread-nav.styl +14 -0
- package/source/css/_components/partial/navbar.styl +10 -2
- package/source/css/_components/sidebar/search.styl +24 -4
- package/source/css/_components/tag-plugins/table.styl +16 -11
- package/source/css/_components/widgets/components.styl +6 -0
- package/source/css/_components/widgets/list.styl +6 -0
- package/source/css/_components/widgets/toc.styl +1 -1
- package/source/css/_custom.styl +1 -12
- package/source/css/_defines/func.styl +28 -3
- package/source/css/_plugins/index.styl +0 -18
- package/source/css/{_plugins/comments → comments}/artalk.styl +4 -0
- package/source/css/{_plugins/comments → comments}/twikoo.styl +4 -0
- package/source/css/{_plugins → plugins}/mermaid.styl +3 -0
- package/source/css/{_plugins → plugins}/swiper.styl +5 -1
- package/source/js/icons.js +60 -0
- package/source/js/main.js +114 -0
- package/source/js/search/highlight.js +82 -0
- package/source/js/search/local-search.js +294 -103
- package/source/js/services/timeline.js +1 -1
- package/source/js/services/weibo.js +3 -3
- package/source/js/services.js +179 -0
- package/source/js/tagtree.js +29 -0
- package/source/js/theme.js +61 -0
- package/{layout/_partial/scripts/utils.ejs → source/js/utils.js} +13 -10
- package/layout/_partial/scripts/services.ejs +0 -178
- package/layout/_partial/scripts/sidebar.ejs +0 -25
- package/layout/_partial/scripts/tagtree.ejs +0 -29
- package/layout/_partial/scripts/theme.ejs +0 -67
- /package/source/css/{_plugins/comments → comments}/beaudar.styl +0 -0
- /package/source/css/{_plugins/comments → comments}/utterances.styl +0 -0
- /package/source/css/{_plugins/comments → comments}/waline.styl +0 -0
- /package/source/css/{_plugins → plugins}/fancybox.styl +0 -0
|
@@ -1,10 +1,105 @@
|
|
|
1
|
-
var searchCache = null;
|
|
2
|
-
var
|
|
1
|
+
var searchCache = null; // 当前可用的搜索数据(数组)
|
|
2
|
+
var searchCacheEntry = null; // 最近读取/写入的缓存条目 { ts, ttl, data }
|
|
3
|
+
var searchCacheKey = 'search_cache_v4';
|
|
4
|
+
var searchFetchPromise = null; // 单飞:同一时刻只允许一个请求
|
|
5
|
+
|
|
6
|
+
var searchLazyLoad = true;
|
|
7
|
+
var searchCacheTtl = 86400;
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
var searchCfg = ctx.search && ctx.search.local_search;
|
|
11
|
+
if (searchCfg) {
|
|
12
|
+
searchLazyLoad = searchCfg.lazy_load !== false;
|
|
13
|
+
searchCacheTtl = typeof searchCfg.cache_ttl === 'number' ? Math.max(0, searchCfg.cache_ttl) : 86400;
|
|
14
|
+
}
|
|
15
|
+
} catch (e) {}
|
|
16
|
+
|
|
17
|
+
function getSearchPath() {
|
|
18
|
+
var path = ctx.search.path;
|
|
19
|
+
if (path.startsWith('/')) {
|
|
20
|
+
path = path.substring(1);
|
|
21
|
+
}
|
|
22
|
+
return ctx.root + path;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readSearchCache() {
|
|
26
|
+
try {
|
|
27
|
+
var raw = localStorage.getItem(searchCacheKey);
|
|
28
|
+
if (!raw) return null;
|
|
29
|
+
var entry = JSON.parse(raw);
|
|
30
|
+
if (!entry || typeof entry.ts !== 'number' || !Array.isArray(entry.data)) return null;
|
|
31
|
+
return entry;
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.warn('搜索缓存读取失败', e);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function writeSearchCache(data) {
|
|
39
|
+
if (searchCacheTtl <= 0) return;
|
|
40
|
+
try {
|
|
41
|
+
var entry = { ts: Date.now(), ttl: searchCacheTtl, data: data };
|
|
42
|
+
localStorage.setItem(searchCacheKey, JSON.stringify(entry));
|
|
43
|
+
searchCacheEntry = entry;
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.warn('搜索缓存写入失败', e);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isCacheFresh(entry) {
|
|
50
|
+
return !!(entry && typeof entry.ts === 'number' && entry.ttl > 0 && Date.now() - entry.ts < entry.ttl * 1000);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 从 localStorage 恢复缓存到内存(ttl=0 时不使用历史缓存)
|
|
54
|
+
function loadCacheIntoMemory() {
|
|
55
|
+
if (searchCacheTtl <= 0) {
|
|
56
|
+
searchCacheEntry = null;
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
if (!searchCacheEntry) {
|
|
60
|
+
searchCacheEntry = readSearchCache();
|
|
61
|
+
}
|
|
62
|
+
if (searchCacheEntry && Array.isArray(searchCacheEntry.data)) {
|
|
63
|
+
searchCache = searchCacheEntry.data;
|
|
64
|
+
}
|
|
65
|
+
return searchCacheEntry;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 是否需要发起请求:不缓存 / 无缓存 / 缓存过期
|
|
69
|
+
function needsFetch() {
|
|
70
|
+
if (searchCacheTtl <= 0) return true;
|
|
71
|
+
var entry = searchCacheEntry || loadCacheIntoMemory();
|
|
72
|
+
return !isCacheFresh(entry);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 拉取搜索数据(单飞),成功后更新内存与 localStorage
|
|
76
|
+
function fetchSearchData(path) {
|
|
77
|
+
if (searchFetchPromise) return searchFetchPromise;
|
|
78
|
+
searchFetchPromise = fetch(path)
|
|
79
|
+
.then(function(res) {
|
|
80
|
+
if (!res.ok) throw new Error('search fetch failed: ' + res.status);
|
|
81
|
+
return res.json();
|
|
82
|
+
})
|
|
83
|
+
.then(function(json) {
|
|
84
|
+
if (!Array.isArray(json)) throw new Error('search data is not an array');
|
|
85
|
+
searchCache = json;
|
|
86
|
+
writeSearchCache(json);
|
|
87
|
+
return json;
|
|
88
|
+
})
|
|
89
|
+
.catch(function(err) {
|
|
90
|
+
console.warn('搜索数据加载失败', err);
|
|
91
|
+
throw err;
|
|
92
|
+
})
|
|
93
|
+
.finally(function() {
|
|
94
|
+
searchFetchPromise = null;
|
|
95
|
+
});
|
|
96
|
+
return searchFetchPromise;
|
|
97
|
+
}
|
|
3
98
|
|
|
4
99
|
var searchFunc = function(path, filter, wrapperId, searchId, contentId) {
|
|
5
100
|
var $input = document.getElementById(searchId);
|
|
6
101
|
if (!$input || $input._searchInitialized === true) return;
|
|
7
|
-
if ($input._searchInitialized === 'pending'
|
|
102
|
+
if ($input._searchInitialized === 'pending') return; // 数据加载中,等待完成后初始化
|
|
8
103
|
|
|
9
104
|
function getAllCombinations(keywords) {
|
|
10
105
|
const result = [];
|
|
@@ -21,8 +116,62 @@ var searchFunc = function(path, filter, wrapperId, searchId, contentId) {
|
|
|
21
116
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
22
117
|
}
|
|
23
118
|
|
|
24
|
-
|
|
25
|
-
|
|
119
|
+
// 关键词组合(含原始大小写):按组合长度降序,用于匹配与跳转高亮
|
|
120
|
+
function buildKeywordPairs(value) {
|
|
121
|
+
const tokens = value.trim().split(" ");
|
|
122
|
+
const combos = getAllCombinations(tokens.map(function(t) { return t.toLowerCase(); }));
|
|
123
|
+
const originalCombos = getAllCombinations(tokens);
|
|
124
|
+
const pairs = combos.map(function(kw, i) {
|
|
125
|
+
return { kw: kw, original: originalCombos[i] };
|
|
126
|
+
});
|
|
127
|
+
return pairs.sort(function(a, b) {
|
|
128
|
+
return b.kw.split(" ").length - a.kw.split(" ").length;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 构建单条搜索结果 DOM
|
|
133
|
+
function buildResultElement(dataTitle, sectionName, secText, secFirst, pairs, href) {
|
|
134
|
+
const li = document.createElement('li');
|
|
135
|
+
|
|
136
|
+
// 文章标题位于链接上方,不参与跳转
|
|
137
|
+
const titleSpan = document.createElement('span');
|
|
138
|
+
titleSpan.className = 'search-result-title';
|
|
139
|
+
titleSpan.textContent = dataTitle;
|
|
140
|
+
li.appendChild(titleSpan);
|
|
141
|
+
|
|
142
|
+
const a = document.createElement('a');
|
|
143
|
+
a.href = href;
|
|
144
|
+
|
|
145
|
+
if (sectionName) {
|
|
146
|
+
const sectionSpan = document.createElement('span');
|
|
147
|
+
sectionSpan.className = 'search-result-section';
|
|
148
|
+
sectionSpan.textContent = sectionName;
|
|
149
|
+
a.appendChild(sectionSpan);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (secText && secFirst >= 0) {
|
|
153
|
+
var start = Math.max(0, secFirst - 20);
|
|
154
|
+
var end = Math.min(secText.length, secFirst + 80);
|
|
155
|
+
if (start === 0) end = 100;
|
|
156
|
+
var matchContent = secText.substring(start, end);
|
|
157
|
+
|
|
158
|
+
var regS = new RegExp(pairs.map(function(p) { return escapeRegExp(p.kw); }).join("|"), "gi");
|
|
159
|
+
matchContent = matchContent.replace(regS, function(keyword) {
|
|
160
|
+
return "<span class=\"search-keyword\">" + keyword + "</span>";
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const para = document.createElement('p');
|
|
164
|
+
para.className = 'search-result-content';
|
|
165
|
+
para.innerHTML = matchContent + '...';
|
|
166
|
+
a.appendChild(para);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
li.appendChild(a);
|
|
170
|
+
return li;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function initSearch() {
|
|
174
|
+
if (!$input) return;
|
|
26
175
|
if ($input._searchInitialized === true) return; // 防止重复绑定
|
|
27
176
|
$input._searchInitialized = true;
|
|
28
177
|
|
|
@@ -30,21 +179,22 @@ var searchFunc = function(path, filter, wrapperId, searchId, contentId) {
|
|
|
30
179
|
var $wrapper = document.getElementById(wrapperId);
|
|
31
180
|
|
|
32
181
|
$input.addEventListener("input", function() {
|
|
33
|
-
var
|
|
34
|
-
var keywords = getAllCombinations(this.value.trim().toLowerCase().split(" "))
|
|
35
|
-
.sort(function(a,b) { return b.split(" ").length - a.split(" ").length; });
|
|
182
|
+
var rawValue = this.value.trim();
|
|
36
183
|
$resultContent.innerHTML = "";
|
|
37
|
-
if (
|
|
184
|
+
if (rawValue.length <= 0) {
|
|
38
185
|
$wrapper.setAttribute('searching', 'false');
|
|
39
186
|
return;
|
|
40
187
|
}
|
|
41
188
|
$wrapper.setAttribute('searching', 'true');
|
|
42
189
|
|
|
43
|
-
|
|
190
|
+
var pairs = buildKeywordPairs(this.value);
|
|
191
|
+
var resultList = [];
|
|
192
|
+
|
|
193
|
+
// 读取模块级 searchCache:后台刷新后即时生效
|
|
194
|
+
(searchCache || []).forEach(function(data) {
|
|
44
195
|
if (!data.content?.trim().length) return;
|
|
45
196
|
if (filter && !data.path.includes(filter)) return;
|
|
46
197
|
|
|
47
|
-
var matches = 0;
|
|
48
198
|
var dataTitle = data.title?.trim() || 'Untitled';
|
|
49
199
|
var dataTitleLowerCase = dataTitle.toLowerCase();
|
|
50
200
|
var dataContent = data.content;
|
|
@@ -52,56 +202,77 @@ var searchFunc = function(path, filter, wrapperId, searchId, contentId) {
|
|
|
52
202
|
var dataUrl = data.path.startsWith('//') ? data.path.slice(1) : data.path;
|
|
53
203
|
dataUrl = dataUrl.replace(/\/?index\.html$/, '/'); // index.html → /
|
|
54
204
|
dataUrl = dataUrl.replace(/\.html$/, '/'); // xxx.html → xxx/
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
var
|
|
58
|
-
var
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
205
|
+
|
|
206
|
+
// 章节划分:anchors 缺失/为空 → 整页一段(回退页面级行为)
|
|
207
|
+
var anchors = Array.isArray(data.anchors) ? data.anchors : [];
|
|
208
|
+
var sections = [];
|
|
209
|
+
if (anchors.length === 0) {
|
|
210
|
+
sections.push({ anchor: null, offset: 0, end: dataContent.length });
|
|
211
|
+
} else {
|
|
212
|
+
for (var s = 0; s <= anchors.length; s++) {
|
|
213
|
+
sections.push({
|
|
214
|
+
anchor: s === 0 ? null : anchors[s - 1],
|
|
215
|
+
offset: s === 0 ? 0 : anchors[s - 1].offset,
|
|
216
|
+
end: s === anchors.length ? dataContent.length : anchors[s].offset
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 标题命中(仅当无任何章节命中时兜底为页面级结果)
|
|
222
|
+
var titleMatches = 0;
|
|
223
|
+
var titleBestKw = null;
|
|
224
|
+
pairs.forEach(function(p) {
|
|
225
|
+
if (dataTitleLowerCase.indexOf(p.kw) >= 0) {
|
|
226
|
+
titleMatches += 1;
|
|
227
|
+
if (titleBestKw == null) titleBestKw = p.original;
|
|
67
228
|
}
|
|
68
229
|
});
|
|
69
230
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
var
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return "<span class=\"search-keyword\">" + keyword + "</span>";
|
|
89
|
-
});
|
|
231
|
+
var hasSectionResult = false;
|
|
232
|
+
|
|
233
|
+
// 逐章节匹配
|
|
234
|
+
sections.forEach(function(section) {
|
|
235
|
+
var secText = dataContent.slice(section.offset, section.end);
|
|
236
|
+
if (!secText.trim().length) return;
|
|
237
|
+
var secLower = dataContentLowerCase.slice(section.offset, section.end);
|
|
238
|
+
var secMatches = 0;
|
|
239
|
+
var secFirst = -1;
|
|
240
|
+
var secBestKw = null;
|
|
241
|
+
pairs.forEach(function(p) {
|
|
242
|
+
var pos = secLower.indexOf(p.kw);
|
|
243
|
+
if (pos < 0) return;
|
|
244
|
+
secMatches += 1;
|
|
245
|
+
if (secFirst < 0 || pos < secFirst) secFirst = pos;
|
|
246
|
+
if (secBestKw == null) secBestKw = p.original;
|
|
247
|
+
});
|
|
248
|
+
if (secMatches === 0) return;
|
|
90
249
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
250
|
+
var href = dataUrl;
|
|
251
|
+
if (section.anchor) {
|
|
252
|
+
href += '?kw=' + encodeURIComponent(secBestKw) + '#' + encodeURIComponent(section.anchor.id);
|
|
253
|
+
} else {
|
|
254
|
+
href += '?kw=' + encodeURIComponent(secBestKw);
|
|
95
255
|
}
|
|
256
|
+
resultList.push({
|
|
257
|
+
rank: secMatches,
|
|
258
|
+
offset: section.offset,
|
|
259
|
+
element: buildResultElement(dataTitle, section.anchor ? section.anchor.text : null, secText, secFirst, pairs, href)
|
|
260
|
+
});
|
|
261
|
+
hasSectionResult = true;
|
|
262
|
+
});
|
|
96
263
|
|
|
97
|
-
|
|
98
|
-
resultList.push({
|
|
264
|
+
if (titleMatches > 0 && !hasSectionResult) {
|
|
265
|
+
resultList.push({
|
|
266
|
+
rank: titleMatches,
|
|
267
|
+
offset: Number.MAX_SAFE_INTEGER,
|
|
268
|
+
element: buildResultElement(dataTitle, null, null, -1, pairs, dataUrl + '?kw=' + encodeURIComponent(titleBestKw))
|
|
269
|
+
});
|
|
99
270
|
}
|
|
100
271
|
});
|
|
101
272
|
|
|
102
273
|
if (resultList.length) {
|
|
103
274
|
resultList.sort(function(a, b) {
|
|
104
|
-
return b.rank - a.rank;
|
|
275
|
+
return b.rank - a.rank || a.offset - b.offset;
|
|
105
276
|
});
|
|
106
277
|
|
|
107
278
|
const ul = document.createElement('ul');
|
|
@@ -114,74 +285,94 @@ var searchFunc = function(path, filter, wrapperId, searchId, contentId) {
|
|
|
114
285
|
$resultContent.appendChild(ul);
|
|
115
286
|
}
|
|
116
287
|
});
|
|
117
|
-
}
|
|
118
288
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const timer = setInterval(() => {
|
|
123
|
-
if (searchCache) {
|
|
124
|
-
clearInterval(timer);
|
|
125
|
-
initSearch(searchCache);
|
|
289
|
+
$input.addEventListener("keydown", function(e) {
|
|
290
|
+
if (e.key == 'Enter') {
|
|
291
|
+
e.preventDefault();
|
|
126
292
|
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// 同步当前状态:已有文字立即执行一次搜索,否则清除加载态
|
|
296
|
+
if ($input.value && $input.value.trim().length > 0) {
|
|
297
|
+
$input.dispatchEvent(new Event('input'));
|
|
298
|
+
} else {
|
|
299
|
+
$wrapper.setAttribute('searching', 'false');
|
|
300
|
+
}
|
|
130
301
|
}
|
|
302
|
+
|
|
303
|
+
// 已有可用数据(新鲜或过期)→ 立即初始化
|
|
304
|
+
if (searchCache) {
|
|
305
|
+
initSearch();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
// 尝试从 localStorage 恢复
|
|
309
|
+
loadCacheIntoMemory();
|
|
310
|
+
if (searchCache) {
|
|
311
|
+
initSearch();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
// 无数据:标记等待,拉取完成后初始化
|
|
315
|
+
$input._searchInitialized = 'pending';
|
|
316
|
+
fetchSearchData(path)
|
|
317
|
+
.then(function() {
|
|
318
|
+
if ($input._searchInitialized === 'pending' && searchCache) {
|
|
319
|
+
initSearch();
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
.catch(function() {
|
|
323
|
+
if ($input._searchInitialized === 'pending') {
|
|
324
|
+
$input._searchInitialized = undefined; // 允许下次聚焦重试
|
|
325
|
+
}
|
|
326
|
+
var $wrapper = document.getElementById(wrapperId);
|
|
327
|
+
if ($wrapper) $wrapper.setAttribute('searching', 'false');
|
|
328
|
+
});
|
|
131
329
|
};
|
|
132
330
|
|
|
331
|
+
// 页面加载预取:仅非懒加载模式(缓存新鲜时不重复请求)
|
|
133
332
|
(function preloadSearchData() {
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
|
|
333
|
+
if (searchLazyLoad) return;
|
|
334
|
+
if (needsFetch()) {
|
|
335
|
+
fetchSearchData(getSearchPath()).catch(function() {});
|
|
137
336
|
}
|
|
138
|
-
|
|
337
|
+
})();
|
|
139
338
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
339
|
+
// 聚焦触发:懒加载模式下首次聚焦搜索框才加载数据
|
|
340
|
+
document.addEventListener("focusin", function(e) {
|
|
341
|
+
var input = e.target;
|
|
342
|
+
if (!input || input.id !== 'search-input') return;
|
|
343
|
+
var path = getSearchPath();
|
|
344
|
+
var filter = input.getAttribute('data-filter') || '';
|
|
345
|
+
|
|
346
|
+
// 已初始化:仅按需后台刷新(刷新失败下次聚焦自动重试)
|
|
347
|
+
if (input._searchInitialized === true) {
|
|
348
|
+
if (needsFetch()) {
|
|
349
|
+
fetchSearchData(path).catch(function() {});
|
|
144
350
|
}
|
|
145
|
-
|
|
146
|
-
console.warn('搜索缓存解析失败', e);
|
|
351
|
+
return;
|
|
147
352
|
}
|
|
148
353
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
localStorage.setItem(searchCacheKey, JSON.stringify(json));
|
|
155
|
-
} catch (e) {
|
|
156
|
-
console.warn('搜索缓存写入失败', e);
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
})();
|
|
354
|
+
// 懒加载:无可用数据时显示加载态(复用 searching 绿色图标状态)
|
|
355
|
+
if (searchLazyLoad && !searchCache && !loadCacheIntoMemory()) {
|
|
356
|
+
var $wrapper = document.getElementById('search-wrapper');
|
|
357
|
+
if ($wrapper) $wrapper.setAttribute('searching', 'true');
|
|
358
|
+
}
|
|
160
359
|
|
|
161
|
-
(
|
|
162
|
-
var inputArea = document.querySelector("input#search-input");
|
|
163
|
-
if (!inputArea) return;
|
|
164
|
-
var resultArea = document.querySelector("div#search-result");
|
|
360
|
+
searchFunc(path, filter, 'search-wrapper', 'search-input', 'search-result');
|
|
165
361
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
path = ctx.root + path;
|
|
172
|
-
const filter = inputArea.getAttribute('data-filter') || '';
|
|
173
|
-
searchFunc(path, filter, 'search-wrapper', 'search-input', 'search-result');
|
|
174
|
-
});
|
|
362
|
+
// 有数据但已过期(或 ttl=0)→ 后台刷新
|
|
363
|
+
if (searchCache && needsFetch()) {
|
|
364
|
+
fetchSearchData(path).catch(function() {});
|
|
365
|
+
}
|
|
366
|
+
});
|
|
175
367
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
});
|
|
368
|
+
// 无结果/有结果状态兜底
|
|
369
|
+
(function() {
|
|
370
|
+
var resultArea = document.querySelector("div#search-result");
|
|
371
|
+
if (!resultArea) return;
|
|
181
372
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
373
|
+
var observer = new MutationObserver(function(mutationsList) {
|
|
374
|
+
var hasResults = resultArea.querySelector(".search-result-list li");
|
|
375
|
+
var wrapper = document.querySelector('.search-wrapper');
|
|
185
376
|
if (wrapper) wrapper.classList.toggle('noresult', !hasResults);
|
|
186
377
|
});
|
|
187
378
|
observer.observe(resultArea, { childList: true, subtree: true });
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
}
|
|
89
89
|
if (item.comments != null) {
|
|
90
90
|
cell += '<a class="item comments last" href="' + item.html_url + '#issuecomment-new" target="_blank" rel="external nofollow noopener noreferrer">';
|
|
91
|
-
cell += '<span
|
|
91
|
+
cell += '<span>' + ctx.icons['weibo:comment'] + ' ' + (item.comments || 0) + '</span>';
|
|
92
92
|
cell += '</a>';
|
|
93
93
|
}
|
|
94
94
|
|
|
@@ -36,16 +36,16 @@
|
|
|
36
36
|
cell += '<div class="flex right">';
|
|
37
37
|
cell += '<div class="item reaction repost">';
|
|
38
38
|
cell += '<a class="item comments last" href="' + item.url + '#issuecomment-new" target="_blank" rel="external nofollow noopener noreferrer">';
|
|
39
|
-
cell += '<span>' + '
|
|
39
|
+
cell += '<span>' + ctx.icons['weibo:repeat'] + ' ' + item.reposts_count + '</span>';
|
|
40
40
|
cell += '</a>';
|
|
41
41
|
cell += '</div>';
|
|
42
42
|
cell += '<a class="item comments last" href="' + item.url + '#issuecomment-new" target="_blank" rel="external nofollow noopener noreferrer">';
|
|
43
|
-
cell += '<span
|
|
43
|
+
cell += '<span>' + ctx.icons['weibo:comment'] + ' '
|
|
44
44
|
+ (item.comments_count || 0) + '</span>';
|
|
45
45
|
cell += '</a>';
|
|
46
46
|
cell += '<div class="item reaction attitudes">';
|
|
47
47
|
cell += '<a class="item comments last" href="' + item.url + '#issuecomment-new" target="_blank" rel="external nofollow noopener noreferrer">';
|
|
48
|
-
cell += '<span>' + '
|
|
48
|
+
cell += '<span>' + ctx.icons['weibo:like'] + ' ' + item.attitudes_count + '</span>';
|
|
49
49
|
cell += '</a>';
|
|
50
50
|
cell += '</div>';
|
|
51
51
|
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const initServices = () => {
|
|
2
|
+
// 用于存储需要清理的资源
|
|
3
|
+
let timers = [];
|
|
4
|
+
|
|
5
|
+
for (let id of Object.keys(ctx.services)) {
|
|
6
|
+
const js = ctx.services[id].js;
|
|
7
|
+
if (id == 'siteinfo') {
|
|
8
|
+
const cardlinks = document.querySelectorAll('a.link-card[cardlink]');
|
|
9
|
+
if (cardlinks?.length > 0) {
|
|
10
|
+
utils.js(js, { defer: true }).then(function () {
|
|
11
|
+
setCardLink(cardlinks);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
} else if (id == 'ghinfo') {
|
|
15
|
+
const els = document.querySelectorAll('.ds-ghinfo');
|
|
16
|
+
if (els.length > 0) {
|
|
17
|
+
utils.js(js, { defer: true });
|
|
18
|
+
}
|
|
19
|
+
} else if (id == 'voice') {
|
|
20
|
+
const voiceAudios = document.querySelectorAll('.voice>audio');
|
|
21
|
+
if (voiceAudios?.length > 0) {
|
|
22
|
+
utils.js(js, { defer: true }).then(function () {
|
|
23
|
+
createVoiceDom(voiceAudios);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
} else if (id == 'video') {
|
|
27
|
+
const videos = document.querySelectorAll('.video>video');
|
|
28
|
+
if (videos?.length > 0) {
|
|
29
|
+
utils.js(js, { defer: true }).then(function () {
|
|
30
|
+
videoEvents(videos);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
} else if (id == 'download-file') {
|
|
34
|
+
const files = document.querySelectorAll('.chat-file');
|
|
35
|
+
if (files?.length > 0) {
|
|
36
|
+
utils.js(js, { defer: true }).then(function () {
|
|
37
|
+
downloadFileEvent(files);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
const els = document.getElementsByClassName(`ds-${id}`);
|
|
42
|
+
if (els?.length > 0) {
|
|
43
|
+
if (id == 'timeline' || id == 'memos' || id == 'marked' || id == 'mdrender') {
|
|
44
|
+
utils.js(deps.marked).then(function () {
|
|
45
|
+
utils.js(js, { defer: true });
|
|
46
|
+
});
|
|
47
|
+
} else {
|
|
48
|
+
utils.js(js, { defer: true });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// search
|
|
55
|
+
if (ctx.search && typeof searchFunc === 'function') {
|
|
56
|
+
const searchCfg = ctx.search.local_search;
|
|
57
|
+
const lazyLoad = searchCfg ? searchCfg.lazy_load !== false : true;
|
|
58
|
+
// 懒加载模式下由 local-search.js 的聚焦事件负责初始化,此处跳过
|
|
59
|
+
if (!lazyLoad) {
|
|
60
|
+
const inputArea = document.querySelector("input#search-input");
|
|
61
|
+
if (inputArea && !inputArea._searchInitialized) {
|
|
62
|
+
const path = ctx.search.path.startsWith('/') ? ctx.root + ctx.search.path.substring(1) : ctx.root + ctx.search.path;
|
|
63
|
+
const filter = inputArea.getAttribute('data-filter') || '';
|
|
64
|
+
searchFunc(path, filter, 'search-wrapper', 'search-input', 'search-result');
|
|
65
|
+
// searchFunc 内部管理 _searchInitialized(pending → true),此处不再覆盖
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// chat iphone time
|
|
71
|
+
let phoneTimes = document.querySelectorAll('.chat .status-bar .time');
|
|
72
|
+
let firstAdjustInterval = null;
|
|
73
|
+
let mainInterval = null;
|
|
74
|
+
|
|
75
|
+
if (phoneTimes.length > 0) {
|
|
76
|
+
NowTime();
|
|
77
|
+
const date = new Date();
|
|
78
|
+
const sec = date.getSeconds();
|
|
79
|
+
firstAdjustInterval = setInterval(firstAdjustTime, 1000 * (60 - sec));
|
|
80
|
+
timers.push(firstAdjustInterval);
|
|
81
|
+
|
|
82
|
+
function firstAdjustTime() {
|
|
83
|
+
NowTime();
|
|
84
|
+
if (firstAdjustInterval) {
|
|
85
|
+
clearInterval(firstAdjustInterval);
|
|
86
|
+
firstAdjustInterval = null;
|
|
87
|
+
}
|
|
88
|
+
mainInterval = setInterval(NowTime, 1000 * 60);
|
|
89
|
+
timers.push(mainInterval);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function NowTime() {
|
|
93
|
+
for (let i = 0; i < phoneTimes.length; ++i) {
|
|
94
|
+
const timeSpan = phoneTimes[i];
|
|
95
|
+
const date = new Date();
|
|
96
|
+
const hour = date.getHours();
|
|
97
|
+
const min = date.getMinutes();
|
|
98
|
+
timeSpan.innerHTML = check(hour) + ":" + check(min);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
function check(val) {
|
|
103
|
+
if (val < 10) {
|
|
104
|
+
return ("0" + val);
|
|
105
|
+
}
|
|
106
|
+
return (val);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// chat quote - 存储事件监听器以便清理
|
|
111
|
+
const quoteClickHandlers = new Map();
|
|
112
|
+
const chat_quote_obverser = new IntersectionObserver((entries, observer) => {
|
|
113
|
+
entries.filter((entry) => { return entry.isIntersecting }).sort((a, b) => a.intersectionRect.y !== b.intersectionRect.y ? a.intersectionRect.y - b.intersectionRect.y : a.intersectionRect.x - b.intersectionRect.x).forEach((entry, index) => {
|
|
114
|
+
observer.unobserve(entry.target);
|
|
115
|
+
setTimeout(() => {
|
|
116
|
+
entry.target.classList.add('quote-blink');
|
|
117
|
+
setTimeout(() => {
|
|
118
|
+
entry.target.classList.remove('quote-blink');
|
|
119
|
+
}, 1000);
|
|
120
|
+
}, Math.max(100, 16) * (index + 1));
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
var chatQuotes = document.querySelectorAll(".chat .talk .quote");
|
|
125
|
+
chatQuotes.forEach((quote) => {
|
|
126
|
+
const handler = function () {
|
|
127
|
+
var chatCellDom = document.getElementById("quote-" + quote.getAttribute("quotedCellTag"));
|
|
128
|
+
if (chatCellDom) {
|
|
129
|
+
var chatDiv = chatCellDom.parentElement;
|
|
130
|
+
var mid = chatDiv.clientHeight / 2;
|
|
131
|
+
var offsetTop = chatCellDom.offsetTop;
|
|
132
|
+
if (offsetTop > mid - chatCellDom.clientHeight / 2) {
|
|
133
|
+
chatDiv.scrollTo({
|
|
134
|
+
top: chatCellDom.offsetTop - mid + chatCellDom.clientHeight / 2,
|
|
135
|
+
behavior: "smooth"
|
|
136
|
+
});
|
|
137
|
+
} else {
|
|
138
|
+
chatDiv.scrollTo({
|
|
139
|
+
top: 0,
|
|
140
|
+
behavior: "smooth"
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
chat_quote_obverser.observe(chatCellDom);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
quote.addEventListener('click', handler);
|
|
147
|
+
quoteClickHandlers.set(quote, handler); // 保存处理器引用
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// 返回清理函数,用于清理定时器和观察器
|
|
151
|
+
return () => {
|
|
152
|
+
// 清理所有定时器(包括可能未完成的 firstAdjustInterval)
|
|
153
|
+
timers.forEach(timer => {
|
|
154
|
+
if (timer) clearInterval(timer);
|
|
155
|
+
});
|
|
156
|
+
timers = [];
|
|
157
|
+
if (firstAdjustInterval) {
|
|
158
|
+
clearInterval(firstAdjustInterval);
|
|
159
|
+
firstAdjustInterval = null;
|
|
160
|
+
}
|
|
161
|
+
if (mainInterval) {
|
|
162
|
+
clearInterval(mainInterval);
|
|
163
|
+
mainInterval = null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 断开观察器
|
|
167
|
+
if (chat_quote_obverser) {
|
|
168
|
+
chat_quote_obverser.disconnect();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 移除所有 click 事件监听器
|
|
172
|
+
quoteClickHandlers.forEach((handler, quote) => {
|
|
173
|
+
quote.removeEventListener('click', handler);
|
|
174
|
+
});
|
|
175
|
+
quoteClickHandlers.clear();
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
utils.initPlugin(initServices, 'services');
|