Add secure administrator operations console

Provide TOTP-authenticated, role-controlled user and credit workflows with paginated audit data and SQL-backed statistics so operations can manage growth safely.
This commit is contained in:
Rocky
2026-08-17 15:20:34 +08:00
parent 676bfd2451
commit 1a9c518f96
76 changed files with 14602 additions and 3 deletions
+102
View File
@@ -0,0 +1,102 @@
import { adminApi } from "../api/client";
import { trendChart } from "../components/charts";
import { renderError, renderLoading } from "../components/ui";
import { escapeHtml, formatNumber } from "../lib/format";
export async function renderOverview(
container: HTMLElement,
range = "30d",
): Promise<void> {
renderLoading(container, "加载总览");
try {
const data = await adminApi.overview(range);
container.innerHTML = `
<div class="page-heading">
<div>
<div class="eyebrow">核心指标</div>
<h1>运营总览</h1>
<p>快速掌握增长与积分消耗情况。</p>
</div>
<fieldset class="segmented-control">
<legend class="sr-only">统计周期</legend>
${rangeOption("7d", "7 天", range)}
${rangeOption("30d", "30 天", range)}
${rangeOption("90d", "90 天", range)}
</fieldset>
</div>
<section class="metric-grid" aria-label="关键指标">
${metric("用户总数", data.totalUsers, "累计账户")}
${metric("活跃用户", data.activeUsers, "当前周期")}
${metric("新增用户", data.newUsers, "当前周期")}
${metric("积分余额", data.totalCreditBalance, "所有账户")}
${metric("赠送积分", data.creditsGranted, "整数积分")}
${metric("消耗积分", data.creditsUsed, "已结算")}
</section>
<section class="panel">
<div class="panel-heading">
<div><h2>新增用户趋势</h2><p>按 UTC 日期统计注册数</p></div>
<span class="legend"><i></i>新增用户</span>
</div>
${trendChart(data.trend)}
</section>
<section class="panel">
<div class="panel-heading"><div><h2>使用汇总</h2><p>按类型统计,不包含用户内容</p></div></div>
${
data.usage.length === 0
? '<p class="muted">当前周期暂无使用记录</p>'
: `
<div class="table-wrap">
<table>
<caption class="sr-only">当前周期使用统计</caption>
<thead><tr><th scope="col">类型</th><th scope="col">请求</th><th scope="col">消耗积分</th><th scope="col">语音毫秒</th><th scope="col">输入 Token</th><th scope="col">输出 Token</th></tr></thead>
<tbody>
${data.usage
.map(
(item) => `
<tr>
<td><code>${escapeHtml(item.kind)}</code></td>
<td>${formatNumber(item.requests)}</td>
<td>${formatNumber(item.chargedCredits)}</td>
<td>${formatNumber(item.asrMillis)}</td>
<td>${formatNumber(item.inputTokens)}</td>
<td>${formatNumber(item.outputTokens)}</td>
</tr>
`,
)
.join("")}
</tbody>
</table>
</div>
`
}
</section>
`;
container.querySelectorAll<HTMLInputElement>("[data-range]").forEach((input) => {
input.addEventListener("change", (event) => {
void renderOverview(container, (event.target as HTMLInputElement).value);
});
});
} catch (error) {
renderError(container, error, () => void renderOverview(container, range));
}
}
function rangeOption(value: string, label: string, selected: string): string {
return `
<label>
<input data-range name="overview-range" type="radio" value="${value}" ${selected === value ? "checked" : ""} />
<span>${label}</span>
</label>
`;
}
function metric(label: string, value: number, hint: string): string {
return `
<article class="metric-card">
<p>${label}</p>
<strong>${formatNumber(value)}</strong>
<span>${hint}</span>
</article>
`;
}