219 lines
8.1 KiB
JavaScript
219 lines
8.1 KiB
JavaScript
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, """)
|
|
.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 `<span class="app-icon"><img src="${escapeHtml(item.iconUrl)}" alt="${escapeHtml(item.name)}" onerror="this.remove(); this.parentElement.textContent='${escapeHtml(getInitial(item.name))}'" /></span>`;
|
|
}
|
|
return `<span class="app-icon" aria-hidden="true">${escapeHtml(getInitial(item.name))}</span>`;
|
|
}
|
|
|
|
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) => `<article class="admin-item">
|
|
${iconMarkup(item)}
|
|
<div>
|
|
<div class="app-name-line">
|
|
<h3 class="app-name">${escapeHtml(item.name)}</h3>
|
|
<span class="version-pill">v${escapeHtml(item.version || "-")}</span>
|
|
<span class="category-pill">${escapeHtml(CATEGORY_NAMES[item.category] || "其他")}</span>
|
|
</div>
|
|
<p class="app-desc">${escapeHtml(item.description || "暂无简介")}</p>
|
|
<div class="app-meta">
|
|
<span>${item.active === false ? "已隐藏" : "前台显示"}</span>
|
|
<span>${item.featured ? "精选" : "普通"}</span>
|
|
<span>${item.versionManifest ? "version.json" : "无版本清单"}</span>
|
|
<span>${escapeHtml(item.size || "未知大小")}</span>
|
|
<span>${formatDate(item.updatedAt)}</span>
|
|
</div>
|
|
</div>
|
|
<div class="admin-item-actions">
|
|
<button class="edit-button" type="button" data-action="edit" data-id="${escapeHtml(item.id)}">编辑</button>
|
|
<button class="danger-button" type="button" data-action="delete" data-id="${escapeHtml(item.id)}">删除</button>
|
|
</div>
|
|
</article>`).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.form.elements.versionManifestContent.value = "";
|
|
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.form.elements.versionManifest.value = "";
|
|
els.form.elements.versionManifestContent.value = item.versionManifest ? JSON.stringify(item.versionManifest, null, 2) : "";
|
|
els.formTitle.textContent = `更新 ${item.name}`;
|
|
els.submitButton.textContent = "更新软件";
|
|
setStatus(els.formStatus, item.versionManifest ? "已载入当前软件,可替换安装包或直接编辑版本内容 JSON。" : "已载入当前软件,上传安装包且版本内容留空时会自动生成 version.json。", "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 后读取列表。");
|
|
}
|