# vue-x6-editor **Repository Path**: tang_zhanwang/vue-x6-editor ## Basic Information - **Project Name**: vue-x6-editor - **Description**: 是一个基于@antv/x6以及plain-ui-composition封装的Vue3.0可视化流程编辑组件;旨在于封装开箱即用的常用功能,包括快速定义画布组件、快速定义画布React组件、撤销重做、放大缩小、数据导入导出、冻结画布、拦截新增(删除)节点(边)等功能; - **Primary Language**: Unknown - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 12 - **Created**: 2023-06-30 - **Last Updated**: 2023-06-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README [toc] - 在线示例:[http://martsforever-pot.gitee.io/vue-x6-editor/](http://martsforever-pot.gitee.io/vue-x6-editor/) - React版本:[https://gitee.com/martsforever-pot/react-x6-editor](https://gitee.com/martsforever-pot/react-x6-editor) # 简介 - 是一个基于[@antv/x6](https://antv-x6.gitee.io/zh/docs/tutorial/about/)以及[plain-ui-composition](http://plain-pot.gitee.io/plain-ui-doc/#components%2Fcomposition%2Fintroduce.entire)封装的Vue3.0可视化流程编辑组件; - 旨在于封装开箱即用的常用功能,包括快速定义画布组件、快速定义画布React组件、撤销重做、放大缩小、数据导入导出、冻结画布、拦截新增(删除)节点(边)等功能; # 安装 ## 安装依赖 ```bash npm i vue-x6-editor plain-ui-compositoin @antv/x6 @antv/x6-vue-shape -S ``` ## 示例代码 - `plain-ui-composition`是一个基于Vue3.0 CompositionAPI封装的插件库,旨在于提供更好的组件与TS类型提示开发体验。 - 目前仅支持组合式API的方式使用。 ### JSX使用示例 ```jsx import {createApp} from "vue"; import {designPage} from 'plain-ui-composition'; import {createBranchNode, createEndNode, createFlowEdge, createFlowNode, createRenderNode, createStartNode, ReactX6Editor, useFlowEditor} from "vue-x6-editor"; import 'vue-x6-editor/dist/vue-x6-editor.css' import './main.scss'; const App = designPage(() => { const editor = useFlowEditor({ onlyOneStartNode: true, operators: [ { label: '选项一', icon: () => (), handler: () => console.log('option 1...') } ] }); editor.components.registry(createStartNode()); editor.components.registry(createFlowNode()); editor.components.registry(createBranchNode()); editor.components.registry(createEndNode()); editor.components.registry(createFlowEdge()); editor.components.registry(createRenderNode()); import('./flow.data.json').then(data => {editor.methods.update(data.default);}); editor.hooks.onCreateContextmenu.use(({ options, edge, node, type }) => { switch (type) { case 'node': options.push({ label: '自定义node选项2', icon: () => , handler: () => alert('自定义node选项2') }); return; case 'edge': options.push({ label: '自定义edge选项', icon: 'data', handler: () => alert('自定义edge选项') }); return; case 'vertex': options.push({ label: '自定义拐点选项', icon: 'data', handler: () => alert('自定义拐点选项') }); return; case 'blank': options.push({ label: '自定义白板选项', icon: 'data', handler: () => alert('自定义白板选项') }); return; } }); return () => (
{/*
{JSON.stringify(editor.state.data)}
*/}
); }); createApp().mount('#app'); ``` ### Template使用示例 - App.vue ```html ``` - main.tsx ```jsx import {createApp} from "vue"; import App from './App.vue'; createApp().mount('#app'); ``` # API说明 ## useFlowEditor对象参数类型 - useFlowEditor(options) |属性名称|类型|说明| |---|---|---| |height|string,number|画布高度| |width|string,number|画布宽度| |processGraphConfig|(data: { graphConfig: Partial, module: iGraphModule, config: iFlowEditorConfig }) => Partial|在创建graph对象之前,处理grapgConfig对象; |nodeConfig|Object|节点默认配置,请见下文nodeConfig说明| |onlyOneStartNode|boolean|只允许有一个开始节点| |cannotDeleteStartNode|boolean|不可以删除所有开始节点| |onlyOneEndNode|boolean|只允许有一个结束节点| |cannotDeleteEndNode|boolean|不可以删除所有结束节点| - nodeConfig |属性名称|类型|说明| |---|---|---| |deepColor|string|节点深色(主色调)| |lightColor|string|节点浅色| |edgeLightColor|string|连接线浅色| |edgeDeepColor|string|连接线深色| |fontSize|string|节点文字大小| |height|nuimber|节点默认高度| |width|number|节点默认宽度| ## x6画布参数设置 - 通过属性graphConfig设置一些功能的启用状态,比如默认关闭网格 - 关于画布的属性设置请见文档:[ API:Graph](https://antv-x6.gitee.io/zh/docs/api/graph/graph) ```jsx const editor = useFlowEditor({ graphConfig: { grid: false, } }); ``` ## 自定义(继承)组件 - `editor.components.registry(createFlowNode())`; - createFlowNode如下示例所示: ```jsx export function createFlowNode(custom: { name?: string, icon?: string, defaultLabel?: string } = {}): iFlowEditorComponent { const code = 'flow-node'; return { name: custom.name || '流程节点', icon: custom.icon || 'node', code, defaultNodeData: ({ config }) => ({ width: config.nodeConfig.width, height: config.nodeConfig.height, }), process: ({ Graph, config }) => { Graph.registerNode(code, { inherit: 'rect', attrs: { body: { stroke: config.nodeConfig.lightColor, fill: config.nodeConfig.lightColor, }, label: { text: custom.defaultLabel || '流程节点', fill: config.nodeConfig.deepColor, fontsize: config.nodeConfig.fontSize, } }, ports: { ...createFlowConnectPorts(config), } }, true); } }; } ``` ## 自定义(继承)连接线 - `editor.components.registry(createFlowEdge())`; - createFlowEdge如下示例所示,具体定义连接线请参考x6文档; ```jsx export function createFlowEdge(): iFlowEditorRegister { const code = 'flow-edge'; return { code, process: ({ Graph, config }) => { Graph.registerEdge(code, { ...createDefaultEdge(config), }, true); }, }; } ``` ## 定义Vue渲染的节点 - `editor.components.registry(createRenderNode());` - createRenderNode如下示例所示,示例中的`DemoRenderNodeContent`就是要实现的Vue组件; ```jsx import {iFlowEditorComponent} from "../utils/flow.type.base"; import {BaseVueComponent} from "../utils/BaseReactComponent"; import {DemoRenderNodeContent} from "./DemoRenderNodeContent"; import {createFlowConnectPorts} from "../utils/FlowConnectPorts"; export function createRenderNode(custom: { name?: string, icon?: string, defaultLabel?: string } = {}): iFlowEditorComponent { const code = 'render-node'; return { name: custom.name || 'Vue节点', icon: custom.icon || 'data', code, defaultNodeData: ({ config }) => ({ width: 300, height: 100, }), process: async ({ Graph, config }) => { Graph.registerNode(code, { inherit: 'vue-shape', component: ( ), ports: { ...createFlowConnectPorts(config), } }, true); } }; } ``` - DemoRenderNodeContent.tsx ```jsx import {designPage, reactive} from "plain-ui-composition"; import './DemoRenderNodeContent.scss'; import {defer, DFD} from 'plain-utils/utils/defer'; import {FlowIcon} from "./FlowIcon"; export const DemoRenderNodeContent = designPage(() => { interface iFormData { taskName: string, } const state = reactive({ formData: { taskName: '简历:前端工程师', } as iFormData }); const edit = (() => { const innerState = reactive({ editData: null as null | iFormData, }); let dfd: DFD | null = null; const open = () => { innerState.editData = JSON.parse(JSON.stringify(state.formData)); dfd = defer(); dfd.promise.then(val => { state.formData = val; }); }; const cancel = () => { dfd!.reject(); innerState.editData = null; dfd = null; }; const confirm = () => { dfd!.resolve(innerState.editData!); innerState.editData = null; dfd = null; }; const renderEdit = (notEditContent: any) => { return !innerState.editData ? notEditContent : <> innerState.editData!.taskName = e.target.value}/> ; }; const renderButton = (externals: any) => { return !innerState.editData ? <> {externals} : <> ; }; return { innerState, open, cancel, confirm, renderEdit, renderButton }; })(); return () => (
{edit.renderEdit(<>
{state.formData.taskName}
{(() => { const rate = Math.min(10, state.formData.taskName.length); return "★★★★★★★★★★☆☆☆☆☆☆☆☆☆☆".slice(10 - rate, 10 - rate + 10); })()}
)}
{edit.renderButton(<> )}
); }); ``` ## Hooks钩子(拦截器) - editor有很多内置的钩子函数,用于监听(拦截)各种行为,比如监听节点的点击事件: ```jsx editor.hooks.onClickNode.use(({ node }) => { console.log('click node', node); }); ``` - 钩子函数可以阻止,在添加的拦截函数中抛出异常或者返回`Promise.reject()`就可以阻止行为。比如添加节点的时候限制只能有一个开始(结束)节点的控制行为: ```jsx hooks.onAddNode.use(async ({ node }) => { if (node.shape == START_NODE) { if (!config.onlyOneStartNode) {return; } const { cells } = await methods.getGraphData(); if (!!cells.find(i => i.shape === START_NODE && i.id !== node.id)) { alert('只能存在一个开始节点!'); return Promise.reject('only one start node!'); } } else if (node.shape == END_NODE) { if (!config.onlyOneEndNode) {return; } const { cells } = await methods.getGraphData(); if (!!cells.find(i => i.shape === END_NODE && i.id !== node.id)) { alert('只能存在一个结束节点!'); return Promise.reject('only one end node!'); } } }); ``` ## 所有可用的Hook钩子 ```jsx export function useFlowEditorHooks() { const hooks = { onMouseupCanvas: createHooks<(e: MouseEvent) => void>(), // 点击画布动作 onGraphLoaded: createHooks<(module: iGraphModule) => void>(), // Graph模块加载完毕 onGraphReady: createHooks<(module: iGraphModule) => void>(), // graph初始化准备完毕触发动作,onGraphLoaded执行完毕之后就会执行这个动作 onFlowMounted: createHooks<() => void>(), // ReactX6Editor组件的mounted动作 onProcessOperators: createSyncHooks<(operators: iFlowEditorOperatorMeta[]) => void>(), // 处理操作栏按钮 /*节点钩子*/ onClickNode: createHooks<(e: NodeView.PositionEventArgs) => void>(), // 单击节点 onDblclickNode: createHooks<(e: NodeView.PositionEventArgs) => void>(), // 双击节点 onNodeContextmenu: createHooks<(e: NodeView.PositionEventArgs) => void>(), // 右击节点触发动作 onAddNode: createHooks<(e: { node: Node, cell: Cell, index: number }) => void>(), // 添加节点钩子 onDeleteNode: createHooks<(e: { node: Node, cell: Cell, index: number }) => void>(), // 删除节点钩子 /*连接线钩子*/ onClickEdge: createHooks<(e: EdgeView.PositionEventArgs) => void>(), // 单击连接线 onDblclickEdge: createHooks<(e: EdgeView.PositionEventArgs) => void>(), // 双击连接线 onEdgeContextmenu: createHooks<(e: EdgeView.PositionEventArgs) => void>(), // 右击边触发动作 onVertexContextmenu: createHooks<(e: EdgeView.PositionEventArgs) => void>(), // 右击拐点触发动作 onAddEdge: createHooks<(e: { edge: Edge, cell: Cell, index: number }) => void>(), // 添加连接线钩子 onDeleteEdge: createHooks<(e: { edge: Edge, cell: Cell, index: number }) => void>(), // 删除连接线钩子 onConnectedEdge: createHooks<(e: EdgeView.EventArgs["edge:connected"]) => void>(), // 连接线连接节点钩子 /*画布钩子*/ onBlankContextmenu: createHooks<(e: CellView.PositionEventArgs) => void>(), // 右击拐点触发动作 /*其他*/ onCreateContextmenu: createHooks<(e: { edge?: Edge, node?: Node, view?: View, type: eCreateContextmenuType | keyof typeof eCreateContextmenuType, options: iFlowContextmenuOption[], pos: { x: number, y: number } }) => void>(), }; return hooks; } ``` ## 自定义右击菜单 ```jsx editor.hooks.onCreateContextmenu.use(({ options, edge, node, type }) => { switch (type) { case 'node': options.push({ label: '自定义node选项', icon: 'data', handler: () => alert('自定义node选项') }); options.push({ label: '自定义node选项2', icon: () => , handler: () => alert('自定义node选项2') }); return; } }); ``` ## 自定义操作栏按钮 ```jsx const editor = useFlowEditor({ onlyOneStartNode: true, operators: [ { label: '选项一', icon: () => (), handler: () => console.log('option 1...') } ] }); ``` ## 可用函数methods - `useFlowEditor`创建的对象有许多可用函数,使用示例如下所示: ```jsx // 获取x6创建的graph对象 const { graph } = await editor.getGraph(); // 获取所有的cell const cells = graph.getSelectedCells(); ``` - 所有可用函数如下所示 |函数名称|类型|说明| |---|---|---| | getGraph | ()=>Promise | 异步获取graph实例对象,因为是按需加载antv/x6以及需要等待Editor挂载,所以graph初始化是一个异步的过程 | | update | (data:iFlowData)=>Promise | 更新数据 | | getGraphData | ()=><{ cells: iFlowMetaData[] }> | 从graph中解析json数据 | | deleteSelection | ()=>Promise) | 删除选中节点 | | undo | ()=>:Promise | 撤销 | | redo | ()=>:Promise | 重做 | | undoAndClearRedo | ()=>Promise | ,撤回,并且清空重做列表 | | setFrozen | (frozen?:boolean)=>Promise | 冻结 | ## createFlowEditorUser - 可以通过函数`createFlowEditorUser`自定义默认配置的组合函数,比如`useFlowEditor`的源码如下所示 ```jsx export const useFlowEditor = createFlowEditorUser({ height: '100%', width: '100%', processGraphConfig: ({ graphConfig, module, config }) => { return Object.assign({}, { background: { color: '#fff' }, // 默认背景色为白色 grid: { // 默认开启网格 size: 10, visible: true, }, connecting: { // 默认连接线为平滑连接线 connector: 'smooth', allowBlank: false, allowLoop: false, allowEdge: false, highlight: true, createEdge() { return new module.Shape.Edge({ shape: 'flow-edge', ...createDefaultEdge(config) }); }, }, panning: true, // 拖拽画布移动所有节点 resizing: true, // 节点选中之后可以调整大小 rotating: true, // 节点选中之后可以旋转 selecting: { // 默认开启框选功能 enabled: true, rubberband: true, showNodeSelectionBox: true, modifiers: ['shift', 'ctrl', 'meta'], }, snapline: { // 辅助对齐线 enabled: true, clean: false, }, keyboard: true, // 开启键盘快捷键功能 clipboard: true, // 开启剪切板功能 history: true, // 开启操作历史功能 } as X6GraphConfig, graphConfig); }, nodeConfig: { deepColor: '#1f74ff', // 深色(主色调) lightColor: '#deeaff', // 浅色(主色调背景色) edgeLightColor: '#b1cdfa', // 连接线浅色 edgeDeepColor: '#1f74ff', // 连接线深色 fontSize: '14', // 节点文字大小 width: 80, // 默认宽度 height: 40, // 默认高度 }, onlyOneStartNode: false, onlyOneEndNode: false, cannotDeleteStartNode: false, cannotDeleteEndNode: false, }); ``` # 操作说明 ## 节点连接桩 - 鼠标进入节点会显示节点的连接桩,拖拽连接桩到另一个节点或者另一个节点的连接桩,会创建一条连接线; - 点击节选会使得节点处于选中状态,处于选中状态的节点不会显示连接桩;但是此时可以拖拽调整节点大小。 - 默认情况下,一个节点连接到另一个节点,不会保留连接线的开始节点连接桩以及目标节点的连接桩。右击节点,启用【保留连接桩】,此时从该节点连接的连线在创建的时候就不会自动去掉连接桩,同理如果这个节点为连接线的目标节点也是一样。如果需要保留连接线的连接桩,需要右击节点勾选保留连接桩; ## 连接线拐点 - 默认情况下拐点的功能,是单击(双击)拖拽连接线添加拐点,或者移动拐点,双击存在的拐点删除拐点。 - 为了支持业务需求,目前默认禁用了这些行为。单击拖拽连接线空白处不会添加拐点,双击拐点不会删除拐点。而是右击连接线,在右击的位置添加拐点,右击拐点选择删除拐点。 - 单击连接线的时候会选中连接线,之后可以删除。同时预留双击连接线的功能用来处理更多业务需求; ## 可交互的React节点 - 默认情况下,选中节点之后,选中框会挡住节点。此时如果节点是自定义React渲染的节点,那么选中框会挡住React节点的内容(按钮、输入框等等)的点击动作。 - 目前为了优化这个逻辑,监听了选中的动作。当单选的时候,通过css选择器的方式,设置选中框的div的css属性`pointer-events`为`none`以避免挡住节点内容的操作;多选的时候则不做处理,允许选择框挡住节点(方便拖拽整体移动节点); # 快捷键 ## 多选 在画布上摁住shift可以框选多个节点,框选完毕之后,可以摁住ctrl或者meta键选中某个节点,以实现额外添加或者取消选中节点的功能; ## 复制 选中节点之后`meta/ctrl + c`可以复制节点;`meta/ctrl + v`可以粘贴节点; ## 剪切 选中节点之后`meta/ctrl + x`可以复制节点;`meta/ctrl + v`可以粘贴节点; ## 撤销 `meta/ctrl + z` 撤销刚刚的动作; ## 重做 `meta/ctrl + shift + z` 重做刚刚的动作; ## 全选 `meta/ctrl + shift + a` 全选画布中的节点; ## 删除 `backspace/delete` 删除选中节点; ## 放大 `meta/ctrl + 1` 放大画布 ## 缩小 `meta/ctrl + 2` 缩小画布