hexo-theme-solitude 3.0.21 → 4.0.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.
Files changed (142) hide show
  1. package/.github/FUNDING.yml +3 -3
  2. package/README.md +11 -1
  3. package/README_en-US.md +11 -1
  4. package/_config.yml +16 -7
  5. package/languages/default.yml +56 -36
  6. package/languages/en.yml +90 -69
  7. package/languages/es.yml +94 -73
  8. package/languages/zh-CN.yml +56 -34
  9. package/languages/zh-TW.yml +80 -57
  10. package/layout/archive.pug +60 -14
  11. package/layout/category.pug +13 -11
  12. package/layout/includes/body/gadsense.pug +2 -2
  13. package/layout/includes/console.pug +71 -72
  14. package/layout/includes/footer.pug +24 -9
  15. package/layout/includes/head/config.pug +83 -39
  16. package/layout/includes/head/mode.pug +7 -7
  17. package/layout/includes/head/page_config.pug +20 -11
  18. package/layout/includes/inject/body.pug +14 -36
  19. package/layout/includes/keyboard.pug +3 -3
  20. package/layout/includes/loading.pug +1 -37
  21. package/layout/includes/mixins/articleSort.pug +3 -3
  22. package/layout/includes/mixins/common.pug +8 -4
  23. package/layout/includes/mixins/pace.pug +1 -1
  24. package/layout/includes/mixins/pagination.pug +3 -3
  25. package/layout/includes/nav.pug +4 -3
  26. package/layout/includes/page/about.pug +9 -8
  27. package/layout/includes/page/brevity.pug +2 -2
  28. package/layout/includes/page/links.pug +23 -11
  29. package/layout/includes/page/music.pug +0 -5
  30. package/layout/includes/rightmenu.pug +20 -20
  31. package/layout/includes/sidebar.pug +2 -2
  32. package/layout/includes/widgets/aside/asideInfoCard.pug +2 -2
  33. package/layout/includes/widgets/aside/asideTag.pug +2 -2
  34. package/layout/includes/widgets/home/bbTimeList.pug +3 -3
  35. package/layout/includes/widgets/home/categoryBar.pug +30 -15
  36. package/layout/includes/widgets/home/hometop.pug +113 -8
  37. package/layout/includes/widgets/home/postList.pug +1 -2
  38. package/layout/includes/widgets/home/topGroup.pug +3 -7
  39. package/layout/includes/widgets/nav/group.pug +4 -2
  40. package/layout/includes/widgets/nav/menu.pug +6 -4
  41. package/layout/includes/widgets/nav/right.pug +7 -6
  42. package/layout/includes/widgets/page/about/award.pug +67 -38
  43. package/layout/includes/widgets/page/about/hobbies.pug +12 -3
  44. package/layout/includes/widgets/page/banner.pug +1 -1
  45. package/layout/includes/widgets/page/kit/content.pug +2 -2
  46. package/layout/includes/widgets/page/links/banner.pug +30 -19
  47. package/layout/includes/widgets/page/message/artalk.pug +3 -3
  48. package/layout/includes/widgets/page/message/content.pug +3 -3
  49. package/layout/includes/widgets/page/message/twikoo.pug +3 -3
  50. package/layout/includes/widgets/page/message/valine.pug +4 -4
  51. package/layout/includes/widgets/page/message/waline.pug +3 -3
  52. package/layout/includes/widgets/page/recentcomment/artalk.pug +5 -9
  53. package/layout/includes/widgets/page/recentcomment/twikoo.pug +5 -9
  54. package/layout/includes/widgets/page/recentcomment/valine.pug +6 -10
  55. package/layout/includes/widgets/page/recentcomment/waline.pug +5 -9
  56. package/layout/includes/widgets/post/award.pug +1 -28
  57. package/layout/includes/widgets/post/copyright.pug +10 -10
  58. package/layout/includes/widgets/post/postInfo.pug +10 -11
  59. package/layout/includes/widgets/post/postMeta.pug +3 -2
  60. package/layout/includes/widgets/rightside/hide.pug +5 -5
  61. package/layout/includes/widgets/rightside/show.pug +7 -7
  62. package/layout/includes/widgets/third-party/comments/artalk.pug +9 -9
  63. package/layout/includes/widgets/third-party/comments/giscus.pug +2 -2
  64. package/layout/includes/widgets/third-party/comments/twikoo.pug +5 -5
  65. package/layout/includes/widgets/third-party/comments/valine.pug +6 -6
  66. package/layout/includes/widgets/third-party/comments/waline.pug +6 -6
  67. package/layout/includes/widgets/third-party/hot/twikoo.pug +1 -1
  68. package/layout/includes/widgets/third-party/music.pug +9 -5
  69. package/layout/includes/widgets/third-party/news-comment/artalk.pug +6 -6
  70. package/layout/includes/widgets/third-party/news-comment/twikoo.pug +7 -7
  71. package/layout/includes/widgets/third-party/news-comment/valine.pug +8 -8
  72. package/layout/includes/widgets/third-party/news-comment/waline.pug +7 -7
  73. package/layout/includes/widgets/third-party/pjax.pug +16 -10
  74. package/layout/includes/widgets/third-party/search/algolia-search.pug +2 -2
  75. package/layout/includes/widgets/third-party/search/docsearch.pug +3 -3
  76. package/layout/includes/widgets/third-party/search/index.pug +1 -3
  77. package/layout/includes/widgets/third-party/search/local-search.pug +9 -9
  78. package/layout/index.pug +8 -6
  79. package/package.json +8 -7
  80. package/plugins.yml +22 -21
  81. package/scripts/event/cdn.js +11 -6
  82. package/scripts/event/friend_links.js +30 -0
  83. package/scripts/event/merge_config.js +15 -7
  84. package/scripts/filter/comment.js +2 -2
  85. package/scripts/filter/default.js +1 -1
  86. package/scripts/helper/inject_head_js.js +79 -33
  87. package/scripts/helper/related_post.js +16 -21
  88. package/scripts/tags/chart.js +1 -1
  89. package/scripts/tags/typeit.js +1 -1
  90. package/source/css/_components/capsule.styl +259 -168
  91. package/source/css/_components/rightside.styl +98 -242
  92. package/source/css/_global/animation.styl +1 -10
  93. package/source/css/_global/index.styl +27 -1
  94. package/source/css/_layout/aside.styl +7 -1
  95. package/source/css/_layout/console.styl +145 -165
  96. package/source/css/_layout/header.styl +238 -47
  97. package/source/css/_layout/pagination.styl +176 -132
  98. package/source/css/_layout/rightmenu.styl +11 -2
  99. package/source/css/_layout/sidebar.styl +39 -17
  100. package/source/css/_mode/index.styl +7 -1
  101. package/source/css/_page/_about/about.styl +18 -2
  102. package/source/css/_page/_about/buff.styl +1 -1
  103. package/source/css/_page/_about/contentinfo.styl +2 -1
  104. package/source/css/_page/_about/game.styl +57 -1
  105. package/source/css/_page/_about/like.styl +7 -2
  106. package/source/css/_page/_about/maxim.styl +1 -1
  107. package/source/css/_page/_about/reward.styl +226 -108
  108. package/source/css/_page/_about/skills.styl +5 -3
  109. package/source/css/_page/_home/category-bar.styl +162 -88
  110. package/source/css/_page/_home/home-top.styl +309 -263
  111. package/source/css/_page/_home/home.styl +34 -7
  112. package/source/css/_page/archive.styl +412 -0
  113. package/source/css/_page/category.styl +27 -1
  114. package/source/css/_page/index.styl +2 -0
  115. package/source/css/_page/links.styl +190 -0
  116. package/source/css/_page/other.styl +37 -0
  117. package/source/css/_post/index.styl +2 -1
  118. package/source/css/_post/meta.styl +245 -217
  119. package/source/css/_search/local-search.styl +158 -82
  120. package/source/css/var.styl +2 -2
  121. package/source/js/archive-page.js +191 -0
  122. package/source/js/core/actions.js +48 -0
  123. package/source/js/core/api.js +32 -0
  124. package/source/js/core/config.js +13 -0
  125. package/source/js/core/lifecycle.js +52 -0
  126. package/source/js/core/preloader.js +34 -0
  127. package/source/js/core/resources.js +65 -0
  128. package/source/js/covercolor/api.js +13 -103
  129. package/source/js/covercolor/ave.js +14 -77
  130. package/source/js/covercolor/local.js +22 -116
  131. package/source/js/covercolor/shared.js +102 -0
  132. package/source/js/friend_links.js +502 -0
  133. package/source/js/main.js +793 -177
  134. package/source/js/music.js +24 -14
  135. package/source/js/post_ai.js +13 -2
  136. package/source/js/right_menu.js +44 -24
  137. package/source/js/search/algolia.js +29 -21
  138. package/source/js/search/local.js +231 -266
  139. package/source/js/tw_cn.js +37 -12
  140. package/source/js/utils.js +37 -25
  141. package/layout/includes/widgets/randomlink.pug +0 -43
  142. package/scripts/filter/randomPosts.js +0 -16
