const CATEGORY_NAMES = { office: "办公效率", dev: "开发工具", media: "影音娱乐", security: "安全系统", network: "网络工具", tools: "系统工具", }; const els = { token: document.querySelector("#admin-token"), saveToken: document.querySelector("#save-token"), tokenStatus: document.querySelector("#token-status"), form: document.querySelector("#software-form"), formTitle: document.querySelector("#form-title"), formStatus: document.querySelector("#form-status"), submitButton: document.querySelector("#submit-button"), resetForm: document.querySelector("#reset-form"), reload: document.querySelector("#reload-admin"), list: document.querySelector("#admin-list"), summary: document.querySelector("#admin-summary"), }; let softwareItems = []; function escapeHtml(value = "") { return String(value) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function getToken() { return localStorage.getItem("sumiSoftwareAdminToken") || ""; } function setStatus(element, message, type = "") { element.textContent = message; element.classList.toggle("is-error", type === "error"); element.classList.toggle("is-ok", type === "ok"); } function getInitial(name = "") { return Array.from(name.trim())[0]?.toUpperCase() || "S"; } function iconMarkup(item) { if (item.iconUrl) { return `${escapeHtml(item.name)}`; } return ``; } function normalizeItems(payload) { const items = Array.isArray(payload) ? payload : payload?.items; return (items || []).map((item) => ({ ...item, tags: Array.isArray(item.tags) ? item.tags : [], })); } function formatDate(value) { if (!value) return "--"; const date = new Date(value); if (Number.isNaN(date.getTime())) return "--"; return date.toLocaleString("zh-CN", { hour12: false }); } function renderList() { els.summary.textContent = `共 ${softwareItems.length} 款软件`; els.list.innerHTML = softwareItems.map((item) => `
${iconMarkup(item)}

${escapeHtml(item.name)}

v${escapeHtml(item.version || "-")} ${escapeHtml(CATEGORY_NAMES[item.category] || "其他")}

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

${item.active === false ? "已隐藏" : "前台显示"} ${item.featured ? "精选" : "普通"} ${escapeHtml(item.size || "未知大小")} ${formatDate(item.updatedAt)}
`).join(""); } async function apiFetch(url, options = {}) { const token = getToken(); const headers = { ...(options.headers || {}), "X-Admin-Token": token, }; const response = await fetch(url, { ...options, headers }); if (!response.ok) { const message = await response.text(); throw new Error(message || `HTTP ${response.status}`); } return response.headers.get("content-type")?.includes("application/json") ? response.json() : response.text(); } async function loadSoftware() { try { setStatus(els.tokenStatus, "正在读取软件列表..."); const payload = await apiFetch("/api/admin/software"); softwareItems = normalizeItems(payload).sort((a, b) => new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0)); renderList(); setStatus(els.tokenStatus, "令牌可用。", "ok"); } catch (error) { softwareItems = []; renderList(); setStatus(els.tokenStatus, `读取失败:${error.message}`, "error"); } } function resetForm() { els.form.reset(); els.form.elements.id.value = ""; els.form.elements.platform.value = "Windows"; els.form.elements.publisher.value = "sumi.work"; els.form.elements.sort.value = "100"; els.form.elements.active.checked = true; els.formTitle.textContent = "新增软件"; els.submitButton.textContent = "保存软件"; setStatus(els.formStatus, ""); } function fillForm(item) { els.form.elements.id.value = item.id; els.form.elements.name.value = item.name || ""; els.form.elements.version.value = item.version || ""; els.form.elements.category.value = item.category || "tools"; els.form.elements.platform.value = item.platform || "Windows"; els.form.elements.publisher.value = item.publisher || "sumi.work"; els.form.elements.homepage.value = item.homepage || ""; els.form.elements.description.value = item.description || ""; els.form.elements.changelog.value = item.changelog || ""; els.form.elements.tags.value = (item.tags || []).join(", "); els.form.elements.sort.value = item.sort ?? 100; els.form.elements.downloadUrl.value = item.externalDownloadUrl || ""; els.form.elements.active.checked = item.active !== false; els.form.elements.featured.checked = Boolean(item.featured); els.form.elements.icon.value = ""; els.form.elements.package.value = ""; els.formTitle.textContent = `更新 ${item.name}`; els.submitButton.textContent = "更新软件"; setStatus(els.formStatus, "已载入当前软件,选择新安装包即可发布新版本。", "ok"); window.scrollTo({ top: 0, behavior: "smooth" }); } async function saveSoftware(event) { event.preventDefault(); const formData = new FormData(els.form); const id = formData.get("id"); formData.delete("id"); try { setStatus(els.formStatus, "正在保存..."); const url = id ? `/api/admin/software/${encodeURIComponent(id)}` : "/api/admin/software"; const method = id ? "PUT" : "POST"; await apiFetch(url, { method, body: formData }); setStatus(els.formStatus, id ? "软件已更新。" : "软件已新增。", "ok"); resetForm(); await loadSoftware(); } catch (error) { setStatus(els.formStatus, `保存失败:${error.message}`, "error"); } } async function deleteSoftware(id) { const item = softwareItems.find((entry) => entry.id === id); if (!item || !window.confirm(`确定删除 ${item.name} 吗?`)) return; try { await apiFetch(`/api/admin/software/${encodeURIComponent(id)}`, { method: "DELETE" }); setStatus(els.tokenStatus, "软件已删除。", "ok"); await loadSoftware(); } catch (error) { setStatus(els.tokenStatus, `删除失败:${error.message}`, "error"); } } els.token.value = getToken(); els.saveToken.addEventListener("click", () => { localStorage.setItem("sumiSoftwareAdminToken", els.token.value.trim()); setStatus(els.tokenStatus, "令牌已保存。", "ok"); loadSoftware(); }); els.reload.addEventListener("click", loadSoftware); els.resetForm.addEventListener("click", resetForm); els.form.addEventListener("submit", saveSoftware); els.list.addEventListener("click", (event) => { const button = event.target.closest("button[data-action]"); if (!button) return; const item = softwareItems.find((entry) => entry.id === button.dataset.id); if (button.dataset.action === "edit" && item) fillForm(item); if (button.dataset.action === "delete") deleteSoftware(button.dataset.id); }); if (getToken()) { loadSoftware(); } else { renderList(); setStatus(els.tokenStatus, "请输入服务端 ADMIN_TOKEN 后读取列表。"); }