feat(admin): 用量 & 计费页 —— SaaS P2 计量观测面(读 /admin/usage)

管理端「分析」组新增页:全平台 / 单租户的 token · 积分 · 成本口径。
- api.adminUsage():拉 /admin/usage(含 trend / totals / tenants 排行 / 单租户余额);
  微单位(×10⁻⁶)前端 ÷1e6 展示。
- UsagePage:租户下拉 + 近7/30天切换;KPI 卡(积分/Token/成本/余额或活跃租户);
  积分消耗按天条形趋势(补零连续);全平台口径下各租户排行表(点名下钻单租户)。
- routes 注册 /usage(HashRouter)。

live 验证(preview):全平台两租户排行 + KPI + 趋势条渲染正确;点租户下钻→
单租户口径 + 当前余额卡;数值与后端自洽;无 console 错误;tsc + 41 vitest 全过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-07 12:38:33 +08:00
parent bf75bb8a62
commit 54de95aa53
3 changed files with 280 additions and 0 deletions
+40
View File
@@ -365,3 +365,43 @@ export async function deactivatePrompt(key: string): Promise<void> {
throw new Error(d.error ?? `deactivate failed: ${res.status}`);
}
}
// —— 用量 / 计费(SaaS P2 计量:token / 积分 / 成本,按租户)——
// 金额与积分均为「微单位」(×10⁻⁶):展示时 ÷1e6。走 /admin/usage(系统级,跨租户)。
export interface UsageDay {
day: string; // YYYYMMDD
total_tok: number;
credits_micro: number;
cost_micros: number;
task_count: number;
}
export interface UsageTenantSum {
tenant_id: string;
name: string;
total_tok: number;
credits_micro: number;
cost_micros: number;
task_count: number;
balance_micro: number;
}
export interface UsageReport {
from: string;
to: string;
tenant: string; // 空 = 全平台口径
trend: UsageDay[];
totals: { total_tok: number; credits_micro: number; cost_micros: number; task_count: number };
balance_micro?: number; // 仅单租户口径
tenants?: UsageTenantSum[]; // 仅全平台口径:各租户排行
}
export async function adminUsage(params?: { tenant?: string; from?: string; to?: string }): Promise<UsageReport> {
const q = new URLSearchParams();
if (params?.tenant) q.set("tenant", params.tenant);
if (params?.from) q.set("from", params.from);
if (params?.to) q.set("to", params.to);
const qs = q.toString();
const res = guard(await fetch(`${ADMIN}/usage${qs ? "?" + qs : ""}`, { headers: authHeaders() }));
if (!res.ok) throw new Error(`usage failed: ${res.status}`);
const d = (await res.json()) as UsageReport;
return { ...d, trend: d.trend ?? [], tenants: d.tenants ?? [] }; // Go 空切片可能序列化为 null
}