feat: bpmnProcessDesigner 迁移(纯复制)

This commit is contained in:
puhui999
2025-09-05 15:38:09 +08:00
parent 0bf1635090
commit 82fdcb5da5
68 changed files with 14547 additions and 0 deletions

View File

@@ -46,10 +46,17 @@
"@vueuse/core": "catalog:",
"@vueuse/integrations": "catalog:",
"ant-design-vue": "catalog:",
"bpmn-js": "^17.11.1",
"bpmn-js-properties-panel": "5.23.0",
"bpmn-js-token-simulation": "^0.36.3",
"camunda-bpmn-moddle": "^7.0.1",
"cropperjs": "catalog:",
"dayjs": "catalog:",
"diagram-js": "^12.8.1",
"fast-xml-parser": "^4.5.3",
"highlight.js": "catalog:",
"pinia": "catalog:",
"steady-xml": "^0.1.0",
"tinymce": "catalog:",
"vue": "catalog:",
"vue-dompurify-html": "catalog:",

View File

@@ -0,0 +1,700 @@
<template>
<div class="my-process-designer">
<div
class="my-process-designer__header"
style="z-index: 999; display: table-row-group"
>
<slot name="control-header"></slot>
<template v-if="!$slots['control-header']">
<ElButtonGroup key="file-control">
<XButton
preIcon="ep:folder-opened"
title="打开文件"
@click="refFile.click()"
/>
<el-tooltip effect="light" placement="bottom">
<template #content>
<div style="color: #409eff">
<!-- <el-button link @click="downloadProcessAsXml()">下载为XML文件</el-button> -->
<XTextButton
title="下载为XML文件"
@click="downloadProcessAsXml()"
/>
<br />
<!-- <el-button link @click="downloadProcessAsSvg()">下载为SVG文件</el-button> -->
<XTextButton
title="下载为SVG文件"
@click="downloadProcessAsSvg()"
/>
<br />
<!-- <el-button link @click="downloadProcessAsBpmn()">下载为BPMN文件</el-button> -->
<XTextButton
title="下载为BPMN文件"
@click="downloadProcessAsBpmn()"
/>
</div>
</template>
<XButton title="下载文件" preIcon="ep:download" />
</el-tooltip>
<el-tooltip effect="light">
<XButton preIcon="ep:view" title="浏览" />
<template #content>
<!-- <el-button link @click="previewProcessXML">预览XML</el-button> -->
<XTextButton title="预览XML" @click="previewProcessXML" />
<br />
<!-- <el-button link @click="previewProcessJson">预览JSON</el-button> -->
<XTextButton title="预览JSON" @click="previewProcessJson" />
</template>
</el-tooltip>
<el-tooltip
v-if="props.simulation"
effect="light"
:content="simulationStatus ? '退出模拟' : '开启模拟'"
>
<XButton preIcon="ep:cpu" title="模拟" @click="processSimulation" />
</el-tooltip>
</ElButtonGroup>
<ElButtonGroup key="align-control">
<el-tooltip effect="light" content="向左对齐">
<!-- <el-button
class="align align-left"
icon="el-icon-s-data"
@click="elementsAlign('left')"
/> -->
<XButton
preIcon="fa:align-left"
class="align align-bottom"
@click="elementsAlign('left')"
/>
</el-tooltip>
<el-tooltip effect="light" content="向右对齐">
<!-- <el-button
class="align align-right"
icon="el-icon-s-data"
@click="elementsAlign('right')"
/> -->
<XButton
preIcon="fa:align-left"
class="align align-top"
@click="elementsAlign('right')"
/>
</el-tooltip>
<el-tooltip effect="light" content="向上对齐">
<!-- <el-button
class="align align-top"
icon="el-icon-s-data"
@click="elementsAlign('top')"
/> -->
<XButton
preIcon="fa:align-left"
class="align align-left"
@click="elementsAlign('top')"
/>
</el-tooltip>
<el-tooltip effect="light" content="向下对齐">
<!-- <el-button
class="align align-bottom"
icon="el-icon-s-data"
@click="elementsAlign('bottom')"
/> -->
<XButton
preIcon="fa:align-left"
class="align align-right"
@click="elementsAlign('bottom')"
/>
</el-tooltip>
<el-tooltip effect="light" content="水平居中">
<!-- <el-button
class="align align-center"
icon="el-icon-s-data"
@click="elementsAlign('center')"
/> -->
<!-- class="align align-center" -->
<XButton
preIcon="fa:align-left"
class="align align-center"
@click="elementsAlign('center')"
/>
</el-tooltip>
<el-tooltip effect="light" content="垂直居中">
<!-- <el-button
class="align align-middle"
icon="el-icon-s-data"
@click="elementsAlign('middle')"
/> -->
<XButton
preIcon="fa:align-left"
class="align align-middle"
@click="elementsAlign('middle')"
/>
</el-tooltip>
</ElButtonGroup>
<ElButtonGroup key="scale-control">
<el-tooltip effect="light" content="缩小视图">
<!-- <el-button
:disabled="defaultZoom < 0.2"
icon="el-icon-zoom-out"
@click="processZoomOut()"
/> -->
<XButton
preIcon="ep:zoom-out"
@click="processZoomOut()"
:disabled="defaultZoom < 0.2"
/>
</el-tooltip>
<el-button>{{ Math.floor(defaultZoom * 10 * 10) + '%' }}</el-button>
<el-tooltip effect="light" content="放大视图">
<!-- <el-button
:disabled="defaultZoom > 4"
icon="el-icon-zoom-in"
@click="processZoomIn()"
/> -->
<XButton
preIcon="ep:zoom-in"
@click="processZoomIn()"
:disabled="defaultZoom > 4"
/>
</el-tooltip>
<el-tooltip effect="light" content="重置视图并居中">
<!-- <el-button icon="el-icon-c-scale-to-original" @click="processReZoom()" /> -->
<XButton preIcon="ep:scale-to-original" @click="processReZoom()" />
</el-tooltip>
</ElButtonGroup>
<ElButtonGroup key="stack-control">
<el-tooltip effect="light" content="撤销">
<!-- <el-button :disabled="!revocable" icon="el-icon-refresh-left" @click="processUndo()" /> -->
<XButton
preIcon="ep:refresh-left"
@click="processUndo()"
:disabled="!revocable"
/>
</el-tooltip>
<el-tooltip effect="light" content="恢复">
<!-- <el-button
:disabled="!recoverable"
icon="el-icon-refresh-right"
@click="processRedo()"
/> -->
<XButton
preIcon="ep:refresh-right"
@click="processRedo()"
:disabled="!recoverable"
/>
</el-tooltip>
<el-tooltip effect="light" content="重新绘制">
<!-- <el-button icon="el-icon-refresh" @click="processRestart" /> -->
<XButton preIcon="ep:refresh" @click="processRestart()" />
</el-tooltip>
</ElButtonGroup>
</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="bpmnCanvas"
id="bpmnCanvas"
style="width: 1680px; height: 800px"
></div>
<!-- <div id="js-properties-panel" class="panel"></div> -->
<!-- <div class="my-process-designer__canvas" ref="bpmn-canvas"></div> -->
</div>
<Dialog
title="预览"
v-model="previewModelVisible"
width="80%"
:scroll="true"
max-height="600px"
>
<div>
<pre><code v-dompurify-html="highlightedCode(previewResult)" class="hljs"></code></pre>
</div>
</Dialog>
</div>
</template>
<script lang="ts" setup>
// import 'bpmn-js/dist/assets/diagram-js.css' // 左边工具栏以及编辑节点的样式
// import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css'
// import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css'
// import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css'
// import 'bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css' // 右侧框样式
import { ElMessage, ElMessageBox } from 'element-plus';
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";
// import propertiesPanelModule from 'bpmn-js-properties-panel'
// import propertiesProviderModule from 'bpmn-js-properties-panel/lib/provider/camunda'
// 标签解析 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 xml2js from 'xml-js'
// import xml2js from 'fast-xml-parser'
import { XmlNode, XmlNodeType, parseXmlString } from 'steady-xml';
// 代码高亮插件
// import hljs from 'highlight.js/lib/highlight'
// import 'highlight.js/styles/github-gist.css'
// hljs.registerLanguage('xml', 'highlight.js/lib/languages/xml')
// hljs.registerLanguage('json', 'highlight.js/lib/languages/json')
// const eventName = reactive({
// name: ''
// })
import hljs from 'highlight.js'; // 导入代码高亮文件
import 'highlight.js/styles/github.css'; // 导入代码高亮样式
defineOptions({ name: 'MyProcessDesigner' });
const bpmnCanvas = ref();
const refFile = ref();
const emit = defineEmits([
'destroy',
'init-finished',
'save',
'commandStack-changed',
'input',
'change',
'canvas-viewbox-changed',
// eventName.name
'element-click',
]);
const props = defineProps({
value: String, // xml 字符串
// valueWatch: true, // xml 字符串的 watch 状态
processId: String, // 流程 key 标识
processName: String, // 流程 name 名字
formId: Number, // 流程 form 表单编号
translations: {
// 自定义的翻译文件
type: Object,
default: () => {},
},
additionalModel: [Object, Array], // 自定义model
moddleExtension: {
// 自定义moddle
type: Object,
default: () => {},
},
onlyCustomizeAddi: {
type: Boolean,
default: false,
},
onlyCustomizeModdle: {
type: Boolean,
default: false,
},
simulation: {
type: Boolean,
default: true,
},
keyboard: {
type: Boolean,
default: true,
},
prefix: {
type: String,
default: 'camunda',
},
events: {
type: Array,
default: () => ['element.click'],
},
headerButtonSize: {
type: String,
default: 'small',
validator: (value: string) =>
['default', 'medium', 'small', 'mini'].indexOf(value) !== -1,
},
headerButtonType: {
type: String,
default: 'primary',
validator: (value: string) =>
['default', 'primary', 'success', 'warning', 'danger', 'info'].indexOf(
value,
) !== -1,
},
});
/**
* 代码高亮
*/
const highlightedCode = (code: string) => {
// 高亮
if (previewType.value === 'json') {
code = JSON.stringify(code, null, 2);
}
const result = hljs.highlight(code, {
language: previewType.value,
ignoreIllegals: true,
});
return result.value || '&nbsp;';
};
provide('configGlobal', props);
let bpmnModeler: any = null;
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);
const additionalModules = computed(() => {
console.log(props.additionalModel, 'additionalModel');
const Modules: any[] = [];
// 仅保留用户自定义扩展模块
if (props.onlyCustomizeAddi) {
if (
Object.prototype.toString.call(props.additionalModel) == '[object Array]'
) {
return props.additionalModel || [];
}
return [props.additionalModel];
}
// 插入用户自定义扩展模块
if (
Object.prototype.toString.call(props.additionalModel) == '[object Array]'
) {
Modules.push(...(props.additionalModel as any[]));
} else {
props.additionalModel && Modules.push(props.additionalModel);
}
// 翻译模块
const TranslateModule = {
translate: ['value', customTranslate(props.translations || translationsCN)],
};
Modules.push(TranslateModule);
// 模拟流转模块
if (props.simulation) {
Modules.push(tokenSimulation);
}
// 根据需要的流程类型设置扩展元素构建模块
// if (this.prefix === "bpmn") {
// Modules.push(bpmnModdleExtension);
// }
console.log(props.prefix, 'props.prefix ');
if (props.prefix === 'camunda') {
Modules.push(camundaModdleExtension);
}
if (props.prefix === 'flowable') {
Modules.push(flowableModdleExtension);
}
if (props.prefix === 'activiti') {
Modules.push(activitiModdleExtension);
}
return Modules;
});
const moddleExtensions = computed(() => {
console.log(props.onlyCustomizeModdle, 'props.onlyCustomizeModdle');
console.log(props.moddleExtension, 'props.moddleExtension');
console.log(props.prefix, 'props.prefix');
const Extensions: any = {};
// 仅使用用户自定义模块
if (props.onlyCustomizeModdle) {
return props.moddleExtension || null;
}
// 插入用户自定义模块
if (props.moddleExtension) {
for (let key in props.moddleExtension) {
Extensions[key] = props.moddleExtension[key];
}
}
// 根据需要的 "流程类型" 设置 对应的解析文件
if (props.prefix === 'activiti') {
Extensions.activiti = activitiModdleDescriptor;
}
if (props.prefix === 'flowable') {
Extensions.flowable = flowableModdleDescriptor;
}
if (props.prefix === 'camunda') {
Extensions.camunda = camundaModdleDescriptor;
}
return Extensions;
});
console.log(additionalModules, 'additionalModules()');
console.log(moddleExtensions, 'moddleExtensions()');
const initBpmnModeler = () => {
if (bpmnModeler) return;
let data = document.getElementById('bpmnCanvas');
console.log(data, 'data');
console.log(props.keyboard, 'props.keyboard');
console.log(additionalModules, 'additionalModules()');
console.log(moddleExtensions, 'moddleExtensions()');
bpmnModeler = new BpmnModeler({
// container: this.$refs['bpmn-canvas'],
// container: getCurrentInstance(),
// container: needClass,
// container: bpmnCanvas.value,
container: data,
// width: '100%',
// 添加控制板
// propertiesPanel: {
// parent: '#js-properties-panel'
// },
keyboard: props.keyboard ? { bindTo: document } : null,
// additionalModules: additionalModules.value,
additionalModules: additionalModules.value,
moddleExtensions: moddleExtensions.value,
// additionalModules: [
// additionalModules.value
// propertiesPanelModule,
// propertiesProviderModule
// propertiesProviderModule
// ],
// moddleExtensions: { camunda: moddleExtensions.value }
});
// bpmnModeler.createDiagram()
// console.log(bpmnModeler, 'bpmnModeler111111')
emit('init-finished', bpmnModeler);
initModelListeners();
};
const initModelListeners = () => {
const EventBus = bpmnModeler.get('eventBus');
console.log(EventBus, 'EventBus');
// 注册需要的监听事件, 将. 替换为 - , 避免解析异常
props.events.forEach((event: any) => {
EventBus.on(event, function (eventObj) {
let eventName = event.replace(/\./g, '-');
// eventName.name = eventName
let element = eventObj ? eventObj.element : null;
console.log(eventName, 'eventName');
console.log(element, 'element');
emit('element-click', element, eventObj);
// emit(eventName, element, eventObj)
});
});
// 监听图形改变返回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);
emit('save', xml);
} catch (e: any) {
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;
});
};
/* 创建新的流程图 */
const createNewDiagram = async (xml) => {
console.log(xml, '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 {
// console.log(xmlString, 'xmlString')
// console.log(this.bpmnModeler.importXML);
let { warnings } = await bpmnModeler.importXML(xmlString);
console.log(warnings, 'warnings');
if (warnings && warnings.length) {
warnings.forEach((warn) => console.warn(warn));
}
} catch (e: any) {
console.error(`[Process Designer Warn]: ${e.message || e}`);
}
};
// 下载流程图到本地
const downloadProcess = async (type) => {
try {
// 按需要类型创建文件并下载
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(), xml);
downloadFunc(href, filename);
} else {
const { err, svg } = await bpmnModeler.saveSVG();
// 读取异常时抛出异常
if (err) {
return console.error(err);
}
let { href, filename } = setEncoded('SVG', svg);
downloadFunc(href, filename);
}
} catch (e: any) {
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 对象
}
}
};
// 根据所需类型进行转码并返回下载地址
const setEncoded = (type, data) => {
const filename = 'diagram';
const encodedData = encodeURIComponent(data);
return {
filename: `${filename}.${type}`,
href: `data:application/${
type === 'svg' ? 'text/xml' : 'bpmn20-xml'
};charset=UTF-8,${encodedData}`,
data: data,
};
};
// 加载本地文件
const importLocalFile = () => {
const file = refFile.value.files[0];
const reader = new FileReader();
reader.readAsText(file);
reader.onload = function () {
let xmlStr = this.result;
createNewDiagram(xmlStr);
emit('save', xmlStr);
};
};
/* ------------------------------------------------ refs methods ------------------------------------------------------ */
const downloadProcessAsXml = () => {
downloadProcess('xml');
};
const downloadProcessAsBpmn = () => {
downloadProcess('bpmn');
};
const downloadProcessAsSvg = () => {
downloadProcess('svg');
};
const processSimulation = () => {
simulationStatus.value = !simulationStatus.value;
console.log(
bpmnModeler.get('toggleMode', 'strict'),
"bpmnModeler.get('toggleMode')",
);
props.simulation && bpmnModeler.get('toggleMode', 'strict').toggleMode();
};
const processRedo = () => {
bpmnModeler.get('commandStack').redo();
};
const processUndo = () => {
bpmnModeler.get('commandStack').undo();
};
const 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);
};
const 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);
};
const processReZoom = () => {
defaultZoom.value = 1;
bpmnModeler.get('canvas').zoom('fit-viewport', 'auto');
};
const processRestart = () => {
recoverable.value = false;
revocable.value = false;
createNewDiagram(null);
};
const elementsAlign = (align) => {
const Align = bpmnModeler.get('alignElements');
const Selection = bpmnModeler.get('selection');
const SelectedElements = Selection.get();
if (!SelectedElements || SelectedElements.length <= 1) {
ElMessage.warning('请按住 Shift 键选择多个元素对齐');
// alert('请按住 Ctrl 键选择多个元素对齐
return;
}
ElMessageBox.confirm('自动对齐可能造成图形变形,是否继续?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
Align.trigger(SelectedElements, align);
});
};
/*----------------------------- 方法结束 ---------------------------------*/
const previewProcessXML = () => {
console.log(bpmnModeler.saveXML, 'bpmnModeler');
bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
// console.log(xml, 'xml111111')
previewResult.value = xml;
previewType.value = 'xml';
previewModelVisible.value = true;
});
};
const previewProcessJson = () => {
bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
const rootNodes = new XmlNode(XmlNodeType.Root, parseXmlString(xml));
previewResult.value = rootNodes.parent?.toJSON() as unknown as string;
previewType.value = 'json';
previewModelVisible.value = true;
});
};
/* ------------------------------------------------ 芋道源码 methods ------------------------------------------------------ */
onMounted(() => {
initBpmnModeler();
createNewDiagram(props.value);
});
onBeforeUnmount(() => {
if (bpmnModeler) bpmnModeler.destroy();
emit('destroy', bpmnModeler);
bpmnModeler = null;
});
</script>

View File

@@ -0,0 +1,431 @@
<template>
<div class="process-viewer">
<div style="height: 100%" ref="processCanvas" v-show="!isLoading"></div>
<!-- 自定义箭头样式用于已完成状态下流程连线箭头 -->
<defs ref="customDefs">
<marker
id="sequenceflow-end-white-success"
viewBox="0 0 20 20"
refX="11"
refY="10"
markerWidth="10"
markerHeight="10"
orient="auto"
>
<path
class="success-arrow"
d="M 1 5 L 11 10 L 1 15 Z"
style="
stroke-width: 1px;
stroke-linecap: round;
stroke-dasharray: 10000, 1;
"
/>
</marker>
<marker
id="conditional-flow-marker-white-success"
viewBox="0 0 20 20"
refX="-1"
refY="10"
markerWidth="10"
markerHeight="10"
orient="auto"
>
<path
class="success-conditional"
d="M 0 10 L 8 6 L 16 10 L 8 14 Z"
style="
stroke-width: 1px;
stroke-linecap: round;
stroke-dasharray: 10000, 1;
"
/>
</marker>
</defs>
<!-- 审批记录 -->
<el-dialog
:title="dialogTitle || '审批记录'"
v-model="dialogVisible"
width="1000px"
>
<el-row>
<el-table
:data="selectTasks"
size="small"
border
header-cell-class-name="table-header-gray"
>
<el-table-column
label="序号"
header-align="center"
align="center"
type="index"
width="50"
/>
<el-table-column
label="审批人"
min-width="100"
align="center"
v-if="selectActivityType === 'bpmn:UserTask'"
>
<template #default="scope">
{{
scope.row.assigneeUser?.nickname ||
scope.row.ownerUser?.nickname
}}
</template>
</el-table-column>
<el-table-column
label="发起人"
prop="assigneeUser.nickname"
min-width="100"
align="center"
v-else
/>
<el-table-column label="部门" min-width="100" align="center">
<template #default="scope">
{{
scope.row.assigneeUser?.deptName ||
scope.row.ownerUser?.deptName
}}
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="开始时间"
prop="createTime"
min-width="140"
/>
<el-table-column
:formatter="dateFormatter"
align="center"
label="结束时间"
prop="endTime"
min-width="140"
/>
<el-table-column
align="center"
label="审批状态"
prop="status"
min-width="90"
>
<template #default="scope">
<dict-tag
:type="DICT_TYPE.BPM_TASK_STATUS"
:value="scope.row.status"
/>
</template>
</el-table-column>
<el-table-column
align="center"
label="审批建议"
prop="reason"
min-width="120"
v-if="selectActivityType === 'bpmn:UserTask'"
/>
<el-table-column
align="center"
label="耗时"
prop="durationInMillis"
width="100"
>
<template #default="scope">
{{ formatPast2(scope.row.durationInMillis) }}
</template>
</el-table-column>
</el-table>
</el-row>
</el-dialog>
<!-- Zoom放大缩小 -->
<div style="position: absolute; top: 0; left: 0; width: 100%">
<el-row type="flex" justify="end">
<el-button-group key="scale-control" size="default">
<el-button
size="default"
:plain="true"
:disabled="defaultZoom <= 0.3"
:icon="ZoomOut"
@click="processZoomOut()"
/>
<el-button size="default" style="width: 90px">
{{ Math.floor(defaultZoom * 10 * 10) + '%' }}
</el-button>
<el-button
size="default"
:plain="true"
:disabled="defaultZoom >= 3.9"
:icon="ZoomIn"
@click="processZoomIn()"
/>
<el-button
size="default"
:icon="ScaleToOriginal"
@click="processReZoom()"
/>
</el-button-group>
</el-row>
</div>
</div>
</template>
<script lang="ts" setup>
import '../theme/index.scss';
import BpmnViewer from 'bpmn-js/lib/Viewer';
import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas';
import { ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue';
import { DICT_TYPE } from '@/utils/dict';
import { dateFormatter, formatPast2 } from '@/utils/formatTime';
import { BpmProcessInstanceStatus } from '@/utils/constants';
const props = defineProps({
xml: {
type: String,
required: true,
},
view: {
type: Object,
require: true,
},
});
const processCanvas = ref();
const bpmnViewer = ref<BpmnViewer | null>(null);
const customDefs = ref();
const defaultZoom = ref(1); // 默认缩放比例
const isLoading = ref(false); // 是否加载中
const processInstance = ref<any>({}); // 流程实例
const tasks = ref([]); // 流程任务
const dialogVisible = ref(false); // 弹窗可见性
const dialogTitle = ref<string | undefined>(undefined); // 弹窗标题
const selectActivityType = ref<string | undefined>(undefined); // 选中 Task 的活动编号
const selectTasks = ref<any[]>([]); // 选中的任务数组
/** Zoom恢复 */
const processReZoom = () => {
defaultZoom.value = 1;
bpmnViewer.value?.get('canvas').zoom('fit-viewport', 'auto');
};
/** Zoom放大 */
const 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;
bpmnViewer.value?.get('canvas').zoom(defaultZoom.value);
};
/** Zoom缩小 */
const 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;
bpmnViewer.value?.get('canvas').zoom(defaultZoom.value);
};
/** 流程图预览清空 */
const clearViewer = () => {
if (processCanvas.value) {
processCanvas.value.innerHTML = '';
}
if (bpmnViewer.value) {
bpmnViewer.value.destroy();
}
bpmnViewer.value = null;
};
/** 添加自定义箭头 */
// TODO 芋艿:自定义箭头不生效,有点奇怪!!!!相关的 marker-end、marker-start 暂时也注释了!!!
const addCustomDefs = () => {
if (!bpmnViewer.value) {
return;
}
const canvas = bpmnViewer.value?.get('canvas');
const svg = canvas?._svg;
svg.appendChild(customDefs.value);
};
/** 节点选中 */
const onSelectElement = (element: any) => {
// 清空原选中
selectActivityType.value = undefined;
dialogTitle.value = undefined;
if (!element || !processInstance.value?.id) {
return;
}
// UserTask 的情况
const activityType = element.type;
selectActivityType.value = activityType;
if (activityType === 'bpmn:UserTask') {
dialogTitle.value = element.businessObject
? element.businessObject.name
: undefined;
selectTasks.value = tasks.value.filter(
(item: any) => item?.taskDefinitionKey === element.id,
);
dialogVisible.value = true;
} else if (
activityType === 'bpmn:EndEvent' ||
activityType === 'bpmn:StartEvent'
) {
dialogTitle.value = '审批信息';
selectTasks.value = [
{
assigneeUser: processInstance.value.startUser,
createTime: processInstance.value.startTime,
endTime: processInstance.value.endTime,
status: processInstance.value.status,
durationInMillis: processInstance.value.durationInMillis,
},
];
dialogVisible.value = true;
}
};
/** 初始化 BPMN 视图 */
const importXML = async (xml: string) => {
// 清空流程图
clearViewer();
// 初始化流程图
if (xml != null && xml !== '') {
try {
bpmnViewer.value = new BpmnViewer({
additionalModules: [MoveCanvasModule],
container: processCanvas.value,
});
// 增加点击事件
bpmnViewer.value.on('element.click', ({ element }) => {
onSelectElement(element);
});
// 初始化 BPMN 视图
isLoading.value = true;
await bpmnViewer.value.importXML(xml);
// 自定义成功的箭头
addCustomDefs();
} catch (e) {
clearViewer();
} finally {
isLoading.value = false;
// 高亮流程
setProcessStatus(props.view);
}
}
};
/** 高亮流程 */
const setProcessStatus = (view: any) => {
// 设置相关变量
if (!view || !view.processInstance) {
return;
}
processInstance.value = view.processInstance;
tasks.value = view.tasks;
if (isLoading.value || !bpmnViewer.value) {
return;
}
const {
unfinishedTaskActivityIds,
finishedTaskActivityIds,
finishedSequenceFlowActivityIds,
rejectedTaskActivityIds,
} = view;
const canvas = bpmnViewer.value.get('canvas');
const elementRegistry = bpmnViewer.value.get('elementRegistry');
// 已完成节点
if (Array.isArray(finishedSequenceFlowActivityIds)) {
finishedSequenceFlowActivityIds.forEach((item: any) => {
if (item != null) {
canvas.addMarker(item, 'success');
const element = elementRegistry.get(item);
const conditionExpression = element.businessObject.conditionExpression;
if (conditionExpression) {
canvas.addMarker(item, 'condition-expression');
}
}
});
}
if (Array.isArray(finishedTaskActivityIds)) {
finishedTaskActivityIds.forEach((item: any) =>
canvas.addMarker(item, 'success'),
);
}
// 未完成节点
if (Array.isArray(unfinishedTaskActivityIds)) {
unfinishedTaskActivityIds.forEach((item: any) =>
canvas.addMarker(item, 'primary'),
);
}
// 被拒绝节点
if (Array.isArray(rejectedTaskActivityIds)) {
rejectedTaskActivityIds.forEach((item: any) => {
if (item != null) {
canvas.addMarker(item, 'danger');
}
});
}
// 特殊:处理 end 节点的高亮。因为 end 在拒绝、取消时,被后端计算成了 finishedTaskActivityIds 里
if (
[BpmProcessInstanceStatus.CANCEL, BpmProcessInstanceStatus.REJECT].includes(
processInstance.value.status,
)
) {
const endNodes = elementRegistry.filter(
(element: any) => element.type === 'bpmn:EndEvent',
);
endNodes.forEach((item: any) => {
canvas.removeMarker(item.id, 'success');
if (processInstance.value.status === BpmProcessInstanceStatus.CANCEL) {
canvas.addMarker(item.id, 'cancel');
} else {
canvas.addMarker(item.id, 'danger');
}
});
}
};
watch(
() => props.xml,
(newXml) => {
importXML(newXml);
},
{ immediate: true },
);
watch(
() => props.view,
(newView) => {
setProcessStatus(newView);
},
{ immediate: true },
);
/** mounted初始化 */
onMounted(() => {
importXML(props.xml);
setProcessStatus(props.view);
});
/** unmount销毁 */
onBeforeUnmount(() => {
clearViewer();
});
</script>

View File

@@ -0,0 +1,8 @@
import MyProcessDesigner from './ProcessDesigner.vue';
MyProcessDesigner.install = function (Vue) {
Vue.component(MyProcessDesigner.name, MyProcessDesigner);
};
// 流程图的设计器,可编辑
export default MyProcessDesigner;

View File

@@ -0,0 +1,8 @@
import MyProcessViewer from './ProcessViewer.vue';
MyProcessViewer.install = function (Vue) {
Vue.component(MyProcessViewer.name, MyProcessViewer);
};
// 流程图的查看器,不可编辑
export default MyProcessViewer;

View File

