diff --git a/generator.js b/generator.js index e5cdb74..38cc630 100644 --- a/generator.js +++ b/generator.js @@ -2,6 +2,19 @@ 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, "'"); +} + // 读取配置文件 function loadConfig() { let config = {}; @@ -54,21 +67,21 @@ function mergeConfigs(defaultConfig, userConfig) { // 生成导航菜单 function generateNavigation(navigation) { return navigation.map(nav => ` - +
- +
- ${nav.name} + ${escapeHtml(nav.name)}
`).join('\n'); } // 生成网站卡片HTML function generateSiteCards(sites) { return sites.map(site => ` - - -

${site.name}

-

${site.description}

+
+ +

${escapeHtml(site.name)}

+

${escapeHtml(site.description)}

`).join('\n'); } @@ -76,7 +89,7 @@ function generateSiteCards(sites) { function generateCategories(categories) { return categories.map(category => `
-

${category.name}

+

${escapeHtml(category.name)}

${generateSiteCards(category.sites)}
@@ -86,11 +99,11 @@ function generateCategories(categories) { // 生成社交链接HTML function generateSocialLinks(social) { return social.map(link => ` - +
- +
- ${link.name} + ${escapeHtml(link.name)}
`).join('\n'); } @@ -99,14 +112,31 @@ function generateSocialLinks(social) { function generatePageContent(pageId, data) { return `
-

${data.title}

-

${data.subtitle}

+

${escapeHtml(data.title)}

+

${escapeHtml(data.subtitle)}

${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 ` + `; + }).join('\n'); + return `
@@ -114,26 +144,7 @@ function generateSearchResultsPage() {

搜索结果

在所有页面中找到的匹配项

- - - - - - - - +${searchSections} `; } @@ -176,13 +187,14 @@ function generateFontVariables(config) { function generateHTML(config) { const googleFontsLink = generateGoogleFontsLink(config); const fontVariables = generateFontVariables(config); + const currentYear = new Date().getFullYear(); return ` - ${config.site.title} + ${escapeHtml(config.site.title)} ${googleFontsLink} @@ -225,7 +237,7 @@ ${generateSocialLinks(config.social)} @@ -243,9 +255,9 @@ ${generateSocialLinks(config.social)}
-

${config.profile.title}

-

${config.profile.subtitle}

-

${config.profile.description}

+

${escapeHtml(config.profile.title)}

+

${escapeHtml(config.profile.subtitle)}

+

${escapeHtml(config.profile.description)}

${generateCategories(config.categories)}
@@ -264,7 +276,7 @@ ${generatePageContent('articles', config.articles)}
${generatePageContent('friends', config.friends)}
-${generateSearchResultsPage()} +${generateSearchResultsPage(config)} diff --git a/index.html b/index.html index 25339fc..32560cb 100644 --- a/index.html +++ b/index.html @@ -110,6 +110,27 @@ + + + + + \ No newline at end of file diff --git a/script.js b/script.js index adc3405..f3fb166 100644 --- a/script.js +++ b/script.js @@ -20,6 +20,57 @@ document.addEventListener('DOMContentLoaded', () => { 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() { @@ -106,6 +157,11 @@ document.addEventListener('DOMContentLoaded', () => { // 搜索功能 function performSearch(searchTerm) { + // 确保搜索索引已初始化 + if (!searchIndex.initialized) { + initSearchIndex(); + } + searchTerm = searchTerm.toLowerCase().trim(); // 如果搜索框为空,重置所有内容 @@ -118,67 +174,205 @@ document.addEventListener('DOMContentLoaded', () => { isSearchActive = true; } - let hasResults = false; - const searchResults = new Map(); + 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; + }); - // 收集所有匹配结果 - pages.forEach(page => { - if (page.id === 'search-results') return; + // 使用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); + } + }); - const pageId = page.id; - const matches = []; + // 使用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`); + } + }); - page.querySelectorAll('.site-card').forEach(card => { - const title = card.querySelector('h3').textContent.toLowerCase(); - const description = card.querySelector('p').textContent.toLowerCase(); - if (title.includes(searchTerm) || description.includes(searchTerm)) { - matches.push(card.cloneNode(true)); - hasResults = true; + // 更新搜索结果页面状态 + 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); } }); - - if (matches.length > 0) { - searchResults.set(pageId, matches); - } - }); - - // 批量更新DOM - requestAnimationFrame(() => { - // 清空并隐藏所有搜索区域 - searchSections.forEach(section => { - const grid = section.querySelector('.sites-grid'); - while (grid.firstChild) { - grid.removeChild(grid.firstChild); + } 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; } - section.style.display = 'none'; - }); - - // 填充匹配结果 - searchResults.forEach((matches, pageId) => { - const section = searchResultsPage.querySelector(`[data-section="${pageId}"]`); - const grid = section.querySelector('.sites-grid'); - const fragment = document.createDocumentFragment(); - matches.forEach(card => fragment.appendChild(card)); - grid.appendChild(fragment); - section.style.display = 'block'; - }); - - // 更新搜索结果页面状态 - searchResultsPage.querySelector('.subtitle').textContent = - hasResults ? '在所有页面中找到的匹配项' : '未找到匹配的结果'; - - // 显示搜索结果页面 - if (currentPageId !== 'search-results') { - currentPageId = 'search-results'; - pages.forEach(page => { - page.classList.toggle('active', page.id === 'search-results'); - }); + + // 添加剩余文本 + if (lastIndex < titleText.length) { + titleFragment.appendChild(document.createTextNode( + titleText.substring(lastIndex) + )); + } + + // 清空原标题并添加新内容 + while (title.firstChild) { + title.removeChild(title.firstChild); + } + title.appendChild(titleFragment); } - - // 更新搜索状态样式 - searchBox.classList.toggle('has-results', hasResults); - searchBox.classList.toggle('no-results', !hasResults); - }); + + // 安全地高亮描述中的匹配文本 + 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,30 +381,52 @@ document.addEventListener('DOMContentLoaded', () => { isSearchActive = false; - requestAnimationFrame(() => { - // 清空搜索结果 - searchSections.forEach(section => { - const grid = section.querySelector('.sites-grid'); - while (grid.firstChild) { - grid.removeChild(grid.firstChild); + 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); } - section.style.display = 'none'; }); - - // 移除搜索状态样式 - searchBox.classList.remove('has-results', 'no-results'); - - // 恢复到当前激活的页面 - const currentActiveNav = document.querySelector('.nav-item.active'); - const targetPageId = currentActiveNav.getAttribute('data-page'); - - if (currentPageId !== targetPageId) { - currentPageId = targetPageId; - pages.forEach(page => { - page.classList.toggle('active', page.id === targetPageId); - }); - } - }); + } catch (error) { + console.error('Error in resetSearch:', error); + } } // 搜索输入事件(使用防抖) @@ -278,6 +494,13 @@ document.addEventListener('DOMContentLoaded', () => { category.style.opacity = '1'; }, index * 100); }); + + // 初始化搜索索引(使用requestIdleCallback或setTimeout延迟初始化,避免影响页面加载) + if ('requestIdleCallback' in window) { + requestIdleCallback(() => initSearchIndex()); + } else { + setTimeout(initSearchIndex, 1000); + } }); }); }); \ No newline at end of file diff --git a/style.css b/style.css index d9cc632..dca7e93 100644 --- a/style.css +++ b/style.css @@ -5,6 +5,21 @@ 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 { font-family: var(--font-body); font-weight: var(--font-weight-body); @@ -13,6 +28,7 @@ body { color: #e4e6eb; min-height: 100vh; overflow: hidden; /* 防止body滚动 */ + padding-right: 0 !important; /* 防止滚动条导致的布局偏移 */ } /* 布局 */ @@ -206,7 +222,7 @@ body { text-align: center; } -/* 主内容区域 */ +/* 主内容区域 - 修复滚动条问题 */ .content { flex: 1; margin-left: 240px; @@ -214,12 +230,30 @@ body { background-color: #1a1b1e; position: relative; height: 100vh; /* 固定高度 */ - overflow-y: auto; /* 允许内容滚动 */ + overflow-y: scroll; /* 始终使用滚动,而不是auto */ overflow-x: hidden; max-width: calc(100vw - 240px); display: flex; flex-direction: column; 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; align-items: center; z-index: 15; + transform: none !important; /* 确保没有变换 */ + min-height: 400px; /* 确保最小高度,防止内容过少时的布局跳动 */ } #search-results.active { @@ -862,25 +898,36 @@ body { .search-section { width: 100%; max-width: 1100px; - margin: 0 auto; + margin: 0 auto 2.5rem auto; /* 添加底部间距 */ position: relative; 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 { - transform: none; + transform: none !important; visibility: visible; transition: none; -} - -#search-results { - transform: none; -} - -.search-section { - transform: none; - opacity: 1; + width: 100% !important; /* 确保宽度一致 */ + left: 0 !important; /* 确保位置固定 */ } /* 优化布局结构 */