From ad254784e46f087bc3759e5fcf46e6f2bc30e0cb Mon Sep 17 00:00:00 2001 From: ddCat <2695092045@qq.com> Date: Fri, 31 Jul 2026 15:33:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(*):=20=E7=94=A8=E6=88=B7=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E5=8F=B0=20-=20=E7=BB=9F=E4=B8=80=E9=BB=91=E7=99=BD=E7=81=B0?= =?UTF-8?q?=E7=A9=BA=E9=97=B4=E7=95=8C=E9=9D=A2=E5=B9=B6=E6=95=B4=E7=90=86?= =?UTF-8?q?=E7=94=BB=E5=B8=83=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/layout/app-top-nav.tsx | 57 +-- .../layout/workspace-sidebar-footer.tsx | 15 +- web/src/constant/navigation-tools.ts | 24 +- web/src/layouts/user-layout.tsx | 7 +- web/src/lib/canvas/canvas-aceternity-style.ts | 4 +- web/src/lib/workspace-routes.ts | 5 + web/src/pages/home/index.tsx | 79 ++++- web/src/pages/projects/detail.tsx | 2 +- web/src/pages/projects/detail/overview.tsx | 14 +- web/src/pages/projects/index.tsx | 2 +- web/src/styles/globals.css | 334 ++++++++++++++++++ 11 files changed, 484 insertions(+), 59 deletions(-) create mode 100644 web/src/lib/workspace-routes.ts diff --git a/web/src/components/layout/app-top-nav.tsx b/web/src/components/layout/app-top-nav.tsx index 5a8e6c2ce..3e9fc2690 100644 --- a/web/src/components/layout/app-top-nav.tsx +++ b/web/src/components/layout/app-top-nav.tsx @@ -1,5 +1,5 @@ import { Infinity as InfinityIcon, PanelLeftClose, PanelLeftOpen } from "lucide-react"; -import { useEffect, useState, type ReactNode } from "react"; +import { Fragment, useEffect, useState, type ReactNode } from "react"; import { Link, useLocation, useNavigate } from "react-router"; import { ModelSetupGuide } from "@/components/layout/model-setup-guide"; @@ -7,6 +7,7 @@ import { WORKSPACE_SIDEBAR_STORAGE_KEY } from "@/components/layout/workspace-sid import { WorkspaceSidebarFooter } from "@/components/layout/workspace-sidebar-footer"; import { navigationTools, type NavigationToolSlug } from "@/constant/navigation-tools"; import { cn } from "@/lib/utils"; +import { isSpatialWorkbenchPath } from "@/lib/workspace-routes"; export function AppWorkspaceShell({ children }: { children: ReactNode }) { const { pathname } = useLocation(); @@ -14,9 +15,11 @@ export function AppWorkspaceShell({ children }: { children: ReactNode }) { const [mobileSidebarExpanded, setMobileSidebarExpanded] = useState(false); const [sidebarCollapsed, setSidebarCollapsed] = useState(() => window.localStorage.getItem(WORKSPACE_SIDEBAR_STORAGE_KEY) === "1"); const hideChrome = pathname.startsWith("/admin") || /^\/canvas\/[^/]+/.test(pathname); + const spatialWorkbench = isSpatialWorkbenchPath(pathname); const compactSidebar = sidebarCollapsed; const slug = pathname.split("/").filter(Boolean)[0]; const activeToolSlug = navigationTools.some((tool) => tool.slug === slug) ? (slug as NavigationToolSlug) : undefined; + const visibleNavigationTools = spatialWorkbench ? navigationTools : navigationTools.filter((tool) => tool.section === "创作空间"); useEffect(() => { window.localStorage.setItem(WORKSPACE_SIDEBAR_STORAGE_KEY, sidebarCollapsed ? "1" : "0"); @@ -35,9 +38,10 @@ export function AppWorkspaceShell({ children }: { children: ReactNode }) { return ( <> -
+
+ {spatialWorkbench && mobileSidebarExpanded ?
- + {user.role === "admin" ? ( + + ) : null}
@@ -89,7 +88,7 @@ export function WorkspaceSidebarFooter({ expandedClassName, collapsedClassName, 深色模式 setTheme(checked ? "dark" : "light")} aria-label="深色模式" />
- + )} > diff --git a/web/src/constant/navigation-tools.ts b/web/src/constant/navigation-tools.ts index ead3723db..5ad00357d 100644 --- a/web/src/constant/navigation-tools.ts +++ b/web/src/constant/navigation-tools.ts @@ -1,25 +1,47 @@ -import { FolderKanban, Images, ListChecks, Maximize2 } from "lucide-react"; +import { BookOpenCheck, Coins, FolderKanban, Images, ListChecks, Maximize2, Settings2 } from "lucide-react"; export const navigationTools = [ { slug: "projects", label: "短剧创作", icon: FolderKanban, + section: "创作空间", }, { slug: "canvas", label: "画布", icon: Maximize2, + section: "创作空间", }, { slug: "tasks", label: "任务", icon: ListChecks, + section: "创作空间", }, { slug: "assets", label: "素材", icon: Images, + section: "创作空间", + }, + { + slug: "skills", + label: "技能库", + icon: BookOpenCheck, + section: "工作台管理", + }, + { + slug: "wallet", + label: "积分中心", + icon: Coins, + section: "工作台管理", + }, + { + slug: "settings", + label: "设置", + icon: Settings2, + section: "工作台管理", }, ] as const; diff --git a/web/src/layouts/user-layout.tsx b/web/src/layouts/user-layout.tsx index 765d3057a..83fcb4c11 100644 --- a/web/src/layouts/user-layout.tsx +++ b/web/src/layouts/user-layout.tsx @@ -1,11 +1,16 @@ import type { ReactNode } from "react"; +import { useLocation } from "react-router"; import { CanvasDeleteProjectsDialog } from "@/components/canvas/canvas-delete-projects-dialog"; import { AppWorkspaceShell } from "@/components/layout/app-top-nav"; +import { cn } from "@/lib/utils"; +import { isSpatialWorkbenchPath } from "@/lib/workspace-routes"; export default function UserLayout({ children }: { children: ReactNode }) { + const { pathname } = useLocation(); + const spatialWorkbench = isSpatialWorkbenchPath(pathname); return ( -
+
{children}
diff --git a/web/src/lib/canvas/canvas-aceternity-style.ts b/web/src/lib/canvas/canvas-aceternity-style.ts index 316e1119b..5ff3e5c9b 100644 --- a/web/src/lib/canvas/canvas-aceternity-style.ts +++ b/web/src/lib/canvas/canvas-aceternity-style.ts @@ -10,8 +10,8 @@ export function canvasDockStyle(theme: CanvasTheme, color: string = theme.toolba boxShadow: `0 18px 52px ${theme.spatial.shadow}, inset 0 1px 0 rgba(255,255,255,.14)`, "--dock-command-bg": theme.spatial.surface, "--dock-command-hover": theme.toolbar.itemHover, - "--dock-command-active": theme.node.text, - "--dock-command-active-text": theme.canvas.background, + "--dock-command-active": theme.toolbar.activeBg, + "--dock-command-active-text": theme.toolbar.activeText, "--dock-command-danger": theme.accent.danger, "--dock-tooltip-bg": theme.spatial.elevated, "--dock-tooltip-border": theme.toolbar.border, diff --git a/web/src/lib/workspace-routes.ts b/web/src/lib/workspace-routes.ts new file mode 100644 index 000000000..301b1ec7f --- /dev/null +++ b/web/src/lib/workspace-routes.ts @@ -0,0 +1,5 @@ +export function isSpatialWorkbenchPath(pathname: string) { + // 画布列表属于用户工作台,只有具体画布编辑器与运营后台保留各自现有视觉契约。 + const canvasEditor = /^\/canvas\/[^/]+(?:\/|$)/.test(pathname); + return !canvasEditor && !pathname.startsWith("/admin"); +} diff --git a/web/src/pages/home/index.tsx b/web/src/pages/home/index.tsx index 8b7adbcf9..60d5ba93e 100644 --- a/web/src/pages/home/index.tsx +++ b/web/src/pages/home/index.tsx @@ -1,13 +1,13 @@ -import { useMemo, type ReactNode } from "react"; +import { useMemo, type CSSProperties, type ReactNode } from "react"; import { useQuery } from "@tanstack/react-query"; import { App, Button } from "antd"; -import { ArrowRight, Bot, CheckCircle2, CircleAlert, Clapperboard, Clock3, FolderKanban, Images, LayoutGrid, ListChecks, Plus, Sparkles } from "lucide-react"; +import { ArrowRight, BookOpenText, Bot, CheckCircle2, CircleAlert, Clapperboard, Clock3, FolderKanban, Images, LayoutGrid, ListChecks, Plus, Sparkles } from "lucide-react"; import { Link, useNavigate } from "react-router"; import { CanvasProjectCard } from "@/components/canvas/canvas-project-card"; import { WorkspaceErrorState, WorkspaceLoadingState } from "@/components/layout/workspace-state"; import { WorkspaceSignalIcon } from "@/components/ui/aceternity/workspace-signal-icon"; -import { projectContinueTarget, projectDetailStage, projectNextActions, projectSummaryCompletion } from "@/lib/project-workbench"; +import { projectAttentionCount, projectContinueTarget, projectDetailStage, projectNextActions, projectSummaryCompletion } from "@/lib/project-workbench"; import { getProject, listProjects, type ProjectSummary } from "@/services/api/projects"; import { createCanvasProjectWithRemoteSync } from "@/services/user-data-sync"; import { useCanvasStore } from "@/stores/canvas/use-canvas-store"; @@ -58,7 +58,7 @@ export default function IndexPage() { const loadingUserWorkspace = !userHydrated || (Boolean(user) && domainProjectsQuery.isLoading); return (
-
+
{loadingUserWorkspace ? ( ) : user && domainProjectsQuery.isError ? ( @@ -99,9 +99,10 @@ function ReturningWorkspace({ summary, detail, detailLoading, detailError, recen const continueTarget = detail ? projectContinueTarget(detail) : { href: `/projects/${summary.project.id}/overview`, title: summary.project.name, context: "打开项目概览", updatedAt: summary.project.updatedAt }; const nextActions = detail ? projectNextActions(detail, 3) : []; const completion = projectSummaryCompletion(summary); + const attentionCount = detail ? projectAttentionCount(detail) : 0; return ( <> -
+
@@ -115,8 +116,15 @@ function ReturningWorkspace({ summary, detail, detailLoading, detailError, recen
-
-
+
+ } label="剧情章节" value={summary.unitCount} detail={`${summary.completedUnitCount} 章已完成`} /> + } label="项目画布" value={summary.canvasCount} detail="已关联制作空间" /> + } label="项目资产" value={summary.assetCount} detail="角色、场景与媒体" /> + } label="需要处理" value={attentionCount} detail={attentionCount ? "建议优先处理" : "当前流程顺畅"} attention={attentionCount > 0} /> +
+ +
+
{stage.label} · @@ -138,7 +146,9 @@ function ReturningWorkspace({ summary, detail, detailLoading, detailError, recen
-
+ + +

下一步

查看项目 @@ -153,7 +163,7 @@ function ReturningWorkspace({ summary, detail, detailLoading, detailError, recen
-
+

最近项目

按最近更新时间排列

@@ -193,7 +203,7 @@ function FirstProjectWorkspace({ authenticated, canvasHydrated, recentIndependen const projectHref = authenticated ? "/projects?create=1" : `/login?next=${encodeURIComponent("/projects?create=1")}`; return ( <> -
+
影策

把一个故事推进到可交付的镜头

从章节、角色和参考图开始,逐步生成分镜、视频和可复用资产。需要自由探索时,也可以先打开一张自由画布。

@@ -205,9 +215,9 @@ function FirstProjectWorkspace({ authenticated, canvasHydrated, recentIndependen

从故事到结果

每一步都保留输入、版本和生成记录,可以随时返回调整。

-
+
{workflow.map((item, index) => ( -
+
0{index + 1}

{item.title}

{item.description}

@@ -244,11 +254,54 @@ function FirstProjectWorkspace({ authenticated, canvasHydrated, recentIndependen ); } +function WorkspaceMetric({ icon, label, value, detail, attention = false }: { icon: ReactNode; label: string; value: number; detail: string; attention?: boolean }) { + return ( +
+
+ {icon} + +
+
{label}
+
{value}
+
{detail}
+
+ ); +} + +function SpatialChapterStack({ detail, loading, projectId }: { detail?: Awaited>; loading: boolean; projectId: string }) { + const units = detail?.units.slice().sort((left, right) => left.position - right.position).slice(0, 6) || []; + const activeUnitId = units.find((unit) => unit.status !== "completed")?.id || units.at(-1)?.id; + return ( +
+
+
制作层级

章节轨道

+ {units.length ? `${units.length} 章` : "待建立"} +
+
+ {loading ? Array.from({ length: 4 }, (_, index) => ) : null} + {!loading && units.length ? units.map((unit, index) => ( + + {String(unit.position + 1).padStart(2, "0")} + {unit.title} + {unit.status === "completed" ? "已完成" : unit.id === activeUnitId ? "当前制作" : "等待推进"} + + )) : null} + {!loading && !units.length ?
创建剧情章节后
这里会形成制作层级
: null} +
+
+ ); +} + function WorkbenchActionLink({ action }: { action: ReturnType[number] }) { const Icon = action.tone === "danger" ? CircleAlert : action.tone === "attention" ? Clock3 : CheckCircle2; return ( - + {action.title}{action.description} {action.actionLabel} diff --git a/web/src/pages/projects/detail.tsx b/web/src/pages/projects/detail.tsx index 2e7b4fed1..02b261a37 100644 --- a/web/src/pages/projects/detail.tsx +++ b/web/src/pages/projects/detail.tsx @@ -72,7 +72,7 @@ export default function ProjectDetailPage() { return (
-
+
diff --git a/web/src/pages/projects/detail/overview.tsx b/web/src/pages/projects/detail/overview.tsx index d6d30cadc..4e27f7828 100644 --- a/web/src/pages/projects/detail/overview.tsx +++ b/web/src/pages/projects/detail/overview.tsx @@ -20,14 +20,14 @@ export default function ProjectOverviewView({ detail }: ProjectDetailViewProps) return (
-
+
当前任务 / {stage.label} - {attentionCount ? {attentionCount} 项待处理 : null} + {attentionCount ? {attentionCount} 项待处理 : null}

{primaryAction.title}

{primaryAction.description}

@@ -39,7 +39,7 @@ export default function ProjectOverviewView({ detail }: ProjectDetailViewProps)
-