feat(icons): 添加favicon模式,自动匹配图标

- 新增配置 icons.mode(manual | favicon),默认 favicon,未配置场景下自动生效
- 模板调用 t3.gstatic.com/faviconV2 获取站点图标;加载中显示旋转占位,失败回退至 Font Awesome 图标
- 新增 ifHttpUrl 与 encodeURIComponent,提升模板安全性与可读性
- 搜索索引优先读取 .icon-fallback,保证 favicon 模式下图标类名一致
- 样式新增 .favicon-icon 与 hover 效果,维持卡片观感一致性
This commit is contained in:
coolzr
2025-10-18 22:32:05 +08:00
parent 95398e074a
commit aa264cc727
6 changed files with 109 additions and 14 deletions

View File

@@ -919,11 +919,27 @@ body .content.expanded {
transition: all 0.3s ease; transition: all 0.3s ease;
} }
/* 网站卡片 favicon 图片样式,与图标尺寸保持一致 */
.site-card .favicon-icon {
display: inline-block;
width: 1.8rem;
height: 1.8rem;
margin-bottom: 1rem;
border-radius: 4px;
object-fit: cover;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.site-card:hover i { .site-card:hover i {
transform: scale(1.1); transform: scale(1.1);
color: var(--accent-hover); color: var(--accent-hover);
} }
.site-card:hover .favicon-icon {
transform: scale(1.1);
box-shadow: 0 0 0 1px var(--border-color);
}
.site-card h3 { .site-card h3 {
font-size: 1rem; font-size: 1rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;

View File

@@ -303,6 +303,10 @@ function ensureConfigDefaults(config) {
result.profile = result.profile || {}; result.profile = result.profile || {};
result.social = result.social || []; result.social = result.social || [];
result.categories = result.categories || []; result.categories = result.categories || [];
// 图标配置默认值
result.icons = result.icons || {};
// icons.mode: manual | favicon, 默认 favicon
result.icons.mode = result.icons.mode || 'favicon';
// 站点基本信息默认值 // 站点基本信息默认值
result.site.title = result.site.title || 'MeNav导航'; result.site.title = result.site.title || 'MeNav导航';

View File

@@ -159,6 +159,20 @@ function not(value, options) {
return !value ? options.fn(this) : options.inverse(this); return !value ? options.fn(this) : options.inverse(this);
} }
/**
* 判断URL是否为http/https
* @param {string} url 输入URL
* @param {object} options Handlebars选项
* @returns {string} 渲染结果
* @example {{#ifHttpUrl url}}...{{else}}...{{/ifHttpUrl}}
*/
function ifHttpUrl(url, options) {
if (typeof url === 'string' && /^https?:\/\//i.test(url)) {
return options.fn(this);
}
return options.inverse(this);
}
// 导出所有条件判断助手函数 // 导出所有条件判断助手函数
module.exports = { module.exports = {
ifEquals, ifEquals,
@@ -169,5 +183,6 @@ module.exports = {
and, and,
or, or,
orHelper, orHelper,
not not,
ifHttpUrl
}; };

View File

@@ -166,6 +166,21 @@ function keys(object) {
return Object.keys(object); return Object.keys(object);
} }
/**
* 对字符串进行URL组件编码encodeURIComponent
* @param {string} text 输入文本
* @returns {string} 编码后的字符串
* @example {{encodeURIComponent url}}
*/
function encodeURIComponentHelper(text) {
if (text === undefined || text === null) return '';
try {
return encodeURIComponent(String(text));
} catch (e) {
return '';
}
}
// 导出所有工具类助手函数 // 导出所有工具类助手函数
module.exports = { module.exports = {
slice, slice,
@@ -175,5 +190,6 @@ module.exports = {
last, last,
range, range,
pick, pick,
keys keys,
encodeURIComponent: encodeURIComponentHelper
}; };

View File

@@ -53,7 +53,8 @@ window.MeNav = {
element.setAttribute('data-description', newData.description); element.setAttribute('data-description', newData.description);
} }
if (newData.icon) { if (newData.icon) {
const iconElement = element.querySelector('i'); // 优先更新站点卡片中的回退图标favicon模式下存在
const iconElement = element.querySelector('i.icon-fallback') || element.querySelector('i');
if (iconElement) { if (iconElement) {
iconElement.className = newData.icon; iconElement.className = newData.icon;
} }
@@ -178,16 +179,41 @@ window.MeNav = {
newSite.setAttribute('data-icon', data.icon || 'fas fa-link'); newSite.setAttribute('data-icon', data.icon || 'fas fa-link');
newSite.setAttribute('data-description', data.description || ''); newSite.setAttribute('data-description', data.description || '');
// 添加内容 // 添加内容(根据图标模式渲染)
newSite.innerHTML = ` try {
<i class="${data.icon || 'fas fa-link'}"></i> const cfg = window.MeNav && typeof window.MeNav.getConfig === 'function' ? window.MeNav.getConfig() : null;
<h3>${data.name || '未命名站点'}</h3> const iconsMode = (cfg && (cfg.data?.icons?.mode || cfg.icons?.mode)) || 'favicon';
<p>${data.description || ''}</p> 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`;
newSite.innerHTML = `
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
<img class="favicon-icon" src="${faviconUrl}" alt="${(data.name || '站点')} favicon" loading="lazy" style="opacity:0;"
onload="this.style.opacity='1'; this.previousElementSibling.style.display='none';"
onerror="this.style.display='none'; this.previousElementSibling.style.display='none'; this.nextElementSibling.style.display='inline-block';" />
<i class="fas fa-link icon-fallback" aria-hidden="true" style="display:none;"></i>
<h3>${data.name || '未命名站点'}</h3>
<p>${data.description || ''}</p>
`;
} else {
newSite.innerHTML = `
<i class="${data.icon || 'fas fa-link'}"></i>
<h3>${data.name || '未命名站点'}</h3>
<p>${data.description || ''}</p>
`;
}
} catch (e) {
newSite.innerHTML = `
<i class="${data.icon || 'fas fa-link'}"></i>
<h3>${data.name || '未命名站点'}</h3>
<p>${data.description || ''}</p>
`;
}
// 添加到DOM // 添加到DOM
sitesGrid.appendChild(newSite); sitesGrid.appendChild(newSite);
// 移除"暂无网站"提示(如果存在) // 移除"暂无网站"提示(如果存在)
const emptyMessage = sitesGrid.querySelector('.empty-sites'); const emptyMessage = sitesGrid.querySelector('.empty-sites');
if (emptyMessage) { if (emptyMessage) {
@@ -504,7 +530,7 @@ document.addEventListener('DOMContentLoaded', () => {
const title = card.querySelector('h3')?.textContent?.toLowerCase() || ''; const title = card.querySelector('h3')?.textContent?.toLowerCase() || '';
const description = card.querySelector('p')?.textContent?.toLowerCase() || ''; const description = card.querySelector('p')?.textContent?.toLowerCase() || '';
const url = card.href || card.getAttribute('href') || '#'; const url = card.href || card.getAttribute('href') || '#';
const icon = card.querySelector('i')?.className || ''; const icon = card.querySelector('i.icon-fallback')?.className || card.querySelector('i')?.className || '';
// 将卡片信息添加到索引中 // 将卡片信息添加到索引中
searchIndex.items.push({ searchIndex.items.push({

View File

@@ -7,7 +7,25 @@
data-url="{{url}}" data-url="{{url}}"
data-icon="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}}" data-icon="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}}"
data-description="{{#if description}}{{description}}{{else}}{{url}}{{/if}}"> data-description="{{#if description}}{{description}}{{else}}{{url}}{{/if}}">
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}}"></i> {{#ifEquals @root.icons.mode "favicon"}}
{{#ifHttpUrl url}}
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
<img
class="favicon-icon"
src="https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url={{encodeURIComponent url}}&size=32"
alt="{{name}} favicon"
loading="lazy"
style="opacity:0;"
onload="this.style.opacity='1'; this.previousElementSibling.style.display='none';"
onerror="this.style.display='none'; this.previousElementSibling.style.display='none'; this.nextElementSibling.style.display='inline-block';"
/>
<i class="fas fa-link icon-fallback" aria-hidden="true" style="display:none;"></i>
{{else}}
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}}"></i>
{{/ifHttpUrl}}
{{else}}
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}}"></i>
{{/ifEquals}}
<h3>{{#if name}}{{name}}{{else}}未命名站点{{/if}}</h3> <h3>{{#if name}}{{name}}{{else}}未命名站点{{/if}}</h3>
<p>{{#if description}}{{description}}{{else}}{{url}}{{/if}}</p> <p>{{#if description}}{{description}}{{else}}{{url}}{{/if}}</p>
</a> </a>