整理项目结构
This commit is contained in:
407
src/generator.js
Normal file
407
src/generator.js
Normal file
@@ -0,0 +1,407 @@
|
||||
const fs = require('fs');
|
||||
const yaml = require('js-yaml');
|
||||
const path = require('path');
|
||||
|
||||
// HTML转义函数,防止XSS攻击
|
||||
function escapeHtml(unsafe) {
|
||||
if (unsafe === undefined || unsafe === null) {
|
||||
return '';
|
||||
}
|
||||
return String(unsafe)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
// 读取配置文件
|
||||
function loadConfig() {
|
||||
let config = {};
|
||||
|
||||
// 读取默认配置
|
||||
try {
|
||||
const defaultConfigFile = fs.readFileSync('config.yml', 'utf8');
|
||||
config = yaml.load(defaultConfigFile);
|
||||
} catch (e) {
|
||||
console.error('Error loading default config file:', e);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 尝试读取用户配置并合并
|
||||
try {
|
||||
if (fs.existsSync('config.user.yml')) {
|
||||
const userConfigFile = fs.readFileSync('config.user.yml', 'utf8');
|
||||
const userConfig = yaml.load(userConfigFile);
|
||||
// 深度合并配置,用户配置优先
|
||||
config = mergeConfigs(config, userConfig);
|
||||
console.log('Using user configuration from config.user.yml');
|
||||
} else {
|
||||
console.log('No user configuration found, using default config.yml');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading user config file:', e);
|
||||
console.log('Falling back to default configuration');
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// 深度合并配置对象
|
||||
function mergeConfigs(defaultConfig, userConfig) {
|
||||
if (!userConfig) return defaultConfig;
|
||||
|
||||
const merged = { ...defaultConfig };
|
||||
|
||||
for (const key in userConfig) {
|
||||
if (typeof userConfig[key] === 'object' && !Array.isArray(userConfig[key])) {
|
||||
merged[key] = mergeConfigs(defaultConfig[key] || {}, userConfig[key]);
|
||||
} else {
|
||||
merged[key] = userConfig[key];
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
// 生成导航菜单
|
||||
function generateNavigation(navigation) {
|
||||
return navigation.map(nav => `
|
||||
<a href="#" class="nav-item${nav.active ? ' active' : ''}" data-page="${escapeHtml(nav.id)}">
|
||||
<div class="icon-container">
|
||||
<i class="${escapeHtml(nav.icon)}"></i>
|
||||
</div>
|
||||
<span class="nav-text">${escapeHtml(nav.name)}</span>
|
||||
</a>`).join('\n');
|
||||
}
|
||||
|
||||
// 生成网站卡片HTML
|
||||
function generateSiteCards(sites) {
|
||||
return sites.map(site => `
|
||||
<a href="${escapeHtml(site.url)}" class="site-card">
|
||||
<i class="${escapeHtml(site.icon)}"></i>
|
||||
<h3>${escapeHtml(site.name)}</h3>
|
||||
<p>${escapeHtml(site.description)}</p>
|
||||
</a>`).join('\n');
|
||||
}
|
||||
|
||||
// 生成分类HTML
|
||||
function generateCategories(categories) {
|
||||
return categories.map(category => `
|
||||
<section class="category">
|
||||
<h2><i class="${escapeHtml(category.icon)}"></i> ${escapeHtml(category.name)}</h2>
|
||||
<div class="sites-grid">
|
||||
${generateSiteCards(category.sites)}
|
||||
</div>
|
||||
</section>`).join('\n');
|
||||
}
|
||||
|
||||
// 生成社交链接HTML
|
||||
function generateSocialLinks(social) {
|
||||
return social.map(link => `
|
||||
<a href="${escapeHtml(link.url)}" class="nav-item" target="_blank">
|
||||
<div class="icon-container">
|
||||
<i class="${escapeHtml(link.icon)}"></i>
|
||||
</div>
|
||||
<span class="nav-text">${escapeHtml(link.name)}</span>
|
||||
<i class="fas fa-external-link-alt external-icon"></i>
|
||||
</a>`).join('\n');
|
||||
}
|
||||
|
||||
// 生成欢迎区域和首页内容
|
||||
function generateHomeContent(config) {
|
||||
return `
|
||||
<div class="welcome-section">
|
||||
<h2>${escapeHtml(config.profile.title)}</h2>
|
||||
<h3>${escapeHtml(config.profile.subtitle)}</h3>
|
||||
<p class="subtitle">${escapeHtml(config.profile.description)}</p>
|
||||
</div>
|
||||
${generateCategories(config.categories)}`;
|
||||
}
|
||||
|
||||
// 生成页面内容
|
||||
function generatePageContent(pageId, data) {
|
||||
return `
|
||||
<div class="welcome-section">
|
||||
<h2>${escapeHtml(data.title)}</h2>
|
||||
<p class="subtitle">${escapeHtml(data.subtitle)}</p>
|
||||
</div>
|
||||
${generateCategories(data.categories)}`;
|
||||
}
|
||||
|
||||
// 生成搜索结果页面
|
||||
function generateSearchResultsPage(config) {
|
||||
// 获取所有导航页面ID
|
||||
const pageIds = config.navigation.map(nav => nav.id);
|
||||
|
||||
// 生成所有页面的搜索结果区域
|
||||
const searchSections = pageIds.map(pageId => {
|
||||
// 根据页面ID获取对应的图标和名称
|
||||
const navItem = config.navigation.find(nav => nav.id === pageId);
|
||||
const icon = navItem ? navItem.icon : 'fas fa-file';
|
||||
const name = navItem ? navItem.name : pageId;
|
||||
|
||||
return `
|
||||
<section class="category search-section" data-section="${escapeHtml(pageId)}" style="display: none;">
|
||||
<h2><i class="${escapeHtml(icon)}"></i> ${escapeHtml(name)}匹配项</h2>
|
||||
<div class="sites-grid"></div>
|
||||
</section>`;
|
||||
}).join('\n');
|
||||
|
||||
return `
|
||||
<!-- 搜索结果页 -->
|
||||
<div class="page" id="search-results">
|
||||
<div class="welcome-section">
|
||||
<h2>搜索结果</h2>
|
||||
<p class="subtitle">在所有页面中找到的匹配项</p>
|
||||
</div>
|
||||
${searchSections}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// 生成Google Fonts链接
|
||||
function generateGoogleFontsLink(config) {
|
||||
const fonts = config.fonts;
|
||||
const googleFonts = [];
|
||||
|
||||
// 收集需要加载的Google字体
|
||||
Object.values(fonts).forEach(font => {
|
||||
if (font.source === 'google') {
|
||||
const fontName = font.family.replace(/["']/g, '');
|
||||
const fontWeight = font.weight || 400;
|
||||
googleFonts.push(`family=${fontName}:wght@${fontWeight}`);
|
||||
}
|
||||
});
|
||||
|
||||
return googleFonts.length > 0
|
||||
? `<link href="https://fonts.googleapis.com/css2?${googleFonts.join('&')}&display=swap" rel="stylesheet">`
|
||||
: '';
|
||||
}
|
||||
|
||||
// 生成字体CSS变量
|
||||
function generateFontVariables(config) {
|
||||
const fonts = config.fonts;
|
||||
let css = ':root {\n';
|
||||
|
||||
Object.entries(fonts).forEach(([key, font]) => {
|
||||
css += ` --font-${key}: ${font.family};\n`;
|
||||
if (font.weight) {
|
||||
css += ` --font-weight-${key}: ${font.weight};\n`;
|
||||
}
|
||||
});
|
||||
|
||||
css += '}';
|
||||
return css;
|
||||
}
|
||||
|
||||
// 生成完整的HTML
|
||||
function generateHTML(config) {
|
||||
const googleFontsLink = generateGoogleFontsLink(config);
|
||||
const fontVariables = generateFontVariables(config);
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${escapeHtml(config.site.title)}</title>
|
||||
<link rel="icon" href="./favicon.ico" type="image/x-icon">
|
||||
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon">
|
||||
${googleFontsLink}
|
||||
<style>
|
||||
${fontVariables}
|
||||
</style>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
</head>
|
||||
<body class="loading">
|
||||
<div class="layout">
|
||||
<!-- 移动端按钮 -->
|
||||
<div class="mobile-buttons">
|
||||
<button class="menu-toggle" aria-label="切换菜单">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<button class="search-toggle" aria-label="切换搜索">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 遮罩层 -->
|
||||
<div class="overlay"></div>
|
||||
|
||||
<!-- 左侧导航 -->
|
||||
<nav class="sidebar">
|
||||
<div class="logo">
|
||||
<h1>导航站</h1>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
${generateNavigation(config.navigation)}
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<div class="section-title">
|
||||
<i class="fas fa-link"></i>
|
||||
</div>
|
||||
${generateSocialLinks(config.social)}
|
||||
</div>
|
||||
|
||||
<div class="copyright">
|
||||
<p>© ${currentYear} <a href="https://github.com/rbetree/menav" target="_blank">MeNav</a></p>
|
||||
<p>by <a href="https://github.com/rbetree" target="_blank">rbetree</a></p>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<main class="content">
|
||||
<!-- 搜索框容器 -->
|
||||
<div class="search-container">
|
||||
<div class="search-box">
|
||||
<input type="text" id="search" placeholder="搜索...">
|
||||
<i class="fas fa-search"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 首页 -->
|
||||
<div class="page active" id="home">
|
||||
<div class="welcome-section">
|
||||
<h2>${escapeHtml(config.profile.title)}</h2>
|
||||
<h3>${escapeHtml(config.profile.subtitle)}</h3>
|
||||
<p class="subtitle">${escapeHtml(config.profile.description)}</p>
|
||||
</div>
|
||||
${generateCategories(config.categories)}
|
||||
</div>
|
||||
|
||||
<!-- 项目页 -->
|
||||
<div class="page" id="projects">
|
||||
${generatePageContent('projects', config.projects)}
|
||||
</div>
|
||||
|
||||
<!-- 文章页 -->
|
||||
<div class="page" id="articles">
|
||||
${generatePageContent('articles', config.articles)}
|
||||
</div>
|
||||
|
||||
<!-- 朋友页 -->
|
||||
<div class="page" id="friends">
|
||||
${generatePageContent('friends', config.friends)}
|
||||
</div>
|
||||
${generateSearchResultsPage(config)}
|
||||
</main>
|
||||
</div>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
// 复制静态文件
|
||||
function copyStaticFiles(config) {
|
||||
// 确保dist目录存在
|
||||
if (!fs.existsSync('dist')) {
|
||||
fs.mkdirSync('dist', { recursive: true });
|
||||
}
|
||||
|
||||
// 复制CSS文件
|
||||
try {
|
||||
fs.copyFileSync('assets/style.css', 'dist/style.css');
|
||||
console.log('Copied style.css to dist/');
|
||||
} catch (e) {
|
||||
console.error('Error copying style.css:', e);
|
||||
}
|
||||
|
||||
// 复制JavaScript文件
|
||||
try {
|
||||
fs.copyFileSync('src/script.js', 'dist/script.js');
|
||||
console.log('Copied script.js to dist/');
|
||||
} catch (e) {
|
||||
console.error('Error copying script.js:', e);
|
||||
}
|
||||
|
||||
// 如果配置了favicon,确保文件存在并复制
|
||||
if (config.site.favicon) {
|
||||
try {
|
||||
if (fs.existsSync(`assets/${config.site.favicon}`)) {
|
||||
fs.copyFileSync(`assets/${config.site.favicon}`, `dist/${path.basename(config.site.favicon)}`);
|
||||
console.log(`Copied favicon: ${config.site.favicon} to dist/`);
|
||||
} else if (fs.existsSync(config.site.favicon)) {
|
||||
fs.copyFileSync(config.site.favicon, `dist/${path.basename(config.site.favicon)}`);
|
||||
console.log(`Copied favicon: ${config.site.favicon} to dist/`);
|
||||
} else {
|
||||
console.warn(`Warning: Favicon file not found: ${config.site.favicon}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error copying favicon:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理模板文件,替换占位符
|
||||
function processTemplate(template, config) {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const googleFontsLink = generateGoogleFontsLink(config);
|
||||
const fontVariables = generateFontVariables(config);
|
||||
|
||||
// 创建替换映射
|
||||
const replacements = {
|
||||
'{{SITE_TITLE}}': escapeHtml(config.site.title),
|
||||
'{{SITE_LOGO_TEXT}}': escapeHtml(config.site.logo_text || '导航站'), // 从配置中获取,如果不存在则使用默认值
|
||||
'{{GOOGLE_FONTS}}': googleFontsLink,
|
||||
'{{{FONT_VARIABLES}}}': fontVariables,
|
||||
'{{NAVIGATION}}': generateNavigation(config.navigation),
|
||||
'{{SOCIAL_LINKS}}': generateSocialLinks(config.social),
|
||||
'{{CURRENT_YEAR}}': currentYear,
|
||||
'{{HOME_CONTENT}}': generateHomeContent(config),
|
||||
'{{PROJECTS_CONTENT}}': generatePageContent('projects', config.projects),
|
||||
'{{ARTICLES_CONTENT}}': generatePageContent('articles', config.articles),
|
||||
'{{FRIENDS_CONTENT}}': generatePageContent('friends', config.friends),
|
||||
'{{SEARCH_RESULTS}}': generateSearchResultsPage(config)
|
||||
};
|
||||
|
||||
// 执行替换
|
||||
let processedTemplate = template;
|
||||
for (const [placeholder, value] of Object.entries(replacements)) {
|
||||
processedTemplate = processedTemplate.replace(placeholder, value);
|
||||
}
|
||||
|
||||
return processedTemplate;
|
||||
}
|
||||
|
||||
// 主函数
|
||||
function main() {
|
||||
const config = loadConfig();
|
||||
|
||||
try {
|
||||
// 确保dist目录存在
|
||||
if (!fs.existsSync('dist')) {
|
||||
fs.mkdirSync('dist', { recursive: true });
|
||||
}
|
||||
|
||||
// 读取模板文件
|
||||
const templatePath = 'templates/index.html';
|
||||
let htmlContent = '';
|
||||
|
||||
if (fs.existsSync(templatePath)) {
|
||||
// 读取模板并处理
|
||||
const template = fs.readFileSync(templatePath, 'utf8');
|
||||
htmlContent = processTemplate(template, config);
|
||||
console.log(`Using template from ${templatePath} and injecting content`);
|
||||
} else {
|
||||
// 如果没有模板文件,使用生成的HTML
|
||||
htmlContent = generateHTML(config);
|
||||
console.log('No template file found, using generated HTML');
|
||||
}
|
||||
|
||||
// 生成HTML
|
||||
fs.writeFileSync('dist/index.html', htmlContent);
|
||||
console.log('Successfully generated dist/index.html');
|
||||
|
||||
// 复制静态文件
|
||||
copyStaticFiles(config);
|
||||
} catch (e) {
|
||||
console.error('Error in main function:', e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
506
src/script.js
Normal file
506
src/script.js
Normal file
@@ -0,0 +1,506 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user