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,8 +1,97 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Check } from "lucide-react";
|
||||
import { IconKitSvg } from "./IconKitSvg";
|
||||
import type { Product, Project, Team, User } from "../types";
|
||||
import type { Notice, Page } from "../routes/route-config";
|
||||
|
||||
const SIDEBAR_COLLAPSED_KEY = "airshelf:sidebar-collapsed";
|
||||
|
||||
// 全局命令面板(Ctrl K / 点搜索框)—— 忠实搬设计稿 SHELL_COMMANDS,href 改成真路由导航
|
||||
type Command = { id: string; group: string; label: string; sub: string; page: Page; icon: string; key?: string };
|
||||
const SHELL_COMMANDS: Command[] = [
|
||||
{ id: "dashboard", group: "导航", label: "工作台", sub: "任务队列、今日消耗、项目进度", page: "dashboard", icon: "dashboard", key: "D" },
|
||||
{ id: "products", group: "导航", label: "商品库", sub: "管理 SKU、商品图册、卖点信息", page: "products", icon: "package", key: "P" },
|
||||
{ id: "projects", group: "导航", label: "视频项目", sub: "查看五阶段短视频流水线", page: "projects", icon: "clapperboard", key: "V" },
|
||||
{ id: "asset-factory", group: "导航", label: "图片生成", sub: "模特上身图、平台套图、图片创作", page: "assetFactory", icon: "sparkles", key: "I" },
|
||||
{ id: "library", group: "导航", label: "资产库", sub: "素材、人物、场景、成片统一管理", page: "library", icon: "folder", key: "A" },
|
||||
{ id: "team", group: "导航", label: "团队", sub: "成员、权限、额度、协作记录", page: "team", icon: "users" },
|
||||
{ id: "account", group: "导航", label: "消费", sub: "余额、充值、账单流水", page: "account", icon: "creditCard" },
|
||||
{ id: "settings", group: "导航", label: "设置", sub: "个人信息、通知、安全、偏好", page: "settings", icon: "settings" },
|
||||
{ id: "messages", group: "常用动作", label: "消息中心", sub: "任务提醒、协作评论、系统通知", page: "messages", icon: "bell", key: "M" },
|
||||
{ id: "new-product", group: "常用动作", label: "新建商品", sub: "从商品信息开始生成素材与视频", page: "productCreateUpload", icon: "productPlus" },
|
||||
{ id: "new-project", group: "常用动作", label: "新建视频项目", sub: "选择商品并进入脚本配置", page: "projectWizard", icon: "clapperboard" },
|
||||
{ id: "model-photo", group: "常用动作", label: "生成模特上身图", sub: "快速生成 3:4 商品展示素材", page: "modelPhoto", icon: "users" },
|
||||
{ id: "platform-cover", group: "常用动作", label: "生成平台套图", sub: "适配电商平台封面与详情图", page: "platformCover", icon: "images" },
|
||||
{ id: "image-optimize", group: "常用动作", label: "图片创作", sub: "对话式生成、编辑、加入资产库", page: "imageOptimize", icon: "images" }
|
||||
];
|
||||
|
||||
function CommandPalette({ open, onClose, navigate }: { open: boolean; onClose: () => void; navigate: Navigate }) {
|
||||
const [query, setQuery] = useState("");
|
||||
useEffect(() => { if (open) setQuery(""); }, [open]);
|
||||
const items = useMemo(() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
return SHELL_COMMANDS.filter((cmd) => !q || [cmd.label, cmd.sub, cmd.group, cmd.id].join(" ").toLowerCase().includes(q));
|
||||
}, [query]);
|
||||
const run = (cmd: Command) => { onClose(); navigate(cmd.page); };
|
||||
if (!open) return null;
|
||||
let lastGroup = "";
|
||||
return createPortal(
|
||||
<div
|
||||
id="shell-command-bg"
|
||||
className="show"
|
||||
aria-hidden="false"
|
||||
onClick={(event) => { if (event.target === event.currentTarget) onClose(); }}
|
||||
>
|
||||
<div className="shell-command" role="dialog" aria-modal="true" aria-label="命令面板">
|
||||
<div className="shell-command-head">
|
||||
<IconKitSvg name="search" />
|
||||
<input
|
||||
id="shell-command-input"
|
||||
autoFocus
|
||||
placeholder="搜索页面、动作…"
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Escape") { event.preventDefault(); onClose(); }
|
||||
else if (event.key === "Enter" && items[0]) { event.preventDefault(); run(items[0]); }
|
||||
}}
|
||||
/>
|
||||
<span id="shell-command-count" className="shell-command-count">{items.length} 项</span>
|
||||
<button id="shell-command-close" type="button" className="shell-command-close" aria-label="关闭" onClick={onClose}>Esc</button>
|
||||
</div>
|
||||
<div id="shell-command-list" className="shell-command-list">
|
||||
{items.length === 0 && (
|
||||
<div className="shell-command-empty">
|
||||
<IconKitSvg name="search" />
|
||||
<span>没有匹配的入口</span>
|
||||
<span className="shell-command-section">// 换个关键词试试</span>
|
||||
</div>
|
||||
)}
|
||||
{items.map((cmd, i) => {
|
||||
const section = cmd.group !== lastGroup ? <div className="shell-command-section">{cmd.group}</div> : null;
|
||||
lastGroup = cmd.group;
|
||||
return (
|
||||
<div key={cmd.id}>
|
||||
{section}
|
||||
<button className={`shell-command-item${i === 0 ? " active" : ""}`} type="button" onClick={() => run(cmd)}>
|
||||
<span className="cmd-ic"><IconKitSvg name={cmd.icon} /></span>
|
||||
<span className="cmd-main">
|
||||
<span className="cmd-title">{cmd.label}</span>
|
||||
<span className="cmd-sub">{cmd.sub}</span>
|
||||
</span>
|
||||
{cmd.key && <span className="cmd-key">{cmd.key}</span>}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
|
||||
type Navigate = (page: Page) => void;
|
||||
|
||||
type NavDef = { id: string; page: Page; label: string; icon: string; badge?: number };
|
||||
@@ -50,18 +139,50 @@ export function Sidebar({ page, navigate, user, team, products, projects }: {
|
||||
const activeNav = PAGE_TO_NAV[page];
|
||||
const badges: Partial<Record<string, number>> = { products: products.length, projects: projects.length };
|
||||
const avatar = (team?.name || user.username || "A").slice(0, 1).toUpperCase();
|
||||
|
||||
// 收窄/展开导航:与设计稿 Shell.toggleSidebarCollapse 一致 —— 切 body.sidebar-collapsed
|
||||
// 类(CSS 在 design-restraint.css),并持久化到 localStorage。
|
||||
const [collapsed, setCollapsed] = useState(() => localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === "1");
|
||||
useEffect(() => {
|
||||
document.body.classList.toggle("sidebar-collapsed", collapsed);
|
||||
localStorage.setItem(SIDEBAR_COLLAPSED_KEY, collapsed ? "1" : "0");
|
||||
return () => document.body.classList.remove("sidebar-collapsed");
|
||||
}, [collapsed]);
|
||||
|
||||
// 命令面板:Ctrl/Cmd K 开关,点搜索框打开
|
||||
const [paletteOpen, setPaletteOpen] = useState(false);
|
||||
const openCommandPalette = () => setPaletteOpen(true);
|
||||
useEffect(() => {
|
||||
const onKey = (event: KeyboardEvent) => {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
|
||||
event.preventDefault();
|
||||
setPaletteOpen((value) => !value);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<aside className="sidebar">
|
||||
<div className="sidebar-head">
|
||||
<a className="brand" href="/dashboard" aria-label="Airshelf 工作台" onClick={(event) => { event.preventDefault(); navigate("dashboard"); }}>
|
||||
<span className="brand-clip"><img className="brand-logo" src="/assets/logo.png" alt="Airshelf" /></span>
|
||||
</a>
|
||||
</div>
|
||||
<button className="sidebar-toggle" type="button" aria-label="收窄导航" title="收窄导航">
|
||||
<button
|
||||
className="sidebar-toggle"
|
||||
type="button"
|
||||
aria-pressed={collapsed}
|
||||
aria-label={collapsed ? "展开导航" : "收窄导航"}
|
||||
title={collapsed ? "展开导航" : "收窄导航"}
|
||||
onClick={() => setCollapsed((value) => !value)}
|
||||
>
|
||||
<span className="sidebar-toggle-icon sidebar-toggle-icon--collapse"><IconKitSvg name="chevronLeft" size={18} strokeWidth={1.8} /></span>
|
||||
<span className="sidebar-toggle-icon sidebar-toggle-icon--expand"><IconKitSvg name="chevronRight" size={18} strokeWidth={1.8} /></span>
|
||||
</button>
|
||||
<div className="search-box" title="搜索">
|
||||
<div className="search-box" title="搜索 (Ctrl K)" role="button" tabIndex={0} onClick={openCommandPalette} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); openCommandPalette(); } }}>
|
||||
<IconKitSvg name="search" />
|
||||
<input id="global-search" placeholder="搜索" readOnly aria-label="打开全局搜索" />
|
||||
<span className="kbd">Ctrl K</span>
|
||||
@@ -90,6 +211,8 @@ export function Sidebar({ page, navigate, user, team, products, projects }: {
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<CommandPalette open={paletteOpen} onClose={() => setPaletteOpen(false)} navigate={navigate} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user