mirror of
https://github.com/Powerful-517/yys-editor.git
synced 2026-01-23 22:43:28 +00:00
feat(flow): 支持框选+网格吸附并新增对齐/等距分布操作
This commit is contained in:
@@ -1,17 +1,17 @@
|
|||||||
# 模块状态总览(重写)
|
# 模块状态总览(重写)
|
||||||
|
|
||||||
总体完成度(粗略):约 68%
|
总体完成度(粗略):约 73%
|
||||||
|
|
||||||
## 1. 画布(LogicFlow) — 完成度:68%
|
## 1. 画布(LogicFlow) — 完成度:75%
|
||||||
- 已完成:
|
- 已完成:
|
||||||
- 初始化与销毁:LogicFlow 实例、网格/缩放/旋转、节点选中/空白取消(src/components/flow/FlowEditor.vue)
|
- 初始化与销毁:LogicFlow 实例、网格/缩放/旋转、节点选中/空白取消(src/components/flow/FlowEditor.vue)
|
||||||
- 自定义节点注册:`shikigamiSelect`、`yuhunSelect`、`propertySelect`、`imageNode`
|
- 自定义节点注册:`shikigamiSelect`、`yuhunSelect`、`propertySelect`、`imageNode`
|
||||||
- 与 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`)
|
- 右键菜单:节点置顶/置底与删除、边删除、画布添加节点(基于 LogicFlow Menu + `setElementZIndex`)
|
||||||
|
- 多选/框选、对齐线、吸附网格;左/右/上/下/水平/垂直居中与横/纵等距分布(SelectionSelect + snapline + 自定义对齐分布指令)
|
||||||
- 未完成:
|
- 未完成:
|
||||||
- 右键菜单层级命令:已接通置顶/置底,单步前移/后移(`bringForward`/`sendBackward`)未实现
|
- 右键菜单层级命令:已接通置顶/置底,单步前移/后移(`bringForward`/`sendBackward`)未实现
|
||||||
- 多选/框选、对齐线、对齐/平均分布、吸附到网格
|
|
||||||
- 撤销重做、组合/锁定/隐藏、快捷键(Del/Ctrl+C/V、方向键微移、Ctrl+Z/Y)
|
- 撤销重做、组合/锁定/隐藏、快捷键(Del/Ctrl+C/V、方向键微移、Ctrl+Z/Y)
|
||||||
- MiniMap/控制条/Snapshot 等扩展能力
|
- MiniMap/控制条/Snapshot 等扩展能力
|
||||||
|
|
||||||
@@ -33,13 +33,12 @@
|
|||||||
- `textNode` 富文本编辑与同步
|
- `textNode` 富文本编辑与同步
|
||||||
- 字段校验/联动、常用模板一键填充、更多样式项(填充/描边/圆角/阴影/透明度)
|
- 字段校验/联动、常用模板一键填充、更多样式项(填充/描边/圆角/阴影/透明度)
|
||||||
|
|
||||||
## 4. 工具栏(Toolbar) — 完成度:70%
|
## 4. 工具栏(Toolbar) — 完成度:80%
|
||||||
- 已完成:
|
- 已完成:
|
||||||
- 导入/导出(走 store)、更新日志、问题反馈(src/components/Toolbar.vue)
|
- 导入/导出(走 store)、更新日志、问题反馈(src/components/Toolbar.vue)
|
||||||
- 水印设置(文本/字号/颜色/角度/行列)并持久化到 localStorage
|
- 水印设置(文本/字号/颜色/角度/行列)并持久化到 localStorage
|
||||||
- 截图预览弹窗框架
|
- 截图预览与导出:基于 LogicFlow Snapshot 获取 PNG,叠加自定义水印,预览/下载
|
||||||
- 未完成:
|
- 未完成:
|
||||||
- 截图逻辑仍按旧的 VueFlow 容器选择器查找,未对接 LogicFlow 容器;`captureFlow` 未实现/被注释
|
|
||||||
- 导出文件命名/版本号与 `schemaVersion`,导入时的校验与迁移提示
|
- 导出文件命名/版本号与 `schemaVersion`,导入时的校验与迁移提示
|
||||||
|
|
||||||
## 5. 弹窗系统(Dialogs) — 完成度:75%
|
## 5. 弹窗系统(Dialogs) — 完成度:75%
|
||||||
@@ -104,9 +103,9 @@
|
|||||||
|
|
||||||
- 推荐开发顺序(每步可独立验收)
|
- 推荐开发顺序(每步可独立验收)
|
||||||
1) 节点最小化打通:已注册并可用 `imageNode`(上传/URL/fit/宽高);`textNode` 待注册 + 富文本/样式;PropertyPanel 已按类型拆分子组件(`src/components/flow/FlowEditor.vue`, `src/components/flow/PropertyPanel.vue`)。
|
1) 节点最小化打通:已注册并可用 `imageNode`(上传/URL/fit/宽高);`textNode` 待注册 + 富文本/样式;PropertyPanel 已按类型拆分子组件(`src/components/flow/FlowEditor.vue`, `src/components/flow/PropertyPanel.vue`)。
|
||||||
2) 截图修复:改为基于 LogicFlow 容器导出 PNG,沿用水印配置(`src/components/Toolbar.vue`)。
|
2) 截图修复:改为基于 LogicFlow 容器导出 PNG,沿用水印配置(`src/components/Toolbar.vue`)。已完成
|
||||||
3) 图层命令 MVP:基于 LogicFlow 的层级/前后置 API 封装 bringToFront/sendToBack/bringForward/sendBackward + 右键菜单,如需持久化仅同步引擎提供的层级信息(`src/components/flow/FlowEditor.vue`)。已完成:置顶/置底 + 右键菜单;待补:单步前移/后移。
|
3) 图层命令 MVP:基于 LogicFlow 的层级/前后置 API 封装 bringToFront/sendToBack/bringForward/sendBackward + 右键菜单,如需持久化仅同步引擎提供的层级信息(`src/components/flow/FlowEditor.vue`)。已完成:置顶/置底 + 右键菜单;待补:单步前移/后移。
|
||||||
4) 多选/对齐/吸附:框选、对齐线、吸附网格;左/右/上/下/水平/垂直居中与横/纵等距分布(FlowEditor/extension)。
|
4) 多选/对齐/吸附:框选、对齐线、吸附网格;左/右/上/下/水平/垂直居中与横/纵等距分布(FlowEditor/extension)。已完成
|
||||||
5) 快捷键与微调:Del 删除、方向键微移、Ctrl+C/V 复制粘贴、Ctrl+G/U 组/解组(简单组:父 meta id + 同步移动)、锁定/隐藏(`properties.locked`/`visible`)。
|
5) 快捷键与微调:Del 删除、方向键微移、Ctrl+C/V 复制粘贴、Ctrl+G/U 组/解组(简单组:父 meta id + 同步移动)、锁定/隐藏(`properties.locked`/`visible`)。
|
||||||
6) 样式模型补齐:统一 `properties.style` 字段并在 PropertyPanel 全量编辑(填充/描边/圆角/阴影/透明度/文字对齐/行高/字重)。
|
6) 样式模型补齐:统一 `properties.style` 字段并在 PropertyPanel 全量编辑(填充/描边/圆角/阴影/透明度/文字对齐/行高/字重)。
|
||||||
7) 扩展与控制:接入 MiniMap/Control/Snapshot;Toolbar 增加吸附/对齐开关与清空画布。
|
7) 扩展与控制:接入 MiniMap/Control/Snapshot;Toolbar 增加吸附/对齐开关与清空画布。
|
||||||
@@ -126,7 +125,7 @@
|
|||||||
- 截图基于 DOM 选择器易漂移;由 FlowEditor 提供 `getCanvasEl()`,Toolbar 仅依赖该接口。
|
- 截图基于 DOM 选择器易漂移;由 FlowEditor 提供 `getCanvasEl()`,Toolbar 仅依赖该接口。
|
||||||
|
|
||||||
- 验收停靠点
|
- 验收停靠点
|
||||||
- 1/2 结束:冻结顶层导出结构 v1(Root Document + LogicFlow GraphData)与截图容器约定;filesStore 开始写入 `schemaVersion`。(当前已完成:Root Document + GraphData + schemaVersion 持久化;截图容器约定待完成)
|
- 1/2 结束:冻结顶层导出结构 v1(Root Document + LogicFlow GraphData)与截图容器约定;filesStore 开始写入 `schemaVersion`。(当前已完成:Root Document + GraphData + schemaVersion 持久化;截图容器约定已完成:LogicFlow Snapshot + 水印)
|
||||||
- 3/4 结束:冻结 CanvasService API;层级/对齐操作均能回放。
|
- 3/4 结束:冻结 CanvasService API;层级/对齐操作均能回放。
|
||||||
- 6 结束:统一样式模型,三类节点在 UI 中可一致编辑。
|
- 6 结束:统一样式模型,三类节点在 UI 中可一致编辑。
|
||||||
- 8 结束:确认 SVG 导入/导出链路稳定。
|
- 8 结束:确认 SVG 导入/导出链路稳定。
|
||||||
|
|||||||
@@ -2,6 +2,50 @@
|
|||||||
<div class="editor-layout" :style="{ height }">
|
<div class="editor-layout" :style="{ height }">
|
||||||
<!-- 中间流程图区域 -->
|
<!-- 中间流程图区域 -->
|
||||||
<div class="flow-container">
|
<div class="flow-container">
|
||||||
|
<div class="flow-controls">
|
||||||
|
<div class="control-row toggles">
|
||||||
|
<label class="control-toggle">
|
||||||
|
<input type="checkbox" v-model="selectionEnabled" />
|
||||||
|
<span>框选</span>
|
||||||
|
</label>
|
||||||
|
<label class="control-toggle">
|
||||||
|
<input type="checkbox" v-model="snapGridEnabled" />
|
||||||
|
<span>吸附网格</span>
|
||||||
|
</label>
|
||||||
|
<span class="control-hint">对齐线已开启</span>
|
||||||
|
<span class="control-hint">已选 {{ selectedCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">对齐</div>
|
||||||
|
<div class="control-buttons">
|
||||||
|
<button
|
||||||
|
v-for="btn in alignmentButtons"
|
||||||
|
:key="btn.key"
|
||||||
|
class="control-button"
|
||||||
|
type="button"
|
||||||
|
:disabled="selectedCount < 2"
|
||||||
|
@click="() => alignSelected(btn.key)"
|
||||||
|
>
|
||||||
|
{{ btn.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="control-label">分布</div>
|
||||||
|
<div class="control-buttons">
|
||||||
|
<button
|
||||||
|
v-for="btn in distributeButtons"
|
||||||
|
:key="btn.key"
|
||||||
|
class="control-button"
|
||||||
|
type="button"
|
||||||
|
:disabled="selectedCount < 3"
|
||||||
|
@click="() => distributeSelected(btn.key)"
|
||||||
|
>
|
||||||
|
{{ btn.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="container" ref="containerRef" :style="{ height: '100%' }"></div>
|
<div class="container" ref="containerRef" :style="{ height: '100%' }"></div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 右侧属性面板 -->
|
<!-- 右侧属性面板 -->
|
||||||
@@ -10,11 +54,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, onMounted, onBeforeUnmount, defineExpose } from 'vue';
|
import { ref, watch, onMounted, onBeforeUnmount } from 'vue';
|
||||||
import LogicFlow, { EventType } from '@logicflow/core';
|
import LogicFlow, { EventType } from '@logicflow/core';
|
||||||
|
import type { Position, NodeData, EdgeData, BaseNodeModel, GraphModel } from '@logicflow/core';
|
||||||
import '@logicflow/core/lib/style/index.css';
|
import '@logicflow/core/lib/style/index.css';
|
||||||
import { Menu, Label, Snapshot } from "@logicflow/extension";
|
import { Menu, Label, Snapshot, SelectionSelect } from '@logicflow/extension';
|
||||||
import "@logicflow/extension/lib/style/index.css";
|
import '@logicflow/extension/lib/style/index.css';
|
||||||
import '@logicflow/core/es/index.css';
|
import '@logicflow/core/es/index.css';
|
||||||
import '@logicflow/extension/es/index.css';
|
import '@logicflow/extension/es/index.css';
|
||||||
|
|
||||||
@@ -25,11 +70,11 @@ import PropertySelectNode from './nodes/yys/PropertySelectNode.vue';
|
|||||||
import ImageNode from './nodes/common/ImageNode.vue';
|
import ImageNode from './nodes/common/ImageNode.vue';
|
||||||
// import TextNode from './nodes/common/TextNode.vue';
|
// import TextNode from './nodes/common/TextNode.vue';
|
||||||
import PropertyPanel from './PropertyPanel.vue';
|
import PropertyPanel from './PropertyPanel.vue';
|
||||||
import { useFilesStore } from "@/ts/useStore";
|
import { useGlobalMessage } from '@/ts/useGlobalMessage';
|
||||||
import { setLogicFlowInstance, destroyLogicFlowInstance } from '@/ts/useLogicFlow';
|
import { setLogicFlowInstance, destroyLogicFlowInstance } from '@/ts/useLogicFlow';
|
||||||
import Position = LogicFlow.Position;
|
|
||||||
import NodeData = LogicFlow.NodeData;
|
type AlignType = 'left' | 'right' | 'top' | 'bottom' | 'hcenter' | 'vcenter';
|
||||||
import EdgeData = LogicFlow.EdgeData;
|
type DistributeType = 'horizontal' | 'vertical';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
height?: string;
|
height?: string;
|
||||||
@@ -37,18 +82,147 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const containerRef = ref<HTMLElement | null>(null);
|
const containerRef = ref<HTMLElement | null>(null);
|
||||||
const lf = ref<LogicFlow | null>(null);
|
const lf = ref<LogicFlow | null>(null);
|
||||||
|
const selectedCount = ref(0);
|
||||||
// 右键菜单相关
|
const selectionEnabled = ref(true);
|
||||||
const contextMenu = ref({
|
const snapGridEnabled = ref(true);
|
||||||
show: false,
|
const alignmentButtons: { key: AlignType; label: string }[] = [
|
||||||
x: 0,
|
{ key: 'left', label: '左对齐' },
|
||||||
y: 0,
|
{ key: 'right', label: '右对齐' },
|
||||||
nodeId: null
|
{ key: 'top', label: '上对齐' },
|
||||||
});
|
{ key: 'bottom', label: '下对齐' },
|
||||||
|
{ key: 'hcenter', label: '水平居中' },
|
||||||
|
{ key: 'vcenter', label: '垂直居中' }
|
||||||
|
];
|
||||||
|
const distributeButtons: { key: DistributeType; label: string }[] = [
|
||||||
|
{ key: 'horizontal', label: '水平等距' },
|
||||||
|
{ key: 'vertical', label: '垂直等距' }
|
||||||
|
];
|
||||||
|
const { showMessage } = useGlobalMessage();
|
||||||
|
|
||||||
// 当前选中节点
|
// 当前选中节点
|
||||||
const selectedNode = ref<any>(null);
|
const selectedNode = ref<any>(null);
|
||||||
|
|
||||||
|
function updateSelectedCount(model?: GraphModel) {
|
||||||
|
const lfInstance = lf.value;
|
||||||
|
const graphModel = model ?? lfInstance?.graphModel;
|
||||||
|
selectedCount.value = graphModel?.selectNodes.length ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySelectionSelect(enabled: boolean) {
|
||||||
|
const lfInstance = lf.value as any;
|
||||||
|
if (!lfInstance) return;
|
||||||
|
if (enabled) {
|
||||||
|
lfInstance.openSelectionSelect?.();
|
||||||
|
} else {
|
||||||
|
lfInstance.closeSelectionSelect?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySnapGrid(enabled: boolean) {
|
||||||
|
const lfInstance = lf.value;
|
||||||
|
if (!lfInstance) return;
|
||||||
|
lfInstance.updateEditConfig({ snapGrid: enabled });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedRects() {
|
||||||
|
const lfInstance = lf.value;
|
||||||
|
if (!lfInstance) return [];
|
||||||
|
return lfInstance.graphModel.selectNodes.map((model: BaseNodeModel) => {
|
||||||
|
const bounds = model.getBounds();
|
||||||
|
const width = bounds.maxX - bounds.minX;
|
||||||
|
const height = bounds.maxY - bounds.minY;
|
||||||
|
return {
|
||||||
|
model,
|
||||||
|
bounds,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
centerX: (bounds.maxX + bounds.minX) / 2,
|
||||||
|
centerY: (bounds.maxY + bounds.minY) / 2
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function alignSelected(direction: AlignType) {
|
||||||
|
const rects = getSelectedRects();
|
||||||
|
if (rects.length < 2) {
|
||||||
|
showMessage('warning', '请选择至少两个节点再执行对齐');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const minX = Math.min(...rects.map((item) => item.bounds.minX));
|
||||||
|
const maxX = Math.max(...rects.map((item) => item.bounds.maxX));
|
||||||
|
const minY = Math.min(...rects.map((item) => item.bounds.minY));
|
||||||
|
const maxY = Math.max(...rects.map((item) => item.bounds.maxY));
|
||||||
|
const centerX = (minX + maxX) / 2;
|
||||||
|
const centerY = (minY + maxY) / 2;
|
||||||
|
|
||||||
|
rects.forEach(({ model, width, height }) => {
|
||||||
|
let targetX = model.x;
|
||||||
|
let targetY = model.y;
|
||||||
|
|
||||||
|
switch (direction) {
|
||||||
|
case 'left':
|
||||||
|
targetX = minX + width / 2;
|
||||||
|
break;
|
||||||
|
case 'right':
|
||||||
|
targetX = maxX - width / 2;
|
||||||
|
break;
|
||||||
|
case 'hcenter':
|
||||||
|
targetX = centerX;
|
||||||
|
break;
|
||||||
|
case 'top':
|
||||||
|
targetY = minY + height / 2;
|
||||||
|
break;
|
||||||
|
case 'bottom':
|
||||||
|
targetY = maxY - height / 2;
|
||||||
|
break;
|
||||||
|
case 'vcenter':
|
||||||
|
targetY = centerY;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
model.moveTo(targetX, targetY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function distributeSelected(type: DistributeType) {
|
||||||
|
const rects = getSelectedRects();
|
||||||
|
if (rects.length < 3) {
|
||||||
|
showMessage('warning', '请选择至少三个节点再执行分布');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorted = [...rects].sort((a, b) =>
|
||||||
|
type === 'horizontal' ? a.bounds.minX - b.bounds.minX : a.bounds.minY - b.bounds.minY
|
||||||
|
);
|
||||||
|
const first = sorted[0];
|
||||||
|
const last = sorted[sorted.length - 1];
|
||||||
|
|
||||||
|
if (type === 'horizontal') {
|
||||||
|
const totalWidth = sorted.reduce((sum, item) => sum + item.width, 0);
|
||||||
|
const gap = (last.bounds.maxX - first.bounds.minX - totalWidth) / (sorted.length - 1);
|
||||||
|
let cursor = first.bounds.minX + first.width;
|
||||||
|
|
||||||
|
for (let i = 1; i < sorted.length - 1; i += 1) {
|
||||||
|
cursor += gap;
|
||||||
|
const item = sorted[i];
|
||||||
|
item.model.moveTo(cursor + item.width / 2, item.centerY);
|
||||||
|
cursor += item.width;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const totalHeight = sorted.reduce((sum, item) => sum + item.height, 0);
|
||||||
|
const gap = (last.bounds.maxY - first.bounds.minY - totalHeight) / (sorted.length - 1);
|
||||||
|
let cursor = first.bounds.minY + first.height;
|
||||||
|
|
||||||
|
for (let i = 1; i < sorted.length - 1; i += 1) {
|
||||||
|
cursor += gap;
|
||||||
|
const item = sorted[i];
|
||||||
|
item.model.moveTo(item.centerX, cursor + item.height / 2);
|
||||||
|
cursor += item.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 注册自定义节点
|
// 注册自定义节点
|
||||||
function registerNodes(lfInstance: LogicFlow) {
|
function registerNodes(lfInstance: LogicFlow) {
|
||||||
register({ type: 'shikigamiSelect', component: ShikigamiSelectNode }, lfInstance);
|
register({ type: 'shikigamiSelect', component: ShikigamiSelectNode }, lfInstance);
|
||||||
@@ -63,12 +237,12 @@ function registerNodes(lfInstance: LogicFlow) {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
lf.value = new LogicFlow({
|
lf.value = new LogicFlow({
|
||||||
container: containerRef.value,
|
container: containerRef.value,
|
||||||
// container: document.querySelector('#container'),
|
grid: { type: 'dot', size: 10 },
|
||||||
grid: true,
|
|
||||||
allowResize: true,
|
allowResize: true,
|
||||||
allowRotate: true,
|
allowRotate: true,
|
||||||
overlapMode:-1,
|
overlapMode: -1,
|
||||||
plugins: [Menu, Label, Snapshot],
|
snapline: true,
|
||||||
|
plugins: [Menu, Label, Snapshot, SelectionSelect],
|
||||||
pluginsOptions: {
|
pluginsOptions: {
|
||||||
label: {
|
label: {
|
||||||
isMultiple: true,
|
isMultiple: true,
|
||||||
@@ -89,14 +263,14 @@ onMounted(() => {
|
|||||||
text: '置于顶层',
|
text: '置于顶层',
|
||||||
callback(node: NodeData) {
|
callback(node: NodeData) {
|
||||||
lfInstance.setElementZIndex(node.id, 'top');
|
lfInstance.setElementZIndex(node.id, 'top');
|
||||||
console.log("置顶"+lfInstance.getNodeModelById(node.id).zIndex)
|
console.log('置顶' + lfInstance.getNodeModelById(node.id).zIndex);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: '置于底层',
|
text: '置于底层',
|
||||||
callback(node: NodeData) {
|
callback(node: NodeData) {
|
||||||
lfInstance.setElementZIndex(node.id, 'bottom');
|
lfInstance.setElementZIndex(node.id, 'bottom');
|
||||||
console.log("置底"+lfInstance.getNodeModelById(node.id).zIndex)
|
console.log('置底' + lfInstance.getNodeModelById(node.id).zIndex);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -133,15 +307,23 @@ onMounted(() => {
|
|||||||
lfInstance.render({
|
lfInstance.render({
|
||||||
// 渲染的数据
|
// 渲染的数据
|
||||||
});
|
});
|
||||||
|
lfInstance.updateEditConfig({
|
||||||
|
multipleSelectKey: 'shift',
|
||||||
|
snapGrid: snapGridEnabled.value
|
||||||
|
});
|
||||||
|
applySelectionSelect(selectionEnabled.value);
|
||||||
|
updateSelectedCount(lfInstance.graphModel);
|
||||||
|
|
||||||
// 监听节点点击事件,更新 selectedNode
|
// 监听节点点击事件,更新 selectedNode
|
||||||
lfInstance.on(EventType.NODE_CLICK, ({ data }) => {
|
lfInstance.on(EventType.NODE_CLICK, ({ data }) => {
|
||||||
selectedNode.value = data;
|
selectedNode.value = data;
|
||||||
|
updateSelectedCount();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听空白点击事件,取消选中
|
// 监听空白点击事件,取消选中
|
||||||
lfInstance.on(EventType.BLANK_CLICK, () => {
|
lfInstance.on(EventType.BLANK_CLICK, () => {
|
||||||
selectedNode.value = null;
|
selectedNode.value = null;
|
||||||
|
updateSelectedCount();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 节点属性改变,如果当前节点是选中节点,则同步更新 selectedNode
|
// 节点属性改变,如果当前节点是选中节点,则同步更新 selectedNode
|
||||||
@@ -156,6 +338,17 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
lfInstance.on('selection:selected', () => updateSelectedCount());
|
||||||
|
lfInstance.on('selection:drop', () => updateSelectedCount());
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(selectionEnabled, (enabled) => {
|
||||||
|
applySelectionSelect(enabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(snapGridEnabled, (enabled) => {
|
||||||
|
applySnapGrid(enabled);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 销毁 LogicFlow
|
// 销毁 LogicFlow
|
||||||
@@ -168,24 +361,6 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 右键菜单相关
|
|
||||||
function handleNodeContextMenu({ data, e }: { data: any; e: MouseEvent }) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
contextMenu.value = {
|
|
||||||
show: true,
|
|
||||||
x: e.clientX,
|
|
||||||
y: e.clientY,
|
|
||||||
nodeId: data.id
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function handlePaneContextMenu({ e }: { e: MouseEvent }) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
contextMenu.value.show = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -204,6 +379,63 @@ function handlePaneContextMenu({ e }: { e: MouseEvent }) {
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
.flow-controls {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
z-index: 10;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||||
|
max-width: 460px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.control-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.control-row:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.control-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
.control-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.control-button {
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
.control-button:hover:enabled {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
.control-button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.control-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
.control-hint {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
.context-menu {
|
.context-menu {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background: white;
|
background: white;
|
||||||
|
|||||||
Reference in New Issue
Block a user