Skip to content

Commit f545b6d

Browse files
committed
全文搜索 #50
1 parent fe58e10 commit f545b6d

File tree

5 files changed

+373
-3
lines changed

5 files changed

+373
-3
lines changed

_config.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,12 @@ cdn:
9191
enable: false
9292
# CDN 提供者
9393
provider: jsdelivr # 可选 jsdelivr、unpkg、bootcdn
94+
95+
search:
96+
enable: false # 是否启用搜索
97+
type: algolia # 可选:json 或 algolia
98+
placeholder: 输入关键词搜索...
99+
algolia:
100+
appID: ''
101+
apiKey: ''
102+
indexName: ''

scripts/helper/export-config.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ hexo.extend.helper.register('export_config', function() {
99

1010
const {theme} = this;
1111

12-
const { image } = Object.assign({image: {
12+
const { image, search } = Object.assign({image: {
1313
lazyload_enable: true,
1414
photo_zoom: 'simple-lightbox'
15-
}}, theme.config);
15+
}, search: {
16+
enable: false
17+
}}, theme);
1618

1719
const theme_config = {
18-
image: image
20+
image: image,
21+
search: search,
22+
language: this.config.language,
1923
};
2024

2125
const script = `<script id="hexo-configurations">

source/js/main.js

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,143 @@ window.addEventListener('DOMContentLoaded', () => {
189189
window.addEventListener('hexo-blog-decrypt', () => {
190190
themeBoot();
191191
});
192+
193+
194+
// 搜索弹窗交互与搜索逻辑
195+
(function() {
196+
if (!theme_config || !theme_config.search || !theme_config.search.enable) {
197+
return
198+
}
199+
let language = theme_config.language || 'zh-CN';
200+
201+
// 动态插入搜索弹窗结构到 body
202+
var modalHtml = `
203+
<div id="search-modal" class="search-modal" style="display:none;">
204+
<div class="search-modal-mask"></div>
205+
<div class="search-modal-content">
206+
<span class="search-modal-close" id="search-modal-close">&times;</span>
207+
<div id="search-container">
208+
<input type="text" id="search-input" placeholder="${theme_config.search.placeholder || (language === 'en' ? 'Enter keyword search...' : '输入关键词搜索...')}">
209+
<ul id="search-results"></ul>
210+
</div>
211+
</div>
212+
</div>
213+
`;
214+
var temp = document.createElement('div');
215+
temp.innerHTML = modalHtml;
216+
document.body.appendChild(temp.firstElementChild);
217+
218+
// 创建右下角悬浮按钮
219+
var floatBtn = document.createElement('button');
220+
floatBtn.id = 'search-float-btn';
221+
floatBtn.title = language === 'en' ? 'Search' : '搜索';
222+
var searchIcon = '<svg width="22" height="22" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8" stroke="#409eff" stroke-width="2" fill="none"/><line x1="17" y1="17" x2="21" y2="21" stroke="#409eff" stroke-width="2" stroke-linecap="round"/></svg>';
223+
var closeIcon = '<svg width="22" height="22" viewBox="0 0 24 24"><line x1="6" y1="6" x2="18" y2="18" stroke="#f56c6c" stroke-width="2" stroke-linecap="round"/><line x1="18" y1="6" x2="6" y2="18" stroke="#f56c6c" stroke-width="2" stroke-linecap="round"/></svg>';
224+
floatBtn.innerHTML = searchIcon;
225+
document.body.appendChild(floatBtn);
226+
227+
var modal = document.getElementById('search-modal');
228+
var closeBtn = document.getElementById('search-modal-close');
229+
var mask = document.querySelector('.search-modal-mask');
230+
231+
// 搜索数据缓存
232+
var posts = [];
233+
var algoliaLoaded = false;
234+
var algoliaIndex = null;
235+
236+
// 搜索类型
237+
var searchType = `${theme_config.search.type || "json"}`;
238+
239+
// 打开弹窗时绑定 input 事件
240+
function openModal() {
241+
modal.style.display = 'flex';
242+
floatBtn.innerHTML = closeIcon;
243+
var input = document.getElementById('search-input');
244+
var results = document.getElementById('search-results');
245+
setTimeout(function() { input && input.focus(); }, 100);
246+
// 解绑旧事件,防止多次绑定
247+
input.oninput = null;
248+
if (searchType === 'algolia') {
249+
if (!algoliaLoaded) {
250+
var appId = theme_config.search.algolia.appID;
251+
var apiKey = theme_config.search.algolia.apiKey;
252+
var indexName = theme_config.search.algolia.indexName;
253+
var script = document.createElement('script');
254+
script.src = 'https://cdn.jsdelivr.net/npm/algoliasearch@4/dist/algoliasearch-lite.umd.js';
255+
script.onload = function() {
256+
var client = algoliasearch(appId, apiKey);
257+
algoliaIndex = client.initIndex(indexName);
258+
algoliaLoaded = true;
259+
};
260+
document.body.appendChild(script);
261+
}
262+
input.oninput = function() {
263+
if (!algoliaIndex) return;
264+
var keyword = this.value.trim();
265+
renderResults(results, []);
266+
if (!keyword) return;
267+
algoliaIndex.search(keyword).then(({ hits }) => {
268+
renderResults(results, hits);
269+
});
270+
};
271+
} else {
272+
// 本地 JSON
273+
if (posts.length === 0) {
274+
fetch('/search.json')
275+
.then(response => response.json())
276+
.then(data => { posts = data; });
277+
}
278+
input.oninput = function() {
279+
var keyword = this.value.trim().toLowerCase();
280+
if (!keyword) return renderResults(results, []);
281+
var filtered = posts.filter(post =>
282+
(post.title && post.title.toLowerCase().includes(keyword)) ||
283+
(post.content && post.content.toLowerCase().includes(keyword))
284+
);
285+
renderResults(results, filtered);
286+
};
287+
}
288+
}
289+
290+
function closeModal() {
291+
modal.style.display = 'none';
292+
floatBtn.innerHTML = searchIcon;
293+
var input = document.getElementById('search-input');
294+
var results = document.getElementById('search-results');
295+
if (input) input.value = '';
296+
if (results) results.innerHTML = '';
297+
}
298+
299+
function renderResults(results, list) {
300+
results.innerHTML = '';
301+
if (!list.length) {
302+
let text = theme_config.language === 'en' ? 'No results found' : '未找到结果';
303+
results.innerHTML = '<li style="color:#888;padding:1em;">'+text+'</li>';
304+
return;
305+
}
306+
list.forEach(function(item) {
307+
var summary = '';
308+
if (item.content) {
309+
var clean = item.content.replace(/<[^>]+>/g, '').replace(/\n/g, '');
310+
summary = clean.length > 80 ? clean.slice(0, 80) + '...' : clean;
311+
}
312+
var li = document.createElement('li');
313+
li.innerHTML = `<a href="${item.url || item.permalink}" target="_blank"><div class="search-title">${item.title}</div><div class="search-summary">${summary}</div></a>`;
314+
results.appendChild(li);
315+
});
316+
}
317+
318+
// 悬浮按钮切换弹窗显示/隐藏
319+
floatBtn.onclick = function() {
320+
if (modal.style.display === 'flex') {
321+
closeModal();
322+
} else {
323+
openModal();
324+
}
325+
};
326+
if (closeBtn) closeBtn.onclick = closeModal;
327+
if (mask) mask.onclick = closeModal;
328+
window.addEventListener('keydown', function(e) {
329+
if (e.key === 'Escape') closeModal();
330+
});
331+
})();
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/* 搜索弹窗样式 */
2+
.search-modal {
3+
position: fixed;
4+
z-index: 9999;
5+
left: 0; top: 0; right: 0; bottom: 0;
6+
display: flex;
7+
align-items: flex-start;
8+
justify-content: center;
9+
}
10+
.search-modal-mask {
11+
position: absolute;
12+
left: 0; top: 0; right: 0; bottom: 0;
13+
background: rgba(0,0,0,0.45);
14+
}
15+
.search-modal-content {
16+
position: relative;
17+
background: #fff;
18+
border-radius: 12px;
19+
box-shadow: 0 8px 32px rgba(0,0,0,0.18);
20+
padding: 2em 2em 1em 2em;
21+
min-width: 320px;
22+
max-width: 90vw;
23+
min-height: 120px;
24+
z-index: 1;
25+
animation: popIn .2s;
26+
margin-top: 5vh;
27+
}
28+
@keyframes popIn {
29+
from { transform: scale(0.9); opacity: 0; }
30+
to { transform: scale(1); opacity: 1; }
31+
}
32+
.search-modal-close {
33+
position: absolute;
34+
right: 1em; top: 1em;
35+
font-size: 1.5em;
36+
color: #888;
37+
cursor: pointer;
38+
transition: color .2s;
39+
z-index: 10;
40+
background: none;
41+
border: none;
42+
padding: 0.2em 0.5em;
43+
border-radius: 50%;
44+
}
45+
.search-modal-close:hover {
46+
color: #f56c6c;
47+
background: #f4f4f4;
48+
}
49+
#search-container {
50+
margin-top: 1.5em;
51+
}
52+
#search-input {
53+
width: 100%;
54+
padding: 0.7em 1em;
55+
border: 1px solid #eee;
56+
border-radius: 6px;
57+
font-size: 1.1em;
58+
outline: none;
59+
margin-bottom: 1em;
60+
box-sizing: border-box;
61+
background: #fafbfc;
62+
transition: border .2s;
63+
position: sticky;
64+
top: 0;
65+
z-index: 2;
66+
}
67+
#search-input:focus {
68+
border: 1.5px solid #a0a0a0;
69+
}
70+
#search-results {
71+
list-style: none;
72+
padding: 0;
73+
margin: 0;
74+
max-height: 300px;
75+
overflow-y: auto;
76+
}
77+
#search-results li {
78+
padding: 0.5em 0;
79+
border-bottom: 1px solid #f0f0f0;
80+
}
81+
#search-results li:last-child {
82+
border-bottom: none;
83+
}
84+
.search-title {
85+
font-weight: 600;
86+
color: #222;
87+
font-size: 1.08em;
88+
margin-bottom: 0.2em;
89+
}
90+
.search-summary {
91+
color: #888;
92+
font-size: 0.98em;
93+
margin-top: 0.1em;
94+
line-height: 1.5;
95+
word-break: break-all;
96+
}
97+
#search-results a {
98+
color: #333;
99+
text-decoration: none;
100+
display: block;
101+
padding: 0.2em 0.1em;
102+
transition: background .2s;
103+
}
104+
#search-results a:hover {
105+
color: #409eff;
106+
background: #f4faff;
107+
border-radius: 4px;
108+
}
109+
#search-float-btn {
110+
position: fixed;
111+
right: 2.2em;
112+
bottom: 2.2em;
113+
z-index: 10000;
114+
width: 54px;
115+
height: 54px;
116+
border-radius: 50%;
117+
background: #fff;
118+
box-shadow: 0 4px 16px rgba(64,158,255,0.18);
119+
border: none;
120+
outline: none;
121+
cursor: pointer;
122+
display: flex;
123+
align-items: center;
124+
justify-content: center;
125+
transition: box-shadow .2s, background .2s, transform .2s;
126+
padding: 0;
127+
}
128+
#search-float-btn:hover {
129+
box-shadow: 0 8px 32px rgba(64,158,255,0.28);
130+
background: #f4faff;
131+
transform: scale(1.08);
132+
}
133+
#search-float-btn svg {
134+
display: block;
135+
}
136+
html.dark .search-modal-content {
137+
background: #23272e;
138+
color: #e0e0e0;
139+
box-shadow: 0 8px 32px rgba(0,0,0,0.38);
140+
}
141+
html.dark #search-input {
142+
background: #181a1b;
143+
color: #e0e0e0;
144+
border: 1px solid #333;
145+
}
146+
html.dark #search-input:focus {
147+
border: 1.5px solid #759eff;
148+
}
149+
html.dark #search-results a {
150+
color: #e0e0e0;
151+
}
152+
html.dark #search-results a:hover {
153+
color: #409eff;
154+
background: #23272e;
155+
}
156+
html.dark .search-title {
157+
color: #c9d1d9;
158+
}
159+
html.dark .search-summary {
160+
color: #afafaf;
161+
}
162+
html.dark .search-modal-close {
163+
color: #aaa;
164+
background: none;
165+
}
166+
html.dark .search-modal-close:hover {
167+
color: #f56c6c;
168+
background: #23272e;
169+
}
170+
html.dark #search-float-btn {
171+
background: #23272e;
172+
box-shadow: 0 4px 16px rgba(64,158,255,0.28);
173+
}
174+
html.dark #search-float-btn:hover {
175+
background: #181a1b;
176+
}
177+
@media (max-width: 600px) {
178+
.search-modal-content {
179+
min-width: 100vw;
180+
max-width: 100vw;
181+
min-height: 100vh;
182+
border-radius: 0;
183+
margin: 0;
184+
padding: 1em 0.5em 0.5em 0.5em;
185+
box-sizing: border-box;
186+
}
187+
.search-modal-close {
188+
display: none;
189+
}
190+
#search-container {
191+
margin-top: 0.5em;
192+
}
193+
#search-results {
194+
max-height: 100vh;
195+
}
196+
#search-input {
197+
font-size: 1em;
198+
padding: 0.6em 0.7em;
199+
}
200+
#search-float-btn {
201+
right: 1em;
202+
bottom: 1em;
203+
width: 44px;
204+
height: 44px;
205+
}
206+
}
207+
@media (prefers-color-scheme: dark) {
208+
#search-float-btn {
209+
background: #222;
210+
box-shadow: 0 4px 16px rgba(64,158,255,0.28);
211+
}
212+
#search-float-btn:hover {
213+
background: #333;
214+
}
215+
}

0 commit comments

Comments
 (0)