@@ -0,0 +1,440 @@
import { getChildLanes } from 'bpmn-js/lib/features/modeling/util/LaneUtil';
import { isAny } from 'bpmn-js/lib/features/modeling/util/ModelingUtil';
import { isEventSubProcess, isExpanded } from 'bpmn-js/lib/util/DiUtil';
import { is } from 'bpmn-js/lib/util/ModelUtil';
import { hasPrimaryModifier } from 'diagram-js/lib/util/Mouse';
import { assign, forEach, isArray } from 'min-dash';
/**
* A provider for BPMN 2.0 elements context pad
*/
export default function ContextPadProvider(
config,
injector,
eventBus,
contextPad,
modeling,
elementFactory,
connect,
create,
popupMenu,
canvas,
rules,
translate,
) {
config = config || {};
contextPad.registerProvider(this);
this._contextPad = contextPad;
this._modeling = modeling;
this._elementFactory = elementFactory;
this._connect = connect;
this._create = create;
this._popupMenu = popupMenu;
this._canvas = canvas;
this._rules = rules;
this._translate = translate;
if (config.autoPlace !== false) {
this._autoPlace = injector.get('autoPlace', false);
}
eventBus.on('create.end', 250, (event) => {
const context = event.context;
const shape = context.shape;
if (!hasPrimaryModifier(event) || !contextPad.isOpen(shape)) {
return;
}
const entries = contextPad.getEntries(shape);
if (entries.replace) {
entries.replace.action.click(event, shape);
}
});
}
ContextPadProvider.$inject = [
'config.contextPad',
'injector',
'eventBus',
'contextPad',
'modeling',
'elementFactory',
'connect',
'create',
'popupMenu',
'canvas',
'rules',
'translate',
'elementRegistry',
];
ContextPadProvider.prototype.getContextPadEntries = function (element) {
const autoPlace = this._autoPlace;
const canvas = this._canvas;
const connect = this._connect;
const contextPad = this._contextPad;
const create = this._create;
const elementFactory = this._elementFactory;
const modeling = this._modeling;
const popupMenu = this._popupMenu;
const rules = this._rules;
const translate = this._translate;
const actions = {};
if (element.type === 'label') {
return actions;
}
const businessObject = element.businessObject;
function startConnect(event, element) {
connect.start(event, element);
}
function removeElement() {
modeling.removeElements([element]);
}
function getReplaceMenuPosition(element) {
const Y_OFFSET = 5;
const diagramContainer = canvas.getContainer();
const pad = contextPad.getPad(element).html;
const diagramRect = diagramContainer.getBoundingClientRect();
const padRect = pad.getBoundingClientRect();
const top = padRect.top - diagramRect.top;
const left = padRect.left - diagramRect.left;
const pos = {
x: left,
y: top + padRect.height + Y_OFFSET,
};
return pos;
}
/**
* Create an append action
*
* @param {string} type
* @param {string} className
* @param {string} [title]
* @param {object} [options]
*
* @return {object} descriptor
*/
function appendAction(type, className, title, options) {
if (typeof title !== 'string') {
options = title;
title = translate('Append {type}', { type: type.replace(/^bpmn:/, '') });
}
function appendStart(event, element) {
const shape = elementFactory.createShape(assign({ type }, options));
create.start(event, shape, {
source: element,
});
}
const append = autoPlace
? function (event, element) {
const shape = elementFactory.createShape(assign({ type }, options));
autoPlace.append(element, shape);
}
: appendStart;
return {
group: 'model',
className,
title,
action: {
dragstart: appendStart,
click: append,
},
};
}
function splitLaneHandler(count) {
return function (event, element) {
// actual split
modeling.splitLane(element, count);
// refresh context pad after split to
// get rid of split icons
contextPad.open(element, true);
};
}
if (
isAny(businessObject, ['bpmn:Lane', 'bpmn:Participant']) &&
isExpanded(businessObject)
) {
const childLanes = getChildLanes(element);
assign(actions, {
'lane-insert-above': {
group: 'lane-insert-above',
className: 'bpmn-icon-lane-insert-above',
title: translate('Add Lane above'),
action: {
click(event, element) {
modeling.addLane(element, 'top');
},
},
},
});
if (childLanes.length < 2) {
if (element.height >= 120) {
assign(actions, {
'lane-divide-two': {
group: 'lane-divide',
className: 'bpmn-icon-lane-divide-two',
title: translate('Divide into two Lanes'),
action: {
click: splitLaneHandler(2),
},
},
});
}
if (element.height >= 180) {
assign(actions, {
'lane-divide-three': {
group: 'lane-divide',
className: 'bpmn-icon-lane-divide-three',
title: translate('Divide into three Lanes'),
action: {
click: splitLaneHandler(3),
},
},
});
}
}
assign(actions, {
'lane-insert-below': {
group: 'lane-insert-below',
className: 'bpmn-icon-lane-insert-below',
title: translate('Add Lane below'),
action: {
click(event, element) {
modeling.addLane(element, 'bottom');
},
},
},
});
}
if (is(businessObject, 'bpmn:FlowNode')) {
if (is(businessObject, 'bpmn:EventBasedGateway')) {
assign(actions, {
'append.receive-task': appendAction(
'bpmn:ReceiveTask',
'bpmn-icon-receive-task',
translate('Append ReceiveTask'),
),
'append.message-intermediate-event': appendAction(
'bpmn:IntermediateCatchEvent',
'bpmn-icon-intermediate-event-catch-message',
translate('Append MessageIntermediateCatchEvent'),
{ eventDefinitionType: 'bpmn:MessageEventDefinition' },
),
'append.timer-intermediate-event': appendAction(
'bpmn:IntermediateCatchEvent',
'bpmn-icon-intermediate-event-catch-timer',
translate('Append TimerIntermediateCatchEvent'),
{ eventDefinitionType: 'bpmn:TimerEventDefinition' },
),
'append.condition-intermediate-event': appendAction(
'bpmn:IntermediateCatchEvent',
'bpmn-icon-intermediate-event-catch-condition',
translate('Append ConditionIntermediateCatchEvent'),
{ eventDefinitionType: 'bpmn:ConditionalEventDefinition' },
),
'append.signal-intermediate-event': appendAction(
'bpmn:IntermediateCatchEvent',
'bpmn-icon-intermediate-event-catch-signal',
translate('Append SignalIntermediateCatchEvent'),
{ eventDefinitionType: 'bpmn:SignalEventDefinition' },
),
});
} else if (
isEventType(
businessObject,
'bpmn:BoundaryEvent',
'bpmn:CompensateEventDefinition',
)
) {
assign(actions, {
'append.compensation-activity': appendAction(
'bpmn:Task',
'bpmn-icon-task',
translate('Append compensation activity'),
{
isForCompensation: true,
},
),
});
} else if (
!is(businessObject, 'bpmn:EndEvent') &&
!businessObject.isForCompensation &&
!isEventType(
businessObject,
'bpmn:IntermediateThrowEvent',
'bpmn:LinkEventDefinition',
) &&
!isEventSubProcess(businessObject)
) {
assign(actions, {
'append.end-event': appendAction(
'bpmn:EndEvent',
'bpmn-icon-end-event-none',
translate('Append EndEvent'),
),
'append.gateway': appendAction(
'bpmn:ExclusiveGateway',
'bpmn-icon-gateway-none',
translate('Append Gateway'),
),
'append.append-task': appendAction(
'bpmn:UserTask',
'bpmn-icon-user-task',
translate('Append Task'),
),
'append.intermediate-event': appendAction(
'bpmn:IntermediateThrowEvent',
'bpmn-icon-intermediate-event-none',
translate('Append Intermediate/Boundary Event'),
),
});
}
}
if (!popupMenu.isEmpty(element, 'bpmn-replace')) {
// Replace menu entry
assign(actions, {
replace: {
group: 'edit',
className: 'bpmn-icon-screw-wrench',
title: '修改类型',
action: {
click(event, element) {
const position = assign(getReplaceMenuPosition(element), {
cursor: { x: event.x, y: event.y },
});
popupMenu.open(element, 'bpmn-replace', position);
},
},
},
});
}
if (
isAny(businessObject, [
'bpmn:FlowNode',
'bpmn:InteractionNode',
'bpmn:DataObjectReference',
'bpmn:DataStoreReference',
])
) {
assign(actions, {
'append.text-annotation': appendAction(
'bpmn:TextAnnotation',
'bpmn-icon-text-annotation',
),
connect: {
group: 'connect',
className: 'bpmn-icon-connection-multi',
title: translate(
`Connect using ${
businessObject.isForCompensation ? '' : 'Sequence/MessageFlow or '
}Association`,
),
action: {
click: startConnect,
dragstart: startConnect,
},
},
});
}
if (
isAny(businessObject, [
'bpmn:DataObjectReference',
'bpmn:DataStoreReference',
])
) {
assign(actions, {
connect: {
group: 'connect',
className: 'bpmn-icon-connection-multi',
title: translate('Connect using DataInputAssociation'),
action: {
click: startConnect,
dragstart: startConnect,
},
},
});
}
if (is(businessObject, 'bpmn:Group')) {
assign(actions, {
'append.text-annotation': appendAction(
'bpmn:TextAnnotation',
'bpmn-icon-text-annotation',
),
});
}
// delete element entry, only show if allowed by rules
let deleteAllowed = rules.allowed('elements.delete', { elements: [element] });
if (isArray(deleteAllowed)) {
// was the element returned as a deletion candidate?
deleteAllowed = deleteAllowed[0] === element;
}
if (deleteAllowed) {
assign(actions, {
delete: {
group: 'edit',
className: 'bpmn-icon-trash',
title: translate('Remove'),
action: {
click: removeElement,
},
},
});
}
return actions;
};
// helpers /////////
function isEventType(eventBo, type, definition) {
const isType = eventBo.$instanceOf(type);
let isDefinition = false;
const definitions = eventBo.eventDefinitions || [];
forEach(definitions, (def) => {
if (def.$type === definition) {
isDefinition = true;
}
});
return isType && isDefinition;
}

View File

@@ -0,0 +1,6 @@
import CustomContextPadProvider from './contentPadProvider';
export default {
__init__: ['contextPadProvider'],
contextPadProvider: ['type', CustomContextPadProvider],
};

View File

@@ -0,0 +1,24 @@
export default (key, name, type) => {
if (!type) type = 'camunda';
const TYPE_TARGET = {
activiti: 'http://activiti.org/bpmn',
camunda: 'http://bpmn.io/schema/bpmn',
flowable: 'http://flowable.org/bpmn',
};
return `<?xml version="1.0" encoding="UTF-8"?>
<bpmn2:definitions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
id="diagram_${key}"
targetNamespace="${TYPE_TARGET[type]}">
<bpmn2:process id="${key}" name="${name}" isExecutable="true">
</bpmn2:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="${key}">
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn2:definitions>`;
};

View File

@@ -0,0 +1,101 @@
'use strict';
import { some } from 'min-dash';
// const some = require('min-dash').some
// const some = some
const ALLOWED_TYPES = {
FailedJobRetryTimeCycle: [
'bpmn:StartEvent',
'bpmn:BoundaryEvent',
'bpmn:IntermediateCatchEvent',
'bpmn:Activity',
],
Connector: ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent'],
Field: ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent'],
};
function is(element, type) {
return (
element &&
typeof element.$instanceOf === 'function' &&
element.$instanceOf(type)
);
}
function exists(element) {
return element && element.length;
}
function includesType(collection, type) {
return (
exists(collection) &&
some(collection, (element) => {
return is(element, type);
})
);
}
function anyType(element, types) {
return some(types, (type) => {
return is(element, type);
});
}
function isAllowed(propName, propDescriptor, newElement) {
const name = propDescriptor.name;
const types = ALLOWED_TYPES[name.replace(/activiti:/, '')];
return name === propName && anyType(newElement, types);
}
function ActivitiModdleExtension(eventBus) {
eventBus.on(
'property.clone',
function (context) {
const newElement = context.newElement;
const propDescriptor = context.propertyDescriptor;
this.canCloneProperty(newElement, propDescriptor);
},
this,
);
}
ActivitiModdleExtension.$inject = ['eventBus'];
ActivitiModdleExtension.prototype.canCloneProperty = function (
newElement,
propDescriptor,
) {
if (
isAllowed('activiti:FailedJobRetryTimeCycle', propDescriptor, newElement)
) {
return (
includesType(newElement.eventDefinitions, 'bpmn:TimerEventDefinition') ||
includesType(newElement.eventDefinitions, 'bpmn:SignalEventDefinition') ||
is(
newElement.loopCharacteristics,
'bpmn:MultiInstanceLoopCharacteristics',
)
);
}
if (isAllowed('activiti:Connector', propDescriptor, newElement)) {
return includesType(
newElement.eventDefinitions,
'bpmn:MessageEventDefinition',
);
}
if (isAllowed('activiti:Field', propDescriptor, newElement)) {
return includesType(
newElement.eventDefinitions,
'bpmn:MessageEventDefinition',
);
}
};
// module.exports = ActivitiModdleExtension;
export default ActivitiModdleExtension;

View File

@@ -0,0 +1,11 @@
/*
* @author igdianov
* address https://github.com/igdianov/activiti-bpmn-moddle
* */
import activitiExtension from './activitiExtension';
export default {
__init__: ['ActivitiModdleExtension'],
ActivitiModdleExtension: ['type', activitiExtension],
};

View File

@@ -0,0 +1,165 @@
'use strict';
import { isFunction, isObject, some } from 'min-dash';
// const isFunction = isFunction,
// isObject = isObject,
// some = some
// const isFunction = require('min-dash').isFunction,
// isObject = require('min-dash').isObject,
// some = require('min-dash').some
const WILDCARD = '*';
function CamundaModdleExtension(eventBus) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
eventBus.on('moddleCopy.canCopyProperty', (context) => {
const parent = context.parent;
const property = context.property;
return self.canCopyProperty(property, parent);
});
}
CamundaModdleExtension.$inject = ['eventBus'];
/**
* Check wether to disallow copying property.
*/
CamundaModdleExtension.prototype.canCopyProperty = function (property, parent) {
// (1) check wether property is allowed in parent
if (isObject(property) && !isAllowedInParent(property, parent)) {
return false;
}
// (2) check more complex scenarios
if (is(property, 'camunda:InputOutput') && !this.canHostInputOutput(parent)) {
return false;
}
if (
isAny(property, ['camunda:Connector', 'camunda:Field']) &&
!this.canHostConnector(parent)
) {
return false;
}
if (is(property, 'camunda:In') && !this.canHostIn(parent)) {
return false;
}
};
CamundaModdleExtension.prototype.canHostInputOutput = function (parent) {
// allowed in camunda:Connector
const connector = getParent(parent, 'camunda:Connector');
if (connector) {
return true;
}
// special rules inside bpmn:FlowNode
const flowNode = getParent(parent, 'bpmn:FlowNode');
if (!flowNode) {
return false;
}
if (
isAny(flowNode, ['bpmn:StartEvent', 'bpmn:Gateway', 'bpmn:BoundaryEvent'])
) {
return false;
}
return !(is(flowNode, 'bpmn:SubProcess') && flowNode.get('triggeredByEvent'));
};
CamundaModdleExtension.prototype.canHostConnector = function (parent) {
const serviceTaskLike = getParent(parent, 'camunda:ServiceTaskLike');
if (is(serviceTaskLike, 'bpmn:MessageEventDefinition')) {
// only allow on throw and end events
return (
getParent(parent, 'bpmn:IntermediateThrowEvent') ||
getParent(parent, 'bpmn:EndEvent')
);
}
return true;
};
CamundaModdleExtension.prototype.canHostIn = function (parent) {
const callActivity = getParent(parent, 'bpmn:CallActivity');
if (callActivity) {
return true;
}
const signalEventDefinition = getParent(parent, 'bpmn:SignalEventDefinition');
if (signalEventDefinition) {
// only allow on throw and end events
return (
getParent(parent, 'bpmn:IntermediateThrowEvent') ||
getParent(parent, 'bpmn:EndEvent')
);
}
return true;
};
// module.exports = CamundaModdleExtension;
export default CamundaModdleExtension;
// helpers //////////
function is(element, type) {
return (
element && isFunction(element.$instanceOf) && element.$instanceOf(type)
);
}
function isAny(element, types) {
return some(types, (t) => {
return is(element, t);
});
}
function getParent(element, type) {
if (!type) {
return element.$parent;
}
if (is(element, type)) {
return element;
}
if (!element.$parent) {
return;
}
return getParent(element.$parent, type);
}
function isAllowedInParent(property, parent) {
// (1) find property descriptor
const descriptor =
property.$type && property.$model.getTypeDescriptor(property.$type);
const allowedIn = descriptor && descriptor.meta && descriptor.meta.allowedIn;
if (!allowedIn || isWildcard(allowedIn)) {
return true;
}
// (2) check wether property has parent of allowed type
return some(allowedIn, (type) => {
return getParent(parent, type);
});
}
function isWildcard(allowedIn) {
return allowedIn.includes(WILDCARD);
}

View File

@@ -0,0 +1,8 @@
'use strict';
import extension from './extension';
export default {
__init__: ['camundaModdleExtension'],
camundaModdleExtension: ['type', extension],
};

View File

@@ -0,0 +1,101 @@
'use strict';
import { some } from 'min-dash';
// const some = some
// const some = require('min-dash').some
const ALLOWED_TYPES = {
FailedJobRetryTimeCycle: [
'bpmn:StartEvent',
'bpmn:BoundaryEvent',
'bpmn:IntermediateCatchEvent',
'bpmn:Activity',
],
Connector: ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent'],
Field: ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent'],
};
function is(element, type) {
return (
element &&
typeof element.$instanceOf === 'function' &&
element.$instanceOf(type)
);
}
function exists(element) {
return element && element.length;
}
function includesType(collection, type) {
return (
exists(collection) &&
some(collection, (element) => {
return is(element, type);
})
);
}
function anyType(element, types) {
return some(types, (type) => {
return is(element, type);
});
}
function isAllowed(propName, propDescriptor, newElement) {
const name = propDescriptor.name;
const types = ALLOWED_TYPES[name.replace(/flowable:/, '')];
return name === propName && anyType(newElement, types);
}
function FlowableModdleExtension(eventBus) {
eventBus.on(
'property.clone',
function (context) {
const newElement = context.newElement;
const propDescriptor = context.propertyDescriptor;
this.canCloneProperty(newElement, propDescriptor);
},
this,
);
}
FlowableModdleExtension.$inject = ['eventBus'];
FlowableModdleExtension.prototype.canCloneProperty = function (
newElement,
propDescriptor,
) {
if (
isAllowed('flowable:FailedJobRetryTimeCycle', propDescriptor, newElement)
) {
return (
includesType(newElement.eventDefinitions, 'bpmn:TimerEventDefinition') ||
includesType(newElement.eventDefinitions, 'bpmn:SignalEventDefinition') ||
is(
newElement.loopCharacteristics,
'bpmn:MultiInstanceLoopCharacteristics',
)
);
}
if (isAllowed('flowable:Connector', propDescriptor, newElement)) {
return includesType(
newElement.eventDefinitions,
'bpmn:MessageEventDefinition',
);
}
if (isAllowed('flowable:Field', propDescriptor, newElement)) {
return includesType(
newElement.eventDefinitions,
'bpmn:MessageEventDefinition',
);
}
};
// module.exports = FlowableModdleExtension;
export default FlowableModdleExtension;

View File

@@ -0,0 +1,10 @@
/*
* @author igdianov
* address https://github.com/igdianov/activiti-bpmn-moddle
* */
import flowableExtension from './flowableExtension';
export default {
__init__: ['FlowableModdleExtension'],
FlowableModdleExtension: ['type', flowableExtension],
};

View File

@@ -0,0 +1,233 @@
import PaletteProvider from 'bpmn-js/lib/features/palette/PaletteProvider';
import { assign } from 'min-dash';
export default function CustomPalette(
palette,
create,
elementFactory,
spaceTool,
lassoTool,
handTool,
globalConnect,
translate,
) {
PaletteProvider.call(
this,
palette,
create,
elementFactory,
spaceTool,
lassoTool,
handTool,
globalConnect,
translate,
2000,
);
}
const F = function () {}; // 核心,利用空对象作为中介;
F.prototype = PaletteProvider.prototype; // 核心将父类的原型赋值给空对象F
// 利用中介函数重写原型链方法
F.prototype.getPaletteEntries = function () {
const actions = {};
const create = this._create;
const elementFactory = this._elementFactory;
const spaceTool = this._spaceTool;
const lassoTool = this._lassoTool;
const handTool = this._handTool;
const globalConnect = this._globalConnect;
const translate = this._translate;
function createAction(type, group, className, title, options) {
function createListener(event) {
const shape = elementFactory.createShape(assign({ type }, options));
if (options) {
shape.businessObject.di.isExpanded = options.isExpanded;
}
create.start(event, shape);
}
const shortType = type.replace(/^bpmn:/, '');
return {
group,
className,
title: title || translate('Create {type}', { type: shortType }),
action: {
dragstart: createListener,
click: createListener,
},
};
}
function createSubprocess(event) {
const subProcess = elementFactory.createShape({
type: 'bpmn:SubProcess',
x: 0,
y: 0,
isExpanded: true,
});
const startEvent = elementFactory.createShape({
type: 'bpmn:StartEvent',
x: 40,
y: 82,
parent: subProcess,
});
create.start(event, [subProcess, startEvent], {
hints: {
autoSelect: [startEvent],
},
});
}
function createParticipant(event) {
create.start(event, elementFactory.createParticipantShape());
}
assign(actions, {
'hand-tool': {
group: 'tools',
className: 'bpmn-icon-hand-tool',
title: '激活抓手工具',
// title: translate("Activate the hand tool"),
action: {
click(event) {
handTool.activateHand(event);
},
},
},
'lasso-tool': {
group: 'tools',
className: 'bpmn-icon-lasso-tool',
title: translate('Activate the lasso tool'),
action: {
click(event) {
lassoTool.activateSelection(event);
},
},
},
'space-tool': {
group: 'tools',
className: 'bpmn-icon-space-tool',
title: translate('Activate the create/remove space tool'),
action: {
click(event) {
spaceTool.activateSelection(event);
},
},
},
'global-connect-tool': {
group: 'tools',
className: 'bpmn-icon-connection-multi',
title: translate('Activate the global connect tool'),
action: {
click(event) {
globalConnect.toggle(event);
},
},
},
'tool-separator': {
group: 'tools',
separator: true,
},
'create.start-event': createAction(
'bpmn:StartEvent',
'event',
'bpmn-icon-start-event-none',
translate('Create StartEvent'),
),
'create.intermediate-event': createAction(
'bpmn:IntermediateThrowEvent',
'event',
'bpmn-icon-intermediate-event-none',
translate('Create Intermediate/Boundary Event'),
),
'create.end-event': createAction(
'bpmn:EndEvent',
'event',
'bpmn-icon-end-event-none',
translate('Create EndEvent'),
),
'create.exclusive-gateway': createAction(
'bpmn:ExclusiveGateway',
'gateway',
'bpmn-icon-gateway-none',
translate('Create Gateway'),
),
'create.user-task': createAction(
'bpmn:UserTask',
'activity',
'bpmn-icon-user-task',
translate('Create User Task'),
),
'create.call-activity': createAction(
'bpmn:CallActivity',
'activity',
'bpmn-icon-call-activity',
translate('Create Call Activity'),
),
'create.service-task': createAction(
'bpmn:ServiceTask',
'activity',
'bpmn-icon-service',
translate('Create Service Task'),
),
'create.data-object': createAction(
'bpmn:DataObjectReference',
'data-object',
'bpmn-icon-data-object',
translate('Create DataObjectReference'),
),
'create.data-store': createAction(
'bpmn:DataStoreReference',
'data-store',
'bpmn-icon-data-store',
translate('Create DataStoreReference'),
),
'create.subprocess-expanded': {
group: 'activity',
className: 'bpmn-icon-subprocess-expanded',
title: translate('Create expanded SubProcess'),
action: {
dragstart: createSubprocess,
click: createSubprocess,
},
},
'create.participant-expanded': {
group: 'collaboration',
className: 'bpmn-icon-participant',
title: translate('Create Pool/Participant'),
action: {
dragstart: createParticipant,
click: createParticipant,
},
},
'create.group': createAction(
'bpmn:Group',
'artifact',
'bpmn-icon-group',
translate('Create Group'),
),
});
return actions;
};
CustomPalette.$inject = [
'palette',
'create',
'elementFactory',
'spaceTool',
'lassoTool',
'handTool',
'globalConnect',
'translate',
];
CustomPalette.prototype = new F(); // 核心,将 F的实例赋值给子类
CustomPalette.prototype.constructor = CustomPalette; // 修复子类CustomPalette的构造器指向防止原型链的混乱

View File

@@ -0,0 +1,22 @@
// import PaletteModule from "diagram-js/lib/features/palette";
// import CreateModule from "diagram-js/lib/features/create";
// import SpaceToolModule from "diagram-js/lib/features/space-tool";
// import LassoToolModule from "diagram-js/lib/features/lasso-tool";
// import HandToolModule from "diagram-js/lib/features/hand-tool";
// import GlobalConnectModule from "diagram-js/lib/features/global-connect";
// import translate from "diagram-js/lib/i18n/translate";
//
// import PaletteProvider from "./paletteProvider";
//
// export default {
// __depends__: [PaletteModule, CreateModule, SpaceToolModule, LassoToolModule, HandToolModule, GlobalConnectModule, translate],
// __init__: ["paletteProvider"],
// paletteProvider: ["type", PaletteProvider]
// };
import CustomPalette from './CustomPalette';
export default {
__init__: ['paletteProvider'],
paletteProvider: ['type', CustomPalette],
};

View File

@@ -0,0 +1,219 @@
import { assign } from 'min-dash';
/**
* A palette provider for BPMN 2.0 elements.
*/
export default function PaletteProvider(
palette,
create,
elementFactory,
spaceTool,
lassoTool,
handTool,
globalConnect,
translate,
) {
this._palette = palette;
this._create = create;
this._elementFactory = elementFactory;
this._spaceTool = spaceTool;
this._lassoTool = lassoTool;
this._handTool = handTool;
this._globalConnect = globalConnect;
this._translate = translate;
palette.registerProvider(this);
}
PaletteProvider.$inject = [
'palette',
'create',
'elementFactory',
'spaceTool',
'lassoTool',
'handTool',
'globalConnect',
'translate',
];
PaletteProvider.prototype.getPaletteEntries = function () {
const actions = {};
const create = this._create;
const elementFactory = this._elementFactory;
const spaceTool = this._spaceTool;
const lassoTool = this._lassoTool;
const handTool = this._handTool;
const globalConnect = this._globalConnect;
const translate = this._translate;
function createAction(type, group, className, title, options) {
function createListener(event) {
const shape = elementFactory.createShape(assign({ type }, options));
if (options) {
shape.businessObject.di.isExpanded = options.isExpanded;
}
create.start(event, shape);
}
const shortType = type.replace(/^bpmn:/, '');
return {
group,
className,
title: title || translate('Create {type}', { type: shortType }),
action: {
dragstart: createListener,
click: createListener,
},
};
}
function createSubprocess(event) {
const subProcess = elementFactory.createShape({
type: 'bpmn:SubProcess',
x: 0,
y: 0,
isExpanded: true,
});
const startEvent = elementFactory.createShape({
type: 'bpmn:StartEvent',
x: 40,
y: 82,
parent: subProcess,
});
create.start(event, [subProcess, startEvent], {
hints: {
autoSelect: [startEvent],
},
});
}
function createParticipant(event) {
create.start(event, elementFactory.createParticipantShape());
}
assign(actions, {
'hand-tool': {
group: 'tools',
className: 'bpmn-icon-hand-tool',
title: translate('Activate the hand tool'),
action: {
click(event) {
handTool.activateHand(event);
},
},
},
'lasso-tool': {
group: 'tools',
className: 'bpmn-icon-lasso-tool',
title: translate('Activate the lasso tool'),
action: {
click(event) {
lassoTool.activateSelection(event);
},
},
},
'space-tool': {
group: 'tools',
className: 'bpmn-icon-space-tool',
title: translate('Activate the create/remove space tool'),
action: {
click(event) {
spaceTool.activateSelection(event);
},
},
},
'global-connect-tool': {
group: 'tools',
className: 'bpmn-icon-connection-multi',
title: translate('Activate the global connect tool'),
action: {
click(event) {
globalConnect.toggle(event);
},
},
},
'tool-separator': {
group: 'tools',
separator: true,
},
'create.start-event': createAction(
'bpmn:StartEvent',
'event',
'bpmn-icon-start-event-none',
translate('Create StartEvent'),
),
'create.intermediate-event': createAction(
'bpmn:IntermediateThrowEvent',
'event',
'bpmn-icon-intermediate-event-none',
translate('Create Intermediate/Boundary Event'),
),
'create.end-event': createAction(
'bpmn:EndEvent',
'event',
'bpmn-icon-end-event-none',
translate('Create EndEvent'),
),
'create.exclusive-gateway': createAction(
'bpmn:ExclusiveGateway',
'gateway',
'bpmn-icon-gateway-none',
translate('Create Gateway'),
),
'create.user-task': createAction(
'bpmn:UserTask',
'activity',
'bpmn-icon-user-task',
translate('Create User Task'),
),
'create.service-task': createAction(
'bpmn:ServiceTask',
'activity',
'bpmn-icon-service',
translate('Create Service Task'),
),
'create.data-object': createAction(
'bpmn:DataObjectReference',
'data-object',
'bpmn-icon-data-object',
translate('Create DataObjectReference'),
),
'create.data-store': createAction(
'bpmn:DataStoreReference',
'data-store',
'bpmn-icon-data-store',
translate('Create DataStoreReference'),
),
'create.subprocess-expanded': {
group: 'activity',
className: 'bpmn-icon-subprocess-expanded',
title: translate('Create expanded SubProcess'),
action: {
dragstart: createSubprocess,
click: createSubprocess,
},
},
'create.participant-expanded': {
group: 'collaboration',
className: 'bpmn-icon-participant',
title: translate('Create Pool/Participant'),
action: {
dragstart: createParticipant,
click: createParticipant,
},
},
'create.group': createAction(
'bpmn:Group',
'artifact',
'bpmn-icon-group',
translate('Create Group'),
),
});
return actions;
};

View File

@@ -0,0 +1,44 @@
// import translations from "./zh";
//
// export default function customTranslate(template, replacements) {
// replacements = replacements || {};
//
// // Translate
// template = translations[template] || template;
//
// // Replace
// return template.replace(/{([^}]+)}/g, function(_, key) {
// let str = replacements[key];
// if (
// translations[replacements[key]] !== null &&
// translations[replacements[key]] !== "undefined"
// ) {
// // eslint-disable-next-line no-mixed-spaces-and-tabs
// str = translations[replacements[key]];
// // eslint-disable-next-line no-mixed-spaces-and-tabs
// }
// return str || "{" + key + "}";
// });
// }
export default function customTranslate(translations) {
return function (template, replacements) {
replacements = replacements || {};
// 将模板和翻译字典的键统一转换为小写进行匹配
const lowerTemplate = template.toLowerCase();
const translation = Object.keys(translations).find(
(key) => key.toLowerCase() === lowerTemplate,
);
// 如果找到匹配的翻译,使用翻译后的模板
if (translation) {
template = translations[translation];
}
// 替换模板中的占位符
return template.replaceAll(/\{([^}]+)\}/g, (_, key) => {
// 如果替换值存在,返回替换值;否则返回原始占位符
return replacements[key] === undefined ? `{${key}}` : replacements[key];
});
};
}

View File

@@ -0,0 +1,246 @@
/**
* This is a sample file that should be replaced with the actual translation.
*
* Checkout https://github.com/bpmn-io/bpmn-js-i18n for a list of available
* translations and labels to translate.
*/
export default {
// 添加部分
'Append EndEvent': '追加结束事件',
'Append Gateway': '追加网关',
'Append Task': '追加任务',
'Append Intermediate/Boundary Event': '追加中间抛出事件/边界事件',
'Activate the global connect tool': '激活全局连接工具',
'Append {type}': '添加 {type}',
'Add Lane above': '在上面添加道',
'Divide into two Lanes': '分割成两个道',
'Divide into three Lanes': '分割成三个道',
'Add Lane below': '在下面添加道',
'Append compensation activity': '追加补偿活动',
'Change type': '修改类型',
'Connect using Association': '使用关联连接',
'Connect using Sequence/MessageFlow or Association':
'使用顺序/消息流或者关联连接',
'Connect using DataInputAssociation': '使用数据输入关联连接',
Remove: '移除',
'Activate the hand tool': '激活抓手工具',
'Activate the lasso tool': '激活套索工具',
'Activate the create/remove space tool': '激活创建/删除空间工具',
'Create expanded SubProcess': '创建扩展子过程',
'Create IntermediateThrowEvent/BoundaryEvent': '创建中间抛出事件/边界事件',
'Create Pool/Participant': '创建池/参与者',
'Parallel Multi Instance': '并行多重事件',
'Sequential Multi Instance': '时序多重事件',
DataObjectReference: '数据对象参考',
DataStoreReference: '数据存储参考',
Loop: '循环',
'Ad-hoc': '即席',
'Create {type}': '创建 {type}',
Task: '任务',
'Send Task': '发送任务',
'Receive Task': '接收任务',
'User Task': '用户任务',
'Manual Task': '手工任务',
'Business Rule Task': '业务规则任务',
'Service Task': '服务任务',
'Script Task': '脚本任务',
'Call Activity': '调用活动',
'Sub-Process (collapsed)': '子流程(折叠的)',
'Sub-Process (expanded)': '子流程(展开的)',
'Start Event': '开始事件',
StartEvent: '开始事件',
'Intermediate Throw Event': '中间事件',
'End Event': '结束事件',
EndEvent: '结束事件',
'Create StartEvent': '创建开始事件',
'Create EndEvent': '创建结束事件',
'Create Task': '创建任务',
'Create User Task': '创建用户任务',
'Create Call Activity': '创建调用活动',
'Create Service Task': '创建服务任务',
'Create Gateway': '创建网关',
'Create DataObjectReference': '创建数据对象',
'Create DataStoreReference': '创建数据存储',
'Create Group': '创建分组',
'Create Intermediate/Boundary Event': '创建中间/边界事件',
'Message Start Event': '消息开始事件',
'Timer Start Event': '定时开始事件',
'Conditional Start Event': '条件开始事件',
'Signal Start Event': '信号开始事件',
'Error Start Event': '错误开始事件',
'Escalation Start Event': '升级开始事件',
'Compensation Start Event': '补偿开始事件',
'Message Start Event (non-interrupting)': '消息开始事件(非中断)',
'Timer Start Event (non-interrupting)': '定时开始事件(非中断)',
'Conditional Start Event (non-interrupting)': '条件开始事件(非中断)',
'Signal Start Event (non-interrupting)': '信号开始事件(非中断)',
'Escalation Start Event (non-interrupting)': '升级开始事件(非中断)',
'Message Intermediate Catch Event': '消息中间捕获事件',
'Message Intermediate Throw Event': '消息中间抛出事件',
'Timer Intermediate Catch Event': '定时中间捕获事件',
'Escalation Intermediate Throw Event': '升级中间抛出事件',
'Conditional Intermediate Catch Event': '条件中间捕获事件',
'Link Intermediate Catch Event': '链接中间捕获事件',
'Link Intermediate Throw Event': '链接中间抛出事件',
'Compensation Intermediate Throw Event': '补偿中间抛出事件',
'Signal Intermediate Catch Event': '信号中间捕获事件',
'Signal Intermediate Throw Event': '信号中间抛出事件',
'Message End Event': '消息结束事件',
'Escalation End Event': '定时结束事件',
'Error End Event': '错误结束事件',
'Cancel End Event': '取消结束事件',
'Compensation End Event': '补偿结束事件',
'Signal End Event': '信号结束事件',
'Terminate End Event': '终止结束事件',
'Message Boundary Event': '消息边界事件',
'Message Boundary Event (non-interrupting)': '消息边界事件(非中断)',
'Timer Boundary Event': '定时边界事件',
'Timer Boundary Event (non-interrupting)': '定时边界事件(非中断)',
'Escalation Boundary Event': '升级边界事件',
'Escalation Boundary Event (non-interrupting)': '升级边界事件(非中断)',
'Conditional Boundary Event': '条件边界事件',
'Conditional Boundary Event (non-interrupting)': '条件边界事件(非中断)',
'Error Boundary Event': '错误边界事件',
'Cancel Boundary Event': '取消边界事件',
'Signal Boundary Event': '信号边界事件',
'Signal Boundary Event (non-interrupting)': '信号边界事件(非中断)',
'Compensation Boundary Event': '补偿边界事件',
'Exclusive Gateway': '互斥网关',
'Parallel Gateway': '并行网关',
'Inclusive Gateway': '相容网关',
'Complex Gateway': '复杂网关',
'Event based Gateway': '事件网关',
Transaction: '转运',
'Sub Process': '子流程',
'Event Sub Process': '事件子流程',
'Collapsed Pool': '折叠池',
'Expanded Pool': '展开池',
// Errors
'no parent for {element} in {parent}': '在{parent}里,{element}没有父类',
'no shape type specified': '没有指定的形状类型',
'flow elements must be children of pools/participants':
'流元素必须是池/参与者的子类',
'out of bounds release': 'out of bounds release',
'more than {count} child lanes': '子道大于{count} ',
'element required': '元素不能为空',
'diagram not part of bpmn:Definitions': '流程图不符合bpmn规范',
'no diagram to display': '没有可展示的流程图',
'no process or collaboration to display': '没有可展示的流程/协作',
'element {element} referenced by {referenced}#{property} not yet drawn':
'由{referenced}#{property}引用的{element}元素仍未绘制',
'already rendered {element}': '{element} 已被渲染',
'failed to import {element}': '导入{element}失败',
// 属性面板的参数
Id: '编号',
Name: '名称',
General: '常规',
Details: '详情',
'Message Name': '消息名称',
Message: '消息',
Initiator: '创建者',
'Asynchronous Continuations': '持续异步',
'Asynchronous Before': '异步前',
'Asynchronous After': '异步后',
'Job Configuration': '工作配置',
Exclusive: '排除',
'Job Priority': '工作优先级',
'Retry Time Cycle': '重试时间周期',
Documentation: '文档',
'Element Documentation': '元素文档',
'History Configuration': '历史配置',
'History Time To Live': '历史的生存时间',
Forms: '表单',
'Form Key': '表单key',
'Form Fields': '表单字段',
'Business Key': '业务key',
'Form Field': '表单字段',
ID: '编号',
Type: '类型',
Label: '名称',
'Default Value': '默认值',
'Default Flow': '默认流转路径',
'Conditional Flow': '条件流转路径',
'Sequence Flow': '普通流转路径',
Validation: '校验',
'Add Constraint': '添加约束',
Config: '配置',
Properties: '属性',
'Add Property': '添加属性',
Value: '值',
Listeners: '监听器',
'Execution Listener': '执行监听',
'Event Type': '事件类型',
'Listener Type': '监听器类型',
'Java Class': 'Java类',
Expression: '表达式',
'Must provide a value': '必须提供一个值',
'Delegate Expression': '代理表达式',
Script: '脚本',
'Script Format': '脚本格式',
'Script Type': '脚本类型',
'Inline Script': '内联脚本',
'External Script': '外部脚本',
Resource: '资源',
'Field Injection': '字段注入',
Extensions: '扩展',
'Input/Output': '输入/输出',
'Input Parameters': '输入参数',
'Output Parameters': '输出参数',
Parameters: '参数',
'Output Parameter': '输出参数',
'Timer Definition Type': '定时器定义类型',
'Timer Definition': '定时器定义',
Date: '日期',
Duration: '持续',
Cycle: '循环',
Signal: '信号',
'Signal Name': '信号名称',
Escalation: '升级',
Error: '错误',
'Link Name': '链接名称',
Condition: '条件名称',
'Variable Name': '变量名称',
'Variable Event': '变量事件',
'Specify more than one variable change event as a comma separated list.':
'多个变量事件以逗号隔开',
'Wait for Completion': '等待完成',
'Activity Ref': '活动参考',
'Version Tag': '版本标签',
Executable: '可执行文件',
'External Task Configuration': '扩展任务配置',
'Task Priority': '任务优先级',
External: '外部',
Connector: '连接器',
'Must configure Connector': '必须配置连接器',
'Connector Id': '连接器编号',
Implementation: '实现方式',
'Field Injections': '字段注入',
Fields: '字段',
'Result Variable': '结果变量',
Topic: '主题',
'Configure Connector': '配置连接器',
'Input Parameter': '输入参数',
Assignee: '代理人',
'Candidate Users': '候选用户',
'Candidate Groups': '候选组',
'Due Date': '到期时间',
'Follow Up Date': '跟踪日期',
Priority: '优先级',
'The follow up date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)':
'跟踪日期必须符合EL表达式 ${someDate} ,或者一个ISO标准日期2015-06-26T09:54:00',
'The due date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)':
'跟踪日期必须符合EL表达式 ${someDate} ,或者一个ISO标准日期2015-06-26T09:54:00',
Variables: '变量',
'Candidate Starter Configuration': '候选人起动器配置',
'Candidate Starter Groups': '候选人起动器组',
'This maps to the process definition key.': '这映射到流程定义键。',
'Candidate Starter Users': '候选人起动器的用户',
'Specify more than one user as a comma separated list.':
'指定多个用户作为逗号分隔的列表。',
'Tasklist Configuration': 'Tasklist配置',
Startable: '启动',
'Specify more than one group as a comma separated list.':
'指定多个组作为逗号分隔的列表。',
};

