import { useEffect, useState } from "react"; import { api } from "../api"; import type { BillingSummary, BillingTrend, Ledger, Project, TeamMember } from "../types"; import { money } from "./stage-config"; import { pageWindow } from "../components/pager"; const ROLE_LABEL: Record = { owner: "超管", admin: "团管", member: "成员", viewer: "访客" }; const STATUS_LABEL: Record = { active: "活跃", invited: "待激活", disabled: "已停用" }; type TrendRange = "day" | "week" | "month"; const RANGE_META: Record = { day: { chip: "日", sub: "// 近 14 天 · 单位 ¥", totalLabel: "14 天合计", avgLabel: "日均" }, week: { chip: "周", sub: "// 近 8 周 · 单位 ¥", totalLabel: "8 周合计", avgLabel: "周均" }, month: { chip: "月", sub: "// 近 6 个月 · 单位 ¥", totalLabel: "6 月合计", avgLabel: "月均" } }; type Tab = "overview" | "by-project" | "by-member" | "bills"; // 账单类型 / 详情 中文化(后端历史英文流水也一并映射;未知值原样透出) const LEDGER_TYPE_LABEL: Record = { recharge: "充值", reserve: "预扣", release: "释放", charge: "扣费", adjustment: "调整", refund: "退款" }; const LEDGER_REASON_LABEL: Record = { "reserve ai task credit": "AI 任务预扣额度", "charge ai task credit": "AI 任务扣费", "release reserved credit": "释放预留额度", "release unused reserved credit": "释放未用预留额度", "release unused credit": "释放未用额度" }; const ledgerTypeLabel = (t: string) => LEDGER_TYPE_LABEL[t] ?? t; const ledgerReasonLabel = (r: string) => LEDGER_REASON_LABEL[r] ?? r; // 分页页码窗口:页数多时折叠成 1 … 当前±1 … 末页(≤7 页则全展开) const RECHARGE: Array<{ amt: number; gift: string; bonus: boolean; bonusAmt: number; ribbon?: string }> = [ { amt: 100, gift: "无赠送", bonus: false, bonusAmt: 0 }, { amt: 500, gift: "+ ¥30 赠送", bonus: true, bonusAmt: 30, ribbon: "推荐" }, { amt: 1000, gift: "+ ¥80 赠送", bonus: true, bonusAmt: 80 }, { amt: 3000, gift: "+ ¥300 赠送", bonus: true, bonusAmt: 300 } ]; const STAGES: Array<{ k: string; color: string; bucket: keyof BillingTrend["by_stage"] }> = [ { k: "视频片段(Seedance)", color: "var(--heat)", bucket: "video" }, { k: "故事板(image-2)", color: "var(--accent-forest)", bucket: "storyboard" }, { k: "基础资产", color: "var(--black-alpha-56)", bucket: "base" }, { k: "脚本 LLM", color: "var(--black-alpha-32)", bucket: "script" } ]; export function AccountPage({ billing, projects, onRecharge }: { billing: BillingSummary | null; projects: Project[]; onRecharge: (amount: number, bonus: number) => void | Promise; }) { const [tab, setTab] = useState("overview"); const [recharge, setRecharge] = useState(500); const [customAmt, setCustomAmt] = useState(""); // 账户页数据本页自取(不再走全局 bootstrap):充值后 bump 刷新流水/趋势 const [reloadFlag, setReloadFlag] = useState(0); // 趋势(day,首屏)+ 团队成员(成员/限额 tab)本页懒加载 const [trend, setTrend] = useState(null); const [teamMembers, setTeamMembers] = useState([]); useEffect(() => { let alive = true; api.billingTrend().then((d) => { if (alive) setTrend(d); }).catch(() => {}); api.teamMembers().then((m) => { if (alive) setTeamMembers(m); }).catch(() => {}); return () => { alive = false; }; }, [reloadFlag]); // 账单流水分页:服务端分页(总数随流水增长,不再写死 100),每页 10 条 const BILLS_PER_PAGE = 10; const [billPage, setBillPage] = useState(1); const [ledgerRows, setLedgerRows] = useState([]); const [ledgerCount, setLedgerCount] = useState(0); const [ledgersLoading, setLedgersLoading] = useState(true); useEffect(() => { let alive = true; setLedgersLoading(true); api.ledgers(billPage, BILLS_PER_PAGE).then((data) => { if (!alive) return; setLedgerRows(data.results); setLedgerCount(data.count); }).catch(() => {}).finally(() => { if (alive) setLedgersLoading(false); }); return () => { alive = false; }; }, [billPage, reloadFlag]); const billTotalPages = Math.max(1, Math.ceil(ledgerCount / BILLS_PER_PAGE)); const safeBillPage = Math.min(billPage, billTotalPages); const selectedCard = RECHARGE.find((item) => item.amt === recharge); const effectiveAmount = Number(customAmt) > 0 ? Number(customAmt) : recharge; const effectiveBonus = Number(customAmt) > 0 ? 0 : selectedCard?.bonusAmt || 0; async function submitRecharge() { if (effectiveAmount <= 0) return; await onRecharge(effectiveAmount, effectiveBonus); setCustomAmt(""); setBillPage(1); setReloadFlag((n) => n + 1); // 充值后刷新流水/趋势(余额由全局 action→loadData 刷新) } const balance = Number(billing?.account.balance || 0); const used = Number(billing?.charged_total || 0); const memberLimit = teamMembers.reduce((sum, m) => sum + Math.max(0, Number(m.monthly_credit_limit || 0)), 0); const limit = memberLimit || balance; const left = Math.max(0, limit - used); const pct = limit > 0 ? Math.min(100, (used / limit) * 100) : 0; // 消费趋势(日/周/月可切)+ 按阶段 / 按项目分布 —— 全部来自真实 CHARGE 流水 // day 用首屏 prop;week/month 切换时按真接口拉对应区间(缺接口已补 ?range=) const [range, setRange] = useState("day"); const [rangeTrend, setRangeTrend] = useState(null); useEffect(() => { if (range === "day") { setRangeTrend(null); return; } let alive = true; api.billingTrend(range).then((data) => { if (alive) setRangeTrend(data); }).catch(() => {}); return () => { alive = false; }; }, [range]); const activeTrend = range === "day" ? trend : rangeTrend; const meta = RANGE_META[range]; const daily = activeTrend?.daily ?? []; const peak = Number(activeTrend?.peak || 0); const total14 = Number(activeTrend?.total_14d ?? used); const avgValue = daily.length ? total14 / daily.length : 0; const stageTotal = STAGES.reduce((sum, s) => sum + Number(trend?.by_stage[s.bucket] || 0), 0) || used; const projectSpend = (id: string) => Number(trend?.by_project[id] || 0); return (

