Initial stun navigation app
This commit is contained in:
314
public/admin.js
Normal file
314
public/admin.js
Normal file
@@ -0,0 +1,314 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user