(function () { 'use strict'; // ============================================================================ // 配置区域 - 请根据实际情况修改 // ============================================================================ const CONFIG = { apiKey: "VramSTWKUAggeZ5viQw8SlCwXQqmGCmA", // ⚠️ 建议使用 Search-Only API Key server: { host: "typesense.biss.click", port: "443", protocol: "https" }, indexName: "blogs", searchParams: { query_by: "title,content", highlight_full_fields: "title,content", per_page: 8, num_typos: 1, typo_tokens_threshold: 1, prefix: true }, ui: { maxRetries: 10, retryDelay: 100, animationDuration: 300 } }; // ============================================================================ // 状态管理 // ============================================================================ let searchInstance = null; let isInitialized = false; let isSearchOpen = false; let initRetryCount = 0; const MAX_INIT_RETRIES = 30; // 最多重试30次 (3秒) // ============================================================================ // 错误提示函数 // ============================================================================ function showErrorMessage() { const hitsContainer = document.getElementById('hits'); if (!hitsContainer) return; hitsContainer.innerHTML = '
' + '
' + '
搜索服务加载失败
' + '
' + '

依赖库未能正确加载,请检查以下配置:

' + '
    ' + '
  1. 确认已在 _config.butterfly.yml 中正确引入依赖
  2. ' + '
  3. 检查 JS 文件加载顺序(先 instantsearch.js,再 adapter)
  4. ' + '
  5. 尝试更换 CDN 或使用本地文件
  6. ' + '
  7. 打开浏览器控制台查看详细错误信息
  8. ' + '
' + '
' + '
' + '' + '
' + '
'; } // ============================================================================ // 1. 动态插入 HTML 结构 // ============================================================================ const searchHTML = ` `; document.body.insertAdjacentHTML('beforeend', searchHTML); const mask = document.getElementById('typesense-search-mask'); const closeBtn = document.getElementById('close-typesense'); const container = document.getElementById('typesense-search-container'); // ============================================================================ // 搜索控制 // ============================================================================ function openSearch() { if (isSearchOpen) return; isSearchOpen = true; mask.style.display = 'block'; void mask.offsetWidth; mask.classList.add('active'); document.body.style.overflow = 'hidden'; if (!isInitialized) { initTypesense(); } focusSearchInput(); } function closeSearch() { if (!isSearchOpen) return; isSearchOpen = false; mask.classList.remove('active'); setTimeout(function() { mask.style.display = 'none'; document.body.style.overflow = ''; }, CONFIG.ui.animationDuration); } function focusSearchInput(retryCount) { retryCount = retryCount || 0; const input = document.querySelector('.ais-SearchBox-input'); if (input) { input.focus(); input.select(); } else if (retryCount < CONFIG.ui.maxRetries) { setTimeout(function() { focusSearchInput(retryCount + 1); }, CONFIG.ui.retryDelay); } } // ============================================================================ // 事件监听 // ============================================================================ document.addEventListener('click', function(e) { if (e.target.closest('.search-typesense-trigger')) { e.preventDefault(); openSearch(); } }); closeBtn.addEventListener('click', closeSearch); mask.addEventListener('click', function(e) { if (e.target === mask) closeSearch(); }); container.addEventListener('click', function(e) { e.stopPropagation(); }); window.addEventListener('keydown', function(e) { if (e.key === 'Escape' && isSearchOpen) { closeSearch(); } if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); openSearch(); } }); // ============================================================================ // Typesense 初始化(带重试限制) // ============================================================================ function initTypesense() { if (isInitialized || searchInstance) { console.warn('Typesense 搜索已初始化'); return; } var instantsearchLoaded = typeof instantsearch !== 'undefined'; var adapterLoaded = typeof TypesenseInstantSearchAdapter !== 'undefined' || typeof window.TypesenseInstantSearchAdapter !== 'undefined'; console.log('📦 依赖库检查 (' + (initRetryCount + 1) + '/' + MAX_INIT_RETRIES + '):'); console.log(' instantsearch.js:', instantsearchLoaded ? '✅ 已加载' : '❌ 未加载'); console.log(' TypesenseAdapter:', adapterLoaded ? '✅ 已加载' : '❌ 未加载'); if (!instantsearchLoaded || !adapterLoaded) { initRetryCount++; if (initRetryCount >= MAX_INIT_RETRIES) { console.error(''); console.error('❌ Typesense 依赖库加载失败!'); console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.error(''); console.error('🔧 请检查 _config.butterfly.yml 配置:'); console.error(''); console.error('inject:'); console.error(' bottom: # ⚠️ 使用 bottom 而不是 head'); console.error(' - '); console.error(' - '); console.error(' - '); console.error(''); console.error('💡 或在控制台手动检查:'); console.error(' typeof instantsearch'); console.error(' typeof TypesenseInstantSearchAdapter'); console.error(''); console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); showErrorMessage(); return; } console.warn('⏳ 100ms 后重试...'); setTimeout(initTypesense, 100); return; } initRetryCount = 0; console.log('🚀 开始初始化 Typesense...'); try { const typesenseAdapter = new TypesenseInstantSearchAdapter({ server: { apiKey: CONFIG.apiKey, nodes: [{ host: CONFIG.server.host, port: CONFIG.server.port, protocol: CONFIG.server.protocol }], cacheSearchResultsForSeconds: 120 }, additionalSearchParameters: CONFIG.searchParams }); searchInstance = instantsearch({ searchClient: typesenseAdapter.searchClient, indexName: CONFIG.indexName, routing: false }); searchInstance.addWidgets([ instantsearch.widgets.searchBox({ container: '#searchbox', placeholder: '输入关键词寻找故事...', autofocus: true, showReset: true, showSubmit: false, showLoadingIndicator: true }), instantsearch.widgets.stats({ container: '#stats', templates: { text: function(data) { if (!data.query) return ''; return '找到 ' + data.nbHits + ' 条结果 (' + data.processingTimeMS + 'ms)'; } } }), instantsearch.widgets.hits({ container: '#hits', templates: { empty: function(results) { return '
' + '
' + '
找不到与 "' + results.query + '" 相关的内容
' + '
试试其他关键词吧 (´·ω·`)
' + '
'; }, item: function(hit) { // 使用 _highlightResult 获取高亮文本 var titleHighlight = hit._highlightResult && hit._highlightResult.title ? hit._highlightResult.title.value : (hit.title || ''); var contentHighlight = hit._highlightResult && hit._highlightResult.content ? hit._highlightResult.content.value : (hit.content || ''); // 截取内容长度 if (contentHighlight.length > 200) { contentHighlight = contentHighlight.substring(0, 200) + '...'; } return '' + '
' + titleHighlight + '
' + '
' + contentHighlight + '
' + '
'; } } }), instantsearch.widgets.pagination({ container: '#pagination', padding: 2, showFirst: false, showLast: false }) ]); searchInstance.start(); isInitialized = true; console.log('✅ Typesense 初始化成功!'); searchInstance.on('render', function() { if (isSearchOpen) { const input = document.querySelector('.ais-SearchBox-input'); if (input && document.activeElement !== input) { input.focus(); } } }); } catch (error) { console.error('❌ 初始化失败:', error); showErrorMessage(); } } // ============================================================================ // 初始化 // ============================================================================ function init() { console.log('🔍 Typesense 搜索已准备就绪'); console.log('💡 快捷键: Ctrl/Cmd + K'); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } // ============================================================================ // 全局接口 // ============================================================================ window.TypesenseSearch = { open: openSearch, close: closeSearch, isOpen: function() { return isSearchOpen; }, getInstance: function() { return searchInstance; } }; })();