diff --git a/package.json b/package.json index fc60498..106376b 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dependencies": { "@element-plus/icons-vue": "2.3.1", "@highlightjs/vue-plugin": "2.1.0", + "@logicflow/core": "^2.0.13", "@vueup/vue-quill": "1.2.0", "@vueuse/core": "13.1.0", "animate.css": "4.1.1", diff --git a/src/components/Process/approvalRecord.vue b/src/components/Process/approvalRecord.vue index 84fbfb1..e70d289 100644 --- a/src/components/Process/approvalRecord.vue +++ b/src/components/Process/approvalRecord.vue @@ -3,21 +3,7 @@ -
- - - -
+
@@ -76,7 +62,7 @@ import { flowImage } from '@/api/workflow/instance'; import { propTypes } from '@/utils/propTypes'; import { listByIds } from '@/api/system/oss'; - +import FlowChart from '@/components/Process/flowChart.vue'; const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { wf_task_status } = toRefs(proxy?.useDict('wf_task_status')); const props = defineProps({ @@ -88,6 +74,7 @@ const visible = ref(false); const historyList = ref>([]); const tabActiveName = ref('image'); const imgUrl = ref(''); +const defJson = ref(''); //初始化查询审批记录 const init = async (businessId: string | number) => { @@ -99,6 +86,7 @@ const init = async (businessId: string | number) => { if (resp.data) { historyList.value = resp.data.list; imgUrl.value = 'data:image/gif;base64,' + resp.data.image; + defJson.value = resp.data.defChart.defJson; if (historyList.value.length > 0) { historyList.value.forEach((item) => { if (item.ext) { @@ -124,109 +112,6 @@ const handleDownload = (ossId: string) => { proxy?.$download.oss(ossId); }; -const imageWrapperRef = ref(null); -const scale = ref(1); // 初始缩放比例 -const maxScale = 3; // 最大缩放比例 -const minScale = 0.5; // 最小缩放比例 - -let isDragging = false; -let startX = 0; -let startY = 0; -let currentTranslateX = 0; -let currentTranslateY = 0; - -const handleMouseWheel = (event: WheelEvent) => { - event.preventDefault(); - let newScale = scale.value - event.deltaY / 1000; - newScale = Math.max(minScale, Math.min(newScale, maxScale)); - if (newScale !== scale.value) { - scale.value = newScale; - resetDragPosition(); // 重置拖拽位置,使图片居中 - } -}; - -const handleMouseDown = (event: MouseEvent) => { - if (scale.value > 1) { - event.preventDefault(); // 阻止默认行为,防止拖拽 - isDragging = true; - startX = event.clientX; - startY = event.clientY; - } -}; - -const handleMouseMove = (event: MouseEvent) => { - if (!isDragging || !imageWrapperRef.value) return; - - const deltaX = event.clientX - startX; - const deltaY = event.clientY - startY; - startX = event.clientX; - startY = event.clientY; - - currentTranslateX += deltaX; - currentTranslateY += deltaY; - - // 边界检测,防止图片被拖出容器 - const bounds = getBounds(); - if (currentTranslateX > bounds.maxTranslateX) { - currentTranslateX = bounds.maxTranslateX; - } else if (currentTranslateX < bounds.minTranslateX) { - currentTranslateX = bounds.minTranslateX; - } - - if (currentTranslateY > bounds.maxTranslateY) { - currentTranslateY = bounds.maxTranslateY; - } else if (currentTranslateY < bounds.minTranslateY) { - currentTranslateY = bounds.minTranslateY; - } - - applyTransform(); -}; - -const handleMouseUp = () => { - isDragging = false; -}; - -const handleMouseLeave = () => { - isDragging = false; -}; - -const resetTransform = () => { - scale.value = 1; - currentTranslateX = 0; - currentTranslateY = 0; - applyTransform(); -}; - -const resetDragPosition = () => { - currentTranslateX = 0; - currentTranslateY = 0; - applyTransform(); -}; - -const applyTransform = () => { - if (imageWrapperRef.value) { - imageWrapperRef.value.style.transform = `translate(${currentTranslateX}px, ${currentTranslateY}px) scale(${scale.value})`; - } -}; - -const getBounds = () => { - if (!imageWrapperRef.value) return { minTranslateX: 0, maxTranslateX: 0, minTranslateY: 0, maxTranslateY: 0 }; - - const imgRect = imageWrapperRef.value.getBoundingClientRect(); - const containerRect = imageWrapperRef.value.parentElement?.getBoundingClientRect() ?? imgRect; - - const minTranslateX = (containerRect.width - imgRect.width * scale.value) / 2; - const maxTranslateX = -(containerRect.width - imgRect.width * scale.value) / 2; - const minTranslateY = (containerRect.height - imgRect.height * scale.value) / 2; - const maxTranslateY = -(containerRect.height - imgRect.height * scale.value) / 2; - - return { minTranslateX, maxTranslateX, minTranslateY, maxTranslateY }; -}; - -const transformStyle = computed(() => ({ - transition: isDragging ? 'none' : 'transform 0.2s ease' -})); - /** * 对外暴露子组件方法 */ @@ -235,46 +120,10 @@ defineExpose({ }); diff --git a/src/components/Process/flowChart.vue b/src/components/Process/flowChart.vue new file mode 100644 index 0000000..d5cd2c1 --- /dev/null +++ b/src/components/Process/flowChart.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/src/components/Process/flowChartImg.vue b/src/components/Process/flowChartImg.vue new file mode 100644 index 0000000..ba35565 --- /dev/null +++ b/src/components/Process/flowChartImg.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/src/components/Process/js/between.js b/src/components/Process/js/between.js new file mode 100644 index 0000000..296ccc9 --- /dev/null +++ b/src/components/Process/js/between.js @@ -0,0 +1,21 @@ +import { RectNode, RectNodeModel } from '@logicflow/core'; + +class BetweenModel extends RectNodeModel { + initNodeData(data) { + super.initNodeData(data); + this.width = 100; + this.height = 80; + this.radius = 5; + } + getNodeStyle() { + return super.getNodeStyle(); + } +} + +class BetweenView extends RectNode {} + +export default { + type: 'between', + model: BetweenModel, + view: BetweenView +}; diff --git a/src/components/Process/js/end.js b/src/components/Process/js/end.js new file mode 100644 index 0000000..aaa9aac --- /dev/null +++ b/src/components/Process/js/end.js @@ -0,0 +1,16 @@ +import { CircleNode, CircleNodeModel } from '@logicflow/core'; + +class endModel extends CircleNodeModel { + initNodeData(data) { + super.initNodeData(data); + this.r = 20; + } +} + +class endView extends CircleNode {} + +export default { + type: 'end', + model: endModel, + view: endView +}; diff --git a/src/components/Process/js/parallel.js b/src/components/Process/js/parallel.js new file mode 100644 index 0000000..052ef27 --- /dev/null +++ b/src/components/Process/js/parallel.js @@ -0,0 +1,55 @@ +import { h, PolygonNode, PolygonNodeModel } from '@logicflow/core'; + +class ParallelModel extends PolygonNodeModel { + static extendKey = 'ParallelModel'; + constructor(data, 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'; + 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/src/components/Process/js/serial.js b/src/components/Process/js/serial.js new file mode 100644 index 0000000..e73bfaf --- /dev/null +++ b/src/components/Process/js/serial.js @@ -0,0 +1,55 @@ +import { h, PolygonNode, PolygonNodeModel } from '@logicflow/core'; + +class SerialModel extends PolygonNodeModel { + static extendKey = 'SerialModel'; + constructor(data, 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'; + 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/src/components/Process/js/skip.js b/src/components/Process/js/skip.js new file mode 100644 index 0000000..02a6fb6 --- /dev/null +++ b/src/components/Process/js/skip.js @@ -0,0 +1,32 @@ +import { PolylineEdge, PolylineEdgeModel } from '@logicflow/core'; + +class SkipModel extends PolylineEdgeModel { + setAttributes() { + this.offset = 20; + } + + getEdgeStyle() { + const style = super.getEdgeStyle(); + const { properties } = this; + if (properties.isActived) { + style.strokeDasharray = '4 4'; + } + return style; + } + + /** + * 重写此方法,使保存数据是能带上锚点数据。 + */ + getData() { + const data = super.getData(); + data.sourceAnchorId = this.sourceAnchorId; + data.targetAnchorId = this.targetAnchorId; + return data; + } +} + +export default { + type: 'skip', + view: PolylineEdge, + model: SkipModel +}; diff --git a/src/components/Process/js/start.js b/src/components/Process/js/start.js new file mode 100644 index 0000000..5cf27d9 --- /dev/null +++ b/src/components/Process/js/start.js @@ -0,0 +1,16 @@ +import { CircleNode, CircleNodeModel } from '@logicflow/core'; + +class StartModel extends CircleNodeModel { + initNodeData(data) { + super.initNodeData(data); + this.r = 20; + } +} + +class StartView extends CircleNode {} + +export default { + type: 'start', + model: StartModel, + view: StartView +}; diff --git a/src/components/Process/js/tool.js b/src/components/Process/js/tool.js new file mode 100644 index 0000000..c4101dc --- /dev/null +++ b/src/components/Process/js/tool.js @@ -0,0 +1,237 @@ +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) => { + const graphData = { + 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, node) => { + if (node.skipList && Array.isArray(node.skipList)) { + acc.push(...node.skipList); + } + return acc; + }, []); + const allNodes = definition.nodeList; + // 解析节点 + if (allNodes.length) { + for (var i = 0, len = allNodes.length; i < len; i++) { + let node = allNodes[i]; + let lfNode = { + text: {}, + properties: {} + }; + // 处理节点 + lfNode.type = NODE_TYPE_MAP[node.nodeType]; + lfNode.id = node.nodeCode; + let coordinate = node.coordinate; + if (coordinate) { + const attr = coordinate.split('|'); + const nodeXy = attr[0].split(','); + lfNode.x = parseInt(nodeXy[0]); + lfNode.y = parseInt(nodeXy[1]); + if (attr.length === 2) { + const textXy = attr[1].split(','); + lfNode.text.x = parseInt(textXy[0]); + lfNode.text.y = 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) => { + 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) { + // 处理边 + let skipEle = null; + let edge = {}; + for (var 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) => { + const pointArr = item.split(','); + edge.pointsList.push({ + x: parseInt(pointArr[0]), + y: parseInt(pointArr[1]) + }); + }); + edge.startPoint = edge.pointsList[0]; + edge.endPoint = edge.pointsList[edge.pointsList.length - 1]; + if (coordinateXy.length > 1) { + let textXy = coordinateXy[1].split(','); + edge.text.x = parseInt(textXy[0]); + edge.text.y = 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) => { + // 先构建成流程对象 + const definition = { + nodeList: [] + }; + + /** + * 根据节点的类型值,获取key + * @param {*} mapValue 节点类型映射 + * @returns + */ + const getNodeTypeValue = (mapValue) => { + for (const key in NODE_TYPE_MAP) { + if (NODE_TYPE_MAP[key] === mapValue) { + return key; + } + } + }; + /** + * 根据节点的编码,获取节点的类型 + * @param {*} nodeCode 当前节点名称 + * @returns + */ + const getNodeType = (nodeCode) => { + for (const node of definition.nodeList) { + if (nodeCode === node.nodeCode) { + return node.nodeType; + } + } + }; + /** + * 拼接skip坐标 + * @param {*} edge logicFlow的edge + * @returns + */ + const getCoordinate = (edge) => { + let coordinate = ''; + for (let i = 0; i < edge.pointsList.length; i++) { + coordinate = coordinate + parseInt(edge.pointsList[i].x) + ',' + parseInt(edge.pointsList[i].y); + if (i !== edge.pointsList.length - 1) { + coordinate = coordinate + ';'; + } + } + if (edge.text) { + coordinate = coordinate + '|' + parseInt(edge.text.x) + ',' + 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) => { + let node = {}; + 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)) { + let 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) => { + if (anyEdge.sourceNodeId === anyNode.id) { + let skip = {}; + 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); +};