feat: 新增 icons.region 配置项&修改 favicon 加载超时机制&修复去除硬编码

- 新增 icons.region: com | cn 配置项,允许用户选择优先使用国内源或国外源
  - com: 优先 gstatic.com,失败回退 gstatic.cn
  - cn: 优先 gstatic.cn,失败回退 gstatic.com
- 修改 favicon 加载超时判断机制
  - 自定义 faviconUrl: 5秒超时后显示回退图标
  - 自动 favicon: 每次尝试3秒超时,最多等待6秒
- 更新配置文档和默认配置示例
- 去除卡片模板中的url硬编码
Issue: #31
This commit is contained in:
rbetree
2026-01-03 16:43:50 +08:00
parent 87b4cea290
commit 3473aaebd7
7 changed files with 127 additions and 13 deletions

View File

@@ -307,6 +307,8 @@ function ensureConfigDefaults(config) {
result.icons = result.icons || {};
// icons.mode: manual | favicon, 默认 favicon
result.icons.mode = result.icons.mode || 'favicon';
// icons.region: com | cn, 默认 com优先使用 gstatic.com失败后回退到 gstatic.cn
result.icons.region = result.icons.region || 'com';
// 站点基本信息默认值
result.site.title = result.site.title || 'MeNav导航';

View File

@@ -194,6 +194,48 @@ function add(a, b) {
return numA + numB;
}
/**
* 根据 icons.region 配置生成 favicon URL
* @param {string} url 站点 URL
* @param {Object} options Handlebars options 对象
* @returns {string} favicon URL
* @example {{faviconUrl url}}
*/
function faviconUrl(url, options) {
if (!url) return '';
const region = options.data.root.icons?.region || 'com';
const domain = region === 'cn' ? 't3.gstatic.cn' : 't3.gstatic.com';
try {
const encodedUrl = encodeURIComponent(String(url));
return `https://${domain}/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodedUrl}&size=32`;
} catch (e) {
return '';
}
}
/**
* 根据 icons.region 配置生成 favicon 回退 URL
* @param {string} url 站点 URL
* @param {Object} options Handlebars options 对象
* @returns {string} favicon 回退 URL
* @example {{faviconFallbackUrl url}}
*/
function faviconFallbackUrl(url, options) {
if (!url) return '';
const region = options.data.root.icons?.region || 'com';
const domain = region === 'cn' ? 't3.gstatic.com' : 't3.gstatic.cn';
try {
const encodedUrl = encodeURIComponent(String(url));
return `https://${domain}/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodedUrl}&size=32`;
} catch (e) {
return '';
}
}
// 导出所有工具类助手函数
module.exports = {
slice,
@@ -205,5 +247,7 @@ module.exports = {
pick,
keys,
encodeURIComponent: encodeURIComponentHelper,
add
add,
faviconUrl,
faviconFallbackUrl
};

View File

@@ -392,14 +392,17 @@ window.MeNav = {
contentWrapper.appendChild(descEl);
let iconsMode = 'favicon';
let iconsRegion = 'com';
try {
const cfg =
window.MeNav && typeof window.MeNav.getConfig === 'function'
? window.MeNav.getConfig()
: null;
iconsMode = (cfg && (cfg.data?.icons?.mode || cfg.icons?.mode)) || 'favicon';
iconsRegion = (cfg && (cfg.data?.icons?.region || cfg.icons?.region)) || 'com';
} catch (e) {
iconsMode = 'favicon';
iconsRegion = 'com';
}
const shouldUseCustomFavicon = Boolean(siteFaviconUrl);
@@ -422,11 +425,23 @@ window.MeNav = {
favicon.src = siteFaviconUrl;
favicon.alt = `${siteName} favicon`;
favicon.loading = 'lazy';
// 超时处理5秒后如果还没加载成功显示回退图标
let loadTimeout = setTimeout(() => {
if (!favicon.classList.contains('loaded')) {
favicon.classList.add('error');
placeholder.classList.add('hidden');
fallback.classList.add('visible');
}
}, 5000);
favicon.addEventListener('load', () => {
clearTimeout(loadTimeout);
favicon.classList.add('loaded');
placeholder.classList.add('hidden');
});
favicon.addEventListener('error', () => {
clearTimeout(loadTimeout);
favicon.classList.add('error');
placeholder.classList.add('hidden');
fallback.classList.add('visible');
@@ -437,8 +452,13 @@ window.MeNav = {
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`;
// 根据 icons.region 配置决定优先使用哪个域名
const faviconUrlPrimary = iconsRegion === 'cn'
? `https://t3.gstatic.cn/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent(siteUrl)}&size=32`
: `https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent(siteUrl)}&size=32`;
const faviconUrlFallback = iconsRegion === 'cn'
? `https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent(siteUrl)}&size=32`
: `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';
@@ -457,14 +477,38 @@ window.MeNav = {
favicon.alt = `${siteName} favicon`;
favicon.loading = 'lazy';
let faviconFallbackTried = false;
let loadTimeout = null;
// 超时处理3秒后如果还没加载成功尝试回退 URL 或显示 Font Awesome 图标
const startTimeout = () => {
if (loadTimeout) clearTimeout(loadTimeout);
loadTimeout = setTimeout(() => {
if (!favicon.classList.contains('loaded')) {
if (!faviconFallbackTried) {
faviconFallbackTried = true;
favicon.src = faviconUrlFallback;
startTimeout(); // 为 fallback URL 也设置超时
} else {
favicon.classList.add('error');
placeholder.classList.add('hidden');
fallback.classList.add('visible');
}
}
}, 3000);
};
startTimeout();
favicon.addEventListener('load', () => {
if (loadTimeout) clearTimeout(loadTimeout);
favicon.classList.add('loaded');
placeholder.classList.add('hidden');
});
favicon.addEventListener('error', () => {
if (loadTimeout) clearTimeout(loadTimeout);
if (!faviconFallbackTried) {
faviconFallbackTried = true;
favicon.src = faviconUrlFallback;
startTimeout(); // 为 fallback URL 也设置超时
return;
}