消费

// 余额 · 充值 · 4 维消费视图 + 账单流水
团队余额
{money(balance)}
// 充值累加 · 不重置
本月限额
{money(limit)}
// 按自然月重置
当月已用
{money(used)}
// 占比 {pct.toFixed(1)}% · {pct >= 80 ? "注意" : "健康"}
团队月剩余 {money(left)} 使用率 {pct.toFixed(1)}%

快速充值

// 充值后立刻到账,可开发票 · 仅超管可操作
已选 ¥{effectiveAmount}{effectiveBonus > 0 ? ` + ¥${effectiveBonus} 赠送` : ""}
{RECHARGE.map((item) => (
setRecharge(item.amt)} > {item.ribbon && {item.ribbon}}
¥{item.amt}
{item.gift}
))}
自定义金额
setCustomAmt(event.target.value)} />

消费趋势

{meta.sub} {(["day", "week", "month"] as TrendRange[]).map((r) => ( ))}
{daily.map((d) => { const amt = Number(d.amount); const h = peak > 0 ? Math.max(amt > 0 ? 4 : 0, (amt / peak) * 100) : 0; const isPeak = peak > 0 && amt === peak; return (
); })}
{daily.map((d, i) => ( {i % 2 === 0 ? d.label : ""} ))}
{meta.totalLabel}{money(total14)}
{meta.avgLabel}{money(avgValue)}
峰值{money(peak)}

本月按阶段分布

// PRD §5.3.5 扣费规则 · 仅确认后扣
{STAGES.map((s) => { const amt = Number(trend?.by_stage[s.bucket] || 0); const w = stageTotal > 0 ? Math.min(100, (amt / stageTotal) * 100) : 0; return (
{s.k}{money(amt)}
); })}
合计{money(used)}

扣费 + 四层额度预检规则

// PRD §5.3.5 + §10.3 · 对接团队请以此页为准
① 失败不扣:模型超时 / 内容审核拦截 / 生成异常一律不扣费。
② 用户重跑不扣首次:第一次重跑保留原扣费,第二次起按次结算。
③ 仅在你点击 [ 确认通过 ] 时入账。
④ 导出不再扣费,所有 token 已在过程中结算。
// 任务确认前 · 四层额度预检(任一不通过即拦截)
1个人日剩余 ≥ 任务预估 × 1.2
2个人月剩余 ≥ 同上
3团队月剩余 ≥ 同上
4团队总余额 ≥ 同上
{ledgersLoading && ledgerRows.length === 0 ? ( ) : ledgerRows.length === 0 ? ( ) : ledgerRows.map((l) => ( ))}
时间项目 / 类型详情成员状态金额
// 加载中…
// 暂无账单流水
{new Date(l.created_at).toLocaleString("zh-CN")} {ledgerTypeLabel(l.ledger_type)} {ledgerReasonLabel(l.reason)} {l.user_label ? {l.user_label.slice(0, 1).toUpperCase()}{l.user_label} : 系统} 成功 {l.amount}
{ledgerCount > BILLS_PER_PAGE && (
// 共 {ledgerCount} 条 · 第 {safeBillPage} / {billTotalPages} 页
{pageWindow(safeBillPage, billTotalPages).map((p, i) => ( p === "ellipsis" ? … : ))}
)}
{projects.map((p) => { const spend = projectSpend(p.id); return ( ); })}
项目当前阶段状态消耗
{p.name}{p.current_stage}{p.status} 0 ? "neg" : "zero"}>{money(spend)}
{teamMembers.map((m) => { const monthly = Number(m.monthly_credit_limit || 0); return ( ); })}
成员角色已用 / 月度额度状态
{m.user.username.slice(0, 1).toUpperCase()}{m.user.username}{ROLE_LABEL[m.role] || m.role}{money(m.month_charged || 0)} / {monthly > 0 ? money(monthly) : "不限"}{STATUS_LABEL[m.status] || m.status}
); }