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:
zyc
2026-06-09 14:46:42 +08:00
co-authored by Claude Opus 4.8
parent 92826dec14
commit 0873e724bf
29 changed files with 2393 additions and 423 deletions
+32 -10
View File
@@ -1,6 +1,6 @@
import { useState } from "react";
import { CircleDollarSign, KeyRound, UserPlus } from "lucide-react";
import type { BillingSummary, Team, TeamMember, User } from "../types";
import type { BillingSummary, Notification, Team, TeamMember, User } from "../types";
import type { Page } from "./route-config";
import { money } from "./stage-config";
import { ConfirmModal, TeamModal } from "../components/overlays";
@@ -24,11 +24,12 @@ const PERM_ROWS: Array<{ cap: string; cells: [string, string, string]; last?: bo
{ cap: "创建项目 / 用 AI 流程", cells: ["✓", "✓", "✓"], last: true }
];
export function TeamPage({ team, user, members, billing, navigate, onCreateMember, onUpdateMember, onRemoveMember, onResetPassword, onRecharge }: {
export function TeamPage({ team, user, members, billing, notifications = [], navigate, onCreateMember, onUpdateMember, onRemoveMember, onResetPassword, onRecharge }: {
team: Team;
user: User;
members: TeamMember[];
billing: BillingSummary | null;
notifications?: Notification[];
navigate: (page: Page) => void;
onCreateMember: (payload: { username: string; password: string; name?: string; role?: string; monthly_credit_limit?: number }) => void | Promise<unknown>;
onUpdateMember: (id: string, payload: { role?: string; monthly_credit_limit?: number }) => void | Promise<unknown>;
@@ -72,6 +73,11 @@ export function TeamPage({ team, user, members, billing, navigate, onCreateMembe
const left = Math.max(0, limit - used);
const pct = limit > 0 ? Math.min(100, (used / limit) * 100) : 0;
// 团队动态:取最近 6 条真实通知
const feedItems = [...notifications]
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
.slice(0, 6);
const needle = search.trim().toLowerCase();
const list = rows.filter((member) => {
const name = member.user.username || "";
@@ -190,21 +196,37 @@ export function TeamPage({ team, user, members, billing, navigate, onCreateMembe
</div>
</div>
{/* 团队动态(banner 右栏)· 对齐 api-bridge renderLiveTeamActivity 的真实状态占位 */}
{/* 团队动态(banner 右栏)· 接真实 ops/notifications 事件流 */}
<div className="team-feed">
<div className="h">
<h3>团队动态</h3>
<span className="ct">// 真实动态接口待接入</span>
<span className="ct">// 最近 {Math.min(feedItems.length, 6)} 条 · 共 {notifications.length}</span>
<a className="more" id="open-feed-all" role="button" tabIndex={0} onClick={() => navigate("messages")}>全部 →</a>
</div>
<div className="feed-list">
<div className="feed-item">
<div className="av">Q</div>
<div>
<div className="txt"><span className="who">真实团队表</span><span className="act">已同步</span><span className="obj">{rows.length} 名成员</span></div>
<div className="ts">local cache</div>
{feedItems.length === 0 ? (
<div className="feed-item">
<div className="av">{(team.name || "T").slice(0, 1).toUpperCase()}</div>
<div>
<div className="txt"><span className="who">{team.name}</span><span className="act">已同步</span><span className="obj">{rows.length} 名成员</span></div>
<div className="ts">// 暂无团队动态</div>
</div>
</div>
</div>
) : (
feedItems.map((n) => (
<div className="feed-item" key={n.id}>
<div className="av">{(n.owner_label || n.source || team.name || "·").slice(0, 1).toUpperCase()}</div>
<div>
<div className="txt">
<span className="who">{n.owner_label || n.source || "系统"}</span>
<span className="act">{n.title}</span>
{n.project_name && <span className="obj">{n.project_name}</span>}
</div>
<div className="ts">{n.created_at ? new Date(n.created_at).toLocaleString("zh-CN") : n.brief}</div>
</div>
</div>
))
)}
</div>
</div>