@@ -1,5 +1,5 @@
import { useEffect , useMemo , useState } from "react" ;
import { ArrowLeft , CreditCard , X } from "lucide-react" ;
import { CreditCard , X } from "lucide-react" ;
import { createPortal } from "react-dom" ;
import { api } from "../api" ;
import type { BillingSummary , BillingTrend , Ledger , Project , Team , TeamMember } from "../types" ;
@@ -63,11 +63,11 @@ const RECHARGE: Array<{ amt: number; gift: string; bonus: boolean; bonusAmt: num
] ;
const MIN_RECHARGE_AMOUNT = 50 ;
const STAGES : Array < { k : string ; color : string ; bucket : keyof BillingTrend [ "by_stage" ] } > = [
{ k : "视频片段(Seedance)" , color : "var(--klein)" , 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" }
const STAGES : Array < { k : string ; short : string ; color : string ; bucket : keyof BillingTrend [ "by_stage" ] } > = [
{ k : "视频片段(Seedance)" , short : "视频生成" , color : "var(--klein)" , bucket : "video" } ,
{ k : "故事板(image-2)" , short : "故事板" , color : "var(--accent-forest)" , bucket : "storyboard" } ,
{ k : "基础资产" , short : "基础资产" , color : "var(--black-alpha-56)" , bucket : "base" } ,
{ k : "脚本 LLM" , short : "脚本生成" , color : "var(--black-alpha-32)" , bucket : "script" }
] ;
// 充值扫码弹窗 · 复用设计系统 .modal-bg/.modal 外壳(QR 占位 + 金额 + 赠送 + 5 分钟有效 + 双按钮)
@@ -116,7 +116,7 @@ function TopupModal({ open, channel, amount, bonus, rate, close, onDone }: {
) ;
}
export function AccountPage ( { billing , projects , team , navigate , onRecharge , onNotify } : {
export function AccountPage ( { billing , projects , team , onRecharge , onNotify } : {
billing : BillingSummary | null ;
projects : Project [ ] ;
// team prop 用于读取团队级月限额(与团队管理页保持同一来源 · #14)
@@ -244,6 +244,11 @@ export function AccountPage({ billing, projects, team, navigate, onRecharge, onN
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 topStage = STAGES . reduce ( ( best , s ) = > {
const amt = Number ( trend ? . by_stage [ s . bucket ] || 0 ) ;
return amt > best . amt ? { label : s.short , amt } : best ;
} , { label : "暂无消耗" , amt : 0 } ) ;
const topPct = stageTotal > 0 ? Math . round ( ( topStage . amt / stageTotal ) * 100 ) : 0 ;
const projectSpend = ( id : string ) = > Number ( trend ? . by_project [ id ] || 0 ) ;
// ── 项目筛选(状态;真实数据无 owner/per-range,故只做状态过滤)──
@@ -282,101 +287,87 @@ export function AccountPage({ billing, projects, team, navigate, onRecharge, onN
return (
< section className = "account-page" >
< div className = "page -head" >
< button className = "ac-back" type = "button" onClick = { ( ) = > navigate ( "dashboard" ) } aria-label = "返回" >
< ArrowLeft size = { 18 } strokeWidth = { 1.75 } / >
< / button >
< header className = "ac -head" >
< div >
< h1 > 账 户 < / h1 >
< div className = "sub" > { money ( balance ) } 余 额 · { money ( used ) } 本 月 已 用 · 充 值 与 账 单 流 水 < / div >
< h1 > 账 单 库 < / h1 >
< p > 余 额 、 充 值 与 流 水 < / p >
< / div >
< / div >
< / header >
< div className = "top-grid " >
< div className = "balance-bann er" >
< span className = "corner-tr" aria-hidden = "true" > < / span > < span className = "corner-bl" aria-hidden = "true" > < / span >
< div className = "balance-hero" >
< div className = "lbl" > 团 队 余 额 < / div >
< div className = "v" > { money ( balance ) } < / div >
< div className = "meta" > 充 值 累 加 · 不 重 置 < / div >
< div className = "ac- top" >
< div className = "ac-h ero " >
< div >
< span > 团 队 余 额 < / span >
< strong > { money ( balance ) } < / strong >
< em > 充 值 累 加 · 不 重 置 < / em >
< / div >
< div className = "balance-sub " >
< div className = "col" >
< div className = "lbl" > 本 月 限 额 < / div >
< div className = "v" > { money ( limit ) } < / div >
< div className = "meta" > 按 自 然 月 重 置 < / div >
< div className = "ac-hero-stats " >
< div >
< span > 本 月 限 额 < / span >
< strong > { money ( limit ) } < / strong >
< / div >
< div className = "col" >
< div className = "lbl" > 当 月 已 用 < / div >
< div className = "v" > { money ( used ) } < / div >
< div className = "meta" > 占 比 { pct . toFixed ( 1 ) } % · { pct >= 80 ? "注意" : "健康" } < / div >
< div >
< span > 当 月 已 用 < / span >
< strong > { money ( used ) } < / strong >
< / div >
< / div >
< div className = "balance-foot" >
< div className = "balance-meter " aria-label = { ` 本月额度使用率 ${ pct . toFixed ( 1 ) } % ` } > < span style = { { width : ` ${ pct } % ` } } / > < / div >
< div className = "balance-foot-meta" >
< div >
< div className = "ac-progress " aria-label = { ` 本月额度使用率 ${ pct . toFixed ( 1 ) } % ` } >
< span style = { { width : ` ${ Math . max ( pct , pct > 0 ? 2 : 0 ) } % ` } } / >
< / div >
< div className = "ac-progress-meta" >
< span > 团 队 月 剩 余 { money ( left ) } < / span >
< span > 使 用 率 { pct . toFixed ( 1 ) } % < / span >
< / div >
< / div >
< / div >
< div className = "pane topup-pan e" >
< div className = "topup-head" >
< div >
< h3 > 快 速 充 值 < / h3 >
< div className = "desc" > 充 值 后 立 刻 到 账 , 可 开 发 票 · 仅 超 管 可 操 作 < / div >
< / div >
< div className = "topup-selected" > 已 选 ¥ { effectiveAmount } ( 到 账 { Math . round ( effectiveAmount * pointsRate + effectiveBonus ) } 积 分 ) { effectiveBonus > 0 ? ` · 含 ${ effectiveBonus } 积分赠送 ` : "" } < / div >
< / div >
< div className = "recharge-row" >
< div className = "ac-recharg e" >
< h2 > 快 速 充 值 < / h2 >
< p > 充 值 后 立 刻 到 账 , 可 开 发 票 · 仅 超 管 可 操 作 · 已 选 ¥ { effectiveAmount } ( 到 账 { Math . round ( effectiveAmount * pointsRate + effectiveBonus ) } 积 分 ) { effectiveBonus > 0 ? ` · 含 ${ effectiveBonus } 积分赠送 ` : "" } < / p >
< div className = "ac-recharge-grid" >
{ RECHARGE . map ( ( item ) = > (
< div
< button
key = { item . amt }
className = { ` recharge-card ${ recharge === item . amt && ! customAmt ? "selected" : "" } ` }
role = "button"
tabIndex = { 0 }
type = "button"
className = { ` ac-option ${ recharge === item . amt && ! customAmt ? " active" : "" } ` }
aria-pressed = { recharge === item . amt && ! customAmt }
onClick = { ( ) = > { setRecharge ( item . amt ) ; setCustomAmt ( "" ) ; } }
onKeyDown = { ( event ) = > { if ( event . key === "Enter" || event . key === " " ) { event . preventDefault ( ) ; setRecharge ( item . amt ) ; setCustomAmt ( "" ) ; } } }
>
{ item . ribbon && < span className = "ribbon" > { item . ribbon } < / span > }
< div className = "amt" > ¥ { item . amt } < / div >
< div className = { ` gift ${ item . bonus ? "bonus " : "" } ` } > { item . gift } < / div >
< / div >
< strong > ¥ { item . amt } < / strong >
< span className = { item . bonus ? "" : "plain " } > { item . gift } < / span >
< / button >
) ) }
< / div >
< div className = "pay-row" >
< div className = "pay-title" > 自 定 义 金 额 < / div >
< input
className = { ` input ${ ! customAmountIsValid ? " is-invalid" : "" } ` }
placeholder = "最低 ¥50,可输入任意金额 "
type = "number"
min = { MIN_RECHARGE_AMOUNT }
step = "1 "
inputMode = "numeric"
value = { customAmt }
aria-invalid = { ! customAmountIsValid }
aria-describedby = "custom-recharge-error"
onChange = { ( event ) = > setCustomIntegerAmount ( event . target . value ) }
/ >
{ ! customAmountIsValid && < div className = "field-hint is-error" id = "custom-recharge-error" role = "alert" > 充 值 金 额 不 能 低 于 { MIN_RECHARGE_AMOUNT } 元 < / div > }
< div className = "pay-btn-row" >
< button className = "btn pay-method-btn pay-wechat" type = "button " aria-label = "微信支付" onClick = { ( ) = > openTopup ( "wechat" ) } >
< span className = "pay-logo" aria-hidden = "true" > < img src = "/assets/pay-wechat.png" alt = "" / > < / span >
微 信 支 付
< / button >
< button className = "btn pay-method-btn pay-alipay" type = "button " aria-label = "支付宝" onClick = { ( ) = > openTopup ( "alipay" ) } >
< span className = "pay-logo" aria-hidden = "true" > < img src = "/assets/pay-alipay.png" alt = "" / > < / span >
支 付 宝
< / button >
< / div >
< div className = "ac- pay-title" > 自 定 义 金 额 < / div >
< input
className = { ` input ${ ! customAmountIsValid ? " is-invalid" : "" } ` }
placeholder = "最低 ¥50,可输入任意金额"
type = "number "
min = { MIN_RECHARGE_AMOUNT }
step = "1"
inputMode = "numeric "
value = { customAmt }
aria-invalid = { ! customAmountIsValid }
aria-describedby = "custom-recharge-error"
onChange = { ( event ) = > setCustomIntegerAmount ( event . target . value ) }
/ >
{ ! customAmountIsValid && < div className = "field-hint is-error" id = "custom-recharge-error" role = "alert" > 充 值 金 额 不 能 低 于 { MIN_RECHARGE_AMOUNT } 元 < / div > }
< div className = "ac-pay-row" >
< button className = "ac-pay" type = "button" aria-label = "微信支付" onClick = { ( ) = > openTopup ( "wechat" ) } >
< span className = "pay-logo" aria-hidden = "true" > < img src = "/assets/pay-wechat.png " alt = "" / > < / span >
微 信 支 付
< / button >
< button className = "ac-pay" type = "button" aria-label = "支付宝" onClick = { ( ) = > openTopup ( "alipay" ) } >
< span className = "pay-logo" aria-hidden = "true" > < img src = "/assets/pay-alipay.png " alt = "" / > < / span >
支 付 宝
< / button >
< / div >
< div className = "tip" style = { { marginTop : 14 } } >
< strong > 计 费 说 明 < / strong >
1 元 = 10 积 分 。 脚 本 / 文 案 10 积 分 / 次 ; 图 片 生 成 20 积 分 / 张 ; 配 音 10 积 分 / 500 字 ;
视 频 按 时 长 与 分 辨 率 计 量 、 按 实 际 用 量 结 算 ( 如 15 秒 720 P 竖 屏 约 224 积 分 ) , 多 退 少 不 补 超 ;
拼 接 导 出 免 费 。 生 成 失 败 、 超 时 均 不 扣 费 , 自 动 退 回 预 留 积 分 。
< div className = "ac- tip" >
< strong > 计 费 说 明 < / strong >
1 元 = 10 积 分 。 脚 本 / 文 案 10 积 分 / 次 ; 图 片 生 成 20 积 分 / 张 ; 配 音 10 积 分 / 500 字 ;
视 频 按 时 长 与 分 辨 率 计 量 。 生 成 失 败 、 超 时 均 不 扣 费 。
< / div >
< / div >
< / div >
@@ -399,24 +390,34 @@ export function AccountPage({ billing, projects, team, navigate, onRecharge, onN
< button key = { r } className = { ` ac-seg ${ range === r ? " active" : "" } ` } type = "button" onClick = { ( ) = > setRange ( r ) } > { RANGE_META [ r ] . chip } < / button >
) ) }
< / div >
< div className = "ac-summary" >
< div >
< span > 本 月 已 用 < / span >
< strong > { money ( used ) } < / strong >
< / div >
< div >
< span > 本 月 剩 余 < / span >
< strong > { money ( left ) } < / strong >
< / div >
< div >
< span > 主 要 消 耗 < / span >
< strong > { topStage . amt > 0 ? ` ${ topStage . label } ${ topPct } % ` : "暂无消耗" } < / strong >
< / div >
< / div >
< div className = "trend-chart" >
< div className = "bars" >
{ 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 (
< div className = { ` bar ${ isPeak ? " peak" : "" } ` } key = { d . date } title = { ` ${ d . label } · ${ money ( amt ) } ` } >
< span style = { { height : ` ${ h } % ` } } / >
< / div >
) ;
} ) }
< / div >
< div className = "x-axis" >
{ daily . map ( ( d , i ) = > (
< span key = { d . date } > { i % 2 === 0 ? d . label : "" } < / span >
) ) }
< / div >
{ daily . length === 0 ? (
< div className = "trend-chart-empty" > 暂 无 消 费 记 录 < / div >
) : daily . map ( ( d ) = > {
const amt = Number ( d . amount ) ;
const h = peak > 0 ? Math . max ( amt > 0 ? 8 : 4 , ( amt / peak ) * 100 ) : 8 ;
return (
< div className = "ac-bar-group" key = { d . date } title = { ` ${ d . label } · ${ money ( amt ) } ` } >
< strong className = "ac-bar-value" > { amt > 0 ? Math . round ( amt ) : "" } < / strong >
< div className = "ac-bar" style = { { height : ` ${ h } % ` } } / >
< span > { d . label } < / span >
< / div >
) ;
} ) }
< / div >
< div className = "trend-foot" >
< div className = "item" > < span className = "k" > { meta . totalLabel } < / span > < span className = "v" > { money ( total14 ) } < / span > < / div >