(() => { const $ = (id) => document.getElementById(id); const token = new URLSearchParams(window.location.search).get("token") || ""; let currentOffset = 0; let currentTotal = 0; function fmtTime(ts) { const n = Number(ts || 0); if (!n) return "-"; const d = new Date(n * 1000); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); const hh = String(d.getHours()).padStart(2, "0"); const mm = String(d.getMinutes()).padStart(2, "0"); const ss = String(d.getSeconds()).padStart(2, "0"); return `${y}-${m}-${day} ${hh}:${mm}:${ss}`; } function setStatus(msg, isError = false) { const el = $("adminStatus"); if (!el) return; el.textContent = msg || ""; el.style.color = isError ? "#b91c1c" : ""; } async function getJSON(url) { const res = await fetch(url, { headers: { "X-Admin-Token": token, }, credentials: "same-origin", }); let data = {}; try { data = await res.json(); } catch (_) { data = {}; } if (!res.ok) { throw new Error(data.detail || `HTTP ${res.status}`); } return data; } async function postJSON(url, payload) { const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "X-Admin-Token": token, }, credentials: "same-origin", body: JSON.stringify(payload || {}), }); let data = {}; try { data = await res.json(); } catch (_) { data = {}; } if (!res.ok || data.ok === false) { throw new Error(data.detail || `HTTP ${res.status}`); } return data; } function fillOptions(id, items) { const el = $(id); if (!el) return; el.innerHTML = ""; for (const it of items || []) { const op = document.createElement("option"); op.value = String(it || ""); el.appendChild(op); } } async function loadPlatformModel() { const data = await getJSON("/api/admin/platform-model"); const cfg = data.config || {}; if ($("platformApiKey")) $("platformApiKey").value = cfg.api_key || ""; if ($("platformBaseUrl")) $("platformBaseUrl").value = cfg.base_url || ""; if ($("platformTextModel")) $("platformTextModel").value = cfg.model || ""; if ($("platformImageModel")) $("platformImageModel").value = cfg.image_model || ""; if ($("platformTimeoutSec")) $("platformTimeoutSec").value = Number(cfg.timeout_sec || 120); if ($("platformMaxOutputTokens")) $("platformMaxOutputTokens").value = Number(cfg.max_output_tokens || 8192); fillOptions("platformTextModelOptions", data.text_model_options || []); fillOptions("platformImageModelOptions", data.image_model_options || []); } async function loadUserOverview() { const data = await getJSON("/api/admin/users/overview?limit=30"); const stats = data.stats || {}; if ($("adminTotalUsers")) $("adminTotalUsers").value = String(stats.total_users || 0); if ($("adminActiveUsers")) $("adminActiveUsers").value = String(stats.active_users || 0); if ($("adminTodayUsers")) $("adminTodayUsers").value = String(stats.today_new_users || 0); if ($("adminDeletedUsers")) $("adminDeletedUsers").value = String(stats.deleted_users || 0); const tbody = $("adminRecentUsersRows"); if (!tbody) return; const rows = Array.isArray(data.recent_users) ? data.recent_users : []; if (!rows.length) { tbody.innerHTML = '暂无记录'; return; } tbody.innerHTML = rows .map((r) => { const status = Number(r.deleted_at || 0) > 0 ? "已注销" : "正常"; return ` ${Number(r.id || 0)} ${String(r.username || "")} ${fmtTime(r.created_at)} ${status} `; }) .join(""); } async function savePlatformModel() { await postJSON("/api/admin/platform-model", { api_key: ($("platformApiKey")?.value || "").trim(), base_url: ($("platformBaseUrl")?.value || "").trim(), model: ($("platformTextModel")?.value || "").trim(), image_model: ($("platformImageModel")?.value || "").trim(), timeout_sec: Number($("platformTimeoutSec")?.value || 120), max_output_tokens: Number($("platformMaxOutputTokens")?.value || 8192), max_retries: 0, }); setStatus("平台模型配置已保存"); if (window.uiAlert) window.uiAlert("平台模型配置已保存并立即生效"); } function renderTable(rows, columns) { const head = $("adminHeadRow"); const body = $("adminBodyRows"); if (!head || !body) return; head.innerHTML = ""; body.innerHTML = ""; if (!columns || !columns.length) { head.innerHTML = "无列信息"; body.innerHTML = '当前页没有数据'; return; } for (const col of columns) { const th = document.createElement("th"); th.textContent = col; head.appendChild(th); } if (!rows || !rows.length) { const tr = document.createElement("tr"); const td = document.createElement("td"); td.colSpan = columns.length; td.className = "muted"; td.textContent = "当前页没有数据"; tr.appendChild(td); body.appendChild(tr); return; } for (const row of rows) { const tr = document.createElement("tr"); for (const col of columns) { const td = document.createElement("td"); const val = row[col]; td.textContent = val == null ? "" : typeof val === "object" ? JSON.stringify(val) : String(val); tr.appendChild(td); } body.appendChild(tr); } } async function loadTables() { const data = await getJSON("/api/admin/tables"); const sel = $("adminTableSelect"); if (!sel) return; sel.innerHTML = ""; const tables = Array.isArray(data.tables) ? data.tables : []; for (const t of tables) { const opt = document.createElement("option"); opt.value = t; opt.textContent = t; sel.appendChild(opt); } if (!tables.length) { setStatus("数据库没有可展示的数据表"); return; } await loadRows(true); } async function loadRows(resetOffset = false) { const sel = $("adminTableSelect"); const limitInput = $("adminLimit"); if (!sel || !limitInput) return; const table = (sel.value || "").trim(); if (!table) return; if (resetOffset) currentOffset = 0; const limit = Math.max(1, Math.min(500, Number(limitInput.value) || 100)); const data = await getJSON( `/api/admin/table/${encodeURIComponent(table)}?limit=${limit}&offset=${currentOffset}` ); if (!data.ok) { throw new Error(data.detail || "加载失败"); } currentTotal = Number(data.total || 0); renderTable(data.rows || [], data.columns || []); const start = currentOffset + 1; const end = Math.min(currentOffset + limit, currentTotal); setStatus(`表 ${table}:第 ${start}-${end > 0 ? end : 0} 条 / 共 ${currentTotal} 条`); } async function safeRun(fn) { try { await fn(); } catch (err) { const msg = err && err.message ? err.message : "请求失败"; setStatus(msg, true); if (window.uiAlert) window.uiAlert(msg); } } $("adminRefreshBtn")?.addEventListener("click", () => safeRun(() => loadRows(false))); $("adminTableSelect")?.addEventListener("change", () => safeRun(() => loadRows(true))); $("platformReloadBtn")?.addEventListener("click", () => safeRun(loadPlatformModel)); $("platformSaveBtn")?.addEventListener("click", () => safeRun(savePlatformModel)); $("adminUserRefreshBtn")?.addEventListener("click", () => safeRun(loadUserOverview)); $("adminPrevBtn")?.addEventListener("click", () => safeRun(async () => { const limit = Math.max(1, Math.min(500, Number($("adminLimit")?.value) || 100)); currentOffset = Math.max(0, currentOffset - limit); await loadRows(false); }) ); $("adminNextBtn")?.addEventListener("click", () => safeRun(async () => { const limit = Math.max(1, Math.min(500, Number($("adminLimit")?.value) || 100)); if (currentOffset + limit < currentTotal) { currentOffset += limit; } await loadRows(false); }) ); safeRun(async () => { await loadUserOverview(); await loadPlatformModel(); await loadTables(); }); })();