From d6a42d53a182196b5c8f046a0a9434dcf55ae8f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=88=B1=E7=AC=91=E7=9A=84=E6=AF=9B=E6=AF=9B=E8=99=AB?= <315515767@qq.com> Date: Tue, 4 Aug 2026 20:46:25 +0800 Subject: [PATCH] =?UTF-8?q?refactor(canvas):=20=E6=B5=AE=E5=B1=82=E4=BD=93?= =?UTF-8?q?=E7=B3=BB=E9=87=8D=E6=9E=84=20=E2=80=94=20z-index=2014=E6=A1=A3?= =?UTF-8?q?+=E4=B8=93=E6=B3=A8=E6=A8=A1=E5=BC=8F+=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E6=A0=8Fclick=E8=A7=A6=E5=8F=91=20#126=20#127=20#128=20(#129)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按视觉层重设计 z-index(12→14 档),新增节点层/激活节点层/节点工具栏层等 语义 token,并下沉节点内部 stacking context(--node-z-content/overlay/handle) 与 dock 子面板(--dock-z-popover),消除硬编码 z 值。 新增专注模式(避让分屏可切换):use-focus-mode 管理 localStorage 偏好与小屏 (<1024px)强制避让;助手面板从 flex 改为 absolute + 垂直分槽(顶部 288px 让位活动任务面板),画布区按面板宽度动态 padding;顶栏新增 PanelRight 切换按钮并带强制态反馈。 节点工具栏由 hover+select 触发放为 click-only(选中即显),组件由 canvas-node-hover-toolbar 重命名为 canvas-node-toolbar;移除 project 与 render-model 中冗余的 toolbarNodeId 状态。 活动任务面板 z-index 由 --z-modal-overlay(110) 降至 --z-panel-floating(80), 归位浮动面板层;AGENTS.md Elevation 同步至 14 档。 Co-authored-by: sikongyue <3155157671@qq.com> --- AGENTS.md | 2 +- .../canvas/canvas-active-task-panel.tsx | 2 +- .../canvas/canvas-assistant-panel.tsx | 46 ++++++++--- web/src/components/canvas/canvas-mini-map.tsx | 2 +- ...er-toolbar.tsx => canvas-node-toolbar.tsx} | 8 +- web/src/components/canvas/canvas-node.tsx | 20 ++--- .../components/canvas/canvas-script-node.tsx | 2 +- .../canvas/canvas-short-drama-entry.tsx | 4 +- web/src/components/canvas/canvas-toolbar.tsx | 6 +- .../canvas/canvas-zoom-controls.tsx | 2 +- web/src/hooks/use-focus-mode.ts | 45 +++++++++++ .../pages/canvas/canvas-project-top-bar.tsx | 28 ++++++- .../canvas/canvas-project-world-layers.tsx | 2 +- web/src/pages/canvas/project.tsx | 77 +++++++++++-------- web/src/pages/canvas/shared.tsx | 6 +- .../pages/canvas/use-canvas-render-model.ts | 5 +- web/src/styles/globals.css | 32 ++++---- 17 files changed, 199 insertions(+), 90 deletions(-) rename web/src/components/canvas/{canvas-node-hover-toolbar.tsx => canvas-node-toolbar.tsx} (99%) create mode 100644 web/src/hooks/use-focus-mode.ts diff --git a/AGENTS.md b/AGENTS.md index aea07a8fb..05aa63681 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,7 +61,7 @@ ### 三层结构 -- **Primitive 层**:纯值,无语义,不随主题变。包含 9 个维度:色彩(neutral 色板 + 节点类型色板 + 状态色)、间距(4px 栅格 `--space-0.5` ~ `--space-24`)、字号(10 档 `--fs-micro` ~ `--fs-display`)、圆角(10 档 `--r-xs` ~ `--r-full`)、阴影(`--shadow-xs` ~ `--shadow-2xl` + glow)、Elevation(12 档 `--z-canvas` ~ `--z-max`)、动效(duration 5 档 + easing 5 曲线 + delay 5 档 + motion-scale)、描边(4 档)、不透明度(6 档)。 +- **Primitive 层**:纯值,无语义,不随主题变。包含 9 个维度:色彩(neutral 色板 + 节点类型色板 + 状态色)、间距(4px 栅格 `--space-0.5` ~ `--space-24`)、字号(10 档 `--fs-micro` ~ `--fs-display`)、圆角(10 档 `--r-xs` ~ `--r-full`)、阴影(`--shadow-xs` ~ `--shadow-2xl` + glow)、Elevation(14 档 `--z-canvas` ~ `--z-max`)、动效(duration 5 档 + easing 5 曲线 + delay 5 档 + motion-scale)、描边(4 档)、不透明度(6 档)。 - **Semantic 层**:引用 Primitive,随主题切换。包含通用语义色(`--bg`/`--fg`/`--border-semantic`)、Canvas 语义(`--cn-text`/`--cn-muted`/`--cn-stroke` 等)、Aceternity spatial 语义(`--cn-spatial-*`)、节点类型语义(`--cn-type-*`)、状态语义(`--status-*`)。 - **Component 层**:引用 Semantic,某组件专属。包含 Dock(`--dock-*`)、Node(`--node-*`)、Modal(`--modal-*`)、Panel(`--panel-*`)、Prompt 面板(`--prompt-panel-*`)、进度条(`--progress-bar-*`)、缩放控制(`--zoom-control-*`)。 diff --git a/web/src/components/canvas/canvas-active-task-panel.tsx b/web/src/components/canvas/canvas-active-task-panel.tsx index 57fba2c29..07b797876 100644 --- a/web/src/components/canvas/canvas-active-task-panel.tsx +++ b/web/src/components/canvas/canvas-active-task-panel.tsx @@ -41,7 +41,7 @@ export function CanvasActiveTaskPanel({ tasks }: { tasks: GenerationTask[] }) { animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: -8, scale: 0.98 }} transition={motionTransition} - className="pointer-events-none absolute right-3 top-[72px] z-[var(--z-modal-overlay)] w-[min(332px,calc(100vw-24px))]" + className="pointer-events-none absolute right-3 top-[72px] z-[var(--z-panel-floating)] w-[min(332px,calc(100vw-24px))]" > void; cinematicEntry?: boolean; onCinematicEntryConsumed?: () => void; + width: number; + onWidthChange: (width: number) => void; + focusMode: boolean; }; -export function CanvasAssistantPanel({ nodes, selectedNodeIds, snapshot, projectId, sessions, activeSessionId, onSelectNodeIds, onSessionsChange, onApplyOps, canUndoOps, undoOpsCount, onUndoOps, onPasteImage, agentMode, onAgentModeChange, autoConnectLocal, closing, onCollapse, cinematicEntry = false, onCinematicEntryConsumed }: CanvasAssistantPanelProps) { +export function CanvasAssistantPanel({ nodes, selectedNodeIds, snapshot, projectId, sessions, activeSessionId, onSelectNodeIds, onSessionsChange, onApplyOps, canUndoOps, undoOpsCount, onUndoOps, onPasteImage, agentMode, onAgentModeChange, autoConnectLocal, closing, onCollapse, cinematicEntry = false, onCinematicEntryConsumed, width, onWidthChange, focusMode }: CanvasAssistantPanelProps) { const theme = canvasThemes[useThemeStore((state) => state.theme)]; const user = useUserStore((state) => state.user); const effectiveConfig = useEffectiveConfig(); @@ -157,7 +160,6 @@ export function CanvasAssistantPanel({ nodes, selectedNodeIds, snapshot, project const updateConfig = useConfigStore((state) => state.updateConfig); const confirmTools = useCanvasAgentStore((state) => state.confirmTools); const setAgentState = useCanvasAgentStore((state) => state.setAgentState); - const [width, setWidth] = useState(520); const [view, setView] = useState("chat"); const [prompt, setPrompt] = useState(""); const [cinematicEntryActive, setCinematicEntryActive] = useState(cinematicEntry); @@ -683,7 +685,7 @@ export function CanvasAssistantPanel({ nodes, selectedNodeIds, snapshot, project }; const startResize = () => { - const move = (event: MouseEvent) => setWidth(Math.min(760, Math.max(320, window.innerWidth - event.clientX))); + const move = (event: MouseEvent) => onWidthChange(Math.min(760, Math.max(320, window.innerWidth - event.clientX))); const stop = () => { setResizing(false); document.body.style.cursor = ""; @@ -832,22 +834,42 @@ export function CanvasAssistantPanel({ nodes, selectedNodeIds, snapshot, project ); + // 垂直分槽:顶部 72px(顶栏)+ 200px(活动任务最大预估高)+ 16px(间距)= 288px,留给活动任务面板 + const PANEL_SLOT_TOP_OFFSET = 288; + const panelOffsetRight = focusMode ? 0 : 12; + const panelOffsetBottom = focusMode ? 0 : 16; + return ( - ) : null} {showStatusTrack ? ( -
+
{resourceLabel ? : null} {hasMediaContent && !readOnly ? : null} {isBatchRoot ? onToggleBatch?.(data.id)} /> : null} @@ -481,7 +481,7 @@ export const CanvasNode = React.memo(function CanvasNode({
) : null} {assetTags.length || (showImageInfo && hasImageContent) ? ( -
+
{assetTags.length ? : null} {showImageInfo && hasImageContent ? : null}
@@ -1128,7 +1128,7 @@ function ResizeHandle({ corner, onMouseDown }: { corner: ResizeCorner; onMouseDo "bottom-right": "-bottom-[14px] -right-[14px] cursor-nwse-resize", }[corner]; - return
onMouseDown(event, corner)} />; + return
onMouseDown(event, corner)} />; } const NODE_EXTERNAL_HEADER_MIN_SCALE = 0.35; @@ -1154,7 +1154,7 @@ function NodeExternalHeader({ node, scale, active, editable, editing, draft, the return (
event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} @@ -1264,7 +1264,7 @@ function ConnectionSideRail({ side, scale, visible, theme, onPointerDown }: { si return ( ); } return ( -
+
{progress.steps.map((step, index) => ( diff --git a/web/src/components/canvas/canvas-toolbar.tsx b/web/src/components/canvas/canvas-toolbar.tsx index bdbb083cc..29c4884a7 100644 --- a/web/src/components/canvas/canvas-toolbar.tsx +++ b/web/src/components/canvas/canvas-toolbar.tsx @@ -175,7 +175,7 @@ export function CanvasToolbar({ const resourceCommands = addNodeCommands.filter((cmd) => cmd.section === "resource").map(toCommand); return ( -
+
{addOpen ? ( {appearanceOpen ? ( - + event.stopPropagation()}> } title="画布外观" subtitle="调整整个创作空间" theme={theme} />
主题模式
@@ -231,7 +231,7 @@ function AddNodeMenu({ x, theme, nodeCommands, resourceCommands }: { resourceCommands: CanvasCreateCommand[]; }) { return ( - + event.stopPropagation()}> } title="创建内容" subtitle="选择节点类型" theme={theme} /> diff --git a/web/src/components/canvas/canvas-zoom-controls.tsx b/web/src/components/canvas/canvas-zoom-controls.tsx index 74e944995..e296c1393 100644 --- a/web/src/components/canvas/canvas-zoom-controls.tsx +++ b/web/src/components/canvas/canvas-zoom-controls.tsx @@ -86,7 +86,7 @@ export function CanvasZoomControls({ scale, onScaleChange, onReset, isMiniMapOpe ]; return ( -
event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} onWheel={(event) => event.stopPropagation()}> +
event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} onWheel={(event) => event.stopPropagation()}> {precisionOpen ? ( (readInitialPreference); + const [viewportWidth, setViewportWidth] = useState(() => (typeof window !== "undefined" ? window.innerWidth : 1024)); + + useEffect(() => { + if (typeof window === "undefined") return; + const handleResize = () => setViewportWidth(window.innerWidth); + handleResize(); + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); + + const forcedOn = viewportWidth < FORCED_FOCUS_BREAKPOINT; + const focusMode = forcedOn || userPreference; + + const toggleFocusMode = useCallback(() => { + setUserPreference((prev) => { + const next = !prev; + if (typeof window !== "undefined") { + window.localStorage.setItem(FOCUS_MODE_KEY, next ? "1" : "0"); + } + return next; + }); + }, []); + + return { + focusMode, + userPreference, + forcedOn, + toggleFocusMode, + }; +} diff --git a/web/src/pages/canvas/canvas-project-top-bar.tsx b/web/src/pages/canvas/canvas-project-top-bar.tsx index 3adc4a352..ed54c37d8 100644 --- a/web/src/pages/canvas/canvas-project-top-bar.tsx +++ b/web/src/pages/canvas/canvas-project-top-bar.tsx @@ -1,7 +1,7 @@ import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react"; import { Link } from "react-router"; -import { Bot, Check, ChevronDown, Coins, FolderKanban, Gauge, Home, LayoutGrid, LoaderCircle, Menu, Pencil, Plus, Redo2, Search, Settings2, Share2, Sparkles, Trash2, Undo2, Upload } from "lucide-react"; +import { Bot, Check, ChevronDown, Coins, FolderKanban, Gauge, Home, LayoutGrid, LoaderCircle, Menu, Pencil, PanelRightClose, PanelRightOpen, Plus, Redo2, Search, Settings2, Share2, Sparkles, Trash2, Undo2, Upload } from "lucide-react"; import { Button, Dropdown, Modal, Tooltip } from "antd"; import { useWalletBalance } from "@/hooks/use-wallet-balance"; @@ -38,6 +38,9 @@ type CanvasTopBarProps = { onMediaPerformanceModeChange: (mode: CanvasMediaPerformanceMode) => void; onOpenSearch: () => void; projectContext?: CanvasContextSummary & { projectId: string; projectName: string }; + focusMode: boolean; + focusModeForced: boolean; + onToggleFocusMode: () => void; }; export function CanvasTopBar({ @@ -66,6 +69,9 @@ export function CanvasTopBar({ onMediaPerformanceModeChange, onOpenSearch, projectContext, + focusMode, + focusModeForced, + onToggleFocusMode, }: CanvasTopBarProps) { const theme = canvasThemes[useThemeStore((state) => state.theme)]; const user = useUserStore((state) => state.user); @@ -89,7 +95,7 @@ export function CanvasTopBar({ return ( <> -
+
{availableMicrocredits === null ? "--" : (availableMicrocredits / 1_000_000).toLocaleString("zh-CN", { maximumFractionDigits: 3 })} ) : null} + +