feat(desktop): 「用量」视图 —— 我的积分余额/消耗趋势/最近消耗(P3 续)

给终端用户一个自己的计量观测面(守边界:只读自己租户):
- 新增 UsageView(左导航 MANAGE 组「用量」,Coins 图标):余额 hero 卡
  (硬拦截+余额≤0→红/偏低→橙)、区间 KPI(积分/Token/成本/运行数)、
  积分消耗按天趋势条形图(近7/近30天可切)、最近消耗明细表(任务/模型/token/积分/成本)。
  读 /api/v1/me/usage。
- 顶栏积分余额芯片改为可点,进「用量」页(onOpenUsage → setView)。

live 验证(preview):视图显示 公司A 余额 15.92 + KPI + 趋势bar(落在实际消耗日) +
最近消耗 3 行真实数据;tsc + 48 测试全过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-08 09:31:12 +08:00
parent f6ea03b34b
commit a878e2dc89
5 changed files with 241 additions and 9 deletions
@@ -7,11 +7,12 @@ import {
Bookmark,
Boxes,
Settings,
Coins,
type LucideIcon,
} from "lucide-react";
import { cn } from "../ui";
export type ViewKey = "home" | "studio" | "kb" | "report" | "runs" | "memory" | "market" | "admin";
export type ViewKey = "home" | "studio" | "kb" | "report" | "runs" | "memory" | "usage" | "market" | "admin";
interface Item {
key: ViewKey;
@@ -28,6 +29,7 @@ const ITEMS: Item[] = [
{ key: "report", label: "报告", icon: FileText, group: "BUILD", ready: true },
{ key: "runs", label: "运行", icon: Activity, group: "RUN", ready: true },
{ key: "memory", label: "记忆", icon: Bookmark, group: "MANAGE", ready: true },
{ key: "usage", label: "用量", icon: Coins, group: "MANAGE", ready: true },
{ key: "market", label: "市场", icon: Boxes, group: "MANAGE" },
{ key: "admin", label: "管理", icon: Settings, group: "MANAGE" },
];
@@ -20,16 +20,17 @@ function Light({ on, label }: { on: boolean; label: string }) {
);
}
// 积分余额芯片:让用户在主产品里随时看到自己租户剩多少积分。
// 积分余额芯片:让用户在主产品里随时看到自己租户剩多少积分。点击进「用量」页。
// 硬拦截开启且余额≤0 → 红色「余额不足」;偏低 → 橙;否则常态。
function CreditChip({ tenant }: { tenant: TenantCtx }) {
function CreditChip({ tenant, onClick }: { tenant: TenantCtx; onClick?: () => void }) {
const credits = tenant.credit_balance_micro / 1_000_000;
const empty = tenant.credit_enforce && credits <= 0;
const low = !empty && credits > 0 && credits < 10;
const tone = empty ? "border-danger/50 text-danger" : low ? "border-warn/50 text-warn" : "border-line text-slate-300";
return (
<span
className={cn("flex items-center gap-1.5 rounded-md border bg-ink-800 px-2.5 py-1 text-xs", tone)}
<button
onClick={onClick}
className={cn("flex items-center gap-1.5 rounded-md border bg-ink-800 px-2.5 py-1 text-xs transition hover:border-ink-600", tone)}
title={
`租户 ${tenant.tenant?.name ?? ""} · 积分余额 ${credits.toLocaleString("zh-CN", { maximumFractionDigits: 2 })}` +
(tenant.credit_enforce ? "(余额≤0 将无法提交任务,请联系管理员充值)" : "(当前不拦截,仅计量)")
@@ -37,12 +38,12 @@ function CreditChip({ tenant }: { tenant: TenantCtx }) {
>
<Coins className="h-3.5 w-3.5 opacity-70" />
{empty ? "余额不足" : credits.toLocaleString("zh-CN", { maximumFractionDigits: credits < 100 ? 1 : 0 })}
</span>
</button>
);
}
// 顶栏:品牌 · 垂直切换 · 健康灯 · 积分余额 · 登录用户 + 登出(深色 + 毛玻璃)。
export function TopBar({ user, tenant, onLogout, onCommand }: { user: AuthUser; tenant?: TenantCtx | null; onLogout: () => void; onCommand?: () => void }) {
export function TopBar({ user, tenant, onLogout, onCommand, onOpenUsage }: { user: AuthUser; tenant?: TenantCtx | null; onLogout: () => void; onCommand?: () => void; onOpenUsage?: () => void }) {
const h = useHealth();
const { theme, toggle } = useTheme();
return (
@@ -85,7 +86,7 @@ export function TopBar({ user, tenant, onLogout, onCommand }: { user: AuthUser;
<Light on={h.neo4j} label="Neo4j" />
</div>
<div className="ml-auto flex items-center gap-2" style={NODRAG}>
{tenant?.tenant && <CreditChip tenant={tenant} />}
{tenant?.tenant && <CreditChip tenant={tenant} onClick={onOpenUsage} />}
<button
onClick={toggle}
className="flex items-center rounded-md border border-line bg-ink-800 px-2 py-1 text-slate-400 transition hover:border-ink-600 hover:text-slate-200"