const tokenInput = document.querySelector("#tokenInput"); const statusBar = document.querySelector("#statusBar"); const refreshButton = document.querySelector("#refreshButton"); const form = document.querySelector("#serviceForm"); const settingsForm = document.querySelector("#settingsForm"); const rowsEl = document.querySelector("#serviceRows"); const adminBrand = document.querySelector("#adminBrand"); const browserTitleInput = document.querySelector("#browserTitleInput"); const brandInput = document.querySelector("#brandInput"); const titleInput = document.querySelector("#titleInput"); const subtitleInput = document.querySelector("#subtitleInput"); const originalIdInput = document.querySelector("#originalIdInput"); const idInput = document.querySelector("#idInput"); const nameInput = document.querySelector("#nameInput"); const groupInput = document.querySelector("#groupInput"); const urlInput = document.querySelector("#urlInput"); const noteInput = document.querySelector("#noteInput"); const resetButton = document.querySelector("#resetButton"); const serviceFormTitle = document.querySelector("#serviceFormTitle"); const newFromListButton = document.querySelector("#newFromListButton"); const tabButtons = document.querySelectorAll(".tab-button"); const quickActionButtons = document.querySelectorAll(".quick-action"); const adminSections = document.querySelectorAll(".admin-section"); let services = []; let settings = {}; function setStatus(message, isError = false) { statusBar.textContent = message; statusBar.classList.toggle("error", isError); } function showPanel(panelId) { for (const button of tabButtons) { button.classList.toggle("active", button.dataset.target === panelId); } for (const button of quickActionButtons) { button.classList.toggle("active", button.dataset.target === panelId); } for (const section of adminSections) { section.classList.toggle("active", section.dataset.panel === panelId); } } function formatTime(value) { if (!value) return "-"; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return new Intl.DateTimeFormat("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }).format(date); } function getToken() { return tokenInput.value.trim(); } function apiUrl(path) { const url = new URL(path, window.location.origin); const token = getToken(); if (token) url.searchParams.set("token", token); return url; } function loadSavedToken() { tokenInput.value = localStorage.getItem("stun-nav-admin-token") || ""; } function saveToken() { localStorage.setItem("stun-nav-admin-token", getToken()); } function applySettings(nextSettings = {}) { settings = nextSettings; document.title = settings.browserTitle ? `${settings.browserTitle} - 管理` : "导航管理"; adminBrand.textContent = settings.brand || "STUN NAV"; browserTitleInput.value = settings.browserTitle || ""; brandInput.value = settings.brand || ""; titleInput.value = settings.title || ""; subtitleInput.value = settings.subtitle || ""; } function clearForm() { originalIdInput.value = ""; form.reset(); tokenInput.value = localStorage.getItem("stun-nav-admin-token") || tokenInput.value; serviceFormTitle.textContent = "新增导航"; showPanel("servicePanel"); idInput.focus(); } function fillForm(service) { originalIdInput.value = service.id; idInput.value = service.id; nameInput.value = service.name || ""; groupInput.value = service.group || ""; urlInput.value = service.url || ""; noteInput.value = service.note || ""; serviceFormTitle.textContent = "编辑导航"; showPanel("servicePanel"); window.scrollTo({ top: 0, behavior: "smooth" }); } async function requestJson(url, options = {}) { const response = await fetch(url, { ...options, headers: { "content-type": "application/json", ...(options.headers || {}) } }); const data = await response.json().catch(() => ({})); if (!response.ok || data.ok === false) { throw new Error(data.error || `请求失败:${response.status}`); } return data; } function renderRows() { rowsEl.replaceChildren(); if (!services.length) { const row = document.createElement("tr"); const cell = document.createElement("td"); cell.colSpan = 6; cell.className = "empty-cell"; cell.textContent = "还没有导航项"; row.append(cell); rowsEl.append(row); return; } for (const service of services) { const row = document.createElement("tr"); row.innerHTML = ` `; const cells = row.querySelectorAll("td"); cells[0].textContent = service.id; cells[1].textContent = service.name || ""; cells[2].textContent = service.group || ""; cells[3].textContent = service.url || ""; cells[4].textContent = formatTime(service.updatedAt); const actions = document.createElement("div"); actions.className = "row-actions"; const editButton = document.createElement("button"); editButton.type = "button"; editButton.className = "small-button"; editButton.textContent = "编辑"; editButton.addEventListener("click", () => fillForm(service)); const deleteButton = document.createElement("button"); deleteButton.type = "button"; deleteButton.className = "small-button danger-button"; deleteButton.textContent = "删除"; deleteButton.addEventListener("click", () => deleteService(service)); actions.append(editButton, deleteButton); cells[5].append(actions); rowsEl.append(row); } } async function loadServices() { try { const data = await requestJson("/api/services", { method: "GET" }); services = Array.isArray(data.services) ? data.services : []; applySettings(data.settings || {}); renderRows(); setStatus(`已读取 ${services.length} 个导航项`); } catch (error) { setStatus(error.message || "读取失败", true); } } async function deleteService(service) { if (!getToken()) { setStatus("请先填写管理密钥", true); tokenInput.focus(); return; } const confirmed = window.confirm(`确定删除「${service.name || service.id}」吗?`); if (!confirmed) return; try { saveToken(); await requestJson(apiUrl(`/api/services/${encodeURIComponent(service.id)}`), { method: "DELETE" }); if (originalIdInput.value === service.id) clearForm(); await loadServices(); setStatus(`已删除 ${service.name || service.id}`); } catch (error) { setStatus(error.message || "删除失败", true); } } async function saveService(event) { event.preventDefault(); if (!getToken()) { setStatus("请先填写管理密钥", true); tokenInput.focus(); return; } const id = idInput.value.trim(); const originalId = originalIdInput.value.trim(); if (!id) { setStatus("请填写 ID", true); return; } const payload = { id, name: nameInput.value.trim(), group: groupInput.value.trim() || "默认", url: urlInput.value.trim(), note: noteInput.value.trim() }; try { saveToken(); await requestJson(apiUrl(`/api/webhook/${encodeURIComponent(id)}`), { method: "POST", body: JSON.stringify(payload) }); if (originalId && originalId !== id) { await requestJson(apiUrl(`/api/services/${encodeURIComponent(originalId)}`), { method: "DELETE" }); } originalIdInput.value = id; await loadServices(); setStatus(`已保存 ${payload.name || id}`); clearForm(); showPanel("listPanel"); } catch (error) { setStatus(error.message || "保存失败", true); } } async function saveSettings(event) { event.preventDefault(); if (!getToken()) { setStatus("请先填写管理密钥", true); tokenInput.focus(); return; } const payload = { browserTitle: browserTitleInput.value.trim(), brand: brandInput.value.trim(), title: titleInput.value.trim(), subtitle: subtitleInput.value.trim() }; try { saveToken(); const data = await requestJson(apiUrl("/api/settings"), { method: "PUT", body: JSON.stringify(payload) }); applySettings(data.settings || payload); setStatus("页面设置已保存"); } catch (error) { setStatus(error.message || "保存页面设置失败", true); } } loadSavedToken(); loadServices(); tokenInput.addEventListener("change", saveToken); refreshButton.addEventListener("click", loadServices); resetButton.addEventListener("click", clearForm); newFromListButton.addEventListener("click", clearForm); form.addEventListener("submit", saveService); settingsForm.addEventListener("submit", saveSettings); for (const button of tabButtons) { button.addEventListener("click", () => { if (button.dataset.mode === "new") { clearForm(); return; } showPanel(button.dataset.target); }); } for (const button of quickActionButtons) { button.addEventListener("click", () => { if (button.dataset.mode === "new") { clearForm(); return; } showPanel(button.dataset.target); }); }