fix: update butterfly theme

This commit is contained in:
2026-02-07 22:19:17 +08:00
parent 4e6a4398ed
commit 6ff595b84a
2 changed files with 1 additions and 1 deletions

View File

@@ -1,545 +0,0 @@
(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 =
'<div class="ts-empty">' +
'<div style="color: #f44336;"><i class="fas fa-exclamation-triangle" style="font-size: 3rem;"></i></div>' +
'<div style="font-size: 1.1rem; font-weight: bold; margin: 15px 0;">搜索服务加载失败</div>' +
'<div style="font-size: 0.9rem; color: #666; line-height: 1.8;">' +
'<p>依赖库未能正确加载,请检查以下配置:</p>' +
'<ol style="text-align: left; max-width: 500px; margin: 15px auto;">' +
'<li>确认已在 <code>_config.butterfly.yml</code> 中正确引入依赖</li>' +
'<li>检查 JS 文件加载顺序(先 instantsearch.js再 adapter</li>' +
'<li>尝试更换 CDN 或使用本地文件</li>' +
'<li>打开浏览器控制台查看详细错误信息</li>' +
'</ol>' +
'</div>' +
'<div style="margin-top: 20px;">' +
'<button onclick="location.reload()" style="padding: 10px 20px; background: #49b1f5; color: white; border: none; border-radius: 5px; cursor: pointer;">重新加载页面</button>' +
'</div>' +
'</div>';
}
// ============================================================================
// 1. 动态插入 HTML 结构
// ============================================================================
const searchHTML = `
<div id="typesense-search-mask" class="ts-mask" style="display:none;">
<div id="typesense-search-container" class="ts-container">
<div class="ts-header">
<span class="ts-title">
<i class="fas fa-search"></i> 本站搜索
</span>
<span id="close-typesense" class="ts-close" aria-label="关闭搜索">&times;</span>
</div>
<div id="searchbox"></div>
<div id="stats" class="ts-stats"></div>
<div id="hits" class="ts-hits"></div>
<div id="pagination" class="ts-pagination"></div>
<div class="ts-footer">
<small>Search powered by <strong>Typesense</strong></small>
</div>
</div>
</div>
<style>
.ts-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 10000;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
opacity: 0;
transition: opacity 300ms ease;
}
.ts-mask.active { opacity: 1; }
.ts-container {
margin: 5% auto;
width: 90%;
max-width: 650px;
background: var(--search-bg, var(--card-bg, #fff));
padding: 25px;
border-radius: 12px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.3);
position: relative;
z-index: 10001;
transform: translateY(-50px);
opacity: 0;
transition: all 300ms ease;
}
.ts-mask.active .ts-container {
transform: translateY(0);
opacity: 1;
}
.ts-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
border-bottom: 2px solid var(--text-highlight-color, #49b1f5);
padding-bottom: 10px;
}
.ts-title {
font-size: 1.2rem;
font-weight: bold;
color: var(--text-highlight-color, #49b1f5);
}
.ts-close {
cursor: pointer;
font-size: 28px;
color: var(--font-color, #333);
line-height: 1;
transition: color 0.2s, transform 0.2s;
}
.ts-close:hover {
color: var(--text-highlight-color, #49b1f5);
transform: scale(1.1);
}
.ais-SearchBox-input {
position: relative;
z-index: 10002;
cursor: text;
padding: 12px 40px 12px 15px !important;
border-radius: 8px !important;
border: 2px solid #eee !important;
width: 100%;
outline: none;
transition: border-color 0.3s, box-shadow 0.3s;
background: var(--card-bg, #fff);
color: var(--font-color, #333);
font-size: 1rem;
}
.ais-SearchBox-input:focus {
border-color: var(--text-highlight-color, #49b1f5) !important;
box-shadow: 0 0 0 3px rgba(73, 177, 245, 0.1);
}
.ts-stats {
margin: 10px 0;
font-size: 0.85rem;
color: var(--font-color, #666);
opacity: 0.8;
}
.ts-hits {
max-height: 55vh;
overflow-y: auto;
margin-top: 15px;
padding-right: 5px;
}
.ts-hits::-webkit-scrollbar { width: 6px; }
.ts-hits::-webkit-scrollbar-track {
background: var(--card-bg, #f1f1f1);
border-radius: 10px;
}
.ts-hits::-webkit-scrollbar-thumb {
background: var(--text-highlight-color, #49b1f5);
border-radius: 10px;
}
.ts-empty {
text-align: center;
padding: 40px 20px;
color: var(--font-color, #999);
}
.ts-empty i {
font-size: 3rem;
margin-bottom: 15px;
opacity: 0.3;
}
.ts-empty code {
background: #f5f5f5;
padding: 2px 6px;
border-radius: 3px;
font-family: monospace;
color: #e91e63;
}
.ts-empty ol {
padding-left: 20px;
}
.ts-empty li {
margin: 8px 0;
}
.ts-result-item {
border-radius: 8px;
transition: all 0.2s ease;
margin-bottom: 10px;
padding: 15px;
border: 1px solid transparent;
text-decoration: none;
display: block;
background: var(--card-bg, #fff);
}
.ts-result-item:hover {
background: var(--text-bg-hover, rgba(73, 177, 245, 0.05));
border-color: var(--text-highlight-color, #49b1f5);
transform: translateX(5px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.ts-result-title {
font-weight: bold;
color: var(--text-highlight-color, #49b1f5);
font-size: 1.1rem;
margin-bottom: 8px;
display: block;
line-height: 1.4;
}
.ts-result-content {
font-size: 0.9rem;
color: var(--font-color, #666);
line-height: 1.6;
opacity: 0.85;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.ts-result-item mark {
background: #ffeb3b;
color: #000;
padding: 2px 4px;
border-radius: 3px;
font-weight: 500;
}
.ts-pagination {
margin-top: 20px;
display: flex;
justify-content: center;
gap: 5px;
}
.ais-Pagination-list {
display: flex;
list-style: none;
padding: 0;
margin: 0;
gap: 5px;
}
.ais-Pagination-link {
display: block;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 5px;
color: var(--font-color, #333);
text-decoration: none;
transition: all 0.2s;
background: var(--card-bg, #fff);
}
.ais-Pagination-link:hover {
background: var(--text-highlight-color, #49b1f5);
color: #fff;
}
.ais-Pagination-item--selected .ais-Pagination-link {
background: var(--text-highlight-color, #49b1f5);
color: #fff;
}
.ts-footer {
text-align: right;
margin-top: 15px;
border-top: 1px solid var(--border-color, #eee);
padding-top: 10px;
}
@media (max-width: 768px) {
.ts-container {
margin: 10px;
width: calc(100% - 20px);
padding: 20px 15px;
}
.ts-hits { max-height: 50vh; }
}
[data-theme="dark"] .ts-mask,
.dark-mode .ts-mask {
background: rgba(0, 0, 0, 0.85);
}
</style>
`;
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(' - <script src="https://cdn.jsdelivr.net/npm/instantsearch.js@4.56.0"></script>');
console.error(' - <script src="https://cdn.jsdelivr.net/npm/typesense-instantsearch-adapter@2.7.0/dist/typesense-instantsearch-adapter.min.js"></script>');
console.error(' - <script src="/js/typesense-search-fixed.js"></script>');
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 '找到 <strong>' + data.nbHits + '</strong> 条结果 (' + data.processingTimeMS + 'ms)';
}
}
}),
instantsearch.widgets.hits({
container: '#hits',
templates: {
empty: function(results) {
return '<div class="ts-empty">' +
'<div><i class="fas fa-search"></i></div>' +
'<div>找不到与 "<strong>' + results.query + '</strong>" 相关的内容</div>' +
'<div style="margin-top: 10px;">试试其他关键词吧 (´·ω·`)</div>' +
'</div>';
},
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 '<a href="' + hit.url + '" class="ts-result-item">' +
'<div class="ts-result-title">' + titleHighlight + '</div>' +
'<div class="ts-result-content">' + contentHighlight + '</div>' +
'</a>';
}
}
}),
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; }
};
})();