vue jsplumb工作流程图
搬砖大户 人气:0最近接到一个需求——给后台开发一个工作流程图,方便给领导看工作流程具体到哪一步。
先写了一个demo,大概样子如下:
官网文档Home | jsPlumb Toolkit Documentation
先安装插件
npm install jsplumb --save
安装panzoom,主要用于鼠标滚轮缩放流程图
npm install panzoom --save
在需要的页面引入插件
import panzoom from 'panzoom' import { jsPlumb } from 'jsplumb'
接下来先写布局
父组件
<template> <div class="workflow"> <div class="flow_region"> <div id="flowWrap" ref="flowWrap" class="flow-wrap" @drop="drop($event)" @dragover="allowDrop($event)"> <div id="flow"> <flowNode v-for="item in data.nodeList" :id="item.id" :key="item.id" :node="item" @setNodeName="setNodeName" @changeLineState="changeLineState" /> </div> </div> </div> </div> </template>
flowNode是子组件
<template> <div ref="node" class="node-item" :class="{ isStart: node.type === 'start', isEnd: node.type === 'end', 'common-circle-node':node.type === 'start' || node.type === 'end' || node.type === 'event', 'common-rectangle-node':node.type === 'common' || node.type === 'freedom' || node.type === 'child-flow', 'common-diamond-node':node.type === 'gateway', 'common-x-lane-node':node.type === 'x-lane', 'common-y-lane-node':node.type === 'y-lane' }" :style="{ top: node.y + 'px', left: node.x + 'px' }" @click="setNotActive" @mouseenter="showAnchor" @mouseleave="hideAnchor" > <div class="nodeName">{{ node.nodeName }}</div> </div> </template>
样式主要是子组件的,父组件样式随意就行
<style lang="less" scoped> @labelColor: #409eff; @nodeSize: 20px; @viewSize: 10px; .node-item { position: absolute; display: flex; height: 40px; width: 120px; justify-content: center; align-items: center; border: 1px solid #b7b6b6; border-radius: 4px; cursor: move; box-sizing: content-box; font-size: 12px; z-index: 9995; &:hover { z-index: 9998; .delete-btn{ display: block; } } .log-wrap{ width: 40px; height: 40px; border-right: 1px solid #b7b6b6; } .nodeName { flex-grow: 1; width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: center; } .node-anchor { display: flex; position: absolute; width: @nodeSize; height: @nodeSize; align-items: center; justify-content: center; border-radius: 10px; cursor: crosshair; z-index: 9999; background: -webkit-radial-gradient(sandybrown 10%, white 30%, #9a54ff 60%); } .anchor-top{ top: calc((@nodeSize / 2)*-1); left: 50%; margin-left: calc((@nodeSize/2)*-1); } .anchor-right{ top: 50%; right: calc((@nodeSize / 2)*-1); margin-top: calc((@nodeSize / 2)*-1); } .anchor-bottom{ bottom: calc((@nodeSize / 2)*-1); left: 50%; margin-left: calc((@nodeSize / 2)*-1); } .anchor-left{ top: 50%; left: calc((@nodeSize / 2)*-1); margin-top: calc((@nodeSize / 2)*-1); } } .active{ border: 1px dashed @labelColor; box-shadow: 0px 5px 9px 0px rgba(0,0,0,0.5); } .common-circle-node{ border-radius: 50%; height: 60px; width: 60px; } </style>
页面样式写完,接下来写插件配置
jsPlumb.ready() 是一个钩子函数,它会在 jsPlumb 准备完毕时执行。
连接线的建立是通过 jsPlumb.connect() 方法实现的。该方法接受一个对象作为配置项。其中包含了与上述概念一一对应的配置项,以及一些额外的样式。
source: 源对象,可以是对象的 id 属性、Element 对象或者 Endpoint 对象。
target: 目标对象,可以是对象的 id 属性、Element 对象或者 Endpoint 对象。
anchor: 是一个数组,数组中每一项定义一个锚点。
初始化方法
init() { this.jsPlumb.ready(() => { // 导入默认配置 this.jsPlumb.importDefaults(this.jsplumbSetting) // 完成连线前的校验 this.jsPlumb.bind('beforeDrop', evt => { const res = () => { } // 此处可以添加是否创建连接的校验, 返回 false 则不添加; return res }) this.loadEasyFlow() // 会使整个jsPlumb立即重绘。 this.jsPlumb.setSuspendDrawing(false, true) }) this.initPanZoom() }, // 加载流程图 loadEasyFlow() { // 初始化节点 for (let i = 0; i < this.data.nodeList.length; i++) { const node = this.data.nodeList[i] // 设置源点,可以拖出线连接其他节点 this.jsPlumb.makeSource(node.id, this.jsplumbSourceOptions) // // 设置目标点,其他源点拖出的线可以连接该节点 this.jsPlumb.makeTarget(node.id, this.jsplumbTargetOptions) // this.jsPlumb.draggable(node.id); this.draggableNode(node.id) } // 初始化连线 this.jsPlumb.unbind('connection') // 取消连接事件 console.log(this.data.lineList) for (let i = 0; i < this.data.lineList.length; i++) { const line = this.data.lineList[i] const conn = this.jsPlumb.connect( { source: line.sourceId, target: line.targetId, paintStyle: { stroke: line.cls.linkColor, strokeWidth: 2 // strokeWidth: line.cls.linkThickness } }, this.jsplumbConnectOptions ) conn.setLabel({ label: line.label, cssClass: `linkLabel ${line.id}` }) } },
this.data 是需要渲染的数据,放在文章末尾,具体数据按照接口实际返回的来写
this.jsplumbSourceOptions 是jsplumb配置信息,新建一个文件编写,具体如下:
export const jsplumbSetting = { grid: [10, 10], // 动态锚点、位置自适应 anchor: ['TopCenter', 'RightMiddle', 'BottomCenter', 'LeftMiddle'], Container: 'flow', // 连线的样式 StateMachine、Flowchart,有四种默认类型:Bezier(贝塞尔曲线),Straight(直线),Flowchart(流程图),State machine(状态机) Connector: ['Flowchart', { cornerRadius: 5, alwaysRespectStubs: true, stub: 5 }], // 鼠标不能拖动删除线 ConnectionsDetachable: false, // 删除线的时候节点不删除 DeleteEndpointsOnDetach: false, // 连线的端点 // Endpoint: ["Dot", {radius: 5}], Endpoint: [ 'Rectangle', { height: 10, width: 10 } ], // 线端点的样式 EndpointStyle: { fill: 'rgba(255,255,255,0)', outlineWidth: 1 }, LogEnabled: false, // 是否打开jsPlumb的内部日志记录 // 绘制线 PaintStyle: { stroke: '#409eff', strokeWidth: 2 }, HoverPaintStyle: { stroke: '#409eff' }, // 绘制箭头 Overlays: [ [ 'Arrow', { width: 8, length: 8, location: 1 } ] ], RenderMode: 'svg' } // jsplumb连接参数 export const jsplumbConnectOptions = { isSource: true, isTarget: true, // 动态锚点、提供了4个方向 Continuous、AutoDefault anchor: ['TopCenter', 'RightMiddle', 'BottomCenter', 'LeftMiddle'] } export const jsplumbSourceOptions = { filter: '.node-anchor', // 触发连线的区域 /* "span"表示标签,".className"表示类,"#id"表示元素id*/ filterExclude: false, anchor: ['TopCenter', 'RightMiddle', 'BottomCenter', 'LeftMiddle'], allowLoopback: false } export const jsplumbTargetOptions = { filter: '.node-anchor', /* "span"表示标签,".className"表示类,"#id"表示元素id*/ filterExclude: false, anchor: ['TopCenter', 'RightMiddle', 'BottomCenter', 'LeftMiddle'], allowLoopback: false }
在父组件引入配置文件和方法
import { jsplumbSetting, jsplumbConnectOptions, jsplumbSourceOptions, jsplumbTargetOptions } from './config/commonConfig'
接下来在上面说的初始化方法文件里面配置鼠标滚轮缩放插件的方法 this.initPanZoom():
// 鼠标滚动放大缩小 initPanZoom() { const mainContainer = this.jsPlumb.getContainer() const mainContainerWrap = mainContainer.parentNode const pan = panzoom(mainContainer, { smoothScroll: false, bounds: true, // autocenter: true, zoomDoubleClickSpeed: 1, minZoom: 0.5, maxZoom: 2, // 设置滚动缩放的组合键,默认不需要组合键 beforeWheel: (e) => { // console.log(e) // let shouldIgnore = !e.ctrlKey // return shouldIgnore }, beforeMouseDown: function(e) { // allow mouse-down panning only if altKey is down. Otherwise - ignore var shouldIgnore = e.ctrlKey return shouldIgnore } }) this.jsPlumb.mainContainerWrap = mainContainerWrap this.jsPlumb.pan = pan // 缩放时设置jsPlumb的缩放比率 pan.on('zoom', e => { const { scale } = e.getTransform() this.jsPlumb.setZoom(scale) }) pan.on('panend', (e) => { }) // 平移时设置鼠标样式 mainContainerWrap.style.cursor = 'grab' mainContainerWrap.addEventListener('mousedown', function wrapMousedown() { this.style.cursor = 'grabbing' mainContainerWrap.addEventListener('mouseout', function wrapMouseout() { this.style.cursor = 'grab' }) }) mainContainerWrap.addEventListener('mouseup', function wrapMouseup() { this.style.cursor = 'grab' }) },
大功告成,data的数据放在这里,测试使用:
{ "FlowJson": { "nodeList": [ { "type": "start", "nodeName": "已新建", "id": "start-HiXWf8wsAcrWXjAAXVWc6AQk00000001", "node_code": "已新建", "trigger_event": "", "branch_flow": "", "icon": "play-circle", "x": 175, "y": 60, "width": 50, "height": 50 }, { "type": "freedom", "nodeName": "待审批", "id": "freedom-YakFJzZ5VSp3Gec6ZULD2JDK00000004", "node_code": "待审批", "trigger_event": "", "branch_flow": "", "icon": "sync", "x": 330, "y": 160, "width": 50, "height": 120 }, { "type": "end", "nodeName": "已通过", "id": "end-JjRvtD5J2GIJKCn8MF7IYwxh00000999", "node_code": "已通过", "trigger_event": "", "branch_flow": "", "icon": "stop", "x": 330, "y": 360, "width": 50, "height": 50 }, { "type": "end", "nodeName": "审批拒绝", "id": "end-J1DMScH5YjSKyk0HeNkbt62F00010001", "node_code": "审批拒绝", "trigger_event": "", "branch_flow": "", "icon": "stop", "x": 500, "y": 350, "width": 50, "height": 50 } ], "linkList": [ { "type": "link", "id": "link-BpI6ZuX1bJywz5SEi3R5QaWoi7g3QiSr", "sourceId": "start-HiXWf8wsAcrWXjAAXVWc6AQk00000001", "targetId": "freedom-YakFJzZ5VSp3Gec6ZULD2JDK00000004", "label": "LINE000000", "role": [], "organize": [], "audit_role": [], "audit_organize": [], "audit_organize_same": "0", "audit_dealer_same": "0", "audit_dealers": [], "notice": "0", "plug": "", "pass_option": "pass", "row_par_json": "", "judge_fields": "", "auth_at": "", "auth_user": "", "auth_stat": "", "auth_mark": "", "cls": { "linkType": "Flowchart", "linkColor": "#008000", "linkThickness": 4 } }, { "type": "link", "id": "link-5xJWzGlkIpUCsjmpfgesJxAOMHwkPlno", "sourceId": "freedom-YakFJzZ5VSp3Gec6ZULD2JDK00000004", "targetId": "end-J1DMScH5YjSKyk0HeNkbt62F00010001", "label": "LINE000001", "role": [], "organize": [], "audit_role": [ "PROJECT_SUPPORT_PLAN_CODE" ], "audit_organize": [], "audit_organize_same": "0", "audit_dealer_same": "0", "audit_dealers": [], "notice": "0", "plug": "", "pass_option": "reject", "row_par_json": "", "judge_fields": "", "auth_at": "", "auth_user": "", "auth_stat": "", "auth_mark": "", "cls": { "linkType": "Flowchart", "linkColor": "#808080", "linkThickness": 1 } }, { "type": "link", "id": "link-g05V3usXa86wAtpcMkvGzybdBlpasMjU", "sourceId": "freedom-YakFJzZ5VSp3Gec6ZULD2JDK00000004", "targetId": "end-JjRvtD5J2GIJKCn8MF7IYwxh00000999", "label": "LINE000002", "role": [], "organize": [], "audit_role": [ "PROJECT_SUPPORT_PLAN_CODE" ], "audit_organize": [], "audit_organize_same": "0", "audit_dealer_same": "0", "audit_dealers": [], "notice": "0", "plug": "", "pass_option": "approve", "row_par_json": "", "judge_fields": "", "auth_at": "", "auth_user": "", "auth_stat": "", "auth_mark": "", "cls": { "linkType": "Flowchart", "linkColor": "#808080", "linkThickness": 1 } } ] } }
加载全部内容