View File

@@ -0,0 +1,9 @@
import './theme/index.scss';
import 'bpmn-js/dist/assets/diagram-js.css';
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css';
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css';
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css';
export { default as MyProcessDesigner } from './designer';
export { default as MyProcessViewer } from './designer/index2';
export { default as MyProcessPenal } from './penal';

View File

@@ -0,0 +1,49 @@
<template>
<div class="my-process-palette">
<div class="test-button" @click="addTask" @mousedown="addTask">
测试任务
</div>
<div class="test-container" id="palette-container">1</div>
</div>
</template>
<script lang="ts" setup>
import { assign } from 'min-dash';
defineOptions({ name: 'MyProcessPalette' });
const bpmnInstances = () => (window as any).bpmnInstances;
const addTask = (event, options: any = {}) => {
const ElementFactory = bpmnInstances().elementFactory;
const create = bpmnInstances().modeler.get('create');
console.log(ElementFactory, create);
const shape = ElementFactory.createShape(
assign({ type: 'bpmn:UserTask' }, options),
);
if (options) {
shape.businessObject.di.isExpanded = options.isExpanded;
}
console.log(event, 'event');
console.log(shape, 'shape');
create.start(event, shape);
};
</script>
<style scoped lang="scss">
.my-process-palette {
box-sizing: border-box;
padding: 80px 20px 20px;
.test-button {
box-sizing: border-box;
padding: 8px 16px;
cursor: pointer;
border: 1px solid rgb(24 144 255 / 80%);
border-radius: 4px;
}
}
</style>

View File

@@ -0,0 +1,358 @@
<template>
<div
class="process-panel__container"
:style="{ width: `${width}px`, maxHeight: '600px' }"
>
<el-collapse v-model="activeTab" v-if="isReady">
<el-collapse-item name="base">
<!-- class="panel-tab__title" -->
<template #title>
<Icon icon="ep:info-filled" />
常规</template
>
<ElementBaseInfo
:id-edit-disabled="idEditDisabled"
:business-object="elementBusinessObject"
:type="elementType"
:model="model"
/>
</el-collapse-item>
<el-collapse-item
name="condition"
v-if="elementType === 'Process'"
key="message"
>
<template #title><Icon icon="ep:comment" />消息与信号</template>
<signal-and-massage />
</el-collapse-item>
<el-collapse-item
name="condition"
v-if="conditionFormVisible"
key="condition"
>
<template #title><Icon icon="ep:promotion" />流转条件</template>
<flow-condition
:business-object="elementBusinessObject"
:type="elementType"
/>
</el-collapse-item>
<el-collapse-item name="condition" v-if="formVisible" key="form">
<template #title><Icon icon="ep:list" />表单</template>
<element-form :id="elementId" :type="elementType" />
</el-collapse-item>
<el-collapse-item
name="task"
v-if="isTaskCollapseItemShow(elementType)"
key="task"
>
<template #title
><Icon icon="ep:checked" />{{
getTaskCollapseItemName(elementType)
}}</template
>
<element-task :id="elementId" :type="elementType" />
</el-collapse-item>
<el-collapse-item
name="multiInstance"
v-if="elementType.indexOf('Task') !== -1"
key="multiInstance"
>
<template #title><Icon icon="ep:help-filled" />多人审批方式</template>
<element-multi-instance
:id="elementId"
:business-object="elementBusinessObject"
:type="elementType"
/>
</el-collapse-item>
<el-collapse-item name="listeners" key="listeners">
<template #title><Icon icon="ep:bell-filled" />执行监听器</template>
<element-listeners :id="elementId" :type="elementType" />
</el-collapse-item>
<el-collapse-item
name="taskListeners"
v-if="elementType === 'UserTask'"
key="taskListeners"
>
<template #title><Icon icon="ep:bell-filled" />任务监听器</template>
<user-task-listeners :id="elementId" :type="elementType" />
</el-collapse-item>
<el-collapse-item name="extensions" key="extensions">
<template #title
><Icon icon="ep:circle-plus-filled" />扩展属性</template
>
<element-properties :id="elementId" :type="elementType" />
</el-collapse-item>
<el-collapse-item name="other" key="other">
<template #title><Icon icon="ep:promotion" />其他</template>
<element-other-config :id="elementId" />
</el-collapse-item>
<el-collapse-item name="customConfig" key="customConfig">
<template #title><Icon icon="ep:tools" />自定义配置</template>
<element-custom-config
:id="elementId"
:type="elementType"
:business-object="elementBusinessObject"
/>
</el-collapse-item>
<!-- 新增的时间事件配置项 -->
<el-collapse-item
v-if="elementType === 'IntermediateCatchEvent'"
name="timeEvent"
>
<template #title><Icon icon="ep:timer" />时间事件</template>
<TimeEventConfig
:businessObject="bpmnElement.value?.businessObject"
:key="elementId"
/>
</el-collapse-item>
</el-collapse>
</div>
</template>
<script lang="ts" setup>
import ElementBaseInfo from './base/ElementBaseInfo.vue';
import ElementOtherConfig from './other/ElementOtherConfig.vue';
import ElementTask from './task/ElementTask.vue';
import ElementMultiInstance from './multi-instance/ElementMultiInstance.vue';
import FlowCondition from './flow-condition/FlowCondition.vue';
import SignalAndMassage from './signal-message/SignalAndMessage.vue';
import ElementListeners from './listeners/ElementListeners.vue';
import ElementProperties from './properties/ElementProperties.vue';
// import ElementForm from './form/ElementForm.vue'
import UserTaskListeners from './listeners/UserTaskListeners.vue';
import { getTaskCollapseItemName, isTaskCollapseItemShow } from './task/data';
import TimeEventConfig from './time-event-config/TimeEventConfig.vue';
import { ref, watch, onMounted } from 'vue';
defineOptions({ name: 'MyPropertiesPanel' });
/**
* 侧边栏
* @Author MiyueFE
* @Home https://github.com/miyuesc
* @Date 2021年3月31日18:57:51
*/
const props = defineProps({
bpmnModeler: {
type: Object,
default: () => {},
},
prefix: {
type: String,
default: 'camunda',
},
width: {
type: Number,
default: 480,
},
idEditDisabled: {
type: Boolean,
default: false,
},
model: Object, // 流程模型的数据
});
const activeTab = ref('base');
const elementId = ref('');
const elementType = ref('');
const elementBusinessObject = ref<any>({}); // 元素 businessObject 镜像,提供给需要做判断的组件使用
const conditionFormVisible = ref(false); // 流转条件设置
const formVisible = ref(false); // 表单配置
const bpmnElement = ref();
const isReady = ref(false);
const type = ref('time');
const condition = ref('');
provide('prefix', props.prefix);
provide('width', props.width);
// 初始化 bpmnInstances
const initBpmnInstances = () => {
if (!props.bpmnModeler) return false;
try {
const instances = {
modeler: props.bpmnModeler,
modeling: props.bpmnModeler.get('modeling'),
moddle: props.bpmnModeler.get('moddle'),
eventBus: props.bpmnModeler.get('eventBus'),
bpmnFactory: props.bpmnModeler.get('bpmnFactory'),
elementFactory: props.bpmnModeler.get('elementFactory'),
elementRegistry: props.bpmnModeler.get('elementRegistry'),
replace: props.bpmnModeler.get('replace'),
selection: props.bpmnModeler.get('selection'),
};
// 检查所有实例是否都存在
const allInstancesExist = Object.values(instances).every(
(instance) => instance,
);
if (allInstancesExist) {
const w = window as any;
w.bpmnInstances = instances;
return true;
}
return false;
} catch (error) {
console.error('初始化 bpmnInstances 失败:', error);
return false;
}
};
const bpmnInstances = () => (window as any)?.bpmnInstances;
// 监听 props.bpmnModeler 然后 initModels
const unwatchBpmn = watch(
() => props.bpmnModeler,
async () => {
// 避免加载时 流程图 并未加载完成
if (!props.bpmnModeler) {
console.log('缺少props.bpmnModeler');
return;
}
try {
// 等待 modeler 初始化完成
await nextTick();
if (initBpmnInstances()) {
isReady.value = true;
await nextTick();
getActiveElement();
} else {
console.error('modeler 实例未完全初始化');
}
} catch (error) {
console.error('初始化失败:', error);
}
},
{
immediate: true,
},
);
const getActiveElement = () => {
if (!isReady.value || !props.bpmnModeler) return;
// 初始第一个选中元素 bpmn:Process
initFormOnChanged(null);
props.bpmnModeler.on('import.done', (e) => {
console.log(e, 'eeeee');
initFormOnChanged(null);
});
// 监听选择事件,修改当前激活的元素以及表单
props.bpmnModeler.on('selection.changed', ({ newSelection }) => {
initFormOnChanged(newSelection[0] || null);
});
props.bpmnModeler.on('element.changed', ({ element }) => {
// 保证 修改 "默认流转路径" 类似需要修改多个元素的事件发生的时候,更新表单的元素与原选中元素不一致。
if (element && element.id === elementId.value) {
initFormOnChanged(element);
}
});
};
// 初始化数据
const initFormOnChanged = (element) => {
if (!isReady.value || !bpmnInstances()) return;
let activatedElement = element;
if (!activatedElement) {
activatedElement =
bpmnInstances().elementRegistry.find(
(el) => el.type === 'bpmn:Process',
) ??
bpmnInstances().elementRegistry.find(
(el) => el.type === 'bpmn:Collaboration',
);
}
if (!activatedElement) return;
try {
console.log(`
----------
select element changed:
id: ${activatedElement.id}
type: ${activatedElement.businessObject.$type}
----------
`);
console.log('businessObject: ', activatedElement.businessObject);
bpmnInstances().bpmnElement = activatedElement;
bpmnElement.value = activatedElement;
elementId.value = activatedElement.id;
elementType.value = activatedElement.type.split(':')[1] || '';
elementBusinessObject.value = JSON.parse(
JSON.stringify(activatedElement.businessObject),
);
conditionFormVisible.value = !!(
elementType.value === 'SequenceFlow' &&
activatedElement.source &&
activatedElement.source.type.indexOf('StartEvent') === -1
);
formVisible.value =
elementType.value === 'UserTask' || elementType.value === 'StartEvent';
} catch (error) {
console.error('初始化表单数据失败:', error);
}
};
onBeforeUnmount(() => {
const w = window as any;
w.bpmnInstances = null;
isReady.value = false;
});
watch(
() => elementId.value,
() => {
activeTab.value = 'base';
},
);
function updateNode() {
const moddle = window.bpmnInstances?.moddle;
const modeling = window.bpmnInstances?.modeling;
const elementRegistry = window.bpmnInstances?.elementRegistry;
if (!moddle || !modeling || !elementRegistry) return;
const element = elementRegistry.get(props.businessObject.id);
if (!element) return;
let timerDef = moddle.create('bpmn:TimerEventDefinition', {});
if (type.value === 'time') {
timerDef.timeDate = moddle.create('bpmn:FormalExpression', {
body: condition.value,
});
} else if (type.value === 'duration') {
timerDef.timeDuration = moddle.create('bpmn:FormalExpression', {
body: condition.value,
});
} else if (type.value === 'cycle') {
timerDef.timeCycle = moddle.create('bpmn:FormalExpression', {
body: condition.value,
});
}
modeling.updateModdleProperties(element, element.businessObject, {
eventDefinitions: [timerDef],
});
}
// 初始化和监听
function syncFromBusinessObject() {
if (props.businessObject) {
const timerDef = (props.businessObject.eventDefinitions || [])[0];
if (timerDef) {
if (timerDef.timeDate) {
type.value = 'time';
condition.value = timerDef.timeDate.body;
} else if (timerDef.timeDuration) {
type.value = 'duration';
condition.value = timerDef.timeDuration.body;
} else if (timerDef.timeCycle) {
type.value = 'cycle';
condition.value = timerDef.timeCycle.body;
}
}
}
}
onMounted(syncFromBusinessObject);
watch(() => props.businessObject, syncFromBusinessObject, { deep: true });
</script>

View File

@@ -0,0 +1,194 @@
<template>
<div class="panel-tab__content">
<el-form label-width="90px" :model="needProps" :rules="rules">
<div v-if="needProps.type == 'bpmn:Process'">
<!-- 如果是 Process 信息的时候使用自定义表单 -->
<el-form-item label="流程标识" prop="id">
<el-input
v-model="needProps.id"
placeholder="请输入流标标识"
:disabled="needProps.id !== undefined && needProps.id.length > 0"
@change="handleKeyUpdate"
/>
</el-form-item>
<el-form-item label="流程名称" prop="name">
<el-input
v-model="needProps.name"
placeholder="请输入流程名称"
clearable
@change="handleNameUpdate"
/>
</el-form-item>
</div>
<div v-else>
<el-form-item label="ID">
<el-input
v-model="elementBaseInfo.id"
clearable
@change="updateBaseInfo('id')"
/>
</el-form-item>
<el-form-item label="名称">
<el-input
v-model="elementBaseInfo.name"
clearable
@change="updateBaseInfo('name')"
/>
</el-form-item>
</div>
</el-form>
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: 'ElementBaseInfo' });
const props = defineProps({
businessObject: {
type: Object,
default: () => {},
},
model: {
type: Object,
default: () => {},
},
});
const needProps = ref<any>({});
const bpmnElement = ref();
const elementBaseInfo = ref<any>({});
// 流程表单的下拉框的数据
// const forms = ref([])
// 流程模型的校验
const rules = reactive({
id: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }],
});
const bpmnInstances = () => (window as any)?.bpmnInstances;
const resetBaseInfo = () => {
console.log(window, 'window');
console.log(bpmnElement.value, 'bpmnElement');
bpmnElement.value = bpmnInstances()?.bpmnElement;
// console.log(bpmnElement.value, 'resetBaseInfo11111111111')
elementBaseInfo.value = bpmnElement.value.businessObject;
needProps.value['type'] = bpmnElement.value.businessObject.$type;
// elementBaseInfo.value['typess'] = bpmnElement.value.businessObject.$type
// elementBaseInfo.value = JSON.parse(JSON.stringify(bpmnElement.value.businessObject))
// console.log(elementBaseInfo.value, 'elementBaseInfo22222222222')
};
const handleKeyUpdate = (value) => {
// 校验 value 的值,只有 XML NCName 通过的情况下,才进行赋值。否则,会导致流程图报错,无法绘制的问题
if (!value) {
return;
}
if (!value.match(/[a-zA-Z_][\-_.0-9a-zA-Z$]*/)) {
console.log('key 不满足 XML NCName 规则,所以不进行赋值');
return;
}
console.log('key 满足 XML NCName 规则,所以进行赋值');
// 在 BPMN 的 XML 中,流程标识 key其实对应的是 id 节点
elementBaseInfo.value['id'] = value;
setTimeout(() => {
updateBaseInfo('id');
}, 100);
};
const handleNameUpdate = (value) => {
console.log(elementBaseInfo, 'elementBaseInfo');
if (!value) {
return;
}
elementBaseInfo.value['name'] = value;
setTimeout(() => {
updateBaseInfo('name');
}, 100);
};
// const handleDescriptionUpdate=(value)=> {
// TODO 芋艿documentation 暂时无法修改,后续在看看
// this.elementBaseInfo['documentation'] = value;
// this.updateBaseInfo('documentation');
// }
const updateBaseInfo = (key) => {
console.log(key, 'key');
// 触发 elementBaseInfo 对应的字段
const attrObj = Object.create(null);
// console.log(attrObj, 'attrObj')
attrObj[key] = elementBaseInfo.value[key];
// console.log(attrObj, 'attrObj111')
// const attrObj = {
// id: elementBaseInfo.value[key]
// // di: { id: `${elementBaseInfo.value[key]}_di` }
// }
// console.log(elementBaseInfo, 'elementBaseInfo11111111111')
needProps.value = { ...elementBaseInfo.value, ...needProps.value };
if (key === 'id') {
// console.log('jinru')
console.log(window, 'window');
console.log(bpmnElement.value, 'bpmnElement');
console.log(toRaw(bpmnElement.value), 'bpmnElement');
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
id: elementBaseInfo.value[key],
di: { id: `${elementBaseInfo.value[key]}_di` },
});
} else {
console.log(attrObj, 'attrObj');
bpmnInstances().modeling.updateProperties(
toRaw(bpmnElement.value),
attrObj,
);
}
};
watch(
() => props.businessObject,
(val) => {
// console.log(val, 'val11111111111111111111')
if (val) {
// nextTick(() => {
resetBaseInfo();
// })
}
},
);
watch(
() => props.model?.key,
(val) => {
// 针对上传的 bpmn 流程图时,保证 key 和 name 的更新
if (val) {
handleKeyUpdate(props.model.key);
handleNameUpdate(props.model.name);
}
},
{
immediate: true,
},
);
// watch(
// () => ({ ...props }),
// (oldVal, newVal) => {
// console.log(oldVal, 'oldVal')
// console.log(newVal, 'newVal')
// if (newVal) {
// needProps.value = newVal
// }
// },
// {
// immediate: true
// }
// )
// 'model.key': {
// immediate: false,
// handler: function (val) {
// this.handleKeyUpdate(val)
// }
// }
onBeforeUnmount(() => {
bpmnElement.value = null;
});
</script>

View File

@@ -0,0 +1,40 @@
<template>
<div class="panel-tab__content">
<component :is="customConfigComponent" v-bind="$props" />
</div>
</template>
<script lang="ts" setup>
import { CustomConfigMap } from './data';
defineOptions({ name: 'ElementCustomConfig' });
const props = defineProps({
id: String,
type: String,
businessObject: {
type: Object,
default: () => {},
},
});
const bpmnInstances = () => (window as any)?.bpmnInstances;
const customConfigComponent = ref<any>(null);
watch(
() => props.businessObject,
() => {
if (props.type && props.businessObject) {
let val = props.type;
if (props.businessObject.eventDefinitions) {
val +=
props.businessObject.eventDefinitions[0]?.$type.split(':')[1] || '';
}
customConfigComponent.value = CustomConfigMap[val]?.componet;
}
},
{ immediate: true },
);
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,286 @@
<template>
<div>
<el-divider content-position="left">审批人超时未处理时</el-divider>
<el-form-item label="启用开关" prop="timeoutHandlerEnable">
<el-switch
v-model="timeoutHandlerEnable"
active-text="开启"
inactive-text="关闭"
@change="timeoutHandlerChange"
/>
</el-form-item>
<el-form-item
label="执行动作"
prop="timeoutHandlerType"
v-if="timeoutHandlerEnable"
>
<el-radio-group
v-model="timeoutHandlerType.value"
@change="onTimeoutHandlerTypeChanged"
>
<el-radio-button
v-for="item in TIMEOUT_HANDLER_TYPES"
:key="item.value"
:value="item.value"
:label="item.label"
/>
</el-radio-group>
</el-form-item>
<el-form-item label="超时时间设置" v-if="timeoutHandlerEnable">
<span class="mr-2">当超过</span>
<el-form-item prop="timeDuration">
<el-input-number
class="mr-2"
:style="{ width: '100px' }"
v-model="timeDuration"
:min="1"
controls-position="right"
@change="
() => {
updateTimeModdle();
updateElementExtensions();
}
"
/>
</el-form-item>
<el-select
v-model="timeUnit"
class="mr-2"
:style="{ width: '100px' }"
@change="onTimeUnitChange"
>
<el-option
v-for="item in TIME_UNIT_TYPES"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
未处理
</el-form-item>
<el-form-item
label="最大提醒次数"
prop="maxRemindCount"
v-if="timeoutHandlerEnable && timeoutHandlerType.value === 1"
>
<el-input-number
v-model="maxRemindCount"
:min="1"
:max="10"
@change="
() => {
updateTimeModdle();
updateElementExtensions();
}
"
/>
</el-form-item>
</div>
</template>
<script lang="ts" setup>
import {
TimeUnitType,
TIME_UNIT_TYPES,
TIMEOUT_HANDLER_TYPES,
} from '@/components/SimpleProcessDesignerV2/src/consts';
import { convertTimeUnit } from '@/components/SimpleProcessDesignerV2/src/utils';
defineOptions({ name: 'ElementCustomConfig4BoundaryEventTimer' });
const props = defineProps({
id: String,
type: String,
});
const prefix = inject('prefix');
const bpmnElement = ref();
const bpmnInstances = () => (window as any)?.bpmnInstances;
const timeoutHandlerEnable = ref(false);
const boundaryEventType = ref();
const timeoutHandlerType = ref({
value: undefined,
});
const timeModdle = ref();
const timeDuration = ref(6);
const timeUnit = ref(TimeUnitType.HOUR);
const maxRemindCount = ref(1);
const elExtensionElements = ref();
const otherExtensions = ref();
const configExtensions = ref([]);
const eventDefinition = ref();
const resetElement = () => {
bpmnElement.value = bpmnInstances().bpmnElement;
eventDefinition.value = bpmnElement.value.businessObject.eventDefinitions[0];
// 获取元素扩展属性 或者 创建扩展属性
elExtensionElements.value =
bpmnElement.value.businessObject?.extensionElements ??
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] });
// 是否开启自定义用户任务超时处理
boundaryEventType.value = elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:BoundaryEventType`,
)?.[0];
if (boundaryEventType.value && boundaryEventType.value.value === 1) {
timeoutHandlerEnable.value = true;
configExtensions.value.push(boundaryEventType.value);
}
// 执行动作
timeoutHandlerType.value = elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:TimeoutHandlerType`,
)?.[0];
if (timeoutHandlerType.value) {
configExtensions.value.push(timeoutHandlerType.value);
if (eventDefinition.value.timeCycle) {
const timeStr = eventDefinition.value.timeCycle.body;
const maxRemindCountStr = timeStr.split('/')[0];
const timeDurationStr = timeStr.split('/')[1];
console.log(maxRemindCountStr);
maxRemindCount.value = parseInt(maxRemindCountStr.slice(1));
timeDuration.value = parseInt(
timeDurationStr.slice(2, timeDurationStr.length - 1),
);
timeUnit.value = convertTimeUnit(
timeDurationStr.slice(timeDurationStr.length - 1),
);
timeModdle.value = eventDefinition.value.timeCycle;
}
if (eventDefinition.value.timeDuration) {
const timeDurationStr = eventDefinition.value.timeDuration.body;
timeDuration.value = parseInt(
timeDurationStr.slice(2, timeDurationStr.length - 1),
);
timeUnit.value = convertTimeUnit(
timeDurationStr.slice(timeDurationStr.length - 1),
);
timeModdle.value = eventDefinition.value.timeDuration;
}
}
// 保留剩余扩展元素,便于后面更新该元素对应属性
otherExtensions.value =
elExtensionElements.value.values?.filter(
(ex) =>
ex.$type !== `${prefix}:BoundaryEventType` &&
ex.$type !== `${prefix}:TimeoutHandlerType`,
) ?? [];
};
const timeoutHandlerChange = (val) => {
timeoutHandlerEnable.value = val;
if (val) {
// 启用自定义用户任务超时处理
// 边界事件类型 --- 超时
boundaryEventType.value = bpmnInstances().moddle.create(
`${prefix}:BoundaryEventType`,
{
value: 1,
},
);
configExtensions.value.push(boundaryEventType.value);
// 超时处理类型
timeoutHandlerType.value = bpmnInstances().moddle.create(
`${prefix}:TimeoutHandlerType`,
{
value: 1,
},
);
configExtensions.value.push(timeoutHandlerType.value);
// 超时时间表达式
timeDuration.value = 6;
timeUnit.value = 2;
maxRemindCount.value = 1;
timeModdle.value = bpmnInstances().moddle.create(`bpmn:Expression`, {
body: 'PT6H',
});
eventDefinition.value.timeDuration = timeModdle.value;
} else {
// 关闭自定义用户任务超时处理
configExtensions.value = [];
delete eventDefinition.value.timeDuration;
delete eventDefinition.value.timeCycle;
}
updateElementExtensions();
};
const onTimeoutHandlerTypeChanged = () => {
maxRemindCount.value = 1;
updateElementExtensions();
updateTimeModdle();
};
const onTimeUnitChange = () => {
// 分钟,默认是 60 分钟
if (timeUnit.value === TimeUnitType.MINUTE) {
timeDuration.value = 60;
}
// 小时,默认是 6 个小时
if (timeUnit.value === TimeUnitType.HOUR) {
timeDuration.value = 6;
}
// 天, 默认 1天
if (timeUnit.value === TimeUnitType.DAY) {
timeDuration.value = 1;
}
updateTimeModdle();
updateElementExtensions();
};
const updateTimeModdle = () => {
if (maxRemindCount.value > 1) {
timeModdle.value.body =
'R' + maxRemindCount.value + '/' + isoTimeDuration();
if (!eventDefinition.value.timeCycle) {
delete eventDefinition.value.timeDuration;
eventDefinition.value.timeCycle = timeModdle.value;
}
} else {
timeModdle.value.body = isoTimeDuration();
if (!eventDefinition.value.timeDuration) {
delete eventDefinition.value.timeCycle;
eventDefinition.value.timeDuration = timeModdle.value;
}
}
};
const isoTimeDuration = () => {
let strTimeDuration = 'PT';
if (timeUnit.value === TimeUnitType.MINUTE) {
strTimeDuration += timeDuration.value + 'M';
}
if (timeUnit.value === TimeUnitType.HOUR) {
strTimeDuration += timeDuration.value + 'H';
}
if (timeUnit.value === TimeUnitType.DAY) {
strTimeDuration += timeDuration.value + 'D';
}
return strTimeDuration;
};
const updateElementExtensions = () => {
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
values: [...otherExtensions.value, ...configExtensions.value],
});
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
extensionElements: extensions,
});
};
watch(
() => props.id,
(val) => {
val &&
val.length &&
nextTick(() => {
resetElement();
});
},
{ immediate: true },
);
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,760 @@
<!-- UserTask 自定义配置
1. 审批人与提交人为同一人时
2. 审批人拒绝时
3. 审批人为空时
4. 操作按钮
5. 字段权限
6. 审批类型
7. 是否需要签名
-->
<template>
<div>
<el-divider content-position="left">审批类型</el-divider>
<el-form-item prop="approveType">
<el-radio-group v-model="approveType.value">
<el-radio
v-for="(item, index) in APPROVE_TYPE"
:key="index"
:value="item.value"
:label="item.value"
>
{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-divider content-position="left">审批人拒绝时</el-divider>
<el-form-item prop="rejectHandlerType">
<el-radio-group
v-model="rejectHandlerType"
:disabled="returnTaskList.length === 0"
@change="updateRejectHandlerType"
>
<div class="flex-col">
<div v-for="(item, index) in REJECT_HANDLER_TYPES" :key="index">
<el-radio
:key="item.value"
:value="item.value"
:label="item.label"
/>
</div>
</div>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="rejectHandlerType == RejectHandlerType.RETURN_USER_TASK"
label="驳回节点"
prop="returnNodeId"
>
<el-select
v-model="returnNodeId"
clearable
style="width: 100%"
@change="updateReturnNodeId"
>
<el-option
v-for="item in returnTaskList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-divider content-position="left">审批人为空时</el-divider>
<el-form-item prop="assignEmptyHandlerType">
<el-radio-group
v-model="assignEmptyHandlerType"
@change="updateAssignEmptyHandlerType"
>
<div class="flex-col">
<div v-for="(item, index) in ASSIGN_EMPTY_HANDLER_TYPES" :key="index">
<el-radio
:key="item.value"
:value="item.value"
:label="item.label"
/>
</div>
</div>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="assignEmptyHandlerType == AssignEmptyHandlerType.ASSIGN_USER"
label="指定用户"
prop="assignEmptyHandlerUserIds"
span="24"
>
<el-select
v-model="assignEmptyUserIds"
clearable
multiple
style="width: 100%"
@change="updateAssignEmptyUserIds"
>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-divider content-position="left">审批人与提交人为同一人时</el-divider>
<el-radio-group
v-model="assignStartUserHandlerType"
@change="updateAssignStartUserHandlerType"
>
<div class="flex-col">
<div
v-for="(item, index) in ASSIGN_START_USER_HANDLER_TYPES"
:key="index"
>
<el-radio :key="item.value" :value="item.value" :label="item.label" />
</div>
</div>
</el-radio-group>
<el-divider content-position="left">操作按钮</el-divider>
<div class="button-setting-pane">
<div class="button-setting-title">
<div class="button-title-label">操作按钮</div>
<div class="button-title-label pl-4">显示名称</div>
<div class="button-title-label">启用</div>
</div>
<div
class="button-setting-item"
v-for="(item, index) in buttonsSettingEl"
:key="index"
>
<div class="button-setting-item-label">
{{ OPERATION_BUTTON_NAME.get(item.id) }}
</div>
<div class="button-setting-item-label">
<input
type="text"
class="editable-title-input"
@blur="btnDisplayNameBlurEvent(index)"
v-mountedFocus
v-model="item.displayName"
:placeholder="item.displayName"
v-if="btnDisplayNameEdit[index]"
/>
<el-button v-else text @click="changeBtnDisplayName(index)"
>{{ item.displayName }} &nbsp;<Icon icon="ep:edit"
/></el-button>
</div>
<div class="button-setting-item-label">
<el-switch v-model="item.enable" />
</div>
</div>
</div>
<el-divider content-position="left">字段权限</el-divider>
<div class="field-setting-pane" v-if="formType === BpmModelFormType.NORMAL">
<div class="field-permit-title">
<div class="setting-title-label first-title">字段名称</div>
<div class="other-titles">
<span
class="setting-title-label cursor-pointer"
@click="updatePermission('READ')"
>只读</span
>
<span
class="setting-title-label cursor-pointer"
@click="updatePermission('WRITE')"
>可编辑</span
>
<span
class="setting-title-label cursor-pointer"
@click="updatePermission('NONE')"
>隐藏</span
>
</div>
</div>
<div
class="field-setting-item"
v-for="(item, index) in fieldsPermissionEl"
:key="index"
>
<div class="field-setting-item-label">{{ item.title }}</div>
<el-radio-group
class="field-setting-item-group"
v-model="item.permission"
>
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.READ"
size="large"
:label="FieldPermissionType.READ"
@change="updateElementExtensions"
>
<span></span>
</el-radio>
</div>
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.WRITE"
size="large"
:label="FieldPermissionType.WRITE"
@change="updateElementExtensions"
>
<span></span>
</el-radio>
</div>
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.NONE"
size="large"
:label="FieldPermissionType.NONE"
@change="updateElementExtensions"
>
<span></span>
</el-radio>
</div>
</el-radio-group>
</div>
</div>
<el-divider content-position="left">是否需要签名</el-divider>
<el-form-item prop="signEnable">
<el-switch
v-model="signEnable.value"
active-text=""
inactive-text=""
@change="updateElementExtensions"
/>
</el-form-item>
<el-divider content-position="left">审批意见</el-divider>
<el-form-item prop="reasonRequire">
<el-switch
v-model="reasonRequire.value"
active-text="必填"
inactive-text="非必填"
@change="updateElementExtensions"
/>
</el-form-item>
</div>
</template>
<script lang="ts" setup>
import {
ASSIGN_START_USER_HANDLER_TYPES,
RejectHandlerType,
REJECT_HANDLER_TYPES,
ASSIGN_EMPTY_HANDLER_TYPES,
AssignEmptyHandlerType,
OPERATION_BUTTON_NAME,
DEFAULT_BUTTON_SETTING,
FieldPermissionType,
APPROVE_TYPE,
ApproveType,
ButtonSetting,
} from '@/components/SimpleProcessDesignerV2/src/consts';
import * as UserApi from '@/api/system/user';
import { useFormFieldsPermission } from '@/components/SimpleProcessDesignerV2/src/node';
import { BpmModelFormType } from '@/utils/constants';
defineOptions({ name: 'ElementCustomConfig4UserTask' });
const props = defineProps({
id: String,
type: String,
});
const prefix = inject('prefix');
// 审批人与提交人为同一人时
const assignStartUserHandlerTypeEl = ref();
const assignStartUserHandlerType = ref();
// 审批人拒绝时
const rejectHandlerTypeEl = ref();
const rejectHandlerType = ref();
const returnNodeIdEl = ref();
const returnNodeId = ref();
const returnTaskList = ref([]);
// 审批人为空时
const assignEmptyHandlerTypeEl = ref();
const assignEmptyHandlerType = ref();
const assignEmptyUserIdsEl = ref();
const assignEmptyUserIds = ref();
// 操作按钮
const buttonsSettingEl = ref();
const { btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } =
useButtonsSetting();
// 字段权限
const fieldsPermissionEl = ref([]);
const { formType, fieldsPermissionConfig, getNodeConfigFormFields } =
useFormFieldsPermission(FieldPermissionType.READ);
// 审批类型
const approveType = ref({ value: ApproveType.USER });
// 是否需要签名
const signEnable = ref({ value: false });
// 审批意见
const reasonRequire = ref({ value: false });
const elExtensionElements = ref();
const otherExtensions = ref();
const bpmnElement = ref();
const bpmnInstances = () => (window as any)?.bpmnInstances;
const resetCustomConfigList = () => {
bpmnElement.value = bpmnInstances().bpmnElement;
// 获取可回退的列表
returnTaskList.value = findAllPredecessorsExcludingStart(
bpmnElement.value.id,
bpmnInstances().modeler,
);
// 获取元素扩展属性 或者 创建扩展属性
elExtensionElements.value =
bpmnElement.value.businessObject?.extensionElements ??
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] });
// 审批类型
approveType.value =
elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:ApproveType`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:ApproveType`, {
value: ApproveType.USER,
});
// 审批人与提交人为同一人时
assignStartUserHandlerTypeEl.value =
elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:AssignStartUserHandlerType`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:AssignStartUserHandlerType`, {
value: 1,
});
assignStartUserHandlerType.value = assignStartUserHandlerTypeEl.value.value;
// 审批人拒绝时
rejectHandlerTypeEl.value =
elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:RejectHandlerType`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:RejectHandlerType`, { value: 1 });
rejectHandlerType.value = rejectHandlerTypeEl.value.value;
returnNodeIdEl.value =
elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:RejectReturnTaskId`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:RejectReturnTaskId`, {
value: '',
});
returnNodeId.value = returnNodeIdEl.value.value;
// 审批人为空时
assignEmptyHandlerTypeEl.value =
elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:AssignEmptyHandlerType`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:AssignEmptyHandlerType`, {
value: 1,
});
assignEmptyHandlerType.value = assignEmptyHandlerTypeEl.value.value;
assignEmptyUserIdsEl.value =
elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:AssignEmptyUserIds`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:AssignEmptyUserIds`, {
value: '',
});
assignEmptyUserIds.value = assignEmptyUserIdsEl.value.value
?.split(',')
.map((item) => {
// 如果数字超出了最大安全整数范围,则将其作为字符串处理
let num = Number(item);
return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER
? item
: num;
});
// 操作按钮
buttonsSettingEl.value = elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:ButtonsSetting`,
);
if (buttonsSettingEl.value.length === 0) {
DEFAULT_BUTTON_SETTING.forEach((item) => {
buttonsSettingEl.value.push(
bpmnInstances().moddle.create(`${prefix}:ButtonsSetting`, {
'flowable:id': item.id,
'flowable:displayName': item.displayName,
'flowable:enable': item.enable,
}),
);
});
}
// 字段权限
if (formType.value === BpmModelFormType.NORMAL) {
const fieldsPermissionList = elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:FieldsPermission`,
);
fieldsPermissionEl.value = [];
getNodeConfigFormFields();
fieldsPermissionConfig.value = fieldsPermissionConfig.value;
fieldsPermissionConfig.value.forEach((element) => {
element.permission =
fieldsPermissionList?.find((obj) => obj.field === element.field)
?.permission ?? '1';
fieldsPermissionEl.value.push(
bpmnInstances().moddle.create(`${prefix}:FieldsPermission`, element),
);
});
}
// 是否需要签名
signEnable.value =
elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:SignEnable`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:SignEnable`, { value: false });
// 审批意见
reasonRequire.value =
elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:ReasonRequire`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:ReasonRequire`, { value: false });
// 保留剩余扩展元素,便于后面更新该元素对应属性
otherExtensions.value =
elExtensionElements.value.values?.filter(
(ex) =>
ex.$type !== `${prefix}:AssignStartUserHandlerType` &&
ex.$type !== `${prefix}:RejectHandlerType` &&
ex.$type !== `${prefix}:RejectReturnTaskId` &&
ex.$type !== `${prefix}:AssignEmptyHandlerType` &&
ex.$type !== `${prefix}:AssignEmptyUserIds` &&
ex.$type !== `${prefix}:ButtonsSetting` &&
ex.$type !== `${prefix}:FieldsPermission` &&
ex.$type !== `${prefix}:ApproveType` &&
ex.$type !== `${prefix}:SignEnable` &&
ex.$type !== `${prefix}:ReasonRequire`,
) ?? [];
// 更新元素扩展属性,避免后续报错
updateElementExtensions();
};
const updateAssignStartUserHandlerType = () => {
assignStartUserHandlerTypeEl.value.value = assignStartUserHandlerType.value;
updateElementExtensions();
};
const updateRejectHandlerType = () => {
rejectHandlerTypeEl.value.value = rejectHandlerType.value;
returnNodeId.value = returnTaskList.value[0].id;
returnNodeIdEl.value.value = returnNodeId.value;
updateElementExtensions();
};
const updateReturnNodeId = () => {
returnNodeIdEl.value.value = returnNodeId.value;
updateElementExtensions();
};
const updateAssignEmptyHandlerType = () => {
assignEmptyHandlerTypeEl.value.value = assignEmptyHandlerType.value;
updateElementExtensions();
};
const updateAssignEmptyUserIds = () => {
assignEmptyUserIdsEl.value.value = assignEmptyUserIds.value.toString();
updateElementExtensions();
};
const updateElementExtensions = () => {
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
values: [
...otherExtensions.value,
assignStartUserHandlerTypeEl.value,
rejectHandlerTypeEl.value,
returnNodeIdEl.value,
assignEmptyHandlerTypeEl.value,
assignEmptyUserIdsEl.value,
approveType.value,
...buttonsSettingEl.value,
...fieldsPermissionEl.value,
signEnable.value,
reasonRequire.value,
],
});
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
extensionElements: extensions,
});
};
watch(
() => props.id,
(val) => {
val &&
val.length &&
nextTick(() => {
resetCustomConfigList();
});
},
{ immediate: true },
);
function findAllPredecessorsExcludingStart(elementId, modeler) {
const elementRegistry = modeler.get('elementRegistry');
const allConnections = elementRegistry.filter(
(element) => element.type === 'bpmn:SequenceFlow',
);
const predecessors = new Set(); // 使用 Set 来避免重复节点
const visited = new Set(); // 用于记录已访问的节点
// 检查是否是开始事件节点
function isStartEvent(element) {
return element.type === 'bpmn:StartEvent';
}
function findPredecessorsRecursively(element) {
// 如果该节点已经访问过,直接返回,避免循环
if (visited.has(element)) {
return;
}
// 标记当前节点为已访问
visited.add(element);
// 获取与当前节点相连的所有连接
const incomingConnections = allConnections.filter(
(connection) => connection.target === element,
);
incomingConnections.forEach((connection) => {
const source = connection.source; // 获取前置节点
// 只添加不是开始事件的前置节点
if (!isStartEvent(source)) {
predecessors.add(source.businessObject);
// 递归查找前置节点
findPredecessorsRecursively(source);
}
});
}
const targetElement = elementRegistry.get(elementId);
if (targetElement) {
findPredecessorsRecursively(targetElement);
}
return Array.from(predecessors); // 返回前置节点数组
}
function useButtonsSetting() {
const buttonsSetting = ref<ButtonSetting[]>();
// 操作按钮显示名称可编辑
const btnDisplayNameEdit = ref<boolean[]>([]);
const changeBtnDisplayName = (index: number) => {
btnDisplayNameEdit.value[index] = true;
};
const btnDisplayNameBlurEvent = (index: number) => {
btnDisplayNameEdit.value[index] = false;
const buttonItem = buttonsSetting.value![index];
buttonItem.displayName =
buttonItem.displayName || OPERATION_BUTTON_NAME.get(buttonItem.id)!;
};
return {
buttonsSetting,
btnDisplayNameEdit,
changeBtnDisplayName,
btnDisplayNameBlurEvent,
};
}
/** 批量更新权限 */
// TODO @lesan这个页面有一些 idea 红色报错,咱要不要 fix 下!
const updatePermission = (type: string) => {
fieldsPermissionEl.value.forEach((field) => {
field.permission =
type === 'READ'
? FieldPermissionType.READ
: type === 'WRITE'
? FieldPermissionType.WRITE
: FieldPermissionType.NONE;
});
};
const userOptions = ref<UserApi.UserVO[]>([]); // 用户列表
onMounted(async () => {
// 获得用户列表
userOptions.value = await UserApi.getSimpleUserList();
});
</script>
<style lang="scss" scoped>
.button-setting-pane {
display: flex;
flex-direction: column;
margin-top: 8px;
font-size: 14px;
.button-setting-desc {
padding-right: 8px;
margin-bottom: 16px;
font-size: 16px;
font-weight: 700;
}
.button-setting-title {
display: flex;
align-items: center;
justify-content: space-between;
height: 45px;
padding-left: 12px;
background-color: #f8fafc0a;
border: 1px solid #1f38581a;
& > :first-child {
width: 100px !important;
text-align: left !important;
}
& > :last-child {
text-align: center !important;
}
.button-title-label {
width: 150px;
font-size: 13px;
font-weight: 700;
color: #000;
text-align: left;
}
}
.button-setting-item {
display: flex;
align-items: center;
justify-content: space-between;
height: 38px;
padding-left: 12px;
border: 1px solid #1f38581a;
border-top: 0;
& > :first-child {
width: 100px !important;
}
& > :last-child {
text-align: center !important;
}
.button-setting-item-label {
width: 150px;
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
white-space: nowrap;
}
.editable-title-input {
max-width: 130px;
height: 24px;
margin-left: 4px;
line-height: 24px;
border: 1px solid #d9d9d9;
border-radius: 4px;
transition: all 0.3s;
&:focus {
outline: 0;
border-color: #40a9ff;
box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
}
}
}
}
.field-setting-pane {
display: flex;
flex-direction: column;
font-size: 14px;
.field-setting-desc {
padding-right: 8px;
margin-bottom: 16px;
font-size: 16px;
font-weight: 700;
}
.field-permit-title {
display: flex;
align-items: center;
justify-content: space-between;
height: 45px;
padding-left: 12px;
line-height: 45px;
background-color: #f8fafc0a;
border: 1px solid #1f38581a;
.first-title {
text-align: left !important;
}
.other-titles {
display: flex;
justify-content: space-between;
}
.setting-title-label {
display: inline-block;
width: 100px;
padding: 5px 0;
font-size: 13px;
font-weight: 700;
color: #000;
text-align: center;
}
}
.field-setting-item {
display: flex;
align-items: center;
justify-content: space-between;
height: 38px;
padding-left: 12px;
border: 1px solid #1f38581a;
border-top: 0;
.field-setting-item-label {
display: inline-block;
width: 100px;
min-height: 16px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: text;
}
.field-setting-item-group {
display: flex;
justify-content: space-between;
.item-radio-wrap {
display: inline-block;
width: 100px;
text-align: center;
}
}
}
}
</style>

