/** * ============================================ * 📦 推荐静态资源模块 (popular.js) * 功能:常用CSS/JS库展示、版本检测、多CDN链接生成 * 作者:gentpan * 更新:2026-01-03 * ============================================ */ import { config } from './config.js'; // ====================== // 基础配置与常量 // ====================== const CACHE_TTL = 5 * 60 * 1000; // 缓存有效期:5分钟 const versionCache = new Map(); const DEFAULT_TIMEOUT = 4000; // 请求超时:4 秒 const MIN_LOADING_MS = 1500; // 加载最少展示时长 const POPULAR_CACHE_URL = '/data/popular-cache.json'; const JSDELIVR_PACKAGE_API = 'https://data.jsdelivr.com/v1/package/npm'; const loadingSpinnerSvg = ` `; async function fetchWithTimeout(url, options = {}, timeout = DEFAULT_TIMEOUT) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { ...options, signal: controller.signal }); return response; } finally { clearTimeout(timer); } } async function fetchPopularCache() { try { const response = await fetchWithTimeout(POPULAR_CACHE_URL, { cache: 'no-cache' }, 1200); if (!response.ok) return null; const data = await response.json(); if (!data || !Array.isArray(data.css) || !Array.isArray(data.js)) { return null; } return data; } catch (error) { console.warn('[popular] 本地热门资源缓存不可用,回退在线加载:', error); return null; } } async function getCachedPopularLibraries(type) { const cache = await fetchPopularCache(); if (!cache) return null; const items = type === 'js' ? cache.js : cache.css; return items.length ? items : null; } function showLoading(container, type) { container.innerHTML = `
${loadingSpinnerSvg} 正在加载热门${type === 'js' ? 'JavaScript' : 'CSS'}库...
`; } function renderLibraryCards(container, libraries, { enableAutoUpdate = false } = {}) { const fragment = document.createDocumentFragment(); libraries.forEach(lib => { fragment.appendChild(createCssCard(lib)); }); container.innerHTML = ''; container.appendChild(fragment); if (enableAutoUpdate) { startAutoVersionUpdate(libraries, container); } } /** * 常用静态资源库元数据配置 * @constant {Array} */ export const popularCssLibraries = Object.freeze([ { id: 'bootstrap-css', name: 'Bootstrap CSS', package: 'bootstrap', file: 'dist/css/bootstrap.min.css', cdnjsFile: 'css/bootstrap.min.css', type: 'css', icon: 'fa-brands fa-bootstrap', description: '最流行的CSS框架', tags: ['响应式', '组件', 'CSS'] }, { id: 'tailwindcss', name: 'Tailwind CSS', package: 'tailwindcss', file: 'dist/tailwind.min.css', type: 'css', icon: 'fa-solid fa-wind', description: '原子化 CSS 框架', tags: ['原子化', '工具类', 'CSS'] }, { id: 'bulma', name: 'Bulma', package: 'bulma', file: 'css/bulma.min.css', type: 'css', icon: 'fa-solid fa-cube', description: '现代响应式 CSS 框架', tags: ['响应式', 'Flexbox', 'CSS'] }, { id: 'uikit', name: 'UIkit', package: 'uikit', file: 'dist/css/uikit.min.css', type: 'css', icon: 'fa-solid fa-layer-group', description: '轻量 UI 组件库', tags: ['组件', 'UI', 'CSS'] }, { id: 'fontawesome', name: 'Font Awesome Pro', package: '@fortawesome/fontawesome-free', fixedUrl: 'https://static.bluecdn.com/libs/fontawesome/7.3.0/css/all.min.css', fixedVersion: '7.3.0', cdnjsPackage: 'font-awesome', cdnjsVersion: '7.3.0', file: 'css/all.min.css', cdnjsFile: 'css/all.min.css', type: 'css', icon: 'fa-brands fa-font-awesome', description: 'Pro 矢量图标库', tags: ['Pro', '图标', 'CSS'] }, { id: 'animate', name: 'Animate.css', package: 'animate.css', file: 'animate.min.css', cdnjsFile: 'animate.min.css', type: 'css', icon: 'fa-solid fa-wand-magic-sparkles', description: '即用型动画库', tags: ['动画', 'CSS'] }, { id: 'swiper-css', name: 'Swiper CSS', package: 'swiper', file: 'swiper-bundle.min.css', cdnjsFile: 'swiper-bundle.min.css', type: 'css', icon: 'fa-solid fa-images', description: '现代触摸滑块样式', tags: ['轮播', 'CSS'] }, { id: 'normalize', name: 'Normalize.css', package: 'normalize.css', file: 'normalize.css', type: 'css', icon: 'fa-solid fa-text-height', description: '跨浏览器样式一致性', tags: ['重置', 'CSS'] }, { id: 'highlightjs-css', name: 'highlight.js Theme', package: 'highlight.js', file: 'styles/github.min.css', type: 'css', icon: 'fa-solid fa-highlighter', description: '代码高亮主题样式', tags: ['代码高亮', 'CSS'] }, { id: 'pico', name: 'Pico.css', package: '@picocss/pico', file: 'css/pico.min.css', type: 'css', icon: 'fa-solid fa-feather', description: '极简语义化 CSS 框架', tags: ['极简', '语义化', 'CSS'] } ]); export const popularJsLibraries = Object.freeze([ { id: 'bootstrap-js', name: 'Bootstrap JS', package: 'bootstrap', file: 'dist/js/bootstrap.bundle.min.js', cdnjsFile: 'js/bootstrap.bundle.min.js', type: 'js', icon: 'fa-brands fa-bootstrap', description: 'Bootstrap 交互组件包', tags: ['组件', '交互', 'JavaScript'] }, { id: 'jquery', name: 'jQuery', package: 'jquery', file: 'dist/jquery.min.js', cdnjsFile: 'jquery.min.js', type: 'js', icon: 'fa-solid fa-code', description: '经典 JavaScript 库', tags: ['DOM', '兼容性', 'JavaScript'] }, { id: 'vue', name: 'Vue.js', package: 'vue', file: 'dist/vue.global.prod.js', cdnjsFile: 'vue.cjs.js', type: 'js', icon: 'fa-brands fa-vuejs', description: '渐进式 JavaScript 框架', tags: ['MVVM', '响应式', 'JavaScript'] }, { id: 'react', name: 'React', package: 'react', file: 'umd/react.production.min.js', cdnjsFile: 'cjs/react.production.min.js', type: 'js', icon: 'fa-brands fa-react', description: 'UI 构建库', tags: ['组件化', '虚拟DOM', 'JavaScript'] }, { id: 'react-dom', name: 'React DOM', package: 'react-dom', file: 'umd/react-dom.production.min.js', type: 'js', icon: 'fa-brands fa-react', description: 'React DOM 渲染器', tags: ['DOM', 'React', 'JavaScript'] }, { id: 'axios', name: 'Axios', package: 'axios', file: 'dist/axios.min.js', cdnjsFile: 'axios.min.js', type: 'js', icon: 'fa-solid fa-globe', description: 'Promise HTTP 客户端', tags: ['HTTP', 'Ajax', 'JavaScript'] }, { id: 'lodash', name: 'Lodash', package: 'lodash', file: 'lodash.min.js', type: 'js', icon: 'fa-solid fa-toolbox', description: '实用工具函数库', tags: ['工具库', 'JavaScript'] }, { id: 'dayjs', name: 'Day.js', package: 'dayjs', file: 'dayjs.min.js', type: 'js', icon: 'fa-solid fa-calendar-days', description: '轻量日期处理库', tags: ['日期', 'JavaScript'] }, { id: 'alpinejs', name: 'Alpine.js', package: 'alpinejs', file: 'dist/cdn.min.js', type: 'js', icon: 'fa-solid fa-mountain', description: '轻量交互框架', tags: ['交互', '轻量', 'JavaScript'] }, { id: 'chartjs', name: 'Chart.js', package: 'chart.js', file: 'dist/chart.umd.min.js', type: 'js', icon: 'fa-solid fa-chart-line', description: '图表可视化库', tags: ['图表', '可视化', 'JavaScript'] } ]); // ====================== // API 请求函数 // ====================== /** * 获取 GitHub 仓库数据 * @param {string} packageName - npm包名 * @returns {Promise} GitHub数据,包含stars数和仓库URL */ async function fetchGitHubData(packageName) { try { // 优先走本站后端(带缓存,避免浏览器直连 GitHub 的限流/不稳定) try { const resp = await fetchWithTimeout(`/api/github?pkg=${encodeURIComponent(packageName)}`, { headers: { 'Accept': 'application/json' } }, 2500); if (resp && resp.ok) { const data = await resp.json(); if (data && data.success && data.github && typeof data.github.stars === 'number') { return { stars: data.github.stars, url: data.github.url }; } } } catch (_) { // 忽略后端失败,继续走前端直连逻辑 } // 特殊包名映射 const specialRepos = { 'animate.css': { owner: 'animate-css', repo: 'animate.css' }, 'tailwindcss': { owner: 'tailwindlabs', repo: 'tailwindcss' }, '@fortawesome/fontawesome-free': { owner: 'FortAwesome', repo: 'Font-Awesome' }, 'swiper': { owner: 'nolimits4web', repo: 'Swiper' }, 'axios': { owner: 'axios', repo: 'axios' }, 'vue': { owner: 'vuejs', repo: 'core' }, 'react': { owner: 'facebook', repo: 'react' }, 'jquery': { owner: 'jquery', repo: 'jquery' }, 'bootstrap': { owner: 'twbs', repo: 'bootstrap' } }; // 检查是否是特殊包 if (specialRepos[packageName]) { const { owner, repo } = specialRepos[packageName]; const githubResponse = await fetchWithTimeout(`https://api.github.com/repos/${owner}/${repo}`, { headers: { 'Accept': 'application/vnd.github.v3+json' } }); if (githubResponse.ok) { const data = await githubResponse.json(); return { stars: data.stargazers_count, url: data.html_url }; } return null; } // 常规包处理逻辑 const npmResponse = await fetchWithTimeout(`${config.npmRegistryApi}/${packageName}`); if (!npmResponse.ok) return null; const npmData = await npmResponse.json(); // 获取仓库URL let repoUrl = ''; // 尝试多种方式获取仓库URL if (npmData.repository?.url) { repoUrl = npmData.repository.url; } else if (typeof npmData.repository === 'string') { repoUrl = npmData.repository; } else if (npmData.homepage?.includes('github.com')) { repoUrl = npmData.homepage; } else if (npmData.bugs?.url?.includes('github.com')) { repoUrl = npmData.bugs.url.replace('/issues', ''); } if (!repoUrl) return null; // 处理URL格式 repoUrl = repoUrl .replace('git+', '') .replace('git:', '') .replace('.git', '') .replace('ssh://', ''); // 提取owner和repo let owner, repo; const patterns = [ /github\.com[\/:]([^\/]+)\/([^\/\s#]+)/, /github:([^\/]+)\/([^\/\s#]+)/, /git@github\.com:([^\/]+)\/([^\/\s#]+)/ ]; for (const pattern of patterns) { const match = repoUrl.match(pattern); if (match) { [, owner, repo] = match; break; } } if (!owner || !repo) return null; // 获取GitHub数据 const githubResponse = await fetchWithTimeout(`https://api.github.com/repos/${owner}/${repo}`, { headers: { 'Accept': 'application/vnd.github.v3+json' } }); if (githubResponse.status === 403) { console.warn('GitHub API 请求限制,请稍后重试'); return null; } if (!githubResponse.ok) return null; const githubData = await githubResponse.json(); return { stars: githubData.stargazers_count, url: githubData.html_url }; } catch (error) { console.warn(`获取 GitHub 数据失败: ${packageName}`, error); return null; } } /** * 获取CSS库最新版本信息(带缓存) * @param {string} packageName - npm包名 * @returns {Promise} 包含版本号和更新时间 */ export async function fetchCssVersion(packageName) { if (versionCache.has(packageName)) { const { timestamp, version, updatedAt } = versionCache.get(packageName); if (Date.now() - timestamp < CACHE_TTL) { return { version, updatedAt }; } } try { let version = 'latest'; let updatedAt = null; const jsdelivrResponse = await fetchWithTimeout(`${JSDELIVR_PACKAGE_API}/${packageName}`, {}, 3000); if (jsdelivrResponse.ok) { const jsdelivrData = await jsdelivrResponse.json(); version = jsdelivrData.tags?.latest || jsdelivrData.versions?.[0] || 'latest'; } else { const response = await fetchWithTimeout(`${config.npmRegistryApi}/${packageName}`); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); version = data['dist-tags']?.latest || 'latest'; updatedAt = data.time?.[version] || data.time?.modified || null; } versionCache.set(packageName, { timestamp: Date.now(), version, updatedAt }); return { version, updatedAt }; } catch (error) { console.warn(`[popularCSS] 获取版本失败: ${packageName}`, error); return { version: 'latest', updatedAt: null }; } } // ====================== // 工具函数 // ====================== /** * 检查是否是当月更新 * @param {string} dateString - 日期字符串 * @returns {boolean} 是否是当月更新 */ function isCurrentMonth(dateString) { if (!dateString) return false; const updateDate = new Date(dateString); const now = new Date(); return updateDate.getMonth() === now.getMonth() && updateDate.getFullYear() === now.getFullYear(); } /** * 格式化更新时间 * @param {string} dateString - 日期字符串 * @returns {string} 格式化后的日期 */ function formatUpdateTime(dateString) { if (!dateString) return ''; return new Date(dateString).toLocaleDateString('zh-CN'); } /** * 格式化 star 数量 * @param {number} num - star数量 * @returns {string} 格式化后的数字 */ function formatStars(num) { if (!num) return '0'; if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`; if (num >= 1000) return `${(num / 1000).toFixed(1)}K`; return num.toString(); } /** * HTML转义 * @param {string} unsafe - 待转义的字符串 * @returns {string} 转义后的字符串 */ function escapeHtml(unsafe) { return unsafe .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } /** * 属性值转义 * @param {string} unsafe - 待转义的字符串 * @returns {string} 转义后的字符串 */ function escapeAttr(unsafe) { return String(unsafe ?? '').replace(/"/g, """); } function safeExternalUrl(url) { if (!url) return ''; try { const parsed = new URL(url); return ['http:', 'https:'].includes(parsed.protocol) ? parsed.href : ''; } catch (_) { return ''; } } /** * 生成CDN URL(根据不同CDN源动态生成) * @param {string} pkg - 包名 * @param {string} version - 版本号 * @param {string} filePath - 文件路径 * @param {Object} libConfig - 库配置对象 * @returns {string} CDN URL */ function generateSafeUrl(pkg, version, filePath, libConfig = null) { if (libConfig?.fixedUrl) { return libConfig.fixedUrl; } const cleanPath = filePath .split(/[/\\]+/) .filter(Boolean) .join('/'); const cdnHost = config.getCurrentCDN().domain; return `https://${cdnHost}/libs/${pkg}/${version}/${cleanPath}`; } /** * 复制到剪贴板 * @param {string} text - 要复制的文本 * @returns {Promise} 是否复制成功 */ async function copyToClipboard(text) { try { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text); return true; } else { const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); try { const result = document.execCommand('copy'); document.body.removeChild(textarea); return result; } catch (e) { document.body.removeChild(textarea); return false; } } } catch (error) { console.error('复制失败:', error); return false; } } /** * 显示错误信息 * @param {HTMLElement} container - 容器元素 * @param {Error} error - 错误对象 */ function showError(container, error) { container.innerHTML = `
加载失败,请刷新重试 ${error.message ? `
${escapeHtml(error.message)}
` : ''}
`; } /** * 下载文件 * @param {string} url - 文件 URL * @param {string} filename - 文件名 */ async function downloadFile(url, filename) { try { const response = await fetch(url); if (!response.ok) { throw new Error(`下载失败: ${response.status} ${response.statusText}`); } const blob = await response.blob(); const blobUrl = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = blobUrl; a.download = filename; document.body.appendChild(a); a.click(); // 清理 document.body.removeChild(a); window.URL.revokeObjectURL(blobUrl); return true; } catch (error) { console.error('下载失败:', error); throw error; } } /** * 复制按钮功能设置 * @param {HTMLElement} card - 卡片元素 */ function setupCopyButtons(card) { // 处理复制按钮 card.querySelectorAll('.copy-btn:not(.download-btn)').forEach(btn => { const originalHTML = btn.innerHTML; btn.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); btn.disabled = true; try { const success = await copyToClipboard(btn.dataset.copy); if (success) { btn.innerHTML = ''; btn.classList.add('copy-success'); setTimeout(() => { btn.innerHTML = originalHTML; btn.classList.remove('copy-success'); btn.disabled = false; }, 2000); } else { throw new Error('复制失败'); } } catch (error) { btn.innerHTML = ''; btn.classList.add('copy-error'); setTimeout(() => { btn.innerHTML = originalHTML; btn.classList.remove('copy-error'); btn.disabled = false; }, 2000); console.error('复制失败:', error); } }); }); // 处理下载按钮 card.querySelectorAll('.download-btn').forEach(btn => { const originalHTML = btn.innerHTML; btn.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); btn.disabled = true; btn.innerHTML = ''; try { const url = btn.dataset.download; const filename = btn.dataset.filename || 'download.css'; await downloadFile(url, filename); btn.innerHTML = ''; btn.classList.add('copy-success'); setTimeout(() => { btn.innerHTML = originalHTML; btn.classList.remove('copy-success'); btn.disabled = false; }, 2000); } catch (error) { btn.innerHTML = ''; btn.classList.add('copy-error'); setTimeout(() => { btn.innerHTML = originalHTML; btn.classList.remove('copy-error'); btn.disabled = false; }, 2000); console.error('下载失败:', error); } }); }); } /** * 创建CSS库卡片元素 * @param {Object} lib - 库信息对象 * @returns {HTMLElement} 卡片DOM元素 */ function createCssCard(lib) { const safeLib = { id: lib.id?.trim() || 'unknown', name: lib.name?.trim() || '未知库', icon: lib.icon || 'fa-solid fa-question', description: lib.description?.trim() || '暂无描述', tags: Array.isArray(lib.tags) ? lib.tags.filter(Boolean) : ['未分类'], type: lib.type || (lib.file && lib.file.endsWith('.css') ? 'css' : 'js'), version: lib.version || 'latest', updatedAt: lib.updatedAt || null, package: lib.package || lib.id || 'unknown-package', file: lib.file?.trim() || 'dist/css/main.min.css', github: lib.github || null, cdnUrl: lib.cdnUrl || null, fixedUrl: lib.fixedUrl, homepage: safeExternalUrl(lib.homepage), cdnjsPackage: lib.cdnjsPackage, cdnjsVersion: lib.cdnjsVersion, cdnjsFile: lib.cdnjsFile }; // 格式化更新时间并检查是否是当月更新 const updateTime = formatUpdateTime(safeLib.updatedAt); const isLatestUpdate = isCurrentMonth(safeLib.updatedAt); const githubUrl = safeExternalUrl(safeLib.github?.url); const projectUrl = safeLib.homepage || githubUrl; const projectName = projectUrl ? ` ${escapeHtml(safeLib.name)} ` : `${escapeHtml(safeLib.name)}`; // 构建 GitHub 星标徽章 const githubBadge = githubUrl ? ` ${formatStars(safeLib.github?.stars)} ` : ''; const versionBadge = `v${escapeHtml(safeLib.version)}`; const updateBadge = updateTime ? ` ${updateTime} ` : ''; safeLib.cdnUrl = safeLib.cdnUrl || generateSafeUrl( safeLib.package, safeLib.version, safeLib.file, safeLib ); // 根据类型生成不同的HTML代码 const htmlCode = safeLib.type === 'css' ? `` : ``; // 文件扩展名 const fileExt = safeLib.type === 'css' ? 'css' : 'js'; const card = document.createElement('div'); card.className = 'lib-item single-line-view'; card.dataset.library = safeLib.id; card.dataset.updateTime = safeLib.updatedAt || ''; card.innerHTML = `
${projectName}
${versionBadge} ${githubBadge} ${escapeHtml(safeLib.description)}
${safeLib.tags.map(tag => ` ${escapeHtml(tag)} `).join('')}
${isLatestUpdate ? ` 本月更新 ` : ''} ${updateBadge}
${escapeHtml(safeLib.cdnUrl)}
`; setupCopyButtons(card); return card; } /** * 渲染CSS库列表 * @param {string} containerId - 容器元素ID */ export async function renderPopularCss(containerId = 'fixedList', options = {}) { const container = document.getElementById(containerId); if (!container) { console.warn(`[popularCSS] 目标容器 #${containerId} 不存在`); return; } const startTime = performance.now(); try { if (!options.silent) { showLoading(container, 'css'); } const cachedLibraries = await getCachedPopularLibraries('css'); if (cachedLibraries) { renderLibraryCards(container, cachedLibraries); return; } // 并行获取所有库的信息,不等待加载动画时间 const results = await Promise.allSettled( popularCssLibraries.map(async lib => { try { if (lib.fixedUrl) { const githubData = await fetchGitHubData(lib.package); return { ...lib, version: lib.fixedVersion || 'latest', updatedAt: null, github: githubData, cdnUrl: lib.fixedUrl }; } const [versionData, githubData] = await Promise.all([ fetchCssVersion(lib.package), fetchGitHubData(lib.package) ]); // 确保版本号不为 'latest',如果获取失败则使用一个合理的默认值 const resolvedVersion = versionData.version && versionData.version !== 'latest' ? versionData.version : versionData.version; return { ...lib, version: resolvedVersion, updatedAt: versionData.updatedAt, github: githubData, cdnUrl: generateSafeUrl(lib.package, resolvedVersion, lib.file, lib) }; } catch (error) { console.warn(`[popularCSS] 处理 ${lib.name} 失败,尝试重新获取版本:`, error); // 失败时再次尝试获取版本 try { const retryVersion = await fetchCssVersion(lib.package); return { ...lib, version: retryVersion.version, updatedAt: retryVersion.updatedAt, github: null, cdnUrl: generateSafeUrl(lib.package, retryVersion.version, lib.file, lib) }; } catch (retryError) { console.warn(`[popularCSS] 重试失败,${lib.name} 版本获取失败:`, retryError); // 如果重试也失败,返回一个占位符版本而不是 'latest' // 这样用户至少能看到一个URL,虽然可能不能直接工作 return { ...lib, version: 'unknown', updatedAt: null, github: null, cdnUrl: generateSafeUrl(lib.package, 'latest', lib.file, lib) }; } } }) ); // 确保加载动画最少显示 MIN_LOADING_MS (1500ms) const elapsed = performance.now() - startTime; if (elapsed < MIN_LOADING_MS) { await new Promise(resolve => setTimeout(resolve, MIN_LOADING_MS - elapsed)); } // 过滤并排序结果 const validResults = results .filter(result => result.status === 'fulfilled') .map(result => result.value) .sort((a, b) => { // 如果没有更新时间,排在最后 if (!a.updatedAt) return 1; if (!b.updatedAt) return -1; // 按更新时间降序排序(新的在前) return new Date(b.updatedAt) - new Date(a.updatedAt); }); renderLibraryCards(container, validResults, { enableAutoUpdate: true }); } catch (error) { console.error('[popularCSS] 渲染失败:', error); const elapsed = performance.now() - startTime; if (elapsed < MIN_LOADING_MS) { await new Promise(resolve => setTimeout(resolve, MIN_LOADING_MS - elapsed)); } showError(container, error); } } /** * 渲染JavaScript库列表 * @param {string} containerId - 容器元素ID */ export async function renderPopularJs(containerId = 'fixedList', options = {}) { const container = document.getElementById(containerId); if (!container) { console.warn(`[popularJS] 目标容器 #${containerId} 不存在`); return; } const startTime = performance.now(); try { if (!options.silent) { showLoading(container, 'js'); } const cachedLibraries = await getCachedPopularLibraries('js'); if (cachedLibraries) { renderLibraryCards(container, cachedLibraries); return; } const results = await Promise.allSettled( popularJsLibraries.map(async lib => { try { const [versionData, githubData] = await Promise.all([ fetchCssVersion(lib.package), fetchGitHubData(lib.package) ]); const resolvedVersion = versionData.version && versionData.version !== 'latest' ? versionData.version : versionData.version; return { ...lib, version: resolvedVersion, updatedAt: versionData.updatedAt, github: githubData, cdnUrl: generateSafeUrl(lib.package, resolvedVersion, lib.file, lib) }; } catch (error) { console.warn(`[popularJS] 处理 ${lib.name} 失败,尝试重新获取版本:`, error); try { const retryVersion = await fetchCssVersion(lib.package); return { ...lib, version: retryVersion.version, updatedAt: retryVersion.updatedAt, github: null, cdnUrl: generateSafeUrl(lib.package, retryVersion.version, lib.file, lib) }; } catch (retryError) { console.warn(`[popularJS] 重试失败,${lib.name} 版本获取失败:`, retryError); return { ...lib, version: 'unknown', updatedAt: null, github: null, cdnUrl: generateSafeUrl(lib.package, 'latest', lib.file, lib) }; } } }) ); const elapsed = performance.now() - startTime; if (elapsed < MIN_LOADING_MS) { await new Promise(resolve => setTimeout(resolve, MIN_LOADING_MS - elapsed)); } const validResults = results .filter(result => result.status === 'fulfilled') .map(result => result.value) .sort((a, b) => { if (!a.updatedAt) return 1; if (!b.updatedAt) return -1; return new Date(b.updatedAt) - new Date(a.updatedAt); }); renderLibraryCards(container, validResults, { enableAutoUpdate: true }); } catch (error) { console.error('[popularJS] 渲染失败:', error); const elapsed = performance.now() - startTime; if (elapsed < MIN_LOADING_MS) { await new Promise(resolve => setTimeout(resolve, MIN_LOADING_MS - elapsed)); } showError(container, error); } } /** * 启动后台版本自动更新 * 每隔一段时间检查并更新热门资源的版本号和CDN链接 * @param {Array} libraries - 库的信息数组 * @param {HTMLElement} container - 容器元素 */ function startAutoVersionUpdate(libraries, container) { // 每 30 分钟检查一次版本更新 const UPDATE_INTERVAL = 30 * 60 * 1000; const updateVersions = async () => { try { for (const lib of libraries) { if (lib.fixedUrl) { continue; } // 异步获取最新版本(不等待,允许并发) fetchCssVersion(lib.package).then(versionData => { // 如果版本号改变,更新DOM中的URL if (versionData.version && versionData.version !== lib.version && versionData.version !== 'latest') { lib.version = versionData.version; lib.updatedAt = versionData.updatedAt; const newUrl = generateSafeUrl(lib.package, versionData.version, lib.file, lib); // 查找并更新对应的卡片 const card = container.querySelector(`[data-library="${lib.id}"]`); if (card) { const urlCode = card.querySelector('.lib-url'); const copyBtn = card.querySelector('.copy-btn[data-copy]'); if (urlCode) { urlCode.textContent = newUrl; } if (copyBtn) { copyBtn.dataset.copy = newUrl; } // 更新时间戳 if (lib.updatedAt) { card.dataset.updateTime = lib.updatedAt; } } } }).catch(error => { console.warn(`[Auto Update] 版本更新失败 ${lib.package}:`, error); }); } } catch (error) { console.warn('[Auto Update] 版本检查出错:', error); } }; // 立即执行一次 updateVersions(); // 设置定期更新 setInterval(updateVersions, UPDATE_INTERVAL); } // ====================== // 初始化配置 // ====================== let autoInitEnabled = false; /** * 禁用自动初始化 */ export function disableAutoInit() { autoInitEnabled = false; } // 自动初始化(仅在浏览器环境) if (typeof window !== 'undefined') { window.addEventListener('DOMContentLoaded', () => { if (autoInitEnabled && document.getElementById('fixedList')) { renderPopularCss().catch(error => { console.error('[popularCSS] 自动初始化失败:', error); }); } }, { once: true }); }