384 lines
11 KiB
Plaintext
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 => ({
|
|
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
}[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> |