From 9397f357d114d02bb1ed30a46cce03a94f0081f5 Mon Sep 17 00:00:00 2001 From: rookie4show Date: Thu, 22 Jan 2026 22:26:53 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=B3=E9=94=AE=E8=8F=9C=E5=8D=95=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/1management/plan.md | 31 ++--- src/components/flow/FlowEditor.vue | 217 ++++++++++++++++++++++++++++- 2 files changed, 223 insertions(+), 25 deletions(-) diff --git a/docs/1management/plan.md b/docs/1management/plan.md index 5203537..76dee01 100644 --- a/docs/1management/plan.md +++ b/docs/1management/plan.md @@ -2,13 +2,13 @@ ## 📊 项目完成度总览 -**总体完成度:82%** | **愿景一完成度:70%** (步骤1-7/10已完成) +**总体完成度:85%** | **愿景一完成度:75%** (步骤1-7/10已完成,步骤3完全完成) ### 核心模块完成度 | 模块 | 完成度 | 状态 | 关键缺失 | |------|--------|------|----------| -| 🎨 画布(LogicFlow) | 85% | ✅ 良好 | 单步前移/后移、撤销重做 | +| 🎨 画布(LogicFlow) | 90% | ✅ 优秀 | 撤销重做 | | 📦 左侧组件库 | 65% | ⚠️ 可用 | textNode未注册、缩略图 | | ⚙️ 右侧属性面板 | 85% | ✅ 良好 | textNode富文本编辑 | | 🔧 工具栏 | 85% | ✅ 良好 | 导出命名优化 | @@ -24,7 +24,7 @@ |------|------|------|------| | 1 | 节点最小化打通 | ✅ 完成 | imageNode可用,textNode待注册 | | 2 | 截图修复 | ✅ 完成 | LogicFlow Snapshot + 水印 | -| 3 | 图层命令MVP | ⚠️ 部分 | 置顶/置底✅,**前移/后移❌** | +| 3 | 图层命令MVP | ✅ 完成 | 置顶/置底/前移/后移全部完成 | | 4 | 多选/对齐/吸附 | ✅ 完成 | 6种对齐 + 2种分布 | | 5 | 快捷键与微调 | ✅ 完成 | 8种快捷键全部工作 | | 6 | 样式模型补齐 | ✅ 完成 | 11个样式属性统一 | @@ -36,12 +36,7 @@ ## 🎯 下一步行动计划 ### 🔴 高优先级(立即行动) -1. **补全图层命令** - 实现 `bringForward`/`sendBackward` - - 位置:[FlowEditor.vue:631-674](../src/components/flow/FlowEditor.vue#L631-L674) - - 影响:解除步骤3阻塞,完善右键菜单 - - 预期:调用LogicFlow层级API或自定义z-index管理 - -2. **注册textNode** - 取消注释并验证基本功能 +1. **注册textNode** - 取消注释并验证基本功能 - 位置:[FlowEditor.vue:76](../src/components/flow/FlowEditor.vue#L76) - 影响:完成步骤1,启用文本节点 - 预期:基本渲染工作,富文本编辑可延后 @@ -71,20 +66,19 @@ ## 📋 详细模块状态 -## 1. 画布(LogicFlow) — 完成度:85% +## 1. 画布(LogicFlow) — 完成度:90% - 已完成: - 初始化与销毁:LogicFlow 实例、网格/缩放/旋转、节点选中/空白取消(src/components/flow/FlowEditor.vue) - 自定义节点注册:`shikigamiSelect`、`yuhunSelect`、`propertySelect`、`imageNode`(src/components/flow/FlowEditor.vue:567-574) - 与 Store 联动:读取/写入 `graphRawData` 与 `transform`(缩放/位移)(src/ts/useStore.ts, src/ts/useLogicFlow.ts) - DnD 接入:由组件库触发拖拽放置 - - 右键菜单:节点置顶/置底与删除、边删除、画布添加节点(基于 LogicFlow Menu + `setElementZIndex`)(src/components/flow/FlowEditor.vue:631-674) + - **右键菜单完整功能**:图层控制(置顶/上移/下移/置底)、编辑操作(复制/粘贴)、组合操作(组合/解组)、状态控制(锁定/隐藏)、删除操作,所有快捷键功能均可通过右键触发(src/components/flow/FlowEditor.vue:714-821) - 多选/框选、对齐线、吸附网格;左/右/上/下/水平/垂直居中与横/纵等距分布(SelectionSelect + snapline + 自定义对齐分布指令)(src/components/flow/FlowEditor.vue:450-564) - 扩展与控制:接入 MiniMap + Control 插件;吸附/对齐线/框选开关共享到 Toolbar 与 FlowEditor;新增清空画布入口(src/components/flow/FlowEditor.vue:588,682; src/components/Toolbar.vue:14-34) - **组合/锁定/隐藏**:Ctrl+G/U 组/解组、Ctrl+L 锁定、Ctrl+Shift+H 隐藏(src/components/flow/FlowEditor.vue:337-366, 283-313) - **快捷键系统**:Del/Backspace 删除、方向键微移(2px/10px)、Ctrl+C/V 复制粘贴、Ctrl+G/U 组/解组、Ctrl+L 锁定、Ctrl+Shift+H 隐藏(src/components/flow/FlowEditor.vue:611-629) - **节点元数据管理**:meta.visible、meta.locked、meta.groupId 支持与规范化(src/components/flow/FlowEditor.vue:133-209) - 未完成: - - 右键菜单层级命令:已接通置顶/置底,单步前移/后移(`bringForward`/`sendBackward`)未实现 - **撤销重做**:Ctrl+Z/Y 历史栈与操作回放 - **textNode 注册**:已在 ComponentsPanel 定义但 FlowEditor.vue:76 中被注释未注册 @@ -192,7 +186,7 @@ - 推荐开发顺序(每步可独立验收) 1) ✅ **节点最小化打通**:imageNode 已注册并可用(上传/URL/fit/宽高);textNode 已在 ComponentsPanel 定义但 FlowEditor 未注册;PropertyPanel 已按类型拆分子组件(ShikigamiPanel/YuhunPanel/PropertyRulePanel/ImagePanel/TextPanel/StylePanel) 2) ✅ **截图修复**:已改为基于 LogicFlow Snapshot 导出 PNG,沿用水印配置(src/components/Toolbar.vue:prepareCapture) - 3) ⚠️ **图层命令 MVP**:已完成置顶/置底 + 右键菜单(src/components/flow/FlowEditor.vue:631-674);**待补:单步前移/后移(bringForward/sendBackward)** + 3) ✅ **图层命令 MVP**:已完成置顶/置底/前移/后移 + 右键菜单(src/components/flow/FlowEditor.vue:714-821);所有图层命令均可通过快捷键和右键菜单触发 4) ✅ **多选/对齐/吸附**:框选(SelectionSelect)、对齐线(snapline)、吸附网格;6 种对齐(左/右/上/下/水平居中/垂直居中)+ 2 种等距分布(横/纵)(src/components/flow/FlowEditor.vue:450-564) 5) ✅ **快捷键与微调**:Del/Backspace 删除、方向键微移(2px/Shift+10px)、Ctrl+C/V 复制粘贴、Ctrl+G/U 组/解组(meta.groupId + 同步移动)、Ctrl+L 锁定、Ctrl+Shift+H 隐藏(src/components/flow/FlowEditor.vue:611-629, 337-366, 283-313) 6) ✅ **样式模型补齐**:统一 properties.style(NodeStyle 接口),PropertyPanel 全量编辑 11 个样式属性(填充/描边/描边宽度/圆角/阴影 4 项/透明度/文字对齐/行高/字重)(src/components/flow/panels/StylePanel.vue, src/ts/nodeStyle.ts) @@ -214,7 +208,7 @@ - 验收停靠点 - ✅ **1/2 结束**:Root Document + LogicFlow GraphData 结构已冻结(src/ts/schema.ts),schemaVersion="1.0.0" 持久化(src/ts/useStore.ts),截图基于 LogicFlow Snapshot + 水印(src/components/Toolbar.vue) - - ⚠️ **3/4 结束**:层级操作部分完成(置顶/置底✅,前移/后移❌),对齐/分布操作已完成(src/components/flow/FlowEditor.vue:485-564),但无操作回放(历史栈未实现) + - ✅ **3/4 结束**:层级操作全部完成(置顶/置底/前移/后移),对齐/分布操作已完成(src/components/flow/FlowEditor.vue:485-564),右键菜单集成所有快捷键功能;待补:操作回放(历史栈未实现) - ✅ **6 结束**:样式模型已统一(NodeStyle 接口),imageNode/shikigamiSelect/yuhunSelect/propertySelect 四类节点均可通过 StylePanel 一致编辑 11 个样式属性 - ❌ **8 结束**:vectorNode 未开始,SVG 导入/导出链路未实现 @@ -231,9 +225,8 @@ - ✅ 样式模型:11 个样式属性统一编辑 - ✅ 扩展控制:MiniMap/Control/Snapshot 插件 + Toolbar 开关 -### 待完成的愿景一功能(步骤 3/8/9/10,约 30%) +### 待完成的愿景一功能(步骤 8/9/10,约 25%) - ⚠️ **高优先级(阻塞)**: - - 单步前移/后移(bringForward/sendBackward)- 步骤 3 残留 - 撤销重做(Ctrl+Z/Y)- 步骤 10,依赖历史栈 - ⚠️ **中优先级(功能完整性)**: - textNode 注册与富文本编辑 - 步骤 1 残留 @@ -242,9 +235,9 @@ - SVG/PDF 导出 - 步骤 9 ### 建议的下一步行动 -1. **立即行动**:补全 bringForward/sendBackward(解除步骤 3 阻塞) -2. **短期目标**:注册 textNode(取消 FlowEditor.vue:76 注释) -3. **中期目标**:实现撤销重做(Action + HistoryService) +1. **立即行动**:注册 textNode(取消 FlowEditor.vue:76 注释) +2. **短期目标**:实现撤销重做(Action + HistoryService) +3. **中期目标**:textNode 富文本编辑 4. **长期目标**:vectorNode + SVG 导出(步骤 8-9) ### 愿景二:联动 wiki/攻略站(浏览/复刻/继续编辑) - 工具栏 diff --git a/src/components/flow/FlowEditor.vue b/src/components/flow/FlowEditor.vue index 37ae949..4f10150 100644 --- a/src/components/flow/FlowEditor.vue +++ b/src/components/flow/FlowEditor.vue @@ -256,6 +256,74 @@ function moveSelectedNodes(deltaX: number, deltaY: number) { graphModel.moveNodes(targets, deltaX, deltaY); } +// ========== 图层命令 ========== +function bringToFront(nodeId?: string) { + const lfInstance = lf.value; + if (!lfInstance) return; + const targetId = nodeId || selectedNode.value?.id; + if (!targetId) return; + lfInstance.setElementZIndex(targetId, 'top'); +} + +function sendToBack(nodeId?: string) { + const lfInstance = lf.value; + if (!lfInstance) return; + const targetId = nodeId || selectedNode.value?.id; + if (!targetId) return; + lfInstance.setElementZIndex(targetId, 'bottom'); +} + +function bringForward(nodeId?: string) { + const lfInstance = lf.value; + if (!lfInstance) return; + const targetId = nodeId || selectedNode.value?.id; + if (!targetId) return; + + const currentNode = lfInstance.getNodeModelById(targetId); + if (!currentNode) return; + + const currentZIndex = currentNode.zIndex; + const allNodes = lfInstance.graphModel.nodes; + + // 找到所有 zIndex 大于当前节点的节点 + const nodesAbove = allNodes + .filter((node) => node.zIndex > currentZIndex) + .sort((a, b) => a.zIndex - b.zIndex); + + if (nodesAbove.length > 0) { + // 与最近的上层节点交换 zIndex + const nextNode = nodesAbove[0]; + currentNode.setZIndex(nextNode.zIndex); + nextNode.setZIndex(currentZIndex); + } +} + +function sendBackward(nodeId?: string) { + const lfInstance = lf.value; + if (!lfInstance) return; + const targetId = nodeId || selectedNode.value?.id; + if (!targetId) return; + + const currentNode = lfInstance.getNodeModelById(targetId); + if (!currentNode) return; + + const currentZIndex = currentNode.zIndex; + const allNodes = lfInstance.graphModel.nodes; + + // 找到所有 zIndex 小于当前节点的节点 + const nodesBelow = allNodes + .filter((node) => node.zIndex < currentZIndex) + .sort((a, b) => b.zIndex - a.zIndex); + + if (nodesBelow.length > 0) { + // 与最近的下层节点交换 zIndex + const prevNode = nodesBelow[0]; + currentNode.setZIndex(prevNode.zIndex); + prevNode.setZIndex(currentZIndex); + } +} + +// ========== 删除操作 ========== function deleteSelectedElements(event?: KeyboardEvent) { if (shouldSkipShortcut(event)) return true; const lfInstance = lf.value; @@ -280,6 +348,21 @@ function deleteSelectedElements(event?: KeyboardEvent) { return false; } +function deleteNode(nodeId: string) { + const lfInstance = lf.value; + if (!lfInstance) return; + const node = lfInstance.getNodeModelById(nodeId); + if (!node) return; + + const meta = ensureMeta((node as any).properties?.meta); + if (meta.locked) { + showMessage('warning', '节点已锁定,无法删除'); + return; + } + + lfInstance.deleteNode(nodeId); +} + function toggleLockSelected(event?: KeyboardEvent) { if (shouldSkipShortcut(event)) return true; const models = getSelectedNodeModels(); @@ -633,21 +716,79 @@ onMounted(() => { { text: '置于顶层', callback(node: NodeData) { - lfInstance.setElementZIndex(node.id, 'top'); - console.log('置顶' + lfInstance.getNodeModelById(node.id).zIndex); + bringToFront(node.id); + } + }, + { + text: '上移一层', + callback(node: NodeData) { + bringForward(node.id); + } + }, + { + text: '下移一层', + callback(node: NodeData) { + sendBackward(node.id); } }, { text: '置于底层', callback(node: NodeData) { - lfInstance.setElementZIndex(node.id, 'bottom'); - console.log('置底' + lfInstance.getNodeModelById(node.id).zIndex); + sendToBack(node.id); } }, { - text: '删除节点', + text: '---' // 分隔线 + }, + { + text: '复制 (Ctrl+C)', + callback() { + handleCopy(); + } + }, + { + text: '粘贴 (Ctrl+V)', + callback() { + handlePaste(); + } + }, + { + text: '---' // 分隔线 + }, + { + text: '组合 (Ctrl+G)', + callback() { + groupSelectedNodes(); + } + }, + { + text: '解组 (Ctrl+U)', + callback() { + ungroupSelectedNodes(); + } + }, + { + text: '---' // 分隔线 + }, + { + text: '锁定/解锁 (Ctrl+L)', + callback() { + toggleLockSelected(); + } + }, + { + text: '显示/隐藏 (Ctrl+Shift+H)', + callback() { + toggleVisibilitySelected(); + } + }, + { + text: '---' // 分隔线 + }, + { + text: '删除节点 (Del)', callback(node: NodeData) { - lfInstance.deleteNode(node.id); + deleteNode(node.id); } } ], @@ -669,6 +810,70 @@ onMounted(() => { y: data.y }); } + }, + { + text: '粘贴 (Ctrl+V)', + callback(data: Position) { + handlePaste(); + } + } + ] + }); + + // 配置多选时的右键菜单(选区菜单) + lfInstance.extension.menu.setMenuByType({ + type: 'lf:defaultSelectionMenu', + menu: [ + { + text: '复制 (Ctrl+C)', + callback() { + handleCopy(); + } + }, + { + text: '粘贴 (Ctrl+V)', + callback() { + handlePaste(); + } + }, + { + text: '---' // 分隔线 + }, + { + text: '组合 (Ctrl+G)', + callback() { + groupSelectedNodes(); + } + }, + { + text: '解组 (Ctrl+U)', + callback() { + ungroupSelectedNodes(); + } + }, + { + text: '---' // 分隔线 + }, + { + text: '锁定/解锁 (Ctrl+L)', + callback() { + toggleLockSelected(); + } + }, + { + text: '显示/隐藏 (Ctrl+Shift+H)', + callback() { + toggleVisibilitySelected(); + } + }, + { + text: '---' // 分隔线 + }, + { + text: '删除选中 (Del)', + callback() { + deleteSelectedElements(); + } } ] });