249 lines
8.4 KiB
JavaScript
249 lines
8.4 KiB
JavaScript
(() => {
|
|
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 = '<tr><td colspan="4" class="muted">暂无记录</td></tr>';
|
|
return;
|
|
}
|
|
tbody.innerHTML = rows
|
|
.map((r) => {
|
|
const status = Number(r.deleted_at || 0) > 0 ? "已注销" : "正常";
|
|
return `<tr>
|
|
<td>${Number(r.id || 0)}</td>
|
|
<td>${String(r.username || "")}</td>
|
|
<td>${fmtTime(r.created_at)}</td>
|
|
<td>${status}</td>
|
|
</tr>`;
|
|
})
|
|
.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 = "<th>无列信息</th>";
|
|
body.innerHTML = '<tr><td class="muted">当前页没有数据</td></tr>';
|
|
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();
|
|
});
|
|
})();
|