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:
@@ -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 {
|
||||
|
||||
@@ -261,10 +261,16 @@ window.MeNav = {
|
||||
const newSite = document.createElement('a');
|
||||
newSite.className = siteCardStyle ? `site-card site-card-${siteCardStyle}` : 'site-card';
|
||||
|
||||
const siteName = data.name || '未命名站点';
|
||||
const siteUrl = data.url || '#';
|
||||
const siteIcon = data.icon || 'fas fa-link';
|
||||
const siteDescription = data.description || (data.url ? menavExtractDomain(data.url) : '');
|
||||
const siteName = data.name || '未命名站点';
|
||||
const siteUrl = data.url || '#';
|
||||
const siteIcon = data.icon || 'fas fa-link';
|
||||
const siteDescription = data.description || (data.url ? menavExtractDomain(data.url) : '');
|
||||
const siteFaviconUrl = data && data.faviconUrl ? String(data.faviconUrl).trim() : '';
|
||||
const siteForceIconModeRaw = data && data.forceIconMode ? String(data.forceIconMode).trim() : '';
|
||||
const siteForceIconMode =
|
||||
siteForceIconModeRaw === 'manual' || siteForceIconModeRaw === 'favicon'
|
||||
? siteForceIconModeRaw
|
||||
: '';
|
||||
|
||||
newSite.href = siteUrl;
|
||||
newSite.title = siteName + (siteDescription ? ' - ' + siteDescription : '');
|
||||
@@ -276,9 +282,11 @@ window.MeNav = {
|
||||
// 设置数据属性
|
||||
newSite.setAttribute('data-type', 'site');
|
||||
newSite.setAttribute('data-name', siteName);
|
||||
newSite.setAttribute('data-url', data.url || '');
|
||||
newSite.setAttribute('data-icon', siteIcon);
|
||||
newSite.setAttribute('data-description', siteDescription);
|
||||
newSite.setAttribute('data-url', data.url || '');
|
||||
newSite.setAttribute('data-icon', siteIcon);
|
||||
if (siteFaviconUrl) newSite.setAttribute('data-favicon-url', siteFaviconUrl);
|
||||
if (siteForceIconMode) newSite.setAttribute('data-force-icon-mode', siteForceIconMode);
|
||||
newSite.setAttribute('data-description', siteDescription);
|
||||
|
||||
// projects repo 风格:与模板中的 repo 结构保持一致(不走 favicon 逻辑)
|
||||
if (siteCardStyle === 'repo') {
|
||||
@@ -394,11 +402,46 @@ window.MeNav = {
|
||||
iconsMode = 'favicon';
|
||||
}
|
||||
|
||||
if (iconsMode === 'favicon' && data.url && /^https?:\/\//i.test(data.url)) {
|
||||
const faviconUrl = `https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent(data.url)}&size=32`;
|
||||
const shouldUseCustomFavicon = Boolean(siteFaviconUrl);
|
||||
const effectiveIconsMode = siteForceIconMode || iconsMode;
|
||||
|
||||
const iconContainer = document.createElement('div');
|
||||
iconContainer.className = 'icon-container';
|
||||
if (shouldUseCustomFavicon) {
|
||||
const iconContainer = document.createElement('div');
|
||||
iconContainer.className = 'icon-container';
|
||||
|
||||
const placeholder = document.createElement('i');
|
||||
placeholder.className = 'fas fa-circle-notch fa-spin icon-placeholder';
|
||||
placeholder.setAttribute('aria-hidden', 'true');
|
||||
|
||||
const fallback = document.createElement('i');
|
||||
fallback.className = `${siteIcon} icon-fallback`;
|
||||
fallback.setAttribute('aria-hidden', 'true');
|
||||
|
||||
const favicon = document.createElement('img');
|
||||
favicon.className = 'favicon-icon';
|
||||
favicon.src = siteFaviconUrl;
|
||||
favicon.alt = `${siteName} favicon`;
|
||||
favicon.loading = 'lazy';
|
||||
favicon.addEventListener('load', () => {
|
||||
favicon.classList.add('loaded');
|
||||
placeholder.classList.add('hidden');
|
||||
});
|
||||
favicon.addEventListener('error', () => {
|
||||
favicon.classList.add('error');
|
||||
placeholder.classList.add('hidden');
|
||||
fallback.classList.add('visible');
|
||||
});
|
||||
|
||||
iconContainer.appendChild(placeholder);
|
||||
iconContainer.appendChild(favicon);
|
||||
iconContainer.appendChild(fallback);
|
||||
iconWrapper.appendChild(iconContainer);
|
||||
} else if (effectiveIconsMode === 'favicon' && siteUrl && /^https?:\/\//i.test(siteUrl)) {
|
||||
const faviconUrlPrimary = `https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent(siteUrl)}&size=32`;
|
||||
const faviconUrlFallback = `https://t3.gstatic.cn/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent(siteUrl)}&size=32`;
|
||||
|
||||
const iconContainer = document.createElement('div');
|
||||
iconContainer.className = 'icon-container';
|
||||
|
||||
const placeholder = document.createElement('i');
|
||||
placeholder.className = 'fas fa-circle-notch fa-spin icon-placeholder';
|
||||
@@ -410,14 +453,21 @@ window.MeNav = {
|
||||
|
||||
const favicon = document.createElement('img');
|
||||
favicon.className = 'favicon-icon';
|
||||
favicon.src = faviconUrl;
|
||||
favicon.src = faviconUrlPrimary;
|
||||
favicon.alt = `${siteName} favicon`;
|
||||
favicon.loading = 'lazy';
|
||||
favicon.loading = 'lazy';
|
||||
let faviconFallbackTried = false;
|
||||
favicon.addEventListener('load', () => {
|
||||
favicon.classList.add('loaded');
|
||||
placeholder.classList.add('hidden');
|
||||
});
|
||||
favicon.addEventListener('error', () => {
|
||||
if (!faviconFallbackTried) {
|
||||
faviconFallbackTried = true;
|
||||
favicon.src = faviconUrlFallback;
|
||||
return;
|
||||
}
|
||||
|
||||
favicon.classList.add('error');
|
||||
placeholder.classList.add('hidden');
|
||||
fallback.classList.add('visible');
|
||||
@@ -425,14 +475,14 @@ window.MeNav = {
|
||||
|
||||
iconContainer.appendChild(placeholder);
|
||||
iconContainer.appendChild(favicon);
|
||||
iconContainer.appendChild(fallback);
|
||||
iconWrapper.appendChild(iconContainer);
|
||||
} else {
|
||||
const iconEl = document.createElement('i');
|
||||
iconEl.className = `${siteIcon} site-icon`;
|
||||
iconEl.setAttribute('aria-hidden', 'true');
|
||||
iconWrapper.appendChild(iconEl);
|
||||
}
|
||||
iconContainer.appendChild(fallback);
|
||||
iconWrapper.appendChild(iconContainer);
|
||||
} else {
|
||||
const iconEl = document.createElement('i');
|
||||
iconEl.className = `${siteIcon} site-icon`;
|
||||
iconEl.setAttribute('aria-hidden', 'true');
|
||||
iconWrapper.appendChild(iconEl);
|
||||
}
|
||||
|
||||
newSite.appendChild(iconWrapper);
|
||||
newSite.appendChild(contentWrapper);
|
||||
|
||||
Reference in New Issue
Block a user