ERP/frontend/js/components/menu-search.js
2025-12-30 14:35:09 +08:00

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();
}
});
})();