From f789499c3607f6f5265c97f227dd146877885c7a Mon Sep 17 00:00:00 2001 From: kdletters Date: Wed, 17 Jun 2026 02:17:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E5=88=86=E5=9B=BE=E7=89=87=E7=94=BB?= =?UTF-8?q?=E5=B8=83=E7=BC=96=E8=BE=91=E5=99=A8=E4=BE=A7=E6=A0=8F=E8=A7=86?= =?UTF-8?q?=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 抽出素材和图层左侧整合面板为 ImageCanvasSidebarView 保留上传、登录、拖到画布和持久化状态机在主视图 更新前端拆分计划和 TRACKING 验证记录 --- TRACKING.md | 4 +- ...构】图片画布编辑器前端拆分计划-2026-06-17.md | 10 +- .../image-editor/ImageCanvasEditorView.tsx | 725 ++------------- .../image-editor/ImageCanvasSidebarView.tsx | 828 ++++++++++++++++++ 4 files changed, 900 insertions(+), 667 deletions(-) create mode 100644 src/components/image-editor/ImageCanvasSidebarView.tsx diff --git a/TRACKING.md b/TRACKING.md index 6b0e9257..09adb9bd 100644 --- a/TRACKING.md +++ b/TRACKING.md @@ -17,7 +17,7 @@ | 前端交互 | 已完成 | 已实现缩放菜单、工具模式、Space 抓手、中键平移、吸附线、元数据弹窗和右侧真实修改结果。 | | 素材库增强 | 已完成 | 已实现账号级素材库持久化、文件夹新建 / 折叠 / 重命名 / 删除、多文件上传、拖拽定向上传、素材框选与批量删除。 | | 画布增强 | 已完成 | 已实现拖拽上传到画布并创建图层、图层打组、Ctrl/Cmd 滚轮缩放、普通滚轮纵向滚动和小地图拖拽移动视图。 | -| 前端拆分 | 进行中 | 已新增前端拆分计划,抽出类型、画布模型、生成模型和导出模型,主视图保留状态编排与 JSX 工作面。 | +| 前端拆分 | 进行中 | 已新增前端拆分计划,抽出类型、画布模型、生成模型、导出模型和素材 / 图层整合侧栏视图,主视图保留状态编排与跨画布状态机。 | | 验证 | 已完成 | 聚焦测试、类型检查、Rust 检查、schema guard、编码检查、diff 空白检查和浏览器 smoke 已通过。 | ## 待办清单 @@ -112,3 +112,5 @@ - 2026-06-16 编辑器回归修正:工程 / 素材 / 上传等编辑器请求恢复全局 401 / 403 登录弹窗;未登录上传会先弹登录并在登录后续传;画布背景入口恢复为 `画布背景设置` 面板,支持预设色、自定义颜色、HEX 输入、非法值不应用、恢复默认和 Escape 关闭。验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/services/image-editor/editorProjectClient.test.ts`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器 smoke:`http://127.0.0.1:10006/editor/canvas` 未登录打开 `账号入口`,登录后上传素材成功,背景面板打开后点击“暖灰”使画布背景变为 `rgb(243, 240, 234)`。 - 2026-06-17 前端拆分第一阶段:新增 `ImageCanvasEditorTypes`、`ImageCanvasEditorModel`、`ImageCanvasGenerationModel` 和 `ImageCanvasExportModel`,把类型、画布快照 / 吸附 / 背景、生成输入快照和导出元数据规则从 `ImageCanvasEditorView` 抽出;新增模型层单测,主视图从 8286 行降至 7054 行。 - 2026-06-17 浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录打开工程和未登录上传均弹出 `账号入口`;关闭登录后点击 `画布背景色` 打开 `画布背景设置` 面板,点击 `暖灰` 后画布背景为 `rgb(243, 240, 234)`;登录开发账号后上传图片成功进入 `项目素材`,`AI画布工具栏` 保持可见。 +- 2026-06-17 前端拆分第二阶段:新增 `ImageCanvasSidebarView`,把素材 / 图层共用左侧整合面板从主视图抽出;上传链路、登录弹窗、素材拖到画布、持久化、图层历史和右键菜单状态机仍保留在主视图,避免过度拆分。验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasEditorPrimitives.test.tsx`、`npm run typecheck`。 +- 2026-06-17 侧栏拆分浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新弹出 `账号入口`;`画布背景色` 打开 `画布背景设置` dialog,包含预设、自定义颜色、HEX 和恢复默认;使用临时开发账号登录后上传图片成功进入 `项目素材`,点击素材可添加到画布,切换 `图层` 侧栏后能看到同一图片图层,`AI画布工具栏` 保持可见。 diff --git a/docs/technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md b/docs/technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md index 7e4c270a..9acf73d7 100644 --- a/docs/technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md +++ b/docs/technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md @@ -34,9 +34,17 @@ - 承载画布素材导出的底层规则:文件名清理、日期格式、图片去重 key、Data URL 转 Blob、Blob 读取和图层导出元数据。 - ZIP 组包和下载触发仍留在主视图,作为 UI 状态编排的一部分。 +## 第二阶段模块 + +- `ImageCanvasSidebarView.tsx` + - 承载素材 / 图层共用左侧整合面板的 JSX,包括素材文件夹、新建 / 折叠 / 重命名 / 删除、上传入口、素材选择模式、框选层、批量删除、素材拖到文件夹和图层列表。 + - 继续通过 props 调用主视图状态机,不接管上传、登录弹窗、持久化、拖到画布的坐标换算和图层历史记录。 + - 保持“素材”和“图层”同一侧栏切换的 Lovart 式布局,不恢复右侧独立图层栏或左侧竖向工具栏。 + +第二阶段以后,主视图仍是画布编排入口。继续拆分前应优先选择能形成稳定边界的深模块,避免把上传链路、DataTransfer、画布坐标和历史快照拆成互相回调的小碎片。 + ## 后续阶段 -- `ImageCanvasSidebarView`:素材 / 图层共用侧栏,等模型层稳定后再拆。 - `ImageCanvasStageView`:画布 viewport、图层渲染、右键菜单和生成占位框,等交互回归覆盖更强后再拆。 - `ImageCanvasGenerationDock`:底部 AI 工具栏和生成面板族,等生成对象状态机进一步收口后再拆。 diff --git a/src/components/image-editor/ImageCanvasEditorView.tsx b/src/components/image-editor/ImageCanvasEditorView.tsx index a4a5bd96..75f6d1ad 100644 --- a/src/components/image-editor/ImageCanvasEditorView.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.tsx @@ -1,7 +1,6 @@ import { Braces, Check, - CheckSquare, ChevronDown, ChevronLeft, ChevronRight, @@ -10,7 +9,6 @@ import { Crop, Download, Folder, - FolderPlus, Hand, ImageIcon, ImagePlus, @@ -19,13 +17,11 @@ import { Map as MapIcon, MousePointer2, Pencil, - PencilLine, Redo2, RotateCcw, Shapes, SlidersHorizontal, Sparkles, - Square, Trash2, Type, Undo2, @@ -71,7 +67,6 @@ import { updateEditorAssetFolder, } from '../../services/image-editor/editorProjectClient'; import { PlatformActionButton } from '../common/PlatformActionButton'; -import { PlatformBatchActionToolbar } from '../common/PlatformBatchActionToolbar'; import { PlatformFieldLabel } from '../common/PlatformFieldLabel'; import { PlatformFloatingMenu, @@ -87,10 +82,8 @@ import { } from '../common/PlatformTextField'; import { UnifiedModal } from '../common/UnifiedModal'; import { useAuthUi } from '../auth/AuthUiContext'; -import { - EditorIconButton, - SidebarMediaItem, -} from './ImageCanvasEditorPrimitives'; +import { EditorIconButton } from './ImageCanvasEditorPrimitives'; +import { ImageCanvasSidebarView } from './ImageCanvasSidebarView'; import { ASSET_DRAG_MIME_TYPE, CANVAS_BACKGROUND_OPTIONS, @@ -4172,662 +4165,64 @@ export function ImageCanvasEditorView() { ?.label ?? '素材'} ) : null} - {activeSidebarPanel ? ( - - ) : null} +
diff --git a/src/components/image-editor/ImageCanvasSidebarView.tsx b/src/components/image-editor/ImageCanvasSidebarView.tsx new file mode 100644 index 00000000..a17a55ea --- /dev/null +++ b/src/components/image-editor/ImageCanvasSidebarView.tsx @@ -0,0 +1,828 @@ +import { + Check, + CheckSquare, + ChevronDown, + ChevronRight, + Folder, + FolderPlus, + ImagePlus, + Pencil, + PencilLine, + Square, + Trash2, + X, +} from 'lucide-react'; +import type { + Dispatch, + PointerEvent as ReactPointerEvent, + RefObject, + SetStateAction, +} from 'react'; + +import { PlatformActionButton } from '../common/PlatformActionButton'; +import { PlatformBatchActionToolbar } from '../common/PlatformBatchActionToolbar'; +import { PlatformTextField } from '../common/PlatformTextField'; +import { + EditorIconButton, + SidebarMediaItem, +} from './ImageCanvasEditorPrimitives'; +import { + ASSET_DRAG_MIME_TYPE, + clamp, + getDraggedAssetId, + hasDataTransferType, +} from './ImageCanvasEditorModel'; +import type { + AssetMarqueeState, + AssetPointerDragState, + CanvasContextMenuState, + CanvasLayer, + EditorAsset, + EditorAssetFolder, + ImageContextMenuState, + SidebarPanel, + UploadTarget, +} from './ImageCanvasEditorTypes'; + +export type GroupedEditorAssetFolder = EditorAssetFolder & { + assets: EditorAsset[]; +}; + +type UploadFilesOptions = { + folderId?: string; + canvasPoint?: { x: number; y: number }; + addToCanvas?: boolean; +}; + +type ImageCanvasSidebarViewProps = { + activeSidebarPanel: SidebarPanel | null; + assetListRef: RefObject; + uploadInputRef: RefObject; + assetPointerDragRef: { current: AssetPointerDragState | null }; + suppressAssetClickRef: { current: boolean }; + assets: EditorAsset[]; + groupedAssets: GroupedEditorAssetFolder[]; + assetFolders: EditorAssetFolder[]; + layers: CanvasLayer[]; + selectedLayerId: string | null; + selectedLayerIds: string[]; + isAssetSelectionMode: boolean; + selectedAssetIds: Set; + assetMoveDropFolderId: string | null; + pinnedAssetMoveFolderId: string | null; + creatingFolder: boolean; + newFolderName: string; + renamingFolder: { folderId: string; value: string } | null; + renamingAsset: { assetId: string; value: string } | null; + allSelectableAssetsSelected: boolean; + assetMarquee: AssetMarqueeState | null; + setIsAssetSelectionMode: Dispatch>; + setCreatingFolder: Dispatch>; + setNewFolderName: Dispatch>; + setRenamingFolder: Dispatch< + SetStateAction<{ folderId: string; value: string } | null> + >; + setRenamingAsset: Dispatch< + SetStateAction<{ assetId: string; value: string } | null> + >; + setActiveUploadFolderId: Dispatch>; + setUploadTarget: Dispatch>; + setUploadDropTarget: Dispatch>; + setAssetPointerDrag: Dispatch>; + setSelectedAssetIds: Dispatch>>; + setImageContextMenu: Dispatch>; + setContextMenu: Dispatch>; + onAssetMarqueePointerDown: ( + event: ReactPointerEvent, + ) => void; + onAssetMarqueePointerMove: ( + event: ReactPointerEvent, + ) => void; + onAssetMarqueePointerUp: ( + event: ReactPointerEvent, + ) => void; + updateAssetMoveDropFolder: (folderId: string | null) => void; + addUploadedFiles: (files: FileList | File[], options?: UploadFilesOptions) => void; + moveAssetToFolder: (assetId: string, folderId: string) => void; + commitNewAssetFolder: () => void | Promise; + toggleAssetFolder: (folderId: string) => void; + startRenamingFolder: (folder: EditorAssetFolder) => void; + commitFolderRename: (folder: EditorAssetFolder) => void; + deleteAssetFolder: (folder: EditorAssetFolder) => void; + startRenamingAsset: (asset: EditorAsset) => void; + commitAssetRename: (asset: EditorAsset) => void; + deleteUploadedAsset: (asset: EditorAsset) => void; + toggleAssetSelected: (assetId: string) => void; + addAssetLayer: (asset: EditorAsset) => void; + toggleAllAssetsSelected: () => void; + deleteSelectedAssets: () => void; + closeAssetSelectionMode: () => void; + groupSelectedLayers: () => void; + selectSingleLayer: (layerId: string | null) => void; + resolveContextMenuPosition: ( + clientX: number, + clientY: number, + menuKind: 'blank' | 'layer', + ) => Omit; + getCanvasPointFromClient: ( + clientX: number, + clientY: number, + ) => { x: number; y: number }; +}; + +export function ImageCanvasSidebarView({ + activeSidebarPanel, + assetListRef, + uploadInputRef, + assetPointerDragRef, + suppressAssetClickRef, + assets, + groupedAssets, + assetFolders, + layers, + selectedLayerId, + selectedLayerIds, + isAssetSelectionMode, + selectedAssetIds, + assetMoveDropFolderId, + pinnedAssetMoveFolderId, + creatingFolder, + newFolderName, + renamingFolder, + renamingAsset, + allSelectableAssetsSelected, + assetMarquee, + setIsAssetSelectionMode, + setCreatingFolder, + setNewFolderName, + setRenamingFolder, + setRenamingAsset, + setActiveUploadFolderId, + setUploadTarget, + setUploadDropTarget, + setAssetPointerDrag, + setSelectedAssetIds, + setImageContextMenu, + setContextMenu, + onAssetMarqueePointerDown, + onAssetMarqueePointerMove, + onAssetMarqueePointerUp, + updateAssetMoveDropFolder, + addUploadedFiles, + moveAssetToFolder, + commitNewAssetFolder, + toggleAssetFolder, + startRenamingFolder, + commitFolderRename, + deleteAssetFolder, + startRenamingAsset, + commitAssetRename, + deleteUploadedAsset, + toggleAssetSelected, + addAssetLayer, + toggleAllAssetsSelected, + deleteSelectedAssets, + closeAssetSelectionMode, + groupSelectedLayers, + selectSingleLayer, + resolveContextMenuPosition, + getCanvasPointFromClient, +}: ImageCanvasSidebarViewProps) { + if (!activeSidebarPanel) { + return null; + } + + return ( + + ); +}