feat(canvas): 画布工作区 - 以悬浮磨砂面板承载侧栏导航

This commit is contained in:
ddcat666
2026-09-25 00:00:22 +08:00
parent 1dc1b6f6b5
commit c0d223f357
8 changed files with 240 additions and 124 deletions

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import type { ReactNode } from "react";
import { AlertCircle, BookOpenCheck, CheckCircle2, ChevronRight, Clapperboard, Copy, Download, GripVertical, Image as ImageIcon, Lock, Maximize2, Music2, Pencil, RefreshCw, ScanSearch, Settings2, Star, Trash2, Type, Video, WandSparkles } from "lucide-react";
import { AlertCircle, BookOpenCheck, CheckCircle2, ChevronRight, Clapperboard, Copy, Download, Image as ImageIcon, Lock, Maximize2, Music2, Pencil, RefreshCw, ScanSearch, Settings2, Star, Trash2, Type, Video, WandSparkles } from "lucide-react";
import { useCanvasNodeActions } from "./canvas-node-action-context";
@@ -307,7 +307,6 @@ export const CanvasNode = React.memo(function CanvasNode({
draft={titleDraft}
theme={theme}
onDraftChange={setTitleDraft}
onDragStart={readOnly ? undefined : (event) => onMouseDown(event, data.id)}
onEdit={() => setIsEditingTitle(true)}
onCommit={commitTitle}
onCancel={() => { setTitleDraft(data.title); setIsEditingTitle(false); }}
@@ -647,7 +646,7 @@ function formatMediaDimensionLabel(node: CanvasNodeData, hasVisualMediaContent:
return `${Math.round(width)}*${Math.round(height)}`;
}
function NodeExternalHeader({ node, scale, dimensionLabel, active, editable, editing, draft, theme, onDragStart, onDraftChange, onEdit, onCommit, onCancel }: {
function NodeExternalHeader({ node, scale, dimensionLabel, active, editable, editing, draft, theme, onDraftChange, onEdit, onCommit, onCancel }: {
node: CanvasNodeData;
scale: number;
dimensionLabel: string | null;
@@ -657,7 +656,6 @@ function NodeExternalHeader({ node, scale, dimensionLabel, active, editable, edi
draft: string;
theme: CanvasTheme;
onDraftChange: (value: string) => void;
onDragStart?: (event: React.MouseEvent) => void;
onEdit: () => void;
onCommit: () => void;
onCancel: () => void;
@@ -677,7 +675,7 @@ function NodeExternalHeader({ node, scale, dimensionLabel, active, editable, edi
"--canvas-node-width": `${node.width}px`,
borderRadius: "var(--r-sm)",
background: "transparent",
paddingInline: "var(--space-1-half)",
paddingInline: "0",
color: active ? theme.node.text : theme.node.label,
transform: `scale(var(--canvas-live-inverse-scale, ${inverseScale}))`,
transformOrigin: "left bottom",
@@ -686,23 +684,6 @@ function NodeExternalHeader({ node, scale, dimensionLabel, active, editable, edi
onPointerDown={(event) => event.stopPropagation()}
>
<div className="flex min-w-0 items-center gap-1" style={{ maxWidth: maxHeaderWidth }}>
<button
type="button"
className="flex size-6 shrink-0 touch-none items-center justify-center rounded cursor-grab active:cursor-grabbing disabled:cursor-not-allowed focus-visible:outline focus-visible:outline-1"
aria-label={node.metadata?.locked ? "节点已锁定" : `拖动节点:${node.title}`}
title={node.metadata?.locked ? "节点已锁定,请先解锁" : "拖动此处移动节点;点击名称可重命名"}
disabled={!onDragStart || Boolean(node.metadata?.locked)}
onPointerDown={(event) => {
if (event.button !== 0) return;
// HTML / SVG 正文在跨源沙箱中;捕获指针,避免经过 iframe 后丢失 move / up。
event.preventDefault();
event.stopPropagation();
event.currentTarget.setPointerCapture(event.pointerId);
onDragStart?.(event);
}}
>
{node.metadata?.locked ? <Lock className="size-3" /> : <GripVertical className="size-3" strokeWidth={1.8} />}
</button>
<Icon className="size-3 shrink-0" strokeWidth={1.8} />
{editing ? (
<input