(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 =
'
' +
'
' +
'
搜索服务加载失败
' +
'
' +
'
依赖库未能正确加载,请检查以下配置:
' +
'
' +
'- 确认已在
_config.butterfly.yml 中正确引入依赖 ' +
'- 检查 JS 文件加载顺序(先 instantsearch.js,再 adapter)
' +
'- 尝试更换 CDN 或使用本地文件
' +
'- 打开浏览器控制台查看详细错误信息
' +
'
' +
'
' +
'
' +
'' +
'
' +
'
';
}
// ============================================================================
// 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; }
};
})();