完善前端UI

This commit is contained in:
Azmat@qq.com
2026-08-20 10:41:28 +08:00
parent dcd9bf6b5e
commit b883fed981
15 changed files with 1182 additions and 583 deletions
+98 -97
View File
@@ -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-banner">
<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-hero">
<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-pane">
<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-recharge">
<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 720P 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>
+1 -1
View File
@@ -2566,7 +2566,7 @@ export function ModelPhotoDemoPage({ variant, products, onBack, navigate }: { va
<div className="dm-form-cta">
<div className="dm-cost">
<span> <span className="v"> 20 /</span></span>
<span></span>
<span></span>
</div>
<button className="dm-gen" type="button" onClick={toRealTool}>
<Sparkles size={15} />
-6
View File
@@ -428,12 +428,6 @@ export function ModelsPage({ onNotify, onBillingChanged }: {
<div className="ml-meta">
<h3 title={m.name}>{m.name}</h3>
<p>{m.description?.trim() || (m.source === "upload" ? "真人上传的人物参考" : "AI 生成的人物参考")}</p>
<div
className={`placeholder ml-triview${m.triview ? " has-mock-media" : ""}`}
style={m.triview ? mediaStyle(m.triview) : undefined}
>
{!m.triview && <span className="ph-frame"></span>}
</div>
<div className="ml-line">
{m.is_official ? (
<>
+8 -8
View File
@@ -571,12 +571,6 @@ export function ProjectsPage({ products, projects, loading = false, navigate, op
<h1></h1>
<p></p>
</div>
<div className="vc-actions">
<button className={`vc-ghost${editMode ? " is-on" : ""}`} type="button" onClick={() => (editMode ? exitEdit() : setEditMode(true))}>
<Settings2 />
<span>{editMode ? "完成" : "管理项目"}</span>
</button>
</div>
</header>
<section className="vc-sec">
@@ -622,8 +616,14 @@ export function ProjectsPage({ products, projects, loading = false, navigate, op
</section>
<div className="vc-sec-h vc-list-h">
<h2></h2>
<p></p>
<div>
<h2></h2>
<p></p>
</div>
<button className={`vc-ghost${editMode ? " is-on" : ""}`} type="button" onClick={() => (editMode ? exitEdit() : setEditMode(true))}>
<Settings2 />
<span>{editMode ? "完成" : "管理项目"}</span>
</button>
</div>
<div className="vc-toolbar">
+2 -2
View File
@@ -75,7 +75,7 @@ export const mainNav: NavItem[] = [
{ page: "projects", label: "视频创作", icon: FolderKanban },
{ page: "pipeline", label: "生产管线", icon: WandSparkles },
{ page: "library", label: "成品库", icon: Library },
{ page: "account", label: "账", icon: Wallet }
{ page: "account", label: "账单库", icon: Wallet }
];
export const supplementNav: NavItem[] = [
@@ -96,7 +96,7 @@ export const routeLabels: Record<Page, string> = {
projectWizard: "新建视频项目",
pipeline: "生产管线",
library: "成品库",
account: "账",
account: "账单库",
team: "团队",
messages: "消息",
assetFactory: "图片工具",