2024-03-05 17:10:53 +08:00
|
|
|
<template>
|
|
|
|
|
<div class="my-process-designer">
|
|
|
|
|
<div class="my-process-designer__header">
|
|
|
|
|
<slot name="control-header"></slot>
|
|
|
|
|
<template v-if="!$slots['control-header']">
|
|
|
|
|
<div slot="content">
|
2024-03-11 16:55:44 +08:00
|
|
|
<!-- <a-button :type="headerButtonType" :icon="h(SaveOutlined)" @click="onSave" class="ml-2">保存流程 </a-button> -->
|
|
|
|
|
<!-- <a-button :type="headerButtonType" @click="previewProcessXML" class="ml-2">预览XML</a-button> -->
|
|
|
|
|
<!-- <a-button :type="headerButtonType" @click="previewProcessJson" class="ml-2">预览JSON</a-button> -->
|
2024-03-05 17:10:53 +08:00
|
|
|
<a-space>
|
|
|
|
|
<a-tooltip placement="bottom" class="ml-2" title="缩小视图">
|
|
|
|
|
<a-button :disabled="defaultZoom <= 0.3" :icon="h(ZoomOutOutlined)" @click="processZoomOut()"></a-button>
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
<a-button>{{ Math.floor(process.defaultZoom * 10 * 10) + "%" }}</a-button>
|
|
|
|
|
<a-tooltip placement="bottom" title="放大视图">
|
|
|
|
|
<a-button :disabled="defaultZoom >= 3.9" :icon="h(ZoomInOutlined)" @click="processZoomIn()"></a-button>
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
<a-tooltip placement="bottom" title="撤销" class="ml-2">
|
|
|
|
|
<a-button :icon="h(RotateLeftOutlined)" @click="processUndo()"></a-button>
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
<a-tooltip placement="bottom" title="恢复">
|
|
|
|
|
<a-button :icon="h(RotateRightOutlined)" @click="processRedo()"></a-button>
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
<a-tooltip placement="bottom" title="重新绘制">
|
|
|
|
|
<a-button :icon="h(ClearOutlined)" @click="processRestart()"></a-button>
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
</a-space>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</template>
|
|
|
|
|
<div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="my-process-designer__container">
|
|
|
|
|
<div class="my-process-designer__canvas" ref="bpmn-canvas" id="bpmn-canvas"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<a-modal v-model:open="process.previewModelVisible" width="60%" title="预览">
|
|
|
|
|
<highlightjs :language="process.previewType" :code="process.previewResult" style="height: 60vh" />
|
|
|
|
|
</a-modal>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
2024-03-07 14:51:01 +08:00
|
|
|
import { h,provide, reactive, onMounted, defineProps, computed, defineEmits } from 'vue';
|
2024-03-05 17:10:53 +08:00
|
|
|
import { SaveOutlined, ZoomOutOutlined, ZoomInOutlined, RotateLeftOutlined, RotateRightOutlined, ClearOutlined } from '@ant-design/icons-vue';
|
|
|
|
|
// 生产环境时优化
|
|
|
|
|
import BpmnModeler from "bpmn-js/lib/Modeler";
|
|
|
|
|
import DefaultEmptyXML from "./plugins/defaultEmpty.ts";
|
|
|
|
|
// 翻译方法(汉化)
|
|
|
|
|
import customTranslate from "./plugins/translate/customTranslate.ts";
|
|
|
|
|
|
|
|
|
|
import MoveModule from 'diagram-js/lib/features/move'
|
|
|
|
|
import ModelingModule from 'bpmn-js/lib/features/modeling'
|
|
|
|
|
import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas'
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits(['init-finished', 'event', 'commandStack-changed', 'input', 'change', 'canvas-viewbox-changed', 'destroy', 'save', 'element-click', 'connection-added', 'connection-removed', 'connection-changed']);
|
|
|
|
|
|
|
|
|
|
// 引入json转换与高亮
|
|
|
|
|
import convert from "xml-js";
|
|
|
|
|
const process = reactive({
|
|
|
|
|
defaultZoom: 1,
|
|
|
|
|
previewModelVisible: false,
|
|
|
|
|
simulationStatus: false,
|
|
|
|
|
previewResult: "",
|
|
|
|
|
previewType: "xml",
|
|
|
|
|
recoverable: false,
|
|
|
|
|
revocable: false,
|
|
|
|
|
bpmnModeler: null
|
|
|
|
|
})
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
value: String, // xml 字符串
|
|
|
|
|
processId: String,
|
|
|
|
|
processName: String,
|
|
|
|
|
translations: Object, // 自定义的翻译文件
|
|
|
|
|
additionalModel: [Object, Array], // 自定义model
|
|
|
|
|
moddleExtension: Object, // 自定义moddle
|
|
|
|
|
onlyCustomizeAddi: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
default: false
|
|
|
|
|
},
|
|
|
|
|
onlyCustomizeModdle: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
default: false
|
|
|
|
|
},
|
|
|
|
|
simulation: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
default: true
|
|
|
|
|
},
|
|
|
|
|
keyboard: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
default: true
|
|
|
|
|
},
|
|
|
|
|
prefix: {
|
|
|
|
|
type: String,
|
|
|
|
|
default: "flowable"
|
|
|
|
|
},
|
|
|
|
|
events: {
|
|
|
|
|
type: Array,
|
|
|
|
|
default: () => ["element.click"]
|
|
|
|
|
},
|
|
|
|
|
headerButtonSize: {
|
|
|
|
|
type: String,
|
|
|
|
|
default: "small",
|
|
|
|
|
validator: value => ["default", "medium", "small", "mini"].indexOf(value) !== -1
|
|
|
|
|
},
|
|
|
|
|
headerButtonType: {
|
|
|
|
|
type: String,
|
|
|
|
|
default: "primary",
|
|
|
|
|
validator: value => ["default", "primary", "success", "warning", "danger", "info"].indexOf(value) !== -1
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
initBpmnModeler();
|
|
|
|
|
createNewDiagram(props.value);
|
|
|
|
|
})
|
|
|
|
|
function onSave() {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
if (process.bpmnModeler == null) {
|
|
|
|
|
reject();
|
|
|
|
|
}
|
|
|
|
|
process.bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
|
|
|
|
|
emit('save', xml);
|
|
|
|
|
resolve(xml);
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
function initBpmnModeler() {
|
|
|
|
|
if (process.bpmnModeler) return;
|
|
|
|
|
const containerEl = document.querySelector('#bpmn-canvas');
|
|
|
|
|
process.bpmnModeler = new BpmnModeler({
|
|
|
|
|
container: '#bpmn-canvas',
|
|
|
|
|
additionalModules: [
|
|
|
|
|
{
|
|
|
|
|
translate: ['value', customTranslate]
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
emit("init-finished", process.bpmnModeler);
|
|
|
|
|
initModelListeners();
|
|
|
|
|
}
|
|
|
|
|
function initModelListeners() {
|
|
|
|
|
const EventBus = process.bpmnModeler.get("eventBus");
|
|
|
|
|
const that = this;
|
|
|
|
|
// 注册需要的监听事件, 将. 替换为 - , 避免解析异常
|
|
|
|
|
props.events.forEach(event => {
|
|
|
|
|
EventBus.on(event, function (eventObj) {
|
2024-03-07 14:51:01 +08:00
|
|
|
provide('wfdesign',eventObj)
|
2024-03-05 17:10:53 +08:00
|
|
|
let eventName = event.replace(/\./g, "-");
|
|
|
|
|
let element = eventObj ? eventObj.element : null;
|
|
|
|
|
emit(eventName, element, eventObj);
|
|
|
|
|
emit('event', eventName, element, eventObj);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
// 监听图形改变返回xml
|
|
|
|
|
EventBus.on("commandStack.changed", async event => {
|
|
|
|
|
try {
|
|
|
|
|
process.recoverable = process.bpmnModeler.get("commandStack").canRedo();
|
|
|
|
|
process.revocable = process.bpmnModeler.get("commandStack").canUndo();
|
|
|
|
|
let { xml } = await process.bpmnModeler.saveXML({ format: true });
|
|
|
|
|
emit("commandStack-changed", event);
|
|
|
|
|
emit("input", xml);
|
|
|
|
|
emit("change", xml);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(`[Process Designer Warn]: ${e.message || e}`);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
// 监听视图缩放变化
|
|
|
|
|
process.bpmnModeler.on("canvas.viewbox.changed", ({ viewbox }) => {
|
|
|
|
|
emit("canvas-viewbox-changed", { viewbox });
|
|
|
|
|
const { scale } = viewbox;
|
|
|
|
|
process.defaultZoom = Math.floor(scale * 100) / 100;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
/* 创建新的流程图 */
|
|
|
|
|
async function createNewDiagram(xml) {
|
|
|
|
|
// 将字符串转换成图显示出来
|
|
|
|
|
let newId = props.processId || `Process_${new Date().getTime()}`;
|
|
|
|
|
let newName = props.processName || `业务流程_${new Date().getTime()}`;
|
|
|
|
|
let xmlString = xml || DefaultEmptyXML(newId, newName, props.prefix);
|
|
|
|
|
try {
|
|
|
|
|
let { warnings } = await process.bpmnModeler.importXML(xmlString);
|
|
|
|
|
if (warnings && warnings.length) {
|
|
|
|
|
warnings.forEach(warn => console.warn(warn));
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(`[Process Designer Warn]: ${e.message || e}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 下载流程图到本地
|
|
|
|
|
async function downloadProcess(type, name) {
|
|
|
|
|
try {
|
|
|
|
|
const _this = this;
|
|
|
|
|
// 按需要类型创建文件并下载
|
|
|
|
|
if (type === "xml" || type === "bpmn") {
|
|
|
|
|
const { err, xml } = await process.bpmnModeler.saveXML();
|
|
|
|
|
// 读取异常时抛出异常
|
|
|
|
|
if (err) {
|
|
|
|
|
console.error(`[Process Designer Warn ]: ${err.message || err}`);
|
|
|
|
|
}
|
|
|
|
|
let { href, filename } = setEncoded(type.toUpperCase(), name, xml);
|
|
|
|
|
downloadFunc(href, filename);
|
|
|
|
|
} else {
|
|
|
|
|
const { err, svg } = await process.bpmnModeler.saveSVG();
|
|
|
|
|
// 读取异常时抛出异常
|
|
|
|
|
if (err) {
|
|
|
|
|
return console.error(err);
|
|
|
|
|
}
|
|
|
|
|
let { href, filename } = setEncoded("SVG", name, svg);
|
|
|
|
|
downloadFunc(href, filename);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(`[Process Designer Warn ]: ${e.message || e}`);
|
|
|
|
|
}
|
|
|
|
|
// 文件下载方法
|
|
|
|
|
function downloadFunc(href, filename) {
|
|
|
|
|
if (href && filename) {
|
|
|
|
|
let a = document.createElement("a");
|
|
|
|
|
a.download = filename; //指定下载的文件名
|
|
|
|
|
a.href = href; // URL对象
|
|
|
|
|
a.click(); // 模拟点击
|
|
|
|
|
URL.revokeObjectURL(a.href); // 释放URL 对象
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 根据所需类型进行转码并返回下载地址
|
|
|
|
|
function setEncoded(type, filename = "diagram", data) {
|
|
|
|
|
const encodedData = encodeURIComponent(data);
|
|
|
|
|
return {
|
|
|
|
|
filename: `${filename}.${type}`,
|
|
|
|
|
href: `data:application/${type === "svg" ? "text/xml" : "bpmn20-xml"};charset=UTF-8,${encodedData}`,
|
|
|
|
|
data: data
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 加载本地文件
|
|
|
|
|
function importLocalFile() {
|
|
|
|
|
const that = this;
|
|
|
|
|
const file = this.$refs.refFile.files[0];
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
reader.readAsText(file);
|
|
|
|
|
reader.onload = function () {
|
|
|
|
|
let xmlStr = this.result;
|
|
|
|
|
createNewDiagram(xmlStr);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
/* ------------------------------------------------ refs methods ------------------------------------------------------ */
|
|
|
|
|
function downloadProcessAsXml() {
|
|
|
|
|
downloadProcess("xml");
|
|
|
|
|
}
|
|
|
|
|
function downloadProcessAsBpmn() {
|
|
|
|
|
downloadProcess("bpmn");
|
|
|
|
|
}
|
|
|
|
|
function downloadProcessAsSvg() {
|
|
|
|
|
downloadProcess("svg");
|
|
|
|
|
}
|
|
|
|
|
function processSimulation() {
|
|
|
|
|
process.simulationStatus = !process.simulationStatus;
|
|
|
|
|
props.simulation && process.bpmnModeler.get("toggleMode").toggleMode();
|
|
|
|
|
}
|
|
|
|
|
function processRedo() {
|
|
|
|
|
process.bpmnModeler.get("commandStack").redo();
|
|
|
|
|
}
|
|
|
|
|
function processUndo() {
|
|
|
|
|
process.bpmnModeler.get("commandStack").undo();
|
|
|
|
|
}
|
|
|
|
|
function processZoomIn(zoomStep = 0.1) {
|
|
|
|
|
let newZoom = Math.floor(process.defaultZoom * 100 + zoomStep * 100) / 100;
|
|
|
|
|
if (newZoom > 4) {
|
|
|
|
|
throw new Error("[Process Designer Warn ]: The zoom ratio cannot be greater than 4");
|
|
|
|
|
}
|
|
|
|
|
process.defaultZoom = newZoom;
|
|
|
|
|
process.bpmnModeler.get("canvas").zoom(process.defaultZoom);
|
|
|
|
|
}
|
|
|
|
|
function processZoomOut(zoomStep = 0.1) {
|
|
|
|
|
let newZoom = Math.floor(process.defaultZoom * 100 - zoomStep * 100) / 100;
|
|
|
|
|
if (newZoom < 0.2) {
|
|
|
|
|
throw new Error("[Process Designer Warn ]: The zoom ratio cannot be less than 0.2");
|
|
|
|
|
}
|
|
|
|
|
process.defaultZoom = newZoom;
|
|
|
|
|
process.bpmnModeler.get("canvas").zoom(process.defaultZoom);
|
|
|
|
|
}
|
|
|
|
|
function processZoomTo(newZoom = 1) {
|
|
|
|
|
if (newZoom < 0.2) {
|
|
|
|
|
throw new Error("[Process Designer Warn ]: The zoom ratio cannot be less than 0.2");
|
|
|
|
|
}
|
|
|
|
|
if (newZoom > 4) {
|
|
|
|
|
throw new Error("[Process Designer Warn ]: The zoom ratio cannot be greater than 4");
|
|
|
|
|
}
|
|
|
|
|
process.defaultZoom = newZoom;
|
|
|
|
|
process.bpmnModeler.get("canvas").zoom(newZoom);
|
|
|
|
|
}
|
|
|
|
|
function processReZoom() {
|
|
|
|
|
process.defaultZoom = 1;
|
|
|
|
|
process.bpmnModeler.get("canvas").zoom("fit-viewport", "auto");
|
|
|
|
|
}
|
|
|
|
|
function processRestart() {
|
|
|
|
|
process.recoverable = false;
|
|
|
|
|
process.revocable = false;
|
|
|
|
|
createNewDiagram(null).then(() => process.bpmnModeler.get("canvas").zoom(1, "auto"));
|
|
|
|
|
}
|
|
|
|
|
function elementsAlign(align) {
|
|
|
|
|
const Align = process.bpmnModeler.get("alignElements");
|
|
|
|
|
const Selection = process.bpmnModeler.get("selection");
|
|
|
|
|
const SelectedElements = Selection.get();
|
|
|
|
|
if (!SelectedElements || SelectedElements.length <= 1) {
|
|
|
|
|
this.$message.warning("请按住 Ctrl 键选择多个元素对齐");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.$confirm("自动对齐可能造成图形变形,是否继续?", "警告", {
|
|
|
|
|
confirmButtonText: "确定",
|
|
|
|
|
cancelButtonText: "取消",
|
|
|
|
|
type: "warning"
|
|
|
|
|
}).then(() => Align.trigger(SelectedElements, align));
|
|
|
|
|
}
|
|
|
|
|
/*----------------------------- 方法结束 ---------------------------------*/
|
|
|
|
|
function previewProcessXML() {
|
|
|
|
|
process.bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
|
|
|
|
|
process.previewResult = xml;
|
|
|
|
|
process.previewType = "xml";
|
|
|
|
|
process.previewModelVisible = true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
function previewProcessJson() {
|
|
|
|
|
process.bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
|
|
|
|
|
console.log(convert.xml2json(xml, { spaces: 2 }))
|
|
|
|
|
process.previewResult = convert.xml2json(xml, { spaces: 2 });
|
|
|
|
|
process.previewType = "json";
|
|
|
|
|
process.previewModelVisible = true;
|
|
|
|
|
});
|
|
|
|
|
}
|
2024-03-11 16:55:44 +08:00
|
|
|
</script>
|
|
|
|
|
<style scoped>
|
|
|
|
|
::v-deep .bjs-container a{
|
|
|
|
|
visibility: hidden;
|
|
|
|
|
}
|
|
|
|
|
</style>
|