fix: 修复外部资源、图标模式与嵌套交互(#30)

Fixes: https://github.com/rbetree/menav/issues/30

- Font Awesome:bootcdn→Cloudflare cdnjs
- favicon:gstatic `.com` 失败自动回退 `.cn`
- `icons.mode`:修复 `site.yml` 配置未生效(提升到顶层)
- 站点级图标覆盖:支持 `faviconUrl` / `forceIconMode`(优先级:`faviconUrl` > `forceIconMode` > `icons.mode`)
- 折叠交互:恢复二级分组折叠按钮(桌面端悬停显示)
- 新标签页:递归补齐多级 `sites.external` 默认值
This commit is contained in:
rbetree
2026-01-02 14:58:53 +08:00
parent d2ceeb674f
commit 30d50ebcd7
13 changed files with 613 additions and 97 deletions

View File

@@ -248,6 +248,8 @@ function loadModularConfig(dirPath) {
if (siteConfig.fonts) config.fonts = siteConfig.fonts;
if (siteConfig.profile) config.profile = siteConfig.profile;
if (siteConfig.social) config.social = siteConfig.social;
// 图标配置icons.mode需要作为顶层字段供模板/运行时读取
if (siteConfig.icons) config.icons = siteConfig.icons;
// 优先使用site.yml中的navigation配置
if (siteConfig.navigation) {
@@ -334,11 +336,24 @@ function ensureConfigDefaults(config) {
site.external = typeof site.external === 'boolean' ? site.external : true;
}
// 递归处理多级结构categories/subcategories/groups/subgroups下的 sites 默认值
function processNodeSitesRecursively(node) {
if (!node || typeof node !== 'object') return;
if (Array.isArray(node.sites)) {
node.sites.forEach(processSiteDefaults);
}
if (Array.isArray(node.subcategories)) node.subcategories.forEach(processNodeSitesRecursively);
if (Array.isArray(node.groups)) node.groups.forEach(processNodeSitesRecursively);
if (Array.isArray(node.subgroups)) node.subgroups.forEach(processNodeSitesRecursively);
}
// 处理分类默认值的辅助函数
function processCategoryDefaults(category) {
category.name = category.name || '未命名分类';
category.sites = category.sites || [];
category.sites.forEach(processSiteDefaults);
processNodeSitesRecursively(category);
}
// 为所有页面配置中的类别和站点设置默认值
@@ -1465,6 +1480,62 @@ function copyStaticFiles(config) {
console.error('Error copying script.js:', e);
}
// faviconUrl站点级自定义图标若使用本地路径建议以 assets/ 开头),则复制到 dist 下同路径
try {
const copied = new Set();
const copyLocalAsset = (rawUrl) => {
const raw = String(rawUrl || '').trim();
if (!raw) return;
if (/^https?:\/\//i.test(raw)) return;
const rel = raw.replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\//, '');
if (!rel.startsWith('assets/')) return;
const normalized = path.posix.normalize(rel);
if (!normalized.startsWith('assets/')) return;
if (copied.has(normalized)) return;
copied.add(normalized);
const srcPath = path.join(process.cwd(), normalized);
const destPath = path.join(process.cwd(), 'dist', normalized);
if (!fs.existsSync(srcPath)) {
console.warn(`[WARN] faviconUrl 本地文件不存在:${normalized}`);
return;
}
fs.mkdirSync(path.dirname(destPath), { recursive: true });
fs.copyFileSync(srcPath, destPath);
};
if (config && Array.isArray(config.navigation)) {
config.navigation.forEach(navItem => {
const pageId = navItem && navItem.id ? String(navItem.id) : '';
if (!pageId) return;
const pageConfig = config[pageId];
if (!pageConfig || typeof pageConfig !== 'object') return;
if (Array.isArray(pageConfig.sites)) {
pageConfig.sites.forEach(site => {
if (!site || typeof site !== 'object') return;
copyLocalAsset(site.faviconUrl);
});
}
if (Array.isArray(pageConfig.categories)) {
const sites = [];
pageConfig.categories.forEach(category => collectSitesRecursively(category, sites));
sites.forEach(site => {
if (!site || typeof site !== 'object') return;
copyLocalAsset(site.faviconUrl);
});
}
});
}
} catch (e) {
console.error('Error copying faviconUrl assets:', e);
}
// 如果配置了favicon确保文件存在并复制
if (config.site.favicon) {
try {