From e6dcd7a832dfddf611925009753fcfe5911ba26e Mon Sep 17 00:00:00 2001 From: HouYunFei <1844025705@qq.com> Date: Fri, 17 Jul 2026 14:04:25 +0800 Subject: [PATCH] refactor(canvas-project): extract CanvasTopBar into its own component Move CanvasTopBar and its private helpers (MenuLabel, CompactAgentStatus, Shortcut) out of project.tsx into canvas-top-bar.tsx. The component already took all data via explicit props, so the move is mechanical with no behavior change. Drop the imports (DOCS_URL, UserStatusActions, Dropdown, several icons) that were only used by the top bar. project.tsx is now ~3080 lines, down from 3536 at the start of the refactor. Co-Authored-By: Claude Opus 4.8 (1M context) --- web/src/components/canvas/canvas-top-bar.tsx | 198 +++++++++++++++++++ web/src/pages/canvas/project.tsx | 197 +----------------- 2 files changed, 201 insertions(+), 194 deletions(-) create mode 100644 web/src/components/canvas/canvas-top-bar.tsx diff --git a/web/src/components/canvas/canvas-top-bar.tsx b/web/src/components/canvas/canvas-top-bar.tsx new file mode 100644 index 0000000..50b2162 --- /dev/null +++ b/web/src/components/canvas/canvas-top-bar.tsx @@ -0,0 +1,198 @@ +import { useEffect, useRef, useState } from "react"; +import { BookOpen, Bot, Home, Images, Menu, Plus, Redo2, Trash2, Undo2, Upload } from "lucide-react"; +import { Button, Dropdown, Modal } from "antd"; + +import { UserStatusActions } from "@/components/layout/user-status-actions"; +import { canvasThemes } from "@/lib/canvas-theme"; +import { useThemeStore } from "@/stores/use-theme-store"; +import { DOCS_URL } from "@/constant/env"; + +export function CanvasTopBar({ + title, + titleDraft, + isTitleEditing, + onTitleDraftChange, + onStartTitleEditing, + onFinishTitleEditing, + onCancelTitleEditing, + canUndo, + canRedo, + onHome, + onProjects, + onCreateProject, + onDeleteProject, + onImportImage, + onOpenPlugins, + onUndo, + onRedo, + agentOpen, + compactAgentStatus, + onToggleAgent, +}: { + title: string; + titleDraft: string; + isTitleEditing: boolean; + onTitleDraftChange: (value: string) => void; + onStartTitleEditing: () => void; + onFinishTitleEditing: () => void; + onCancelTitleEditing: () => void; + canUndo: boolean; + canRedo: boolean; + onHome: () => void; + onProjects: () => void; + onCreateProject: () => void; + onDeleteProject: () => void; + onImportImage: () => void; + onOpenPlugins: () => void; + onUndo: () => void; + onRedo: () => void; + agentOpen: boolean; + compactAgentStatus: { connected: boolean; enabled: boolean; activity: string }; + onToggleAgent: () => void; +}) { + const colorTheme = useThemeStore((state) => state.theme); + const theme = canvasThemes[colorTheme]; + const titleRef = useRef(null); + const [shortcutsOpen, setShortcutsOpen] = useState(false); + + useEffect(() => { + if (!isTitleEditing) return; + const close = (event: PointerEvent) => { + if (!titleRef.current?.contains(event.target as Node)) onFinishTitleEditing(); + }; + document.addEventListener("pointerdown", close, true); + return () => document.removeEventListener("pointerdown", close, true); + }, [isTitleEditing, onFinishTitleEditing]); + + return ( + <> +
+
+ , label: "主页", onClick: onHome }, + { key: "docs", icon: , label: "文档", onClick: () => window.open(DOCS_URL, "_blank", "noopener,noreferrer") }, + { key: "projects", icon: , label: "我的画布", onClick: onProjects }, + { type: "divider" }, + { key: "new", icon: , label: "新建画布", onClick: onCreateProject }, + { key: "delete", danger: true, icon: , label: "删除当前画布", onClick: onDeleteProject }, + { type: "divider" }, + { key: "import", icon: , label: "导入资产", onClick: onImportImage }, + { type: "divider" }, + { key: "undo", disabled: !canUndo, icon: , label: , onClick: onUndo }, + { key: "redo", disabled: !canRedo, icon: , label: , onClick: onRedo }, + ], + }} + > + + + +
+ {isTitleEditing ? ( + onTitleDraftChange(event.target.value)} + onBlur={onFinishTitleEditing} + onKeyDown={(event) => { + if (event.key === "Enter") onFinishTitleEditing(); + if (event.key === "Escape") onCancelTitleEditing(); + }} + className="max-w-[280px] bg-transparent p-0 text-left text-lg font-semibold tracking-normal outline-none" + style={{ color: theme.node.text }} + /> + ) : ( + + )} +
+ +
+ +
+ setShortcutsOpen(true)} onOpenPlugins={onOpenPlugins} /> + + +
+
+ setShortcutsOpen(false)} footer={null} centered> +
+ + + + + + + + + + + + + +
+
+ + ); +} + +function MenuLabel({ text, shortcut }: { text: string; shortcut: string }) { + return ( + + {text} + {shortcut} + + ); +} + +function CompactAgentStatus({ status, onClick }: { status: { connected: boolean; enabled: boolean; activity: string }; onClick: () => void }) { + const colorTheme = useThemeStore((state) => state.theme); + const theme = canvasThemes[colorTheme]; + const label = status.connected ? "Codex 已连接" : status.enabled ? `Codex ${status.activity || "连接中"}` : "Codex 未连接"; + const dotColor = status.connected ? "#22c55e" : status.enabled ? "#f59e0b" : theme.node.muted; + return ( + + ); +} + +function Shortcut({ keys, value }: { keys: string[]; value: string }) { + return ( +
+ + {keys.map((key, index) => ( + + {index ? + : null} + + {key} + + + ))} + + {value} +
+ ); +} diff --git a/web/src/pages/canvas/project.tsx b/web/src/pages/canvas/project.tsx index e89c4b5..fc6849a 100644 --- a/web/src/pages/canvas/project.tsx +++ b/web/src/pages/canvas/project.tsx @@ -1,25 +1,23 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import type { ChangeEvent as ReactChangeEvent, DragEvent as ReactDragEvent, MouseEvent as ReactMouseEvent, PointerEvent as ReactPointerEvent } from "react"; import { useNavigate, useParams, useSearchParams } from "react-router-dom"; -import { BookOpen, Bot, Group, Home, Images, Menu, Plus, Redo2, Trash2, Undo2, Upload, Video } from "lucide-react"; +import { Group, Video } from "lucide-react"; import { saveAs } from "file-saver"; import { requestEdit, requestGeneration, requestImageQuestion, type AiTextMessage } from "@/services/api/image"; import { requestAudioGeneration, storeGeneratedAudio } from "@/services/api/audio"; import { requestVideoGeneration, storeGeneratedVideo } from "@/services/api/video"; -import { DOCS_URL } from "@/constant/env"; import { defaultConfig, decodeChannelModel, selectableModelsByCapability, type AiConfig, type ModelCapability, useConfigStore, useEffectiveConfig } from "@/stores/use-config-store"; import { uploadImage } from "@/services/image-storage"; import { uploadMediaFile } from "@/services/file-storage"; import { nanoid } from "nanoid"; import { getDataUrlByteSize, readImageMeta } from "@/lib/image-utils"; import { canvasThemes, type CanvasBackgroundMode } from "@/lib/canvas-theme"; -import { UserStatusActions } from "@/components/layout/user-status-actions"; import { useAssetStore } from "@/stores/use-asset-store"; import { useThemeStore } from "@/stores/use-theme-store"; import { cropDataUrl, splitDataUrl, upscaleDataUrl } from "@/lib/canvas/canvas-image-data"; import { fitNodeSize, nodeSizeFromRatio } from "@/lib/canvas/canvas-node-size"; -import { App, Button, Dropdown, Modal } from "antd"; +import { App, Button, Modal } from "antd"; import { NODE_DEFAULT_SIZE, getNodeSpec } from "@/constant/canvas"; import { ActiveConnectionPath, ConnectionPath } from "@/components/canvas/canvas-connections"; import { CanvasConfigComposer } from "@/components/canvas/canvas-config-composer"; @@ -70,6 +68,7 @@ import { ensurePluginsLoaded } from "@/lib/canvas/plugin-loader"; import { registerBuiltinNodes } from "@/components/canvas/nodes/builtin-nodes"; import { CanvasPluginManagerModal } from "@/components/canvas/canvas-plugin-manager-modal"; import { CanvasRefreshShell } from "@/components/canvas/canvas-refresh-shell"; +import { CanvasTopBar } from "@/components/canvas/canvas-top-bar"; import { ConnectionCreateMenu, NodeCreateMenu, type PendingConnectionCreate } from "@/components/canvas/canvas-create-menus"; import type { CanvasNodeToolbarItem, CanvasPluginAi, CanvasPluginHost } from "@/types/canvas-plugin"; import { @@ -3086,193 +3085,3 @@ function InfiniteCanvasPage() { ); } - -function CanvasTopBar({ - title, - titleDraft, - isTitleEditing, - onTitleDraftChange, - onStartTitleEditing, - onFinishTitleEditing, - onCancelTitleEditing, - canUndo, - canRedo, - onHome, - onProjects, - onCreateProject, - onDeleteProject, - onImportImage, - onOpenPlugins, - onUndo, - onRedo, - agentOpen, - compactAgentStatus, - onToggleAgent, -}: { - title: string; - titleDraft: string; - isTitleEditing: boolean; - onTitleDraftChange: (value: string) => void; - onStartTitleEditing: () => void; - onFinishTitleEditing: () => void; - onCancelTitleEditing: () => void; - canUndo: boolean; - canRedo: boolean; - onHome: () => void; - onProjects: () => void; - onCreateProject: () => void; - onDeleteProject: () => void; - onImportImage: () => void; - onOpenPlugins: () => void; - onUndo: () => void; - onRedo: () => void; - agentOpen: boolean; - compactAgentStatus: { connected: boolean; enabled: boolean; activity: string }; - onToggleAgent: () => void; -}) { - const colorTheme = useThemeStore((state) => state.theme); - const theme = canvasThemes[colorTheme]; - const titleRef = useRef(null); - const [shortcutsOpen, setShortcutsOpen] = useState(false); - - useEffect(() => { - if (!isTitleEditing) return; - const close = (event: PointerEvent) => { - if (!titleRef.current?.contains(event.target as Node)) onFinishTitleEditing(); - }; - document.addEventListener("pointerdown", close, true); - return () => document.removeEventListener("pointerdown", close, true); - }, [isTitleEditing, onFinishTitleEditing]); - - return ( - <> -
-
- , label: "主页", onClick: onHome }, - { key: "docs", icon: , label: "文档", onClick: () => window.open(DOCS_URL, "_blank", "noopener,noreferrer") }, - { key: "projects", icon: , label: "我的画布", onClick: onProjects }, - { type: "divider" }, - { key: "new", icon: , label: "新建画布", onClick: onCreateProject }, - { key: "delete", danger: true, icon: , label: "删除当前画布", onClick: onDeleteProject }, - { type: "divider" }, - { key: "import", icon: , label: "导入资产", onClick: onImportImage }, - { type: "divider" }, - { key: "undo", disabled: !canUndo, icon: , label: , onClick: onUndo }, - { key: "redo", disabled: !canRedo, icon: , label: , onClick: onRedo }, - ], - }} - > - - - -
- {isTitleEditing ? ( - onTitleDraftChange(event.target.value)} - onBlur={onFinishTitleEditing} - onKeyDown={(event) => { - if (event.key === "Enter") onFinishTitleEditing(); - if (event.key === "Escape") onCancelTitleEditing(); - }} - className="max-w-[280px] bg-transparent p-0 text-left text-lg font-semibold tracking-normal outline-none" - style={{ color: theme.node.text }} - /> - ) : ( - - )} -
- -
- -
- setShortcutsOpen(true)} onOpenPlugins={onOpenPlugins} /> - - -
-
- setShortcutsOpen(false)} footer={null} centered> -
- - - - - - - - - - - - - -
-
- - ); -} - -function MenuLabel({ text, shortcut }: { text: string; shortcut: string }) { - return ( - - {text} - {shortcut} - - ); -} - -function CompactAgentStatus({ status, onClick }: { status: { connected: boolean; enabled: boolean; activity: string }; onClick: () => void }) { - const colorTheme = useThemeStore((state) => state.theme); - const theme = canvasThemes[colorTheme]; - const label = status.connected ? "Codex 已连接" : status.enabled ? `Codex ${status.activity || "连接中"}` : "Codex 未连接"; - const dotColor = status.connected ? "#22c55e" : status.enabled ? "#f59e0b" : theme.node.muted; - return ( - - ); -} - -function Shortcut({ keys, value }: { keys: string[]; value: string }) { - return ( -
- - {keys.map((key, index) => ( - - {index ? + : null} - - {key} - - - ))} - - {value} -
- ); -}