UNPKG

rh-vue3-snakerflow-designer

Version:

情怀!N年前使用的工作流引擎,虽然原作者已经不再维护,但是还是有那么一波人还在使用。个人看来其核心功能尚好,麻雀虽少,五脏俱全,是用来学习工作流引擎的好项目。但其流程设计器所用的技术栈可能就跟不上时代了(2016年前的开源项目)。为了让大伙或自己用得更舒心,或者也为了让更多人认识或重新认识这个工作流引擎。本人就花些许时间,使用现阶段前端主流的技术栈重新开发此款流程设计器。如大家在使用的过程中如有啥问题,欢迎留言或进群交流!

412 lines (408 loc) 13.1 kB
import { BaseEdgeModel, BaseNodeModel } from '@logicflow/core' import { ProcessModel } from '../types' /** * 节点样式处理方法 * @param {*}} _this * @param {*} style * @returns */ export const nodeStyleHandle = (_this: BaseNodeModel, style: { [x: string]: any; r?: number; fill?: string; stroke?: string; strokeWidth?: number; }) : { [x: string]: any; r?: number; fill?: string; stroke?: string; strokeWidth?: number; } => { if (_this.properties.state === 'active') { style.stroke = '#00ff00' } else if (_this.properties.state === 'history') { style.stroke = '#ff0000' } return style } /** * 边样式处理方法 * @param {*}} _this * @param {*} style * @returns */ export const edgeStyleHandle = (_this: BaseEdgeModel, style: { [x: string]: any; r?: number; fill?: string; stroke?: string; strokeWidth?: number; }):{ [x: string]: any; r?: number; fill?: string; stroke?: string; strokeWidth?: number; } => { if (_this.properties.state === 'active') { style.stroke = '#00ff00' } else if (_this.properties.state === 'history') { style.stroke = '#ff0000' } return style } /** * 解析xml成Dom对象 * @param {} xml * @returns */ export const parseXml2Dom = (xml:string): any => { let xmlDoc = null if (window.DOMParser) { const parser = new DOMParser() xmlDoc = parser.parseFromString(xml, 'text/xml') } else { // Internet Explorer // eslint-disable-next-line no-undef xmlDoc = new ActiveXObject('Microsoft.XMLDOM') xmlDoc.async = false xmlDoc.loadXML(xml) } return xmlDoc } // 节点标签 const NODE_NAMES = ['start', 'task', 'decision', 'end', 'custom', 'join', 'fork', 'subProcess'] // 流程节点属性 const PROCESS_ATTR_KEYS = ['name', 'displayName', 'instanceUrl', 'expireTime', 'instanceNoClass'] // 节点属性 const NODE_ATTR_KEYS = ['name', 'displayName', 'form', 'assignee', 'assignmentHandler', 'views', 'taskType', 'performType', 'preInterceptors', 'postInterceptors', 'reminderTime', 'reminderRepeat', 'expireTime', 'autoExecute', 'callback', 'expr', 'handleClass', 'clazz', 'methodName', 'args', 'layout', 'g'] // 变迁节点属性 const TRANSITION_ATTR_KEYS = ['name', 'displayName', 'to', 'expr', 'g'] /** * 将snaker的定义文件转成LogicFlow支持的数据格式 * @param {*} xml * @returns */ export const snakerXml2LogicFlowJson = (xml: string): any => { const graphData = { nodes: [] as BaseNodeModel[], edges: [] as BaseNodeModel[] } as ProcessModel const xmlDoc = parseXml2Dom(xml) const processDom = xmlDoc.getElementsByTagName('process') if (!processDom.length) { return graphData } let value = null // 解析process属性 PROCESS_ATTR_KEYS.forEach(key => { value = processDom[0].getAttribute(key) if (value) { graphData[key] = value } }) let nodeEles: any = null let node: any = null let lfNode: any = {} // 解析节点 NODE_NAMES.forEach(key => { nodeEles = processDom[0].getElementsByTagName(key) if (nodeEles.length) { for (let i = 0, len = nodeEles.length; i < len; i++) { node = nodeEles[i] lfNode = { type: 'snaker:' + key, properties: {} } // 处理节点 NODE_ATTR_KEYS.forEach(attrKey => { value = node?.getAttribute(attrKey) if (value) { if (attrKey === 'name') { lfNode.id = value } else if (attrKey === 'layout') { const attr = value.split(',') if (attr.length === 4) { lfNode.x = attr[0] lfNode.y = attr[1] lfNode.properties.width = attr[2] <= 0 ? 100 : attr[2] lfNode.properties.height = attr[3] <= 0 ? 50 : attr[3] } } else if (attrKey === 'displayName') { lfNode.text = value } else { lfNode.properties[attrKey] = value } } }) // 处理扩展属性 if (key === 'task') { const fieldEles = node.getElementsByTagName('field') if (fieldEles.length) { const fieldEle = fieldEles[0] const field = { } const attr = fieldEle.getElementsByTagName('attr') for (let iii = 0; iii < attr.length; iii++) { field[attr[iii].getAttribute('name')] = attr[iii].getAttribute('value') } lfNode.properties.field = field } } graphData.nodes.push(lfNode) // 处理边 let transitionEles = null let transitionEle = null let edge :any = {} if (key !== 'end') { transitionEles = node.getElementsByTagName('transition') for (let j = 0, lenn = transitionEles.length; j < lenn; j++) { transitionEle = transitionEles[j] edge = {} edge.id = transitionEle.getAttribute('name') edge.type = 'snaker:transition' edge.sourceNodeId = lfNode.id edge.targetNodeId = transitionEle.getAttribute('to') edge.text = { value: transitionEle.getAttribute('displayName') ? transitionEle.getAttribute('displayName') : '' } edge.properties = {} const expr = transitionEle.getAttribute('expr') if (expr) { edge.properties.expr = expr } const g = transitionEle.getAttribute('g') if (g) { const points = g.split(';') if (points.length >= 2) { edge.pointsList = [] points.forEach((item: any) => { const pointArr = item.split(',') edge.pointsList.push({ x: Number(pointArr[0]), y: Number(pointArr[1]) }) }) edge.startPoint = edge.pointsList[0] edge.endPoint = edge.pointsList[edge.pointsList.length - 1] } } graphData.edges.push(edge) } } } } }) return graphData } /** * 将LogicFlow的数据转成snaker的定义文件 * @param {*} data(...processInfo,nodes,edges) * @returns */ export const logicFlowJsonToSnakerXml = (data: ProcessModel): string => { let xml = '' // data的数据由流程定义文件信息+logicFlow数据构成 // 先构建成流程对象 const processObj = { name: data.name, // 流程定义名称 displayName: data.displayName, // 流程定义显示名称 instanceUrl: data.instanceUrl, // 实例启动Url expireTime: data.expireTime, // 期望完成时间 instanceNoClass: data.instanceNoClass // 实例编号生成类 } /** * 获取所有子流程 */ const getSubProcessNodes = () => { return data.nodes.filter((node) => { return node.type === 'snaker:subProcess' }) } /** * 获取开始节点 * @returns */ const getStartNode = () => { const subProcessNodes = getSubProcessNodes() return data.nodes.find((node) => { // 是开始节点,且非子流程中的开始节点 return node.type === 'snaker:start' && !subProcessNodes.some((item: any) => { return item.children.includes(node.id) }) }) } /** * 获取当前节点的所有下一个节点集合 * @param {*} id 当前节点名称 * @returns */ const getNextNodes = (id:string) => { return data.edges.filter(edge => { return edge.sourceNodeId === id }).map(edge => { return data.nodes.find((node) => { return node.id === edge.targetNodeId }) }) } /** * 获取节点所有输出边 * @param {*} id * @returns */ const getTransitions = (id:string) => { return data.edges.filter((edge) => { return edge.sourceNodeId === id }).map(edge => { return { name: edge.id, displayName: (edge.text instanceof String || edge.text === undefined) ? edge.text : edge.text.value, to: edge.targetNodeId, // 目地节点id expr: edge.properties.expr, // 表达式 g: edge.pointsList.map((point: { x: string; y: string; }) => { // 转换点集合 return point.x + ',' + point.y }).join(';') } }) } /** * 构建节点属性 * @param {}} node * @returns */ const buildNode = (node: any) => { let field if (node.properties.field && Object.keys(node.properties.field).length) { field = { name: 'ext', displayName: '扩展属性', attr: Object.keys(node.properties.field).map(key => { return { name: key, value: node.properties.field[key] } }) } } return { name: node.id, displayName: (node.text instanceof String || node.text === undefined) ? node.text : node.text.value, layout: node.x + ',' + node.y + ',' + (node.properties.nodeSize?.width || node.properties.width || (node.type === 'snaker:subProcess' ? '500' : '120')) + ',' + (node.properties.nodeSize?.height || node.properties.height || (node.type === 'snaker:subProcess' ? '300' : '80')), ...node.properties, transition: getTransitions(node.id), field } } /** * 特殊字符转义 * @param {*} text * @returns */ const textEncode = (text: string) => { text = text.replace(/&/g, '&amp;') .replace(/"/g, '&quot;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') return text } /** * 递归构建节点属性 * @param {} node */ const recursionBuildNode = (node: any) => { const nodeName = node.type.replace('snaker:', '') if (!processObj[nodeName + '_' + node.id]) { processObj[nodeName + '_' + node.id] = buildNode(node) const nextNodes = getNextNodes(node.id) nextNodes.forEach(nextNode => { recursionBuildNode(nextNode) }) } } const startNode = getStartNode() if (!startNode) { // 开始节点不存在,xml不合法 return '' } recursionBuildNode(startNode) xml = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n' xml += '<process ' Object.keys(processObj).forEach(key => { const value = processObj[key] if (PROCESS_ATTR_KEYS.includes(key) && value) { xml += ' ' + key + '=' + '"' + textEncode(value) + '"' } }) xml += '>\n' // 生成节点xml Object.keys(processObj).forEach(key => { const value = processObj[key] const nodeName = key.split('_')[0] if (NODE_NAMES.includes(nodeName)) { xml += '\t<' + nodeName // 构造属性 Object.keys(value).forEach(nodeAttrKey => { if (NODE_ATTR_KEYS.includes(nodeAttrKey) && value[nodeAttrKey]) { xml += ' ' + nodeAttrKey + '=' + '"' + textEncode(value[nodeAttrKey]) + '"' } }) xml += '>\n\t' // 构建transition if (value.transition) { value.transition.forEach((tran: any) => { xml += '\t<transition' // transition属性 Object.keys(tran).forEach(tranAttrKey => { if (TRANSITION_ATTR_KEYS.includes(tranAttrKey) && tran[tranAttrKey]) { xml += ' ' + tranAttrKey + '=' + '"' + textEncode(tran[tranAttrKey]) + '"' } }) xml += '>' xml += '</transition>\n' }) } if (value.field && nodeName === 'task') { xml += '\t<field name="' + value.field.name + '" displayName="' + value.field.displayName + '">\n' value.field.attr.forEach((attrItem: any) => { xml += '\t\t<attr name="' + attrItem.name + '" value="' + textEncode(attrItem.value) + '"></attr>\n' }) xml += '\t</field>\n' } xml += '\t</' + nodeName + '>\n' } }) xml += '</process>' // 构建子流程模型数据 const buildSubProcessList = () => { const subProcessNodes = getSubProcessNodes() const subProcessModel = subProcessNodes.map(n => { const subModel = { name: n.id, // 流程定义名称 displayName: n.properties.displayName, // 流程定义显示名称 instanceUrl: n.properties.instanceUrl, // 实例启动Url expireTime: n.properties.expireTime, // 期望完成时间 instanceNoClass: n.properties.instanceNoClass, // 实例编号生成类 nodes: data.nodes.filter((item: any) => n.children.includes(item.id)), edges: data.edges.filter((item: any) => n.children.includes(item.id)) } subModel.nodes.forEach((item: any) => { data.edges.forEach((edge: any) => { if (edge.sourceNodeId === item.id) { subModel.edges.push(edge) } }) }) return subModel }) return subProcessModel } const subProcessList = buildSubProcessList() subProcessList.forEach((processModel: ProcessModel) => { xml += logicFlowJsonToSnakerXml(processModel).replace('<?xml version="1.0" encoding="UTF-8" standalone="no"?>', '') }) return xml }