feat(*): 短剧创作工作台 - 完善章节分镜、资产管理、技能运行与生成任务恢复

This commit is contained in:
ddCat
2026-08-29 23:34:46 +08:00
parent 4f07daae9e
commit 2f6832f99c
113 changed files with 7016 additions and 723 deletions

View File

@@ -6,6 +6,7 @@ import { WorkspaceCommandPalette } from "@/components/layout/workspace-command-p
import { WorkspaceSidebarNav } from "@/components/layout/workspace-sidebar-nav";
import { readWorkspaceSidebarCollapsed, writeWorkspaceSidebarCollapsed } from "@/components/layout/workspace-sidebar-state";
import { WorkspaceTopBar } from "@/components/layout/workspace-top-bar";
import { WorkspaceTopBarExtensionProvider } from "@/components/layout/workspace-top-bar-extension";
import { cn } from "@/lib/utils";
import { isSpatialWorkbenchPath } from "@/lib/workspace-routes";
@@ -69,35 +70,37 @@ export function AppWorkspaceShell({ children }: { children: ReactNode }) {
return (
<>
<div className={cn("app-workspace-shell flex h-dvh min-h-0 w-full flex-col overflow-hidden", spatialWorkbench && "is-spatial", creationWorkspace && "is-creation-workspace")}>
{!hideChrome && mobileSidebarExpanded ? <button type="button" className="app-workspace-sidebar-scrim lg:hidden" aria-label="收起侧栏" onClick={() => setMobileSidebarExpanded(false)} /> : null}
<WorkspaceTopBarExtensionProvider>
<div className={cn("app-workspace-shell flex h-dvh min-h-0 w-full flex-col overflow-hidden", spatialWorkbench && "is-spatial", creationWorkspace && "is-creation-workspace")}>
{!hideChrome && mobileSidebarExpanded ? <button type="button" className="app-workspace-sidebar-scrim lg:hidden" aria-label="收起侧栏" onClick={() => setMobileSidebarExpanded(false)} /> : null}
<div className="app-workspace-main-row flex min-h-0 min-w-0 flex-1 overflow-hidden">
{!hideChrome ? (
<aside
className={cn(
"app-workspace-sidebar flex h-full shrink-0 flex-col overflow-hidden",
mobileSidebarExpanded && "is-mobile-expanded",
desktopSidebarCollapsed && "is-collapsed",
)}
>
<WorkspaceSidebarNav
collapsed={desktopSidebarCollapsed}
onNavigate={handleNavClick}
onOpenSearch={() => setPaletteOpen(true)}
onExpand={expandDesktopSidebar}
/>
</aside>
) : null}
<div className="app-workspace-main-row flex min-h-0 min-w-0 flex-1 overflow-hidden">
{!hideChrome ? (
<aside
className={cn(
"app-workspace-sidebar flex h-full shrink-0 flex-col overflow-hidden",
mobileSidebarExpanded && "is-mobile-expanded",
desktopSidebarCollapsed && "is-collapsed",
)}
>
<WorkspaceSidebarNav
collapsed={desktopSidebarCollapsed}
onNavigate={handleNavClick}
onOpenSearch={() => setPaletteOpen(true)}
onExpand={expandDesktopSidebar}
/>
</aside>
) : null}
<div className="app-workspace-stage relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
{showGlobalTopBar ? <WorkspaceTopBar sidebarOpen={isMobileViewport() ? mobileSidebarExpanded : !desktopSidebarCollapsed} onToggleSidebar={toggleSidebar} /> : null}
<div className="relative min-h-0 min-w-0 flex-1 overflow-hidden">{children}</div>
<div className="app-workspace-stage relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
{showGlobalTopBar ? <WorkspaceTopBar sidebarOpen={isMobileViewport() ? mobileSidebarExpanded : !desktopSidebarCollapsed} onToggleSidebar={toggleSidebar} /> : null}
<div className="relative min-h-0 min-w-0 flex-1 overflow-hidden">{children}</div>
</div>
</div>
</div>
<WorkspaceCommandPalette open={paletteOpen} onClose={() => setPaletteOpen(false)} />
</div>
<WorkspaceCommandPalette open={paletteOpen} onClose={() => setPaletteOpen(false)} />
</div>
</WorkspaceTopBarExtensionProvider>
<ModelSetupGuide hidden={pathname === "/login" || pathname === "/register" || pathname.startsWith("/admin")} />
</>
);

View File

@@ -0,0 +1,44 @@
import { createContext, useCallback, useContext, useId, useLayoutEffect, useMemo, useState, type ReactNode } from "react";
type WorkspaceTopBarRegistration = {
id: string;
content: ReactNode;
};
type WorkspaceTopBarRegistrar = {
register: (registration: WorkspaceTopBarRegistration) => void;
unregister: (id: string) => void;
};
const WorkspaceTopBarRegistrarContext = createContext<WorkspaceTopBarRegistrar | null>(null);
const WorkspaceTopBarContentContext = createContext<ReactNode>(null);
export function WorkspaceTopBarExtensionProvider({ children }: { children: ReactNode }) {
const [registration, setRegistration] = useState<WorkspaceTopBarRegistration | null>(null);
const register = useCallback((next: WorkspaceTopBarRegistration) => setRegistration(next), []);
const unregister = useCallback((id: string) => setRegistration((current) => current?.id === id ? null : current), []);
const registrar = useMemo(() => ({ register, unregister }), [register, unregister]);
return (
<WorkspaceTopBarRegistrarContext.Provider value={registrar}>
<WorkspaceTopBarContentContext.Provider value={registration?.content || null}>
{children}
</WorkspaceTopBarContentContext.Provider>
</WorkspaceTopBarRegistrarContext.Provider>
);
}
export function useWorkspaceTopBarExtension(content: ReactNode) {
const registrar = useContext(WorkspaceTopBarRegistrarContext);
const id = useId();
useLayoutEffect(() => {
if (!registrar) return;
registrar.register({ id, content });
return () => registrar.unregister(id);
}, [content, id, registrar]);
}
export function useWorkspaceTopBarContent() {
return useContext(WorkspaceTopBarContentContext);
}

View File

@@ -3,6 +3,7 @@ import { Link, useLocation } from "react-router";
import { SystemAnnouncementCenter } from "@/components/layout/system-announcement-center";
import { WorkspaceAccountMenu } from "@/components/layout/workspace-account-menu";
import { useWorkspaceTopBarContent } from "@/components/layout/workspace-top-bar-extension";
import { AnimatedThemeToggler } from "@/components/ui/animated-theme-toggler";
import { useThemeStore } from "@/stores/use-theme-store";
import { useUserStore } from "@/stores/use-user-store";
@@ -24,21 +25,23 @@ export function WorkspaceTopBar({ sidebarOpen, onToggleSidebar }: { sidebarOpen:
const setTheme = useThemeStore((state) => state.setTheme);
const user = useUserStore((state) => state.user);
const { pathname } = useLocation();
const extension = useWorkspaceTopBarContent();
const slug = pathname.split("/").filter(Boolean)[0];
const pageTitle = (slug && PAGE_TITLES[slug]) || "影策";
return (
<header className="app-workspace-topbar flex shrink-0 items-center justify-between gap-3 px-3 sm:px-4">
<div className="flex min-w-0 items-center gap-1.5">
<header className={`app-workspace-topbar flex shrink-0 items-center justify-between gap-2 px-3 sm:px-4 ${extension ? "has-extension" : ""}`}>
<div className="flex min-w-0 flex-1 items-center gap-2">
<button type="button" className="app-workspace-topbar-icon-button" aria-label={sidebarOpen ? "收起侧栏" : "展开侧栏"} onClick={onToggleSidebar}>
{sidebarOpen ? <PanelLeftClose className="size-4" /> : <PanelLeftOpen className="size-4" />}
</button>
<nav className="flex min-w-0 items-center gap-2 text-[var(--fs-caption)] text-foreground/50" aria-label="当前位置">
<nav className={`${extension ? "hidden 2xl:flex" : "flex"} min-w-0 items-center gap-2 text-[var(--fs-caption)] text-foreground/50`} aria-label="当前位置">
<Link to="/" className="shrink-0 font-medium text-foreground/70 transition-colors hover:text-foreground">影策</Link>
<span className="shrink-0 text-foreground/30">/</span>
<span className="truncate font-medium text-foreground">{pageTitle}</span>
</nav>
{extension ? <div className="min-w-0 flex-1">{extension}</div> : null}
</div>
<div className="flex shrink-0 items-center gap-1">