diff --git a/apps/web-antd/package.json b/apps/web-antd/package.json index 8a0dc7bd..170bda60 100644 --- a/apps/web-antd/package.json +++ b/apps/web-antd/package.json @@ -27,6 +27,7 @@ }, "dependencies": { "@ant-design/icons-vue": "^7.0.1", + "@logicflow/core": "^2.0.13", "@tinymce/tinymce-vue": "^6.0.1", "@vben/access": "workspace:*", "@vben/common-ui": "workspace:*", diff --git a/apps/web-antd/src/api/workflow/instance/model.d.ts b/apps/web-antd/src/api/workflow/instance/model.d.ts index 8ef4b22e..0cbe92cd 100644 --- a/apps/web-antd/src/api/workflow/instance/model.d.ts +++ b/apps/web-antd/src/api/workflow/instance/model.d.ts @@ -38,4 +38,9 @@ export interface Flow { export interface FlowInfoResponse { image: string; list: Flow[]; + defChart: { + defJson: Record; + nodeJsonList: Record[]; + skipJsonList: Record[]; + }; } diff --git a/apps/web-antd/src/views/workflow/components/approval-panel.vue b/apps/web-antd/src/views/workflow/components/approval-panel.vue index 43bf1e3d..5b446fa2 100644 --- a/apps/web-antd/src/views/workflow/components/approval-panel.vue +++ b/apps/web-antd/src/views/workflow/components/approval-panel.vue @@ -41,6 +41,7 @@ import { import { renderDict } from '#/utils/render'; import { approvalModal, approvalRejectionModal, flowInterfereModal } from '.'; +import FlowPreview from '../components/flow-preview/index.vue'; import ApprovalDetails from './approval-details.vue'; import { approveWithReasonModal } from './helper'; import userSelectModal from './user-select-modal.vue'; @@ -442,9 +443,9 @@ async function handleCopy(text: string) { /> - diff --git a/apps/web-antd/src/views/workflow/components/flow-preview.vue b/apps/web-antd/src/views/workflow/components/flow-preview.vue new file mode 100644 index 00000000..e69de29b diff --git a/apps/web-antd/src/views/workflow/components/flow-preview/index.vue b/apps/web-antd/src/views/workflow/components/flow-preview/index.vue new file mode 100644 index 00000000..a69151a3 --- /dev/null +++ b/apps/web-antd/src/views/workflow/components/flow-preview/index.vue @@ -0,0 +1,99 @@ + + + diff --git a/apps/web-antd/src/views/workflow/components/flow-preview/model/between.ts b/apps/web-antd/src/views/workflow/components/flow-preview/model/between.ts new file mode 100644 index 00000000..6b877479 --- /dev/null +++ b/apps/web-antd/src/views/workflow/components/flow-preview/model/between.ts @@ -0,0 +1,21 @@ +import LogicFlow, { RectNode, RectNodeModel } from '@logicflow/core'; + +class BetweenModel extends RectNodeModel { + override getNodeStyle() { + return super.getNodeStyle(); + } + override initNodeData(data: LogicFlow.NodeConfig) { + super.initNodeData(data); + this.width = 100; + this.height = 80; + this.radius = 5; + } +} + +class BetweenView extends RectNode {} + +export default { + type: 'between', + model: BetweenModel, + view: BetweenView, +}; diff --git a/apps/web-antd/src/views/workflow/components/flow-preview/model/end.ts b/apps/web-antd/src/views/workflow/components/flow-preview/model/end.ts new file mode 100644 index 00000000..b29ef3ed --- /dev/null +++ b/apps/web-antd/src/views/workflow/components/flow-preview/model/end.ts @@ -0,0 +1,16 @@ +import LogicFlow, { CircleNode, CircleNodeModel } from '@logicflow/core'; + +class endModel extends CircleNodeModel { + override initNodeData(data: LogicFlow.NodeConfig) { + super.initNodeData(data); + this.r = 20; + } +} + +class endView extends CircleNode {} + +export default { + type: 'end', + model: endModel, + view: endView, +}; diff --git a/apps/web-antd/src/views/workflow/components/flow-preview/model/parallel.ts b/apps/web-antd/src/views/workflow/components/flow-preview/model/parallel.ts new file mode 100644 index 00000000..84623e75 --- /dev/null +++ b/apps/web-antd/src/views/workflow/components/flow-preview/model/parallel.ts @@ -0,0 +1,59 @@ +import type { GraphModel } from '@logicflow/core'; + +import LogicFlow, { h, PolygonNode, PolygonNodeModel } from '@logicflow/core'; + +class ParallelModel extends PolygonNodeModel { + static extendKey = 'ParallelModel'; + + constructor(data: LogicFlow.NodeConfig, graphModel: GraphModel) { + if (!data.text) { + data.text = ''; + } + if (data.text && typeof data.text === 'string') { + data.text = { + value: data.text, + x: data.x, + y: data.y + 40, + }; + } + super(data, graphModel); + this.points = [ + [25, 0], + [50, 25], + [25, 50], + [0, 25], + ]; + } +} + +class ParallelView extends PolygonNode { + static extendKey = 'ParallelNode'; + + override getShape() { + const { model } = this.props; + const { x, y, width, height, points } = model; + const style = model.getNodeStyle(); + return h( + 'g', + { + transform: `matrix(1 0 0 1 ${x - width / 2} ${y - height / 2})`, + }, + h('polygon', { + ...style, + x, + y, + points, + }), + h('path', { + d: 'm 23,10 0,12.5 -12.5,0 0,5 12.5,0 0,12.5 5,0 0,-12.5 12.5,0 0,-5 -12.5,0 0,-12.5 -5,0 z', + ...style, + }), + ); + } +} + +export default { + type: 'parallel', + view: ParallelView, + model: ParallelModel, +}; diff --git a/apps/web-antd/src/views/workflow/components/flow-preview/model/serial.ts b/apps/web-antd/src/views/workflow/components/flow-preview/model/serial.ts new file mode 100644 index 00000000..8e78041e --- /dev/null +++ b/apps/web-antd/src/views/workflow/components/flow-preview/model/serial.ts @@ -0,0 +1,59 @@ +import type { GraphModel } from '@logicflow/core'; + +import LogicFlow, { h, PolygonNode, PolygonNodeModel } from '@logicflow/core'; + +class SerialModel extends PolygonNodeModel { + static extendKey = 'SerialModel'; + + constructor(data: LogicFlow.NodeConfig, graphModel: GraphModel) { + if (!data.text) { + data.text = ''; + } + if (data.text && typeof data.text === 'string') { + data.text = { + value: data.text, + x: data.x, + y: data.y + 40, + }; + } + super(data, graphModel); + this.points = [ + [25, 0], + [50, 25], + [25, 50], + [0, 25], + ]; + } +} + +class SerialView extends PolygonNode { + static extendKey = 'SerialNode'; + + override getShape() { + const { model } = this.props; + const { x, y, width, height, points } = model; + const style = model.getNodeStyle(); + return h( + 'g', + { + transform: `matrix(1 0 0 1 ${x - width / 2} ${y - height / 2})`, + }, + h('polygon', { + ...style, + x, + y, + points, + }), + h('path', { + d: 'm 16,15 7.42857142857143,9.714285714285715 -7.42857142857143,9.714285714285715 3.428571428571429,0 5.714285714285715,-7.464228571428572 5.714285714285715,7.464228571428572 3.428571428571429,0 -7.42857142857143,-9.714285714285715 7.42857142857143,-9.714285714285715 -3.428571428571429,0 -5.714285714285715,7.464228571428572 -5.714285714285715,-7.464228571428572 -3.428571428571429,0 z', + ...style, + }), + ); + } +} + +export default { + type: 'serial', + view: SerialView, + model: SerialModel, +}; diff --git a/apps/web-antd/src/views/workflow/components/flow-preview/model/skip.ts b/apps/web-antd/src/views/workflow/components/flow-preview/model/skip.ts new file mode 100644 index 00000000..25cde40a --- /dev/null +++ b/apps/web-antd/src/views/workflow/components/flow-preview/model/skip.ts @@ -0,0 +1,32 @@ +import { PolylineEdge, PolylineEdgeModel } from '@logicflow/core'; + +class SkipModel extends PolylineEdgeModel { + /** + * 重写此方法,使保存数据是能带上锚点数据。 + */ + override getData() { + const data = super.getData(); + data.sourceAnchorId = this.sourceAnchorId; + data.targetAnchorId = this.targetAnchorId; + return data; + } + + override getEdgeStyle() { + const style = super.getEdgeStyle(); + const { properties } = this; + if (properties.isActived) { + style.strokeDasharray = '4 4'; + } + return style; + } + + override setAttributes() { + this.offset = 20; + } +} + +export default { + type: 'skip', + view: PolylineEdge, + model: SkipModel, +}; diff --git a/apps/web-antd/src/views/workflow/components/flow-preview/model/start.ts b/apps/web-antd/src/views/workflow/components/flow-preview/model/start.ts new file mode 100644 index 00000000..57f761cd --- /dev/null +++ b/apps/web-antd/src/views/workflow/components/flow-preview/model/start.ts @@ -0,0 +1,16 @@ +import LogicFlow, { CircleNode, CircleNodeModel } from '@logicflow/core'; + +class StartModel extends CircleNodeModel { + override initNodeData(data: LogicFlow.NodeConfig) { + super.initNodeData(data); + this.r = 20; + } +} + +class StartView extends CircleNode {} + +export default { + type: 'start', + model: StartModel, + view: StartView, +}; diff --git a/apps/web-antd/src/views/workflow/components/flow-preview/model/tool.ts b/apps/web-antd/src/views/workflow/components/flow-preview/model/tool.ts new file mode 100644 index 00000000..1de518e9 --- /dev/null +++ b/apps/web-antd/src/views/workflow/components/flow-preview/model/tool.ts @@ -0,0 +1,248 @@ +/* eslint-disable unicorn/no-array-reduce */ +const NODE_TYPE_MAP = { + 0: 'start', + 1: 'between', + 2: 'end', + 3: 'serial', + 4: 'parallel', +}; + +/** + * 将warm-flow的定义json数据转成LogicFlow支持的数据格式 + * @param {*} json + * @returns LogicFlow的数据 + */ +export const json2LogicFlowJson = (definition: any) => { + const graphData: any = { + nodes: [], + edges: [], + }; + // 解析definition属性 + graphData.flowCode = definition.flowCode; + graphData.flowName = definition.flowName; + graphData.version = definition.version; + graphData.fromCustom = definition.fromCustom; + graphData.fromPath = definition.fromPath; + // 解析节点 + const allSkips = definition.nodeList.reduce((acc: any, node: any) => { + if (node.skipList && Array.isArray(node.skipList)) { + acc.push(...node.skipList); + } + return acc; + }, []); + const allNodes = definition.nodeList; + // 解析节点 + if (allNodes.length > 0) { + for (let i = 0, len = allNodes.length; i < len; i++) { + const node = allNodes[i]; + const lfNode: any = { + text: {}, + properties: {}, + }; + // 处理节点 + lfNode.type = (NODE_TYPE_MAP as any)[node.nodeType]; + lfNode.id = node.nodeCode; + const coordinate = node.coordinate; + if (coordinate) { + const attr = coordinate.split('|'); + const nodeXy = attr[0].split(','); + lfNode.x = Number.parseInt(nodeXy[0]); + lfNode.y = Number.parseInt(nodeXy[1]); + if (attr.length === 2) { + const textXy = attr[1].split(','); + lfNode.text.x = Number.parseInt(textXy[0]); + lfNode.text.y = Number.parseInt(textXy[1]); + } + } + lfNode.text.value = node.nodeName; + lfNode.properties.nodeRatio = node.nodeRatio.toString(); + lfNode.properties.permissionFlag = node.permissionFlag; + lfNode.properties.anyNodeSkip = node.anyNodeSkip; + lfNode.properties.listenerType = node.listenerType; + lfNode.properties.listenerPath = node.listenerPath; + lfNode.properties.formCustom = node.formCustom; + lfNode.properties.formPath = node.formPath; + lfNode.properties.ext = {}; + if (node.ext && typeof node.ext === 'string') { + try { + node.ext = JSON.parse(node.ext); + node.ext.forEach((e: any) => { + lfNode.properties.ext[e.code] = String(e.value).includes(',') + ? e.value.split(',') + : String(e.value); + }); + } catch (error) { + console.error('Error parsing JSON:', error); + } + } + lfNode.properties.style = {}; + if (node.status === 2) { + lfNode.properties.style.fill = '#F0FFD9'; + lfNode.properties.style.stroke = '#9DFF00'; + } + if (node.status === 1) { + lfNode.properties.style.fill = '#FFF8DC'; + lfNode.properties.style.stroke = '#FFCD17'; + } + graphData.nodes.push(lfNode); + } + } + if (allSkips.length > 0) { + // 处理边 + let skipEle = null; + let edge: any = {}; + for (let j = 0, lenn = allSkips.length; j < lenn; j++) { + skipEle = allSkips[j]; + edge = { + text: {}, + properties: {}, + }; + edge.id = skipEle.id; + edge.type = 'skip'; + edge.sourceNodeId = skipEle.nowNodeCode; + edge.targetNodeId = skipEle.nextNodeCode; + edge.text = { value: skipEle.skipName }; + edge.properties.skipCondition = skipEle.skipCondition; + edge.properties.skipName = skipEle.skipName; + edge.properties.skipType = skipEle.skipType; + const expr = skipEle.expr; + if (expr) { + edge.properties.expr = skipEle.expr; + } + const coordinate = skipEle.coordinate; + if (coordinate) { + const coordinateXy = coordinate.split('|'); + edge.pointsList = []; + coordinateXy[0].split(';').forEach((item: any) => { + const pointArr = item.split(','); + edge.pointsList.push({ + x: Number.parseInt(pointArr[0]), + y: Number.parseInt(pointArr[1]), + }); + }); + edge.startPoint = edge.pointsList[0]; + edge.endPoint = edge.pointsList[edge.pointsList.length - 1]; + if (coordinateXy.length > 1) { + const textXy = coordinateXy[1].split(','); + edge.text.x = Number.parseInt(textXy[0]); + edge.text.y = Number.parseInt(textXy[1]); + } + } + graphData.edges.push(edge); + } + } + console.log(graphData); + return graphData; +}; + +/** + * 将LogicFlow的数据转成warm-flow的json定义文件 + * @param {*} data(...definitionInfo,nodes,edges) + * @returns + */ +export const logicFlowJsonToWarmFlow = (data: any) => { + // 先构建成流程对象 + const definition: any = { + nodeList: [], + }; + + /** + * 根据节点的类型值,获取key + * @param {*} mapValue 节点类型映射 + * @returns + */ + const getNodeTypeValue = (mapValue: any) => { + for (const key in NODE_TYPE_MAP) { + if ((NODE_TYPE_MAP as any)[key] === mapValue) { + return key; + } + } + }; + /** + * 根据节点的编码,获取节点的类型 + * @param {*} nodeCode 当前节点名称 + * @returns + */ + const getNodeType = (nodeCode: any) => { + for (const node of definition.nodeList) { + if (nodeCode === node.nodeCode) { + return node.nodeType; + } + } + }; + /** + * 拼接skip坐标 + * @param {*} edge logicFlow的edge + * @returns + */ + const getCoordinate = (edge: any) => { + let coordinate = ''; + for (let i = 0; i < edge.pointsList.length; i++) { + coordinate = `${ + coordinate + Number.parseInt(edge.pointsList[i].x) + },${Number.parseInt(edge.pointsList[i].y)}`; + if (i !== edge.pointsList.length - 1) { + coordinate = `${coordinate};`; + } + } + if (edge.text) { + coordinate = `${coordinate}|${Number.parseInt(edge.text.x)},${Number.parseInt(edge.text.y)}`; + } + return coordinate; + }; + // 流程定义 + definition.id = data.id; + definition.flowCode = data.flowCode; + definition.flowName = data.flowName; + definition.version = data.version; + definition.fromCustom = data.fromCustom; + definition.fromPath = data.fromPath; + // 流程节点 + data.nodes.forEach((anyNode: any) => { + const node: any = {}; + node.nodeType = getNodeTypeValue(anyNode.type); + node.nodeCode = anyNode.id; + if (anyNode.text) { + node.nodeName = anyNode.text.value; + } + node.permissionFlag = anyNode.properties.permissionFlag; + node.nodeRatio = anyNode.properties.nodeRatio; + node.anyNodeSkip = anyNode.properties.anyNodeSkip; + node.listenerType = anyNode.properties.listenerType; + node.listenerPath = anyNode.properties.listenerPath; + node.formCustom = anyNode.properties.formCustom; + node.formPath = anyNode.properties.formPath; + node.ext = []; + for (const key in anyNode.properties.ext) { + if (Object.prototype.hasOwnProperty.call(anyNode.properties.ext, key)) { + const e = anyNode.properties.ext[key]; + node.ext.push({ code: key, value: Array.isArray(e) ? e.join(',') : e }); + } + } + node.ext = JSON.stringify(node.ext); + node.coordinate = `${anyNode.x},${anyNode.y}`; + if (anyNode.text && anyNode.text.x && anyNode.text.y) { + node.coordinate = `${node.coordinate}|${anyNode.text.x},${anyNode.text.y}`; + } + node.handlerType = anyNode.properties.handlerType; + node.handlerPath = anyNode.properties.handlerPath; + node.version = definition.version; + node.skipList = []; + data.edges.forEach((anyEdge: any) => { + if (anyEdge.sourceNodeId === anyNode.id) { + const skip: any = {}; + skip.skipType = anyEdge.properties.skipType; + skip.skipCondition = anyEdge.properties.skipCondition; + skip.skipName = anyEdge?.text?.value || anyEdge.properties.skipName; + skip.nowNodeCode = anyEdge.sourceNodeId; + skip.nowNodeType = getNodeType(skip.nowNodeCode); + skip.nextNodeCode = anyEdge.targetNodeId; + skip.nextNodeType = getNodeType(skip.nextNodeCode); + skip.coordinate = getCoordinate(anyEdge); + node.skipList.push(skip); + } + }); + definition.nodeList.push(node); + }); + return JSON.stringify(definition); +};