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:
@@ -919,11 +919,27 @@ body .content.expanded {
|
||||
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 {
|
||||
transform: scale(1.1);
|
||||
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 {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
@@ -303,6 +303,10 @@ function ensureConfigDefaults(config) {
|
||||
result.profile = result.profile || {};
|
||||
result.social = result.social || [];
|
||||
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导航';
|
||||
|
||||
@@ -159,6 +159,20 @@ function not(value, options) {
|
||||
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 = {
|
||||
ifEquals,
|
||||
@@ -169,5 +183,6 @@ module.exports = {
|
||||
and,
|
||||
or,
|
||||
orHelper,
|
||||
not
|
||||
not,
|
||||
ifHttpUrl
|
||||
};
|
||||
@@ -166,6 +166,21 @@ function 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 = {
|
||||
slice,
|
||||
@@ -175,5 +190,6 @@ module.exports = {
|
||||
last,
|
||||
range,
|
||||
pick,
|
||||
keys
|
||||
keys,
|
||||
encodeURIComponent: encodeURIComponentHelper
|
||||
};
|
||||
@@ -53,7 +53,8 @@ window.MeNav = {
|
||||
element.setAttribute('data-description', newData.description);
|
||||
}
|
||||
if (newData.icon) {
|
||||
const iconElement = element.querySelector('i');
|
||||
// 优先更新站点卡片中的回退图标(favicon模式下存在)
|
||||
const iconElement = element.querySelector('i.icon-fallback') || element.querySelector('i');
|
||||
if (iconElement) {
|
||||
iconElement.className = newData.icon;
|
||||
}
|
||||
@@ -178,16 +179,41 @@ window.MeNav = {
|
||||
newSite.setAttribute('data-icon', data.icon || 'fas fa-link');
|
||||
newSite.setAttribute('data-description', data.description || '');
|
||||
|
||||
// 添加内容
|
||||
newSite.innerHTML = `
|
||||
<i class="${data.icon || 'fas fa-link'}"></i>
|
||||
<h3>${data.name || '未命名站点'}</h3>
|
||||
<p>${data.description || ''}</p>
|
||||
`;
|
||||
// 添加内容(根据图标模式渲染)
|
||||
try {
|
||||
const cfg = window.MeNav && typeof window.MeNav.getConfig === 'function' ? window.MeNav.getConfig() : null;
|
||||
const iconsMode = (cfg && (cfg.data?.icons?.mode || cfg.icons?.mode)) || '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`;
|
||||
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
|
||||
sitesGrid.appendChild(newSite);
|
||||
|
||||
|
||||
|
||||
// 移除"暂无网站"提示(如果存在)
|
||||
const emptyMessage = sitesGrid.querySelector('.empty-sites');
|
||||
if (emptyMessage) {
|
||||
@@ -504,7 +530,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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 || '';
|
||||
const icon = card.querySelector('i.icon-fallback')?.className || card.querySelector('i')?.className || '';
|
||||
|
||||
// 将卡片信息添加到索引中
|
||||
searchIndex.items.push({
|
||||
|
||||
@@ -7,7 +7,25 @@
|
||||
data-url="{{url}}"
|
||||
data-icon="{{#if icon}}{{icon}}{{else}}fas fa-link{{/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>
|
||||
<p>{{#if description}}{{description}}{{else}}{{url}}{{/if}}</p>
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user