View File

@@ -0,0 +1,13 @@
import BoundaryEventTimer from './components/BoundaryEventTimer.vue';
import UserTaskCustomConfig from './components/UserTaskCustomConfig.vue';
export const CustomConfigMap = {
UserTask: {
name: '用户任务',
componet: UserTaskCustomConfig,
},
BoundaryEventTimerEventDefinition: {
name: '定时边界事件(非中断)',
componet: BoundaryEventTimer,
},
};

View File

@@ -0,0 +1,218 @@
<template>
<div class="panel-tab__content">
<el-form :model="flowConditionForm" label-width="90px" size="small">
<el-form-item label="流转类型">
<el-select v-model="flowConditionForm.type" @change="updateFlowType">
<el-option label="普通流转路径" value="normal" />
<el-option label="默认流转路径" value="default" />
<el-option label="条件流转路径" value="condition" />
</el-select>
</el-form-item>
<el-form-item
label="条件格式"
v-if="flowConditionForm.type === 'condition'"
key="condition"
>
<el-select v-model="flowConditionForm.conditionType">
<el-option label="表达式" value="expression" />
<el-option label="脚本" value="script" />
</el-select>
</el-form-item>
<el-form-item
label="表达式"
v-if="
flowConditionForm.conditionType &&
flowConditionForm.conditionType === 'expression'
"
key="express"
>
<el-input
v-model="flowConditionForm.body"
style="width: 192px"
clearable
@change="updateFlowCondition"
/>
</el-form-item>
<template
v-if="
flowConditionForm.conditionType &&
flowConditionForm.conditionType === 'script'
"
>
<el-form-item label="脚本语言" key="language">
<el-input
v-model="flowConditionForm.language"
clearable
@change="updateFlowCondition"
/>
</el-form-item>
<el-form-item label="脚本类型" key="scriptType">
<el-select v-model="flowConditionForm.scriptType">
<el-option label="内联脚本" value="inlineScript" />
<el-option label="外部脚本" value="externalScript" />
</el-select>
</el-form-item>
<el-form-item
label="脚本"
v-if="flowConditionForm.scriptType === 'inlineScript'"
key="body"
>
<el-input
v-model="flowConditionForm.body"
type="textarea"
clearable
@change="updateFlowCondition"
/>
</el-form-item>
<el-form-item
label="资源地址"
v-if="flowConditionForm.scriptType === 'externalScript'"
key="resource"
>
<el-input
v-model="flowConditionForm.resource"
clearable
@change="updateFlowCondition"
/>
</el-form-item>
</template>
</el-form>
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: 'FlowCondition' });
const props = defineProps({
businessObject: Object,
type: String,
});
const flowConditionForm = ref<any>({});
const bpmnElement = ref();
const bpmnElementSource = ref();
const bpmnElementSourceRef = ref();
const flowConditionRef = ref();
const bpmnInstances = () => (window as any)?.bpmnInstances;
const resetFlowCondition = () => {
bpmnElement.value = bpmnInstances().bpmnElement;
bpmnElementSource.value = bpmnElement.value.source;
bpmnElementSourceRef.value = bpmnElement.value.businessObject.sourceRef;
// 初始化默认type为default
flowConditionForm.value = { type: 'default' };
if (
bpmnElementSourceRef.value &&
bpmnElementSourceRef.value.default &&
bpmnElementSourceRef.value.default.id === bpmnElement.value.id
) {
flowConditionForm.value = { type: 'default' };
} else if (!bpmnElement.value.businessObject.conditionExpression) {
// 普通
flowConditionForm.value = { type: 'normal' };
} else {
// 带条件
const conditionExpression =
bpmnElement.value.businessObject.conditionExpression;
flowConditionForm.value = { ...conditionExpression, type: 'condition' };
// resource 可直接标识 是否是外部资源脚本
if (flowConditionForm.value.resource) {
// this.$set(this.flowConditionForm, "conditionType", "script");
// this.$set(this.flowConditionForm, "scriptType", "externalScript");
flowConditionForm.value['conditionType'] = 'script';
flowConditionForm.value['scriptType'] = 'externalScript';
return;
}
if (conditionExpression.language) {
// this.$set(this.flowConditionForm, "conditionType", "script");
// this.$set(this.flowConditionForm, "scriptType", "inlineScript");
flowConditionForm.value['conditionType'] = 'script';
flowConditionForm.value['scriptType'] = 'inlineScript';
return;
}
// this.$set(this.flowConditionForm, "conditionType", "expression");
flowConditionForm.value['conditionType'] = 'expression';
}
};
const updateFlowType = (flowType) => {
// 正常条件类
if (flowType === 'condition') {
flowConditionRef.value = bpmnInstances().moddle.create(
'bpmn:FormalExpression',
);
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
conditionExpression: flowConditionRef.value,
});
return;
}
// 默认路径
if (flowType === 'default') {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
conditionExpression: null,
});
bpmnInstances().modeling.updateProperties(toRaw(bpmnElementSource.value), {
default: toRaw(bpmnElement.value),
});
return;
}
// 正常路径,如果来源节点的默认路径是当前连线时,清除父元素的默认路径配置
if (
bpmnElementSourceRef.value.default &&
bpmnElementSourceRef.value.default.id === bpmnElement.value.id
) {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElementSource.value), {
default: null,
});
}
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
conditionExpression: null,
});
};
const updateFlowCondition = () => {
let { conditionType, scriptType, body, resource, language } =
flowConditionForm.value;
let condition;
if (conditionType === 'expression') {
condition = bpmnInstances().moddle.create('bpmn:FormalExpression', {
body,
});
} else {
if (scriptType === 'inlineScript') {
condition = bpmnInstances().moddle.create('bpmn:FormalExpression', {
body,
language,
});
// this.$set(this.flowConditionForm, "resource", "");
flowConditionForm.value['resource'] = '';
} else {
// this.$set(this.flowConditionForm, "body", "");
flowConditionForm.value['body'] = '';
condition = bpmnInstances().moddle.create('bpmn:FormalExpression', {
resource,
language,
});
}
}
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
conditionExpression: condition,
});
};
onBeforeUnmount(() => {
bpmnElement.value = null;
bpmnElementSource.value = null;
bpmnElementSourceRef.value = null;
});
watch(
() => props.businessObject,
(val) => {
console.log(val, 'val');
nextTick(() => {
resetFlowCondition();
});
},
{
immediate: true,
},
);
</script>

View File

