大量修改UI
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { CreditCard, X } from "lucide-react";
|
||||
import { ArrowLeft, CreditCard, X } from "lucide-react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { api } from "../api";
|
||||
import type { BillingSummary, BillingTrend, Ledger, Project, Team, TeamMember } from "../types";
|
||||
import type { NavigateFn } from "./route-config";
|
||||
import { money, pts, stageMeta, yuan } from "./stage-config";
|
||||
import { pageWindow } from "../components/pager";
|
||||
import { useBodyScrollLock, useOverlayTransition } from "../components/overlays";
|
||||
@@ -14,9 +15,9 @@ const ROLE_PILL: Record<string, string> = { owner: "role-super", admin: "role-ad
|
||||
|
||||
type TrendRange = "day" | "week" | "month";
|
||||
const RANGE_META: Record<TrendRange, { chip: string; sub: string; totalLabel: string; avgLabel: string }> = {
|
||||
day: { chip: "日", sub: "// 近 14 天 · 单位 积分", totalLabel: "14 天合计", avgLabel: "日均" },
|
||||
week: { chip: "周", sub: "// 近 8 周 · 单位 积分", totalLabel: "8 周合计", avgLabel: "周均" },
|
||||
month: { chip: "月", sub: "// 近 6 个月 · 单位 积分", totalLabel: "6 月合计", avgLabel: "月均" }
|
||||
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";
|
||||
@@ -63,7 +64,7 @@ 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(--heat)", bucket: "video" },
|
||||
{ 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" }
|
||||
@@ -92,18 +93,18 @@ function TopupModal({ open, channel, amount, bonus, rate, close, onDone }: {
|
||||
<span className="corner-tr">+</span><span className="corner-bl">+</span>
|
||||
<div className="modal-h">
|
||||
<div className="ic-m"><CreditCard size={16} /></div>
|
||||
<div className="ti">扫码支付<span>// {channelLabel}</span></div>
|
||||
<div className="ti">扫码支付<span>{channelLabel}</span></div>
|
||||
<button className="x modal-x" type="button" onClick={close} aria-label="关闭"><X size={14} /></button>
|
||||
</div>
|
||||
<div className="modal-b">
|
||||
<div className="topup-info">支付金额</div>
|
||||
<div className="topup-amt">{yuan(amount)}</div>
|
||||
<div className="topup-note">{`// 到账 ${money(Math.round(amount * rate + bonus))}`}{bonus > 0 ? `(含 ${bonus} 积分赠送)` : ""}</div>
|
||||
<div className="topup-note">{`到账 ${money(Math.round(amount * rate + bonus))}`}{bonus > 0 ? `(含 ${bonus} 积分赠送)` : ""}</div>
|
||||
<div className="topup-qr" aria-label="二维码占位">
|
||||
<div className="center">{scanLabel}<br /><span className="ref">{txRef}</span></div>
|
||||
</div>
|
||||
<div className="topup-valid">// 5 分钟内有效 · 到账后自动关闭</div>
|
||||
<div className="topup-valid">// 积分不可提现、不可转让 · 长期有效 · 发票按实际支付金额开具</div>
|
||||
<div className="topup-valid">5 分钟内有效 · 到账后自动关闭</div>
|
||||
<div className="topup-valid">积分不可提现、不可转让 · 长期有效 · 发票按实际支付金额开具</div>
|
||||
</div>
|
||||
<div className="modal-f">
|
||||
<button className="btn" type="button" onClick={close}>取消</button>
|
||||
@@ -115,11 +116,12 @@ function TopupModal({ open, channel, amount, bonus, rate, close, onDone }: {
|
||||
);
|
||||
}
|
||||
|
||||
export function AccountPage({ billing, projects, team, onRecharge, onNotify }: {
|
||||
export function AccountPage({ billing, projects, team, navigate, onRecharge, onNotify }: {
|
||||
billing: BillingSummary | null;
|
||||
projects: Project[];
|
||||
// team prop 用于读取团队级月限额(与团队管理页保持同一来源 · #14)
|
||||
team?: Team | null;
|
||||
navigate: NavigateFn;
|
||||
onRecharge: (amount: number, bonus: number) => void | Promise<unknown>;
|
||||
onNotify: (type: "success" | "error" | "info", text: string) => void;
|
||||
}) {
|
||||
@@ -281,9 +283,12 @@ export function AccountPage({ billing, projects, team, onRecharge, onNotify }: {
|
||||
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>
|
||||
<div>
|
||||
<h1>消费</h1>
|
||||
<div className="sub"><span className="mono">// 余额 · 充值 · 4 维消费视图 + 账单流水</span></div>
|
||||
<h1>账户</h1>
|
||||
<div className="sub">{money(balance)} 余额 · {money(used)} 本月已用 · 充值与账单流水</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -293,18 +298,18 @@ export function AccountPage({ billing, projects, team, onRecharge, onNotify }: {
|
||||
<div className="balance-hero">
|
||||
<div className="lbl">团队余额</div>
|
||||
<div className="v">{money(balance)}</div>
|
||||
<div className="meta">// 充值累加 · 不重置</div>
|
||||
<div className="meta">充值累加 · 不重置</div>
|
||||
</div>
|
||||
<div className="balance-sub">
|
||||
<div className="col">
|
||||
<div className="lbl">本月限额</div>
|
||||
<div className="v">{money(limit)}</div>
|
||||
<div className="meta">// 按自然月重置</div>
|
||||
<div className="meta">按自然月重置</div>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="lbl">当月已用</div>
|
||||
<div className="v">{money(used)}</div>
|
||||
<div className="meta">// 占比 {pct.toFixed(1)}% · {pct >= 80 ? "注意" : "健康"}</div>
|
||||
<div className="meta">占比 {pct.toFixed(1)}% · {pct >= 80 ? "注意" : "健康"}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="balance-foot">
|
||||
@@ -320,7 +325,7 @@ export function AccountPage({ billing, projects, team, onRecharge, onNotify }: {
|
||||
<div className="topup-head">
|
||||
<div>
|
||||
<h3>快速充值</h3>
|
||||
<div className="desc">// 充值后立刻到账,可开发票 · 仅超管可操作</div>
|
||||
<div className="desc">充值后立刻到账,可开发票 · 仅超管可操作</div>
|
||||
</div>
|
||||
<div className="topup-selected">已选 ¥{effectiveAmount}(到账 {Math.round(effectiveAmount * pointsRate + effectiveBonus)} 积分){effectiveBonus > 0 ? ` · 含 ${effectiveBonus} 积分赠送` : ""}</div>
|
||||
</div>
|
||||
@@ -377,10 +382,10 @@ export function AccountPage({ billing, projects, team, onRecharge, onNotify }: {
|
||||
</div>
|
||||
|
||||
<div className="billing-tabs" role="tablist">
|
||||
<button className={`tab ${tab === "overview" ? "active" : ""}`} type="button" onClick={() => setTab("overview")}>总览</button>
|
||||
<button className={`tab ${tab === "by-project" ? "active" : ""}`} type="button" onClick={() => setTab("by-project")}>项目 <span className="count">{projects.length}</span></button>
|
||||
<button className={`tab ${tab === "by-member" ? "active" : ""}`} type="button" onClick={() => setTab("by-member")}>成员 <span className="count">{teamMembers.length}</span></button>
|
||||
<button className={`tab ${tab === "bills" ? "active" : ""}`} type="button" onClick={() => setTab("bills")}>账单流水 <span className="count">{ledgerCount}</span></button>
|
||||
<button className={`ac-tab ${tab === "overview" ? "active" : ""}`} type="button" onClick={() => setTab("overview")}>总览</button>
|
||||
<button className={`ac-tab ${tab === "by-project" ? "active" : ""}`} type="button" onClick={() => setTab("by-project")}>项目 <span className="count">{projects.length}</span></button>
|
||||
<button className={`ac-tab ${tab === "by-member" ? "active" : ""}`} type="button" onClick={() => setTab("by-member")}>成员 <span className="count">{teamMembers.length}</span></button>
|
||||
<button className={`ac-tab ${tab === "bills" ? "active" : ""}`} type="button" onClick={() => setTab("bills")}>账单流水 <span className="count">{ledgerCount}</span></button>
|
||||
</div>
|
||||
|
||||
<div className={`tab-panel ${tab === "overview" ? "active" : ""}`}>
|
||||
@@ -391,7 +396,7 @@ export function AccountPage({ billing, projects, team, onRecharge, onNotify }: {
|
||||
<span className="sub">{meta.sub}</span>
|
||||
<span className="spacer"></span>
|
||||
{(["day", "week", "month"] as TrendRange[]).map((r) => (
|
||||
<button key={r} className={`chip${range === r ? " active" : ""}`} type="button" onClick={() => setRange(r)}>{RANGE_META[r].chip}</button>
|
||||
<button key={r} className={`ac-seg${range === r ? " active" : ""}`} type="button" onClick={() => setRange(r)}>{RANGE_META[r].chip}</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="trend-chart">
|
||||
@@ -422,7 +427,7 @@ export function AccountPage({ billing, projects, team, onRecharge, onNotify }: {
|
||||
|
||||
<div className="pane stage-pane">
|
||||
<h3>本月按阶段分布</h3>
|
||||
<div className="desc">// PRD §5.3.5 扣费规则 · 仅确认后扣</div>
|
||||
<div className="desc">PRD §5.3.5 扣费规则 · 仅确认后扣</div>
|
||||
{STAGES.map((s) => {
|
||||
const amt = Number(trend?.by_stage[s.bucket] || 0);
|
||||
const w = stageTotal > 0 ? Math.min(100, (amt / stageTotal) * 100) : 0;
|
||||
@@ -439,7 +444,7 @@ export function AccountPage({ billing, projects, team, onRecharge, onNotify }: {
|
||||
|
||||
<div className="pane rule-pane" style={{ marginTop: 16 }}>
|
||||
<h3>扣费 + 四层额度预检规则</h3>
|
||||
<div className="desc">// PRD §5.3.5 + §10.3 · 对接团队请以此页为准</div>
|
||||
<div className="desc">PRD §5.3.5 + §10.3 · 对接团队请以此页为准</div>
|
||||
<div className="rule-list">
|
||||
<strong>① 失败不扣</strong>:模型超时 / 内容审核拦截 / 生成异常一律不扣费。<br />
|
||||
<strong>② 用户重跑不扣首次</strong>:第一次重跑保留原扣费,第二次起按次结算。<br />
|
||||
@@ -447,7 +452,7 @@ export function AccountPage({ billing, projects, team, onRecharge, onNotify }: {
|
||||
<strong>④ 导出不再扣费</strong>,所有 token 已在过程中结算。
|
||||
</div>
|
||||
<div className="quota-rules">
|
||||
<div className="qr-head">// 任务确认前 · 四层额度预检(任一不通过即拦截)</div>
|
||||
<div className="qr-head">任务确认前 · 四层额度预检(任一不通过即拦截)</div>
|
||||
<div className="step"><span className="num">1</span><span><strong>个人日剩余</strong> ≥ 任务预估 × <span className="formula">1.2</span></span></div>
|
||||
<div className="step"><span className="num">2</span><span><strong>个人月剩余</strong> ≥ 同上</span></div>
|
||||
<div className="step"><span className="num">3</span><span><strong>团队月剩余</strong> ≥ 同上</span></div>
|
||||
@@ -482,9 +487,9 @@ export function AccountPage({ billing, projects, team, onRecharge, onNotify }: {
|
||||
<thead><tr><th>时间</th><th>项目 / 类型</th><th>详情</th><th>成员</th><th>状态</th><th style={{ textAlign: "right" }}>金额</th></tr></thead>
|
||||
<tbody>
|
||||
{ledgersLoading && ledgerRows.length === 0 ? (
|
||||
<tr><td colSpan={6} className="muted" style={{ textAlign: "center", padding: "24px 0" }}>// 加载中…</td></tr>
|
||||
<tr><td colSpan={6} className="muted" style={{ textAlign: "center", padding: "24px 0" }}>加载中…</td></tr>
|
||||
) : ledgerRows.length === 0 ? (
|
||||
<tr><td colSpan={6} className="muted" style={{ textAlign: "center", padding: "24px 0" }}>{billFiltered ? "// 无匹配筛选条件的账单" : "// 暂无账单流水"}</td></tr>
|
||||
<tr><td colSpan={6} className="muted" style={{ textAlign: "center", padding: "24px 0" }}>{billFiltered ? "无匹配筛选条件的账单" : "暂无账单流水"}</td></tr>
|
||||
) : ledgerRows.map((l) => (
|
||||
<tr key={l.id}>
|
||||
<td className="ts">{new Date(l.created_at).toLocaleString("zh-CN")}</td>
|
||||
@@ -502,7 +507,7 @@ export function AccountPage({ billing, projects, team, onRecharge, onNotify }: {
|
||||
</div>
|
||||
{ledgerCount > BILLS_PER_PAGE && (
|
||||
<div className="bill-pager">
|
||||
<span className="total">// 共 {ledgerCount} 条 · 第 {safeBillPage} / {billTotalPages} 页</span>
|
||||
<span className="total">共 {ledgerCount} 条 · 第 {safeBillPage} / {billTotalPages} 页</span>
|
||||
<div className="pages">
|
||||
<button type="button" disabled={safeBillPage <= 1} onClick={() => setBillPage(safeBillPage - 1)}>上一页</button>
|
||||
{pageWindow(safeBillPage, billTotalPages).map((p, i) => (
|
||||
@@ -548,7 +553,7 @@ export function AccountPage({ billing, projects, team, onRecharge, onNotify }: {
|
||||
<thead><tr><th>项目</th><th>商品</th><th>当前阶段</th><th>状态</th><th style={{ textAlign: "right" }}>消耗</th></tr></thead>
|
||||
<tbody>
|
||||
{filteredProjects.length === 0 ? (
|
||||
<tr><td colSpan={5} className="muted" style={{ textAlign: "center", padding: "24px 0" }}>// 当前筛选条件下没有项目</td></tr>
|
||||
<tr><td colSpan={5} className="muted" style={{ textAlign: "center", padding: "24px 0" }}>当前筛选条件下没有项目</td></tr>
|
||||
) : filteredProjects.map((p) => {
|
||||
const spend = projectSpend(p.id);
|
||||
const stg = projStage(p);
|
||||
@@ -587,7 +592,7 @@ export function AccountPage({ billing, projects, team, onRecharge, onNotify }: {
|
||||
<thead><tr><th>成员</th><th>角色</th><th>已用 / 月度额度</th><th>状态</th></tr></thead>
|
||||
<tbody>
|
||||
{filteredMembers.length === 0 ? (
|
||||
<tr><td colSpan={4} className="muted" style={{ textAlign: "center", padding: "24px 0" }}>// 当前筛选条件下没有成员</td></tr>
|
||||
<tr><td colSpan={4} className="muted" style={{ textAlign: "center", padding: "24px 0" }}>当前筛选条件下没有成员</td></tr>
|
||||
) : filteredMembers.map((m) => {
|
||||
const monthly = Number(m.monthly_credit_limit || 0);
|
||||
const monthUsed = Number(m.month_charged || 0);
|
||||
@@ -600,7 +605,7 @@ export function AccountPage({ billing, projects, team, onRecharge, onNotify }: {
|
||||
<td className="quota">
|
||||
<span className="used">{money(monthUsed)}</span> <span className="lim">/ {monthly > 0 ? money(monthly) : "不限"}{monthly > 0 ? ` · ${usedPct.toFixed(1)}%` : ""}</span>
|
||||
{monthly > 0 && (
|
||||
<span className="progress-mini"><span style={{ width: `${usedPct}%`, background: usedPct >= 85 ? "var(--accent-honey)" : "var(--heat)" }} /></span>
|
||||
<span className="progress-mini"><span style={{ width: `${usedPct}%`, background: usedPct >= 85 ? "var(--accent-honey)" : "var(--klein)" }} /></span>
|
||||
)}
|
||||
</td>
|
||||
<td>{STATUS_LABEL[m.status] || m.status}</td>
|
||||
|
||||
Reference in New Issue
Block a user