const CATEGORY_NAMES = { office: "办公效率", dev: "开发工具", media: "影音娱乐", security: "安全系统", network: "网络工具", tools: "系统工具", }; const FALLBACK_SOFTWARE = [ { id: "sumi-launcher", name: "Sumi Launcher", version: "1.0.0", category: "office", description: "常用工具快速启动入口", publisher: "sumi.work", platform: "Windows", size: "待上传", updatedAt: "2026-06-08T00:00:00.000Z", iconUrl: "assets/icons/software.svg", downloadUrl: "", tags: ["推荐"], featured: true, }, { id: "git-client", name: "Git Client", version: "2.0.0", category: "dev", description: "代码仓库和版本管理工具", publisher: "sumi.work", platform: "Windows", size: "待上传", updatedAt: "2026-06-08T00:00:00.000Z", iconUrl: "assets/icons/git.svg", downloadUrl: "", tags: ["开发"], featured: true, }, { id: "sync-tool", name: "Sync Tool", version: "1.2.0", category: "network", description: "文件同步和分发工具", publisher: "sumi.work", platform: "Windows", size: "待上传", updatedAt: "2026-06-08T00:00:00.000Z", iconUrl: "assets/icons/syncthing.svg", downloadUrl: "", tags: ["同步"], featured: false, }, ]; const state = { items: [], category: "all", keyword: "", player: null, authLoaded: false, }; const els = { year: document.querySelector("#year"), keyword: document.querySelector("#software-keyword"), categoryButtons: [...document.querySelectorAll(".category-button")], featuredList: document.querySelector("#featured-list"), allList: document.querySelector("#all-list"), emptyState: document.querySelector("#empty-state"), resultSummary: document.querySelector("#result-summary"), refreshButton: document.querySelector("#refresh-button"), statCount: document.querySelector("#stat-count"), statFeatured: document.querySelector("#stat-featured"), statUpdated: document.querySelector("#stat-updated"), topicOffice: document.querySelector("#topic-office"), topicDev: document.querySelector("#topic-dev"), topicSecurity: document.querySelector("#topic-security"), playerStatus: document.querySelector("#player-status"), }; function escapeHtml(value = "") { return String(value) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function formatDate(value) { if (!value) return "--"; const date = new Date(value); if (Number.isNaN(date.getTime())) return "--"; return date.toLocaleDateString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", }); } function getCategoryName(category) { return CATEGORY_NAMES[category] || "其他"; } function getInitial(name = "") { return Array.from(name.trim())[0]?.toUpperCase() || "S"; } function getDownloadHref(item) { if (!item.downloadUrl) return ""; return `/api/software/${encodeURIComponent(item.id)}/download`; } function iconMarkup(item) { const safeName = escapeHtml(item.name); if (item.iconUrl) { return `${safeName}`; } return ``; } function downloadButton(item) { const href = getDownloadHref(item); if (!href) { return `待上传`; } if (!state.player) { return ` 登录下载 `; } return ` 下载 `; } function renderPlayerStatus() { if (!els.playerStatus) return; if (!state.authLoaded) { els.playerStatus.innerHTML = `检查登录中`; return; } if (!state.player) { els.playerStatus.innerHTML = `玩家登录`; return; } els.playerStatus.innerHTML = `${escapeHtml(state.player.displayName || state.player.username)} `; } function softwareCard(item) { return `
${iconMarkup(item)}

${escapeHtml(item.name)}

v${escapeHtml(item.version || "-")}

${escapeHtml(item.description || "暂无简介")}

${escapeHtml(getCategoryName(item.category))} ${escapeHtml(item.size || "未知大小")} ${formatDate(item.updatedAt)}
${downloadButton(item)}
`; } function topicItem(item) { return `
${iconMarkup(item)}

${escapeHtml(item.name)}

${escapeHtml(item.description || "暂无简介")}

${downloadButton(item)}
`; } function softwareRow(item) { const tags = (item.tags || []).slice(0, 2).map((tag) => `${escapeHtml(tag)}`).join(""); return `
${iconMarkup(item)}

${escapeHtml(item.name)}

v${escapeHtml(item.version || "-")} ${escapeHtml(getCategoryName(item.category))} ${tags}

${escapeHtml(item.description || "暂无简介")}

${escapeHtml(item.publisher || "sumi.work")} ${escapeHtml(item.platform || "Windows")} ${escapeHtml(item.size || "未知大小")} 更新 ${formatDate(item.updatedAt)}
${downloadButton(item)}
`; } function normalizeItems(payload) { const items = Array.isArray(payload) ? payload : payload?.items; return (items || []).map((item) => ({ ...item, tags: Array.isArray(item.tags) ? item.tags : [], })); } function sortItems(items) { return [...items].sort((a, b) => { if (Boolean(a.featured) !== Boolean(b.featured)) return a.featured ? -1 : 1; return new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0); }); } function getFilteredItems() { const keyword = state.keyword.trim().toLowerCase(); return sortItems(state.items).filter((item) => { const matchesCategory = state.category === "all" || item.category === state.category; const matchesKeyword = !keyword || [item.name, item.description, item.publisher, ...(item.tags || [])] .join(" ") .toLowerCase() .includes(keyword); return matchesCategory && matchesKeyword; }); } function renderStats() { const featured = state.items.filter((item) => item.featured).length; const latest = sortItems(state.items)[0]?.updatedAt; els.statCount.textContent = String(state.items.length); els.statFeatured.textContent = String(featured); els.statUpdated.textContent = formatDate(latest); } function renderTopics() { const byCategory = (category) => sortItems(state.items).filter((item) => item.category === category).slice(0, 5); els.topicOffice.innerHTML = byCategory("office").map(topicItem).join("") || `

暂无办公软件。

`; els.topicDev.innerHTML = byCategory("dev").map(topicItem).join("") || `

暂无开发工具。

`; els.topicSecurity.innerHTML = byCategory("security").map(topicItem).join("") || `

暂无安全工具。

`; } function render() { const filtered = getFilteredItems(); const featured = filtered.filter((item) => item.featured).slice(0, 8); els.featuredList.innerHTML = (featured.length ? featured : filtered.slice(0, 8)).map(softwareCard).join(""); els.allList.innerHTML = filtered.map(softwareRow).join(""); els.emptyState.hidden = filtered.length > 0; els.resultSummary.textContent = `共找到 ${filtered.length} 款软件`; renderStats(); renderTopics(); renderPlayerStatus(); } async function loadAuth() { try { const response = await fetch("/api/auth/me", { headers: { Accept: "application/json" } }); const payload = await response.json(); state.player = payload.authenticated ? payload.player : null; } catch { state.player = null; } finally { state.authLoaded = true; render(); } } async function loadSoftware() { try { const response = await fetch("/api/software", { headers: { Accept: "application/json" } }); if (!response.ok) throw new Error(`HTTP ${response.status}`); state.items = normalizeItems(await response.json()); } catch (error) { state.items = FALLBACK_SOFTWARE; } render(); } els.year.textContent = new Date().getFullYear(); els.keyword.addEventListener("input", (event) => { state.keyword = event.target.value; render(); }); els.categoryButtons.forEach((button) => { button.addEventListener("click", () => { state.category = button.dataset.category; els.categoryButtons.forEach((item) => item.classList.toggle("is-active", item === button)); render(); }); }); els.refreshButton.addEventListener("click", loadSoftware); els.playerStatus.addEventListener("click", async (event) => { if (!event.target.closest("#player-logout")) return; await fetch("/api/auth/logout", { method: "POST", headers: { Accept: "application/json" } }); state.player = null; state.authLoaded = true; render(); }); loadAuth(); loadSoftware();