@@ -189,3 +189,143 @@ window.addEventListener('DOMContentLoaded', () => {
189189window . 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">×</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+ } ) ( ) ;
0 commit comments