优化
This commit is contained in:
92
generator.js
92
generator.js
@@ -2,6 +2,19 @@ const fs = require('fs');
|
|||||||
const yaml = require('js-yaml');
|
const yaml = require('js-yaml');
|
||||||
const path = require('path');
|
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() {
|
function loadConfig() {
|
||||||
let config = {};
|
let config = {};
|
||||||
@@ -54,21 +67,21 @@ function mergeConfigs(defaultConfig, userConfig) {
|
|||||||
// 生成导航菜单
|
// 生成导航菜单
|
||||||
function generateNavigation(navigation) {
|
function generateNavigation(navigation) {
|
||||||
return navigation.map(nav => `
|
return navigation.map(nav => `
|
||||||
<a href="#" class="nav-item${nav.active ? ' active' : ''}" data-page="${nav.id}">
|
<a href="#" class="nav-item${nav.active ? ' active' : ''}" data-page="${escapeHtml(nav.id)}">
|
||||||
<div class="icon-container">
|
<div class="icon-container">
|
||||||
<i class="${nav.icon}"></i>
|
<i class="${escapeHtml(nav.icon)}"></i>
|
||||||
</div>
|
</div>
|
||||||
<span class="nav-text">${nav.name}</span>
|
<span class="nav-text">${escapeHtml(nav.name)}</span>
|
||||||
</a>`).join('\n');
|
</a>`).join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成网站卡片HTML
|
// 生成网站卡片HTML
|
||||||
function generateSiteCards(sites) {
|
function generateSiteCards(sites) {
|
||||||
return sites.map(site => `
|
return sites.map(site => `
|
||||||
<a href="${site.url}" class="site-card">
|
<a href="${escapeHtml(site.url)}" class="site-card">
|
||||||
<i class="${site.icon}"></i>
|
<i class="${escapeHtml(site.icon)}"></i>
|
||||||
<h3>${site.name}</h3>
|
<h3>${escapeHtml(site.name)}</h3>
|
||||||
<p>${site.description}</p>
|
<p>${escapeHtml(site.description)}</p>
|
||||||
</a>`).join('\n');
|
</a>`).join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +89,7 @@ function generateSiteCards(sites) {
|
|||||||
function generateCategories(categories) {
|
function generateCategories(categories) {
|
||||||
return categories.map(category => `
|
return categories.map(category => `
|
||||||
<section class="category">
|
<section class="category">
|
||||||
<h2><i class="${category.icon}"></i> ${category.name}</h2>
|
<h2><i class="${escapeHtml(category.icon)}"></i> ${escapeHtml(category.name)}</h2>
|
||||||
<div class="sites-grid">
|
<div class="sites-grid">
|
||||||
${generateSiteCards(category.sites)}
|
${generateSiteCards(category.sites)}
|
||||||
</div>
|
</div>
|
||||||
@@ -86,11 +99,11 @@ function generateCategories(categories) {
|
|||||||
// 生成社交链接HTML
|
// 生成社交链接HTML
|
||||||
function generateSocialLinks(social) {
|
function generateSocialLinks(social) {
|
||||||
return social.map(link => `
|
return social.map(link => `
|
||||||
<a href="${link.url}" class="nav-item" target="_blank">
|
<a href="${escapeHtml(link.url)}" class="nav-item" target="_blank">
|
||||||
<div class="icon-container">
|
<div class="icon-container">
|
||||||
<i class="${link.icon}"></i>
|
<i class="${escapeHtml(link.icon)}"></i>
|
||||||
</div>
|
</div>
|
||||||
<span class="nav-text">${link.name}</span>
|
<span class="nav-text">${escapeHtml(link.name)}</span>
|
||||||
<i class="fas fa-external-link-alt external-icon"></i>
|
<i class="fas fa-external-link-alt external-icon"></i>
|
||||||
</a>`).join('\n');
|
</a>`).join('\n');
|
||||||
}
|
}
|
||||||
@@ -99,14 +112,31 @@ function generateSocialLinks(social) {
|
|||||||
function generatePageContent(pageId, data) {
|
function generatePageContent(pageId, data) {
|
||||||
return `
|
return `
|
||||||
<div class="welcome-section">
|
<div class="welcome-section">
|
||||||
<h2>${data.title}</h2>
|
<h2>${escapeHtml(data.title)}</h2>
|
||||||
<p class="subtitle">${data.subtitle}</p>
|
<p class="subtitle">${escapeHtml(data.subtitle)}</p>
|
||||||
</div>
|
</div>
|
||||||
${generateCategories(data.categories)}`;
|
${generateCategories(data.categories)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成搜索结果页面
|
// 生成搜索结果页面
|
||||||
function generateSearchResultsPage() {
|
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 `
|
return `
|
||||||
<!-- 搜索结果页 -->
|
<!-- 搜索结果页 -->
|
||||||
<div class="page" id="search-results">
|
<div class="page" id="search-results">
|
||||||
@@ -114,26 +144,7 @@ function generateSearchResultsPage() {
|
|||||||
<h2>搜索结果</h2>
|
<h2>搜索结果</h2>
|
||||||
<p class="subtitle">在所有页面中找到的匹配项</p>
|
<p class="subtitle">在所有页面中找到的匹配项</p>
|
||||||
</div>
|
</div>
|
||||||
|
${searchSections}
|
||||||
<section class="category search-section" data-section="home" style="display: none;">
|
|
||||||
<h2><i class="fas fa-home"></i> 首页匹配项</h2>
|
|
||||||
<div class="sites-grid"></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="category search-section" data-section="projects" style="display: none;">
|
|
||||||
<h2><i class="fas fa-project-diagram"></i> 项目匹配项</h2>
|
|
||||||
<div class="sites-grid"></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="category search-section" data-section="articles" style="display: none;">
|
|
||||||
<h2><i class="fas fa-book"></i> 文章匹配项</h2>
|
|
||||||
<div class="sites-grid"></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="category search-section" data-section="friends" style="display: none;">
|
|
||||||
<h2><i class="fas fa-users"></i> 朋友匹配项</h2>
|
|
||||||
<div class="sites-grid"></div>
|
|
||||||
</section>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,13 +187,14 @@ function generateFontVariables(config) {
|
|||||||
function generateHTML(config) {
|
function generateHTML(config) {
|
||||||
const googleFontsLink = generateGoogleFontsLink(config);
|
const googleFontsLink = generateGoogleFontsLink(config);
|
||||||
const fontVariables = generateFontVariables(config);
|
const fontVariables = generateFontVariables(config);
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>${config.site.title}</title>
|
<title>${escapeHtml(config.site.title)}</title>
|
||||||
<link rel="icon" href="./favicon.ico" type="image/x-icon">
|
<link rel="icon" href="./favicon.ico" type="image/x-icon">
|
||||||
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon">
|
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon">
|
||||||
${googleFontsLink}
|
${googleFontsLink}
|
||||||
@@ -225,7 +237,7 @@ ${generateSocialLinks(config.social)}
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="copyright">
|
<div class="copyright">
|
||||||
<p>© 2025 <a href="https://github.com/RZLNB/menav" target="_blank">MeNav</a></p>
|
<p>© ${currentYear} <a href="https://github.com/RZLNB/menav" target="_blank">MeNav</a></p>
|
||||||
<p>by <a href="https://github.com/RZLNB" target="_blank">RZLNB</a></p>
|
<p>by <a href="https://github.com/RZLNB" target="_blank">RZLNB</a></p>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -243,9 +255,9 @@ ${generateSocialLinks(config.social)}
|
|||||||
<!-- 首页 -->
|
<!-- 首页 -->
|
||||||
<div class="page active" id="home">
|
<div class="page active" id="home">
|
||||||
<div class="welcome-section">
|
<div class="welcome-section">
|
||||||
<h2>${config.profile.title}</h2>
|
<h2>${escapeHtml(config.profile.title)}</h2>
|
||||||
<h3>${config.profile.subtitle}</h3>
|
<h3>${escapeHtml(config.profile.subtitle)}</h3>
|
||||||
<p class="subtitle">${config.profile.description}</p>
|
<p class="subtitle">${escapeHtml(config.profile.description)}</p>
|
||||||
</div>
|
</div>
|
||||||
${generateCategories(config.categories)}
|
${generateCategories(config.categories)}
|
||||||
</div>
|
</div>
|
||||||
@@ -264,7 +276,7 @@ ${generatePageContent('articles', config.articles)}
|
|||||||
<div class="page" id="friends">
|
<div class="page" id="friends">
|
||||||
${generatePageContent('friends', config.friends)}
|
${generatePageContent('friends', config.friends)}
|
||||||
</div>
|
</div>
|
||||||
${generateSearchResultsPage()}
|
${generateSearchResultsPage(config)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="script.js"></script>
|
<script src="script.js"></script>
|
||||||
|
|||||||
122
index.html
122
index.html
@@ -110,6 +110,27 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 导入/导出功能 -->
|
||||||
|
<div class="nav-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<i class="fas fa-cog"></i>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item import-export-btn" id="exportDataBtn">
|
||||||
|
<div class="icon-container">
|
||||||
|
<i class="fas fa-download"></i>
|
||||||
|
</div>
|
||||||
|
<span class="nav-text">导出数据</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item import-export-btn" id="importDataBtn">
|
||||||
|
<div class="icon-container">
|
||||||
|
<i class="fas fa-upload"></i>
|
||||||
|
</div>
|
||||||
|
<span class="nav-text">导入数据</span>
|
||||||
|
</div>
|
||||||
|
<!-- 隐藏的文件上传输入 -->
|
||||||
|
<input type="file" id="importFileInput" style="display: none;" accept=".json,.yml,.yaml">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="copyright">
|
<div class="copyright">
|
||||||
<p>© 2025 <a href="https://github.com/RZLNB/menav" target="_blank">MeNav</a></p>
|
<p>© 2025 <a href="https://github.com/RZLNB/menav" target="_blank">MeNav</a></p>
|
||||||
<p>by <a href="https://github.com/RZLNB" target="_blank">RZLNB</a></p>
|
<p>by <a href="https://github.com/RZLNB" target="_blank">RZLNB</a></p>
|
||||||
@@ -552,5 +573,106 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="script.js"></script>
|
<script src="script.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 从localStorage加载用户数据
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
try {
|
||||||
|
const userData = localStorage.getItem('menav_user_data');
|
||||||
|
if (userData) {
|
||||||
|
const data = JSON.parse(userData);
|
||||||
|
console.log('Loading user data from localStorage');
|
||||||
|
|
||||||
|
// 应用用户配置
|
||||||
|
applyUserData(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading user data:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用用户数据到页面
|
||||||
|
function applyUserData(data) {
|
||||||
|
try {
|
||||||
|
// 应用分类和网站
|
||||||
|
if (data.categories && data.categories.length > 0) {
|
||||||
|
const homePage = document.getElementById('home');
|
||||||
|
if (homePage) {
|
||||||
|
// 保留欢迎区域
|
||||||
|
const welcomeSection = homePage.querySelector('.welcome-section');
|
||||||
|
|
||||||
|
// 清除当前分类
|
||||||
|
const oldCategories = homePage.querySelectorAll('.category');
|
||||||
|
oldCategories.forEach(cat => {
|
||||||
|
if (!cat.classList.contains('welcome-section')) {
|
||||||
|
cat.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加新分类
|
||||||
|
data.categories.forEach(category => {
|
||||||
|
const categoryElem = document.createElement('section');
|
||||||
|
categoryElem.className = 'category';
|
||||||
|
categoryElem.innerHTML = `
|
||||||
|
<h2><i class="${category.icon}"></i> ${category.name}</h2>
|
||||||
|
<div class="sites-grid"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const sitesGrid = categoryElem.querySelector('.sites-grid');
|
||||||
|
|
||||||
|
// 添加站点
|
||||||
|
category.sites.forEach(site => {
|
||||||
|
const siteCard = document.createElement('a');
|
||||||
|
siteCard.className = 'site-card';
|
||||||
|
siteCard.href = site.url;
|
||||||
|
siteCard.innerHTML = `
|
||||||
|
<i class="${site.icon}"></i>
|
||||||
|
<h3>${site.name}</h3>
|
||||||
|
<p>${site.description}</p>
|
||||||
|
`;
|
||||||
|
sitesGrid.appendChild(siteCard);
|
||||||
|
});
|
||||||
|
|
||||||
|
homePage.appendChild(categoryElem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用社交链接
|
||||||
|
if (data.social && data.social.length > 0) {
|
||||||
|
const socialSection = document.querySelector('.nav-section:has(.section-title i.fa-link)');
|
||||||
|
if (socialSection) {
|
||||||
|
// 清除当前社交链接
|
||||||
|
const oldLinks = socialSection.querySelectorAll('.nav-item');
|
||||||
|
oldLinks.forEach(link => link.remove());
|
||||||
|
|
||||||
|
// 添加新社交链接
|
||||||
|
data.social.forEach(link => {
|
||||||
|
const linkElem = document.createElement('a');
|
||||||
|
linkElem.className = 'nav-item';
|
||||||
|
linkElem.href = link.url;
|
||||||
|
linkElem.target = '_blank';
|
||||||
|
linkElem.innerHTML = `
|
||||||
|
<div class="icon-container">
|
||||||
|
<i class="${link.icon}"></i>
|
||||||
|
</div>
|
||||||
|
<span class="nav-text">${link.name}</span>
|
||||||
|
<i class="fas fa-external-link-alt external-icon"></i>
|
||||||
|
`;
|
||||||
|
socialSection.appendChild(linkElem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新初始化搜索索引
|
||||||
|
if (window.initSearchIndex) {
|
||||||
|
window.initSearchIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error applying user data:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
275
script.js
275
script.js
@@ -21,6 +21,57 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
let isSidebarOpen = false;
|
let isSidebarOpen = false;
|
||||||
let isSearchOpen = 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() {
|
function toggleSidebar() {
|
||||||
isSidebarOpen = !isSidebarOpen;
|
isSidebarOpen = !isSidebarOpen;
|
||||||
@@ -106,6 +157,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// 搜索功能
|
// 搜索功能
|
||||||
function performSearch(searchTerm) {
|
function performSearch(searchTerm) {
|
||||||
|
// 确保搜索索引已初始化
|
||||||
|
if (!searchIndex.initialized) {
|
||||||
|
initSearchIndex();
|
||||||
|
}
|
||||||
|
|
||||||
searchTerm = searchTerm.toLowerCase().trim();
|
searchTerm = searchTerm.toLowerCase().trim();
|
||||||
|
|
||||||
// 如果搜索框为空,重置所有内容
|
// 如果搜索框为空,重置所有内容
|
||||||
@@ -118,54 +174,75 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
isSearchActive = true;
|
isSearchActive = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let hasResults = false;
|
try {
|
||||||
|
// 使用搜索索引进行搜索
|
||||||
const searchResults = new Map();
|
const searchResults = new Map();
|
||||||
|
let hasResults = false;
|
||||||
|
|
||||||
// 收集所有匹配结果
|
// 使用更高效的搜索算法
|
||||||
pages.forEach(page => {
|
const matchedItems = searchIndex.items.filter(item => {
|
||||||
if (page.id === 'search-results') return;
|
return item.searchText.includes(searchTerm);
|
||||||
|
});
|
||||||
|
|
||||||
const pageId = page.id;
|
// 按页面分组结果
|
||||||
const matches = [];
|
matchedItems.forEach(item => {
|
||||||
|
if (!searchResults.has(item.pageId)) {
|
||||||
page.querySelectorAll('.site-card').forEach(card => {
|
searchResults.set(item.pageId, []);
|
||||||
const title = card.querySelector('h3').textContent.toLowerCase();
|
}
|
||||||
const description = card.querySelector('p').textContent.toLowerCase();
|
// 克隆元素以避免修改原始DOM
|
||||||
if (title.includes(searchTerm) || description.includes(searchTerm)) {
|
searchResults.get(item.pageId).push(item.element.cloneNode(true));
|
||||||
matches.push(card.cloneNode(true));
|
|
||||||
hasResults = true;
|
hasResults = true;
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (matches.length > 0) {
|
// 使用requestAnimationFrame批量更新DOM,减少重排重绘
|
||||||
searchResults.set(pageId, matches);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 批量更新DOM
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
|
try {
|
||||||
// 清空并隐藏所有搜索区域
|
// 清空并隐藏所有搜索区域
|
||||||
searchSections.forEach(section => {
|
searchSections.forEach(section => {
|
||||||
|
try {
|
||||||
const grid = section.querySelector('.sites-grid');
|
const grid = section.querySelector('.sites-grid');
|
||||||
while (grid.firstChild) {
|
if (grid) {
|
||||||
grid.removeChild(grid.firstChild);
|
grid.innerHTML = ''; // 使用innerHTML清空,比removeChild更高效
|
||||||
}
|
}
|
||||||
section.style.display = 'none';
|
section.style.display = 'none';
|
||||||
|
} catch (sectionError) {
|
||||||
|
console.error('Error clearing search section:', sectionError);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 填充匹配结果
|
// 使用DocumentFragment批量添加DOM元素,减少重排
|
||||||
searchResults.forEach((matches, pageId) => {
|
searchResults.forEach((matches, pageId) => {
|
||||||
const section = searchResultsPage.querySelector(`[data-section="${pageId}"]`);
|
const section = searchResultsPage.querySelector(`[data-section="${pageId}"]`);
|
||||||
|
if (section) {
|
||||||
|
try {
|
||||||
const grid = section.querySelector('.sites-grid');
|
const grid = section.querySelector('.sites-grid');
|
||||||
|
if (grid) {
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
matches.forEach(card => fragment.appendChild(card));
|
|
||||||
|
matches.forEach(card => {
|
||||||
|
// 高亮匹配文本
|
||||||
|
highlightSearchTerm(card, searchTerm);
|
||||||
|
fragment.appendChild(card);
|
||||||
|
});
|
||||||
|
|
||||||
grid.appendChild(fragment);
|
grid.appendChild(fragment);
|
||||||
section.style.display = 'block';
|
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`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新搜索结果页面状态
|
// 更新搜索结果页面状态
|
||||||
searchResultsPage.querySelector('.subtitle').textContent =
|
const subtitle = searchResultsPage.querySelector('.subtitle');
|
||||||
hasResults ? '在所有页面中找到的匹配项' : '未找到匹配的结果';
|
if (subtitle) {
|
||||||
|
subtitle.textContent = hasResults
|
||||||
|
? `在所有页面中找到 ${matchedItems.length} 个匹配项`
|
||||||
|
: '未找到匹配的结果';
|
||||||
|
}
|
||||||
|
|
||||||
// 显示搜索结果页面
|
// 显示搜索结果页面
|
||||||
if (currentPageId !== 'search-results') {
|
if (currentPageId !== 'search-results') {
|
||||||
@@ -178,7 +255,124 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// 更新搜索状态样式
|
// 更新搜索状态样式
|
||||||
searchBox.classList.toggle('has-results', hasResults);
|
searchBox.classList.toggle('has-results', hasResults);
|
||||||
searchBox.classList.toggle('no-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, '\\$&');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置搜索状态
|
// 重置搜索状态
|
||||||
@@ -187,14 +381,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
isSearchActive = false;
|
isSearchActive = false;
|
||||||
|
|
||||||
|
try {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
|
try {
|
||||||
// 清空搜索结果
|
// 清空搜索结果
|
||||||
searchSections.forEach(section => {
|
searchSections.forEach(section => {
|
||||||
|
try {
|
||||||
const grid = section.querySelector('.sites-grid');
|
const grid = section.querySelector('.sites-grid');
|
||||||
|
if (grid) {
|
||||||
while (grid.firstChild) {
|
while (grid.firstChild) {
|
||||||
grid.removeChild(grid.firstChild);
|
grid.removeChild(grid.firstChild);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
section.style.display = 'none';
|
section.style.display = 'none';
|
||||||
|
} catch (sectionError) {
|
||||||
|
console.error('Error clearing search section:', sectionError);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 移除搜索状态样式
|
// 移除搜索状态样式
|
||||||
@@ -202,16 +404,30 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// 恢复到当前激活的页面
|
// 恢复到当前激活的页面
|
||||||
const currentActiveNav = document.querySelector('.nav-item.active');
|
const currentActiveNav = document.querySelector('.nav-item.active');
|
||||||
|
if (currentActiveNav) {
|
||||||
const targetPageId = currentActiveNav.getAttribute('data-page');
|
const targetPageId = currentActiveNav.getAttribute('data-page');
|
||||||
|
|
||||||
if (currentPageId !== targetPageId) {
|
if (targetPageId && currentPageId !== targetPageId) {
|
||||||
currentPageId = targetPageId;
|
currentPageId = targetPageId;
|
||||||
pages.forEach(page => {
|
pages.forEach(page => {
|
||||||
page.classList.toggle('active', page.id === targetPageId);
|
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) => {
|
const debounce = (fn, delay) => {
|
||||||
@@ -278,6 +494,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
category.style.opacity = '1';
|
category.style.opacity = '1';
|
||||||
}, index * 100);
|
}, index * 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 初始化搜索索引(使用requestIdleCallback或setTimeout延迟初始化,避免影响页面加载)
|
||||||
|
if ('requestIdleCallback' in window) {
|
||||||
|
requestIdleCallback(() => initSearchIndex());
|
||||||
|
} else {
|
||||||
|
setTimeout(initSearchIndex, 1000);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
73
style.css
73
style.css
@@ -5,6 +5,21 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 防止滚动条导致的布局偏移 */
|
||||||
|
html {
|
||||||
|
overflow-y: scroll; /* 始终显示垂直滚动条 */
|
||||||
|
scrollbar-width: thin; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索高亮样式 */
|
||||||
|
.highlight {
|
||||||
|
background-color: rgba(74, 158, 255, 0.3);
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 0 2px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
font-weight: var(--font-weight-body);
|
font-weight: var(--font-weight-body);
|
||||||
@@ -13,6 +28,7 @@ body {
|
|||||||
color: #e4e6eb;
|
color: #e4e6eb;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
overflow: hidden; /* 防止body滚动 */
|
overflow: hidden; /* 防止body滚动 */
|
||||||
|
padding-right: 0 !important; /* 防止滚动条导致的布局偏移 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 布局 */
|
/* 布局 */
|
||||||
@@ -206,7 +222,7 @@ body {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 主内容区域 */
|
/* 主内容区域 - 修复滚动条问题 */
|
||||||
.content {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin-left: 240px;
|
margin-left: 240px;
|
||||||
@@ -214,12 +230,30 @@ body {
|
|||||||
background-color: #1a1b1e;
|
background-color: #1a1b1e;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100vh; /* 固定高度 */
|
height: 100vh; /* 固定高度 */
|
||||||
overflow-y: auto; /* 允许内容滚动 */
|
overflow-y: scroll; /* 始终使用滚动,而不是auto */
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
max-width: calc(100vw - 240px);
|
max-width: calc(100vw - 240px);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
scrollbar-width: thin; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
.content::-webkit-scrollbar {
|
||||||
|
width: 8px; /* 固定滚动条宽度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.content::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 搜索框容器 - 固定在顶部 */
|
/* 搜索框容器 - 固定在顶部 */
|
||||||
@@ -851,6 +885,8 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 15;
|
z-index: 15;
|
||||||
|
transform: none !important; /* 确保没有变换 */
|
||||||
|
min-height: 400px; /* 确保最小高度,防止内容过少时的布局跳动 */
|
||||||
}
|
}
|
||||||
|
|
||||||
#search-results.active {
|
#search-results.active {
|
||||||
@@ -862,25 +898,36 @@ body {
|
|||||||
.search-section {
|
.search-section {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1100px;
|
max-width: 1100px;
|
||||||
margin: 0 auto;
|
margin: 0 auto 2.5rem auto; /* 添加底部间距 */
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
transform: none !important; /* 确保没有变换 */
|
||||||
|
opacity: 1 !important; /* 确保可见 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保搜索结果中的网格有正确的间距 */
|
||||||
|
.search-section .sites-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||||
|
gap: 1.2rem; /* 确保间距一致 */
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保搜索结果中的卡片样式正确 */
|
||||||
|
.search-section .site-card {
|
||||||
|
margin: 0; /* 重置可能的外边距 */
|
||||||
|
width: 100%; /* 确保宽度正确 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 清除之前可能导致问题的样式 */
|
/* 清除之前可能导致问题的样式 */
|
||||||
.page {
|
.page {
|
||||||
transform: none;
|
transform: none !important;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
width: 100% !important; /* 确保宽度一致 */
|
||||||
|
left: 0 !important; /* 确保位置固定 */
|
||||||
#search-results {
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-section {
|
|
||||||
transform: none;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 优化布局结构 */
|
/* 优化布局结构 */
|
||||||
|
|||||||
Reference in New Issue
Block a user