feat(desktop): 顶栏积分余额芯片 + 提交遇余额不足透出友好文案(P3 租户感知)

让计费对终端用户可见(守 desktop/admin 边界:只读自己租户,不碰 admin 配置):
- TopBar 加积分余额芯片(Coins 图标):常态显余额;硬拦截开+余额≤0 → 红「余额不足」;
  偏低 → 橙。tooltip 显租户名 + 是否拦截。api.tenantCurrent() 读 /tenants/current。
- App 登录后拉租户上下文,每 20s 轮询 + 运行结束即刷新(余额跟手)。
- submitTask 遇 402 解析后端 error 文案透出(如「租户积分余额不足,请充值后再试」),
  在运行面板报错处显示,用户知道为何被拦。

live 验证(preview):芯片显示 公司A 余额 15.9 ✓;enforce 开+余额0 时 /tenants/current
标 enforce+余额0(芯片转「余额不足」)、提交 402 带友好文案 ✓;tsc + 48 测试全过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-08 09:18:30 +08:00
parent d5a4670c2f
commit 511ae199a4
3 changed files with 81 additions and 7 deletions
+26 -4
View File
@@ -1,6 +1,6 @@
import type { CSSProperties } from "react";
import { User, ChevronDown, Search as SearchIcon, LogOut, Sun, Moon } from "lucide-react";
import type { AuthUser } from "../lib/api";
import { User, ChevronDown, Search as SearchIcon, LogOut, Sun, Moon, Coins } from "lucide-react";
import type { AuthUser, TenantCtx } from "../lib/api";
import { useHealth } from "../lib/health";
import { isMacDesktop } from "../lib/desktop";
import { useTheme } from "../lib/theme";
@@ -20,8 +20,29 @@ function Light({ on, label }: { on: boolean; label: string }) {
);
}
// 顶栏:品牌 · 垂直切换 · 健康灯 · 登录用户 + 登出(深色 + 毛玻璃)
export function TopBar({ user, onLogout, onCommand }: { user: AuthUser; onLogout: () => void; onCommand?: () => void }) {
// 积分余额芯片:让用户在主产品里随时看到自己租户剩多少积分
// 硬拦截开启且余额≤0 → 红色「余额不足」;偏低 → 橙;否则常态。
function CreditChip({ tenant }: { tenant: TenantCtx }) {
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)}
title={
`租户 ${tenant.tenant?.name ?? ""} · 积分余额 ${credits.toLocaleString("zh-CN", { maximumFractionDigits: 2 })}` +
(tenant.credit_enforce ? "(余额≤0 将无法提交任务,请联系管理员充值)" : "(当前不拦截,仅计量)")
}
>
<Coins className="h-3.5 w-3.5 opacity-70" />
{empty ? "余额不足" : credits.toLocaleString("zh-CN", { maximumFractionDigits: credits < 100 ? 1 : 0 })}
</span>
);
}
// 顶栏:品牌 · 垂直切换 · 健康灯 · 积分余额 · 登录用户 + 登出(深色 + 毛玻璃)。
export function TopBar({ user, tenant, onLogout, onCommand }: { user: AuthUser; tenant?: TenantCtx | null; onLogout: () => void; onCommand?: () => void }) {
const h = useHealth();
const { theme, toggle } = useTheme();
return (
@@ -64,6 +85,7 @@ export function TopBar({ user, onLogout, onCommand }: { user: AuthUser; onLogout
<Light on={h.neo4j} label="Neo4j" />
</div>
<div className="ml-auto flex items-center gap-2" style={NODRAG}>
{tenant?.tenant && <CreditChip tenant={tenant} />}
<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"