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; } }); }); function setStatusBadge(el, online) { el.textContent = online ? "在线" : "离线"; el.classList.toggle("online", online); el.classList.toggle("offline", !online); } function updateStats(data) { const summary = data.summary || {}; const onlineEl = document.getElementById("summaryOnline"); const upEl = document.getElementById("summaryUp"); const downEl = document.getElementById("summaryDown"); const statusEl = document.getElementById("summaryStatus"); if (onlineEl) { onlineEl.textContent = `${summary.online || 0} / ${summary.total_nodes || 0}`; } if (upEl) upEl.textContent = summary.upload_speed_human || "0 B/s"; if (downEl) downEl.textContent = summary.download_speed_human || "0 B/s"; if (statusEl) { if (data.singbox) { statusEl.textContent = "正常"; statusEl.className = "status-text ok"; } else { statusEl.textContent = "不可用"; statusEl.className = "status-text err"; } } document.querySelectorAll(".node-card[data-id]").forEach((card) => { const node = (data.nodes || {})[card.dataset.id]; if (!node) return; const status = card.querySelector('[data-role="status"]'); if (status) setStatusBadge(status, node.online); const setText = (role, value) => { const el = card.querySelector(`[data-role="${role}"]`); if (el) el.textContent = value; }; setText("connections", String(node.connections ?? 0)); setText("speed-up", node.upload_speed_human || "0 B/s"); setText("speed-down", node.download_speed_human || "0 B/s"); setText("total-up", node.upload_total_human || "0 B"); setText("total-down", node.download_total_human || "0 B"); }); } async function refreshStats() { try { const res = await fetch("/api/stats"); if (!res.ok) return; const data = await res.json(); updateStats(data); } catch { const statusEl = document.getElementById("summaryStatus"); if (statusEl) { statusEl.textContent = "不可用"; statusEl.className = "status-text err"; } } } if (document.getElementById("summaryBar")) { refreshStats(); setInterval(refreshStats, 5000); }