feat: add web admin panel for node management
Add Flask panel with login, add/delete nodes, and share link copy. Generate sing-box config from SQLite; add uninstall script and clean install flow. Panel served at https://DOMAIN:8444 via nginx. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
function toast(msg) {
|
||||
const el = document.getElementById("toast");
|
||||
el.textContent = msg;
|
||||
el.classList.remove("hidden");
|
||||
setTimeout(() => el.classList.add("hidden"), 2200);
|
||||
}
|
||||
|
||||
document.querySelectorAll("[data-copy]").forEach((btn) => {
|
||||
btn.addEventListener("click", async () => {
|
||||
const text = btn.dataset.copy;
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
toast("已复制到剪贴板");
|
||||
} catch {
|
||||
toast("复制失败,请手动选择文本");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const modal = document.getElementById("modal");
|
||||
const addBtn = document.getElementById("addBtn");
|
||||
const cancelBtn = document.getElementById("cancelBtn");
|
||||
const confirmAddBtn = document.getElementById("confirmAddBtn");
|
||||
const nodeName = document.getElementById("nodeName");
|
||||
|
||||
if (addBtn) {
|
||||
addBtn.addEventListener("click", () => {
|
||||
nodeName.value = "";
|
||||
modal.classList.remove("hidden");
|
||||
nodeName.focus();
|
||||
});
|
||||
}
|
||||
|
||||
if (cancelBtn) {
|
||||
cancelBtn.addEventListener("click", () => modal.classList.add("hidden"));
|
||||
}
|
||||
|
||||
if (confirmAddBtn) {
|
||||
confirmAddBtn.addEventListener("click", async () => {
|
||||
const name = nodeName.value.trim() || "新节点";
|
||||
confirmAddBtn.disabled = true;
|
||||
try {
|
||||
const res = await fetch("/api/nodes", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || "创建失败");
|
||||
location.reload();
|
||||
} catch (err) {
|
||||
toast(err.message);
|
||||
} finally {
|
||||
confirmAddBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll(".delete-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", async () => {
|
||||
const id = btn.dataset.id;
|
||||
if (!confirm("确定删除该节点?删除后对应链接将失效。")) return;
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const res = await fetch(`/api/nodes/${id}`, { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || "删除失败");
|
||||
location.reload();
|
||||
} catch (err) {
|
||||
toast(err.message);
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user