feat: 实现MeNav浏览器扩展支持接口
为支持浏览器扩展的HTML替换方案,对原仓库进行以下修改: - 在generator.js中添加配置数据序列化和嵌入功能 - 在default.hbs中添加配置数据存储元素 - 在site-card.hbs和category.hbs中添加数据属性标识符 - 在script.js中添加全局MeNav对象和API方法
This commit is contained in:
210
src/generator.js
210
src/generator.js
@@ -13,12 +13,12 @@ registerAllHelpers(handlebars);
|
||||
// 加载和注册Handlebars模板的函数
|
||||
function loadHandlebarsTemplates() {
|
||||
const templatesDir = path.join(process.cwd(), 'templates');
|
||||
|
||||
|
||||
// 检查基本模板目录是否存在
|
||||
if (!fs.existsSync(templatesDir)) {
|
||||
throw new Error('Templates directory not found. Cannot proceed without templates.');
|
||||
}
|
||||
|
||||
|
||||
// 加载布局模板
|
||||
const layoutsDir = path.join(templatesDir, 'layouts');
|
||||
if (fs.existsSync(layoutsDir)) {
|
||||
@@ -33,7 +33,7 @@ function loadHandlebarsTemplates() {
|
||||
} else {
|
||||
throw new Error('Layouts directory not found. Cannot proceed without layout templates.');
|
||||
}
|
||||
|
||||
|
||||
// 加载组件模板
|
||||
const componentsDir = path.join(templatesDir, 'components');
|
||||
if (fs.existsSync(componentsDir)) {
|
||||
@@ -48,7 +48,7 @@ function loadHandlebarsTemplates() {
|
||||
} else {
|
||||
throw new Error('Components directory not found. Cannot proceed without component templates.');
|
||||
}
|
||||
|
||||
|
||||
// 识别并检查默认布局模板是否存在
|
||||
const defaultLayoutPath = path.join(layoutsDir, 'default.hbs');
|
||||
if (!fs.existsSync(defaultLayoutPath)) {
|
||||
@@ -62,17 +62,17 @@ function loadHandlebarsTemplates() {
|
||||
*/
|
||||
function getDefaultLayoutTemplate() {
|
||||
const defaultLayoutPath = path.join(process.cwd(), 'templates', 'layouts', 'default.hbs');
|
||||
|
||||
|
||||
// 检查默认布局模板是否存在
|
||||
if (!fs.existsSync(defaultLayoutPath)) {
|
||||
throw new Error('Default layout template not found. Cannot proceed without default layout.');
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// 读取布局内容并编译模板
|
||||
const layoutContent = fs.readFileSync(defaultLayoutPath, 'utf8');
|
||||
const layoutTemplate = handlebars.compile(layoutContent);
|
||||
|
||||
|
||||
return {
|
||||
path: defaultLayoutPath,
|
||||
template: layoutTemplate
|
||||
@@ -85,41 +85,41 @@ function getDefaultLayoutTemplate() {
|
||||
// 渲染Handlebars模板函数
|
||||
function renderTemplate(templateName, data, useLayout = true) {
|
||||
const templatePath = path.join(process.cwd(), 'templates', 'pages', `${templateName}.hbs`);
|
||||
|
||||
|
||||
// 检查模板是否存在
|
||||
if (!fs.existsSync(templatePath)) {
|
||||
// 尝试使用通用模板 page.hbs
|
||||
const genericTemplatePath = path.join(process.cwd(), 'templates', 'pages', 'page.hbs');
|
||||
|
||||
|
||||
if (fs.existsSync(genericTemplatePath)) {
|
||||
console.log(`模板 ${templateName}.hbs 不存在,使用通用模板 page.hbs 代替`);
|
||||
const genericTemplateContent = fs.readFileSync(genericTemplatePath, 'utf8');
|
||||
const genericTemplate = handlebars.compile(genericTemplateContent);
|
||||
|
||||
|
||||
// 添加 pageId 到数据中,以便通用模板使用
|
||||
const enhancedData = {
|
||||
...data,
|
||||
pageId: templateName // 确保pageId在模板中可用
|
||||
};
|
||||
|
||||
|
||||
// 渲染页面内容
|
||||
const pageContent = genericTemplate(enhancedData);
|
||||
|
||||
|
||||
// 如果不使用布局,直接返回页面内容
|
||||
if (!useLayout) {
|
||||
return pageContent;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// 使用辅助函数获取默认布局模板
|
||||
const { template: layoutTemplate } = getDefaultLayoutTemplate();
|
||||
|
||||
|
||||
// 准备布局数据,包含页面内容
|
||||
const layoutData = {
|
||||
...enhancedData,
|
||||
body: pageContent
|
||||
};
|
||||
|
||||
|
||||
// 渲染完整页面
|
||||
return layoutTemplate(layoutData);
|
||||
} catch (layoutError) {
|
||||
@@ -129,29 +129,29 @@ function renderTemplate(templateName, data, useLayout = true) {
|
||||
throw new Error(`Template ${templateName}.hbs not found and generic template page.hbs not found. Cannot proceed without template.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const templateContent = fs.readFileSync(templatePath, 'utf8');
|
||||
const template = handlebars.compile(templateContent);
|
||||
|
||||
|
||||
// 渲染页面内容
|
||||
const pageContent = template(data);
|
||||
|
||||
|
||||
// 如果不使用布局,直接返回页面内容
|
||||
if (!useLayout) {
|
||||
return pageContent;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// 使用辅助函数获取默认布局模板
|
||||
const { template: layoutTemplate } = getDefaultLayoutTemplate();
|
||||
|
||||
|
||||
// 准备布局数据,包含页面内容
|
||||
const layoutData = {
|
||||
...data,
|
||||
body: pageContent
|
||||
};
|
||||
|
||||
|
||||
// 渲染完整页面
|
||||
return layoutTemplate(layoutData);
|
||||
} catch (layoutError) {
|
||||
@@ -193,7 +193,7 @@ function safeLoadYamlConfig(filePath) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
return yaml.load(fileContent);
|
||||
@@ -228,7 +228,7 @@ function loadModularConfig(dirPath) {
|
||||
if (siteConfig) {
|
||||
// 将site.yml中的内容分配到正确的配置字段
|
||||
config.site = siteConfig;
|
||||
|
||||
|
||||
// 提取特殊字段到顶层配置
|
||||
if (siteConfig.fonts) config.fonts = siteConfig.fonts;
|
||||
if (siteConfig.profile) config.profile = siteConfig.profile;
|
||||
@@ -245,22 +245,22 @@ function loadModularConfig(dirPath) {
|
||||
// 加载页面配置
|
||||
const pagesPath = path.join(dirPath, 'pages');
|
||||
if (fs.existsSync(pagesPath)) {
|
||||
const files = fs.readdirSync(pagesPath).filter(file =>
|
||||
const files = fs.readdirSync(pagesPath).filter(file =>
|
||||
file.endsWith('.yml') || file.endsWith('.yaml'));
|
||||
|
||||
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(pagesPath, file);
|
||||
const fileConfig = safeLoadYamlConfig(filePath);
|
||||
|
||||
|
||||
if (fileConfig) {
|
||||
// 提取文件名(不含扩展名)作为配置键
|
||||
const configKey = path.basename(file, path.extname(file));
|
||||
|
||||
|
||||
// 特殊处理home.yml中的categories字段
|
||||
if (configKey === 'home' && fileConfig.categories) {
|
||||
config.categories = fileConfig.categories;
|
||||
}
|
||||
|
||||
|
||||
// 将页面配置添加到主配置对象
|
||||
config[configKey] = fileConfig;
|
||||
}
|
||||
@@ -278,7 +278,7 @@ function loadModularConfig(dirPath) {
|
||||
function ensureConfigDefaults(config) {
|
||||
// 创建一个新对象,避免修改原始配置
|
||||
const result = { ...config };
|
||||
|
||||
|
||||
// 确保基本结构存在
|
||||
result.site = result.site || {};
|
||||
result.navigation = result.navigation || [];
|
||||
@@ -286,30 +286,30 @@ function ensureConfigDefaults(config) {
|
||||
result.profile = result.profile || {};
|
||||
result.social = result.social || [];
|
||||
result.categories = result.categories || [];
|
||||
|
||||
|
||||
// 站点基本信息默认值
|
||||
result.site.title = result.site.title || 'MeNav导航';
|
||||
result.site.favicon = result.site.favicon || 'favicon.ico';
|
||||
result.site.logo = result.site.logo || null;
|
||||
result.site.footer = result.site.footer || '';
|
||||
result.site.theme = result.site.theme || {
|
||||
result.site.theme = result.site.theme || {
|
||||
primary: '#4a89dc',
|
||||
background: '#f5f7fa',
|
||||
modeToggle: true
|
||||
};
|
||||
|
||||
|
||||
// 用户资料默认值
|
||||
result.profile = result.profile || {};
|
||||
result.profile.title = result.profile.title || '欢迎使用';
|
||||
result.profile.subtitle = result.profile.subtitle || 'MeNav个人导航系统';
|
||||
result.profile.description = result.profile.description || '简单易用的个人导航站点';
|
||||
|
||||
|
||||
// 为每个类别和站点设置默认值
|
||||
result.categories = result.categories || [];
|
||||
result.categories.forEach(category => {
|
||||
category.name = category.name || '未命名分类';
|
||||
category.sites = category.sites || [];
|
||||
|
||||
|
||||
// 为每个站点设置默认值
|
||||
category.sites.forEach(site => {
|
||||
site.name = site.name || '未命名站点';
|
||||
@@ -319,7 +319,7 @@ function ensureConfigDefaults(config) {
|
||||
site.external = typeof site.external === 'boolean' ? site.external : true;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -334,10 +334,10 @@ function validateConfig(config) {
|
||||
console.error('配置无效: 配置必须是一个对象');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 所有其他验证被移除,因为它们只是检查但没有实际操作
|
||||
// 配置默认值和数据修复已经在ensureConfigDefaults函数中处理
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -351,7 +351,7 @@ function getSubmenuForNavItem(navItem, config) {
|
||||
if (!navItem || !navItem.id || !config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// 首页页面添加子菜单(分类)
|
||||
if (navItem.id === 'home' && Array.isArray(config.categories)) {
|
||||
return config.categories;
|
||||
@@ -376,7 +376,7 @@ function getSubmenuForNavItem(navItem, config) {
|
||||
else if (config[navItem.id] && config[navItem.id].categories && Array.isArray(config[navItem.id].categories)) {
|
||||
return config[navItem.id].categories;
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -388,20 +388,27 @@ function getSubmenuForNavItem(navItem, config) {
|
||||
function prepareRenderData(config) {
|
||||
// 创建渲染数据对象,包含原始配置
|
||||
const renderData = { ...config };
|
||||
|
||||
|
||||
// 添加额外渲染数据
|
||||
renderData._meta = {
|
||||
generated_at: new Date(),
|
||||
version: process.env.npm_package_version || '1.0.0',
|
||||
generator: 'MeNav'
|
||||
};
|
||||
|
||||
|
||||
// 确保navigation是数组
|
||||
if (!Array.isArray(renderData.navigation)) {
|
||||
renderData.navigation = [];
|
||||
// 移除警告日志,数据处理逻辑保留
|
||||
}
|
||||
|
||||
|
||||
// 添加序列化的配置数据,用于浏览器扩展
|
||||
renderData.configJSON = JSON.stringify({
|
||||
version: process.env.npm_package_version || '1.0.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
data: config
|
||||
});
|
||||
|
||||
// 添加导航项的活动状态标记和子菜单
|
||||
if (Array.isArray(renderData.navigation)) {
|
||||
renderData.navigation = renderData.navigation.map((item, index) => {
|
||||
@@ -411,7 +418,7 @@ function prepareRenderData(config) {
|
||||
id: item.id || `nav-${index}`,
|
||||
active: index === 0 // 兼容原有逻辑
|
||||
};
|
||||
|
||||
|
||||
// 使用辅助函数获取子菜单
|
||||
const submenu = getSubmenuForNavItem(navItem, renderData);
|
||||
if (submenu) {
|
||||
@@ -421,15 +428,15 @@ function prepareRenderData(config) {
|
||||
return navItem;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 为Handlebars模板特别准备navigationData数组
|
||||
renderData.navigationData = renderData.navigation;
|
||||
|
||||
|
||||
// 确保social数据格式正确
|
||||
if (Array.isArray(renderData.social)) {
|
||||
renderData.socialLinks = renderData.social; // 兼容模板中的不同引用名
|
||||
}
|
||||
|
||||
|
||||
return renderData;
|
||||
}
|
||||
|
||||
@@ -444,11 +451,11 @@ function loadConfig() {
|
||||
social: [],
|
||||
categories: []
|
||||
};
|
||||
|
||||
|
||||
// 检查模块化配置来源是否存在
|
||||
const hasUserModularConfig = fs.existsSync('config/user');
|
||||
const hasDefaultModularConfig = fs.existsSync('config/_default');
|
||||
|
||||
|
||||
// 根据优先级顺序选择最高优先级的配置
|
||||
if (hasUserModularConfig) {
|
||||
// 1. 最高优先级: config/user/ 目录
|
||||
@@ -459,7 +466,7 @@ function loadConfig() {
|
||||
} else {
|
||||
// 3. 最低优先级: 旧版单文件配置 (config.yml or config.yaml)
|
||||
const legacyConfigPath = fs.existsSync('config.yml') ? 'config.yml' : 'config.yaml';
|
||||
|
||||
|
||||
if (fs.existsSync(legacyConfigPath)) {
|
||||
try {
|
||||
const fileContent = fs.readFileSync(legacyConfigPath, 'utf8');
|
||||
@@ -475,14 +482,14 @@ function loadConfig() {
|
||||
|
||||
// 确保配置有默认值并通过验证
|
||||
config = ensureConfigDefaults(config);
|
||||
|
||||
|
||||
if (!validateConfig(config)) {
|
||||
// 移除警告日志,保留函数调用
|
||||
}
|
||||
|
||||
|
||||
// 准备渲染数据
|
||||
const renderData = prepareRenderData(config);
|
||||
|
||||
|
||||
return renderData;
|
||||
}
|
||||
|
||||
@@ -491,10 +498,10 @@ function generateNavigation(navigation, config) {
|
||||
return navigation.map(nav => {
|
||||
// 根据页面ID获取对应的子菜单项(分类)
|
||||
let submenuItems = '';
|
||||
|
||||
|
||||
// 使用辅助函数获取子菜单数据
|
||||
const submenu = getSubmenuForNavItem(nav, config);
|
||||
|
||||
|
||||
// 如果存在子菜单,生成HTML
|
||||
if (submenu && Array.isArray(submenu)) {
|
||||
submenuItems = `
|
||||
@@ -527,7 +534,7 @@ function generateSiteCards(sites) {
|
||||
if (!sites || !Array.isArray(sites) || sites.length === 0) {
|
||||
return `<p class="empty-sites">暂无网站</p>`;
|
||||
}
|
||||
|
||||
|
||||
return sites.map(site => `
|
||||
<a href="${escapeHtml(site.url)}" class="site-card" title="${escapeHtml(site.name)} - ${escapeHtml(site.description || '')}">
|
||||
<i class="${escapeHtml(site.icon || 'fas fa-link')}"></i>
|
||||
@@ -545,7 +552,7 @@ function generateCategories(categories) {
|
||||
<p>请在配置文件中添加分类</p>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
|
||||
return categories.map(category => `
|
||||
<section class="category" id="${escapeHtml(category.name)}">
|
||||
<h2><i class="${escapeHtml(category.icon)}"></i> ${escapeHtml(category.name)}</h2>
|
||||
@@ -560,7 +567,7 @@ function generateSocialLinks(social) {
|
||||
if (!social || !Array.isArray(social) || social.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
// 尝试使用Handlebars模板
|
||||
try {
|
||||
const socialLinksPath = path.join(process.cwd(), 'templates', 'components', 'social-links.hbs');
|
||||
@@ -574,7 +581,7 @@ function generateSocialLinks(social) {
|
||||
console.error('Error rendering social-links template:', error);
|
||||
// 出错时回退到原始生成方法
|
||||
}
|
||||
|
||||
|
||||
// 回退到原始生成方法
|
||||
return social.map(link => `
|
||||
<a href="${escapeHtml(link.url)}" class="nav-item" target="_blank">
|
||||
@@ -597,11 +604,11 @@ function generatePageContent(pageId, data) {
|
||||
<p class="subtitle">请配置 ${pageId} 页面</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
||||
// 首页使用profile数据,其他页面使用自身数据
|
||||
if (pageId === 'home') {
|
||||
const profile = data.profile || {};
|
||||
|
||||
|
||||
return `
|
||||
<div class="welcome-section">
|
||||
<h2>${escapeHtml(profile.title || '欢迎使用')}</h2>
|
||||
@@ -614,7 +621,7 @@ ${generateCategories(data.categories)}`;
|
||||
const title = data.title || `${pageId} 页面`;
|
||||
const subtitle = data.subtitle || '';
|
||||
const categories = data.categories || [];
|
||||
|
||||
|
||||
return `
|
||||
<div class="welcome-section">
|
||||
<h2>${escapeHtml(title)}</h2>
|
||||
@@ -628,14 +635,14 @@ ${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>
|
||||
@@ -658,7 +665,7 @@ ${searchSections}
|
||||
function generateGoogleFontsLink(config) {
|
||||
const fonts = config.fonts;
|
||||
const googleFonts = [];
|
||||
|
||||
|
||||
// 收集需要加载的Google字体
|
||||
Object.values(fonts).forEach(font => {
|
||||
if (font.source === 'google') {
|
||||
@@ -667,8 +674,8 @@ function generateGoogleFontsLink(config) {
|
||||
googleFonts.push(`family=${fontName}:wght@${fontWeight}`);
|
||||
}
|
||||
});
|
||||
|
||||
return googleFonts.length > 0
|
||||
|
||||
return googleFonts.length > 0
|
||||
? `<link href="https://fonts.googleapis.com/css2?${googleFonts.join('&')}&display=swap" rel="stylesheet">`
|
||||
: '';
|
||||
}
|
||||
@@ -677,14 +684,14 @@ function generateGoogleFontsLink(config) {
|
||||
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;
|
||||
}
|
||||
@@ -702,7 +709,7 @@ function renderPage(pageId, config) {
|
||||
currentPage: pageId,
|
||||
pageId // 同时保留pageId字段,用于通用模板
|
||||
};
|
||||
|
||||
|
||||
// 确保navigation是数组
|
||||
if (!Array.isArray(config.navigation)) {
|
||||
console.warn('Warning: config.navigation is not an array in renderPage. Using empty array.');
|
||||
@@ -715,35 +722,35 @@ function renderPage(pageId, config) {
|
||||
isActive: nav.id === pageId,
|
||||
active: nav.id === pageId // 兼容原有逻辑
|
||||
};
|
||||
|
||||
|
||||
// 使用辅助函数获取子菜单
|
||||
const submenu = getSubmenuForNavItem(navItem, config);
|
||||
if (submenu) {
|
||||
navItem.submenu = submenu;
|
||||
}
|
||||
|
||||
|
||||
return navItem;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 确保socialLinks字段存在
|
||||
data.socialLinks = Array.isArray(config.social) ? config.social : [];
|
||||
|
||||
|
||||
// 确保navigationData可用(针对模板使用)
|
||||
data.navigationData = data.navigation;
|
||||
|
||||
|
||||
// 页面特定的额外数据
|
||||
if (config[pageId]) {
|
||||
Object.assign(data, config[pageId]);
|
||||
}
|
||||
|
||||
|
||||
// 检查页面配置中是否指定了模板
|
||||
let templateName = pageId;
|
||||
if (config[pageId] && config[pageId].template) {
|
||||
templateName = config[pageId].template;
|
||||
console.log(`页面 ${pageId} 使用指定模板: ${templateName}`);
|
||||
}
|
||||
|
||||
|
||||
// 直接渲染页面内容,不使用layout布局(因为layout会在generateHTML中统一应用)
|
||||
return renderTemplate(templateName, data, false);
|
||||
}
|
||||
@@ -756,27 +763,27 @@ function renderPage(pageId, config) {
|
||||
function generateAllPagesHTML(config) {
|
||||
// 页面内容集合
|
||||
const pages = {};
|
||||
|
||||
|
||||
// 渲染配置中定义的所有页面
|
||||
if (Array.isArray(config.navigation)) {
|
||||
config.navigation.forEach(navItem => {
|
||||
const pageId = navItem.id;
|
||||
|
||||
|
||||
// 渲染页面内容
|
||||
pages[pageId] = renderPage(pageId, config);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 确保首页存在
|
||||
if (!pages.home) {
|
||||
pages.home = renderPage('home', config);
|
||||
}
|
||||
|
||||
|
||||
// 确保搜索结果页存在
|
||||
if (!pages['search-results']) {
|
||||
pages['search-results'] = renderPage('search-results', config);
|
||||
}
|
||||
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
@@ -788,29 +795,29 @@ function generateAllPagesHTML(config) {
|
||||
function generateHTML(config) {
|
||||
// 获取所有页面内容
|
||||
const pages = generateAllPagesHTML(config);
|
||||
|
||||
|
||||
// 获取当前年份
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
|
||||
// 准备导航数据,添加submenu字段
|
||||
const navigationData = config.navigation.map(nav => {
|
||||
const navItem = { ...nav };
|
||||
|
||||
|
||||
// 使用辅助函数获取子菜单
|
||||
const submenu = getSubmenuForNavItem(navItem, config);
|
||||
if (submenu) {
|
||||
navItem.submenu = submenu;
|
||||
}
|
||||
|
||||
|
||||
return navItem;
|
||||
});
|
||||
|
||||
|
||||
// 准备Google Fonts链接
|
||||
const googleFontsLink = generateGoogleFontsLink(config);
|
||||
|
||||
|
||||
// 准备CSS字体变量
|
||||
const fontVariables = generateFontVariables(config);
|
||||
|
||||
|
||||
// 准备社交链接
|
||||
const socialLinks = generateSocialLinks(config.social);
|
||||
|
||||
@@ -824,13 +831,16 @@ function generateHTML(config) {
|
||||
currentYear,
|
||||
socialLinks,
|
||||
navigation: generateNavigation(config.navigation, config), // 兼容旧版
|
||||
social: Array.isArray(config.social) ? config.social : [] // 兼容旧版
|
||||
social: Array.isArray(config.social) ? config.social : [], // 兼容旧版
|
||||
|
||||
// 确保配置数据可用于浏览器扩展
|
||||
configJSON: config.configJSON // 从prepareRenderData函数中获取的配置数据
|
||||
};
|
||||
|
||||
|
||||
try {
|
||||
// 使用辅助函数获取默认布局模板
|
||||
const { template: layoutTemplate } = getDefaultLayoutTemplate();
|
||||
|
||||
|
||||
// 渲染模板
|
||||
return layoutTemplate(layoutData);
|
||||
} catch (error) {
|
||||
@@ -845,21 +855,21 @@ function copyStaticFiles(config) {
|
||||
if (!fs.existsSync('dist')) {
|
||||
fs.mkdirSync('dist', { recursive: true });
|
||||
}
|
||||
|
||||
|
||||
// 复制CSS文件
|
||||
try {
|
||||
fs.copyFileSync('assets/style.css', 'dist/style.css');
|
||||
} catch (e) {
|
||||
console.error('Error copying style.css:', e);
|
||||
}
|
||||
|
||||
|
||||
// 复制JavaScript文件
|
||||
try {
|
||||
fs.copyFileSync('src/script.js', 'dist/script.js');
|
||||
} catch (e) {
|
||||
console.error('Error copying script.js:', e);
|
||||
}
|
||||
|
||||
|
||||
// 如果配置了favicon,确保文件存在并复制
|
||||
if (config.site.favicon) {
|
||||
try {
|
||||
@@ -879,22 +889,22 @@ function copyStaticFiles(config) {
|
||||
// 主函数
|
||||
function main() {
|
||||
const config = loadConfig();
|
||||
|
||||
|
||||
try {
|
||||
// 确保dist目录存在
|
||||
if (!fs.existsSync('dist')) {
|
||||
fs.mkdirSync('dist', { recursive: true });
|
||||
}
|
||||
|
||||
|
||||
// 初始化Handlebars模板系统
|
||||
loadHandlebarsTemplates();
|
||||
|
||||
|
||||
// 使用generateHTML函数生成完整的HTML
|
||||
const htmlContent = generateHTML(config);
|
||||
|
||||
|
||||
// 生成HTML
|
||||
fs.writeFileSync('dist/index.html', htmlContent);
|
||||
|
||||
|
||||
// 复制静态文件
|
||||
copyStaticFiles(config);
|
||||
} catch (e) {
|
||||
@@ -903,7 +913,7 @@ function main() {
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
main();
|
||||
|
||||
// 导出供测试使用的函数
|
||||
module.exports = {
|
||||
|
||||
413
src/script.js
413
src/script.js
@@ -1,3 +1,185 @@
|
||||
// 全局MeNav对象 - 用于浏览器扩展
|
||||
window.MeNav = {
|
||||
version: "1.0.0",
|
||||
|
||||
// 获取配置数据
|
||||
getConfig: function() {
|
||||
const configData = document.getElementById('menav-config-data');
|
||||
return configData ? JSON.parse(configData.textContent) : null;
|
||||
},
|
||||
|
||||
// 更新DOM元素
|
||||
updateElement: function(id, newData) {
|
||||
const element = document.querySelector(`[data-menav-id="${id}"]`);
|
||||
if (!element) return false;
|
||||
|
||||
// 根据元素类型更新内容
|
||||
const type = element.getAttribute('data-menav-type');
|
||||
|
||||
if (type === 'site') {
|
||||
// 更新站点卡片
|
||||
if (newData.url) element.href = newData.url;
|
||||
if (newData.name) element.querySelector('h3').textContent = newData.name;
|
||||
if (newData.description) element.querySelector('p').textContent = newData.description;
|
||||
if (newData.icon) {
|
||||
const iconElement = element.querySelector('i');
|
||||
if (iconElement) {
|
||||
iconElement.className = newData.icon;
|
||||
}
|
||||
}
|
||||
if (newData.title) element.title = newData.title;
|
||||
|
||||
// 触发元素更新事件
|
||||
this.events.emit('elementUpdated', {
|
||||
id: id,
|
||||
type: 'site',
|
||||
data: newData
|
||||
});
|
||||
|
||||
return true;
|
||||
} else if (type === 'category') {
|
||||
// 更新分类
|
||||
if (newData.name) {
|
||||
const titleElement = element.querySelector('h2');
|
||||
if (titleElement) {
|
||||
// 保留图标
|
||||
const iconElement = titleElement.querySelector('i');
|
||||
const iconClass = iconElement ? iconElement.className : '';
|
||||
titleElement.innerHTML = `<i class="${newData.icon || iconClass}"></i> ${newData.name}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 触发元素更新事件
|
||||
this.events.emit('elementUpdated', {
|
||||
id: id,
|
||||
type: 'category',
|
||||
data: newData
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
// 添加新元素
|
||||
addElement: function(type, parentId, data) {
|
||||
const parent = document.querySelector(`[data-menav-id="${parentId}"]`);
|
||||
if (!parent) return null;
|
||||
|
||||
if (type === 'site' && parent.getAttribute('data-menav-type') === 'category') {
|
||||
// 添加站点卡片到分类
|
||||
const sitesGrid = parent.querySelector('.sites-grid');
|
||||
if (!sitesGrid) return null;
|
||||
|
||||
// 创建新的站点卡片
|
||||
const newSite = document.createElement('a');
|
||||
newSite.className = 'site-card';
|
||||
newSite.href = data.url || '#';
|
||||
newSite.title = data.name + (data.description ? ' - ' + data.description : '');
|
||||
const elementId = `site-new-${Date.now()}`;
|
||||
newSite.setAttribute('data-menav-id', elementId);
|
||||
newSite.setAttribute('data-menav-type', 'site');
|
||||
newSite.setAttribute('data-menav-category', parent.id);
|
||||
|
||||
// 添加内容
|
||||
newSite.innerHTML = `
|
||||
<i class="${data.icon || 'fas fa-link'}"></i>
|
||||
<h3>${data.name || '未命名站点'}</h3>
|
||||
<p>${data.description || ''}</p>
|
||||
`;
|
||||
|
||||
// 添加到DOM
|
||||
sitesGrid.appendChild(newSite);
|
||||
|
||||
// 移除"暂无网站"提示(如果存在)
|
||||
const emptyMessage = sitesGrid.querySelector('.empty-sites');
|
||||
if (emptyMessage) {
|
||||
emptyMessage.remove();
|
||||
}
|
||||
|
||||
// 触发元素添加事件
|
||||
this.events.emit('elementAdded', {
|
||||
id: elementId,
|
||||
type: 'site',
|
||||
parentId: parentId,
|
||||
data: data
|
||||
});
|
||||
|
||||
return elementId;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
// 删除元素
|
||||
removeElement: function(id) {
|
||||
const element = document.querySelector(`[data-menav-id="${id}"]`);
|
||||
if (!element) return false;
|
||||
|
||||
// 获取元素类型和分类(如果是站点卡片)
|
||||
const type = element.getAttribute('data-menav-type');
|
||||
const category = element.getAttribute('data-menav-category');
|
||||
|
||||
// 删除元素
|
||||
element.remove();
|
||||
|
||||
// 触发元素删除事件
|
||||
this.events.emit('elementRemoved', {
|
||||
id: id,
|
||||
type: type,
|
||||
category: category
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
// 获取所有元素
|
||||
getAllElements: function(type) {
|
||||
return Array.from(document.querySelectorAll(`[data-menav-type="${type}"]`)).map(el => {
|
||||
return {
|
||||
id: el.getAttribute('data-menav-id'),
|
||||
type: el.getAttribute('data-menav-type'),
|
||||
element: el
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// 事件系统
|
||||
events: {
|
||||
listeners: {},
|
||||
|
||||
// 添加事件监听器
|
||||
on: function(event, callback) {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = [];
|
||||
}
|
||||
this.listeners[event].push(callback);
|
||||
return this;
|
||||
},
|
||||
|
||||
// 触发事件
|
||||
emit: function(event, data) {
|
||||
if (this.listeners[event]) {
|
||||
this.listeners[event].forEach(callback => callback(data));
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
// 移除事件监听器
|
||||
off: function(event, callback) {
|
||||
if (this.listeners[event]) {
|
||||
if (callback) {
|
||||
this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
|
||||
} else {
|
||||
delete this.listeners[event];
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 先声明所有状态变量
|
||||
let isSearchActive = false;
|
||||
@@ -9,13 +191,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
let isSidebarCollapsed = false; // 侧边栏折叠状态
|
||||
let pages; // 页面元素的全局引用
|
||||
let currentSearchEngine = 'local'; // 当前选择的搜索引擎
|
||||
|
||||
|
||||
// 搜索索引,用于提高搜索效率
|
||||
let searchIndex = {
|
||||
initialized: false,
|
||||
items: []
|
||||
};
|
||||
|
||||
|
||||
// 搜索引擎配置
|
||||
const searchEngines = {
|
||||
local: {
|
||||
@@ -39,46 +221,46 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
url: 'https://www.baidu.com/s?wd='
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 获取DOM元素 - 基本元素
|
||||
const searchInput = document.getElementById('search');
|
||||
const searchBox = document.querySelector('.search-box');
|
||||
const searchResultsPage = document.getElementById('search-results');
|
||||
const searchSections = searchResultsPage.querySelectorAll('.search-section');
|
||||
|
||||
|
||||
// 搜索引擎相关元素
|
||||
const searchIcon = document.querySelector('.search-icon');
|
||||
const searchEngineDropdown = document.querySelector('.search-engine-dropdown');
|
||||
const searchEngineOptions = document.querySelectorAll('.search-engine-option');
|
||||
|
||||
|
||||
// 移动端元素
|
||||
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');
|
||||
|
||||
|
||||
// 侧边栏折叠功能
|
||||
const sidebarToggle = document.querySelector('.sidebar-toggle');
|
||||
const content = document.querySelector('.content');
|
||||
|
||||
|
||||
// 主题切换元素
|
||||
const themeToggle = document.querySelector('.theme-toggle');
|
||||
const themeIcon = themeToggle.querySelector('i');
|
||||
|
||||
|
||||
// 滚动进度条元素
|
||||
const scrollProgress = document.querySelector('.scroll-progress');
|
||||
|
||||
|
||||
// 移除预加载类,允许CSS过渡效果
|
||||
document.documentElement.classList.remove('preload');
|
||||
|
||||
|
||||
// 应用从localStorage读取的主题设置
|
||||
if (document.documentElement.classList.contains('theme-preload')) {
|
||||
document.documentElement.classList.remove('theme-preload');
|
||||
document.body.classList.add('light-theme');
|
||||
isLightTheme = true;
|
||||
}
|
||||
|
||||
|
||||
// 应用从localStorage读取的侧边栏状态
|
||||
if (document.documentElement.classList.contains('sidebar-collapsed-preload')) {
|
||||
document.documentElement.classList.remove('sidebar-collapsed-preload');
|
||||
@@ -86,30 +268,30 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
content.classList.add('expanded');
|
||||
isSidebarCollapsed = true;
|
||||
}
|
||||
|
||||
|
||||
// 即时移除loading类,确保侧边栏可见
|
||||
document.body.classList.remove('loading');
|
||||
document.body.classList.add('loaded');
|
||||
|
||||
|
||||
// 侧边栏折叠功能
|
||||
function toggleSidebarCollapse() {
|
||||
isSidebarCollapsed = !isSidebarCollapsed;
|
||||
|
||||
|
||||
// 使用 requestAnimationFrame 确保平滑过渡
|
||||
requestAnimationFrame(() => {
|
||||
sidebar.classList.toggle('collapsed', isSidebarCollapsed);
|
||||
content.classList.toggle('expanded', isSidebarCollapsed);
|
||||
|
||||
|
||||
// 保存折叠状态到localStorage
|
||||
localStorage.setItem('sidebarCollapsed', isSidebarCollapsed ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 初始化侧边栏折叠状态 - 已在页面加载前处理,此处仅完成图标状态初始化等次要任务
|
||||
function initSidebarState() {
|
||||
// 从localStorage获取侧边栏状态
|
||||
const savedState = localStorage.getItem('sidebarCollapsed');
|
||||
|
||||
|
||||
// 图标状态与折叠状态保持一致
|
||||
if (savedState === 'true' && !isMobile()) {
|
||||
isSidebarCollapsed = true;
|
||||
@@ -117,17 +299,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
isSidebarCollapsed = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 侧边栏折叠按钮点击事件
|
||||
if (sidebarToggle) {
|
||||
sidebarToggle.addEventListener('click', toggleSidebarCollapse);
|
||||
}
|
||||
|
||||
|
||||
// 主题切换功能
|
||||
function toggleTheme() {
|
||||
isLightTheme = !isLightTheme;
|
||||
document.body.classList.toggle('light-theme', isLightTheme);
|
||||
|
||||
|
||||
// 更新图标
|
||||
if (isLightTheme) {
|
||||
themeIcon.classList.remove('fa-moon');
|
||||
@@ -136,16 +318,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
themeIcon.classList.remove('fa-sun');
|
||||
themeIcon.classList.add('fa-moon');
|
||||
}
|
||||
|
||||
|
||||
// 保存主题偏好到localStorage
|
||||
localStorage.setItem('theme', isLightTheme ? 'light' : 'dark');
|
||||
}
|
||||
|
||||
|
||||
// 初始化主题 - 已在页面加载前处理,此处仅完成图标状态初始化等次要任务
|
||||
function initTheme() {
|
||||
// 从localStorage获取主题偏好
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
|
||||
|
||||
// 更新图标状态以匹配当前主题
|
||||
if (savedTheme === 'light') {
|
||||
isLightTheme = true;
|
||||
@@ -157,34 +339,34 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
themeIcon.classList.add('fa-moon');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 主题切换按钮点击事件
|
||||
themeToggle.addEventListener('click', toggleTheme);
|
||||
|
||||
|
||||
// 初始化搜索索引
|
||||
function initSearchIndex() {
|
||||
if (searchIndex.initialized) return;
|
||||
|
||||
|
||||
searchIndex.items = [];
|
||||
|
||||
|
||||
try {
|
||||
// 为每个页面创建索引
|
||||
if (!pages) {
|
||||
pages = document.querySelectorAll('.page');
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
@@ -201,7 +383,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
searchIndex.initialized = true;
|
||||
} catch (error) {
|
||||
console.error('Error initializing search index:', error);
|
||||
@@ -265,7 +447,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
sidebar.classList.remove('collapsed');
|
||||
content.classList.remove('expanded');
|
||||
}
|
||||
|
||||
|
||||
// 重新计算滚动进度
|
||||
updateScrollProgress();
|
||||
});
|
||||
@@ -277,17 +459,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const scrollPercent = (scrollTop / scrollHeight) * 100;
|
||||
scrollProgress.style.width = scrollPercent + '%';
|
||||
}
|
||||
|
||||
|
||||
// 监听内容区域的滚动事件
|
||||
content.addEventListener('scroll', updateScrollProgress);
|
||||
|
||||
|
||||
// 初始化时更新一次滚动进度
|
||||
updateScrollProgress();
|
||||
|
||||
// 页面切换功能
|
||||
function showPage(pageId, skipSearchReset = false) {
|
||||
if (currentPageId === pageId && !skipSearchReset && !isInitialLoad) return;
|
||||
|
||||
|
||||
currentPageId = pageId;
|
||||
|
||||
// 使用 RAF 确保动画流畅
|
||||
@@ -295,7 +477,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (!pages) {
|
||||
pages = document.querySelectorAll('.page');
|
||||
}
|
||||
|
||||
|
||||
pages.forEach(page => {
|
||||
const shouldBeActive = page.id === pageId;
|
||||
if (shouldBeActive !== page.classList.contains('active')) {
|
||||
@@ -309,11 +491,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
document.body.classList.add('loaded');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 重置滚动位置并更新进度条
|
||||
content.scrollTop = 0;
|
||||
updateScrollProgress();
|
||||
|
||||
|
||||
// 只有在非搜索状态下才重置搜索
|
||||
if (!skipSearchReset) {
|
||||
searchInput.value = '';
|
||||
@@ -327,7 +509,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (!searchIndex.initialized) {
|
||||
initSearchIndex();
|
||||
}
|
||||
|
||||
|
||||
searchTerm = searchTerm.toLowerCase().trim();
|
||||
|
||||
// 如果搜索框为空,重置所有内容
|
||||
@@ -344,12 +526,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// 使用搜索索引进行搜索
|
||||
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)) {
|
||||
@@ -384,13 +566,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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';
|
||||
}
|
||||
@@ -403,8 +585,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// 更新搜索结果页面状态
|
||||
const subtitle = searchResultsPage.querySelector('.subtitle');
|
||||
if (subtitle) {
|
||||
subtitle.textContent = hasResults
|
||||
? `在所有页面中找到 ${matchedItems.length} 个匹配项`
|
||||
subtitle.textContent = hasResults
|
||||
? `在所有页面中找到 ${matchedItems.length} 个匹配项`
|
||||
: '未找到匹配的结果';
|
||||
}
|
||||
|
||||
@@ -427,27 +609,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
console.error('Error performing search');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 高亮搜索匹配文本
|
||||
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) {
|
||||
@@ -457,43 +639,43 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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) {
|
||||
@@ -503,26 +685,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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);
|
||||
@@ -533,7 +715,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
console.error('Error highlighting search term');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 转义正则表达式特殊字符
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
@@ -570,7 +752,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const currentActiveNav = document.querySelector('.nav-item.active');
|
||||
if (currentActiveNav) {
|
||||
const targetPageId = currentActiveNav.getAttribute('data-page');
|
||||
|
||||
|
||||
if (targetPageId && currentPageId !== targetPageId) {
|
||||
currentPageId = targetPageId;
|
||||
pages.forEach(page => {
|
||||
@@ -626,60 +808,60 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (savedEngine && searchEngines[savedEngine]) {
|
||||
currentSearchEngine = savedEngine;
|
||||
}
|
||||
|
||||
|
||||
// 设置当前搜索引擎的激活状态及图标
|
||||
updateSearchEngineUI();
|
||||
|
||||
|
||||
// 初始化搜索引擎下拉菜单事件
|
||||
searchIcon.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
searchEngineDropdown.classList.toggle('active');
|
||||
});
|
||||
|
||||
|
||||
// 点击搜索引擎选项
|
||||
searchEngineOptions.forEach(option => {
|
||||
// 初始化激活状态
|
||||
if (option.getAttribute('data-engine') === currentSearchEngine) {
|
||||
option.classList.add('active');
|
||||
}
|
||||
|
||||
|
||||
option.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
|
||||
// 获取选中的搜索引擎
|
||||
const engine = option.getAttribute('data-engine');
|
||||
|
||||
|
||||
// 更新当前搜索引擎
|
||||
if (engine && searchEngines[engine]) {
|
||||
// 如果搜索引擎变更,且之前有活跃的本地搜索结果,重置搜索状态
|
||||
if (currentSearchEngine !== engine && isSearchActive) {
|
||||
resetSearch();
|
||||
}
|
||||
|
||||
|
||||
currentSearchEngine = engine;
|
||||
localStorage.setItem('searchEngine', engine);
|
||||
|
||||
|
||||
// 更新UI显示
|
||||
updateSearchEngineUI();
|
||||
|
||||
|
||||
// 关闭下拉菜单
|
||||
searchEngineDropdown.classList.remove('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// 点击页面其他位置关闭下拉菜单
|
||||
document.addEventListener('click', () => {
|
||||
searchEngineDropdown.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 更新搜索引擎UI显示
|
||||
function updateSearchEngineUI() {
|
||||
// 移除所有选项的激活状态
|
||||
searchEngineOptions.forEach(option => {
|
||||
option.classList.remove('active');
|
||||
|
||||
|
||||
// 如果是当前选中的搜索引擎,添加激活状态
|
||||
if (option.getAttribute('data-engine') === currentSearchEngine) {
|
||||
option.classList.add('active');
|
||||
@@ -691,7 +873,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// 清除所有类,保留基本的search-icon类
|
||||
const classList = searchIcon.className.split(' ').filter(cls => cls === 'search-icon');
|
||||
searchIcon.className = classList.join(' ');
|
||||
|
||||
|
||||
// 添加当前搜索引擎的图标类
|
||||
const engine = searchEngines[currentSearchEngine];
|
||||
if (engine) {
|
||||
@@ -699,17 +881,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
iconClasses.forEach(cls => {
|
||||
searchIcon.classList.add(cls);
|
||||
});
|
||||
|
||||
|
||||
// 更新标题提示
|
||||
searchIcon.setAttribute('title', engine.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 执行搜索(根据选择的搜索引擎)
|
||||
function executeSearch(searchTerm) {
|
||||
if (!searchTerm.trim()) return;
|
||||
|
||||
|
||||
// 根据当前搜索引擎执行搜索
|
||||
if (currentSearchEngine === 'local') {
|
||||
// 执行本地搜索
|
||||
@@ -754,27 +936,38 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const navItemWrappers = document.querySelectorAll('.nav-item-wrapper');
|
||||
const submenuItems = document.querySelectorAll('.submenu-item');
|
||||
pages = document.querySelectorAll('.page');
|
||||
|
||||
|
||||
// 初始化主题
|
||||
initTheme();
|
||||
|
||||
|
||||
// 初始化侧边栏状态
|
||||
initSidebarState();
|
||||
|
||||
|
||||
// 初始化搜索引擎选择
|
||||
initSearchEngine();
|
||||
|
||||
|
||||
// 初始化MeNav对象版本信息
|
||||
try {
|
||||
const config = window.MeNav.getConfig();
|
||||
if (config && config.version) {
|
||||
window.MeNav.version = config.version;
|
||||
console.log('MeNav API initialized with version:', config.version);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error initializing MeNav API:', error);
|
||||
}
|
||||
|
||||
// 立即执行初始化,不再使用requestAnimationFrame延迟
|
||||
// 显示首页
|
||||
showPage('home');
|
||||
|
||||
|
||||
// 添加载入动画
|
||||
categories.forEach((category, index) => {
|
||||
setTimeout(() => {
|
||||
category.style.opacity = '1';
|
||||
}, index * 100);
|
||||
});
|
||||
|
||||
|
||||
// 初始展开当前页面的子菜单
|
||||
const activeNavItem = document.querySelector('.nav-item.active');
|
||||
if (activeNavItem) {
|
||||
@@ -782,18 +975,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (activeWrapper) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 导航项点击效果
|
||||
navItems.forEach(item => {
|
||||
item.addEventListener('click', (e) => {
|
||||
if (item.getAttribute('target') === '_blank') return;
|
||||
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
// 获取当前项的父级wrapper
|
||||
const wrapper = item.closest('.nav-item-wrapper');
|
||||
const hasSubmenu = wrapper && wrapper.querySelector('.submenu');
|
||||
|
||||
|
||||
// 处理子菜单展开/折叠
|
||||
if (hasSubmenu) {
|
||||
// 如果点击的导航项已经激活且有子菜单,则切换子菜单展开状态
|
||||
@@ -806,12 +999,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
navWrapper.classList.remove('expanded');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 展开当前子菜单
|
||||
wrapper.classList.add('expanded');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 激活导航项
|
||||
navItems.forEach(nav => {
|
||||
nav.classList.toggle('active', nav === item);
|
||||
@@ -820,7 +1013,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const pageId = item.getAttribute('data-page');
|
||||
if (pageId) {
|
||||
showPage(pageId);
|
||||
|
||||
|
||||
// 在移动端视图下点击导航项后自动收起侧边栏
|
||||
if (isMobile() && isSidebarOpen && !hasSubmenu) {
|
||||
closeAllPanels();
|
||||
@@ -833,28 +1026,28 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
submenuItems.forEach(item => {
|
||||
item.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
// 获取页面ID和分类名称
|
||||
const pageId = item.getAttribute('data-page');
|
||||
const categoryName = item.getAttribute('data-category');
|
||||
|
||||
|
||||
if (pageId) {
|
||||
// 清除所有子菜单项的激活状态
|
||||
submenuItems.forEach(subItem => {
|
||||
subItem.classList.remove('active');
|
||||
});
|
||||
|
||||
|
||||
// 激活当前子菜单项
|
||||
item.classList.add('active');
|
||||
|
||||
|
||||
// 激活相应的导航项
|
||||
navItems.forEach(nav => {
|
||||
nav.classList.toggle('active', nav.getAttribute('data-page') === pageId);
|
||||
});
|
||||
|
||||
|
||||
// 显示对应页面
|
||||
showPage(pageId);
|
||||
|
||||
|
||||
// 等待页面切换完成后滚动到对应分类
|
||||
setTimeout(() => {
|
||||
// 查找目标分类元素
|
||||
@@ -863,24 +1056,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const targetCategory = Array.from(targetPage.querySelectorAll('.category h2')).find(
|
||||
heading => heading.textContent.trim().includes(categoryName)
|
||||
);
|
||||
|
||||
|
||||
if (targetCategory) {
|
||||
// 优化的滚动实现:滚动到使目标分类位于视口1/4处(更靠近顶部位置)
|
||||
try {
|
||||
// 直接获取所需元素和属性,减少重复查询
|
||||
const contentElement = document.querySelector('.content');
|
||||
|
||||
|
||||
if (contentElement && contentElement.scrollHeight > contentElement.clientHeight) {
|
||||
// 获取目标元素相对于内容区域的位置
|
||||
const rect = targetCategory.getBoundingClientRect();
|
||||
const containerRect = contentElement.getBoundingClientRect();
|
||||
|
||||
|
||||
// 计算目标应该在视口中的位置(视口高度的1/4处)
|
||||
const desiredPosition = containerRect.height / 4;
|
||||
|
||||
|
||||
// 计算需要滚动的位置
|
||||
const scrollPosition = contentElement.scrollTop + rect.top - containerRect.top - desiredPosition;
|
||||
|
||||
|
||||
// 执行滚动
|
||||
contentElement.scrollTo({
|
||||
top: scrollPosition,
|
||||
@@ -898,7 +1091,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
}
|
||||
}, 25); // 延迟时间
|
||||
|
||||
|
||||
// 在移动端视图下点击子菜单项后自动收起侧边栏
|
||||
if (isMobile() && isSidebarOpen) {
|
||||
closeAllPanels();
|
||||
@@ -906,7 +1099,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// 初始化搜索索引(使用requestIdleCallback或setTimeout延迟初始化,避免影响页面加载)
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(() => initSearchIndex());
|
||||
@@ -914,4 +1107,4 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
setTimeout(initSearchIndex, 1000);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user