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:
@@ -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>
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user