refactor: 模块化重构 generator 和 runtime

This commit is contained in:
rbetree
2026-01-15 21:08:26 +08:00
parent bcfa6e6316
commit 1a90f8fbe3
38 changed files with 4881 additions and 4411 deletions

440
src/runtime/app/search.js Normal file
View File

@@ -0,0 +1,440 @@
const searchEngines = require('./searchEngines');
const highlightSearchTerm = require('./search/highlight');
module.exports = function initSearch(state, dom) {
const {
searchInput,
searchBox,
searchResultsPage,
searchSections,
searchEngineToggle,
searchEngineToggleIcon,
searchEngineToggleLabel,
searchEngineDropdown,
searchEngineOptions,
} = dom;
if (!state.searchIndex) {
state.searchIndex = { initialized: false, items: [] };
}
if (!state.currentSearchEngine) {
state.currentSearchEngine = 'local';
}
if (typeof state.isSearchActive !== 'boolean') {
state.isSearchActive = false;
}
// 初始化搜索索引
function initSearchIndex() {
if (state.searchIndex.initialized) return;
state.searchIndex.items = [];
try {
// 为每个页面创建索引
if (!state.pages) {
state.pages = document.querySelectorAll('.page');
}
state.pages.forEach((page) => {
if (page.id === 'search-results') return;
const pageId = page.id;
page.querySelectorAll('.site-card').forEach((card) => {
try {
// 排除“扩展写回影子结构”等不应参与搜索的卡片
if (card.closest('[data-search-exclude="true"]')) return;
// 兼容不同页面/卡片样式:优先取可见文本,其次回退到 data-*(确保 projects repo 卡片也能被搜索)
const dataTitle = card.dataset?.name || card.getAttribute('data-name') || '';
const dataDescription = card.dataset?.description || card.getAttribute('data-description') || '';
const titleText =
card.querySelector('h3')?.textContent || card.querySelector('.repo-title')?.textContent || dataTitle;
const descriptionText =
card.querySelector('p')?.textContent || card.querySelector('.repo-desc')?.textContent || dataDescription;
const title = String(titleText || '').toLowerCase();
const description = String(descriptionText || '').toLowerCase();
const url = card.href || card.getAttribute('href') || '#';
const icon =
card.querySelector('i.icon-fallback')?.className || card.querySelector('i')?.className || '';
// 将卡片信息添加到索引中
state.searchIndex.items.push({
pageId,
title,
description,
url,
icon,
element: card,
// 预先计算搜索文本,提高搜索效率
searchText: (title + ' ' + description).toLowerCase(),
});
} catch (cardError) {
console.error('Error processing card:', cardError);
}
});
});
state.searchIndex.initialized = true;
} catch (error) {
console.error('Error initializing search index:', error);
state.searchIndex.initialized = true; // 防止反复尝试初始化
}
}
// 搜索功能
function performSearch(searchTerm) {
// 确保搜索索引已初始化
if (!state.searchIndex.initialized) {
initSearchIndex();
}
searchTerm = searchTerm.toLowerCase().trim();
// 如果搜索框为空,重置所有内容
if (!searchTerm) {
resetSearch();
return;
}
if (!state.isSearchActive) {
state.isSearchActive = true;
}
try {
// 使用搜索索引进行搜索
const searchResults = new Map();
let hasResults = false;
// 使用更高效的搜索算法
const matchedItems = state.searchIndex.items.filter((item) => {
return item.searchText.includes(searchTerm) || PinyinMatch.match(item.searchText, 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');
}
});
// 使用 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 grid');
}
}
});
// 更新搜索结果页面状态
const subtitle = searchResultsPage.querySelector('.subtitle');
if (subtitle) {
subtitle.textContent = hasResults
? `在所有页面中找到 ${matchedItems.length} 个匹配项`
: '未找到匹配的结果';
}
// 显示搜索结果页面
if (state.currentPageId !== 'search-results') {
state.currentPageId = 'search-results';
if (!state.pages) state.pages = document.querySelectorAll('.page');
state.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');
}
});
} catch (searchError) {
console.error('Error performing search');
}
}
// 重置搜索状态
function resetSearch() {
if (!state.isSearchActive) return;
state.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');
}
});
// 移除搜索状态样式
searchBox.classList.remove('has-results', 'no-results');
// 恢复到当前激活的页面
const currentActiveNav = document.querySelector('.nav-item.active');
if (currentActiveNav) {
const targetPageId = currentActiveNav.getAttribute('data-page');
if (targetPageId && state.currentPageId !== targetPageId) {
state.currentPageId = targetPageId;
if (!state.pages) state.pages = document.querySelectorAll('.page');
state.pages.forEach((page) => {
page.classList.toggle('active', page.id === targetPageId);
});
}
} else {
// 如果没有激活的导航项,默认显示首页
state.currentPageId = state.homePageId;
if (!state.pages) state.pages = document.querySelectorAll('.page');
state.pages.forEach((page) => {
page.classList.toggle('active', page.id === state.homePageId);
});
}
} catch (resetError) {
console.error('Error resetting search UI');
}
});
} catch (error) {
console.error('Error in resetSearch');
}
}
// 搜索输入事件(使用防抖)
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) => {
// 只有在选择了本地搜索时,才在输入时实时显示本地搜索结果
if (state.currentSearchEngine === 'local') {
debouncedSearch(e.target.value);
} else {
// 对于非本地搜索,重置之前的本地搜索结果(如果有)
if (state.isSearchActive) {
resetSearch();
}
}
});
// 更新搜索引擎 UI 显示
function updateSearchEngineUI() {
// 移除所有选项的激活状态
searchEngineOptions.forEach((option) => {
option.classList.remove('active');
// 如果是当前选中的搜索引擎,添加激活状态
if (option.getAttribute('data-engine') === state.currentSearchEngine) {
option.classList.add('active');
}
});
// 更新搜索引擎按钮(方案 B前缀按钮显示当前引擎
const engine = searchEngines[state.currentSearchEngine];
if (!engine) return;
const displayName = engine.shortName || engine.name.replace(/搜索$/, '');
if (searchEngineToggleIcon) {
if (engine.iconSvg) {
searchEngineToggleIcon.className = 'search-engine-icon search-engine-icon-svg';
searchEngineToggleIcon.innerHTML = engine.iconSvg;
} else {
searchEngineToggleIcon.innerHTML = '';
searchEngineToggleIcon.className = `search-engine-icon ${engine.icon}`;
}
}
if (searchEngineToggleLabel) {
searchEngineToggleLabel.textContent = displayName;
}
if (searchEngineToggle) {
searchEngineToggle.setAttribute('aria-label', `当前搜索引擎:${engine.name},点击切换`);
}
}
// 初始化搜索引擎设置
function initSearchEngine() {
// 从本地存储获取上次选择的搜索引擎
const savedEngine = localStorage.getItem('searchEngine');
if (savedEngine && searchEngines[savedEngine]) {
state.currentSearchEngine = savedEngine;
}
// 设置当前搜索引擎的激活状态及图标
updateSearchEngineUI();
// 初始化搜索引擎下拉菜单事件
const toggleEngineDropdown = () => {
if (!searchEngineDropdown) return;
const next = !searchEngineDropdown.classList.contains('active');
searchEngineDropdown.classList.toggle('active', next);
if (searchBox) {
searchBox.classList.toggle('dropdown-open', next);
}
if (searchEngineToggle) {
searchEngineToggle.setAttribute('aria-expanded', String(next));
}
};
if (searchEngineToggle) {
searchEngineToggle.addEventListener('click', (e) => {
e.stopPropagation();
toggleEngineDropdown();
});
// 键盘可访问性Enter/Space 触发
searchEngineToggle.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
e.stopPropagation();
toggleEngineDropdown();
}
});
}
// 点击搜索引擎选项
searchEngineOptions.forEach((option) => {
// 初始化激活状态
if (option.getAttribute('data-engine') === state.currentSearchEngine) {
option.classList.add('active');
}
option.addEventListener('click', (e) => {
e.stopPropagation();
// 获取选中的搜索引擎
const engine = option.getAttribute('data-engine');
// 更新当前搜索引擎
if (engine && searchEngines[engine]) {
// 如果搜索引擎变更,且之前有活跃的本地搜索结果,重置搜索状态
if (state.currentSearchEngine !== engine && state.isSearchActive) {
resetSearch();
}
state.currentSearchEngine = engine;
localStorage.setItem('searchEngine', engine);
// 更新 UI 显示
updateSearchEngineUI();
// 关闭下拉菜单
if (searchEngineDropdown) {
searchEngineDropdown.classList.remove('active');
}
if (searchBox) {
searchBox.classList.remove('dropdown-open');
}
}
});
});
// 点击页面其他位置关闭下拉菜单
document.addEventListener('click', () => {
if (!searchEngineDropdown) return;
searchEngineDropdown.classList.remove('active');
if (searchBox) {
searchBox.classList.remove('dropdown-open');
}
});
}
// 执行搜索(根据选择的搜索引擎)
function executeSearch(searchTerm) {
if (!searchTerm.trim()) return;
// 根据当前搜索引擎执行搜索
if (state.currentSearchEngine === 'local') {
// 执行本地搜索
performSearch(searchTerm);
} else {
// 使用外部搜索引擎
const engine = searchEngines[state.currentSearchEngine];
if (engine && engine.url) {
// 打开新窗口进行搜索
window.open(engine.url + encodeURIComponent(searchTerm), '_blank');
}
}
}
// 搜索框事件处理
searchInput.addEventListener('keyup', (e) => {
if (e.key === 'Escape') {
searchInput.value = '';
resetSearch();
} else if (e.key === 'Enter') {
executeSearch(searchInput.value);
}
});
// 阻止搜索框的回车默认行为
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
}
});
return {
initSearchIndex,
initSearchEngine,
resetSearch,
performSearch,
};
};