Files
car_management/src/plugins/package/designer/ProcessDesigner.vue
2022-12-22 17:30:52 +08:00

657 lines
19 KiB
Vue

<template>
<div class="my-process-designer">
<div class="my-process-designer__header">
<slot name="control-header"></slot>
<template v-if="!$slots['control-header']">
<el-button-group key="file-control">
<el-button
:size="headerButtonSize"
:type="headerButtonType"
icon="edit"
@click="onSave"
>保存流程</el-button
>
<el-button
:size="headerButtonSize"
:type="headerButtonType"
icon="folder-opened"
@click="refFile.click()"
>打开文件</el-button
>
<el-tooltip effect="light">
<template #content>
<el-button
:size="headerButtonSize"
link
type="primary"
@click="downloadProcessAsXml()"
>下载为XML文件</el-button
>
<br />
<el-button
:size="headerButtonSize"
link
type="primary"
@click="downloadProcessAsSvg()"
>下载为SVG文件</el-button
>
<br />
<el-button
:size="headerButtonSize"
link
type="primary"
@click="downloadProcessAsBpmn()"
>下载为BPMN文件</el-button
>
</template>
<el-button
:size="headerButtonSize"
:type="headerButtonType"
icon="download"
>下载文件</el-button
>
</el-tooltip>
<el-tooltip effect="light">
<template #content>
<el-button
:size="headerButtonSize"
link
type="primary"
@click="previewProcessXML"
>预览XML</el-button
>
<br />
<el-button
:size="headerButtonSize"
link
type="primary"
@click="previewProcessJson"
>预览JSON</el-button
>
</template>
<el-button
:size="headerButtonSize"
:type="headerButtonType"
icon="view"
>预览</el-button
>
</el-tooltip>
<el-tooltip
v-if="simulation"
effect="light"
:content="simulationStatus ? '退出模拟' : '开启模拟'"
>
<el-button
:size="headerButtonSize"
:type="headerButtonType"
icon="cpu"
@click="processSimulation"
>
模拟
</el-button>
</el-tooltip>
</el-button-group>
<el-button-group key="align-control">
<el-tooltip effect="light" content="向左对齐">
<el-button
:size="headerButtonSize"
class="align align-left"
icon="histogram"
@click="elementsAlign('left')"
/>
</el-tooltip>
<el-tooltip effect="light" content="向右对齐">
<el-button
:size="headerButtonSize"
class="align align-right"
icon="histogram"
@click="elementsAlign('right')"
/>
</el-tooltip>
<el-tooltip effect="light" content="向上对齐">
<el-button
:size="headerButtonSize"
class="align align-top"
icon="histogram"
@click="elementsAlign('top')"
/>
</el-tooltip>
<el-tooltip effect="light" content="向下对齐">
<el-button
:size="headerButtonSize"
class="align align-bottom"
icon="histogram"
@click="elementsAlign('bottom')"
/>
</el-tooltip>
<el-tooltip effect="light" content="水平居中">
<el-button
:size="headerButtonSize"
class="align align-center"
icon="histogram"
@click="elementsAlign('center')"
/>
</el-tooltip>
<el-tooltip effect="light" content="垂直居中">
<el-button
:size="headerButtonSize"
class="align align-middle"
icon="histogram"
@click="elementsAlign('middle')"
/>
</el-tooltip>
</el-button-group>
<el-button-group key="scale-control">
<el-tooltip effect="light" content="缩小视图">
<el-button
:size="headerButtonSize"
:disabled="defaultZoom <= 0.3"
icon="zoom-out"
@click="processZoomOut()"
/>
</el-tooltip>
<el-button :size="headerButtonSize">{{
Math.floor(defaultZoom * 10 * 10) + "%"
}}</el-button>
<el-tooltip effect="light" content="放大视图">
<el-button
:size="headerButtonSize"
:disabled="defaultZoom >= 3.9"
icon="zoom-in"
@click="processZoomIn()"
/>
</el-tooltip>
<el-tooltip effect="light" content="重置视图并居中">
<el-button
:size="headerButtonSize"
icon="ScaleToOriginal"
@click="processReZoom()"
/>
</el-tooltip>
</el-button-group>
<el-button-group key="stack-control">
<el-tooltip effect="light" content="撤销">
<el-button
:size="headerButtonSize"
:disabled="!revocable"
icon="refresh-left"
@click="processUndo()"
/>
</el-tooltip>
<el-tooltip effect="light" content="恢复">
<el-button
:size="headerButtonSize"
:disabled="!recoverable"
icon="refresh-right"
@click="processRedo()"
/>
</el-tooltip>
<el-tooltip effect="light" content="重新绘制">
<el-button
:size="headerButtonSize"
icon="refresh"
@click="processRestart"
/>
</el-tooltip>
</el-button-group>
</template>
<!-- 用于打开本地文件-->
<input
type="file"
id="files"
ref="refFile"
style="display: none"
accept=".xml, .bpmn"
@change="importLocalFile"
/>
</div>
<div class="my-process-designer__container">
<div class="my-process-designer__canvas" ref="bpmnCanvasRef"></div>
</div>
<el-dialog
title="预览"
width="60%"
v-model="previewModelVisible"
append-to-body
destroy-on-close
>
<!-- <highlightjs
:language="previewType"
:code="previewResult"
style="height: 60vh"
/> -->
<!-- <div
v-html="
highlightjs.highlight(previewResult, { language: previewType }).value
"
></div> -->
</el-dialog>
</div>
</template>
<script setup name="BpmnProcessDesigner">
// 生产环境时优化
// const BpmnModeler = window.BpmnJS;
import BpmnModeler from "bpmn-js/lib/Modeler";
import DefaultEmptyXML from "./plugins/defaultEmpty";
// 翻译方法
import customTranslate from "./plugins/translate/customTranslate";
import translationsCN from "./plugins/translate/zh";
// 模拟流转流程
import tokenSimulation from "bpmn-js-token-simulation";
// 标签解析构建器
// import bpmnPropertiesProvider from "bpmn-js-properties-panel/lib/provider/bpmn";
// 标签解析 Moddle
import camundaModdleDescriptor from "./plugins/descriptor/camundaDescriptor.json";
import activitiModdleDescriptor from "./plugins/descriptor/activitiDescriptor.json";
import flowableModdleDescriptor from "./plugins/descriptor/flowableDescriptor.json";
// 标签解析 Extension
import camundaModdleExtension from "./plugins/extension-moddle/camunda";
import activitiModdleExtension from "./plugins/extension-moddle/activiti";
import flowableModdleExtension from "./plugins/extension-moddle/flowable";
// 引入json转换与高亮
import convert from "xml-js";
// import highlightjs from "@/plugins/package/highlight";
import { computed, onMounted, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
const bpmnCanvasRef = ref();
const refFile = ref();
const props = defineProps({
modelValue: String, // xml 字符串
// value: String,
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",
},
eventlist: {
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,
},
});
const {
// value,
modelValue,
processId,
processName,
translations,
additionalModel,
moddleExtension,
onlyCustomizeAddi,
onlyCustomizeModdle,
simulation,
keyboard,
prefix,
eventlist,
headerButtonSize,
headerButtonType,
} = toRefs(props);
const emit = defineEmits([
"destroy",
"save",
"commandStack-changed",
"input",
"change",
"init-finished",
"element-click",
"event",
"canvas-viewbox-changed",
"connection-added",
"connection-removed",
"connection-changed",
]);
const defaultZoom = ref(1);
const previewModelVisible = ref(false);
const simulationStatus = ref(false);
const previewResult = ref("");
const previewType = ref("xml");
const recoverable = ref(false);
const revocable = ref(false);
let bpmnModeler;
const additionalModules = computed(() => {
const Modules = [];
// 仅保留用户自定义扩展模块
if (onlyCustomizeAddi.value) {
if (
Object.prototype.toString.call(additionalModel.value) === "[object Array]"
) {
return additionalModel.value || [];
}
return [additionalModel.value];
}
// 插入用户自定义扩展模块
if (
Object.prototype.toString.call(additionalModel.value) === "[object Array]"
) {
Modules.push(...additionalModel.value);
} else {
additionalModel.value && Modules.push(additionalModel.value);
}
// 翻译模块
const TranslateModule = {
translate: ["value", customTranslate(translations.value || translationsCN)],
};
Modules.push(TranslateModule);
// 模拟流转模块
if (simulation.value) {
Modules.push(tokenSimulation);
}
// 根据需要的流程类型设置扩展元素构建模块
// // if (prefix.value === "bpmn") {
// // Modules.push(bpmnModdleExtension);
// // }
if (prefix.value === "camunda") {
Modules.push(camundaModdleExtension);
}
if (prefix.value === "flowable") {
Modules.push(flowableModdleExtension);
}
if (prefix.value === "activiti") {
Modules.push(activitiModdleExtension);
}
return Modules;
});
const moddleExtensions = computed(() => {
const Extensions = {};
// 仅使用用户自定义模块
if (onlyCustomizeModdle.value) {
return moddleExtension.value || null;
}
// 插入用户自定义模块
if (moddleExtension.value) {
for (let key in moddleExtension.value) {
Extensions[key] = moddleExtension.value[key];
}
}
// 根据需要的 "流程类型" 设置 对应的解析文件
if (prefix.value === "activiti") {
Extensions.activiti = activitiModdleDescriptor;
}
if (prefix.value === "flowable") {
Extensions.flowable = flowableModdleDescriptor;
}
if (prefix.value === "camunda") {
Extensions.camunda = camundaModdleDescriptor;
}
return Extensions;
});
onMounted(() => {
initBpmnModeler();
createNewDiagram(modelValue.value);
});
onBeforeUnmount(() => {
if (bpmnModeler) bpmnModeler.destroy();
emit("destroy", bpmnModeler);
bpmnModeler = null;
});
function onSave() {
return new Promise((resolve, reject) => {
if (bpmnModeler == null) {
reject();
}
bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
emit("save", xml);
resolve(xml);
});
});
}
window.BpmnModeler = BpmnModeler;
function initBpmnModeler() {
if (bpmnModeler) return;
bpmnModeler = new BpmnModeler({
container: bpmnCanvasRef.value,
keyboard: keyboard.value ? { bindTo: document } : null,
additionalModules: additionalModules.value,
moddleExtensions: moddleExtensions.value,
});
emit("init-finished", bpmnModeler);
initModelListeners();
}
function initModelListeners() {
const EventBus = bpmnModeler.get("eventBus");
// 注册需要的监听事件, 将. 替换为 - , 避免解析异常
eventlist.value.forEach((event) => {
EventBus.on(event, function (eventObj) {
let eventName = event.replace(/\./g, "-");
let element = eventObj ? eventObj.element : null;
emit(eventName, element, eventObj);
emit("event", eventName, element, eventObj);
});
});
//FIXME: 监听图形改变返回xml
EventBus.on("commandStack.changed", async (event) => {
try {
recoverable.value = bpmnModeler.get("commandStack").canRedo();
revocable.value = bpmnModeler.get("commandStack").canUndo();
let { xml } = await bpmnModeler.saveXML({ format: true });
emit("commandStack-changed", event);
emit("input", xml);
emit("change", xml);
} catch (e) {
console.error(`[Process Designer Warn]: ${e.message || e}`);
}
});
// 监听视图缩放变化
bpmnModeler.on("canvas.viewbox.changed", ({ viewbox }) => {
emit("canvas-viewbox-changed", { viewbox });
const { scale } = viewbox;
defaultZoom.value = Math.floor(scale * 100) / 100;
});
}
/* 创建新的流程图 */
async function createNewDiagram(xml) {
// 将字符串转换成图显示出来
let newId = processId.value || `Process_${new Date().getTime()}`;
let newName = processName.value || `业务流程_${new Date().getTime()}`;
let xmlString = xml || DefaultEmptyXML(newId, newName, prefix.value);
try {
let { warnings } = await 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 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 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 = refFile.value.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() {
simulationStatus.value = !simulationStatus.value;
simulation.value && bpmnModeler.get("toggleMode").toggleMode();
}
function processRedo() {
bpmnModeler.get("commandStack").redo();
}
function processUndo() {
bpmnModeler.get("commandStack").undo();
}
function processZoomIn(zoomStep = 0.1) {
let newZoom = Math.floor(defaultZoom.value * 100 + zoomStep * 100) / 100;
if (newZoom > 4) {
throw new Error(
"[Process Designer Warn ]: The zoom ratio cannot be greater than 4"
);
}
defaultZoom.value = newZoom;
bpmnModeler.get("canvas").zoom(defaultZoom.value);
}
function processZoomOut(zoomStep = 0.1) {
let newZoom = Math.floor(defaultZoom.value * 100 - zoomStep * 100) / 100;
if (newZoom < 0.2) {
throw new Error(
"[Process Designer Warn ]: The zoom ratio cannot be less than 0.2"
);
}
defaultZoom.value = newZoom;
bpmnModeler.get("canvas").zoom(defaultZoom.value);
}
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"
);
}
defaultZoom.value = newZoom;
bpmnModeler.get("canvas").zoom(newZoom);
}
function processReZoom() {
defaultZoom.value = 1;
bpmnModeler.get("canvas").zoom("fit-viewport", "auto");
}
function processRestart() {
recoverable.value = false;
revocable.value = false;
createNewDiagram(null).then(() => bpmnModeler.get("canvas").zoom(1, "auto"));
}
function elementsAlign(align) {
const Align = bpmnModeler.get("alignElements");
const Selection = bpmnModeler.get("selection");
const SelectedElements = Selection.get();
if (!SelectedElements || SelectedElements.length <= 1) {
ElMessage.warning("请按住 Ctrl 键选择多个元素对齐");
return;
}
ElMessageBox.confirm("自动对齐可能造成图形变形,是否继续?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(() => Align.trigger(SelectedElements, align));
}
/*---------------------------- 方法结束 ---------------------------------*/
function previewProcessXML() {
bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
previewResult.value = xml;
previewType.value = "xml";
previewModelVisible.value = true;
});
}
function previewProcessJson() {
bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
previewResult.value = convert.xml2json(xml, { spaces: 2 });
previewType.value = "json";
previewModelVisible.value = true;
});
}
</script>
<style>
@import "highlight.js/styles/atom-one-dark-reasonable.css";
</style>