@@ -1,3 +1,6 @@
1
+ import { Solitude } from "../core/api.js";
2
+
3
+ (() => {
1
4
  class LocalSearch {
2
5
  constructor() {
3
6
  this.store = [];
@@ -5,61 +8,65 @@ class LocalSearch {
5
8
  this.currentPage = 0;
6
9
  this.resultsPerPage = 10;
7
10
  this.currentResults = [];
11
+ this.lastSearchTime = null;
8
12
  this.isLoading = false;
9
13
  this.searchTimeout = null;
10
-
11
- // DOM 元素缓存
14
+ this.boundElements = new WeakSet();
15
+ this.keyboardBound = false;
16
+ this.pjaxBound = false;
17
+ this.fixSafariHeight = this.fixSafariHeight.bind(this);
18
+ this.handleKeydown = this.handleKeydown.bind(this);
19
+ this.handlePjaxComplete = this.handlePjaxComplete.bind(this);
20
+ this.handleSearchInputDebounced = this.debounce((event) => {
21
+ this.handleSearchInput(event.target.value.trim());
22
+ }, 300);
12
23
  this.elements = this.cacheElements();
13
-
14
- // 初始化
15
24
  this.init();
16
25
  }
17
26
 
18
- /**
19
- * 缓存常用的DOM元素
20
- */
21
27
  cacheElements() {
22
28
  return {
23
- searchMask: document.getElementById("search-mask"),
24
- searchDialog: document.querySelector("#local-search .search-dialog"),
25
- searchInput: document.getElementById("search-input"),
26
- searchResults: document.getElementById("search-results"),
27
- searchPagination: document.getElementById("search-pagination"),
28
- searchTips: document.getElementById("search-tips"),
29
- searchButton: document.querySelector("#search-button > .search"),
30
- closeButton: document.querySelector("#local-search .search-close-button"),
31
- menuSearch: document.getElementById("menu-search")
29
+ searchMask: document.getElementById('search-mask'),
30
+ searchDialog: document.querySelector('#local-search .search-dialog'),
31
+ searchInput: document.getElementById('search-input'),
32
+ searchSuggestions: document.getElementById('search-suggestions'),
33
+ searchResults: document.getElementById('search-results'),
34
+ searchPagination: document.getElementById('search-pagination'),
35
+ searchTips: document.getElementById('search-tips'),
36
+ searchButton: document.querySelector('#search-button > .search'),
37
+ closeButton: document.querySelector('#local-search .search-close-button'),
38
+ menuSearch: document.getElementById('menu-search')
32
39
  };
33
40
  }
34
41
 
35
- /**
36
- * 初始化搜索功能
37
- */
38
42
  async init() {
43
+ this.bindEvents();
44
+ this.bindKeyboardShortcuts();
45
+ this.bindPjaxEvents();
46
+ this.syncSearchState();
47
+ Solitude.openSearch = () => this.openSearch();
48
+
39
49
  try {
40
50
  await this.loadSearchData();
41
- this.bindEvents();
42
- this.bindKeyboardShortcuts();
51
+ const query = this.elements.searchInput?.value.trim();
52
+ if (query) this.handleSearchInput(query);
43
53
  } catch (error) {
44
54
  console.error('Search initialization failed:', error);
45
55
  }
46
56
  }
47
57
 
48
- /**
49
- * 加载搜索数据
50
- */
51
58
  async loadSearchData() {
52
- if (!GLOBAL_CONFIG?.localsearch?.path) {
59
+ if (!Solitude.config?.localsearch?.path) {
53
60
  throw new Error('Search data path not configured');
54
61
  }
55
62
 
56
63
  this.isLoading = true;
57
64
  try {
58
- const response = await fetch(GLOBAL_CONFIG.localsearch.path);
65
+ const response = await fetch(Solitude.config.localsearch.path);
59
66
  if (!response.ok) {
60
67
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
61
68
  }
62
-
69
+
63
70
  const data = await response.text();
64
71
  this.parseSearchData(data);
65
72
  } catch (error) {
@@ -69,25 +76,22 @@ class LocalSearch {
69
76
  }
70
77
  }
71
78
 
72
- /**
73
- * 解析搜索数据
74
- */
75
79
  parseSearchData(xmlData) {
76
80
  try {
77
81
  const parser = new DOMParser();
78
- const xmlDoc = parser.parseFromString(xmlData, "text/xml");
79
- const entries = xmlDoc.getElementsByTagName("entry");
80
-
82
+ const xmlDoc = parser.parseFromString(xmlData, 'text/xml');
83
+ const entries = xmlDoc.getElementsByTagName('entry');
84
+
81
85
  this.store = Array.from(entries).map(entry => {
82
86
  const getTextContent = (tagName) => {
83
87
  const element = entry.getElementsByTagName(tagName)[0];
84
88
  return element ? element.textContent.trim() : '';
85
89
  };
86
-
90
+
87
91
  return {
88
- title: getTextContent("title"),
89
- link: getTextContent("url"),
90
- content: getTextContent("content")
92
+ title: getTextContent('title'),
93
+ link: getTextContent('url'),
94
+ content: getTextContent('content')
91
95
  };
92
96
  }).filter(item => item.title && item.link);
93
97
  } catch (error) {
@@ -95,144 +99,140 @@ class LocalSearch {
95
99
  }
96
100
  }
97
101
 
98
- /**
99
- * 绑定事件监听器
100
- */
102
+ bindOnce(element, eventName, handler) {
103
+ if (!element || this.boundElements.has(element)) return;
104
+ element.addEventListener(eventName, handler);
105
+ this.boundElements.add(element);
106
+ }
107
+
101
108
  bindEvents() {
102
- // 搜索输入事件
103
- this.elements.searchInput?.addEventListener('input', this.debounce((e) => {
104
- this.handleSearchInput(e.target.value.trim());
105
- }, 300));
106
-
107
- // 打开/关闭搜索
108
- this.elements.searchButton?.addEventListener('click', () => this.openSearch());
109
- this.elements.closeButton?.addEventListener('click', () => this.closeSearch());
110
- this.elements.searchMask?.addEventListener('click', () => this.closeSearch());
111
-
112
- // 标签列表点击事件
113
- this.bindTagListEvents();
114
-
115
- // 右键菜单搜索
116
- if (GLOBAL_CONFIG.right_menu && this.elements.menuSearch) {
117
- this.elements.menuSearch.addEventListener('click', () => {
118
- rm.hideRightMenu();
109
+ this.bindOnce(this.elements.searchInput, 'input', this.handleSearchInputDebounced);
110
+ this.bindOnce(this.elements.searchButton, 'click', () => this.openSearch());
111
+ this.bindOnce(this.elements.closeButton, 'click', () => this.closeSearch());
112
+ this.bindOnce(this.elements.searchMask, 'click', () => this.closeSearch());
113
+
114
+ document.querySelectorAll('#local-search .tag-list').forEach(button => {
115
+ this.bindOnce(button, 'click', () => {
116
+ const query = button.dataset.query?.trim();
117
+ if (!query || !this.elements.searchInput) return;
118
+ this.elements.searchInput.value = query;
119
+ this.handleSearchInput(query);
120
+ this.elements.searchInput.focus();
121
+ });
122
+ });
123
+
124
+ if (Solitude.config.right_menu && this.elements.menuSearch) {
125
+ this.bindOnce(this.elements.menuSearch, 'click', () => {
126
+ Solitude.hideRightMenu?.();
119
127
  this.openSearch();
120
- if (window.selectTextNow) {
121
- this.elements.searchInput.value = window.selectTextNow;
122
- this.handleSearchInput(window.selectTextNow);
128
+ if (Solitude.selectedText && this.elements.searchInput) {
129
+ this.elements.searchInput.value = Solitude.selectedText;
130
+ this.handleSearchInput(Solitude.selectedText.trim());
123
131
  }
124
132
  });
125
133
  }
134
+ }
126
135
 
127
- // PJAX 兼容性
128
- window.addEventListener('pjax:complete', () => {
129
- this.elements = this.cacheElements();
130
- this.bindEvents();
131
- });
136
+ bindKeyboardShortcuts() {
137
+ if (this.keyboardBound) return;
138
+ document.addEventListener('keydown', this.handleKeydown);
139
+ this.keyboardBound = true;
132
140
  }
133
141
 
134
- /**
135
- * 绑定标签列表事件
136
- */
137
- bindTagListEvents() {
138
- const tagLists = document.querySelectorAll("#local-search .tag-list");
139
- tagLists.forEach(el => {
140
- el.addEventListener("click", () => this.closeSearch());
141
- });
142
+ bindPjaxEvents() {
143
+ if (this.pjaxBound) return;
144
+ window.addEventListener('pjax:complete', this.handlePjaxComplete);
145
+ this.pjaxBound = true;
142
146
  }
143
147
 
144
- /**
145
- * 绑定键盘快捷键
146
- */
147
- bindKeyboardShortcuts() {
148
- document.addEventListener("keydown", (event) => {
149
- // Ctrl+K 打开搜索
150
- if (event.ctrlKey && event.key === "k") {
151
- event.preventDefault();
152
- this.openSearch();
153
- return;
154
- }
148
+ handleKeydown(event) {
149
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') {
150
+ event.preventDefault();
151
+ this.openSearch();
152
+ return;
153
+ }
155
154
 
156
- // ESC 关闭搜索
157
- if (event.code === "Escape" && this.isSearchOpen()) {
158
- this.closeSearch();
159
- }
160
- });
155
+ if (event.code === 'Escape' && this.isSearchOpen()) {
156
+ this.closeSearch();
157
+ }
158
+ }
159
+
160
+ handlePjaxComplete() {
161
+ this.elements = this.cacheElements();
162
+ this.bindEvents();
163
+ this.syncSearchState();
161
164
  }
162
165
 
163
- /**
164
- * 打开搜索框
165
- */
166
166
  openSearch() {
167
167
  if (!this.elements.searchMask || !this.elements.searchDialog) return;
168
168
 
169
- utils.animateIn(this.elements.searchMask, "to_show 0.5s");
170
- this.elements.searchDialog.style.display = "flex";
171
-
172
- // 延迟聚焦以确保动画完成
173
- setTimeout(() => {
174
- this.elements.searchInput?.focus();
175
- }, 100);
176
-
169
+ Solitude.animateIn(this.elements.searchMask, 'to_show 0.5s');
170
+ this.elements.searchDialog.style.display = 'flex';
171
+ document.documentElement.classList.add('search-open');
172
+ this.syncSearchState();
177
173
  this.fixSafariHeight();
178
- window.addEventListener("resize", this.fixSafariHeight);
179
-
180
- // 暴露到全局作用域以保持兼容性
181
- window.openSearch = () => this.openSearch();
174
+ window.addEventListener('resize', this.fixSafariHeight);
175
+
176
+ setTimeout(() => this.elements.searchInput?.focus(), 100);
182
177
  }
183
178
 
184
- /**
185
- * 关闭搜索框
186
- */
187
179
  closeSearch() {
188
180
  if (!this.elements.searchMask || !this.elements.searchDialog) return;
189
181
 
190
- utils.animateOut(this.elements.searchDialog, "search_close .5s");
191
- utils.animateOut(this.elements.searchMask, "to_hide 0.5s");
192
- window.removeEventListener("resize", this.fixSafariHeight);
193
-
194
- // 清理搜索状态
195
- this.currentQuery = '';
196
- this.currentPage = 0;
182
+ Solitude.animateOut(this.elements.searchDialog, 'search_close .5s');
183
+ Solitude.animateOut(this.elements.searchMask, 'to_hide 0.5s');
184
+ document.documentElement.classList.remove('search-open');
185
+ window.removeEventListener('resize', this.fixSafariHeight);
197
186
  }
198
187
 
199
- /**
200
- * 检查搜索框是否打开
201
- */
202
188
  isSearchOpen() {
203
- return this.elements.searchDialog?.style.display === "flex";
189
+ return this.elements.searchDialog?.style.display === 'flex';
204
190
  }
205
191
 
206
- /**
207
- * 修复Safari高度问题
208
- */
209
- fixSafariHeight = () => {
210
- if (window.innerWidth < 768 && this.elements.searchDialog) {
211
- this.elements.searchDialog.style.setProperty("--search-height", `${window.innerHeight}px`);
192
+ fixSafariHeight() {
193
+ if (!this.elements.searchDialog) return;
194
+ if (window.innerWidth < 768) {
195
+ this.elements.searchDialog.style.setProperty('--search-height', `${window.innerHeight}px`);
196
+ } else {
197
+ this.elements.searchDialog.style.removeProperty('--search-height');
212
198
  }
213
- };
199
+ }
214
200
 
215
- /**
216
- * 处理搜索输入
217
- */
218
- handleSearchInput(query) {
219
- if (this.isLoading) return;
201
+ syncSearchState() {
202
+ const query = this.elements.searchInput?.value.trim() || '';
203
+ this.setQueryState(Boolean(query));
204
+ }
220
205
 
206
+ setQueryState(hasQuery) {
207
+ if (this.elements.searchSuggestions) {
208
+ this.elements.searchSuggestions.hidden = hasQuery;
209
+ }
210
+ if (this.elements.searchResults) {
211
+ this.elements.searchResults.hidden = !hasQuery;
212
+ }
213
+ }
214
+
215
+ handleSearchInput(query) {
221
216
  this.currentQuery = query;
222
217
  this.currentPage = 0;
223
218
 
224
- if (query === '') {
219
+ if (!query) {
225
220
  this.clearSearchResults();
226
221
  return;
227
222
  }
228
223
 
224
+ this.setQueryState(true);
225
+
226
+ if (this.isLoading) {
227
+ this.showStatusMessage(Solitude.config.lang?.search?.loading || 'Searching...', 'search-result-loading');
228
+ return;
229
+ }
230
+
229
231
  try {
230
232
  const startTime = performance.now();
231
233
  this.currentResults = this.performSearch(query);
232
- const endTime = performance.now();
233
- const searchTime = (endTime - startTime).toFixed(2);
234
-
235
- this.renderResults(this.currentResults, this.currentPage, searchTime);
234
+ this.lastSearchTime = (performance.now() - startTime).toFixed(2);
235
+ this.renderResults(this.currentResults, this.currentPage, this.lastSearchTime);
236
236
  this.renderPagination(this.currentResults.length);
237
237
  } catch (error) {
238
238
  console.error('Search error:', error);
@@ -240,57 +240,41 @@ class LocalSearch {
240
240
  }
241
241
  }
242
242
 
243
- /**
244
- * 执行搜索
245
- */
246
243
  performSearch(query) {
247
244
  if (!query || !this.store.length) return [];
248
245
 
249
- const keywords = query.toLowerCase().split(/\s+/).filter(k => k.length > 0);
250
- if (keywords.length === 0) return [];
246
+ const keywords = query.toLowerCase().split(/\s+/).filter(Boolean);
247
+ if (!keywords.length) return [];
251
248
 
252
249
  return this.store.filter(item => {
253
250
  const titleLower = item.title.toLowerCase();
254
251
  const contentLower = item.content.toLowerCase();
255
-
256
- // 所有关键词都必须在标题或内容中找到
257
- return keywords.every(keyword =>
252
+ return keywords.every(keyword =>
258
253
  titleLower.includes(keyword) || contentLower.includes(keyword)
259
254
  );
260
- }).sort((a, b) => {
261
- // 按标题匹配度排序
262
- const aScore = this.calculateRelevanceScore(a, keywords);
263
- const bScore = this.calculateRelevanceScore(b, keywords);
264
- return bScore - aScore;
265
- });
255
+ }).sort((a, b) =>
256
+ this.calculateRelevanceScore(b, keywords) - this.calculateRelevanceScore(a, keywords)
257
+ );
266
258
  }
267
259
 
268
- /**
269
- * 计算相关性评分
270
- */
271
260
  calculateRelevanceScore(item, keywords) {
272
- let score = 0;
273
261
  const titleLower = item.title.toLowerCase();
274
-
275
- keywords.forEach(keyword => {
276
- // 标题完全匹配加分更多
277
- if (titleLower === keyword) score += 10;
278
- else if (titleLower.includes(keyword)) score += 5;
279
- // 内容匹配加分较少
280
- else if (item.content.toLowerCase().includes(keyword)) score += 1;
281
- });
282
-
283
- return score;
262
+ const contentLower = item.content.toLowerCase();
263
+
264
+ return keywords.reduce((score, keyword) => {
265
+ if (titleLower === keyword) return score + 10;
266
+ if (titleLower.includes(keyword)) return score + 5;
267
+ if (contentLower.includes(keyword)) return score + 1;
268
+ return score;
269
+ }, 0);
284
270
  }
285
271
 
286
- /**
287
- * 渲染搜索结果
288
- */
289
- renderResults(results, page, searchTime = null) {
272
+ renderResults(results, page, searchTime = this.lastSearchTime) {
290
273
  if (!this.elements.searchResults || !this.elements.searchTips) return;
291
274
 
292
275
  this.elements.searchResults.innerHTML = '';
293
276
  this.elements.searchTips.innerHTML = '';
277
+ this.setQueryState(true);
294
278
 
295
279
  const start = page * this.resultsPerPage;
296
280
  const end = start + this.resultsPerPage;
@@ -300,158 +284,133 @@ class LocalSearch {
300
284
  return;
301
285
  }
302
286
 
303
- // 创建结果列表
304
287
  const fragment = document.createDocumentFragment();
305
288
  results.slice(start, end).forEach(result => {
306
- const resultElement = this.createResultElement(result);
307
- fragment.appendChild(resultElement);
289
+ fragment.appendChild(this.createResultElement(result));
308
290
  });
309
291
  this.elements.searchResults.appendChild(fragment);
310
-
311
- // 显示结果计数
312
292
  this.showResultCount(results.length, searchTime);
313
293
  }
314
294
 
315
- /**
316
- * 创建单个结果元素
317
- */
318
295
  createResultElement(result) {
319
- const $result = document.createElement("li");
320
- $result.className = "search-result-item";
321
-
322
- const $link = document.createElement("a");
323
- $link.className = "search-result-title";
324
- $link.href = result.link;
325
- $link.innerHTML = this.highlightKeywords(result.title, this.currentQuery);
326
-
327
- $result.appendChild($link);
328
- return $result;
329
- }
330
-
331
- /**
332
- * 高亮关键词
333
- */
296
+ const resultItem = document.createElement('li');
297
+ resultItem.className = 'search-result-item';
298
+
299
+ const link = document.createElement('a');
300
+ link.className = 'search-result-title';
301
+ link.href = result.link;
302
+ link.innerHTML = this.highlightKeywords(result.title, this.currentQuery);
303
+ link.addEventListener('click', () => this.closeSearch());
304
+
305
+ resultItem.appendChild(link);
306
+ return resultItem;
307
+ }
308
+
334
309
  highlightKeywords(text, query) {
335
310
  if (!query) return text;
336
-
337
- const keywords = query.split(/\s+/).filter(k => k.length > 0);
338
- let highlightedText = text;
339
-
340
- keywords.forEach(keyword => {
311
+
312
+ return query.split(/\s+/).filter(Boolean).reduce((highlightedText, keyword) => {
341
313
  const regex = new RegExp(`(${this.escapeRegExp(keyword)})`, 'gi');
342
- highlightedText = highlightedText.replace(regex, '<em>$1</em>');
343
- });
344
-
345
- return highlightedText;
314
+ return highlightedText.replace(regex, '<em>$1</em>');
315
+ }, text);
346
316
  }
347
317
 
348
- /**
349
- * 转义正则表达式特殊字符
350
- */
351
318
  escapeRegExp(string) {
352
319
  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
353
320
  }
354
321
 
355
- /**
356
- * 渲染分页
357
- */
358
322
  renderPagination(totalResults) {
359
323
  if (!this.elements.searchPagination) return;
360
324
 
361
325
  const totalPages = Math.ceil(totalResults / this.resultsPerPage);
362
326
  this.elements.searchPagination.innerHTML = '';
363
-
364
327
  if (totalPages <= 1) return;
365
328
 
366
- const paginationList = document.createElement("ul");
367
- paginationList.className = "pagination-list";
329
+ const paginationList = document.createElement('ul');
330
+ paginationList.className = 'pagination-list';
368
331
 
369
- for (let i = 0; i < totalPages; i++) {
370
- const button = this.createPaginationButton(i + 1, i === this.currentPage);
371
- button.addEventListener('click', () => this.goToPage(i));
372
- paginationList.appendChild(button);
332
+ for (let page = 0; page < totalPages; page++) {
333
+ paginationList.appendChild(this.createPaginationItem(page));
373
334
  }
374
335
 
375
336
  this.elements.searchPagination.appendChild(paginationList);
376
337
  }
377
338
 
378
- /**
379
- * 创建分页按钮
380
- */
381
- createPaginationButton(pageNumber, isActive) {
382
- const button = document.createElement("li");
383
- button.className = "pagination-item";
384
- button.textContent = pageNumber;
385
-
386
- if (isActive) {
339
+ createPaginationItem(page) {
340
+ const item = document.createElement('li');
341
+ item.className = 'pagination-item';
342
+
343
+ const button = document.createElement('button');
344
+ button.type = 'button';
345
+ button.className = 'pagination-button';
346
+ button.textContent = page + 1;
347
+ button.setAttribute('aria-label', `${page + 1}`);
348
+
349
+ if (page === this.currentPage) {
387
350
  button.classList.add('select');
351
+ button.setAttribute('aria-current', 'page');
352
+ } else {
353
+ button.addEventListener('click', () => this.goToPage(page));
388
354
  }
389
-
390
- return button;
355
+
356
+ item.appendChild(button);
357
+ return item;
391
358
  }
392
359
 
393
- /**
394
- * 跳转到指定页面
395
- */
396
360
  goToPage(page) {
397
361
  this.currentPage = page;
398
- this.renderResults(this.currentResults, page);
399
-
400
- // 更新分页按钮状态
401
- document.querySelectorAll(".pagination-item").forEach((btn, index) => {
402
- btn.classList.toggle('select', index === page);
403
- });
362
+ this.renderResults(this.currentResults, page, this.lastSearchTime);
363
+ this.renderPagination(this.currentResults.length);
364
+ if (this.elements.searchResults) this.elements.searchResults.scrollTop = 0;
404
365
  }
405
366
 
406
- /**
407
- * 显示空结果消息
408
- */
409
367
  showEmptyMessage() {
410
- const $empty = document.createElement("span");
411
- $empty.className = "search-result-empty";
412
- $empty.textContent = GLOBAL_CONFIG.lang?.search?.empty?.replace(/\$\{query}/, this.currentQuery) ||
413
- `没有找到与 "${this.currentQuery}" 相关的内容`;
414
- this.elements.searchResults.appendChild($empty);
368
+ const empty = document.createElement('span');
369
+ empty.className = 'search-result-empty';
370
+ empty.textContent = Solitude.config.lang?.search?.empty?.replace(/\$\{query}/g, this.currentQuery) ||
371
+ `没有找到与 "${this.currentQuery}" 相关的内容`;
372
+ this.elements.searchResults.appendChild(empty);
415
373
  }
416
374
 
417
- /**
418
- * 显示结果计数
419
- */
420
375
  showResultCount(count, time) {
421
- const countElement = document.createElement("span");
422
- countElement.className = "search-result-count";
423
- countElement.innerHTML = GLOBAL_CONFIG.lang?.search?.hit?.replace(/\$\{hits}/, count).replace(/\$\{time}/, time);
376
+ const countElement = document.createElement('span');
377
+ countElement.className = 'search-result-count';
378
+ const template = Solitude.config.lang?.search?.hit || 'Found ${hits} results in ${time} ms';
379
+ countElement.innerHTML = template
380
+ .replace(/\$\{hits}/g, count)
381
+ .replace(/\$\{query}/g, count)
382
+ .replace(/\$\{time}/g, time || '0.00');
424
383
  this.elements.searchTips.appendChild(countElement);
425
384
  }
426
385
 
427
- /**
428
- * 显示错误消息
429
- */
430
- showErrorMessage(message) {
386
+ showStatusMessage(message, className) {
431
387
  if (!this.elements.searchResults) return;
432
-
433
388
  this.elements.searchResults.innerHTML = '';
434
- const $error = document.createElement("span");
435
- $error.className = "search-result-error";
436
- $error.textContent = message;
437
- this.elements.searchResults.appendChild($error);
389
+ this.elements.searchTips.innerHTML = '';
390
+ this.elements.searchPagination.innerHTML = '';
391
+ this.setQueryState(true);
392
+
393
+ const status = document.createElement('span');
394
+ status.className = className;
395
+ status.textContent = message;
396
+ this.elements.searchResults.appendChild(status);
397
+ }
398
+
399
+ showErrorMessage(message) {
400
+ this.showStatusMessage(message, 'search-result-error');
438
401
  }
439
402
 
440
- /**
441
- * 清空搜索结果
442
- */
443
403
  clearSearchResults() {
444
404
  if (this.elements.searchResults) this.elements.searchResults.innerHTML = '';
445
405
  if (this.elements.searchPagination) this.elements.searchPagination.innerHTML = '';
446
406
  if (this.elements.searchTips) this.elements.searchTips.innerHTML = '';
447
-
407
+
448
408
  this.currentResults = [];
449
409
  this.currentPage = 0;
410
+ this.lastSearchTime = null;
411
+ this.setQueryState(false);
450
412
  }
451
413
 
452
- /**
453
- * 防抖函数
454
- */
455
414
  debounce(func, wait) {
456
415
  return (...args) => {
457
416
  clearTimeout(this.searchTimeout);
@@ -460,7 +419,13 @@ class LocalSearch {
460
419
  }
461
420
  }
462
421
 
463
- // 页面加载完成后初始化搜索功能
464
- window.addEventListener("load", () => {
465
- window.localSearch = new LocalSearch();
466
- });
422
+ const initializeLocalSearch = () => {
423
+ if (!Solitude.localSearch) Solitude.localSearch = new LocalSearch();
424
+ };
425
+
426
+ if (document.readyState === 'loading') {
427
+ document.addEventListener('DOMContentLoaded', initializeLocalSearch, { once: true });
428
+ } else {
429
+ initializeLocalSearch();
430
+ }
431
+ })();