import { useCallback, useEffect, useRef, useState } from "react"; import QRCode from "qrcode"; import { Coins, ReceiptText, Ticket, QrCode } from "lucide-react"; import { useTenant } from "../shell/AppShell"; import { myUsage, redeemCode, billingOrders, billingPacks, createWechatOrder, orderStatus, fmtCredits, type MyUsage, type TopupOrder, type Pack } from "../api"; import { Badge, Button, Dialog, Input, Panel, Table, Tr, Td, cn, useToast } from "../ui"; // 用量与账单:余额 + 兑换码充值(P5.1) + 消耗趋势 + 最近消耗/充值。 // 微信扫码支付在 P5.2 挂上(渠道适配器已留位),此前不做假支付入口。 export function Usage() { const toast = useToast(); const { ctx, refresh } = useTenant(); const [days, setDays] = useState(7); const [u, setU] = useState(null); const [orders, setOrders] = useState([]); const [code, setCode] = useState(""); const [busy, setBusy] = useState(false); const [packs, setPacks] = useState([]); const [wechatOn, setWechatOn] = useState(false); // 服务端配了商户号才亮 const [paying, setPaying] = useState(null); // 正在扫码支付的包 const load = useCallback(() => { myUsage(days).then(setU).catch(() => {}); billingOrders().then(setOrders).catch(() => {}); billingPacks() .then((r) => { setPacks(r.packs); setWechatOn(r.channels.includes("wechat")); }) .catch(() => {}); }, [days]); useEffect(load, [load, ctx?.tenant?.id]); const canTopup = ctx?.role !== "viewer"; // 真闸在后端(≥member),这里只是不摆没用的输入框 const redeem = async () => { if (!code.trim() || busy) return; setBusy(true); try { const r = await redeemCode(code.trim()); toast.push("success", `已入账,当前余额 ${fmtCredits(r.balance_micro)} 积分`); setCode(""); load(); refresh(); // 顶栏/概览的余额也跟着变 } catch (e) { toast.push("error", (e as Error).message); } finally { setBusy(false); } }; const maxTok = Math.max(...(u?.trend.map((d) => d.total_tok) ?? []), 1); const fmtTok = (n: number) => (n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : `${n}`); return (

用量与账单

{ctx?.tenant?.name ?? "…"} · 消耗以积分计

{/* 余额 hero */}
当前余额
{fmtCredits(u?.balance_micro ?? ctx?.credit_balance_micro ?? 0)} 积分
{u?.credit_enforce && (u?.balance_micro ?? 0) <= 0 ? ( 余额不足,任务提交将被拒绝 ) : null}
{canTopup ? (
{/* 微信扫码:服务端配了商户号且有在售包才出现 */} {wechatOn && packs.length > 0 && (
微信扫码充值
{packs.map((p) => ( ))}
)}
兑换码充值
setCode(e.target.value)} onKeyDown={(e) => e.key === "Enter" && redeem()} placeholder="SDX-XXXX-XXXX-XXXX" /> {wechatOn ? "兑换码请向平台获取。" : "微信扫码支付即将上线;兑换码请向平台获取。"}
) : (

你是只读角色,充值请联系组织管理员。

)}
{/* 趋势 */} {[7, 30].map((d) => ( ))}
}>
{(u?.trend ?? []).map((d) => (
{d.day.slice(-2)}
))} {(!u || u.trend.length === 0) &&

区间内暂无消耗

}
合计 {fmtTok(u?.totals.total_tok ?? 0)} tok {fmtCredits(u?.totals.credits_micro ?? 0)} 积分 {u?.totals.task_count ?? 0} 次任务
{/* 最近消耗 */} {(u?.recent ?? []).map((r) => ( ))}
{r.task_id} {r.model || "—"} {fmtTok(r.total_tok)} {fmtCredits(r.credits_micro)}
{(!u || u.recent.length === 0) &&

暂无消耗记录。

}
{/* 最近充值(订单流:兑换码与将来的微信支付一个查法) */} {orders.length > 0 && ( {orders.map((o) => ( ))}
{new Date(o.created_at).toLocaleString()} {o.channel === "redeem" ? "兑换码" : o.channel === "wechat" ? "微信支付" : o.channel} +{fmtCredits(o.credits_micro)} {o.status === "paid" ? "已入账" : o.status === "pending" ? "待支付" : o.status}
)} {paying && ( { setPaying(null); if (paid) { toast.push("success", "支付成功,积分已入账"); load(); refresh(); } }} /> )}
); } // PayDialog 微信 Native 扫码支付:下单 → code_url 画二维码 → 轮询单态(2.5s; // 服务端 pending 时会顺路主动查单,收不到公网回调的环境也能确认到账)。 function PayDialog({ pack, onClose }: { pack: Pack; onClose: (paid: boolean) => void }) { const [qr, setQr] = useState(""); const [err, setErr] = useState(""); const [state, setState] = useState<"creating" | "waiting" | "paid" | "expired">("creating"); const orderRef = useRef(""); useEffect(() => { let alive = true; let timer: number | null = null; (async () => { try { const o = await createWechatOrder(pack.id); if (!alive) return; orderRef.current = o.order_id; setQr(await QRCode.toDataURL(o.code_url, { width: 240, margin: 1 })); setState("waiting"); timer = window.setInterval(async () => { try { const s = await orderStatus(orderRef.current); if (!alive) return; if (s.status === "paid") { setState("paid"); if (timer) window.clearInterval(timer); window.setTimeout(() => onClose(true), 800); } else if (s.status === "expired" || s.status === "failed") { setState("expired"); if (timer) window.clearInterval(timer); } } catch { /* 单次轮询失败忽略,下个 tick 再试 */ } }, 2500); } catch (e) { if (alive) setErr((e as Error).message); } })(); return () => { alive = false; if (timer) window.clearInterval(timer); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [pack.id]); return ( onClose(state === "paid")}>
{err ? (

{err}

) : state === "creating" ? (

正在生成支付二维码…

) : ( <> {/* 二维码底色固定纯白:扫码器对暗色主题下的低对比码识别率差 */}
微信支付二维码
¥{(pack.price_fen / 100).toFixed(2)}
{state === "paid" ? "✅ 已支付,入账中…" : state === "expired" ? "订单已过期,请关闭后重新下单" : `微信扫一扫支付,到账 ${fmtCredits(pack.credits_micro)} 积分`}
)}
); }