vue-x6-editor
v0.0.5
Published
[toc]
Downloads
14
Readme
[toc]
- 在线示例:http://martsforever-pot.gitee.io/vue-x6-editor/
- React版本:https://gitee.com/martsforever-pot/react-x6-editor
简介
- 是一个基于@antv/x6以及plain-ui-composition封装的Vue3.0可视化流程编辑组件;
- 旨在于封装开箱即用的常用功能,包括快速定义画布组件、快速定义画布React组件、撤销重做、放大缩小、数据导入导出、冻结画布、拦截新增(删除)节点(边)等功能;
安装
安装依赖
npm i vue-x6-editor plain-ui-compositoin @antv/x6 @antv/x6-vue-shape -S
示例代码
plain-ui-composition
是一个基于Vue3.0 CompositionAPI封装的插件库,旨在于提供更好的组件与TS类型提示开发体验。- 目前仅支持组合式API的方式使用。
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: () => (<i class="iconfont icon-pay_wechat" style={{ height: '27px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}/>),
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: () => <i class="iconfont icon-pay_zhifubao" style={{ padding: '0 4px' }}/>,
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 () => (
<div style={{ height: '100%', width: '100%' }}>
<ReactX6Editor editor={editor}/>
{/*<div style={{ position: 'absolute', bottom: 0, right: 0 }}>
{JSON.stringify(editor.state.data)}
</div>*/}
</div>
);
});
createApp(<App/>).mount('#app');
Template使用示例
- App.vue
<template>
<div :style="{ height: '100%', width: '100%' }">
<ReactX6Editor :editor="editor"/>
</div>
</template>
<script setup lang="tsx">
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 editor = useFlowEditor({
onlyOneStartNode: true,
operators: [
{
label: '选项一',
icon: () => (<i class="iconfont icon-pay_wechat" style={{ height: '27px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}/>),
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: () => <i class="iconfont icon-pay_zhifubao" style={{ padding: '0 4px' }}/>,
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;
}
});
</script>
- main.tsx
import {createApp} from "vue";
import App from './App.vue';
createApp(<App/>).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
const editor = useFlowEditor({
graphConfig: {
grid: false,
}
});
自定义(继承)组件
editor.components.registry(createFlowNode())
;- createFlowNode如下示例所示:
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文档;
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组件;
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: (
<BaseVueComponent config={config}>
<DemoRenderNodeContent/>
</BaseVueComponent>
),
ports: {
...createFlowConnectPorts(config),
}
}, true);
}
};
}
- DemoRenderNodeContent.tsx
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<iFormData> | null = null;
const open = () => {
innerState.editData = JSON.parse(JSON.stringify(state.formData));
dfd = defer<iFormData>();
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 : <>
<input type="text" value={innerState.editData.taskName} onInput={(e: any) => innerState.editData!.taskName = e.target.value}/>
</>;
};
const renderButton = (externals: any) => {
return !innerState.editData ? <>
<button onClick={open}>编辑</button>
{externals}
</> : <>
<button onClick={cancel}>取消</button>
<button onClick={confirm}>确认</button>
</>;
};
return { innerState, open, cancel, confirm, renderEdit, renderButton };
})();
return () => (
<div class="demo-render-node-content">
<div class="demo-render-node-content-form">
{edit.renderEdit(<>
<div class="demo-render-node-content-form-browse">
<div class="demo-render-node-content-form-icon">
<FlowIcon icon="node"/>
</div>
<div>{state.formData.taskName}</div>
<div>{(() => {
const rate = Math.min(10, state.formData.taskName.length);
return "★★★★★★★★★★☆☆☆☆☆☆☆☆☆☆".slice(10 - rate, 10 - rate + 10);
})()}</div>
</div>
</>)}
</div>
<div class="demo-render-node-content-options">
{edit.renderButton(<>
<button onClick={() => alert('accept')}>接受</button>
<button onClick={() => alert('refuse')}>拒绝</button>
<button onClick={() => alert('transfer')}>转交</button>
</>)}
</div>
</div>
);
});
Hooks钩子(拦截器)
- editor有很多内置的钩子函数,用于监听(拦截)各种行为,比如监听节点的点击事件:
editor.hooks.onClickNode.use(({ node }) => {
console.log('click node', node);
});
- 钩子函数可以阻止,在添加的拦截函数中抛出异常或者返回
Promise.reject()
就可以阻止行为。比如添加节点的时候限制只能有一个开始(结束)节点的控制行为:
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钩子
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<any>) => void>(), // 单击节点
onDblclickNode: createHooks<(e: NodeView.PositionEventArgs<any>) => void>(), // 双击节点
onNodeContextmenu: createHooks<(e: NodeView.PositionEventArgs<any>) => 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<any>) => void>(), // 单击连接线
onDblclickEdge: createHooks<(e: EdgeView.PositionEventArgs<any>) => void>(), // 双击连接线
onEdgeContextmenu: createHooks<(e: EdgeView.PositionEventArgs<any>) => void>(), // 右击边触发动作
onVertexContextmenu: createHooks<(e: EdgeView.PositionEventArgs<any>) => 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;
}
自定义右击菜单
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: () => <i className="iconfont icon-pay_zhifubao" style={{ padding: '0 4px' }}/>,
handler: () => alert('自定义node选项2')
});
return;
}
});
自定义操作栏按钮
const editor = useFlowEditor({
onlyOneStartNode: true,
operators: [
{
label: '选项一',
icon: () => (<i className="iconfont icon-pay_wechat" style={{ height: '27px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}/>),
handler: () => console.log('option 1...')
}
]
});
可用函数methods
useFlowEditor
创建的对象有许多可用函数,使用示例如下所示:
// 获取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
的源码如下所示
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
缩小画布