feat(core/frontend): pipeline stage editor (burn-in controls) + double-submit guard & button greying
Pipeline (脚本→资产→故事板→视频→拼接): - Stage1 render real script shots + wire 确认脚本→adopt (advance stage) - Stage2 add person/scene AI-生成 buttons + clickable category tabs - Stage4 auto-poll videos to completion + per-segment upload + real frame thumbnails + download - Stage5 real timeline editor: clips undo/redo/split/copy/delete/drag-reorder/zoom, subtitle style + per-clip text editor, transition select (xfade preview), BGM upload + volume, save draft, export-with-save → shows/download final MP4 - embedded asset URLs everywhere (beat assets pagination) UX: re-entry guard in action() (no double-submit anywhere) + greyed :disabled styles for btn-aigen/chat-mode/pill-cta/tl-action so generate buttons visibly disable while generating. Also includes prior uncommitted frontend work: settings preferences/sessions/avatar, asset delete, account/team/products pages, fonts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,15 @@
|
||||
import { useState } from "react";
|
||||
import type { BillingSummary, Ledger, Project, TeamMember } from "../types";
|
||||
import { useEffect, useState } from "react";
|
||||
import { api } from "../api";
|
||||
import type { BillingSummary, BillingTrend, Ledger, Project, TeamMember } from "../types";
|
||||
import { money } from "./stage-config";
|
||||
|
||||
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: "月均" }
|
||||
};
|
||||
|
||||
type Tab = "overview" | "by-project" | "by-member" | "bills";
|
||||
|
||||
const RECHARGE: Array<{ amt: number; gift: string; bonus: boolean; bonusAmt: number; ribbon?: string }> = [
|
||||
@@ -11,16 +19,17 @@ const RECHARGE: Array<{ amt: number; gift: string; bonus: boolean; bonusAmt: num
|
||||
{ amt: 3000, gift: "+ ¥300 赠送", bonus: true, bonusAmt: 300 }
|
||||
];
|
||||
|
||||
const STAGES: Array<{ k: string; color: string }> = [
|
||||
{ k: "视频片段(Seedance)", color: "var(--heat)" },
|
||||
{ k: "故事板(image-2)", color: "var(--accent-forest)" },
|
||||
{ k: "基础资产", color: "var(--black-alpha-56)" },
|
||||
{ k: "脚本 LLM", color: "var(--black-alpha-32)" }
|
||||
const STAGES: Array<{ k: string; color: string; bucket: keyof BillingTrend["by_stage"] }> = [
|
||||
{ k: "视频片段(Seedance)", color: "var(--heat)", 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" }
|
||||
];
|
||||
|
||||
export function AccountPage({ billing, ledgers, projects, teamMembers, onRecharge }: {
|
||||
export function AccountPage({ billing, ledgers, trend, projects, teamMembers, onRecharge }: {
|
||||
billing: BillingSummary | null;
|
||||
ledgers: Ledger[];
|
||||
trend: BillingTrend | null;
|
||||
projects: Project[];
|
||||
teamMembers: TeamMember[];
|
||||
onRecharge: (amount: number, bonus: number) => void | Promise<unknown>;
|
||||
@@ -46,6 +55,26 @@ export function AccountPage({ billing, ledgers, projects, teamMembers, onRecharg
|
||||
const left = Math.max(0, limit - used);
|
||||
const pct = limit > 0 ? Math.min(100, (used / limit) * 100) : 0;
|
||||
|
||||
// 消费趋势(日/周/月可切)+ 按阶段 / 按项目分布 —— 全部来自真实 CHARGE 流水
|
||||
// day 用首屏 prop;week/month 切换时按真接口拉对应区间(缺接口已补 ?range=)
|
||||
const [range, setRange] = useState<TrendRange>("day");
|
||||
const [rangeTrend, setRangeTrend] = useState<BillingTrend | null>(null);
|
||||
useEffect(() => {
|
||||
if (range === "day") { setRangeTrend(null); return; }
|
||||
let alive = true;
|
||||
api.billingTrend(range).then((data) => { if (alive) setRangeTrend(data); }).catch(() => {});
|
||||
return () => { alive = false; };
|
||||
}, [range]);
|
||||
const activeTrend = range === "day" ? trend : rangeTrend;
|
||||
const meta = RANGE_META[range];
|
||||
|
||||
const daily = activeTrend?.daily ?? [];
|
||||
const peak = Number(activeTrend?.peak || 0);
|
||||
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 projectSpend = (id: string) => Number(trend?.by_project[id] || 0);
|
||||
|
||||
return (
|
||||
<section className="account-page">
|
||||
<div className="page-head">
|
||||
@@ -137,32 +166,51 @@ export function AccountPage({ billing, ledgers, projects, teamMembers, onRecharg
|
||||
<div className="pane trend-pane">
|
||||
<div className="trend-head">
|
||||
<h3>消费趋势</h3>
|
||||
<span className="sub">// 近 14 天 · 单位 ¥</span>
|
||||
<span className="sub">{meta.sub}</span>
|
||||
<span className="spacer"></span>
|
||||
<button className="chip active" type="button">日</button>
|
||||
<button className="chip" type="button">周</button>
|
||||
<button className="chip" type="button">月</button>
|
||||
{(["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>
|
||||
))}
|
||||
</div>
|
||||
<div className="trend-chart">
|
||||
<div className="bars"></div>
|
||||
<div className="x-axis"></div>
|
||||
<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>
|
||||
</div>
|
||||
<div className="trend-foot">
|
||||
<div className="item"><span className="k">14 天合计</span><span className="v">{money(used)}</span></div>
|
||||
<div className="item"><span className="k">日均</span><span className="v">{money(used / 14)}</span></div>
|
||||
<div className="item"><span className="k">峰值</span><span className="v warn">{money(used)}</span></div>
|
||||
<div className="item"><span className="k">{meta.totalLabel}</span><span className="v">{money(total14)}</span></div>
|
||||
<div className="item"><span className="k">{meta.avgLabel}</span><span className="v">{money(avgValue)}</span></div>
|
||||
<div className="item"><span className="k">峰值</span><span className="v warn">{money(peak)}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pane stage-pane">
|
||||
<h3>本月按阶段分布</h3>
|
||||
<div className="desc">// PRD §5.3.5 扣费规则 · 仅确认后扣</div>
|
||||
{STAGES.map((s) => (
|
||||
<div key={s.k}>
|
||||
<div className="usage-line"><span className="k">{s.k}</span><span className="v">{money(0)}</span></div>
|
||||
<div className="usage-bar"><span style={{ width: "0%", background: s.color }} /></div>
|
||||
</div>
|
||||
))}
|
||||
{STAGES.map((s) => {
|
||||
const amt = Number(trend?.by_stage[s.bucket] || 0);
|
||||
const w = stageTotal > 0 ? Math.min(100, (amt / stageTotal) * 100) : 0;
|
||||
return (
|
||||
<div key={s.k}>
|
||||
<div className="usage-line"><span className="k">{s.k}</span><span className="v">{money(amt)}</span></div>
|
||||
<div className="usage-bar"><span style={{ width: `${w}%`, background: s.color }} /></div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="total"><span>合计</span><span className="v">{money(used)}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -208,9 +256,12 @@ export function AccountPage({ billing, ledgers, projects, teamMembers, onRecharg
|
||||
<table className="billing-table">
|
||||
<thead><tr><th>项目</th><th>当前阶段</th><th>状态</th><th style={{ textAlign: "right" }}>消耗</th></tr></thead>
|
||||
<tbody>
|
||||
{projects.map((p) => (
|
||||
<tr key={p.id}><td>{p.name}</td><td>{p.current_stage}</td><td>{p.status}</td><td className="zero">{money(0)}</td></tr>
|
||||
))}
|
||||
{projects.map((p) => {
|
||||
const spend = projectSpend(p.id);
|
||||
return (
|
||||
<tr key={p.id}><td>{p.name}</td><td>{p.current_stage}</td><td>{p.status}</td><td className={spend > 0 ? "neg" : "zero"}>{money(spend)}</td></tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user