506 lines
19 KiB
JavaScript
506 lines
19 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
||
const searchInput = document.getElementById('search');
|
||
const siteCards = document.querySelectorAll('.site-card');
|
||
const categories = document.querySelectorAll('.category');
|
||
const navItems = document.querySelectorAll('.nav-item');
|
||
const pages = document.querySelectorAll('.page');
|
||
const searchBox = document.querySelector('.search-box');
|
||
const searchResultsPage = document.getElementById('search-results');
|
||
const searchSections = searchResultsPage.querySelectorAll('.search-section');
|
||
|
||
// 移动端元素
|
||
const menuToggle = document.querySelector('.menu-toggle');
|
||
const searchToggle = document.querySelector('.search-toggle');
|
||
const sidebar = document.querySelector('.sidebar');
|
||
const searchContainer = document.querySelector('.search-container');
|
||
const overlay = document.querySelector('.overlay');
|
||
|
||
let isSearchActive = false;
|
||
let currentPageId = 'home';
|
||
let isInitialLoad = true;
|
||
let isSidebarOpen = false;
|
||
let isSearchOpen = false;
|
||
|
||
// 搜索索引,用于提高搜索效率
|
||
let searchIndex = {
|
||
initialized: false,
|
||
items: []
|
||
};
|
||
|
||
// 初始化搜索索引
|
||
function initSearchIndex() {
|
||
if (searchIndex.initialized) return;
|
||
|
||
searchIndex.items = [];
|
||
|
||
try {
|
||
// 为每个页面创建索引
|
||
pages.forEach(page => {
|
||
if (page.id === 'search-results') return;
|
||
|
||
const pageId = page.id;
|
||
|
||
page.querySelectorAll('.site-card').forEach(card => {
|
||
try {
|
||
const title = card.querySelector('h3')?.textContent?.toLowerCase() || '';
|
||
const description = card.querySelector('p')?.textContent?.toLowerCase() || '';
|
||
const url = card.href || card.getAttribute('href') || '#';
|
||
const icon = card.querySelector('i')?.className || '';
|
||
|
||
// 将卡片信息添加到索引中
|
||
searchIndex.items.push({
|
||
pageId,
|
||
title,
|
||
description,
|
||
url,
|
||
icon,
|
||
element: card,
|
||
// 预先计算搜索文本,提高搜索效率
|
||
searchText: (title + ' ' + description).toLowerCase()
|
||
});
|
||
} catch (cardError) {
|
||
console.error('Error processing card:', cardError);
|
||
}
|
||
});
|
||
});
|
||
|
||
searchIndex.initialized = true;
|
||
console.log('Search index initialized with', searchIndex.items.length, 'items');
|
||
} catch (error) {
|
||
console.error('Error initializing search index:', error);
|
||
searchIndex.initialized = true; // 防止反复尝试初始化
|
||
}
|
||
}
|
||
|
||
// 移动端菜单切换
|
||
function toggleSidebar() {
|
||
isSidebarOpen = !isSidebarOpen;
|
||
sidebar.classList.toggle('active', isSidebarOpen);
|
||
overlay.classList.toggle('active', isSidebarOpen);
|
||
if (isSearchOpen) {
|
||
toggleSearch();
|
||
}
|
||
}
|
||
|
||
// 移动端搜索切换
|
||
function toggleSearch() {
|
||
isSearchOpen = !isSearchOpen;
|
||
searchContainer.classList.toggle('active', isSearchOpen);
|
||
overlay.classList.toggle('active', isSearchOpen);
|
||
if (isSearchOpen) {
|
||
searchInput.focus();
|
||
if (isSidebarOpen) {
|
||
toggleSidebar();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 关闭所有移动端面板
|
||
function closeAllPanels() {
|
||
if (isSidebarOpen) {
|
||
toggleSidebar();
|
||
}
|
||
if (isSearchOpen) {
|
||
toggleSearch();
|
||
}
|
||
}
|
||
|
||
// 移动端事件监听
|
||
menuToggle.addEventListener('click', toggleSidebar);
|
||
searchToggle.addEventListener('click', toggleSearch);
|
||
overlay.addEventListener('click', closeAllPanels);
|
||
|
||
// 检查是否是移动设备
|
||
function isMobile() {
|
||
return window.innerWidth <= 768;
|
||
}
|
||
|
||
// 窗口大小改变时处理
|
||
window.addEventListener('resize', () => {
|
||
if (!isMobile()) {
|
||
sidebar.classList.remove('active');
|
||
searchContainer.classList.remove('active');
|
||
overlay.classList.remove('active');
|
||
isSidebarOpen = false;
|
||
isSearchOpen = false;
|
||
}
|
||
});
|
||
|
||
// 页面切换功能
|
||
function showPage(pageId, skipSearchReset = false) {
|
||
if (currentPageId === pageId && !skipSearchReset && !isInitialLoad) return;
|
||
|
||
currentPageId = pageId;
|
||
|
||
// 使用 RAF 确保动画流畅
|
||
requestAnimationFrame(() => {
|
||
pages.forEach(page => {
|
||
const shouldBeActive = page.id === pageId;
|
||
if (shouldBeActive !== page.classList.contains('active')) {
|
||
page.classList.toggle('active', shouldBeActive);
|
||
}
|
||
});
|
||
|
||
// 初始加载完成后设置标志
|
||
if (isInitialLoad) {
|
||
isInitialLoad = false;
|
||
document.body.classList.add('loaded');
|
||
}
|
||
});
|
||
|
||
// 只有在非搜索状态下才重置搜索
|
||
if (!skipSearchReset) {
|
||
searchInput.value = '';
|
||
resetSearch();
|
||
}
|
||
}
|
||
|
||
// 搜索功能
|
||
function performSearch(searchTerm) {
|
||
// 确保搜索索引已初始化
|
||
if (!searchIndex.initialized) {
|
||
initSearchIndex();
|
||
}
|
||
|
||
searchTerm = searchTerm.toLowerCase().trim();
|
||
|
||
// 如果搜索框为空,重置所有内容
|
||
if (!searchTerm) {
|
||
resetSearch();
|
||
return;
|
||
}
|
||
|
||
if (!isSearchActive) {
|
||
isSearchActive = true;
|
||
}
|
||
|
||
try {
|
||
// 使用搜索索引进行搜索
|
||
const searchResults = new Map();
|
||
let hasResults = false;
|
||
|
||
// 使用更高效的搜索算法
|
||
const matchedItems = searchIndex.items.filter(item => {
|
||
return item.searchText.includes(searchTerm);
|
||
});
|
||
|
||
// 按页面分组结果
|
||
matchedItems.forEach(item => {
|
||
if (!searchResults.has(item.pageId)) {
|
||
searchResults.set(item.pageId, []);
|
||
}
|
||
// 克隆元素以避免修改原始DOM
|
||
searchResults.get(item.pageId).push(item.element.cloneNode(true));
|
||
hasResults = true;
|
||
});
|
||
|
||
// 使用requestAnimationFrame批量更新DOM,减少重排重绘
|
||
requestAnimationFrame(() => {
|
||
try {
|
||
// 清空并隐藏所有搜索区域
|
||
searchSections.forEach(section => {
|
||
try {
|
||
const grid = section.querySelector('.sites-grid');
|
||
if (grid) {
|
||
grid.innerHTML = ''; // 使用innerHTML清空,比removeChild更高效
|
||
}
|
||
section.style.display = 'none';
|
||
} catch (sectionError) {
|
||
console.error('Error clearing search section:', sectionError);
|
||
}
|
||
});
|
||
|
||
// 使用DocumentFragment批量添加DOM元素,减少重排
|
||
searchResults.forEach((matches, pageId) => {
|
||
const section = searchResultsPage.querySelector(`[data-section="${pageId}"]`);
|
||
if (section) {
|
||
try {
|
||
const grid = section.querySelector('.sites-grid');
|
||
if (grid) {
|
||
const fragment = document.createDocumentFragment();
|
||
|
||
matches.forEach(card => {
|
||
// 高亮匹配文本
|
||
highlightSearchTerm(card, searchTerm);
|
||
fragment.appendChild(card);
|
||
});
|
||
|
||
grid.appendChild(fragment);
|
||
section.style.display = 'block';
|
||
}
|
||
} catch (gridError) {
|
||
console.error(`Error updating search results for ${pageId}:`, gridError);
|
||
}
|
||
} else {
|
||
console.warn(`Search section for page "${pageId}" not found`);
|
||
}
|
||
});
|
||
|
||
// 更新搜索结果页面状态
|
||
const subtitle = searchResultsPage.querySelector('.subtitle');
|
||
if (subtitle) {
|
||
subtitle.textContent = hasResults
|
||
? `在所有页面中找到 ${matchedItems.length} 个匹配项`
|
||
: '未找到匹配的结果';
|
||
}
|
||
|
||
// 显示搜索结果页面
|
||
if (currentPageId !== 'search-results') {
|
||
currentPageId = 'search-results';
|
||
pages.forEach(page => {
|
||
page.classList.toggle('active', page.id === 'search-results');
|
||
});
|
||
}
|
||
|
||
// 更新搜索状态样式
|
||
searchBox.classList.toggle('has-results', hasResults);
|
||
searchBox.classList.toggle('no-results', !hasResults);
|
||
} catch (uiError) {
|
||
console.error('Error updating search UI:', uiError);
|
||
}
|
||
});
|
||
} catch (searchError) {
|
||
console.error('Error performing search:', searchError);
|
||
}
|
||
}
|
||
|
||
// 高亮搜索匹配文本
|
||
function highlightSearchTerm(card, searchTerm) {
|
||
if (!card || !searchTerm) return;
|
||
|
||
try {
|
||
const title = card.querySelector('h3');
|
||
const description = card.querySelector('p');
|
||
|
||
if (!title || !description) return;
|
||
|
||
// 安全地高亮标题中的匹配文本
|
||
if (title.textContent.toLowerCase().includes(searchTerm)) {
|
||
const titleText = title.textContent;
|
||
const regex = new RegExp(`(${escapeRegExp(searchTerm)})`, 'gi');
|
||
|
||
// 创建安全的DOM结构而不是直接使用innerHTML
|
||
const titleFragment = document.createDocumentFragment();
|
||
let lastIndex = 0;
|
||
let match;
|
||
|
||
// 使用正则表达式查找所有匹配项
|
||
const titleRegex = new RegExp(regex);
|
||
while ((match = titleRegex.exec(titleText)) !== null) {
|
||
// 添加匹配前的文本
|
||
if (match.index > lastIndex) {
|
||
titleFragment.appendChild(document.createTextNode(
|
||
titleText.substring(lastIndex, match.index)
|
||
));
|
||
}
|
||
|
||
// 添加高亮的匹配文本
|
||
const span = document.createElement('span');
|
||
span.className = 'highlight';
|
||
span.textContent = match[0];
|
||
titleFragment.appendChild(span);
|
||
|
||
lastIndex = match.index + match[0].length;
|
||
|
||
// 防止无限循环
|
||
if (titleRegex.lastIndex === 0) break;
|
||
}
|
||
|
||
// 添加剩余文本
|
||
if (lastIndex < titleText.length) {
|
||
titleFragment.appendChild(document.createTextNode(
|
||
titleText.substring(lastIndex)
|
||
));
|
||
}
|
||
|
||
// 清空原标题并添加新内容
|
||
while (title.firstChild) {
|
||
title.removeChild(title.firstChild);
|
||
}
|
||
title.appendChild(titleFragment);
|
||
}
|
||
|
||
// 安全地高亮描述中的匹配文本
|
||
if (description.textContent.toLowerCase().includes(searchTerm)) {
|
||
const descText = description.textContent;
|
||
const regex = new RegExp(`(${escapeRegExp(searchTerm)})`, 'gi');
|
||
|
||
// 创建安全的DOM结构而不是直接使用innerHTML
|
||
const descFragment = document.createDocumentFragment();
|
||
let lastIndex = 0;
|
||
let match;
|
||
|
||
// 使用正则表达式查找所有匹配项
|
||
const descRegex = new RegExp(regex);
|
||
while ((match = descRegex.exec(descText)) !== null) {
|
||
// 添加匹配前的文本
|
||
if (match.index > lastIndex) {
|
||
descFragment.appendChild(document.createTextNode(
|
||
descText.substring(lastIndex, match.index)
|
||
));
|
||
}
|
||
|
||
// 添加高亮的匹配文本
|
||
const span = document.createElement('span');
|
||
span.className = 'highlight';
|
||
span.textContent = match[0];
|
||
descFragment.appendChild(span);
|
||
|
||
lastIndex = match.index + match[0].length;
|
||
|
||
// 防止无限循环
|
||
if (descRegex.lastIndex === 0) break;
|
||
}
|
||
|
||
// 添加剩余文本
|
||
if (lastIndex < descText.length) {
|
||
descFragment.appendChild(document.createTextNode(
|
||
descText.substring(lastIndex)
|
||
));
|
||
}
|
||
|
||
// 清空原描述并添加新内容
|
||
while (description.firstChild) {
|
||
description.removeChild(description.firstChild);
|
||
}
|
||
description.appendChild(descFragment);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error highlighting search term:', error);
|
||
}
|
||
}
|
||
|
||
// 转义正则表达式特殊字符
|
||
function escapeRegExp(string) {
|
||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||
}
|
||
|
||
// 重置搜索状态
|
||
function resetSearch() {
|
||
if (!isSearchActive) return;
|
||
|
||
isSearchActive = false;
|
||
|
||
try {
|
||
requestAnimationFrame(() => {
|
||
try {
|
||
// 清空搜索结果
|
||
searchSections.forEach(section => {
|
||
try {
|
||
const grid = section.querySelector('.sites-grid');
|
||
if (grid) {
|
||
while (grid.firstChild) {
|
||
grid.removeChild(grid.firstChild);
|
||
}
|
||
}
|
||
section.style.display = 'none';
|
||
} catch (sectionError) {
|
||
console.error('Error clearing search section:', sectionError);
|
||
}
|
||
});
|
||
|
||
// 移除搜索状态样式
|
||
searchBox.classList.remove('has-results', 'no-results');
|
||
|
||
// 恢复到当前激活的页面
|
||
const currentActiveNav = document.querySelector('.nav-item.active');
|
||
if (currentActiveNav) {
|
||
const targetPageId = currentActiveNav.getAttribute('data-page');
|
||
|
||
if (targetPageId && currentPageId !== targetPageId) {
|
||
currentPageId = targetPageId;
|
||
pages.forEach(page => {
|
||
page.classList.toggle('active', page.id === targetPageId);
|
||
});
|
||
}
|
||
} else {
|
||
// 如果没有激活的导航项,默认显示首页
|
||
currentPageId = 'home';
|
||
pages.forEach(page => {
|
||
page.classList.toggle('active', page.id === 'home');
|
||
});
|
||
}
|
||
} catch (uiError) {
|
||
console.error('Error resetting search UI:', uiError);
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('Error in resetSearch:', error);
|
||
}
|
||
}
|
||
|
||
// 搜索输入事件(使用防抖)
|
||
const debounce = (fn, delay) => {
|
||
let timer = null;
|
||
return (...args) => {
|
||
if (timer) clearTimeout(timer);
|
||
timer = setTimeout(() => {
|
||
fn.apply(this, args);
|
||
timer = null;
|
||
}, delay);
|
||
};
|
||
};
|
||
|
||
const debouncedSearch = debounce(performSearch, 300);
|
||
|
||
searchInput.addEventListener('input', (e) => {
|
||
debouncedSearch(e.target.value);
|
||
});
|
||
|
||
// 搜索框事件处理
|
||
searchInput.addEventListener('keyup', (e) => {
|
||
if (e.key === 'Escape') {
|
||
searchInput.value = '';
|
||
resetSearch();
|
||
} else if (e.key === 'Enter') {
|
||
performSearch(searchInput.value);
|
||
}
|
||
});
|
||
|
||
// 阻止搜索框的回车默认行为
|
||
searchInput.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
}
|
||
});
|
||
|
||
// 导航项点击效果
|
||
navItems.forEach(item => {
|
||
item.addEventListener('click', (e) => {
|
||
if (item.getAttribute('target') === '_blank') return;
|
||
|
||
e.preventDefault();
|
||
navItems.forEach(nav => {
|
||
nav.classList.toggle('active', nav === item);
|
||
});
|
||
|
||
const pageId = item.getAttribute('data-page');
|
||
if (pageId) {
|
||
showPage(pageId);
|
||
}
|
||
});
|
||
});
|
||
|
||
// 初始化
|
||
window.addEventListener('load', () => {
|
||
// 延迟一帧执行初始化,确保样式已经应用
|
||
requestAnimationFrame(() => {
|
||
// 显示首页
|
||
showPage('home');
|
||
|
||
// 添加载入动画
|
||
categories.forEach((category, index) => {
|
||
setTimeout(() => {
|
||
category.style.opacity = '1';
|
||
}, index * 100);
|
||
});
|
||
|
||
// 初始化搜索索引(使用requestIdleCallback或setTimeout延迟初始化,避免影响页面加载)
|
||
if ('requestIdleCallback' in window) {
|
||
requestIdleCallback(() => initSearchIndex());
|
||
} else {
|
||
setTimeout(initSearchIndex, 1000);
|
||
}
|
||
});
|
||
});
|
||
});
|