右键菜单实现

This commit is contained in:
2026-01-22 22:26:53 +08:00
parent 066637c1fa
commit 9397f357d1
2 changed files with 223 additions and 25 deletions

View File

@@ -2,13 +2,13 @@
## 📊 项目完成度总览 ## 📊 项目完成度总览
**总体完成度82%** | **愿景一完成度70%** (步骤1-7/10已完成) **总体完成度85%** | **愿景一完成度75%** (步骤1-7/10已完成步骤3完全完成)
### 核心模块完成度 ### 核心模块完成度
| 模块 | 完成度 | 状态 | 关键缺失 | | 模块 | 完成度 | 状态 | 关键缺失 |
|------|--------|------|----------| |------|--------|------|----------|
| 🎨 画布LogicFlow | 85% | ✅ 良好 | 单步前移/后移、撤销重做 | | 🎨 画布LogicFlow | 90% | ✅ 优秀 | 撤销重做 |
| 📦 左侧组件库 | 65% | ⚠️ 可用 | textNode未注册、缩略图 | | 📦 左侧组件库 | 65% | ⚠️ 可用 | textNode未注册、缩略图 |
| ⚙️ 右侧属性面板 | 85% | ✅ 良好 | textNode富文本编辑 | | ⚙️ 右侧属性面板 | 85% | ✅ 良好 | textNode富文本编辑 |
| 🔧 工具栏 | 85% | ✅ 良好 | 导出命名优化 | | 🔧 工具栏 | 85% | ✅ 良好 | 导出命名优化 |
@@ -24,7 +24,7 @@
|------|------|------|------| |------|------|------|------|
| 1 | 节点最小化打通 | ✅ 完成 | imageNode可用textNode待注册 | | 1 | 节点最小化打通 | ✅ 完成 | imageNode可用textNode待注册 |
| 2 | 截图修复 | ✅ 完成 | LogicFlow Snapshot + 水印 | | 2 | 截图修复 | ✅ 完成 | LogicFlow Snapshot + 水印 |
| 3 | 图层命令MVP | ⚠️ 部分 | 置顶/置底✅,**前移/后移❌** | | 3 | 图层命令MVP | ✅ 完成 | 置顶/置底/前移/后移全部完成 |
| 4 | 多选/对齐/吸附 | ✅ 完成 | 6种对齐 + 2种分布 | | 4 | 多选/对齐/吸附 | ✅ 完成 | 6种对齐 + 2种分布 |
| 5 | 快捷键与微调 | ✅ 完成 | 8种快捷键全部工作 | | 5 | 快捷键与微调 | ✅ 完成 | 8种快捷键全部工作 |
| 6 | 样式模型补齐 | ✅ 完成 | 11个样式属性统一 | | 6 | 样式模型补齐 | ✅ 完成 | 11个样式属性统一 |
@@ -36,12 +36,7 @@
## 🎯 下一步行动计划 ## 🎯 下一步行动计划
### 🔴 高优先级(立即行动) ### 🔴 高优先级(立即行动)
1. **补全图层命令** - 实现 `bringForward`/`sendBackward` 1. **注册textNode** - 取消注释并验证基本功能
- 位置:[FlowEditor.vue:631-674](../src/components/flow/FlowEditor.vue#L631-L674)
- 影响解除步骤3阻塞完善右键菜单
- 预期调用LogicFlow层级API或自定义z-index管理
2. **注册textNode** - 取消注释并验证基本功能
- 位置:[FlowEditor.vue:76](../src/components/flow/FlowEditor.vue#L76) - 位置:[FlowEditor.vue:76](../src/components/flow/FlowEditor.vue#L76)
- 影响完成步骤1启用文本节点 - 影响完成步骤1启用文本节点
- 预期:基本渲染工作,富文本编辑可延后 - 预期:基本渲染工作,富文本编辑可延后
@@ -71,20 +66,19 @@
## 📋 详细模块状态 ## 📋 详细模块状态
## 1. 画布LogicFlow — 完成度:85% ## 1. 画布LogicFlow — 完成度:90%
- 已完成: - 已完成:
- 初始化与销毁LogicFlow 实例、网格/缩放/旋转、节点选中/空白取消src/components/flow/FlowEditor.vue - 初始化与销毁LogicFlow 实例、网格/缩放/旋转、节点选中/空白取消src/components/flow/FlowEditor.vue
- 自定义节点注册:`shikigamiSelect``yuhunSelect``propertySelect``imageNode`src/components/flow/FlowEditor.vue:567-574 - 自定义节点注册:`shikigamiSelect``yuhunSelect``propertySelect``imageNode`src/components/flow/FlowEditor.vue:567-574
- 与 Store 联动:读取/写入 `graphRawData``transform`(缩放/位移src/ts/useStore.ts, src/ts/useLogicFlow.ts - 与 Store 联动:读取/写入 `graphRawData``transform`(缩放/位移src/ts/useStore.ts, src/ts/useLogicFlow.ts
- DnD 接入:由组件库触发拖拽放置 - 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 - 多选/框选、对齐线、吸附网格;左/右/上/下/水平/垂直居中与横/纵等距分布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 - 扩展与控制:接入 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 - **组合/锁定/隐藏**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 - **快捷键系统**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 - **节点元数据管理**meta.visible、meta.locked、meta.groupId 支持与规范化src/components/flow/FlowEditor.vue:133-209
- 未完成: - 未完成:
- 右键菜单层级命令:已接通置顶/置底,单步前移/后移(`bringForward`/`sendBackward`)未实现
- **撤销重做**Ctrl+Z/Y 历史栈与操作回放 - **撤销重做**Ctrl+Z/Y 历史栈与操作回放
- **textNode 注册**:已在 ComponentsPanel 定义但 FlowEditor.vue:76 中被注释未注册 - **textNode 注册**:已在 ComponentsPanel 定义但 FlowEditor.vue:76 中被注释未注册
@@ -192,7 +186,7 @@
- 推荐开发顺序(每步可独立验收) - 推荐开发顺序(每步可独立验收)
1) ✅ **节点最小化打通**imageNode 已注册并可用(上传/URL/fit/宽高textNode 已在 ComponentsPanel 定义但 FlowEditor 未注册PropertyPanel 已按类型拆分子组件ShikigamiPanel/YuhunPanel/PropertyRulePanel/ImagePanel/TextPanel/StylePanel 1) ✅ **节点最小化打通**imageNode 已注册并可用(上传/URL/fit/宽高textNode 已在 ComponentsPanel 定义但 FlowEditor 未注册PropertyPanel 已按类型拆分子组件ShikigamiPanel/YuhunPanel/PropertyRulePanel/ImagePanel/TextPanel/StylePanel
2) ✅ **截图修复**:已改为基于 LogicFlow Snapshot 导出 PNG沿用水印配置src/components/Toolbar.vue:prepareCapture 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 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 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.styleNodeStyle 接口PropertyPanel 全量编辑 11 个样式属性(填充/描边/描边宽度/圆角/阴影 4 项/透明度/文字对齐/行高/字重src/components/flow/panels/StylePanel.vue, src/ts/nodeStyle.ts 6) ✅ **样式模型补齐**:统一 properties.styleNodeStyle 接口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.tsschemaVersion="1.0.0" 持久化src/ts/useStore.ts截图基于 LogicFlow Snapshot + 水印src/components/Toolbar.vue -**1/2 结束**Root Document + LogicFlow GraphData 结构已冻结src/ts/schema.tsschemaVersion="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 个样式属性 -**6 结束**样式模型已统一NodeStyle 接口imageNode/shikigamiSelect/yuhunSelect/propertySelect 四类节点均可通过 StylePanel 一致编辑 11 个样式属性
-**8 结束**vectorNode 未开始SVG 导入/导出链路未实现 -**8 结束**vectorNode 未开始SVG 导入/导出链路未实现
@@ -231,9 +225,8 @@
- ✅ 样式模型11 个样式属性统一编辑 - ✅ 样式模型11 个样式属性统一编辑
- ✅ 扩展控制MiniMap/Control/Snapshot 插件 + Toolbar 开关 - ✅ 扩展控制MiniMap/Control/Snapshot 插件 + Toolbar 开关
### 待完成的愿景一功能(步骤 3/8/9/1030% ### 待完成的愿景一功能(步骤 8/9/1025%
- ⚠️ **高优先级(阻塞)** - ⚠️ **高优先级(阻塞)**
- 单步前移/后移bringForward/sendBackward- 步骤 3 残留
- 撤销重做Ctrl+Z/Y- 步骤 10依赖历史栈 - 撤销重做Ctrl+Z/Y- 步骤 10依赖历史栈
- ⚠️ **中优先级(功能完整性)** - ⚠️ **中优先级(功能完整性)**
- textNode 注册与富文本编辑 - 步骤 1 残留 - textNode 注册与富文本编辑 - 步骤 1 残留
@@ -242,9 +235,9 @@
- SVG/PDF 导出 - 步骤 9 - SVG/PDF 导出 - 步骤 9
### 建议的下一步行动 ### 建议的下一步行动
1. **立即行动**补全 bringForward/sendBackward解除步骤 3 阻塞 1. **立即行动**注册 textNode取消 FlowEditor.vue:76 注释
2. **短期目标**注册 textNode取消 FlowEditor.vue:76 注释 2. **短期目标**实现撤销重做Action + HistoryService
3. **中期目标**实现撤销重做Action + HistoryService 3. **中期目标**textNode 富文本编辑
4. **长期目标**vectorNode + SVG 导出(步骤 8-9 4. **长期目标**vectorNode + SVG 导出(步骤 8-9
### 愿景二:联动 wiki/攻略站(浏览/复刻/继续编辑) ### 愿景二:联动 wiki/攻略站(浏览/复刻/继续编辑)
- 工具栏 - 工具栏

View File

@@ -256,6 +256,74 @@ function moveSelectedNodes(deltaX: number, deltaY: number) {
graphModel.moveNodes(targets, deltaX, deltaY); 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) { function deleteSelectedElements(event?: KeyboardEvent) {
if (shouldSkipShortcut(event)) return true; if (shouldSkipShortcut(event)) return true;
const lfInstance = lf.value; const lfInstance = lf.value;
@@ -280,6 +348,21 @@ function deleteSelectedElements(event?: KeyboardEvent) {
return false; 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) { function toggleLockSelected(event?: KeyboardEvent) {
if (shouldSkipShortcut(event)) return true; if (shouldSkipShortcut(event)) return true;
const models = getSelectedNodeModels(); const models = getSelectedNodeModels();
@@ -633,21 +716,79 @@ onMounted(() => {
{ {
text: '置于顶层', text: '置于顶层',
callback(node: NodeData) { callback(node: NodeData) {
lfInstance.setElementZIndex(node.id, 'top'); bringToFront(node.id);
console.log('置顶' + lfInstance.getNodeModelById(node.id).zIndex); }
},
{
text: '上移一层',
callback(node: NodeData) {
bringForward(node.id);
}
},
{
text: '下移一层',
callback(node: NodeData) {
sendBackward(node.id);
} }
}, },
{ {
text: '置于底层', text: '置于底层',
callback(node: NodeData) { callback(node: NodeData) {
lfInstance.setElementZIndex(node.id, 'bottom'); sendToBack(node.id);
console.log('置底' + lfInstance.getNodeModelById(node.id).zIndex);
} }
}, },
{ {
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) { callback(node: NodeData) {
lfInstance.deleteNode(node.id); deleteNode(node.id);
} }
} }
], ],
@@ -669,6 +810,70 @@ onMounted(() => {
y: data.y 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();
}
} }
] ]
}); });