315 lines
9.0 KiB
JavaScript
315 lines
9.0 KiB
JavaScript
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 = `
|
|
<td class="mono"></td>
|
|
<td></td>
|
|
<td></td>
|
|
<td class="url-cell"></td>
|
|
<td></td>
|
|
<td></td>
|
|
`;
|
|
|
|
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);
|
|
});
|
|
}
|