@@ -0,0 +1,518 @@
<template>
<div class="panel-tab__content">
<el-form label-width="80px">
<el-form-item label="流程表单">
<!-- <el-input v-model="formKey" clearable @change="updateElementFormKey" />-->
<el-select v-model="formKey" clearable @change="updateElementFormKey">
<el-option
v-for="form in formList"
:key="form.id"
:label="form.name"
:value="form.id"
/>
</el-select>
</el-form-item>
<!-- <el-form-item label="业务标识">-->
<!-- <el-select v-model="businessKey" @change="updateElementBusinessKey">-->
<!-- <el-option v-for="i in fieldList" :key="i.id" :value="i.id" :label="i.label" />-->
<!-- <el-option label="无" value="" />-->
<!-- </el-select>-->
<!-- </el-form-item>-->
</el-form>
<!--字段列表-->
<!-- <div class="element-property list-property">-->
<!-- <el-divider><Icon icon="ep:coin" /> 表单字段</el-divider>-->
<!-- <el-table :data="fieldList" max-height="240" fit border>-->
<!-- <el-table-column label="序号" type="index" width="50px" />-->
<!-- <el-table-column label="字段名称" prop="label" min-width="80px" show-overflow-tooltip />-->
<!-- <el-table-column-->
<!-- label="字段类型"-->
<!-- prop="type"-->
<!-- min-width="80px"-->
<!-- :formatter="(row) => fieldType[row.type] || row.type"-->
<!-- show-overflow-tooltip-->
<!-- />-->
<!-- <el-table-column-->
<!-- label="默认值"-->
<!-- prop="defaultValue"-->
<!-- min-width="80px"-->
<!-- show-overflow-tooltip-->
<!-- />-->
<!-- <el-table-column label="操作" width="90px">-->
<!-- <template #default="scope">-->
<!-- <el-button type="primary" link @click="openFieldForm(scope, scope.$index)"-->
<!-- >编辑</el-button-->
<!-- >-->
<!-- <el-divider direction="vertical" />-->
<!-- <el-button-->
<!-- type="primary"-->
<!-- link-->
<!-- style="color: #ff4d4f"-->
<!-- @click="removeField(scope, scope.$index)"-->
<!-- >移除</el-button-->
<!-- >-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- </el-table>-->
<!-- </div>-->
<!-- <div class="element-drawer__button">-->
<!-- <XButton type="primary" proIcon="ep:plus" title="添加字段" @click="openFieldForm(null, -1)" />-->
<!-- </div>-->
<!--字段配置侧边栏-->
<!-- <el-drawer-->
<!-- v-model="fieldModelVisible"-->
<!-- title="字段配置"-->
<!-- :size="`${width}px`"-->
<!-- append-to-body-->
<!-- destroy-on-close-->
<!-- >-->
<!-- <el-form :model="formFieldForm" label-width="90px">-->
<!-- <el-form-item label="字段ID">-->
<!-- <el-input v-model="formFieldForm.id" clearable />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="类型">-->
<!-- <el-select-->
<!-- v-model="formFieldForm.typeType"-->
<!-- placeholder="请选择字段类型"-->
<!-- clearable-->
<!-- @change="changeFieldTypeType"-->
<!-- >-->
<!-- <el-option v-for="(value, key) of fieldType" :label="value" :value="key" :key="key" />-->
<!-- </el-select>-->
<!-- </el-form-item>-->
<!-- <el-form-item label="类型名称" v-if="formFieldForm.typeType === 'custom'">-->
<!-- <el-input v-model="formFieldForm.type" clearable />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="名称">-->
<!-- <el-input v-model="formFieldForm.label" clearable />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="时间格式" v-if="formFieldForm.typeType === 'date'">-->
<!-- <el-input v-model="formFieldForm.datePattern" clearable />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="默认值">-->
<!-- <el-input v-model="formFieldForm.defaultValue" clearable />-->
<!-- </el-form-item>-->
<!-- </el-form>-->
<!-- &lt;!&ndash; 枚举值设置 &ndash;&gt;-->
<!-- <template v-if="formFieldForm.type === 'enum'">-->
<!-- <el-divider key="enum-divider" />-->
<!-- <p class="listener-filed__title" key="enum-title">-->
<!-- <span><Icon icon="ep:menu" />枚举值列表</span>-->
<!-- <el-button type="primary" @click="openFieldOptionForm(null, -1, 'enum')"-->
<!-- >添加枚举值</el-button-->
<!-- >-->
<!-- </p>-->
<!-- <el-table :data="fieldEnumList" key="enum-table" max-height="240" fit border>-->
<!-- <el-table-column label="序号" width="50px" type="index" />-->
<!-- <el-table-column label="枚举值编号" prop="id" min-width="100px" show-overflow-tooltip />-->
<!-- <el-table-column label="枚举值名称" prop="name" min-width="100px" show-overflow-tooltip />-->
<!-- <el-table-column label="操作" width="90px">-->
<!-- <template #default="scope">-->
<!-- <el-button-->
<!-- type="primary"-->
<!-- link-->
<!-- @click="openFieldOptionForm(scope, scope.$index, 'enum')"-->
<!-- >编辑</el-button-->
<!-- >-->
<!-- <el-divider direction="vertical" />-->
<!-- <el-button-->
<!-- type="primary"-->
<!-- link-->
<!-- style="color: #ff4d4f"-->
<!-- @click="removeFieldOptionItem(scope, scope.$index, 'enum')"-->
<!-- >移除</el-button-->
<!-- >-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- </el-table>-->
<!-- </template>-->
<!-- &lt;!&ndash; 校验规则 &ndash;&gt;-->
<!-- <el-divider key="validation-divider" />-->
<!-- <p class="listener-filed__title" key="validation-title">-->
<!-- <span><Icon icon="ep:menu" />约束条件列表</span>-->
<!-- <el-button type="primary" @click="openFieldOptionForm(null, -1, 'constraint')"-->
<!-- >添加约束</el-button-->
<!-- >-->
<!-- </p>-->
<!-- <el-table :data="fieldConstraintsList" key="validation-table" max-height="240" fit border>-->
<!-- <el-table-column label="序号" width="50px" type="index" />-->
<!-- <el-table-column label="约束名称" prop="name" min-width="100px" show-overflow-tooltip />-->
<!-- <el-table-column label="约束配置" prop="config" min-width="100px" show-overflow-tooltip />-->
<!-- <el-table-column label="操作" width="90px">-->
<!-- <template #default="scope">-->
<!-- <el-button-->
<!-- type="primary"-->
<!-- link-->
<!-- @click="openFieldOptionForm(scope, scope.$index, 'constraint')"-->
<!-- >编辑</el-button-->
<!-- >-->
<!-- <el-divider direction="vertical" />-->
<!-- <el-button-->
<!-- type="primary"-->
<!-- link-->
<!-- style="color: #ff4d4f"-->
<!-- @click="removeFieldOptionItem(scope, scope.$index, 'constraint')"-->
<!-- >移除</el-button-->
<!-- >-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- </el-table>-->
<!-- &lt;!&ndash; 表单属性 &ndash;&gt;-->
<!-- <el-divider key="property-divider" />-->
<!-- <p class="listener-filed__title" key="property-title">-->
<!-- <span><Icon icon="ep:menu" />字段属性列表</span>-->
<!-- <el-button type="primary" @click="openFieldOptionForm(null, -1, 'property')"-->
<!-- >添加属性</el-button-->
<!-- >-->
<!-- </p>-->
<!-- <el-table :data="fieldPropertiesList" key="property-table" max-height="240" fit border>-->
<!-- <el-table-column label="序号" width="50px" type="index" />-->
<!-- <el-table-column label="属性编号" prop="id" min-width="100px" show-overflow-tooltip />-->
<!-- <el-table-column label="属性值" prop="value" min-width="100px" show-overflow-tooltip />-->
<!-- <el-table-column label="操作" width="90px">-->
<!-- <template #default="scope">-->
<!-- <el-button-->
<!-- type="primary"-->
<!-- link-->
<!-- @click="openFieldOptionForm(scope, scope.$index, 'property')"-->
<!-- >编辑</el-button-->
<!-- >-->
<!-- <el-divider direction="vertical" />-->
<!-- <el-button-->
<!-- type="primary"-->
<!-- link-->
<!-- style="color: #ff4d4f"-->
<!-- @click="removeFieldOptionItem(scope, scope.$index, 'property')"-->
<!-- >移除</el-button-->
<!-- >-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- </el-table>-->
<!-- &lt;!&ndash; 底部按钮 &ndash;&gt;-->
<!-- <div class="element-drawer__button">-->
<!-- <el-button> </el-button>-->
<!-- <el-button type="primary" @click="saveField"> </el-button>-->
<!-- </div>-->
<!-- </el-drawer>-->
<!-- <el-dialog-->
<!-- v-model="fieldOptionModelVisible"-->
<!-- :title="optionModelTitle"-->
<!-- width="600px"-->
<!-- append-to-body-->
<!-- destroy-on-close-->
<!-- >-->
<!-- <el-form :model="fieldOptionForm" label-width="96px">-->
<!-- <el-form-item label="编号/ID" v-if="fieldOptionType !== 'constraint'" key="option-id">-->
<!-- <el-input v-model="fieldOptionForm.id" clearable />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="名称" v-if="fieldOptionType !== 'property'" key="option-name">-->
<!-- <el-input v-model="fieldOptionForm.name" clearable />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="配置" v-if="fieldOptionType === 'constraint'" key="option-config">-->
<!-- <el-input v-model="fieldOptionForm.config" clearable />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="值" v-if="fieldOptionType === 'property'" key="option-value">-->
<!-- <el-input v-model="fieldOptionForm.value" clearable />-->
<!-- </el-form-item>-->
<!-- </el-form>-->
<!-- <template #footer>-->
<!-- <el-button @click="fieldOptionModelVisible = false"> </el-button>-->
<!-- <el-button type="primary" @click="saveFieldOption"> </el-button>-->
<!-- </template>-->
<!-- </el-dialog>-->
</div>
</template>
<script lang="ts" setup>
import * as FormApi from '@/api/bpm/form';
defineOptions({ name: 'ElementForm' });
const props = defineProps({
id: String,
type: String,
});
const prefix = inject('prefix');
const width = inject('width');
const formKey = ref(undefined);
const businessKey = ref('');
const optionModelTitle = ref('');
const fieldList = ref<any[]>([]);
const formFieldForm = ref<any>({});
const fieldType = ref({
long: '长整型',
string: '字符串',
boolean: '布尔类',
date: '日期类',
enum: '枚举类',
custom: '自定义类型',
});
const formFieldIndex = ref(-1); // 编辑中的字段, -1 为新增
const formFieldOptionIndex = ref(-1); // 编辑中的字段配置项, -1 为新增
const fieldModelVisible = ref(false);
const fieldOptionModelVisible = ref(false);
const fieldOptionForm = ref<any>({}); // 当前激活的字段配置项数据
const fieldOptionType = ref(''); // 当前激活的字段配置项弹窗 类型
const fieldEnumList = ref<any[]>([]); // 枚举值列表
const fieldConstraintsList = ref<any[]>([]); // 约束条件列表
const fieldPropertiesList = ref<any[]>([]); // 绑定属性列表
const bpmnELement = ref();
const elExtensionElements = ref();
const formData = ref();
const otherExtensions = ref();
const bpmnInstances = () => (window as any)?.bpmnInstances;
const resetFormList = () => {
bpmnELement.value = bpmnInstances().bpmnElement;
formKey.value = bpmnELement.value.businessObject.formKey;
// if (formKey.value?.length > 0) {
// formKey.value = parseInt(formKey.value)
// }
// 获取元素扩展属性 或者 创建扩展属性
elExtensionElements.value =
bpmnELement.value.businessObject.get('extensionElements') ||
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] });
// 获取元素表单配置 或者 创建新的表单配置
formData.value =
elExtensionElements.value.values.filter(
(ex) => ex.$type === `${prefix}:FormData`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:FormData`, { fields: [] });
// 业务标识 businessKey 绑定在 formData 中
businessKey.value = formData.value.businessKey;
// 保留剩余扩展元素,便于后面更新该元素对应属性
otherExtensions.value = elExtensionElements.value.values.filter(
(ex) => ex.$type !== `${prefix}:FormData`,
);
// 复制原始值,填充表格
fieldList.value = JSON.parse(JSON.stringify(formData.value.fields || []));
// 更新元素扩展属性,避免后续报错
updateElementExtensions();
};
const updateElementFormKey = () => {
bpmnInstances().modeling.updateProperties(toRaw(bpmnELement.value), {
formKey: formKey.value,
});
};
const updateElementBusinessKey = () => {
bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnELement.value),
formData.value,
{
businessKey: businessKey.value,
},
);
};
// 根据类型调整字段type
const changeFieldTypeType = (type) => {
// this.$set(this.formFieldForm, "type", type === "custom" ? "" : type);
formFieldForm.value['type'] = type === 'custom' ? '' : type;
};
// 打开字段详情侧边栏
const openFieldForm = (field, index) => {
formFieldIndex.value = index;
if (index !== -1) {
const FieldObject = formData.value.fields[index];
formFieldForm.value = JSON.parse(JSON.stringify(field));
// 设置自定义类型
// this.$set(this.formFieldForm, "typeType", !this.fieldType[field.type] ? "custom" : field.type);
formFieldForm.value['typeType'] = !fieldType.value[field.type]
? 'custom'
: field.type;
// 初始化枚举值列表
field.type === 'enum' &&
(fieldEnumList.value = JSON.parse(
JSON.stringify(FieldObject?.values || []),
));
// 初始化约束条件列表
fieldConstraintsList.value = JSON.parse(
JSON.stringify(FieldObject?.validation?.constraints || []),
);
// 初始化自定义属性列表
fieldPropertiesList.value = JSON.parse(
JSON.stringify(FieldObject?.properties?.values || []),
);
} else {
formFieldForm.value = {};
// 初始化枚举值列表
fieldEnumList.value = [];
// 初始化约束条件列表
fieldConstraintsList.value = [];
// 初始化自定义属性列表
fieldPropertiesList.value = [];
}
fieldModelVisible.value = true;
};
// 打开字段 某个 配置项 弹窗
const openFieldOptionForm = (option, index, type) => {
fieldOptionModelVisible.value = true;
fieldOptionType.value = type;
formFieldOptionIndex.value = index;
if (type === 'property') {
fieldOptionForm.value = option ? JSON.parse(JSON.stringify(option)) : {};
return (optionModelTitle.value = '属性配置');
}
if (type === 'enum') {
fieldOptionForm.value = option ? JSON.parse(JSON.stringify(option)) : {};
return (optionModelTitle.value = '枚举值配置');
}
fieldOptionForm.value = option ? JSON.parse(JSON.stringify(option)) : {};
return (optionModelTitle.value = '约束条件配置');
};
// 保存字段 某个 配置项
const saveFieldOption = () => {
if (formFieldOptionIndex.value === -1) {
if (fieldOptionType.value === 'property') {
fieldPropertiesList.value.push(fieldOptionForm.value);
}
if (fieldOptionType.value === 'constraint') {
fieldConstraintsList.value.push(fieldOptionForm.value);
}
if (fieldOptionType.value === 'enum') {
fieldEnumList.value.push(fieldOptionForm.value);
}
} else {
fieldOptionType.value === 'property' &&
fieldPropertiesList.value.splice(
formFieldOptionIndex.value,
1,
fieldOptionForm.value,
);
fieldOptionType.value === 'constraint' &&
fieldConstraintsList.value.splice(
formFieldOptionIndex.value,
1,
fieldOptionForm.value,
);
fieldOptionType.value === 'enum' &&
fieldEnumList.value.splice(
formFieldOptionIndex.value,
1,
fieldOptionForm.value,
);
}
fieldOptionModelVisible.value = false;
fieldOptionForm.value = {};
};
// 保存字段配置
const saveField = () => {
const { id, type, label, defaultValue, datePattern } = formFieldForm.value;
const Field = bpmnInstances().moddle.create(`${prefix}:FormField`, {
id,
type,
label,
});
defaultValue && (Field.defaultValue = defaultValue);
datePattern && (Field.datePattern = datePattern);
// 构建属性
if (fieldPropertiesList.value && fieldPropertiesList.value.length) {
const fieldPropertyList = fieldPropertiesList.value.map((fp) => {
return bpmnInstances().moddle.create(`${prefix}:Property`, {
id: fp.id,
value: fp.value,
});
});
Field.properties = bpmnInstances().moddle.create(`${prefix}:Properties`, {
values: fieldPropertyList,
});
}
// 构建校验规则
if (fieldConstraintsList.value && fieldConstraintsList.value.length) {
const fieldConstraintList = fieldConstraintsList.value.map((fc) => {
return bpmnInstances().moddle.create(`${prefix}:Constraint`, {
name: fc.name,
config: fc.config,
});
});
Field.validation = bpmnInstances().moddle.create(`${prefix}:Validation`, {
constraints: fieldConstraintList,
});
}
// 构建枚举值
if (fieldEnumList.value && fieldEnumList.value.length) {
Field.values = fieldEnumList.value.map((fe) => {
return bpmnInstances().moddle.create(`${prefix}:Value`, {
name: fe.name,
id: fe.id,
});
});
}
// 更新数组 与 表单配置实例
if (formFieldIndex.value === -1) {
fieldList.value.push(formFieldForm.value);
formData.value.fields.push(Field);
} else {
fieldList.value.splice(formFieldIndex.value, 1, formFieldForm.value);
formData.value.fields.splice(formFieldIndex.value, 1, Field);
}
updateElementExtensions();
fieldModelVisible.value = false;
};
// 移除某个 字段的 配置项
const removeFieldOptionItem = (option, index, type) => {
// console.log(option, 'option')
if (type === 'property') {
fieldPropertiesList.value.splice(index, 1);
return;
}
if (type === 'enum') {
fieldEnumList.value.splice(index, 1);
return;
}
fieldConstraintsList.value.splice(index, 1);
};
// 移除 字段
const removeField = (field, index) => {
console.log(field, 'field');
fieldList.value.splice(index, 1);
formData.value.fields.splice(index, 1);
updateElementExtensions();
};
const updateElementExtensions = () => {
// 更新回扩展元素
const newElExtensionElements = bpmnInstances().moddle.create(
`bpmn:ExtensionElements`,
{
values: otherExtensions.value.concat(formData.value),
},
);
// 更新到元素上
bpmnInstances().modeling.updateProperties(toRaw(bpmnELement.value), {
extensionElements: newElExtensionElements,
});
};
const formList = ref([]); // 流程表单的下拉框的数据
onMounted(async () => {
formList.value = await FormApi.getFormSimpleList();
formKey.value = parseInt(formKey.value);
});
watch(
() => props.id,
(val) => {
val &&
val.length &&
nextTick(() => {
resetFormList();
});
},
{ immediate: true },
);
</script>

View File

@@ -0,0 +1,7 @@
import MyPropertiesPanel from './PropertiesPanel.vue';
MyPropertiesPanel.install = function (Vue) {
Vue.component(MyPropertiesPanel.name, MyPropertiesPanel);
};
export default MyPropertiesPanel;

View File

@@ -0,0 +1,504 @@
<template>
<div class="panel-tab__content">
<el-table :data="elementListenersList" size="small" border>
<el-table-column label="序号" width="50px" type="index" />
<el-table-column label="事件类型" min-width="100px" prop="event" />
<el-table-column
label="监听器类型"
min-width="100px"
show-overflow-tooltip
:formatter="(row) => listenerTypeObject[row.listenerType]"
/>
<el-table-column label="操作" width="100px">
<template #default="scope">
<el-button
size="small"
link
@click="openListenerForm(scope.row, scope.$index)"
>编辑</el-button
>
<el-divider direction="vertical" />
<el-button
size="small"
link
style="color: #ff4d4f"
@click="removeListener(scope.$index)"
>移除</el-button
>
</template>
</el-table-column>
</el-table>
<div class="element-drawer__button">
<XButton
type="primary"
preIcon="ep:plus"
title="添加监听器"
size="small"
@click="openListenerForm(null)"
/>
<XButton
type="success"
preIcon="ep:select"
title="选择监听器"
size="small"
@click="openProcessListenerDialog"
/>
</div>
<!-- 监听器 编辑/创建 部分 -->
<el-drawer
v-model="listenerFormModelVisible"
title="执行监听器"
:size="`${width}px`"
append-to-body
destroy-on-close
>
<el-form :model="listenerForm" label-width="96px" ref="listenerFormRef">
<el-form-item
label="事件类型"
prop="event"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-select v-model="listenerForm.event">
<el-option label="start" value="start" />
<el-option label="end" value="end" />
</el-select>
</el-form-item>
<el-form-item
label="监听器类型"
prop="listenerType"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-select v-model="listenerForm.listenerType">
<el-option
v-for="i in Object.keys(listenerTypeObject)"
:key="i"
:label="listenerTypeObject[i]"
:value="i"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="listenerForm.listenerType === 'classListener'"
label="Java类"
prop="class"
key="listener-class"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerForm.class" clearable />
</el-form-item>
<el-form-item
v-if="listenerForm.listenerType === 'expressionListener'"
label="表达式"
prop="expression"
key="listener-expression"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerForm.expression" clearable />
</el-form-item>
<el-form-item
v-if="listenerForm.listenerType === 'delegateExpressionListener'"
label="代理表达式"
prop="delegateExpression"
key="listener-delegate"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerForm.delegateExpression" clearable />
</el-form-item>
<template v-if="listenerForm.listenerType === 'scriptListener'">
<el-form-item
label="脚本格式"
prop="scriptFormat"
key="listener-script-format"
:rules="{
required: true,
trigger: ['blur', 'change'],
message: '请填写脚本格式',
}"
>
<el-input v-model="listenerForm.scriptFormat" clearable />
</el-form-item>
<el-form-item
label="脚本类型"
prop="scriptType"
key="listener-script-type"
:rules="{
required: true,
trigger: ['blur', 'change'],
message: '请选择脚本类型',
}"
>
<el-select v-model="listenerForm.scriptType">
<el-option label="内联脚本" value="inlineScript" />
<el-option label="外部脚本" value="externalScript" />
</el-select>
</el-form-item>
<el-form-item
v-if="listenerForm.scriptType === 'inlineScript'"
label="脚本内容"
prop="value"
key="listener-script"
:rules="{
required: true,
trigger: ['blur', 'change'],
message: '请填写脚本内容',
}"
>
<el-input v-model="listenerForm.value" clearable />
</el-form-item>
<el-form-item
v-if="listenerForm.scriptType === 'externalScript'"
label="资源地址"
prop="resource"
key="listener-resource"
:rules="{
required: true,
trigger: ['blur', 'change'],
message: '请填写资源地址',
}"
>
<el-input v-model="listenerForm.resource" clearable />
</el-form-item>
</template>
</el-form>
<el-divider />
<p class="listener-filed__title">
<span><Icon icon="ep:menu" />注入字段</span>
<XButton
type="primary"
@click="openListenerFieldForm(null)"
title="添加字段"
/>
</p>
<el-table
:data="fieldsListOfListener"
size="small"
max-height="240"
fit
border
style="flex: none"
>
<el-table-column label="序号" width="50px" type="index" />
<el-table-column label="字段名称" min-width="100px" prop="name" />
<el-table-column
label="字段类型"
min-width="80px"
show-overflow-tooltip
:formatter="(row) => fieldTypeObject[row.fieldType]"
/>
<el-table-column
label="字段值/表达式"
min-width="100px"
show-overflow-tooltip
:formatter="(row) => row.string || row.expression"
/>
<el-table-column label="操作" width="130px">
<template #default="scope">
<el-button
size="small"
link
@click="openListenerFieldForm(scope.row, scope.$index)"
>编辑</el-button
>
<el-divider direction="vertical" />
<el-button
size="small"
link
style="color: #ff4d4f"
@click="removeListenerField(scope.$index)"
>移除</el-button
>
</template>
</el-table-column>
</el-table>
<div class="element-drawer__button">
<el-button @click="listenerFormModelVisible = false"> </el-button>
<el-button type="primary" @click="saveListenerConfig"> </el-button>
</div>
</el-drawer>
<!-- 注入西段 编辑/创建 部分 -->
<el-dialog
title="字段配置"
v-model="listenerFieldFormModelVisible"
width="600px"
append-to-body
destroy-on-close
>
<el-form
:model="listenerFieldForm"
label-width="96spx"
ref="listenerFieldFormRef"
style="height: 136px"
>
<el-form-item
label="字段名称:"
prop="name"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerFieldForm.name" clearable />
</el-form-item>
<el-form-item
label="字段类型:"
prop="fieldType"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-select v-model="listenerFieldForm.fieldType">
<el-option
v-for="i in Object.keys(fieldTypeObject)"
:key="i"
:label="fieldTypeObject[i]"
:value="i"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="listenerFieldForm.fieldType === 'string'"
label="字段值:"
prop="string"
key="field-string"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerFieldForm.string" clearable />
</el-form-item>
<el-form-item
v-if="listenerFieldForm.fieldType === 'expression'"
label="表达式:"
prop="expression"
key="field-expression"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerFieldForm.expression" clearable />
</el-form-item>
</el-form>
<template #footer>
<el-button size="small" @click="listenerFieldFormModelVisible = false"
> </el-button
>
<el-button size="small" type="primary" @click="saveListenerFiled"
> </el-button
>
</template>
</el-dialog>
</div>
<!-- 选择弹窗 -->
<ProcessListenerDialog
ref="processListenerDialogRef"
@select="selectProcessListener"
/>
</template>
<script lang="ts" setup>
import { ElMessageBox } from 'element-plus';
import { createListenerObject, updateElementExtensions } from '../../utils';
import {
initListenerType,
initListenerForm,
listenerType,
fieldType,
initListenerForm2,
} from './utilSelf';
import ProcessListenerDialog from './ProcessListenerDialog.vue';
defineOptions({ name: 'ElementListeners' });
const props = defineProps({
id: String,
type: String,
});
const prefix = inject('prefix');
const width = inject('width');
const elementListenersList = ref<any[]>([]); // 监听器列表
const listenerForm = ref<any>({}); // 监听器详情表单
const listenerFormModelVisible = ref(false); // 监听器 编辑 侧边栏显示状态
const fieldsListOfListener = ref<any[]>([]);
const listenerFieldForm = ref<any>({}); // 监听器 注入字段 详情表单
const listenerFieldFormModelVisible = ref(false); // 监听器 注入字段表单弹窗 显示状态
const editingListenerIndex = ref(-1); // 监听器所在下标,-1 为新增
const editingListenerFieldIndex = ref(-1); // 字段所在下标,-1 为新增
const listenerTypeObject = ref(listenerType);
const fieldTypeObject = ref(fieldType);
const bpmnElement = ref();
const otherExtensionList = ref();
const bpmnElementListeners = ref();
const listenerFormRef = ref();
const listenerFieldFormRef = ref();
const bpmnInstances = () => (window as any)?.bpmnInstances;
const resetListenersList = () => {
bpmnElement.value = bpmnInstances().bpmnElement;
otherExtensionList.value = [];
bpmnElementListeners.value =
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
(ex) => ex.$type === `${prefix}:ExecutionListener`,
) ?? [];
elementListenersList.value = bpmnElementListeners.value.map((listener) =>
initListenerType(listener),
);
};
// 打开 监听器详情 侧边栏
const openListenerForm = (listener, index?) => {
// debugger
if (listener) {
listenerForm.value = initListenerForm(listener);
editingListenerIndex.value = index;
} else {
listenerForm.value = {};
editingListenerIndex.value = -1; // 标记为新增
}
if (listener && listener.fields) {
fieldsListOfListener.value = listener.fields.map((field) => ({
...field,
fieldType: field.string ? 'string' : 'expression',
}));
} else {
fieldsListOfListener.value = [];
listenerForm.value['fields'] = [];
}
// 打开侧边栏并清楚验证状态
listenerFormModelVisible.value = true;
nextTick(() => {
if (listenerFormRef.value) {
listenerFormRef.value.clearValidate();
}
});
};
// 打开监听器字段编辑弹窗
const openListenerFieldForm = (field, index?) => {
listenerFieldForm.value = field ? JSON.parse(JSON.stringify(field)) : {};
editingListenerFieldIndex.value = field ? index : -1;
listenerFieldFormModelVisible.value = true;
nextTick(() => {
if (listenerFieldFormRef.value) {
listenerFieldFormRef.value.clearValidate();
}
});
};
// 保存监听器注入字段
const saveListenerFiled = async () => {
// debugger
let validateStatus = await listenerFieldFormRef.value.validate();
if (!validateStatus) return; // 验证不通过直接返回
if (editingListenerFieldIndex.value === -1) {
fieldsListOfListener.value.push(listenerFieldForm.value);
listenerForm.value.fields.push(listenerFieldForm.value);
} else {
fieldsListOfListener.value.splice(
editingListenerFieldIndex.value,
1,
listenerFieldForm.value,
);
listenerForm.value.fields.splice(
editingListenerFieldIndex.value,
1,
listenerFieldForm.value,
);
}
listenerFieldFormModelVisible.value = false;
nextTick(() => {
listenerFieldForm.value = {};
});
};
// 移除监听器字段
const removeListenerField = (index) => {
// debugger
ElMessageBox.confirm('确认移除该字段吗?', '提示', {
confirmButtonText: '确 认',
cancelButtonText: '取 消',
})
.then(() => {
fieldsListOfListener.value.splice(index, 1);
listenerForm.value.fields.splice(index, 1);
})
.catch(() => console.info('操作取消'));
};
// 移除监听器
const removeListener = (index) => {
ElMessageBox.confirm('确认移除该监听器吗?', '提示', {
confirmButtonText: '确 认',
cancelButtonText: '取 消',
})
.then(() => {
bpmnElementListeners.value.splice(index, 1);
elementListenersList.value.splice(index, 1);
updateElementExtensions(
bpmnElement.value,
otherExtensionList.value.concat(bpmnElementListeners.value),
);
})
.catch(() => console.info('操作取消'));
};
// 保存监听器配置
const saveListenerConfig = async () => {
// debugger
let validateStatus = await listenerFormRef.value.validate();
if (!validateStatus) return; // 验证不通过直接返回
const listenerObject = createListenerObject(
listenerForm.value,
false,
prefix,
);
if (editingListenerIndex.value === -1) {
bpmnElementListeners.value.push(listenerObject);
elementListenersList.value.push(listenerForm.value);
} else {
bpmnElementListeners.value.splice(
editingListenerIndex.value,
1,
listenerObject,
);
elementListenersList.value.splice(
editingListenerIndex.value,
1,
listenerForm.value,
);
}
// 保存其他配置
otherExtensionList.value =
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
(ex) => ex.$type !== `${prefix}:ExecutionListener`,
) ?? [];
updateElementExtensions(
bpmnElement.value,
otherExtensionList.value.concat(bpmnElementListeners.value),
);
// 4. 隐藏侧边栏
listenerFormModelVisible.value = false;
listenerForm.value = {};
};
// 打开监听器弹窗
const processListenerDialogRef = ref();
const openProcessListenerDialog = async () => {
processListenerDialogRef.value.open('execution');
};
const selectProcessListener = (listener) => {
const listenerForm = initListenerForm2(listener);
const listenerObject = createListenerObject(listenerForm, false, prefix);
bpmnElementListeners.value.push(listenerObject);
elementListenersList.value.push(listenerForm);
// 保存其他配置
otherExtensionList.value =
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
(ex) => ex.$type !== `${prefix}:ExecutionListener`,
) ?? [];
updateElementExtensions(
bpmnElement.value,
otherExtensionList.value.concat(bpmnElementListeners.value),
);
};
watch(
() => props.id,
(val) => {
val &&
val.length &&
nextTick(() => {
resetListenersList();
});
},
{ immediate: true },
);
</script>

View File

@@ -0,0 +1,98 @@
<!-- 执行器选择 -->
<template>
<Dialog title="请选择监听器" v-model="dialogVisible" width="1024px">
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
>
<el-table-column label="名字" align="center" prop="name" />
<el-table-column label="类型" align="center" prop="type">
<template #default="scope">
<dict-tag
:type="DICT_TYPE.BPM_PROCESS_LISTENER_TYPE"
:value="scope.row.type"
/>
</template>
</el-table-column>
<el-table-column label="事件" align="center" prop="event" />
<el-table-column label="值类型" align="center" prop="valueType">
<template #default="scope">
<dict-tag
:type="DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE"
:value="scope.row.valueType"
/>
</template>
</el-table-column>
<el-table-column label="值" align="center" prop="value" />
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button link type="primary" @click="select(scope.row)">
选择
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
</Dialog>
</template>
<script setup lang="ts">
import {
ProcessListenerApi,
ProcessListenerVO,
} from '@/api/bpm/processListener';
import { DICT_TYPE } from '@/utils/dict';
import { CommonStatusEnum } from '@/utils/constants';
/** BPM 流程 表单 */
defineOptions({ name: 'ProcessListenerDialog' });
const dialogVisible = ref(false); // 弹窗的是否展示
const loading = ref(true); // 列表的加载中
const list = ref<ProcessListenerVO[]>([]); // 列表的数据
const total = ref(0); // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
type: '',
status: CommonStatusEnum.ENABLE,
});
/** 打开弹窗 */
const open = async (type: string) => {
queryParams.pageNo = 1;
queryParams.type = type;
getList();
dialogVisible.value = true;
};
defineExpose({ open }); // 提供 open 方法,用于打开弹窗
/** 查询列表 */
const getList = async () => {
loading.value = true;
try {
const data = await ProcessListenerApi.getProcessListenerPage(queryParams);
list.value = data.list;
total.value = data.total;
} finally {
loading.value = false;
}
};
/** 提交表单 */
const emit = defineEmits(['success']); // 定义 success 事件,用于操作成功后的回调
const select = async (row) => {
dialogVisible.value = false;
// 发送操作成功的事件
emit('select', row);
};
</script>

View File

@@ -0,0 +1,564 @@
<template>
<div class="panel-tab__content">
<el-table :data="elementListenersList" size="small" border>
<el-table-column label="序号" width="50px" type="index" />
<el-table-column
label="事件类型"
min-width="80px"
show-overflow-tooltip
:formatter="(row) => listenerEventTypeObject[row.event]"
/>
<el-table-column
label="事件id"
min-width="80px"
prop="id"
show-overflow-tooltip
/>
<el-table-column
label="监听器类型"
min-width="80px"
show-overflow-tooltip
:formatter="(row) => listenerTypeObject[row.listenerType]"
/>
<el-table-column label="操作" width="90px">
<template #default="scope">
<el-button
size="small"
link
@click="openListenerForm(scope.row, scope.$index)"
>编辑</el-button
>
<el-divider direction="vertical" />
<el-button
size="small"
link
style="color: #ff4d4f"
@click="removeListener(scope.row, scope.$index)"
>移除</el-button
>
</template>
</el-table-column>
</el-table>
<div class="element-drawer__button">
<XButton
size="small"
type="primary"
preIcon="ep:plus"
title="添加监听器"
@click="openListenerForm(null)"
/>
<XButton
type="success"
preIcon="ep:select"
title="选择监听器"
size="small"
@click="openProcessListenerDialog"
/>
</div>
<!-- 监听器 编辑/创建 部分 -->
<el-drawer
v-model="listenerFormModelVisible"
title="任务监听器"
:size="`${width}px`"
append-to-body
destroy-on-close
>
<el-form
size="small"
:model="listenerForm"
label-width="96px"
ref="listenerFormRef"
>
<el-form-item
label="事件类型"
prop="event"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-select v-model="listenerForm.event">
<el-option
v-for="i in Object.keys(listenerEventTypeObject)"
:key="i"
:label="listenerEventTypeObject[i]"
:value="i"
/>
</el-select>
</el-form-item>
<el-form-item
label="监听器ID"
prop="id"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerForm.id" clearable />
</el-form-item>
<el-form-item
label="监听器类型"
prop="listenerType"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-select v-model="listenerForm.listenerType">
<el-option
v-for="i in Object.keys(listenerTypeObject)"
:key="i"
:label="listenerTypeObject[i]"
:value="i"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="listenerForm.listenerType === 'classListener'"
label="Java类"
prop="class"
key="listener-class"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerForm.class" clearable />
</el-form-item>
<el-form-item
v-if="listenerForm.listenerType === 'expressionListener'"
label="表达式"
prop="expression"
key="listener-expression"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerForm.expression" clearable />
</el-form-item>
<el-form-item
v-if="listenerForm.listenerType === 'delegateExpressionListener'"
label="代理表达式"
prop="delegateExpression"
key="listener-delegate"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerForm.delegateExpression" clearable />
</el-form-item>
<template v-if="listenerForm.listenerType === 'scriptListener'">
<el-form-item
label="脚本格式"
prop="scriptFormat"
key="listener-script-format"
:rules="{
required: true,
trigger: ['blur', 'change'],
message: '请填写脚本格式',
}"
>
<el-input v-model="listenerForm.scriptFormat" clearable />
</el-form-item>
<el-form-item
label="脚本类型"
prop="scriptType"
key="listener-script-type"
:rules="{
required: true,
trigger: ['blur', 'change'],
message: '请选择脚本类型',
}"
>
<el-select v-model="listenerForm.scriptType">
<el-option label="内联脚本" value="inlineScript" />
<el-option label="外部脚本" value="externalScript" />
</el-select>
</el-form-item>
<el-form-item
v-if="listenerForm.scriptType === 'inlineScript'"
label="脚本内容"
prop="value"
key="listener-script"
:rules="{
required: true,
trigger: ['blur', 'change'],
message: '请填写脚本内容',
}"
>
<el-input v-model="listenerForm.value" clearable />
</el-form-item>
<el-form-item
v-if="listenerForm.scriptType === 'externalScript'"
label="资源地址"
prop="resource"
key="listener-resource"
:rules="{
required: true,
trigger: ['blur', 'change'],
message: '请填写资源地址',
}"
>
<el-input v-model="listenerForm.resource" clearable />
</el-form-item>
</template>
<template v-if="listenerForm.event === 'timeout'">
<el-form-item
label="定时器类型"
prop="eventDefinitionType"
key="eventDefinitionType"
>
<el-select v-model="listenerForm.eventDefinitionType">
<el-option label="日期" value="date" />
<el-option label="持续时长" value="duration" />
<el-option label="循环" value="cycle" />
<el-option label="无" value="null" />
</el-select>
</el-form-item>
<el-form-item
v-if="
!!listenerForm.eventDefinitionType &&
listenerForm.eventDefinitionType !== 'null'
"
label="定时器"
prop="eventTimeDefinitions"
key="eventTimeDefinitions"
:rules="{
required: true,
trigger: ['blur', 'change'],
message: '请填写定时器配置',
}"
>
<el-input v-model="listenerForm.eventTimeDefinitions" clearable />
</el-form-item>
</template>
</el-form>
<el-divider />
<p class="listener-filed__title">
<span><Icon icon="ep:menu" />注入字段:</span>
<el-button
size="small"
type="primary"
@click="openListenerFieldForm(null)"
>添加字段</el-button
>
</p>
<el-table
:data="fieldsListOfListener"
size="small"
max-height="240"
fit
border
style="flex: none"
>
<el-table-column label="序号" width="50px" type="index" />
<el-table-column label="字段名称" min-width="100px" prop="name" />
<el-table-column
label="字段类型"
min-width="80px"
show-overflow-tooltip
:formatter="(row) => fieldTypeObject[row.fieldType]"
/>
<el-table-column
label="字段值/表达式"
min-width="100px"
show-overflow-tooltip
:formatter="(row) => row.string || row.expression"
/>
<el-table-column label="操作" width="100px">
<template #default="scope">
<el-button
size="small"
link
@click="openListenerFieldForm(scope.row, scope.$index)"
>编辑</el-button
>
<el-divider direction="vertical" />
<el-button
size="small"
link
style="color: #ff4d4f"
@click="removeListenerField(scope.row, scope.$index)"
>移除</el-button
>
</template>
</el-table-column>
</el-table>
<div class="element-drawer__button">
<el-button size="small" @click="listenerFormModelVisible = false"
>取 消</el-button
>
<el-button size="small" type="primary" @click="saveListenerConfig"
>保 存</el-button
>
</div>
</el-drawer>
<!-- 注入西段 编辑/创建 部分 -->
<el-dialog
title="字段配置"
v-model="listenerFieldFormModelVisible"
width="600px"
append-to-body
destroy-on-close
>
<el-form
:model="listenerFieldForm"
size="small"
label-width="96px"
ref="listenerFieldFormRef"
style="height: 136px"
>
<el-form-item
label="字段名称:"
prop="name"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerFieldForm.name" clearable />
</el-form-item>
<el-form-item
label="字段类型:"
prop="fieldType"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-select v-model="listenerFieldForm.fieldType">
<el-option
v-for="i in Object.keys(fieldTypeObject)"
:key="i"
:label="fieldTypeObject[i]"
:value="i"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="listenerFieldForm.fieldType === 'string'"
label="字段值:"
prop="string"
key="field-string"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerFieldForm.string" clearable />
</el-form-item>
<el-form-item
v-if="listenerFieldForm.fieldType === 'expression'"
label="表达式:"
prop="expression"
key="field-expression"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerFieldForm.expression" clearable />
</el-form-item>
</el-form>
<template #footer>
<el-button size="small" @click="listenerFieldFormModelVisible = false"
>取 消</el-button
>
<el-button size="small" type="primary" @click="saveListenerFiled"
>确 定</el-button
>
</template>
</el-dialog>
</div>
<!-- 选择弹窗 -->
<ProcessListenerDialog
ref="processListenerDialogRef"
@select="selectProcessListener"
/>
</template>
<script lang="ts" setup>
import { ElMessageBox } from 'element-plus';
import { createListenerObject, updateElementExtensions } from '../../utils';
import {
initListenerForm,
initListenerType,
eventType,
listenerType,
fieldType,
initListenerForm2,
} from './utilSelf';
import ProcessListenerDialog from '@/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue';
defineOptions({ name: 'UserTaskListeners' });
const props = defineProps({
id: String,
type: String,
});
const prefix = inject('prefix');
const width = inject('width');
const elementListenersList = ref<any[]>([]);
const listenerEventTypeObject = ref(eventType);
const listenerTypeObject = ref(listenerType);
const listenerFormModelVisible = ref(false);
const listenerForm = ref<any>({});
const fieldTypeObject = ref(fieldType);
const fieldsListOfListener = ref<any[]>([]);
const listenerFieldFormModelVisible = ref(false); // 监听器 注入字段表单弹窗 显示状态
const editingListenerIndex = ref(-1); // 监听器所在下标,-1 为新增
const editingListenerFieldIndex = ref(-1); // 字段所在下标,-1 为新增
const listenerFieldForm = ref<any>({}); // 监听器 注入字段 详情表单
const bpmnElement = ref();
const bpmnElementListeners = ref();
const otherExtensionList = ref();
const listenerFormRef = ref();
const listenerFieldFormRef = ref();
const bpmnInstances = () => (window as any)?.bpmnInstances;
const resetListenersList = () => {
console.log(
bpmnInstances().bpmnElement,
'window.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElement',
);
bpmnElement.value = bpmnInstances().bpmnElement;
otherExtensionList.value = [];
bpmnElementListeners.value =
bpmnElement.value.businessObject?.extensionElements?.values.filter(
(ex) => ex.$type === `${prefix}:TaskListener`,
) ?? [];
elementListenersList.value = bpmnElementListeners.value.map((listener) =>
initListenerType(listener),
);
};
const openListenerForm = (listener, index?) => {
if (listener) {
listenerForm.value = initListenerForm(listener);
editingListenerIndex.value = index;
} else {
listenerForm.value = {};
editingListenerIndex.value = -1; // 标记为新增
}
if (listener && listener.fields) {
fieldsListOfListener.value = listener.fields.map((field) => ({
...field,
fieldType: field.string ? 'string' : 'expression',
}));
} else {
fieldsListOfListener.value = [];
listenerForm.value['fields'] = [];
}
// 打开侧边栏并清楚验证状态
listenerFormModelVisible.value = true;
nextTick(() => {
if (listenerFormRef.value) listenerFormRef.value.clearValidate();
});
};
// 移除监听器
const removeListener = (listener, index?) => {
console.log(listener, 'listener');
ElMessageBox.confirm('确认移除该监听器吗?', '提示', {
confirmButtonText: '确 认',
cancelButtonText: '取 消',
})
.then(() => {
bpmnElementListeners.value.splice(index, 1);
elementListenersList.value.splice(index, 1);
updateElementExtensions(
bpmnElement.value,
otherExtensionList.value.concat(bpmnElementListeners.value),
);
})
.catch(() => console.info('操作取消'));
};
// 保存监听器
const saveListenerConfig = async () => {
let validateStatus = await listenerFormRef.value.validate();
if (!validateStatus) return; // 验证不通过直接返回
const listenerObject = createListenerObject(listenerForm.value, true, prefix);
if (editingListenerIndex.value === -1) {
bpmnElementListeners.value.push(listenerObject);
elementListenersList.value.push(listenerForm.value);
} else {
bpmnElementListeners.value.splice(
editingListenerIndex.value,
1,
listenerObject,
);
elementListenersList.value.splice(
editingListenerIndex.value,
1,
listenerForm.value,
);
}
// 保存其他配置
otherExtensionList.value =
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
(ex) => ex.$type !== `${prefix}:TaskListener`,
) ?? [];
updateElementExtensions(
bpmnElement.value,
otherExtensionList.value.concat(bpmnElementListeners.value),
);
// 4. 隐藏侧边栏
listenerFormModelVisible.value = false;
listenerForm.value = {};
};
// 打开监听器字段编辑弹窗
const openListenerFieldForm = (field, index?) => {
listenerFieldForm.value = field ? JSON.parse(JSON.stringify(field)) : {};
editingListenerFieldIndex.value = field ? index : -1;
listenerFieldFormModelVisible.value = true;
nextTick(() => {
if (listenerFieldFormRef.value) listenerFieldFormRef.value.clearValidate();
});
};
// 保存监听器注入字段
const saveListenerFiled = async () => {
let validateStatus = await listenerFieldFormRef.value.validate();
if (!validateStatus) return; // 验证不通过直接返回
if (editingListenerFieldIndex.value === -1) {
fieldsListOfListener.value.push(listenerFieldForm.value);
listenerForm.value.fields.push(listenerFieldForm.value);
} else {
fieldsListOfListener.value.splice(
editingListenerFieldIndex.value,
1,
listenerFieldForm.value,
);
listenerForm.value.fields.splice(
editingListenerFieldIndex.value,
1,
listenerFieldForm.value,
);
}
listenerFieldFormModelVisible.value = false;
nextTick(() => {
listenerFieldForm.value = {};
});
};
// 移除监听器字段
const removeListenerField = (field, index) => {
console.log(field, 'field');
ElMessageBox.confirm('确认移除该字段吗?', '提示', {
confirmButtonText: '确 认',
cancelButtonText: '取 消',
})
.then(() => {
fieldsListOfListener.value.splice(index, 1);
listenerForm.value.fields.splice(index, 1);
})
.catch(() => console.info('操作取消'));
};
// 打开监听器弹窗
const processListenerDialogRef = ref();
const openProcessListenerDialog = async () => {
processListenerDialogRef.value.open('task');
};
const selectProcessListener = (listener) => {
const listenerForm = initListenerForm2(listener);
const listenerObject = createListenerObject(listenerForm, true, prefix);
bpmnElementListeners.value.push(listenerObject);
elementListenersList.value.push(listenerForm);
// 保存其他配置
otherExtensionList.value =
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
(ex) => ex.$type !== `${prefix}:TaskListener`,
) ?? [];
updateElementExtensions(
bpmnElement.value,
otherExtensionList.value.concat(bpmnElementListeners.value),
);
};
watch(
() => props.id,
(val) => {
val &&
val.length &&
nextTick(() => {
resetListenersList();
});
},
{ immediate: true },
);
</script>

View File

@@ -0,0 +1,178 @@
export const template = (isTaskListener) => {
return `
<div class="panel-tab__content">
<el-table :data="elementListenersList" size="small" border>
<el-table-column label="序号" width="50px" type="index" />
<el-table-column label="事件类型" min-width="100px" prop="event" />
<el-table-column label="监听器类型" min-width="100px" show-overflow-tooltip :formatter="row => listenerTypeObject[row.listenerType]" />
<el-table-column label="操作" width="90px">
<template #default="scope">
<el-button size="small" type="primary" link @click="openListenerForm(scope, scope.$index)">编辑</el-button>
<el-divider direction="vertical" />
<el-button size="small" type="primary" link style="color: #ff4d4f" @click="removeListener(scope, scope.$index)">移除</el-button>
</template>
</el-table-column>
</el-table>
<div class="element-drawer__button">
<el-button size="small" type="primary" icon="el-icon-plus" @click="openListenerForm(null)">添加监听器</el-button>
</div>
<!-- 监听器 编辑/创建 部分 -->
<el-drawer :visible.sync="listenerFormModelVisible" title="执行监听器" :size="width + 'px'" append-to-body destroy-on-close>
<el-form size="small" :model="listenerForm" label-width="96px" ref="listenerFormRef" @submit.native.prevent>
<el-form-item label="事件类型" prop="event" :rules="{ required: true, trigger: ['blur', 'change'] }">
<el-select v-model="listenerForm.event">
<el-option label="start" value="start" />
<el-option label="end" value="end" />
</el-select>
</el-form-item>
<el-form-item label="监听器类型" prop="listenerType" :rules="{ required: true, trigger: ['blur', 'change'] }">
<el-select v-model="listenerForm.listenerType">
<el-option v-for="i in Object.keys(listenerTypeObject)" :key="i" :label="listenerTypeObject[i]" :value="i" />
</el-select>
</el-form-item>
<el-form-item
v-if="listenerForm.listenerType === 'classListener'"
label="Java类"
prop="class"
key="listener-class"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerForm.class" clearable />
</el-form-item>
<el-form-item
v-if="listenerForm.listenerType === 'expressionListener'"
label="表达式"
prop="expression"
key="listener-expression"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerForm.expression" clearable />
</el-form-item>
<el-form-item
v-if="listenerForm.listenerType === 'delegateExpressionListener'"
label="代理表达式"
prop="delegateExpression"
key="listener-delegate"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerForm.delegateExpression" clearable />
</el-form-item>
<template v-if="listenerForm.listenerType === 'scriptListener'">
<el-form-item
label="脚本格式"
prop="scriptFormat"
key="listener-script-format"
:rules="{ required: true, trigger: ['blur', 'change'], message: '请填写脚本格式' }"
>
<el-input v-model="listenerForm.scriptFormat" clearable />
</el-form-item>
<el-form-item
label="脚本类型"
prop="scriptType"
key="listener-script-type"
:rules="{ required: true, trigger: ['blur', 'change'], message: '请选择脚本类型' }"
>
<el-select v-model="listenerForm.scriptType">
<el-option label="内联脚本" value="inlineScript" />
<el-option label="外部脚本" value="externalScript" />
</el-select>
</el-form-item>
<el-form-item
v-if="listenerForm.scriptType === 'inlineScript'"
label="脚本内容"
prop="value"
key="listener-script"
:rules="{ required: true, trigger: ['blur', 'change'], message: '请填写脚本内容' }"
>
<el-input v-model="listenerForm.value" clearable />
</el-form-item>
<el-form-item
v-if="listenerForm.scriptType === 'externalScript'"
label="资源地址"
prop="resource"
key="listener-resource"
:rules="{ required: true, trigger: ['blur', 'change'], message: '请填写资源地址' }"
>
<el-input v-model="listenerForm.resource" clearable />
</el-form-item>
</template>
${
isTaskListener
? "<el-form-item label='定时器类型' prop='eventDefinitionType' key='eventDefinitionType'>" +
"<el-select v-model='listenerForm.eventDefinitionType'>" +
"<el-option label='日期' value='date' />" +
"<el-option label='持续时长' value='duration' />" +
"<el-option label='循环' value='cycle' />" +
"<el-option label='无' value='' />" +
'</el-select>' +
'</el-form-item>' +
"<el-form-item v-if='!!listenerForm.eventDefinitionType' label='定时器' prop='eventDefinitions' key='eventDefinitions'>" +
"<el-input v-model='listenerForm.eventDefinitions' clearable />" +
'</el-form-item>'
: ''
}
</el-form>
<el-divider />
<p class="listener-filed__title">
<span><i class="el-icon-menu"></i>注入字段:</span>
<el-button size="small" type="primary" @click="openListenerFieldForm(null)">添加字段</el-button>
</p>
<el-table :data="fieldsListOfListener" size="small" max-height="240" border fit style="flex: none">
<el-table-column label="序号" width="50px" type="index" />
<el-table-column label="字段名称" min-width="100px" prop="name" />
<el-table-column label="字段类型" min-width="80px" show-overflow-tooltip :formatter="row => fieldTypeObject[row.fieldType]" />
<el-table-column label="字段值/表达式" min-width="100px" show-overflow-tooltip :formatter="row => row.string || row.expression" />
<el-table-column label="操作" width="100px">
<template #default="scope">
<el-button size="small" type="primary" link @click="openListenerFieldForm(scope, scope.$index)">编辑</el-button>
<el-divider direction="vertical" />
<el-button size="small" type="primary" link style="color: #ff4d4f" @click="removeListenerField(scope, scope.$index)">移除</el-button>
</template>
</el-table-column>
</el-table>
<div class="element-drawer__button">
<el-button size="small" @click="listenerFormModelVisible = false">取 消</el-button>
<el-button size="small" type="primary" @click="saveListenerConfig">保 存</el-button>
</div>
</el-drawer>
<!-- 注入西段 编辑/创建 部分 -->
<el-dialog title="字段配置" :visible.sync="listenerFieldFormModelVisible" width="600px" append-to-body destroy-on-close>
<el-form :model="listenerFieldForm" size="small" label-width="96px" ref="listenerFieldFormRef" style="height: 136px" @submit.native.prevent>
<el-form-item label="字段名称:" prop="name" :rules="{ required: true, trigger: ['blur', 'change'] }">
<el-input v-model="listenerFieldForm.name" clearable />
</el-form-item>
<el-form-item label="字段类型:" prop="fieldType" :rules="{ required: true, trigger: ['blur', 'change'] }">
<el-select v-model="listenerFieldForm.fieldType">
<el-option v-for="i in Object.keys(fieldTypeObject)" :key="i" :label="fieldTypeObject[i]" :value="i" />
</el-select>
</el-form-item>
<el-form-item
v-if="listenerFieldForm.fieldType === 'string'"
label="字段值:"
prop="string"
key="field-string"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerFieldForm.string" clearable />
</el-form-item>
<el-form-item
v-if="listenerFieldForm.fieldType === 'expression'"
label="表达式:"
prop="expression"
key="field-expression"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerFieldForm.expression" clearable />
</el-form-item>
</el-form>
<template #footer>
<el-button size="small" @click="listenerFieldFormModelVisible = false">取 消</el-button>
<el-button size="small" type="primary" @click="saveListenerFiled">确 定</el-button>
</template>
</el-dialog>
</div>
`;
};

View File

@@ -0,0 +1,96 @@
// 初始化表单数据
export function initListenerForm(listener) {
let self = {
...listener,
};
if (listener.script) {
self = {
...listener,
...listener.script,
scriptType: listener.script.resource ? 'externalScript' : 'inlineScript',
};
}
if (
listener.event === 'timeout' &&
listener.eventDefinitions &&
listener.eventDefinitions.length > 0
) {
let k = '';
for (const key in listener.eventDefinitions[0]) {
console.log(listener.eventDefinitions, key);
if (key.includes('time')) {
k = key;
self.eventDefinitionType = key.replace('time', '').toLowerCase();
}
}
console.log(k);
self.eventTimeDefinitions = listener.eventDefinitions[0][k].body;
}
return self;
}
export function initListenerType(listener) {
let listenerType;
if (listener.class) listenerType = 'classListener';
if (listener.expression) listenerType = 'expressionListener';
if (listener.delegateExpression) listenerType = 'delegateExpressionListener';
if (listener.script) listenerType = 'scriptListener';
return {
...JSON.parse(JSON.stringify(listener)),
...listener.script,
listenerType,
};
}
/** 将 ProcessListenerDO 转换成 initListenerForm 想同的 Form 对象 */
export function initListenerForm2(processListener) {
switch (processListener.valueType) {
case 'class': {
return {
listenerType: 'classListener',
class: processListener.value,
event: processListener.event,
fields: [],
};
}
case 'delegateExpression': {
return {
listenerType: 'delegateExpressionListener',
delegateExpression: processListener.value,
event: processListener.event,
fields: [],
};
}
case 'expression': {
return {
listenerType: 'expressionListener',
expression: processListener.value,
event: processListener.event,
fields: [],
};
}
// No default
}
throw new Error('未知的监听器类型');
}
export const listenerType = {
classListener: 'Java 类',
expressionListener: '表达式',
delegateExpressionListener: '代理表达式',
scriptListener: '脚本',
};
export const eventType = {
create: '创建',
assignment: '指派',
complete: '完成',
delete: '删除',
update: '更新',
timeout: '超时',
};
export const fieldType = {
string: '字符串',
expression: '表达式',
};

View File

@@ -0,0 +1,459 @@
<template>
<div class="panel-tab__content">
<el-radio-group
v-if="type === 'UserTask'"
v-model="approveMethod"
@change="onApproveMethodChange"
>
<div class="flex-col">
<div v-for="(item, index) in APPROVE_METHODS" :key="index">
<el-radio :value="item.value" :label="item.value">
{{ item.label }}
</el-radio>
<el-form-item prop="approveRatio">
<el-input-number
v-model="approveRatio"
:min="10"
:max="100"
:step="10"
size="small"
v-if="
item.value === ApproveMethodType.APPROVE_BY_RATIO &&
approveMethod === ApproveMethodType.APPROVE_BY_RATIO
"
@change="onApproveRatioChange"
/>
</el-form-item>
</div>
</div>
</el-radio-group>
<div v-else>除了UserTask以外节点的多实例待实现</div>
<!-- 与Simple设计器配置合并保留以前的代码 -->
<el-form label-width="90px" style="display: none">
<el-form-item label="快捷配置">
<el-button size="small" @click="changeConfig('依次审批')"
>依次审批</el-button
>
<el-button size="small" @click="changeConfig('会签')">会签</el-button>
<el-button size="small" @click="changeConfig('或签')">或签</el-button>
</el-form-item>
<el-form-item label="会签类型">
<el-select
v-model="loopCharacteristics"
@change="changeLoopCharacteristicsType"
>
<el-option label="并行多重事件" value="ParallelMultiInstance" />
<el-option label="时序多重事件" value="SequentialMultiInstance" />
<el-option label="无" value="Null" />
</el-select>
</el-form-item>
<template
v-if="
loopCharacteristics === 'ParallelMultiInstance' ||
loopCharacteristics === 'SequentialMultiInstance'
"
>
<el-form-item label="循环数量" key="loopCardinality">
<el-input
v-model="loopInstanceForm.loopCardinality"
clearable
@change="updateLoopCardinality"
/>
</el-form-item>
<el-form-item label="集合" key="collection" v-show="false">
<el-input
v-model="loopInstanceForm.collection"
clearable
@change="updateLoopBase"
/>
</el-form-item>
<!-- add by 芋艿:由于「元素变量」暂时用不到,所以这里 display 为 none -->
<el-form-item
label="元素变量"
key="elementVariable"
style="display: none"
>
<el-input
v-model="loopInstanceForm.elementVariable"
clearable
@change="updateLoopBase"
/>
</el-form-item>
<el-form-item label="完成条件" key="completionCondition">
<el-input
v-model="loopInstanceForm.completionCondition"
clearable
@change="updateLoopCondition"
/>
</el-form-item>
<!-- add by 芋艿:由于「异步状态」暂时用不到,所以这里 display 为 none -->
<el-form-item label="异步状态" key="async" style="display: none">
<el-checkbox
v-model="loopInstanceForm.asyncBefore"
label="异步前"
value="异步前"
@change="updateLoopAsync('asyncBefore')"
/>
<el-checkbox
v-model="loopInstanceForm.asyncAfter"
label="异步后"
value="异步后"
@change="updateLoopAsync('asyncAfter')"
/>
<el-checkbox
v-model="loopInstanceForm.exclusive"
v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore"
label="排除"
value="排除"
@change="updateLoopAsync('exclusive')"
/>
</el-form-item>
<el-form-item
label="重试周期"
prop="timeCycle"
v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore"
key="timeCycle"
>
<el-input
v-model="loopInstanceForm.timeCycle"
clearable
@change="updateLoopTimeCycle"
/>
</el-form-item>
</template>
</el-form>
</div>
</template>
<script lang="ts" setup>
import {
ApproveMethodType,
APPROVE_METHODS,
} from '@/components/SimpleProcessDesignerV2/src/consts';
defineOptions({ name: 'ElementMultiInstance' });
const props = defineProps({
businessObject: Object,
type: String,
id: String,
});
const prefix = inject('prefix');
const loopCharacteristics = ref('');
//默认配置,用来覆盖原始不存在的选项,避免报错
const defaultLoopInstanceForm = ref({
completionCondition: '',
loopCardinality: '',
extensionElements: [],
asyncAfter: false,
asyncBefore: false,
exclusive: false,
});
const loopInstanceForm = ref<any>({});
const bpmnElement = ref(null);
const multiLoopInstance = ref(null);
const bpmnInstances = () => (window as any)?.bpmnInstances;
const getElementLoop = (businessObject) => {
if (!businessObject.loopCharacteristics) {
loopCharacteristics.value = 'Null';
loopInstanceForm.value = {};
return;
}
if (
businessObject.loopCharacteristics.$type ===
'bpmn:StandardLoopCharacteristics'
) {
loopCharacteristics.value = 'StandardLoop';
loopInstanceForm.value = {};
return;
}
if (businessObject.loopCharacteristics.isSequential) {
loopCharacteristics.value = 'SequentialMultiInstance';
} else {
loopCharacteristics.value = 'ParallelMultiInstance';
}
// 合并配置
loopInstanceForm.value = {
...defaultLoopInstanceForm.value,
...businessObject.loopCharacteristics,
completionCondition:
businessObject.loopCharacteristics?.completionCondition?.body ?? '',
loopCardinality:
businessObject.loopCharacteristics?.loopCardinality?.body ?? '',
};
// 保留当前元素 businessObject 上的 loopCharacteristics 实例
multiLoopInstance.value =
bpmnInstances().bpmnElement.businessObject.loopCharacteristics;
// 更新表单
if (
businessObject.loopCharacteristics.extensionElements &&
businessObject.loopCharacteristics.extensionElements.values &&
businessObject.loopCharacteristics.extensionElements.values.length
) {
loopInstanceForm.value['timeCycle'] =
businessObject.loopCharacteristics.extensionElements.values[0].body;
}
};
const changeLoopCharacteristicsType = (type) => {
// this.loopInstanceForm = { ...this.defaultLoopInstanceForm }; // 切换类型取消原表单配置
// 取消多实例配置
if (type === 'Null') {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
loopCharacteristics: null,
});
return;
}
// 配置循环
if (type === 'StandardLoop') {
const loopCharacteristicsObject = bpmnInstances().moddle.create(
'bpmn:StandardLoopCharacteristics',
);
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
loopCharacteristics: loopCharacteristicsObject,
});
multiLoopInstance.value = null;
return;
}
// 时序
if (type === 'SequentialMultiInstance') {
multiLoopInstance.value = bpmnInstances().moddle.create(
'bpmn:MultiInstanceLoopCharacteristics',
{ isSequential: true },
);
} else {
multiLoopInstance.value = bpmnInstances().moddle.create(
'bpmn:MultiInstanceLoopCharacteristics',
{ collection: '${coll_userList}' },
);
}
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
loopCharacteristics: toRaw(multiLoopInstance.value),
});
};
// 循环基数
const updateLoopCardinality = (cardinality) => {
let loopCardinality = null;
if (cardinality && cardinality.length) {
loopCardinality = bpmnInstances().moddle.create('bpmn:FormalExpression', {
body: cardinality,
});
}
bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnElement.value),
multiLoopInstance.value,
{
loopCardinality,
},
);
};
// 完成条件
const updateLoopCondition = (condition) => {
let completionCondition = null;
if (condition && condition.length) {
completionCondition = bpmnInstances().moddle.create(
'bpmn:FormalExpression',
{
body: condition,
},
);
}
bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnElement.value),
multiLoopInstance.value,
{
completionCondition,
},
);
};
// 重试周期
const updateLoopTimeCycle = (timeCycle) => {
const extensionElements = bpmnInstances().moddle.create(
'bpmn:ExtensionElements',
{
values: [
bpmnInstances().moddle.create(`${prefix}:FailedJobRetryTimeCycle`, {
body: timeCycle,
}),
],
},
);
bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnElement.value),
multiLoopInstance.value,
{
extensionElements,
},
);
};
// 直接更新的基础信息
const updateLoopBase = () => {
bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnElement.value),
multiLoopInstance.value,
{
collection: loopInstanceForm.value.collection || null,
elementVariable: loopInstanceForm.value.elementVariable || null,
},
);
};
// 各异步状态
const updateLoopAsync = (key) => {
const { asyncBefore, asyncAfter } = loopInstanceForm.value;
let asyncAttr = Object.create(null);
if (!asyncBefore && !asyncAfter) {
// this.$set(this.loopInstanceForm, "exclusive", false);
loopInstanceForm.value['exclusive'] = false;
asyncAttr = {
asyncBefore: false,
asyncAfter: false,
exclusive: false,
extensionElements: null,
};
} else {
asyncAttr[key] = loopInstanceForm.value[key];
}
bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnElement.value),
multiLoopInstance.value,
asyncAttr,
);
};
const changeConfig = (config) => {
if (config === '依次审批') {
changeLoopCharacteristicsType('SequentialMultiInstance');
updateLoopCardinality('1');
updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }');
} else if (config === '会签') {
changeLoopCharacteristicsType('ParallelMultiInstance');
updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }');
} else if (config === '或签') {
changeLoopCharacteristicsType('ParallelMultiInstance');
updateLoopCondition('${ nrOfCompletedInstances > 0 }');
}
};
/**
* -----新版本多实例-----
*/
const approveMethod = ref();
const approveRatio = ref(100);
const otherExtensions = ref();
const getElementLoopNew = () => {
if (props.type === 'UserTask') {
const extensionElements =
bpmnElement.value.businessObject?.extensionElements ??
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] });
approveMethod.value = extensionElements.values.filter(
(ex) => ex.$type === `${prefix}:ApproveMethod`,
)?.[0]?.value;
otherExtensions.value =
extensionElements.values.filter(
(ex) => ex.$type !== `${prefix}:ApproveMethod`,
) ?? [];
if (!approveMethod.value) {
approveMethod.value = ApproveMethodType.SEQUENTIAL_APPROVE;
updateLoopCharacteristics();
}
}
};
const onApproveMethodChange = () => {
approveRatio.value = 100;
updateLoopCharacteristics();
};
const onApproveRatioChange = () => {
updateLoopCharacteristics();
};
const updateLoopCharacteristics = () => {
// 根据ApproveMethod生成multiInstanceLoopCharacteristics节点
if (approveMethod.value === ApproveMethodType.RANDOM_SELECT_ONE_APPROVE) {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
loopCharacteristics: null,
});
} else {
if (approveMethod.value === ApproveMethodType.APPROVE_BY_RATIO) {
multiLoopInstance.value = bpmnInstances().moddle.create(
'bpmn:MultiInstanceLoopCharacteristics',
{ isSequential: false, collection: '${coll_userList}' },
);
multiLoopInstance.value.completionCondition =
bpmnInstances().moddle.create('bpmn:FormalExpression', {
body:
'${ nrOfCompletedInstances/nrOfInstances >= ' +
approveRatio.value / 100 +
'}',
});
}
if (approveMethod.value === ApproveMethodType.ANY_APPROVE) {
multiLoopInstance.value = bpmnInstances().moddle.create(
'bpmn:MultiInstanceLoopCharacteristics',
{ isSequential: false, collection: '${coll_userList}' },
);
multiLoopInstance.value.completionCondition =
bpmnInstances().moddle.create('bpmn:FormalExpression', {
body: '${ nrOfCompletedInstances > 0 }',
});
}
if (approveMethod.value === ApproveMethodType.SEQUENTIAL_APPROVE) {
multiLoopInstance.value = bpmnInstances().moddle.create(
'bpmn:MultiInstanceLoopCharacteristics',
{ isSequential: true, collection: '${coll_userList}' },
);
multiLoopInstance.value.loopCardinality = bpmnInstances().moddle.create(
'bpmn:FormalExpression',
{
body: '1',
},
);
multiLoopInstance.value.completionCondition =
bpmnInstances().moddle.create('bpmn:FormalExpression', {
body: '${ nrOfCompletedInstances >= nrOfInstances }',
});
}
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
loopCharacteristics: toRaw(multiLoopInstance.value),
});
}
// 添加ApproveMethod到ExtensionElements
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
values: [
...otherExtensions.value,
bpmnInstances().moddle.create(`${prefix}:ApproveMethod`, {
value: approveMethod.value,
}),
],
});
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
extensionElements: extensions,
});
};
onBeforeUnmount(() => {
multiLoopInstance.value = null;
bpmnElement.value = null;
});
watch(
() => props.id,
(val) => {
if (val) {
nextTick(() => {
bpmnElement.value = bpmnInstances().bpmnElement;
// getElementLoop(val)
getElementLoopNew();
});
}
},
{ immediate: true },
);
</script>

View File

@@ -0,0 +1,60 @@
<template>
<div class="panel-tab__content">
<div class="element-property input-property">
<div class="element-property__label">元素文档</div>
<div class="element-property__value">
<el-input
type="textarea"
v-model="documentation"
resize="vertical"
:autosize="{ minRows: 2, maxRows: 4 }"
@input="updateDocumentation"
@blur="updateDocumentation"
/>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: 'ElementOtherConfig' });
const props = defineProps({
id: String,
});
const documentation = ref('');
const bpmnElement = ref();
const bpmnInstances = () => (window as any).bpmnInstances;
const updateDocumentation = () => {
(bpmnElement.value && bpmnElement.value.id === props.id) ||
(bpmnElement.value = bpmnInstances().elementRegistry.get(props.id));
const documentations = bpmnInstances().bpmnFactory.create(
'bpmn:Documentation',
{
text: documentation.value,
},
);
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
documentation: [documentations],
});
};
onBeforeUnmount(() => {
bpmnElement.value = null;
});
watch(
() => props.id,
(id) => {
if (id && id.length) {
nextTick(() => {
const documentations =
bpmnInstances().bpmnElement.businessObject?.documentation;
documentation.value =
documentations && documentations.length ? documentations[0].text : '';
});
} else {
documentation.value = '';
}
},
{ immediate: true },
);
</script>

View File

@@ -0,0 +1,195 @@
<template>
<div class="panel-tab__content">
<el-table :data="elementPropertyList" max-height="240" fit border>
<el-table-column label="序号" width="50px" type="index" />
<el-table-column
label="属性名"
prop="name"
min-width="100px"
show-overflow-tooltip
/>
<el-table-column
label="属性值"
prop="value"
min-width="100px"
show-overflow-tooltip
/>
<el-table-column label="操作" width="110px">
<template #default="scope">
<el-button
link
@click="openAttributesForm(scope.row, scope.$index)"
size="small"
>
编辑
</el-button>
<el-divider direction="vertical" />
<el-button
link
size="small"
style="color: #ff4d4f"
@click="removeAttributes(scope.row, scope.$index)"
>
移除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="element-drawer__button">
<XButton
type="primary"
preIcon="ep:plus"
title="添加属性"
@click="openAttributesForm(null, -1)"
/>
</div>
<el-dialog
v-model="propertyFormModelVisible"
title="属性配置"
width="600px"
append-to-body
destroy-on-close
>
<el-form :model="propertyForm" label-width="80px" ref="attributeFormRef">
<el-form-item label="属性名:" prop="name">
<el-input v-model="propertyForm.name" clearable />
</el-form-item>
<el-form-item label="属性值:" prop="value">
<el-input v-model="propertyForm.value" clearable />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="propertyFormModelVisible = false"> </el-button>
<el-button type="primary" @click="saveAttribute"> </el-button>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ElMessageBox } from 'element-plus';
defineOptions({ name: 'ElementProperties' });
const props = defineProps({
id: String,
type: String,
});
const prefix = inject('prefix');
// const width = inject('width')
const elementPropertyList = ref<any[]>([]);
const propertyForm = ref<any>({});
const editingPropertyIndex = ref(-1);
const propertyFormModelVisible = ref(false);
const bpmnElement = ref();
const otherExtensionList = ref();
const bpmnElementProperties = ref();
const bpmnElementPropertyList = ref();
const attributeFormRef = ref();
const bpmnInstances = () => (window as any)?.bpmnInstances;
const resetAttributesList = () => {
bpmnElement.value = bpmnInstances().bpmnElement;
otherExtensionList.value = []; // 其他扩展配置
bpmnElementProperties.value =
// bpmnElement.value.businessObject?.extensionElements?.filter((ex) => {
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
(ex) => {
if (ex.$type !== `${prefix}:Properties`) {
otherExtensionList.value.push(ex);
}
return ex.$type === `${prefix}:Properties`;
},
) ?? [];
// 保存所有的 扩展属性字段
bpmnElementPropertyList.value = bpmnElementProperties.value.reduce(
(pre, current) => pre.concat(current.values),
[],
);
// 复制 显示
elementPropertyList.value = JSON.parse(
JSON.stringify(bpmnElementPropertyList.value ?? []),
);
};
const openAttributesForm = (attr, index) => {
editingPropertyIndex.value = index;
propertyForm.value = index === -1 ? {} : JSON.parse(JSON.stringify(attr));
propertyFormModelVisible.value = true;
nextTick(() => {
if (attributeFormRef.value) attributeFormRef.value.clearValidate();
});
};
const removeAttributes = (attr, index) => {
console.log(attr, 'attr');
ElMessageBox.confirm('确认移除该属性吗?', '提示', {
confirmButtonText: '确 认',
cancelButtonText: '取 消',
})
.then(() => {
elementPropertyList.value.splice(index, 1);
bpmnElementPropertyList.value.splice(index, 1);
// 新建一个属性字段的保存列表
const propertiesObject = bpmnInstances().moddle.create(
`${prefix}:Properties`,
{
values: bpmnElementPropertyList.value,
},
);
updateElementExtensions(propertiesObject);
resetAttributesList();
})
.catch(() => console.info('操作取消'));
};
const saveAttribute = () => {
console.log(propertyForm.value, 'propertyForm.value');
const { name, value } = propertyForm.value;
if (editingPropertyIndex.value !== -1) {
bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnElement.value),
toRaw(bpmnElementPropertyList.value)[toRaw(editingPropertyIndex.value)],
{
name,
value,
},
);
} else {
// 新建属性字段
const newPropertyObject = bpmnInstances().moddle.create(
`${prefix}:Property`,
{
name,
value,
},
);
// 新建一个属性字段的保存列表
const propertiesObject = bpmnInstances().moddle.create(
`${prefix}:Properties`,
{
values: bpmnElementPropertyList.value.concat([newPropertyObject]),
},
);
updateElementExtensions(propertiesObject);
}
propertyFormModelVisible.value = false;
resetAttributesList();
};
const updateElementExtensions = (properties) => {
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
values: otherExtensionList.value.concat([properties]),
});
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
extensionElements: extensions,
});
};
watch(
() => props.id,
(val) => {
if (val) {
val && val.length && resetAttributesList();
}
},
{ immediate: true },
);
</script>

View File

@@ -0,0 +1,159 @@
<template>
<div class="panel-tab__content">
<div class="panel-tab__content--title">
<span
><Icon
icon="ep:menu"
style="margin-right: 8px; color: #555"
/>消息列表</span
>
<XButton
type="primary"
title="创建新消息"
preIcon="ep:plus"
@click="openModel('message')"
/>
</div>
<el-table :data="messageList" border>
<el-table-column type="index" label="序号" width="60px" />
<el-table-column
label="消息ID"
prop="id"
max-width="300px"
show-overflow-tooltip
/>
<el-table-column
label="消息名称"
prop="name"
max-width="300px"
show-overflow-tooltip
/>
</el-table>
<div
class="panel-tab__content--title"
style="padding-top: 8px; margin-top: 8px; border-top: 1px solid #eee"
>
<span
><Icon
icon="ep:menu"
style="margin-right: 8px; color: #555"
/>信号列表</span
>
<XButton
type="primary"
title="创建新信号"
preIcon="ep:plus"
@click="openModel('signal')"
/>
</div>
<el-table :data="signalList" border>
<el-table-column type="index" label="序号" width="60px" />
<el-table-column
label="信号ID"
prop="id"
max-width="300px"
show-overflow-tooltip
/>
<el-table-column
label="信号名称"
prop="name"
max-width="300px"
show-overflow-tooltip
/>
</el-table>
<el-dialog
v-model="dialogVisible"
:title="modelConfig.title"
:close-on-click-modal="false"
width="400px"
append-to-body
destroy-on-close
>
<el-form :model="modelObjectForm" label-width="90px">
<el-form-item :label="modelConfig.idLabel">
<el-input v-model="modelObjectForm.id" clearable />
</el-form-item>
<el-form-item :label="modelConfig.nameLabel">
<el-input v-model="modelObjectForm.name" clearable />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="addNewObject"> </el-button>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: 'SignalAndMassage' });
const message = useMessage();
const signalList = ref<any[]>([]);
const messageList = ref<any[]>([]);
const dialogVisible = ref(false);
const modelType = ref('');
const modelObjectForm = ref<any>({});
const rootElements = ref();
const messageIdMap = ref();
const signalIdMap = ref();
const modelConfig = computed(() => {
if (modelType.value === 'message') {
return { title: '创建消息', idLabel: '消息ID', nameLabel: '消息名称' };
} else {
return { title: '创建信号', idLabel: '信号ID', nameLabel: '信号名称' };
}
});
const bpmnInstances = () => (window as any)?.bpmnInstances;
const initDataList = () => {
console.log(window, 'window');
rootElements.value = bpmnInstances().modeler.getDefinitions().rootElements;
messageIdMap.value = {};
signalIdMap.value = {};
messageList.value = [];
signalList.value = [];
rootElements.value.forEach((el) => {
if (el.$type === 'bpmn:Message') {
messageIdMap.value[el.id] = true;
messageList.value.push({ ...el });
}
if (el.$type === 'bpmn:Signal') {
signalIdMap.value[el.id] = true;
signalList.value.push({ ...el });
}
});
};
const openModel = (type) => {
modelType.value = type;
modelObjectForm.value = {};
dialogVisible.value = true;
};
const addNewObject = () => {
if (modelType.value === 'message') {
if (messageIdMap.value[modelObjectForm.value.id]) {
message.error('该消息已存在请修改id后重新保存');
}
const messageRef = bpmnInstances().moddle.create(
'bpmn:Message',
modelObjectForm.value,
);
rootElements.value.push(messageRef);
} else {
if (signalIdMap.value[modelObjectForm.value.id]) {
message.error('该信号已存在请修改id后重新保存');
}
const signalRef = bpmnInstances().moddle.create(
'bpmn:Signal',
modelObjectForm.value,
);
rootElements.value.push(signalRef);
}
dialogVisible.value = false;
initDataList();
};
onMounted(() => {
initDataList();
});
</script>

View File

@@ -0,0 +1,81 @@
<template>
<div class="panel-tab__content">
<el-form size="small" label-width="90px">
<!-- add by 芋艿由于异步延续暂时用不到所以这里 display none -->
<el-form-item label="异步延续" style="display: none">
<el-checkbox
v-model="taskConfigForm.asyncBefore"
label="异步前"
value="异步前"
@change="changeTaskAsync"
/>
<el-checkbox
v-model="taskConfigForm.asyncAfter"
label="异步后"
value="异步后"
@change="changeTaskAsync"
/>
<el-checkbox
v-model="taskConfigForm.exclusive"
v-if="taskConfigForm.asyncAfter || taskConfigForm.asyncBefore"
label="排除"
value="排除"
@change="changeTaskAsync"
/>
</el-form-item>
<component :is="witchTaskComponent" v-bind="$props" />
</el-form>
</div>
</template>
<script lang="ts" setup>
import { installedComponent } from './data';
defineOptions({ name: 'ElementTaskConfig' });
const props = defineProps({
id: String,
type: String,
});
const taskConfigForm = ref({
asyncAfter: false,
asyncBefore: false,
exclusive: false,
});
const witchTaskComponent = ref();
const bpmnElement = ref();
const bpmnInstances = () => (window as any).bpmnInstances;
const changeTaskAsync = () => {
if (!taskConfigForm.value.asyncBefore && !taskConfigForm.value.asyncAfter) {
taskConfigForm.value.exclusive = false;
}
bpmnInstances().modeling.updateProperties(bpmnInstances().bpmnElement, {
...taskConfigForm.value,
});
};
watch(
() => props.id,
() => {
bpmnElement.value = bpmnInstances().bpmnElement;
taskConfigForm.value.asyncBefore =
bpmnElement.value?.businessObject?.asyncBefore;
taskConfigForm.value.asyncAfter =
bpmnElement.value?.businessObject?.asyncAfter;
taskConfigForm.value.exclusive =
bpmnElement.value?.businessObject?.exclusive;
},
{ immediate: true },
);
watch(
() => props.type,
() => {
if (props.type) {
witchTaskComponent.value = installedComponent[props.type].component;
}
},
{ immediate: true },
);
</script>

View File

@@ -0,0 +1,36 @@
import CallActivity from './task-components/CallActivity.vue';
import ReceiveTask from './task-components/ReceiveTask.vue';
import ScriptTask from './task-components/ScriptTask.vue';
import ServiceTask from './task-components/ServiceTask.vue';
import UserTask from './task-components/UserTask.vue';
export const installedComponent = {
UserTask: {
name: '用户任务',
component: UserTask,
},
ServiceTask: {
name: '服务任务',
component: ServiceTask,
},
ScriptTask: {
name: '脚本任务',
component: ScriptTask,
},
ReceiveTask: {
name: '接收任务',
component: ReceiveTask,
},
CallActivity: {
name: '调用活动',
component: CallActivity,
},
};
export const getTaskCollapseItemName = (elementType) => {
return installedComponent[elementType].name;
};
export const isTaskCollapseItemShow = (elementType) => {
return installedComponent[elementType];
};

View File

@@ -0,0 +1,325 @@
<template>
<div>
<el-form label-width="100px">
<el-form-item label="实例名称" prop="processInstanceName">
<el-input
v-model="formData.processInstanceName"
clearable
placeholder="请输入实例名称"
@change="updateCallActivityAttr('processInstanceName')"
/>
</el-form-item>
<!-- TODO 需要可选择已存在的流程 -->
<el-form-item label="被调用流程" prop="calledElement">
<el-input
v-model="formData.calledElement"
clearable
placeholder="请输入被调用流程"
@change="updateCallActivityAttr('calledElement')"
/>
</el-form-item>
<el-form-item label="继承变量" prop="inheritVariables">
<el-switch
v-model="formData.inheritVariables"
@change="updateCallActivityAttr('inheritVariables')"
/>
</el-form-item>
<el-form-item label="继承业务键" prop="inheritBusinessKey">
<el-switch
v-model="formData.inheritBusinessKey"
@change="updateCallActivityAttr('inheritBusinessKey')"
/>
</el-form-item>
<el-form-item
v-if="!formData.inheritBusinessKey"
label="业务键表达式"
prop="businessKey"
>
<el-input
v-model="formData.businessKey"
clearable
placeholder="请输入业务键表达式"
@change="updateCallActivityAttr('businessKey')"
/>
</el-form-item>
<el-divider />
<div>
<div class="mb-10px flex">
<el-text>输入参数</el-text>
<XButton
class="ml-auto"
type="primary"
preIcon="ep:plus"
title="添加参数"
size="small"
@click="openVariableForm('in', null, -1)"
/>
</div>
<el-table :data="inVariableList" max-height="240" fit border>
<el-table-column
label="源"
prop="source"
min-width="100px"
show-overflow-tooltip
/>
<el-table-column
label="目标"
prop="target"
min-width="100px"
show-overflow-tooltip
/>
<el-table-column label="操作" width="110px">
<template #default="scope">
<el-button
link
@click="openVariableForm('in', scope.row, scope.$index)"
size="small"
>
编辑
</el-button>
<el-divider direction="vertical" />
<el-button
link
size="small"
style="color: #ff4d4f"
@click="removeVariable('in', scope.$index)"
>
移除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<el-divider />
<div>
<div class="mb-10px flex">
<el-text>输出参数</el-text>
<XButton
class="ml-auto"
type="primary"
preIcon="ep:plus"
title="添加参数"
size="small"
@click="openVariableForm('out', null, -1)"
/>
</div>
<el-table :data="outVariableList" max-height="240" fit border>
<el-table-column
label="源"
prop="source"
min-width="100px"
show-overflow-tooltip
/>
<el-table-column
label="目标"
prop="target"
min-width="100px"
show-overflow-tooltip
/>
<el-table-column label="操作" width="110px">
<template #default="scope">
<el-button
link
@click="openVariableForm('out', scope.row, scope.$index)"
size="small"
>
编辑
</el-button>
<el-divider direction="vertical" />
<el-button
link
size="small"
style="color: #ff4d4f"
@click="removeVariable('out', scope.$index)"
>
移除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-form>
<!-- 添加或修改参数 -->
<el-dialog
v-model="variableDialogVisible"
title="参数配置"
width="600px"
append-to-body
destroy-on-close
>
<el-form
:model="varialbeFormData"
label-width="80px"
ref="varialbeFormRef"
>
<el-form-item label="源:" prop="source">
<el-input v-model="varialbeFormData.source" clearable />
</el-form-item>
<el-form-item label="目标:" prop="target">
<el-input v-model="varialbeFormData.target" clearable />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="variableDialogVisible = false"> </el-button>
<el-button type="primary" @click="saveVariable"> </el-button>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: 'CallActivity' });
const props = defineProps({
id: String,
type: String,
});
const prefix = inject('prefix');
const message = useMessage();
const formData = ref({
processInstanceName: '',
calledElement: '',
inheritVariables: false,
businessKey: '',
inheritBusinessKey: false,
calledElementType: 'key',
});
const inVariableList = ref();
const outVariableList = ref();
const variableType = ref(); // 参数类型
const editingVariableIndex = ref(-1); // 编辑参数下标
const variableDialogVisible = ref(false);
const varialbeFormRef = ref();
const varialbeFormData = ref({
source: '',
target: '',
});
const bpmnInstances = () => (window as any)?.bpmnInstances;
const bpmnElement = ref();
const otherExtensionList = ref();
const initCallActivity = () => {
bpmnElement.value = bpmnInstances().bpmnElement;
console.log(bpmnElement.value.businessObject, 'callActivity');
// 初始化所有配置项
Object.keys(formData.value).forEach((key) => {
formData.value[key] =
bpmnElement.value.businessObject[key] ?? formData.value[key];
});
otherExtensionList.value = []; // 其他扩展配置
inVariableList.value = [];
outVariableList.value = [];
// 初始化输入参数
bpmnElement.value.businessObject?.extensionElements?.values?.forEach((ex) => {
if (ex.$type === `${prefix}:In`) {
inVariableList.value.push(ex);
} else if (ex.$type === `${prefix}:Out`) {
outVariableList.value.push(ex);
} else {
otherExtensionList.value.push(ex);
}
});
// 默认添加
// bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
// calledElementType: 'key'
// })
};
const updateCallActivityAttr = (attr) => {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
[attr]: formData.value[attr],
});
};
const openVariableForm = (type, data, index) => {
editingVariableIndex.value = index;
variableType.value = type;
varialbeFormData.value = index === -1 ? {} : { ...data };
variableDialogVisible.value = true;
};
const removeVariable = async (type, index) => {
try {
await message.delConfirm();
if (type === 'in') {
inVariableList.value.splice(index, 1);
}
if (type === 'out') {
outVariableList.value.splice(index, 1);
}
updateElementExtensions();
} catch {}
};
const saveVariable = () => {
if (editingVariableIndex.value === -1) {
if (variableType.value === 'in') {
inVariableList.value.push(
bpmnInstances().moddle.create(`${prefix}:In`, {
...varialbeFormData.value,
}),
);
}
if (variableType.value === 'out') {
outVariableList.value.push(
bpmnInstances().moddle.create(`${prefix}:Out`, {
...varialbeFormData.value,
}),
);
}
updateElementExtensions();
} else {
if (variableType.value === 'in') {
inVariableList.value[editingVariableIndex.value].source =
varialbeFormData.value.source;
inVariableList.value[editingVariableIndex.value].target =
varialbeFormData.value.target;
}
if (variableType.value === 'out') {
outVariableList.value[editingVariableIndex.value].source =
varialbeFormData.value.source;
outVariableList.value[editingVariableIndex.value].target =
varialbeFormData.value.target;
}
}
variableDialogVisible.value = false;
};
const updateElementExtensions = () => {
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
values: [
...inVariableList.value,
...outVariableList.value,
...otherExtensionList.value,
],
});
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
extensionElements: extensions,
});
};
watch(
() => props.id,
(val) => {
val &&
val.length &&
nextTick(() => {
initCallActivity();
});
},
{ immediate: true },
);
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,81 @@
<!-- 表达式选择 -->
<template>
<Dialog title="请选择表达式" v-model="dialogVisible" width="1024px">
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
>
<el-table-column label="名字" align="center" prop="name" />
<el-table-column label="表达式" align="center" prop="expression" />
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button link type="primary" @click="select(scope.row)">
选择
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
</Dialog>
</template>
<script setup lang="ts">
import { CommonStatusEnum } from '@/utils/constants';
import {
ProcessExpressionApi,
ProcessExpressionVO,
} from '@/api/bpm/processExpression';
/** BPM 流程 表单 */
defineOptions({ name: 'ProcessExpressionDialog' });
const dialogVisible = ref(false); // 弹窗的是否展示
const loading = ref(true); // 列表的加载中
const list = ref<ProcessExpressionVO[]>([]); // 列表的数据
const total = ref(0); // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
type: '',
status: CommonStatusEnum.ENABLE,
});
/** 打开弹窗 */
const open = (type: string) => {
queryParams.pageNo = 1;
queryParams.type = type;
getList();
dialogVisible.value = true;
};
defineExpose({ open }); // 提供 open 方法,用于打开弹窗
/** 查询列表 */
const getList = async () => {
loading.value = true;
try {
const data =
await ProcessExpressionApi.getProcessExpressionPage(queryParams);
list.value = data.list;
total.value = data.total;
} finally {
loading.value = false;
}
};
/** 提交表单 */
const emit = defineEmits(['success']); // 定义 success 事件,用于操作成功后的回调
const select = async (row) => {
dialogVisible.value = false;
// 发送操作成功的事件
emit('select', row);
};
</script>

View File

@@ -0,0 +1,132 @@
<template>
<div style="margin-top: 16px">
<el-form-item label="消息实例">
<div
style="
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
"
>
<el-select v-model="bindMessageId" @change="updateTaskMessage">
<el-option
v-for="key in Object.keys(messageMap)"
:value="key"
:label="messageMap[key]"
:key="key"
/>
</el-select>
<XButton
type="primary"
preIcon="ep:plus"
style="margin-left: 8px"
@click="openMessageModel"
/>
</div>
</el-form-item>
<el-dialog
v-model="messageModelVisible"
:close-on-click-modal="false"
title="创建新消息"
width="400px"
append-to-body
destroy-on-close
>
<el-form :model="newMessageForm" size="small" label-width="90px">
<el-form-item label="消息ID">
<el-input v-model="newMessageForm.id" clearable />
</el-form-item>
<el-form-item label="消息名称">
<el-input v-model="newMessageForm.name" clearable />
</el-form-item>
</el-form>
<template #footer>
<el-button size="small" type="primary" @click="createNewMessage"
> </el-button
>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: 'ReceiveTask' });
const props = defineProps({
id: String,
type: String,
});
const message = useMessage();
const bindMessageId = ref('');
const newMessageForm = ref<any>({});
const messageMap = ref<any>({});
const messageModelVisible = ref(false);
const bpmnElement = ref<any>();
const bpmnMessageRefsMap = ref<any>();
const bpmnRootElements = ref<any>();
const bpmnInstances = () => (window as any).bpmnInstances;
const getBindMessage = () => {
bpmnElement.value = bpmnInstances().bpmnElement;
bindMessageId.value =
bpmnElement.value.businessObject?.messageRef?.id || '-1';
};
const openMessageModel = () => {
messageModelVisible.value = true;
newMessageForm.value = {};
};
const createNewMessage = () => {
if (messageMap.value[newMessageForm.value.id]) {
message.error('该消息已存在请修改id后重新保存');
return;
}
const newMessage = bpmnInstances().moddle.create(
'bpmn:Message',
newMessageForm.value,
);
bpmnRootElements.value.push(newMessage);
messageMap.value[newMessageForm.value.id] = newMessageForm.value.name;
bpmnMessageRefsMap.value[newMessageForm.value.id] = newMessage;
messageModelVisible.value = false;
};
const updateTaskMessage = (messageId) => {
if (messageId === '-1') {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
messageRef: null,
});
} else {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
messageRef: bpmnMessageRefsMap.value[messageId],
});
}
};
onMounted(() => {
bpmnMessageRefsMap.value = Object.create(null);
bpmnRootElements.value =
bpmnInstances().modeler.getDefinitions().rootElements;
bpmnRootElements.value
.filter((el) => el.$type === 'bpmn:Message')
.forEach((m) => {
bpmnMessageRefsMap.value[m.id] = m;
messageMap.value[m.id] = m.name;
});
messageMap.value['-1'] = '无';
});
onBeforeUnmount(() => {
bpmnElement.value = null;
});
watch(
() => props.id,
() => {
// bpmnElement.value = bpmnInstances().bpmnElement
nextTick(() => {
getBindMessage();
});
},
{ immediate: true },
);
</script>

View File

@@ -0,0 +1,105 @@
<template>
<div style="margin-top: 16px">
<el-form-item label="脚本格式">
<el-input
v-model="scriptTaskForm.scriptFormat"
clearable
@input="updateElementTask()"
@change="updateElementTask()"
/>
</el-form-item>
<el-form-item label="脚本类型">
<el-select v-model="scriptTaskForm.scriptType">
<el-option label="内联脚本" value="inline" />
<el-option label="外部资源" value="external" />
</el-select>
</el-form-item>
<el-form-item label="脚本" v-show="scriptTaskForm.scriptType === 'inline'">
<el-input
v-model="scriptTaskForm.script"
type="textarea"
resize="vertical"
:autosize="{ minRows: 2, maxRows: 4 }"
clearable
@input="updateElementTask()"
@change="updateElementTask()"
/>
</el-form-item>
<el-form-item
label="资源地址"
v-show="scriptTaskForm.scriptType === 'external'"
>
<el-input
v-model="scriptTaskForm.resource"
clearable
@input="updateElementTask()"
@change="updateElementTask()"
/>
</el-form-item>
<el-form-item label="结果变量">
<el-input
v-model="scriptTaskForm.resultVariable"
clearable
@input="updateElementTask()"
@change="updateElementTask()"
/>
</el-form-item>
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: 'ScriptTask' });
const props = defineProps({
id: String,
type: String,
});
const defaultTaskForm = ref({
scriptFormat: '',
script: '',
resource: '',
resultVariable: '',
});
const scriptTaskForm = ref<any>({});
const bpmnElement = ref();
const bpmnInstances = () => (window as any)?.bpmnInstances;
const resetTaskForm = () => {
for (let key in defaultTaskForm.value) {
let value =
bpmnElement.value?.businessObject[key] || defaultTaskForm.value[key];
scriptTaskForm.value[key] = value;
}
scriptTaskForm.value.scriptType = scriptTaskForm.value.script
? 'inline'
: 'external';
};
const updateElementTask = () => {
let taskAttr = Object.create(null);
taskAttr.scriptFormat = scriptTaskForm.value.scriptFormat || null;
taskAttr.resultVariable = scriptTaskForm.value.resultVariable || null;
if (scriptTaskForm.value.scriptType === 'inline') {
taskAttr.script = scriptTaskForm.value.script || null;
taskAttr.resource = null;
} else {
taskAttr.resource = scriptTaskForm.value.resource || null;
taskAttr.script = null;
}
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr);
};
onBeforeUnmount(() => {
bpmnElement.value = null;
});
watch(
() => props.id,
() => {
bpmnElement.value = bpmnInstances().bpmnElement;
nextTick(() => {
resetTaskForm();
});
},
{ immediate: true },
);
</script>

View File

@@ -0,0 +1,103 @@
<template>
<div>
<el-form-item label="执行类型" key="executeType">
<el-select v-model="serviceTaskForm.executeType">
<el-option label="Java类" value="class" />
<el-option label="表达式" value="expression" />
<el-option label="代理表达式" value="delegateExpression" />
</el-select>
</el-form-item>
<el-form-item
v-if="serviceTaskForm.executeType === 'class'"
label="Java类"
prop="class"
key="execute-class"
>
<el-input
v-model="serviceTaskForm.class"
clearable
@change="updateElementTask"
/>
</el-form-item>
<el-form-item
v-if="serviceTaskForm.executeType === 'expression'"
label="表达式"
prop="expression"
key="execute-expression"
>
<el-input
v-model="serviceTaskForm.expression"
clearable
@change="updateElementTask"
/>
</el-form-item>
<el-form-item
v-if="serviceTaskForm.executeType === 'delegateExpression'"
label="代理表达式"
prop="delegateExpression"
key="execute-delegate"
>
<el-input
v-model="serviceTaskForm.delegateExpression"
clearable
@change="updateElementTask"
/>
</el-form-item>
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: 'ServiceTask' });
const props = defineProps({
id: String,
type: String,
});
const defaultTaskForm = ref({
executeType: '',
class: '',
expression: '',
delegateExpression: '',
});
const serviceTaskForm = ref<any>({});
const bpmnElement = ref();
const bpmnInstances = () => (window as any)?.bpmnInstances;
const resetTaskForm = () => {
for (let key in defaultTaskForm.value) {
let value =
bpmnElement.value?.businessObject[key] || defaultTaskForm.value[key];
serviceTaskForm.value[key] = value;
if (value) {
serviceTaskForm.value.executeType = key;
}
}
};
const updateElementTask = () => {
let taskAttr = Object.create(null);
const type = serviceTaskForm.value.executeType;
for (let key in serviceTaskForm.value) {
if (key !== 'executeType' && key !== type) taskAttr[key] = null;
}
taskAttr[type] = serviceTaskForm.value[type] || '';
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr);
};
onBeforeUnmount(() => {
bpmnElement.value = null;
});
watch(
() => props.id,
() => {
bpmnElement.value = bpmnInstances().bpmnElement;
nextTick(() => {
resetTaskForm();
});
},
{ immediate: true },
);
</script>

View File

@@ -0,0 +1,520 @@
<template>
<el-form label-width="120px">
<el-form-item label="规则类型" prop="candidateStrategy">
<el-select
v-model="userTaskForm.candidateStrategy"
clearable
style="width: 100%"
@change="changeCandidateStrategy"
>
<el-option
v-for="(dict, index) in CANDIDATE_STRATEGY"
:key="index"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="userTaskForm.candidateStrategy == CandidateStrategy.ROLE"
label="指定角色"
prop="candidateParam"
>
<el-select
v-model="userTaskForm.candidateParam"
clearable
multiple
style="width: 100%"
@change="updateElementTask"
>
<el-option
v-for="item in roleOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="
userTaskForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER ||
userTaskForm.candidateStrategy == CandidateStrategy.DEPT_LEADER ||
userTaskForm.candidateStrategy ==
CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
"
label="指定部门"
prop="candidateParam"
span="24"
>
<el-tree-select
ref="treeRef"
v-model="userTaskForm.candidateParam"
:data="deptTreeOptions"
:props="defaultProps"
empty-text="加载中,请稍后"
multiple
node-key="id"
show-checkbox
@change="updateElementTask"
/>
</el-form-item>
<el-form-item
v-if="userTaskForm.candidateStrategy == CandidateStrategy.POST"
label="指定岗位"
prop="candidateParam"
span="24"
>
<el-select
v-model="userTaskForm.candidateParam"
clearable
multiple
style="width: 100%"
@change="updateElementTask"
>
<el-option
v-for="item in postOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="userTaskForm.candidateStrategy == CandidateStrategy.USER"
label="指定用户"
prop="candidateParam"
span="24"
>
<el-select
v-model="userTaskForm.candidateParam"
clearable
multiple
style="width: 100%"
@change="updateElementTask"
>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="userTaskForm.candidateStrategy === CandidateStrategy.USER_GROUP"
label="指定用户组"
prop="candidateParam"
>
<el-select
v-model="userTaskForm.candidateParam"
clearable
multiple
style="width: 100%"
@change="updateElementTask"
>
<el-option
v-for="item in userGroupOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="userTaskForm.candidateStrategy === CandidateStrategy.FORM_USER"
label="表单内用户字段"
prop="formUser"
>
<el-select
v-model="userTaskForm.candidateParam"
clearable
style="width: 100%"
@change="handleFormUserChange"
>
<el-option
v-for="(item, idx) in userFieldOnFormOptions"
:key="idx"
:label="item.title"
:value="item.field"
:disabled="!item.required"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="
userTaskForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER
"
label="表单内部门字段"
prop="formDept"
>
<el-select
v-model="userTaskForm.candidateParam"
clearable
style="width: 100%"
@change="updateElementTask"
>
<el-option
v-for="(item, idx) in deptFieldOnFormOptions"
:key="idx"
:label="item.title"
:value="item.field"
:disabled="!item.required"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="
userTaskForm.candidateStrategy ==
CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
userTaskForm.candidateStrategy ==
CandidateStrategy.START_USER_DEPT_LEADER ||
userTaskForm.candidateStrategy ==
CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER ||
userTaskForm.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER
"
:label="deptLevelLabel!"
prop="deptLevel"
span="24"
>
<el-select v-model="deptLevel" clearable @change="updateElementTask">
<el-option
v-for="(item, index) in MULTI_LEVEL_DEPT"
:key="index"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="userTaskForm.candidateStrategy === CandidateStrategy.EXPRESSION"
label="流程表达式"
prop="candidateParam"
>
<el-input
type="textarea"
v-model="userTaskForm.candidateParam[0]"
clearable
style="width: 100%"
@change="updateElementTask"
/>
<XButton
class="!w-1/1 mt-5px"
type="success"
preIcon="ep:select"
title="选择表达式"
size="small"
@click="openProcessExpressionDialog"
/>
<!-- 选择弹窗 -->
<ProcessExpressionDialog
ref="processExpressionDialogRef"
@select="selectProcessExpression"
/>
</el-form-item>
<el-form-item label="跳过表达式" prop="skipExpression">
<el-input
type="textarea"
v-model="userTaskForm.skipExpression"
clearable
style="width: 100%"
@change="updateSkipExpression"
/>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import {
CANDIDATE_STRATEGY,
CandidateStrategy,
FieldPermissionType,
MULTI_LEVEL_DEPT,
} from '@/components/SimpleProcessDesignerV2/src/consts';
import { defaultProps, handleTree } from '@/utils/tree';
import * as RoleApi from '@/api/system/role';
import * as DeptApi from '@/api/system/dept';
import * as PostApi from '@/api/system/post';
import * as UserApi from '@/api/system/user';
import * as UserGroupApi from '@/api/bpm/userGroup';
import ProcessExpressionDialog from './ProcessExpressionDialog.vue';
import { ProcessExpressionVO } from '@/api/bpm/processExpression';
import { useFormFieldsPermission } from '@/components/SimpleProcessDesignerV2/src/node';
defineOptions({ name: 'UserTask' });
const props = defineProps({
id: String,
type: String,
});
const prefix = inject('prefix');
const userTaskForm = ref({
candidateStrategy: undefined, // 分配规则
candidateParam: [], // 分配选项
skipExpression: '', // 跳过表达式
});
const bpmnElement = ref();
const bpmnInstances = () => (window as any)?.bpmnInstances;
const roleOptions = ref<RoleApi.RoleVO[]>([]); // 角色列表
const deptTreeOptions = ref(); // 部门树
const postOptions = ref<PostApi.PostVO[]>([]); // 岗位列表
const userOptions = ref<UserApi.UserVO[]>([]); // 用户列表
const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]); // 用户组列表
const { formFieldOptions } = useFormFieldsPermission(FieldPermissionType.READ);
// 表单内用户字段选项, 必须是必填和用户选择器
const userFieldOnFormOptions = computed(() => {
return formFieldOptions.filter((item) => item.type === 'UserSelect');
});
// 表单内部门字段选项, 必须是必填和部门选择器
const deptFieldOnFormOptions = computed(() => {
return formFieldOptions.filter((item) => item.type === 'DeptSelect');
});
const deptLevel = ref(1);
const deptLevelLabel = computed(() => {
let label = '部门负责人来源';
if (
userTaskForm.value.candidateStrategy ==
CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
) {
label = label + '(指定部门向上)';
} else if (
userTaskForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER
) {
label = label + '(表单内部门向上)';
} else {
label = label + '(发起人部门向上)';
}
return label;
});
const otherExtensions = ref();
const resetTaskForm = () => {
const businessObject = bpmnElement.value.businessObject;
if (!businessObject) {
return;
}
const extensionElements =
businessObject?.extensionElements ??
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] });
userTaskForm.value.candidateStrategy = extensionElements.values?.filter(
(ex) => ex.$type === `${prefix}:CandidateStrategy`,
)?.[0]?.value;
const candidateParamStr = extensionElements.values?.filter(
(ex) => ex.$type === `${prefix}:CandidateParam`,
)?.[0]?.value;
if (candidateParamStr && candidateParamStr.length > 0) {
if (userTaskForm.value.candidateStrategy === CandidateStrategy.EXPRESSION) {
// 特殊:流程表达式,只有一个 input 输入框
userTaskForm.value.candidateParam = [candidateParamStr];
} else if (
userTaskForm.value.candidateStrategy ==
CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
) {
// 特殊:多级不部门负责人,需要通过'|'分割
userTaskForm.value.candidateParam = candidateParamStr
.split('|')[0]
.split(',')
.map((item) => {
// 如果数字超出了最大安全整数范围,则将其作为字符串处理
let num = Number(item);
return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER
? item
: num;
});
deptLevel.value = +candidateParamStr.split('|')[1];
} else if (
userTaskForm.value.candidateStrategy ==
CandidateStrategy.START_USER_DEPT_LEADER ||
userTaskForm.value.candidateStrategy ==
CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
) {
userTaskForm.value.candidateParam = +candidateParamStr;
deptLevel.value = +candidateParamStr;
} else if (
userTaskForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER
) {
userTaskForm.value.candidateParam = candidateParamStr.split('|')[0];
deptLevel.value = +candidateParamStr.split('|')[1];
} else {
userTaskForm.value.candidateParam = candidateParamStr
.split(',')
.map((item) => {
// 如果数字超出了最大安全整数范围,则将其作为字符串处理
let num = Number(item);
return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER
? item
: num;
});
}
} else {
userTaskForm.value.candidateParam = [];
}
otherExtensions.value =
extensionElements.values?.filter(
(ex) =>
ex.$type !== `${prefix}:CandidateStrategy` &&
ex.$type !== `${prefix}:CandidateParam`,
) ?? [];
// 跳过表达式
if (businessObject.skipExpression != undefined) {
userTaskForm.value.skipExpression = businessObject.skipExpression;
} else {
userTaskForm.value.skipExpression = '';
}
// 改用通过extensionElements来存储数据
return;
if (businessObject.candidateStrategy != undefined) {
userTaskForm.value.candidateStrategy = parseInt(
businessObject.candidateStrategy,
) as any;
} else {
userTaskForm.value.candidateStrategy = undefined;
}
if (
businessObject.candidateParam &&
businessObject.candidateParam.length > 0
) {
if (userTaskForm.value.candidateStrategy === 60) {
// 特殊:流程表达式,只有一个 input 输入框
userTaskForm.value.candidateParam = [businessObject.candidateParam];
} else {
userTaskForm.value.candidateParam = businessObject.candidateParam
.split(',')
.map((item) => item);
}
} else {
userTaskForm.value.candidateParam = [];
}
};
/** 更新 candidateStrategy 字段时,需要清空 candidateParam并触发 bpmn 图更新 */
const changeCandidateStrategy = () => {
userTaskForm.value.candidateParam = [];
deptLevel.value = 1;
// 注释 by 芋艿这个交互很多用户反馈费解https://t.zsxq.com/xNmas 所以暂时屏蔽
// if (userTaskForm.value.candidateStrategy === CandidateStrategy.FORM_USER) {
// // 特殊处理表单内用户字段,当只有发起人选项时应选中发起人
// if (!userFieldOnFormOptions.value || userFieldOnFormOptions.value.length <= 1) {
// userTaskForm.value.candidateStrategy = CandidateStrategy.START_USER
// }
// }
updateElementTask();
};
/** 选中某个 options 时候,更新 bpmn 图 */
const updateElementTask = () => {
let candidateParam =
userTaskForm.value.candidateParam instanceof Array
? userTaskForm.value.candidateParam.join(',')
: userTaskForm.value.candidateParam;
// 特殊处理多级部门情况
if (
userTaskForm.value.candidateStrategy ==
CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
userTaskForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER
) {
candidateParam += '|' + deptLevel.value;
}
// 特殊处理发起人部门负责人、发起人连续部门负责人
if (
userTaskForm.value.candidateStrategy ==
CandidateStrategy.START_USER_DEPT_LEADER ||
userTaskForm.value.candidateStrategy ==
CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
) {
candidateParam = deptLevel.value + '';
}
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
values: [
...otherExtensions.value,
bpmnInstances().moddle.create(`${prefix}:CandidateStrategy`, {
value: userTaskForm.value.candidateStrategy,
}),
bpmnInstances().moddle.create(`${prefix}:CandidateParam`, {
value: candidateParam,
}),
],
});
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
extensionElements: extensions,
});
// 改用通过extensionElements来存储数据
return;
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
candidateStrategy: userTaskForm.value.candidateStrategy,
candidateParam: userTaskForm.value.candidateParam.join(','),
});
};
const updateSkipExpression = () => {
if (
userTaskForm.value.skipExpression &&
userTaskForm.value.skipExpression !== ''
) {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
skipExpression: userTaskForm.value.skipExpression,
});
} else {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
skipExpression: null,
});
}
};
// 打开监听器弹窗
const processExpressionDialogRef = ref();
const openProcessExpressionDialog = async () => {
processExpressionDialogRef.value.open();
};
const selectProcessExpression = (expression: ProcessExpressionVO) => {
userTaskForm.value.candidateParam = [expression.expression];
updateElementTask();
};
const handleFormUserChange = (e) => {
if (e === 'PROCESS_START_USER_ID') {
userTaskForm.value.candidateParam = [];
userTaskForm.value.candidateStrategy = CandidateStrategy.START_USER;
}
updateElementTask();
};
watch(
() => props.id,
() => {
bpmnElement.value = bpmnInstances().bpmnElement;
nextTick(() => {
resetTaskForm();
});
},
{ immediate: true },
);
onMounted(async () => {
// 获得角色列表
roleOptions.value = await RoleApi.getSimpleRoleList();
// 获得部门列表
const deptOptions = await DeptApi.getSimpleDeptList();
deptTreeOptions.value = handleTree(deptOptions, 'id');
// 获得岗位列表
postOptions.value = await PostApi.getSimplePostList();
// 获得用户列表
userOptions.value = await UserApi.getSimpleUserList();
// 获得用户组列表
userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList();
});
onBeforeUnmount(() => {
bpmnElement.value = null;
});
</script>

View File

@@ -0,0 +1,342 @@
<template>
<el-tabs v-model="tab">
<el-tab-pane label="CRON表达式" name="cron">
<div style="margin-bottom: 10px">
<el-input
v-model="cronStr"
readonly
style="width: 400px; font-weight: bold"
:key="'cronStr'"
/>
</div>
<div style="display: flex; gap: 8px; margin-bottom: 8px">
<el-input
v-model="fields.second"
placeholder="秒"
style="width: 80px"
:key="'second'"
/>
<el-input
v-model="fields.minute"
placeholder="分"
style="width: 80px"
:key="'minute'"
/>
<el-input
v-model="fields.hour"
placeholder="时"
style="width: 80px"
:key="'hour'"
/>
<el-input
v-model="fields.day"
placeholder="天"
style="width: 80px"
:key="'day'"
/>
<el-input
v-model="fields.month"
placeholder="月"
style="width: 80px"
:key="'month'"
/>
<el-input
v-model="fields.week"
placeholder="周"
style="width: 80px"
:key="'week'"
/>
<el-input
v-model="fields.year"
placeholder="年"
style="width: 80px"
:key="'year'"
/>
</div>
<el-tabs v-model="activeField" type="card" style="margin-bottom: 8px">
<el-tab-pane
v-for="f in cronFieldList"
:label="f.label"
:name="f.key"
:key="f.key"
>
<div style="margin-bottom: 8px">
<el-radio-group v-model="cronMode[f.key]" :key="'radio-' + f.key">
<el-radio label="every" :key="'every-' + f.key"
>{{ f.label }}</el-radio
>
<el-radio label="range" :key="'range-' + f.key"
>
<el-input-number
v-model="cronRange[f.key][0]"
:min="f.min"
:max="f.max"
size="small"
style="width: 60px"
:key="'range0-' + f.key"
/>
<el-input-number
v-model="cronRange[f.key][1]"
:min="f.min"
:max="f.max"
size="small"
style="width: 60px"
:key="'range1-' + f.key"
/>
之间每{{ f.label }}</el-radio
>
<el-radio label="step" :key="'step-' + f.key"
>从第
<el-input-number
v-model="cronStep[f.key][0]"
:min="f.min"
:max="f.max"
size="small"
style="width: 60px"
:key="'step0-' + f.key"
/>
开始每
<el-input-number
v-model="cronStep[f.key][1]"
:min="1"
:max="f.max"
size="small"
style="width: 60px"
:key="'step1-' + f.key"
/>
{{ f.label }}</el-radio
>
<el-radio label="appoint" :key="'appoint-' + f.key"
>指定</el-radio
>
</el-radio-group>
</div>
<div v-if="cronMode[f.key] === 'appoint'">
<el-checkbox-group
v-model="cronAppoint[f.key]"
:key="'group-' + f.key"
>
<el-checkbox
v-for="n in f.max + 1"
:label="pad(n - 1)"
:key="'cb-' + f.key + '-' + (n - 1)"
>{{ pad(n - 1) }}</el-checkbox
>
</el-checkbox-group>
</div>
</el-tab-pane>
</el-tabs>
</el-tab-pane>
<el-tab-pane label="标准格式" name="iso" :key="'iso-tab'">
<div style="margin-bottom: 10px">
<el-input
v-model="isoStr"
placeholder="如R1/2025-05-21T21:59:54/P3DT30M30S"
style="width: 400px; font-weight: bold"
:key="'isoStr'"
/>
</div>
<div style="margin-bottom: 10px">
循环次数<el-input-number
v-model="repeat"
:min="1"
style="width: 100px"
:key="'repeat'"
/>
</div>
<div style="margin-bottom: 10px">
日期时间<el-date-picker
v-model="isoDate"
type="datetime"
placeholder="选择日期时间"
style="width: 200px"
:key="'isoDate'"
/>
</div>
<div style="margin-bottom: 10px">
当前时长<el-input
v-model="isoDuration"
placeholder="如P3DT30M30S"
style="width: 200px"
:key="'isoDuration'"
/>
</div>
<div>
<div>
<el-button
v-for="s in [5, 10, 30, 50]"
@click="setDuration('S', s)"
:key="'sec-' + s"
>{{ s }}</el-button
>自定义
</div>
<div>
<el-button
v-for="m in [5, 10, 30, 50]"
@click="setDuration('M', m)"
:key="'min-' + m"
>{{ m }}</el-button
>自定义
</div>
<div>
小时<el-button
v-for="h in [4, 8, 12, 24]"
@click="setDuration('H', h)"
:key="'hour-' + h"
>{{ h }}</el-button
>自定义
</div>
<div>
<el-button
v-for="d in [1, 2, 3, 4]"
@click="setDuration('D', d)"
:key="'day-' + d"
>{{ d }}</el-button
>自定义
</div>
<div>
<el-button
v-for="mo in [1, 2, 3, 4]"
@click="setDuration('M', mo)"
:key="'mon-' + mo"
>{{ mo }}</el-button
>自定义
</div>
<div>
<el-button
v-for="y in [1, 2, 3, 4]"
@click="setDuration('Y', y)"
:key="'year-' + y"
>{{ y }}</el-button
>自定义
</div>
</div>
</el-tab-pane>
</el-tabs>
</template>
<script setup>
import { ref, watch, computed } from 'vue';
const props = defineProps({ value: String });
const emit = defineEmits(['change']);
const tab = ref('cron');
const cronStr = ref(props.value || '* * * * * ?');
const fields = ref({
second: '*',
minute: '*',
hour: '*',
day: '*',
month: '*',
week: '?',
year: '',
});
const cronFieldList = [
{ key: 'second', label: '秒', min: 0, max: 59 },
{ key: 'minute', label: '分', min: 0, max: 59 },
{ key: 'hour', label: '时', min: 0, max: 23 },
{ key: 'day', label: '天', min: 1, max: 31 },
{ key: 'month', label: '月', min: 1, max: 12 },
{ key: 'week', label: '周', min: 1, max: 7 },
{ key: 'year', label: '年', min: 1970, max: 2099 },
];
const activeField = ref('second');
const cronMode = ref({
second: 'appoint',
minute: 'every',
hour: 'every',
day: 'every',
month: 'every',
week: 'every',
year: 'every',
});
const cronAppoint = ref({
second: ['00', '01'],
minute: [],
hour: [],
day: [],
month: [],
week: [],
year: [],
});
const cronRange = ref({
second: [0, 1],
minute: [0, 1],
hour: [0, 1],
day: [1, 2],
month: [1, 2],
week: [1, 2],
year: [1970, 1971],
});
const cronStep = ref({
second: [1, 1],
minute: [1, 1],
hour: [1, 1],
day: [1, 1],
month: [1, 1],
week: [1, 1],
year: [1970, 1],
});
function pad(n) {
return n < 10 ? '0' + n : '' + n;
}
watch(
[fields, cronMode, cronAppoint, cronRange, cronStep],
() => {
// 组装cron表达式
let arr = cronFieldList.map((f) => {
if (cronMode.value[f.key] === 'every') return '*';
if (cronMode.value[f.key] === 'appoint')
return cronAppoint.value[f.key].join(',') || '*';
if (cronMode.value[f.key] === 'range')
return `${cronRange.value[f.key][0]}-${cronRange.value[f.key][1]}`;
if (cronMode.value[f.key] === 'step')
return `${cronStep.value[f.key][0]}/${cronStep.value[f.key][1]}`;
return fields.value[f.key] || '*';
});
// week和year特殊处理
arr[5] = arr[5] || '?';
cronStr.value = arr.join(' ');
if (tab.value === 'cron') emit('change', cronStr.value);
},
{ deep: true },
);
// 标准格式
const isoStr = ref('');
const repeat = ref(1);
const isoDate = ref('');
const isoDuration = ref('');
function setDuration(type, val) {
// 组装ISO 8601字符串
let d = isoDuration.value;
if (!d.includes(type)) d += val + type;
else d = d.replace(new RegExp(`\\d+${type}`), val + type);
isoDuration.value = d;
updateIsoStr();
}
function updateIsoStr() {
let str = `R${repeat.value}`;
if (isoDate.value)
str +=
'/' +
(typeof isoDate.value === 'string'
? isoDate.value
: new Date(isoDate.value).toISOString());
if (isoDuration.value) str += '/' + isoDuration.value;
isoStr.value = str;
if (tab.value === 'iso') emit('change', isoStr.value);
}
watch([repeat, isoDate, isoDuration], updateIsoStr);
watch(
() => props.value,
(val) => {
if (!val) return;
if (tab.value === 'cron') cronStr.value = val;
if (tab.value === 'iso') isoStr.value = val;
},
{ immediate: true },
);
</script>

View File

@@ -0,0 +1,86 @@
<template>
<div>
<div style="margin-bottom: 10px">
当前选择<el-input v-model="isoString" readonly style="width: 300px" />
</div>
<div v-for="unit in units" :key="unit.key" style="margin-bottom: 8px">
<span>{{ unit.label }}</span>
<el-button-group>
<el-button
v-for="val in unit.presets"
:key="val"
size="mini"
@click="setUnit(unit.key, val)"
>{{ val }}</el-button
>
<el-input
v-model.number="custom[unit.key]"
size="mini"
style="width: 60px; margin-left: 8px"
placeholder="自定义"
@change="setUnit(unit.key, custom[unit.key])"
/>
</el-button-group>
</div>
</div>
</template>
<script setup>
import { ref, watch, computed } from 'vue';
const props = defineProps({ value: String });
const emit = defineEmits(['change']);
const units = [
{ key: 'Y', label: '年', presets: [1, 2, 3, 4] },
{ key: 'M', label: '月', presets: [1, 2, 3, 4] },
{ key: 'D', label: '天', presets: [1, 2, 3, 4] },
{ key: 'H', label: '时', presets: [4, 8, 12, 24] },
{ key: 'm', label: '分', presets: [5, 10, 30, 50] },
{ key: 'S', label: '秒', presets: [5, 10, 30, 50] },
];
const custom = ref({ Y: '', M: '', D: '', H: '', m: '', S: '' });
const isoString = ref('');
function setUnit(key, val) {
if (!val || isNaN(val)) {
custom.value[key] = '';
return;
}
custom.value[key] = val;
updateIsoString();
}
function updateIsoString() {
let str = 'P';
if (custom.value.Y) str += custom.value.Y + 'Y';
if (custom.value.M) str += custom.value.M + 'M';
if (custom.value.D) str += custom.value.D + 'D';
if (custom.value.H || custom.value.m || custom.value.S) str += 'T';
if (custom.value.H) str += custom.value.H + 'H';
if (custom.value.m) str += custom.value.m + 'M';
if (custom.value.S) str += custom.value.S + 'S';
isoString.value = str === 'P' ? '' : str;
emit('change', isoString.value);
}
watch(
() => props.value,
(val) => {
if (!val) return;
// 解析ISO 8601字符串到custom
const match = val.match(
/^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/,
);
if (match) {
custom.value.Y = match[1] || '';
custom.value.M = match[2] || '';
custom.value.D = match[3] || '';
custom.value.H = match[4] || '';
custom.value.m = match[5] || '';
custom.value.S = match[6] || '';
updateIsoString();
}
},
{ immediate: true },
);
</script>

View File

@@ -0,0 +1,345 @@
<template>
<div class="panel-tab__content">
<div style="margin-top: 10px">
<span>类型</span>
<el-button-group>
<el-button
size="mini"
:type="type === 'time' ? 'primary' : ''"
@click="setType('time')"
>时间</el-button
>
<el-button
size="mini"
:type="type === 'duration' ? 'primary' : ''"
@click="setType('duration')"
>持续</el-button
>
<el-button
size="mini"
:type="type === 'cycle' ? 'primary' : ''"
@click="setType('cycle')"
>循环</el-button
>
</el-button-group>
<el-icon v-if="valid" color="green" style="margin-left: 8px"
><CircleCheckFilled
/></el-icon>
</div>
<div style=" display: flex; align-items: center;margin-top: 10px">
<span>条件</span>
<el-input
v-model="condition"
:placeholder="placeholder"
style="width: calc(100% - 100px)"
:readonly="type !== 'duration' && type !== 'cycle'"
@focus="handleInputFocus"
@blur="updateNode"
>
<template #suffix>
<el-tooltip v-if="!valid" content="格式错误" placement="top">
<el-icon color="orange"><WarningFilled /></el-icon>
</el-tooltip>
<el-tooltip :content="helpText" placement="top">
<el-icon
color="#409EFF"
style="cursor: pointer"
@click="showHelp = true"
><QuestionFilled
/></el-icon>
</el-tooltip>
<el-button
v-if="type === 'time'"
@click="showDatePicker = true"
style="margin-left: 4px"
circle
size="small"
>
<Icon icon="ep:calendar" />
</el-button>
<el-button
v-if="type === 'duration'"
@click="showDurationDialog = true"
style="margin-left: 4px"
circle
size="small"
>
<Icon icon="ep:timer" />
</el-button>
<el-button
v-if="type === 'cycle'"
@click="showCycleDialog = true"
style="margin-left: 4px"
circle
size="small"
>
<Icon icon="ep:setting" />
</el-button>
</template>
</el-input>
</div>
<!-- 时间选择器 -->
<el-dialog
v-model="showDatePicker"
title="选择时间"
width="400px"
@close="showDatePicker = false"
>
<el-date-picker
v-model="dateValue"
type="datetime"
placeholder="选择日期时间"
style="width: 100%"
@change="onDateChange"
/>
<template #footer>
<el-button @click="showDatePicker = false">取消</el-button>
<el-button type="primary" @click="onDateConfirm">确定</el-button>
</template>
</el-dialog>
<!-- 持续时长选择器 -->
<el-dialog
v-model="showDurationDialog"
title="时间配置"
width="600px"
@close="showDurationDialog = false"
>
<DurationConfig :value="condition" @change="onDurationChange" />
<template #footer>
<el-button @click="showDurationDialog = false">取消</el-button>
<el-button type="primary" @click="onDurationConfirm">确定</el-button>
</template>
</el-dialog>
<!-- 循环配置器 -->
<el-dialog
v-model="showCycleDialog"
title="时间配置"
width="800px"
@close="showCycleDialog = false"
>
<CycleConfig :value="condition" @change="onCycleChange" />
<template #footer>
<el-button @click="showCycleDialog = false">取消</el-button>
<el-button type="primary" @click="onCycleConfirm">确定</el-button>
</template>
</el-dialog>
<!-- 帮助说明 -->
<el-dialog
v-model="showHelp"
title="格式说明"
width="600px"
@close="showHelp = false"
>
<div v-html="helpHtml"></div>
<template #footer>
<el-button @click="showHelp = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch, onMounted } from 'vue';
import {
CircleCheckFilled,
WarningFilled,
QuestionFilled,
} from '@element-plus/icons-vue';
import DurationConfig from './DurationConfig.vue';
import CycleConfig from './CycleConfig.vue';
import { createListenerObject, updateElementExtensions } from '../../utils';
const bpmnInstances = () => (window as any).bpmnInstances;
const props = defineProps({ businessObject: Object });
const type = ref('time');
const condition = ref('');
const valid = ref(true);
const showDatePicker = ref(false);
const showDurationDialog = ref(false);
const showCycleDialog = ref(false);
const showHelp = ref(false);
const dateValue = ref(null);
const bpmnElement = ref(null);
const placeholder = computed(() => {
if (type.value === 'time') return '请输入时间';
if (type.value === 'duration') return '请输入持续时长';
if (type.value === 'cycle') return '请输入循环表达式';
return '';
});
const helpText = computed(() => {
if (type.value === 'time') return '选择具体时间';
if (type.value === 'duration') return 'ISO 8601格式如PT1H';
if (type.value === 'cycle') return 'CRON表达式或ISO 8601周期';
return '';
});
const helpHtml = computed(() => {
if (type.value === 'duration') {
return `指定定时器之前要等待多长时间。S表示秒M表示分D表示天P表示时间段T表示精确到时间的时间段。<br>
时间格式依然为ISO 8601格式一年两个月三天四小时五分六秒内可以写成P1Y2M3DT4H5M6S。<br>
P是开始标记T是时间和日期分割标记没有日期只有时间T是不能省去的比如1小时执行一次应写成PT1H。`;
}
if (type.value === 'cycle') {
return `支持CRON表达式如0 0/30 * * * ?或ISO 8601周期如R3/PT10M`;
}
return '';
});
// 初始化和监听
function syncFromBusinessObject() {
if (props.businessObject) {
const timerDef = (props.businessObject.eventDefinitions || [])[0];
if (timerDef) {
if (timerDef.timeDate) {
type.value = 'time';
condition.value = timerDef.timeDate.body;
} else if (timerDef.timeDuration) {
type.value = 'duration';
condition.value = timerDef.timeDuration.body;
} else if (timerDef.timeCycle) {
type.value = 'cycle';
condition.value = timerDef.timeCycle.body;
}
}
}
}
onMounted(syncFromBusinessObject);
// 切换类型
function setType(t) {
type.value = t;
condition.value = '';
updateNode();
}
// 输入校验
watch([type, condition], () => {
valid.value = validate();
// updateNode() // 可以注释掉,避免频繁触发
});
function validate() {
if (type.value === 'time') {
return !!condition.value && !isNaN(Date.parse(condition.value));
}
if (type.value === 'duration') {
return /^P.*$/.test(condition.value);
}
if (type.value === 'cycle') {
return /^([0-9*\/?, ]+|R\d*\/P.*)$/.test(condition.value);
}
return true;
}
// 选择时间
function onDateChange(val) {
dateValue.value = val;
}
function onDateConfirm() {
if (dateValue.value) {
condition.value = new Date(dateValue.value).toISOString();
showDatePicker.value = false;
updateNode();
}
}
// 持续时长
function onDurationChange(val) {
condition.value = val;
}
function onDurationConfirm() {
showDurationDialog.value = false;
updateNode();
}
// 循环
function onCycleChange(val) {
condition.value = val;
}
function onCycleConfirm() {
showCycleDialog.value = false;
updateNode();
}
// 输入框聚焦时弹窗(可选)
function handleInputFocus() {
if (type.value === 'time') showDatePicker.value = true;
if (type.value === 'duration') showDurationDialog.value = true;
if (type.value === 'cycle') showCycleDialog.value = true;
}
// 同步到节点
function updateNode() {
const moddle = window.bpmnInstances?.moddle;
const modeling = window.bpmnInstances?.modeling;
const elementRegistry = window.bpmnInstances?.elementRegistry;
if (!moddle || !modeling || !elementRegistry) return;
// 获取元素
if (!props.businessObject || !props.businessObject.id) return;
const element = elementRegistry.get(props.businessObject.id);
if (!element) return;
// 1. 复用原有 timerDef或新建
let timerDef =
element.businessObject.eventDefinitions &&
element.businessObject.eventDefinitions[0];
if (!timerDef) {
timerDef = bpmnInstances().bpmnFactory.create(
'bpmn:TimerEventDefinition',
{},
);
modeling.updateProperties(element, {
eventDefinitions: [timerDef],
});
}
// 2. 清空原有
delete timerDef.timeDate;
delete timerDef.timeDuration;
delete timerDef.timeCycle;
// 3. 设置新的
if (type.value === 'time' && condition.value) {
timerDef.timeDate = bpmnInstances().bpmnFactory.create(
'bpmn:FormalExpression',
{
body: condition.value,
},
);
} else if (type.value === 'duration' && condition.value) {
timerDef.timeDuration = bpmnInstances().bpmnFactory.create(
'bpmn:FormalExpression',
{
body: condition.value,
},
);
} else if (type.value === 'cycle' && condition.value) {
timerDef.timeCycle = bpmnInstances().bpmnFactory.create(
'bpmn:FormalExpression',
{
body: condition.value,
},
);
}
bpmnInstances().modeling.updateProperties(toRaw(element), {
eventDefinitions: [timerDef],
});
}
watch(
() => props.businessObject,
(val) => {
if (val) {
nextTick(() => {
syncFromBusinessObject();
});
}
},
{ immediate: true },
);
</script>
<style scoped>
/* 相关样式 */
</style>

View File

@@ -0,0 +1,81 @@
/* 改变主题色变量 */
$--color-primary: #1890ff;
$--color-danger: #ff4d4f;
/* 改变 icon 字体路径变量,必需 */
$--font-path: '~element-ui/lib/theme-chalk/fonts';
@use '~element-ui/packages/theme-chalk/src/index';
.el-table td,
.el-table th {
color: #333;
}
.el-drawer__header {
box-sizing: border-box;
padding: 16px 16px 8px;
margin: 0;
font-size: 18px;
line-height: 24px;
color: #303133;
border-bottom: 1px solid #e8e8e8;
}
div[class^='el-drawer']:focus,
span:focus {
outline: none;
}
.el-drawer__body {
box-sizing: border-box;
width: 100%;
padding: 16px;
overflow-y: auto;
}
.el-dialog {
margin-top: 50vh !important;
overflow: hidden;
transform: translateY(-50%);
}
.el-dialog__wrapper {
max-height: 100vh;
overflow: hidden;
}
.el-dialog__header {
box-sizing: border-box;
padding: 16px 16px 8px;
border-bottom: 1px solid #e8e8e8;
}
.el-dialog__body {
box-sizing: border-box;
max-height: 80vh;
padding: 16px;
overflow-y: auto;
}
.el-dialog__footer {
box-sizing: border-box;
padding: 16px;
border-top: 1px solid #e8e8e8;
}
.el-dialog__close {
font-weight: 600;
}
.el-select {
width: 100%;
}
.el-divider:not(.el-divider--horizontal) {
margin: 0 8px;
}
.el-divider.el-divider--horizontal {
margin: 16px 0;
}

View File

@@ -0,0 +1,120 @@
@use './process-designer';
@use './process-panel';
$success-color: #4eb819;
$primary-color: #409eff;
$danger-color: #f56c6c;
$cancel-color: #909399;
.process-viewer {
position: relative;
background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+')
repeat !important;
border: 1px solid #efefef;
.success-arrow {
fill: $success-color;
stroke: $success-color;
}
.success-conditional {
fill: white;
stroke: $success-color;
}
.success.djs-connection {
.djs-visual path {
stroke: $success-color !important;
//marker-end: url(#sequenceflow-end-white-success)!important;
}
}
.success.djs-connection.condition-expression {
.djs-visual path {
//marker-start: url(#conditional-flow-marker-white-success)!important;
}
}
.success.djs-shape {
.djs-visual rect {
fill: $success-color !important;
fill-opacity: 0.15 !important;
stroke: $success-color !important;
}
.djs-visual polygon {
stroke: $success-color !important;
}
.djs-visual path:nth-child(2) {
fill: $success-color !important;
stroke: $success-color !important;
}
.djs-visual circle {
fill: $success-color !important;
fill-opacity: 0.15 !important;
stroke: $success-color !important;
}
}
.primary.djs-shape {
.djs-visual rect {
fill: $primary-color !important;
fill-opacity: 0.15 !important;
stroke: $primary-color !important;
}
.djs-visual polygon {
stroke: $primary-color !important;
}
.djs-visual circle {
fill: $primary-color !important;
fill-opacity: 0.15 !important;
stroke: $primary-color !important;
}
}
.danger.djs-shape {
.djs-visual rect {
fill: $danger-color !important;
fill-opacity: 0.15 !important;
stroke: $danger-color !important;
}
.djs-visual polygon {
stroke: $danger-color !important;
}
.djs-visual circle {
fill: $danger-color !important;
fill-opacity: 0.15 !important;
stroke: $danger-color !important;
}
}
.cancel.djs-shape {
.djs-visual rect {
fill: $cancel-color !important;
fill-opacity: 0.15 !important;
stroke: $cancel-color !important;
}
.djs-visual polygon {
stroke: $cancel-color !important;
}
.djs-visual circle {
fill: $cancel-color !important;
fill-opacity: 0.15 !important;
stroke: $cancel-color !important;
}
}
}
.process-viewer .djs-tooltip-container,
.process-viewer .djs-overlay-container,
.process-viewer .djs-palette {
display: none;
}

View File

@@ -0,0 +1,184 @@
@use 'bpmn-js-token-simulation/assets/css/bpmn-js-token-simulation.css';
// 边框被 token-simulation 样式覆盖了
.djs-palette {
background: var(--palette-background-color);
border: solid 1px var(--palette-border-color) !important;
border-radius: 2px;
}
.my-process-designer {
box-sizing: border-box;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
.my-process-designer__header {
width: 100%;
min-height: 36px;
.el-button {
text-align: center;
}
.el-button-group {
margin: 4px;
}
.el-tooltip__popper {
.el-button {
width: 100%;
padding-right: 8px;
padding-left: 8px;
text-align: left;
}
.el-button:hover {
color: #fff;
background: rgb(64 158 255 / 80%);
}
}
.align {
position: relative;
i {
&::after {
position: absolute;
content: '|';
// transform: rotate(90deg) translate(200%, 60%);
transform: rotate(180deg) translate(271%, -10%);
}
}
}
.align.align-left i {
transform: rotate(90deg);
}
.align.align-right i {
transform: rotate(-90deg);
}
.align.align-top i {
transform: rotate(180deg);
}
.align.align-bottom i {
transform: rotate(0deg);
}
.align.align-center i {
transform: rotate(0deg);
&::after {
// transform: rotate(90deg) translate(0, 60%);
transform: rotate(0deg) translate(-0%, -5%);
}
}
.align.align-middle i {
transform: rotate(-90deg);
&::after {
// transform: rotate(90deg) translate(0, 60%);
transform: rotate(0deg) translate(0, -10%);
}
}
}
.my-process-designer__container {
display: inline-flex;
flex: 1;
width: 100%;
.my-process-designer__canvas {
position: relative;
flex: 1;
height: 100%;
background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+')
repeat !important;
div.toggle-mode {
display: none;
}
}
.my-process-designer__property-panel {
z-index: 10;
height: 100%;
overflow: scroll;
overflow-y: auto;
* {
box-sizing: border-box;
}
}
// svg {
// width: 100%;
// height: 100%;
// min-height: 100%;
// overflow: hidden;
// }
}
}
//侧边栏配置
// .djs-palette .two-column .open {
.open {
// .djs-palette.open {
.djs-palette-entries {
div[class^='bpmn-icon-']::before,
div[class*='bpmn-icon-']::before {
line-height: unset;
}
div.entry {
position: relative;
}
div.entry:hover {
&::after {
position: absolute;
top: 0;
right: -10px;
bottom: 0;
z-index: 100;
box-sizing: border-box;
display: inline-block;
width: max-content;
padding: 0 16px;
overflow: hidden;
font-size: 0.5em;
font-variant: normal;
vertical-align: text-bottom;
text-transform: none;
text-decoration: inherit;
content: attr(title);
background: #fafafa;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 0 6px #eee;
transform: translateX(100%);
}
}
}
}
pre {
height: 100%;
max-height: calc(80vh - 32px);
margin: 0;
overflow: hidden;
overflow-y: auto;
}
.hljs {
word-break: break-word;
white-space: pre-wrap;
}
.hljs * {
font-family: Consolas, Monaco, monospace;
}

View File

@@ -0,0 +1,127 @@
.process-panel__container {
box-sizing: border-box;
max-height: 100%;
padding: 0 8px;
overflow-y: scroll;
border-left: 1px solid #eee;
box-shadow: 0 0 8px #ccc;
}
.panel-tab__title {
padding: 0 8px;
font-size: 1.1em;
font-weight: 600;
line-height: 1.2em;
i {
margin-right: 8px;
font-size: 1.2em;
}
}
.panel-tab__content {
box-sizing: border-box;
width: 100%;
padding: 8px 16px;
border-top: 1px solid #eee;
.panel-tab__content--title {
display: flex;
justify-content: space-between;
padding-bottom: 8px;
span {
flex: 1;
text-align: left;
}
}
}
.element-property {
display: flex;
align-items: flex-start;
width: 100%;
margin: 8px 0;
.element-property__label {
box-sizing: border-box;
display: block;
width: 90px;
padding-right: 12px;
overflow: hidden;
font-size: 14px;
line-height: 32px;
text-align: right;
}
.element-property__value {
flex: 1;
line-height: 32px;
}
.el-form-item {
width: 100%;
padding-bottom: 18px;
margin-bottom: 0;
}
}
.list-property {
flex-direction: column;
.element-listener-item {
display: inline-grid;
grid-template-columns: 16px auto 32px 32px;
grid-column-gap: 8px;
width: 100%;
}
.element-listener-item + .element-listener-item {
margin-top: 8px;
}
}
.listener-filed__title {
display: inline-flex;
align-items: center;
justify-content: space-between;
width: 100%;
margin-top: 0;
span {
width: 200px;
font-size: 14px;
text-align: left;
}
i {
margin-right: 8px;
}
}
.element-drawer__button {
display: inline-flex;
justify-content: space-around;
width: 100%;
margin-top: 8px;
}
.element-drawer__button > .el-button {
width: 100%;
}
.el-collapse-item__content {
padding-bottom: 0;
}
.el-input.is-disabled .el-input__inner {
color: #999;
}
.el-form-item.el-form-item--mini {
margin-bottom: 0;
& + .el-form-item {
margin-top: 16px;
}
}

View File

@@ -0,0 +1,93 @@
import { toRaw } from 'vue';
const bpmnInstances = () => (window as any)?.bpmnInstances;
// 创建监听器实例
export function createListenerObject(options, isTask, prefix) {
const listenerObj = Object.create(null);
listenerObj.event = options.event;
isTask && (listenerObj.id = options.id); // 任务监听器特有的 id 字段
switch (options.listenerType) {
case 'delegateExpressionListener': {
listenerObj.delegateExpression = options.delegateExpression;
break;
}
case 'expressionListener': {
listenerObj.expression = options.expression;
break;
}
case 'scriptListener': {
listenerObj.script = createScriptObject(options, prefix);
break;
}
default: {
listenerObj.class = options.class;
}
}
// 注入字段
if (options.fields) {
listenerObj.fields = options.fields.map((field) => {
return createFieldObject(field, prefix);
});
}
// 任务监听器的 定时器 设置
if (isTask && options.event === 'timeout' && !!options.eventDefinitionType) {
const timeDefinition = bpmnInstances().moddle.create(
'bpmn:FormalExpression',
{
body: options.eventTimeDefinitions,
},
);
const TimerEventDefinition = bpmnInstances().moddle.create(
'bpmn:TimerEventDefinition',
{
id: `TimerEventDefinition_${uuid(8)}`,
[`time${options.eventDefinitionType.replace(/^\S/, (s) => s.toUpperCase())}`]:
timeDefinition,
},
);
listenerObj.eventDefinitions = [TimerEventDefinition];
}
return bpmnInstances().moddle.create(
`${prefix}:${isTask ? 'TaskListener' : 'ExecutionListener'}`,
listenerObj,
);
}
// 创建 监听器的注入字段 实例
export function createFieldObject(option, prefix) {
const { name, fieldType, string, expression } = option;
const fieldConfig =
fieldType === 'string' ? { name, string } : { name, expression };
return bpmnInstances().moddle.create(`${prefix}:Field`, fieldConfig);
}
// 创建脚本实例
export function createScriptObject(options, prefix) {
const { scriptType, scriptFormat, value, resource } = options;
const scriptConfig =
scriptType === 'inlineScript'
? { scriptFormat, value }
: { scriptFormat, resource };
return bpmnInstances().moddle.create(`${prefix}:Script`, scriptConfig);
}
// 更新元素扩展属性
export function updateElementExtensions(element, extensionList) {
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
values: extensionList,
});
bpmnInstances().modeling.updateProperties(toRaw(element), {
extensionElements: extensions,
});
}
// 创建一个id
export function uuid(length = 8, chars?) {
let result = '';
const charsString =
chars || '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
for (let i = length; i > 0; --i) {
result += charsString[Math.floor(Math.random() * charsString.length)];
}
return result;
}

View File

@@ -0,0 +1,5 @@
const hljs = require('highlight.js/lib/core');
hljs.registerLanguage('xml', require('highlight.js/lib/languages/xml'));
hljs.registerLanguage('json', require('highlight.js/lib/languages/json'));
module.exports = hljs;

View File

@@ -0,0 +1,30 @@
import BpmnRenderer from 'bpmn-js/lib/draw/BpmnRenderer';
export default function CustomRenderer(
config,
eventBus,
styles,
pathMap,
canvas,
textRenderer,
) {
BpmnRenderer.call(
this,
config,
eventBus,
styles,
pathMap,
canvas,
textRenderer,
2000,
);
this.handlers.label = function () {
return null;
};
}
const F = function () {}; // 核心,利用空对象作为中介;
F.prototype = BpmnRenderer.prototype; // 核心将父类的原型赋值给空对象F
CustomRenderer.prototype = new F(); // 核心,将 F的实例赋值给子类
CustomRenderer.prototype.constructor = CustomRenderer; // 修复子类CustomRenderer的构造器指向防止原型链的混乱

View File

@@ -0,0 +1,6 @@
import CustomRenderer from './CustomRenderer';
export default {
__init__: ['customRenderer'],
customRenderer: ['type', CustomRenderer],
};

View File

@@ -0,0 +1,16 @@
import BpmnRules from 'bpmn-js/lib/features/rules/BpmnRules';
import inherits from 'inherits';
export default function CustomRules(eventBus) {
BpmnRules.call(this, eventBus);
}
inherits(CustomRules, BpmnRules);
CustomRules.prototype.canDrop = function () {
return false;
};
CustomRules.prototype.canMove = function () {
return false;
};

View File

@@ -0,0 +1,6 @@
import CustomRules from './CustomRules';
export default {
__init__: ['customRules'],
customRules: ['type', CustomRules],
};

View File

@@ -0,0 +1,25 @@
/**
* This is a sample file that should be replaced with the actual translation.
*
* Checkout https://github.com/bpmn-io/bpmn-js-i18n for a list of available
* translations and labels to translate.
*/
export default {
'Exclusive Gateway': 'Exklusives Gateway',
'Parallel Gateway': 'Paralleles Gateway',
'Inclusive Gateway': 'Inklusives Gateway',
'Complex Gateway': 'Komplexes Gateway',
'Event based Gateway': 'Ereignis-basiertes Gateway',
'Message Start Event': '消息启动事件',
'Timer Start Event': '定时启动事件',
'Conditional Start Event': '条件启动事件',
'Signal Start Event': '信号启动事件',
'Error Start Event': '错误启动事件',
'Escalation Start Event': '升级启动事件',
'Compensation Start Event': '补偿启动事件',
'Message Start Event (non-interrupting)': '消息启动事件 (非中断)',
'Timer Start Event (non-interrupting)': '定时启动事件 (非中断)',
'Conditional Start Event (non-interrupting)': '条件启动事件 (非中断)',
'Signal Start Event (non-interrupting)': '信号启动事件 (非中断)',
'Escalation Start Event (non-interrupting)': '升级启动事件 (非中断)',
};

View File

@@ -0,0 +1,39 @@
// outside.js
const ctx = '@@clickoutsideContext';
export default {
bind(el, binding, vnode) {
const ele = el;
const documentHandler = (e) => {
if (!vnode.context || ele.contains(e.target)) {
return false;
}
// 调用指令回调
if (binding.expression) {
vnode.context[el[ctx].methodName](e);
} else {
el[ctx].bindingFn(e);
}
};
// 将方法添加到ele
ele[ctx] = {
documentHandler,
methodName: binding.expression,
bindingFn: binding.value,
};
setTimeout(() => {
document.addEventListener('touchstart', documentHandler); // 为document绑定事件
});
},
update(el, binding) {
const ele = el;
ele[ctx].methodName = binding.expression;
ele[ctx].bindingFn = binding.value;
},
unbind(el) {
document.removeEventListener('touchstart', el[ctx].documentHandler); // 解绑
delete el[ctx];
},
};

View File

@@ -0,0 +1,10 @@
export function debounce(fn, delay = 500) {
let timer;
return function (...args) {
if (timer) {
clearTimeout(timer);
timer = null;
}
timer = setTimeout(fn.bind(this, ...args), delay);
};
}

View File

@@ -0,0 +1,50 @@
function xmlStr2XmlObj(xmlStr) {
let xmlObj = {};
if (document.all) {
const xmlDom = new window.ActiveXObject('Microsoft.XMLDOM');
xmlDom.loadXML(xmlStr);
xmlObj = xmlDom;
} else {
xmlObj = new DOMParser().parseFromString(xmlStr, 'text/xml');
}
return xmlObj;
}
function xml2json(xml) {
try {
let obj = {};
if (xml.children.length > 0) {
for (let i = 0; i < xml.children.length; i++) {
const item = xml.children.item(i);
const nodeName = item.nodeName;
if (obj[nodeName] === undefined) {
obj[nodeName] = xml2json(item);
} else {
if (obj[nodeName].push === undefined) {
const old = obj[nodeName];
obj[nodeName] = [];
obj[nodeName].push(old);
}
obj[nodeName].push(xml2json(item));
}
}
} else {
obj = xml.textContent;
}
return obj;
} catch (error) {
console.log(error.message);
}
}
function xmlObj2json(xml) {
const xmlObj = xmlStr2XmlObj(xml);
console.log(xmlObj);
let jsonObj = {};
if (xmlObj.childNodes.length > 0) {
jsonObj = xml2json(xmlObj);
}
return jsonObj;
}
export default xmlObj2json;