feat(admin): 计费页加「余额硬拦截」开关 + 各租户「充值」按钮
闭合积分环的配置/操作端,接到 P4 后端: - 计费规则区:加「余额硬拦截」开关(credit_enforce),与汇率合成一个「保存规则」。 - 用量观测·租户排行:每行加「充值」按钮(正=充值、负=校正),调 /admin/credits/grant, 充值后即时刷新余额。 - api:getBillingConfig/saveBillingConfig 带 credit_enforce;新增 grantCredits。 live 验证(preview):开关渲染+可存;点公司A「充值 10」→余额 5.92→15.92、账本落对租户; tsc + 41 vitest 全过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import { adminUsage, type UsageReport, type UsageTenantSum, type UsageDay } from "../api";
|
||||
import { adminUsage, grantCredits, type UsageReport, type UsageTenantSum, type UsageDay } from "../api";
|
||||
import { BillingRules } from "../components/BillingRules";
|
||||
|
||||
// 管理端「用量 & 计费」= 计费闭环一页:顶部配「规则」(单价/积分权重/汇率),下方看「结果」(用量观测)。
|
||||
@@ -68,6 +68,23 @@ export function UsagePage() {
|
||||
|
||||
const series = useMemo(() => (report ? fillDays(report.trend, days) : []), [report, days]);
|
||||
|
||||
const [granting, setGranting] = useState("");
|
||||
const doGrant = async (tenantId: string, name: string) => {
|
||||
const s = window.prompt(`给「${name}」充值积分(正=充值,负=扣减/校正)`, "100");
|
||||
if (s === null) return;
|
||||
const n = Number(s);
|
||||
if (!n) return;
|
||||
setGranting(tenantId);
|
||||
try {
|
||||
await grantCredits(tenantId, n, "admin 手动充值");
|
||||
await load();
|
||||
} catch (e) {
|
||||
window.alert((e as Error).message);
|
||||
} finally {
|
||||
setGranting("");
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <div className="text-sm text-gray-400">加载用量数据中…</div>;
|
||||
if (err) return <div className="text-sm text-rose-500">用量加载失败:{err}</div>;
|
||||
if (!report) return null;
|
||||
@@ -170,7 +187,8 @@ export function UsagePage() {
|
||||
<th className="py-2 pr-3 text-right font-medium">Token</th>
|
||||
<th className="py-2 pr-3 text-right font-medium">成本</th>
|
||||
<th className="py-2 pr-3 text-right font-medium">运行数</th>
|
||||
<th className="py-2 text-right font-medium">当前余额</th>
|
||||
<th className="py-2 pr-3 text-right font-medium">当前余额</th>
|
||||
<th className="py-2 text-right font-medium">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -185,7 +203,16 @@ export function UsagePage() {
|
||||
<td className="py-2 pr-3 text-right tabular-nums text-gray-500">{int(r.total_tok)}</td>
|
||||
<td className="py-2 pr-3 text-right tabular-nums text-gray-500">{money(r.cost_micros)}</td>
|
||||
<td className="py-2 pr-3 text-right tabular-nums text-gray-500">{int(r.task_count)}</td>
|
||||
<td className={`py-2 text-right tabular-nums ${r.balance_micro >= 0 ? "text-gray-800" : "text-rose-500"}`}>{credits(r.balance_micro)}</td>
|
||||
<td className={`py-2 pr-3 text-right tabular-nums ${r.balance_micro >= 0 ? "text-gray-800" : "text-rose-500"}`}>{credits(r.balance_micro)}</td>
|
||||
<td className="py-2 text-right">
|
||||
<button
|
||||
onClick={() => void doGrant(r.tenant_id, r.name || r.tenant_id)}
|
||||
disabled={granting === r.tenant_id}
|
||||
className="rounded border border-violet-200 px-2.5 py-1 text-xs text-violet-600 hover:bg-violet-50 disabled:opacity-40"
|
||||
>
|
||||
{granting === r.tenant_id ? "…" : "充值"}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user