2026-05-16 04:24:10 +08:00

384 lines
11 KiB
Plaintext

<!-- 搜索按钮 -->
<mdui-button-icon id="search-toggle">
<mdui-icon name="search"></mdui-icon>
</mdui-button-icon>
<!-- 搜索弹窗 -->
<mdui-dialog id="search-dialog" close-on-esc close-on-overlay-click class="search-dialog">
<div class="search-header">
<mdui-text-field class="search-input" placeholder="搜索文章、标签、分类..." id="search-input">
<mdui-icon name="search" slot="icon"></mdui-icon>
</mdui-text-field>
<mdui-button-icon class="close-btn" id="search-close">
<mdui-icon name="close"></mdui-icon>
</mdui-button-icon>
</div>
<div class="search-content">
<div id="search-results" class="search-results">
<div class="search-placeholder">
<mdui-icon name="search"></mdui-icon>
<div>输入关键词开始搜索</div>
</div>
</div>
</div>
<div class="search-footer">
<span class="result-count" id="result-count"></span>
<mdui-button id="search-close-btn">关闭</mdui-button>
</div>
</mdui-dialog>
<style>
.search-dialog::part(overlay) {
backdrop-filter: blur(10px) saturate(180%);
-webkit-backdrop-filter: blur(10px) saturate(180%); /* Safari 支持 */
background: rgba(0, 0, 0, 0.4);
}
.search-dialog {
--mdui-dialog-width: 600px;
--mdui-dialog-height: 70vh;
}
.search-header {
display: flex;
align-items: center;
gap: 12px;
padding: 20px 24px 0;
margin-bottom: 16px;
}
.search-input {
flex: 1;
}
.close-btn {
margin-left: auto;
}
.search-content {
padding: 0 24px;
flex: 1;
overflow: hidden;
}
.search-results {
height: 100%;
overflow-y: auto;
}
.search-results::-webkit-scrollbar {
width: 6px;
}
.search-results::-webkit-scrollbar-track {
background: var(--mdui-color-surface-container);
}
.search-results::-webkit-scrollbar-thumb {
background: var(--mdui-color-outline-variant);
border-radius: 3px;
}
.search-placeholder {
text-align: center;
padding: 60px 20px;
color: var(--mdui-color-on-surface-variant);
}
.search-placeholder mdui-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.search-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
border-top: 1px solid var(--mdui-color-outline-variant);
}
.result-count {
font-size: 0.875rem;
color: var(--mdui-color-on-surface-variant);
}
.search-result-item {
padding: 16px;
border-radius: 12px;
margin-bottom: 8px;
border: 1px solid var(--mdui-color-outline-variant);
cursor: pointer;
transition: all 0.3s ease;
background: var(--mdui-color-surface);
}
.search-result-item:hover {
background: var(--mdui-color-surface-container-hover);
border-color: var(--mdui-color-primary);
transform: translateY(-2px);
}
.search-result-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: var(--mdui-color-on-surface);
margin-bottom: 8px;
}
.search-result-content {
color: var(--mdui-color-on-surface-variant);
font-size: 0.875rem;
line-height: 1.4;
margin-bottom: 8px;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.search-result-meta {
display: flex;
gap: 12px;
font-size: 0.75rem;
color: var(--mdui-color-on-surface-variant);
}
.search-highlight {
background: var(--mdui-color-primary-container);
color: var(--mdui-color-on-primary-container);
padding: 1px 4px;
border-radius: 4px;
font-weight: 600;
}
.no-results, .search-loading {
text-align: center;
padding: 60px 20px;
color: var(--mdui-color-on-surface-variant);
}
@media (max-width: 600px) {
.search-dialog {
--mdui-dialog-width: 95vw;
--mdui-dialog-height: 80vh;
}
.search-header {
padding: 16px 16px 0;
}
.search-content {
padding: 0 16px;
}
.search-footer {
padding: 12px 16px;
}
}
</style>
<script>
class HexoSearch {
constructor() {
this.searchData = null;
this.dialog = document.getElementById('search-dialog');
this.init();
}
async init() {
this.bindEvents();
this.preloadSearchData();
}
async preloadSearchData() {
try {
const response = await fetch('<%- url_for("/search.json") %>');
this.searchData = await response.json();
} catch (error) {
console.error('搜索数据加载失败:', error);
}
}
bindEvents() {
const searchToggle = document.getElementById('search-toggle');
const searchInput = document.getElementById('search-input');
const searchClose = document.getElementById('search-close');
const searchCloseBtn = document.getElementById('search-close-btn');
searchToggle.addEventListener('click', () => {
this.dialog.open = true;
setTimeout(() => searchInput.focus(), 100);
});
const closeDialog = () => {
this.dialog.open = false;
searchInput.value = '';
this.showPlaceholder();
};
searchClose.addEventListener('click', closeDialog);
searchCloseBtn.addEventListener('click', closeDialog);
searchInput.addEventListener('input', this.debounce((e) => {
this.performSearch(e.target.value);
}, 300));
// 键盘快捷键
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
searchToggle.click();
}
});
}
debounce(func, wait) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
performSearch(query) {
if (!query.trim()) {
this.showPlaceholder();
return;
}
if (!this.searchData) {
this.showError('搜索数据加载中...');
return;
}
const results = this.searchPosts(query);
this.displayResults(results, query);
}
searchPosts(query) {
const keywords = query.toLowerCase().split(' ').filter(k => k);
const allItems = [...(this.searchData.posts || []), ...(this.searchData.pages || [])];
return allItems
.map(item => {
let score = 0;
const title = item.title || '';
const content = item.content || '';
keywords.forEach(keyword => {
if (title.toLowerCase().includes(keyword)) score += 10;
if (content.toLowerCase().includes(keyword)) score += 2;
if (item.tags?.some(tag => tag.toLowerCase().includes(keyword))) score += 5;
if (item.categories?.some(cat => cat.toLowerCase().includes(keyword))) score += 3;
});
return { ...item, score };
})
.filter(item => item.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, 20);
}
displayResults(results, query) {
const container = document.getElementById('search-results');
const countElement = document.getElementById('result-count');
if (results.length === 0) {
container.innerHTML = `
<div class="no-results">
<mdui-icon name="search_off" style="font-size: 48px; margin-bottom: 16px;"></mdui-icon>
<div>没有找到包含 "${this.escapeHtml(query)}" 的内容</div>
</div>
`;
countElement.textContent = '';
return;
}
container.innerHTML = results.map(item => this.createResultItem(item, query)).join('');
countElement.textContent = `找到 ${results.length} 个结果`;
container.querySelectorAll('.search-result-item').forEach((item, index) => {
item.addEventListener('click', () => {
window.location.href = results[index].url;
this.dialog.open = false;
});
});
}
createResultItem(item, query) {
const title = item.title || '无标题';
const content = item.content || '';
const highlightedTitle = this.highlightText(title, query);
const excerpt = this.createExcerpt(content, query);
return `
<div class="search-result-item">
<div class="search-result-title">
<mdui-icon name="article" style="font-size: 16px;"></mdui-icon>
${highlightedTitle}
</div>
<div class="search-result-content">${excerpt}</div>
<div class="search-result-meta">
${item.date ? `<span>${item.date}</span>` : ''}
</div>
</div>
`;
}
highlightText(text, query) {
let highlighted = this.escapeHtml(text);
query.toLowerCase().split(' ').filter(k => k).forEach(keyword => {
const regex = new RegExp(`(${this.escapeRegex(keyword)})`, 'gi');
highlighted = highlighted.replace(regex, '<span class="search-highlight">$1</span>');
});
return highlighted;
}
createExcerpt(content, query) {
const keywords = query.toLowerCase().split(' ').filter(k => k);
let excerpt = this.escapeHtml(content.substring(0, 120));
keywords.forEach(keyword => {
const regex = new RegExp(`(${this.escapeRegex(keyword)})`, 'gi');
excerpt = excerpt.replace(regex, '<span class="search-highlight">$1</span>');
});
return excerpt + (content.length > 120 ? '...' : '');
}
escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
escapeHtml(unsafe) {
return unsafe.replace(/[&<>"']/g, m => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;'
}[m]));
}
showPlaceholder() {
document.getElementById('search-results').innerHTML = `
<div class="search-placeholder">
<mdui-icon name="search"></mdui-icon>
<div>输入关键词开始搜索</div>
</div>
`;
document.getElementById('result-count').textContent = '';
}
showError(message) {
document.getElementById('search-results').innerHTML = `
<div class="no-results">${message}</div>
`;
}
}
document.addEventListener('DOMContentLoaded', () => new HexoSearch());
</script>