${escapeHtml(item.name)}
v${escapeHtml(item.version || "-")}${escapeHtml(item.description || "暂无简介")}
${downloadButton(item)}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"),
authLinks: [...document.querySelectorAll("[data-player-auth]")],
};
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 getLoginHref(next = "/software.html") {
return `login.html?next=${encodeURIComponent(next)}`;
}
function iconMarkup(item) {
const safeName = escapeHtml(item.name);
if (item.iconUrl) {
return ``;
}
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 ` ${escapeHtml(item.description || "暂无简介")} ${escapeHtml(item.description || "暂无简介")} ${escapeHtml(item.description || "暂无简介")}${escapeHtml(item.name)}
v${escapeHtml(item.version || "-")}
${escapeHtml(item.name)}
${escapeHtml(item.name)}
v${escapeHtml(item.version || "-")}
${escapeHtml(getCategoryName(item.category))}
${tags}
暂无办公软件。
`; 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(); } return state.player; } async function ensurePlayer(next) { if (!state.authLoaded) { await loadAuth(); } if (state.player) return true; window.location.href = getLoginHref(next); return false; } 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.authLinks.forEach((link) => { link.addEventListener("click", async (event) => { const href = link.getAttribute("href") || "/software.html"; const next = link.dataset.next || href; if (state.authLoaded && state.player) return; event.preventDefault(); const isAuthenticated = await ensurePlayer(next); if (!isAuthenticated) return; if (href.startsWith("#")) { document.querySelector(href)?.scrollIntoView({ behavior: "smooth" }); return; } window.location.href = href; }); }); 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();