rh-vue3-snakerflow-designer
Version:
情怀!N年前使用的工作流引擎,虽然原作者已经不再维护,但是还是有那么一波人还在使用。个人看来其核心功能尚好,麻雀虽少,五脏俱全,是用来学习工作流引擎的好项目。但其流程设计器所用的技术栈可能就跟不上时代了(2016年前的开源项目)。为了让大伙或自己用得更舒心,或者也为了让更多人认识或重新认识这个工作流引擎。本人就花些许时间,使用现阶段前端主流的技术栈重新开发此款流程设计器。如大家在使用的过程中如有啥问题,欢迎留言或进群交流!
412 lines (408 loc) • 13.1 kB
text/typescript
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, '&')
.replace(/"/g, '"')
.replace(/</g, '<')
.replace(/>/g, '>')
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
}