const { menavExtractDomain, menavSanitizeClassList, menavSanitizeUrl } = require('../shared'); // 添加新元素 module.exports = function addElement(type, parentId, data) { if (type === 'site') { // 查找父级分类 const parent = document.querySelector(`[data-type="category"][data-name="${parentId}"]`); if (!parent) return null; // 添加站点卡片到分类 const sitesContainer = parent.querySelector('[data-container="sites"]'); if (!sitesContainer) return null; // 站点卡片样式:根据“页面模板”决定(friends/articles/projects 等) let siteCardStyle = ''; try { const pageEl = parent.closest('.page'); const pageId = pageEl && pageEl.id ? String(pageEl.id).trim() : ''; const cfg = window.MeNav && typeof window.MeNav.getConfig === 'function' ? window.MeNav.getConfig() : null; let templateName = ''; const pageTemplates = cfg && cfg.data && cfg.data.pageTemplates && typeof cfg.data.pageTemplates === 'object' ? cfg.data.pageTemplates : null; const templateFromMap = pageTemplates && pageId && pageTemplates[pageId] ? String(pageTemplates[pageId]).trim() : ''; // 兼容旧版:cfg.data[pageId].template const legacyPageConfig = cfg && cfg.data && pageId ? cfg.data[pageId] : null; const templateFromLegacy = legacyPageConfig && legacyPageConfig.template ? String(legacyPageConfig.template).trim() : ''; if (templateFromMap) { templateName = templateFromMap; } else if (templateFromLegacy) { templateName = templateFromLegacy; } // projects 模板使用代码仓库风格卡片(与生成端 templates/components/site-card.hbs 保持一致) if (templateName === 'projects') siteCardStyle = 'repo'; } catch (e) { siteCardStyle = ''; } // 创建新的站点卡片 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 siteFaviconUrl = data && data.faviconUrl ? String(data.faviconUrl).trim() : ''; const siteForceIconModeRaw = data && data.forceIconMode ? String(data.forceIconMode).trim() : ''; const siteForceIconMode = siteForceIconModeRaw === 'manual' || siteForceIconModeRaw === 'favicon' ? siteForceIconModeRaw : ''; const safeSiteUrl = menavSanitizeUrl(siteUrl, 'addElement(site).url'); const safeSiteIcon = menavSanitizeClassList(siteIcon, 'addElement(site).icon'); newSite.setAttribute('href', safeSiteUrl); newSite.title = siteName + (siteDescription ? ' - ' + siteDescription : ''); newSite.setAttribute( 'data-tooltip', siteName + (siteDescription ? ' - ' + siteDescription : '') ); if (/^https?:\/\//i.test(safeSiteUrl)) { newSite.target = '_blank'; newSite.rel = 'noopener'; } // 设置数据属性 newSite.setAttribute('data-type', 'site'); newSite.setAttribute('data-name', siteName); // 保留原始 URL(data-url)供扩展/调试读取;href 仍会做安全降级 newSite.setAttribute('data-url', String(data.url || '').trim()); newSite.setAttribute('data-icon', safeSiteIcon); 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') { const repoHeader = document.createElement('div'); repoHeader.className = 'repo-header'; const repoIcon = document.createElement('i'); repoIcon.className = `${safeSiteIcon || 'fas fa-code'} repo-icon`; repoIcon.setAttribute('aria-hidden', 'true'); const repoTitle = document.createElement('div'); repoTitle.className = 'repo-title'; repoTitle.textContent = siteName; repoHeader.appendChild(repoIcon); repoHeader.appendChild(repoTitle); const repoDesc = document.createElement('div'); repoDesc.className = 'repo-desc'; repoDesc.textContent = siteDescription; newSite.appendChild(repoHeader); newSite.appendChild(repoDesc); const hasStats = data && (data.language || data.stars || data.forks || data.issues); if (hasStats) { const repoStats = document.createElement('div'); repoStats.className = 'repo-stats'; if (data.language) { const languageItem = document.createElement('div'); languageItem.className = 'stat-item'; const langDot = document.createElement('span'); langDot.className = 'lang-dot'; langDot.style.backgroundColor = data.languageColor || '#909296'; languageItem.appendChild(langDot); languageItem.appendChild(document.createTextNode(String(data.language))); repoStats.appendChild(languageItem); } if (data.stars) { const starsItem = document.createElement('div'); starsItem.className = 'stat-item'; const starIcon = document.createElement('i'); starIcon.className = 'far fa-star'; starIcon.setAttribute('aria-hidden', 'true'); starsItem.appendChild(starIcon); starsItem.appendChild(document.createTextNode(String(data.stars))); repoStats.appendChild(starsItem); } if (data.forks) { const forksItem = document.createElement('div'); forksItem.className = 'stat-item'; const forkIcon = document.createElement('i'); forkIcon.className = 'fas fa-code-branch'; forkIcon.setAttribute('aria-hidden', 'true'); forksItem.appendChild(forkIcon); forksItem.appendChild(document.createTextNode(String(data.forks))); repoStats.appendChild(forksItem); } if (data.issues) { const issuesItem = document.createElement('div'); issuesItem.className = 'stat-item'; const issueIcon = document.createElement('i'); issueIcon.className = 'fas fa-exclamation-circle'; issueIcon.setAttribute('aria-hidden', 'true'); issuesItem.appendChild(issueIcon); issuesItem.appendChild(document.createTextNode(String(data.issues))); repoStats.appendChild(issuesItem); } newSite.appendChild(repoStats); } } else { // 普通站点卡片:复用现有结构(支持 favicon) const siteCardIcon = document.createElement('div'); siteCardIcon.className = 'site-card-icon'; const iconEl = document.createElement('i'); iconEl.className = safeSiteIcon || 'fas fa-link'; iconEl.setAttribute('aria-hidden', 'true'); // 添加内容(根据图标模式渲染,避免 innerHTML 注入) siteCardIcon.appendChild(iconEl); const titleEl = document.createElement('h3'); titleEl.textContent = siteName; const descEl = document.createElement('p'); descEl.textContent = siteDescription; newSite.appendChild(siteCardIcon); newSite.appendChild(titleEl); newSite.appendChild(descEl); // favicon 模式:优先加载 faviconUrl;否则按 url 生成 try { const cfg = window.MeNav && typeof window.MeNav.getConfig === 'function' ? window.MeNav.getConfig() : null; const iconsMode = cfg && cfg.icons && cfg.icons.mode ? String(cfg.icons.mode).trim() : 'favicon'; const iconsRegion = cfg && cfg.icons && cfg.icons.region ? String(cfg.icons.region).trim() : 'com'; const forceMode = siteForceIconMode || ''; const shouldUseFavicon = forceMode ? forceMode === 'favicon' : iconsMode === 'favicon'; if (shouldUseFavicon) { const faviconImg = document.createElement('img'); faviconImg.className = 'site-icon'; faviconImg.loading = 'lazy'; faviconImg.alt = siteName; const fallbackToIcon = () => { faviconImg.remove(); iconEl.classList.add('icon-fallback'); }; // 超时处理:5秒后如果还没加载成功,显示回退图标 const timeoutId = setTimeout(() => { fallbackToIcon(); }, 5000); faviconImg.onerror = () => { clearTimeout(timeoutId); fallbackToIcon(); }; faviconImg.onload = () => { clearTimeout(timeoutId); iconEl.remove(); }; if (siteFaviconUrl) { faviconImg.src = siteFaviconUrl; } else { const urlToUse = String(data.url || '').trim(); if (urlToUse) { // 根据 icons.region 配置决定优先使用哪个域名 const urls = []; // drop_404_icon=true:缺失 favicon 时返回空 404,避免占位图导致 onerror 不触发,从而可靠走回退逻辑 const comUrl = `https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent( urlToUse )}&size=32&drop_404_icon=true`; const cnUrl = `https://t3.gstatic.cn/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent( urlToUse )}&size=32&drop_404_icon=true`; if (iconsRegion === 'cn') { urls.push(cnUrl, comUrl); } else { urls.push(comUrl, cnUrl); } let idx = 0; faviconImg.src = urls[idx]; // 超时处理:3秒后如果还没加载成功,尝试回退 URL 或显示 Font Awesome 图标 const fallbackTimeoutId = setTimeout(() => { idx += 1; if (idx < urls.length) { faviconImg.src = urls[idx]; } else { fallbackToIcon(); } }, 3000); const cleanup = () => clearTimeout(fallbackTimeoutId); faviconImg.onload = () => { clearTimeout(timeoutId); cleanup(); iconEl.remove(); }; faviconImg.onerror = () => { clearTimeout(timeoutId); cleanup(); idx += 1; if (idx < urls.length) { faviconImg.src = urls[idx]; } else { fallbackToIcon(); } }; } } siteCardIcon.insertBefore(faviconImg, iconEl); } else { iconEl.classList.add('icon-fallback'); } } catch (e) { iconEl.classList.add('icon-fallback'); } } // 添加到 DOM sitesContainer.appendChild(newSite); // 移除“暂无网站”提示(如果存在) const emptySites = sitesContainer.querySelector('.empty-sites'); if (emptySites) { emptySites.remove(); } // 触发元素添加事件 this.events.emit('elementAdded', { id: siteName, type: 'site', parentId: parentId, data: data, }); return siteName; } else if (type === 'category') { // 查找父级页面容器 const parent = document.querySelector(`[data-page="${parentId}"]`); if (!parent) return null; // 创建新的分类 const newCategory = document.createElement('section'); newCategory.className = 'category'; newCategory.setAttribute('data-type', 'category'); newCategory.setAttribute('data-name', data.name || '未命名分类'); if (data.icon) { newCategory.setAttribute( 'data-icon', menavSanitizeClassList(data.icon, 'addElement(category).data-icon') ); } // 设置数据属性 newCategory.setAttribute('data-level', '1'); // 添加内容(用 DOM API 构建,避免 innerHTML 注入) const titleEl = document.createElement('h2'); const iconEl = document.createElement('i'); iconEl.className = menavSanitizeClassList( data.icon || 'fas fa-folder', 'addElement(category).icon' ); titleEl.appendChild(iconEl); titleEl.appendChild(document.createTextNode(' ' + String(data.name || '未命名分类'))); const sitesGrid = document.createElement('div'); sitesGrid.className = 'sites-grid'; sitesGrid.setAttribute('data-container', 'sites'); const emptyEl = document.createElement('p'); emptyEl.className = 'empty-sites'; emptyEl.textContent = '暂无网站'; sitesGrid.appendChild(emptyEl); newCategory.appendChild(titleEl); newCategory.appendChild(sitesGrid); // 添加到 DOM parent.appendChild(newCategory); // 触发元素添加事件 this.events.emit('elementAdded', { id: data.name, type: 'category', data: data, }); return data.name; } return null; };