328 lines
9.5 KiB
JavaScript
328 lines
9.5 KiB
JavaScript
(() => {
|
|
// 菜单搜索功能
|
|
class MenuSearch {
|
|
constructor() {
|
|
this.searchInput = document.querySelector('.sidebar-search-input');
|
|
this.searchIcon = document.querySelector('.search-icon');
|
|
this.menuItems = [];
|
|
this.searchResults = [];
|
|
this.isSearchVisible = false;
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
if (!this.searchInput) return;
|
|
|
|
// 收集所有菜单项
|
|
this.collectMenuItems();
|
|
|
|
// 监听输入事件
|
|
this.searchInput.addEventListener('input', (e) => {
|
|
this.handleSearch(e.target.value);
|
|
});
|
|
|
|
// 监听焦点事件
|
|
this.searchInput.addEventListener('focus', () => {
|
|
if (this.searchInput.value.trim()) {
|
|
this.showSearchResults();
|
|
}
|
|
});
|
|
|
|
// 监听失焦事件(延迟隐藏,以便点击搜索结果)
|
|
this.searchInput.addEventListener('blur', () => {
|
|
setTimeout(() => {
|
|
this.hideSearchResults();
|
|
}, 200);
|
|
});
|
|
|
|
// 监听键盘事件
|
|
this.searchInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
this.navigateFirstResult();
|
|
} else if (e.key === 'Escape') {
|
|
this.clearSearch();
|
|
this.searchInput.blur();
|
|
}
|
|
});
|
|
|
|
// 创建搜索结果容器
|
|
this.createSearchResultsContainer();
|
|
}
|
|
|
|
collectMenuItems() {
|
|
// 收集主导航项
|
|
document.querySelectorAll('.topnav-item[data-route]').forEach(item => {
|
|
const text = item.querySelector('.topnav-text')?.textContent.trim();
|
|
const route = item.dataset.route;
|
|
const href = item.getAttribute('href');
|
|
|
|
if (text && route) {
|
|
this.menuItems.push({
|
|
text,
|
|
route,
|
|
href,
|
|
type: 'menu',
|
|
element: item
|
|
});
|
|
}
|
|
});
|
|
|
|
// 收集下拉菜单项
|
|
document.querySelectorAll('.dropdown-item[data-route]').forEach(item => {
|
|
const text = item.textContent.trim();
|
|
const route = item.dataset.route;
|
|
const href = item.getAttribute('href');
|
|
const parentMenu = item.closest('.topnav-item.has-dropdown')?.querySelector('.topnav-text')?.textContent.trim();
|
|
|
|
if (text && route) {
|
|
this.menuItems.push({
|
|
text,
|
|
route,
|
|
href,
|
|
type: 'submenu',
|
|
parent: parentMenu,
|
|
element: item
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
createSearchResultsContainer() {
|
|
// 创建搜索结果下拉框
|
|
this.resultsContainer = document.createElement('div');
|
|
this.resultsContainer.className = 'search-results-container';
|
|
this.resultsContainer.innerHTML = `
|
|
<div class="search-results-list"></div>
|
|
`;
|
|
|
|
// 插入到搜索框后面
|
|
const searchWrapper = this.searchInput.closest('.sidebar-search');
|
|
searchWrapper.appendChild(this.resultsContainer);
|
|
|
|
// 添加样式
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
.sidebar-search {
|
|
position: relative;
|
|
}
|
|
|
|
.search-results-container {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
background: var(--card-bg, #ffffff);
|
|
border: 1px solid var(--border-color, #e0e0e0);
|
|
border-radius: 0 0 8px 8px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
z-index: 9999;
|
|
display: none;
|
|
}
|
|
|
|
[data-theme="dark"] .search-results-container {
|
|
background: var(--card-bg, #1a1a1a);
|
|
border-color: var(--border-color, #333333);
|
|
}
|
|
|
|
.search-results-container.show {
|
|
display: block;
|
|
}
|
|
|
|
.search-results-list {
|
|
padding: 4px 0;
|
|
}
|
|
|
|
.search-result-item {
|
|
padding: 8px 16px;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 14px;
|
|
color: var(--text-color, #333333);
|
|
background: transparent;
|
|
}
|
|
|
|
.search-result-item:hover {
|
|
background: var(--hover-bg, #f5f5f5);
|
|
}
|
|
|
|
.search-result-item.active {
|
|
background: var(--primary-color, #007bff);
|
|
color: white !important;
|
|
}
|
|
|
|
.search-result-item .result-icon {
|
|
opacity: 0.8;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.search-result-item .result-text {
|
|
flex: 1;
|
|
opacity: 1 !important;
|
|
}
|
|
|
|
.search-result-item .result-parent {
|
|
font-size: 12px;
|
|
opacity: 0.7;
|
|
margin-left: 8px;
|
|
}
|
|
|
|
.search-result-item .result-highlight {
|
|
font-weight: 600;
|
|
color: var(--primary-color, #007bff);
|
|
}
|
|
|
|
.search-result-item.active .result-highlight {
|
|
color: inherit !important;
|
|
}
|
|
|
|
.search-empty {
|
|
padding: 16px;
|
|
text-align: center;
|
|
color: var(--text-muted, #666666);
|
|
font-size: 14px;
|
|
}
|
|
|
|
.search-shortcut {
|
|
padding: 8px 16px;
|
|
border-top: 1px solid var(--border-color, #e0e0e0);
|
|
font-size: 12px;
|
|
color: var(--text-muted, #666666);
|
|
}
|
|
|
|
[data-theme="dark"] .search-result-item {
|
|
color: var(--text-color, #e0e0e0);
|
|
}
|
|
|
|
[data-theme="dark"] .search-result-item:hover {
|
|
background: var(--hover-bg, #2a2a2a);
|
|
}
|
|
|
|
[data-theme="dark"] .search-empty,
|
|
[data-theme="dark"] .search-shortcut {
|
|
color: var(--text-muted, #999999);
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
handleSearch(query) {
|
|
const trimmedQuery = query.trim().toLowerCase();
|
|
|
|
if (!trimmedQuery) {
|
|
this.hideSearchResults();
|
|
return;
|
|
}
|
|
|
|
// 搜索匹配的菜单项
|
|
this.searchResults = this.menuItems.filter(item => {
|
|
return item.text.toLowerCase().includes(trimmedQuery) ||
|
|
(item.parent && item.parent.toLowerCase().includes(trimmedQuery));
|
|
});
|
|
|
|
// 按匹配度和类型排序
|
|
this.searchResults.sort((a, b) => {
|
|
// 优先匹配开头的
|
|
const aStarts = a.text.toLowerCase().startsWith(trimmedQuery);
|
|
const bStarts = b.text.toLowerCase().startsWith(trimmedQuery);
|
|
if (aStarts && !bStarts) return -1;
|
|
if (!aStarts && bStarts) return 1;
|
|
|
|
// 主菜单优先于子菜单
|
|
if (a.type === 'menu' && b.type === 'submenu') return -1;
|
|
if (a.type === 'submenu' && b.type === 'menu') return 1;
|
|
|
|
// 按文本排序
|
|
return a.text.localeCompare(b.text);
|
|
});
|
|
|
|
this.displaySearchResults(trimmedQuery);
|
|
}
|
|
|
|
displaySearchResults(query) {
|
|
const resultsList = this.resultsContainer.querySelector('.search-results-list');
|
|
|
|
if (this.searchResults.length === 0) {
|
|
resultsList.innerHTML = '<div class="search-empty">未找到匹配的菜单项</div>';
|
|
} else {
|
|
resultsList.innerHTML = this.searchResults.map((item, index) => {
|
|
const highlightedText = this.highlightText(item.text, query);
|
|
const parentText = item.parent ? `<span class="result-parent">${item.parent}</span>` : '';
|
|
|
|
return `
|
|
<div class="search-result-item ${index === 0 ? 'active' : ''}" data-index="${index}" data-href="${item.href}">
|
|
<span class="result-icon">${item.type === 'menu' ? '📄' : '📋'}</span>
|
|
<span class="result-text">${highlightedText}</span>
|
|
${parentText}
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
// 添加点击事件
|
|
resultsList.querySelectorAll('.search-result-item').forEach(item => {
|
|
item.addEventListener('click', () => {
|
|
const href = item.dataset.href;
|
|
if (href) {
|
|
window.location.href = href;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// 添加快捷键提示
|
|
const shortcut = document.createElement('div');
|
|
shortcut.className = 'search-shortcut';
|
|
shortcut.innerHTML = '↑↓ 选择 Enter 确认 Esc 取消';
|
|
resultsList.appendChild(shortcut);
|
|
|
|
this.showSearchResults();
|
|
}
|
|
|
|
highlightText(text, query) {
|
|
const regex = new RegExp(`(${query})`, 'gi');
|
|
return text.replace(regex, '<span class="result-highlight">$1</span>');
|
|
}
|
|
|
|
showSearchResults() {
|
|
this.resultsContainer.classList.add('show');
|
|
this.isSearchVisible = true;
|
|
}
|
|
|
|
hideSearchResults() {
|
|
this.resultsContainer.classList.remove('show');
|
|
this.isSearchVisible = false;
|
|
}
|
|
|
|
clearSearch() {
|
|
this.searchInput.value = '';
|
|
this.hideSearchResults();
|
|
}
|
|
|
|
navigateFirstResult() {
|
|
if (this.searchResults.length > 0) {
|
|
const firstResult = this.searchResults[0];
|
|
if (firstResult.href) {
|
|
window.location.href = firstResult.href;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 初始化搜索功能
|
|
const menuSearch = new MenuSearch();
|
|
|
|
// 全局快捷键 Ctrl+K 或 Cmd+K 聚焦搜索框
|
|
document.addEventListener('keydown', (e) => {
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
e.preventDefault();
|
|
menuSearch.searchInput?.focus();
|
|
}
|
|
});
|
|
})();
|