feat(*): 短剧创作工作台 - 完善章节分镜、资产管理、技能运行与生成任务恢复
This commit is contained in:
@@ -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")} />
|
||||
</>
|
||||
);
|
||||
|
||||
44
web/src/components/layout/workspace-top-bar-extension.tsx
Normal file
44
web/src/components/layout/workspace-top-bar-extension.tsx
Normal 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);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user