feat(billing): guide parents to recharge the child account

This commit is contained in:
brother7 committed 2026-10-09 08:43:05 +08:00
1 parent fe1a3775c5
commit 281e6dc151
10 files changed
+105 -14

No files matched your search

+5 -3
View File
@@ -21,7 +21,7 @@ const money = (cents: number) => `¥${(cents / 100).toFixed(2)}`;
const time = (value: string) => new Date(value).toLocaleString('zh-CN');
const unresolved = (order: WorksRechargeOrder) => order.status === 'pending' || order.status === 'manual_review';
export function PointWallet({ active = true }: { active?: boolean }) {
export function PointWallet({ active = true, accountName = '当前登录账号', accountUsername }: { active?: boolean; accountName?: string; accountUsername?: string }) {
const [summary, setSummary] = useState<WorksBillingSummary | null>(null);
const [summaryError, setSummaryError] = useState(false);
const [open, setOpen] = useState(false);
@@ -166,7 +166,7 @@ export function PointWallet({ active = true }: { active?: boolean }) {
{own && <p className="mt-1 text-[11px] text-muted-foreground">永久有效 · 预占 {points(own.reserved)} 点</p>}
{usingShared && <p className="mt-1 text-[11px] text-muted-foreground">AI 编程使用共享点数:{selected?.shared_available ? '可用' : '已用尽,请联系付款人'}</p>}
{summary?.funding_error && <p className="mt-1 text-[11px] text-muted-foreground">{summary.funding_error}</p>}
{isWorksTokenPointBalanceExhausted(own) && !usingShared && <p className="mt-1 text-[11px] text-muted-foreground">{summary?.can_recharge ? '点数已用尽,充值后可继续使用。' : '点数已用尽,请联系家长。'}</p>}
{isWorksTokenPointBalanceExhausted(own) && !usingShared && <p className="mt-1 text-[11px] text-muted-foreground">{summary?.can_recharge ? '点数已用尽,请由家长完成当前账号充值。' : '点数已用尽,当前账号暂不能充值,请联系支持。'}</p>}
{summaryError && <button className="mt-1 underline" onClick={() => void refreshSummary()}>重试余额</button>}
</div>
<button type="button" role="menuitem" data-testid="sidebar-point-wallet-open" className="flex min-h-10 w-full items-center gap-2 rounded-md px-1 text-left text-sm hover:bg-surface-subtle" onClick={() => { setOpen(true); setTab('recharge'); setOffset(0); }}><Coins className="h-4 w-4" />词元点数与充值</button>
@@ -192,6 +192,8 @@ export function PointWallet({ active = true }: { active?: boolean }) {
</div>
<Button variant="outline" size="sm" disabled={loading || busy} onClick={() => { setError(''); void refreshSummary(); void refreshPage(); }}><RefreshCw className="mr-1 h-3.5 w-3.5" />刷新</Button>
</div>
<p className="text-sm" data-testid="point-recharge-recipient">充值到:{accountName}{accountUsername && accountUsername !== accountName ? `(${accountUsername})` : ''}</p>
<p className="text-sm text-muted-foreground">为孩子充值时,请由家长登录孩子的账号完成付款。点数仅进入当前账号的钱包。</p>
{summary?.funding_error && <p className="text-sm text-muted-foreground">{summary.funding_error}</p>}
{usingShared && <p className="text-sm text-muted-foreground">AI 编程使用共享点数({selected?.shared_available ? '可用' : '已用尽'})。下方余额、充值与记录均属于你自己的钱包;充值不会自动切换付款来源。</p>}
<div className="flex gap-2" role="tablist" aria-label="词元点数记录">{(['recharge', 'orders', 'history'] as const).map((value) => <Button key={value} role="tab" aria-selected={tab === value} variant={tab === value ? 'default' : 'outline'} onClick={() => { setTab(value); setOffset(0); }}>{value === 'recharge' ? '充值' : value === 'orders' ? '充值订单' : '收支记录'}</Button>)}</div>
@@ -199,7 +201,7 @@ export function PointWallet({ active = true }: { active?: boolean }) {
{error && <p role="alert" className="text-sm text-destructive">{error}</p>}
{loading && <p role="status" className="text-sm text-muted-foreground">正在读取记录…</p>}
{tab === 'recharge' && <div className="grid gap-3">
{summary && !summary.can_recharge && <p className="text-sm text-muted-foreground">仅家长身份可以充值,请联系家长。你仍可查看自己的余额和收支。</p>}
{summary && !summary.can_recharge && <p className="text-sm text-muted-foreground">当前账号暂不能充值,请在社区账户页查看原因或联系支持。你仍可查看自己的余额和收支。</p>}
{summary?.can_recharge && !summary.payment_configured && <p className="text-sm text-muted-foreground">充值暂未开放,请稍后再试。</p>}
{pending && checkout?.id !== pending.id && <div className="rounded-lg border p-3 text-sm"><p>已有{labels[pending.status]}订单:{money(pending.amount_cents)} / {points(pending.point_amount)} 点。请先完成或联系运营核查。</p><Button variant="outline" className="mt-2" onClick={() => setCheckout(pending)}>查看原订单</Button></div>}
{(!checkout || !unresolved(checkout)) && <div className="grid grid-cols-1 gap-2 sm:grid-cols-3">{summary?.recharge_products.map((product) => <Button key={product.id} className="h-auto flex-col gap-1 py-4" variant="outline" disabled={!loaded || busy || Boolean(pending) || !summary.can_recharge || !summary.payment_configured || Boolean(unknownProduct)} onClick={() => void purchase(product.id)}><span className="text-lg font-semibold">{points(product.point_amount)} 点</span><span>{money(product.amount_cents)}</span></Button>)}</div>}
+1 -1
View File
@@ -872,7 +872,7 @@ export function Sidebar({
</div>
<div className="mt-1 grid gap-0.5">
{authUser && <PointWallet key={authUser.userId ?? authUser.username} active={accountMenuOpen} />}
{authUser && <PointWallet key={authUser.userId ?? authUser.username} active={accountMenuOpen} accountName={accountName} accountUsername={authUser.username} />}
<button
type="button"
role="menuitem"