(() => {
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();
});
})();