This commit is contained in:
xingyu4j
2025-09-15 13:41:30 +08:00
71 changed files with 17334 additions and 2995 deletions

View File

@@ -46,10 +46,18 @@
"@vueuse/core": "catalog:",
"@vueuse/integrations": "catalog:",
"ant-design-vue": "catalog:",
"bpmn-js": "catalog:",
"bpmn-js-properties-panel": "catalog:",
"bpmn-js-token-simulation": "catalog:",
"camunda-bpmn-moddle": "catalog:",
"cropperjs": "catalog:",
"dayjs": "catalog:",
"diagram-js": "catalog:",
"fast-xml-parser": "catalog:",
"highlight.js": "catalog:",
"min-dash": "catalog:",
"pinia": "catalog:",
"steady-xml": "catalog:",
"tinymce": "catalog:",
"vue": "catalog:",
"vue-dompurify-html": "catalog:",

View File

@@ -0,0 +1,684 @@
<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 {
computed,
defineEmits,
defineOptions,
defineProps,
onBeforeUnmount,
onMounted,
provide,
ref,
} from 'vue';
import {
AlignLeftOutlined,
ApiOutlined,
DownloadOutlined,
EyeOutlined,
FolderOpenOutlined,
RedoOutlined,
ReloadOutlined,
UndoOutlined,
WarningOutlined,
ZoomInOutlined,
ZoomOutOutlined,
} from '@vben/icons';
import { Button, ButtonGroup, message, Modal, Tooltip } from 'ant-design-vue';
// 模拟流转流程
// @ts-ignore
import tokenSimulation from 'bpmn-js-token-simulation';
import BpmnModeler from 'bpmn-js/lib/Modeler';
// 代码高亮插件
// 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'; // 导入代码高亮文件
// 引入json转换与高亮
// import xml2js from 'xml-js'
// import xml2js from 'fast-xml-parser'
import { parseXmlString, XmlNode } from 'steady-xml';
import DefaultEmptyXML from './plugins/defaultEmpty';
import activitiModdleDescriptor from './plugins/descriptor/activitiDescriptor.json';
// 标签解析构建器
// 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 flowableModdleDescriptor from './plugins/descriptor/flowableDescriptor.json';
import activitiModdleExtension from './plugins/extension-moddle/activiti';
// 标签解析 Extension
import camundaModdleExtension from './plugins/extension-moddle/camunda';
import flowableModdleExtension from './plugins/extension-moddle/flowable';
// 翻译方法
import customTranslate from './plugins/translate/customTranslate';
import translationsCN from './plugins/translate/zh';
import 'highlight.js/styles/github.css';
defineOptions({ name: 'MyProcessDesigner' });
const props = defineProps({
value: { type: String, default: '' }, // xml 字符串
// valueWatch: true, // xml 字符串的 watch 状态
processId: { type: String, default: '' }, // 流程 key 标识
processName: { type: String, default: '' }, // 流程 name 名字
formId: { type: Number, default: undefined }, // 流程 form 表单编号
translations: {
// 自定义的翻译文件
type: Object,
default: () => {},
},
// eslint-disable-next-line vue/require-default-prop
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', 'mini', 'small'].includes(value),
},
headerButtonType: {
type: String,
default: 'primary',
validator: (value: string) =>
['danger', 'default', 'info', 'primary', 'success', 'warning'].includes(
value,
),
},
});
// 导入代码高亮样式
const emit = defineEmits([
'destroy',
'init-finished',
'save',
'commandStack-changed',
'input',
'change',
'canvas-viewbox-changed',
// eventName.name
'element-click',
]);
const bpmnCanvas = ref();
const refFile = ref();
/**
* 代码高亮
*/
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 (const 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;
const data: any = document.querySelector('#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 as any[],
moddleExtensions: moddleExtensions.value,
// additionalModules: [
// additionalModules.value
// propertiesPanelModule,
// propertiesProviderModule
// propertiesProviderModule
// ],
// moddleExtensions: { camunda: moddleExtensions.value }
});
// bpmnModeler.createDiagram()
// console.log(bpmnModeler, 'bpmnModeler111111')
// eslint-disable-next-line vue/custom-event-name-casing
emit('init-finished', bpmnModeler);
initModelListeners();
};
const initModelListeners = () => {
const EventBus = bpmnModeler.get('eventBus');
// console.log(EventBus, 'EventBus');
// 注册需要的监听事件, 将. 替换为 - , 避免解析异常
props.events.forEach((event: any) => {
EventBus.on(event, (eventObj: any) => {
// const eventName = event.replaceAll('.', '-');
// eventName.name = eventName
const element = eventObj ? eventObj.element : null;
// console.log(eventName, 'eventName');
// console.log(element, 'element');
// eslint-disable-next-line vue/custom-event-name-casing
emit('element-click', element, eventObj);
// emit(eventName, element, eventObj)
});
});
// 监听图形改变返回xml
EventBus.on('commandStack.changed', async (event: any) => {
try {
recoverable.value = bpmnModeler.get('commandStack').canRedo();
revocable.value = bpmnModeler.get('commandStack').canUndo();
const { xml } = await bpmnModeler.saveXML({ format: true });
// eslint-disable-next-line vue/custom-event-name-casing
emit('commandStack-changed', event);
emit('input', xml);
emit('change', xml);
emit('save', xml);
} catch {
// console.error(`[Process Designer Warn]: ${e.message || e}`);
}
});
// 监听视图缩放变化
bpmnModeler.on('canvas.viewbox.changed', ({ viewbox }: { viewbox: any }) => {
// eslint-disable-next-line vue/custom-event-name-casing
emit('canvas-viewbox-changed', { viewbox });
const { scale } = viewbox;
defaultZoom.value = Math.floor(scale * 100) / 100;
});
};
/* 创建新的流程图 */
const createNewDiagram = async (xml: any) => {
// console.log(xml, 'xml');
// 将字符串转换成图显示出来
const newId = props.processId || `Process_${Date.now()}`;
const newName = props.processName || `业务流程_${Date.now()}`;
const xmlString = xml || DefaultEmptyXML(newId, newName, props.prefix);
try {
// console.log(xmlString, 'xmlString')
// console.log(this.bpmnModeler.importXML);
const { warnings } = await bpmnModeler.importXML(xmlString);
// console.log(warnings, 'warnings');
if (warnings && warnings.length > 0) {
// warnings.forEach((warn: any) => console.warn(warn));
}
} catch {
// console.error(`[Process Designer Warn]: ${e.message || e}`);
}
};
// 下载流程图到本地
const downloadProcess = async (type: string) => {
try {
// 按需要类型创建文件并下载
if (type === 'xml' || type === 'bpmn') {
const { err, xml } = await bpmnModeler.saveXML();
// 读取异常时抛出异常
if (err) {
// console.error(`[Process Designer Warn ]: ${err.message || err}`);
}
const { href, filename } = setEncoded(type.toUpperCase(), xml);
downloadFunc(href, filename);
} else {
const { err, svg } = await bpmnModeler.saveSVG();
// 读取异常时抛出异常
if (err) {
// return console.error(err);
}
const { href, filename } = setEncoded('SVG', svg);
downloadFunc(href, filename);
}
} catch (error: any) {
console.error(`[Process Designer Warn ]: ${error.message || error}`);
}
// 文件下载方法
function downloadFunc(href: string, filename: string) {
if (href && filename) {
const a = document.createElement('a');
a.download = filename; // 指定下载的文件名
a.href = href; // URL对象
a.click(); // 模拟点击
URL.revokeObjectURL(a.href); // 释放URL 对象
}
}
};
// 根据所需类型进行转码并返回下载地址
const setEncoded = (type: string, data: string) => {
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,
};
};
// 加载本地文件
const importLocalFile = () => {
const file = refFile.value.files[0];
const reader = new FileReader();
// eslint-disable-next-line unicorn/prefer-blob-reading-methods
reader.readAsText(file);
reader.addEventListener('load', function () {
const 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) => {
const 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) => {
const 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: string) => {
const Align = bpmnModeler.get('alignElements');
const Selection = bpmnModeler.get('selection');
const SelectedElements = Selection.get();
if (!SelectedElements || SelectedElements.length <= 1) {
message.warning('请按住 Shift 键选择多个元素对齐');
// alert('请按住 Ctrl 键选择多个元素对齐
return;
}
Modal.confirm({
title: '警告',
content: '自动对齐可能造成图形变形,是否继续?',
okText: '确定',
cancelText: '取消',
icon: WarningOutlined as any,
onOk() {
Align.trigger(SelectedElements, align);
},
});
};
/* ----------------------------- 方法结束 ---------------------------------*/
const previewProcessXML = () => {
// console.log(bpmnModeler.saveXML, 'bpmnModeler');
bpmnModeler.saveXML({ format: true }).then(({ xml }: { xml: string }) => {
// console.log(xml, 'xml111111')
previewResult.value = xml;
previewType.value = 'xml';
previewModelVisible.value = true;
});
};
const previewProcessJson = () => {
bpmnModeler.saveXML({ format: true }).then(({ xml }: { xml: string }) => {
const rootNodes = new XmlNode('root' as any, 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>
<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']">
<ButtonGroup key="file-control">
<Button
:icon="FolderOpenOutlined"
title="打开文件"
@click="refFile.click()"
/>
<Tooltip placement="bottom">
<template #title>
<div>
<Button type="link" @click="downloadProcessAsXml()">
下载为XML文件
</Button>
<br />
<Button type="link" @click="downloadProcessAsSvg()">
下载为SVG文件
</Button>
<br />
<Button type="link" @click="downloadProcessAsBpmn()">
下载为BPMN文件
</Button>
</div>
</template>
<Button :icon="DownloadOutlined" title="下载文件" />
</Tooltip>
<Tooltip>
<template #title>
<Button type="link" @click="previewProcessXML">预览XML</Button>
<br />
<Button type="link" @click="previewProcessJson">预览JSON</Button>
</template>
<Button :icon="EyeOutlined" title="浏览" />
</Tooltip>
<Tooltip
v-if="props.simulation"
:title="simulationStatus ? '退出模拟' : '开启模拟'"
>
<Button
:icon="ApiOutlined"
title="模拟"
@click="processSimulation"
/>
</Tooltip>
</ButtonGroup>
<ButtonGroup key="align-control">
<Tooltip title="向左对齐">
<Button
:icon="AlignLeftOutlined"
class="align align-bottom"
@click="elementsAlign('left')"
/>
</Tooltip>
<Tooltip title="向右对齐">
<Button
:icon="AlignLeftOutlined"
class="align align-top"
@click="elementsAlign('right')"
/>
</Tooltip>
<Tooltip title="向上对齐">
<Button
:icon="AlignLeftOutlined"
class="align align-left"
@click="elementsAlign('top')"
/>
</Tooltip>
<Tooltip title="向下对齐">
<Button
:icon="AlignLeftOutlined"
class="align align-right"
@click="elementsAlign('bottom')"
/>
</Tooltip>
<Tooltip title="水平居中">
<Button
:icon="AlignLeftOutlined"
class="align align-center"
@click="elementsAlign('center')"
/>
</Tooltip>
<Tooltip title="垂直居中">
<Button
:icon="AlignLeftOutlined"
class="align align-middle"
@click="elementsAlign('middle')"
/>
</Tooltip>
</ButtonGroup>
<ButtonGroup key="scale-control">
<Tooltip title="缩小视图">
<Button
:icon="ZoomOutOutlined"
@click="processZoomOut()"
:disabled="defaultZoom < 0.2"
/>
</Tooltip>
<Button>{{ `${Math.floor(defaultZoom * 10 * 10)}%` }}</Button>
<Tooltip title="放大视图">
<Button
:icon="ZoomInOutlined"
@click="processZoomIn()"
:disabled="defaultZoom > 4"
/>
</Tooltip>
<Tooltip title="重置视图并居中">
<Button :icon="ReloadOutlined" @click="processReZoom()" />
</Tooltip>
</ButtonGroup>
<ButtonGroup key="stack-control">
<Tooltip title="撤销">
<Button
:icon="UndoOutlined"
@click="processUndo()"
:disabled="!revocable"
/>
</Tooltip>
<Tooltip title="恢复">
<Button
:icon="RedoOutlined"
@click="processRedo()"
:disabled="!recoverable"
/>
</Tooltip>
<Tooltip title="重新绘制">
<Button :icon="ReloadOutlined" @click="processRestart()" />
</Tooltip>
</ButtonGroup>
</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:open="previewModelVisible"
width="80%"
:scroll="true"
style="max-height: 600px"
>
<div>
<pre><code v-dompurify-html="highlightedCode(previewResult)" class="hljs"></code></pre>
</div>
</Dialog>
</div>
</template>

View File

@@ -0,0 +1,418 @@
<script lang="ts" setup>
import { defineProps, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { BpmProcessInstanceStatus } from '@vben/constants';
import { UndoOutlined, ZoomInOutlined, ZoomOutOutlined } from '@vben/icons';
import { dateFormatter, formatPast2 } from '@vben/utils';
import { Button, ButtonGroup, Modal, Row, Table } from 'ant-design-vue';
import BpmnViewer from 'bpmn-js/lib/Viewer';
import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas';
import { DictTag } from '#/components/dict-tag';
import { DICT_TYPE } from '#/utils/dict';
import '../theme/index.scss';
const props = defineProps({
xml: {
type: String,
required: true,
},
view: {
type: Object,
require: true,
default: () => ({}),
},
});
const processCanvas = ref();
const bpmnViewer = ref<any | BpmnViewer>(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) => {
const 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) => {
const 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.append(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 }: { element: any }) => {
onSelectElement(element);
});
// 初始化 BPMN 视图
isLoading.value = true;
await bpmnViewer.value.importXML(xml);
// 自定义成功的箭头
addCustomDefs();
} catch {
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: any = bpmnViewer.value.get('canvas');
const elementRegistry: any = 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>
<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>
<!-- 审批记录 -->
<Modal
:title="dialogTitle || '审批记录'"
v-model:open="dialogVisible"
:width="1000"
>
<Row>
<Table :data-source="selectTasks" size="small" :bordered="true">
<Table.Column title="序号" align="center" width="50">
<template #default="{ index }">
{{ index + 1 }}
</template>
</Table.Column>
<Table.Column
title="审批人"
width="100"
align="center"
v-if="selectActivityType === 'bpmn:UserTask'"
>
<template #default="{ record }">
{{ record.assigneeUser?.nickname || record.ownerUser?.nickname }}
</template>
</Table.Column>
<Table.Column
title="发起人"
data-index="assigneeUser.nickname"
width="100"
align="center"
v-else
/>
<Table.Column title="部门" width="100" align="center">
<template #default="{ record }">
{{ record.assigneeUser?.deptName || record.ownerUser?.deptName }}
</template>
</Table.Column>
<Table.Column
:custom-render="({ text }) => dateFormatter(text)"
align="center"
title="开始时间"
data-index="createTime"
width="140"
/>
<Table.Column
:custom-render="({ text }) => dateFormatter(text)"
align="center"
title="结束时间"
data-index="endTime"
width="140"
/>
<Table.Column
align="center"
title="审批状态"
data-index="status"
width="90"
>
<template #default="{ record }">
<DictTag
:type="DICT_TYPE.BPM_TASK_STATUS"
:value="record.status"
/>
</template>
</Table.Column>
<Table.Column
align="center"
title="审批建议"
data-index="reason"
width="120"
v-if="selectActivityType === 'bpmn:UserTask'"
/>
<Table.Column
align="center"
title="耗时"
data-index="durationInMillis"
width="100"
>
<template #default="{ record }">
{{ formatPast2(record.durationInMillis) }}
</template>
</Table.Column>
</Table>
</Row>
</Modal>
<!-- Zoom放大、缩小 -->
<div style="position: absolute; top: 0; left: 0; width: 100%">
<Row justify="end">
<ButtonGroup key="scale-control">
<Button
:disabled="defaultZoom <= 0.3"
:icon="ZoomOutOutlined"
@click="processZoomOut()"
/>
<Button style="width: 90px">
{{ `${Math.floor(defaultZoom * 10 * 10)}%` }}
</Button>
<Button
:disabled="defaultZoom >= 3.9"
:icon="ZoomInOutlined"
@click="processZoomIn()"
/>
<Button :icon="UndoOutlined" @click="processReZoom()" />
</ButtonGroup>
</Row>
</div>
</div>
</template>

View File

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

View File

@@ -0,0 +1,8 @@
import MyProcessViewer from './ProcessViewer.vue';
MyProcessViewer.install = function (Vue: any) {
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,37 @@
<script lang="ts" setup>
import { Button } from 'ant-design-vue';
import { assign } from 'min-dash';
defineOptions({ name: 'MyProcessPalette' });
const bpmnInstances = () =>
(window as typeof window & { bpmnInstances?: any }).bpmnInstances;
const addTask = (event: MouseEvent, options: any = {}) => {
const ElementFactory = bpmnInstances().elementFactory;
const create = bpmnInstances().modeler.get('create');
const shape = ElementFactory.createShape(
assign({ type: 'bpmn:UserTask' }, options),
);
if (options) {
shape.businessObject.di.isExpanded = options.isExpanded;
}
create.start(event, shape);
};
</script>
<template>
<div class="my-process-palette p-20 pt-80">
<Button type="primary" @click="addTask" @mousedown="addTask">
测试任务
</Button>
<div class="test-container" id="palette-container">1</div>
</div>
</template>
<style scoped>
.test-container {
margin-top: 16px;
}
</style>

View File

@@ -0,0 +1,401 @@
<script lang="ts" setup>
import { nextTick, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { Collapse } from 'ant-design-vue';
import ElementCustomConfig from '#/components/bpmnProcessDesigner/package/penal/custom-config/ElementCustomConfig.vue';
import ElementForm from '#/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue';
import ElementBaseInfo from './base/ElementBaseInfo.vue';
import FlowCondition from './flow-condition/FlowCondition.vue';
import ElementListeners from './listeners/ElementListeners.vue';
// import ElementForm from './form/ElementForm.vue'
import UserTaskListeners from './listeners/UserTaskListeners.vue';
import ElementMultiInstance from './multi-instance/ElementMultiInstance.vue';
import ElementOtherConfig from './other/ElementOtherConfig.vue';
import ElementProperties from './properties/ElementProperties.vue';
import SignalAndMassage from './signal-message/SignalAndMessage.vue';
import { getTaskCollapseItemName, isTaskCollapseItemShow } from './task/data';
import ElementTask from './task/ElementTask.vue';
import TimeEventConfig from './time-event-config/TimeEventConfig.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,
},
businessObject: {
type: Object,
default: () => ({}),
},
model: {
type: Object,
default: () => ({}),
}, // 流程模型的数据
});
const CollapsePanel = Collapse.Panel;
const activeTab = ref('base');
const elementId = ref('');
const elementType = ref<any>('');
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(Boolean);
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
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', (_: any) => {
// console.log(e, 'eeeee');
initFormOnChanged(null);
});
// 监听选择事件,修改当前激活的元素以及表单
props.bpmnModeler.on(
'selection.changed',
({ newSelection }: { newSelection: any }) => {
initFormOnChanged(newSelection[0] || null);
},
);
props.bpmnModeler.on('element.changed', ({ element }: { element: any }) => {
// 保证 修改 "默认流转路径" 类似需要修改多个元素的事件发生的时候,更新表单的元素与原选中元素不一致。
if (element && element.id === elementId.value) {
initFormOnChanged(element);
}
});
};
// 初始化数据
const initFormOnChanged = (element: any) => {
if (!isReady.value || !bpmnInstances()) return;
let activatedElement = element;
if (!activatedElement) {
activatedElement =
bpmnInstances().elementRegistry.find(
(el: any) => el.type === 'bpmn:Process',
) ??
bpmnInstances().elementRegistry.find(
(el: any) => 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 = structuredClone(
activatedElement.businessObject,
);
conditionFormVisible.value =
elementType.value === 'SequenceFlow' &&
activatedElement.source &&
(activatedElement.source.type as string).includes('StartEvent');
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;
//
// const timerDef = moddle.create('bpmn:TimerEventDefinition', {});
// switch (type.value) {
// case 'cycle': {
// timerDef.timeCycle = moddle.create('bpmn:FormalExpression', {
// body: condition.value,
// });
//
// break;
// }
// case 'duration': {
// timerDef.timeDuration = moddle.create('bpmn:FormalExpression', {
// body: condition.value,
// });
//
// break;
// }
// case 'time': {
// timerDef.timeDate = moddle.create('bpmn:FormalExpression', {
// body: condition.value,
// });
//
// break;
// }
// // No default
// }
//
// 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>
<template>
<div
class="process-panel__container"
:style="{ width: `${width}px`, maxHeight: '600px' }"
>
<Collapse v-model:active-key="activeTab" v-if="isReady">
<CollapsePanel key="base" header="常规">
<template #extra>
<IconifyIcon icon="ep:info-filled" />
</template>
<ElementBaseInfo
:id-edit-disabled="idEditDisabled"
:business-object="elementBusinessObject"
:type="elementType"
:model="model"
/>
</CollapsePanel>
<CollapsePanel
key="message"
header="消息与信号"
v-if="elementType === 'Process'"
>
<template #extra>
<IconifyIcon icon="ep:comment" />
</template>
<SignalAndMassage />
</CollapsePanel>
<CollapsePanel
key="condition"
header="流转条件"
v-if="conditionFormVisible"
>
<template #extra>
<IconifyIcon icon="ep:promotion" />
</template>
<FlowCondition
:business-object="elementBusinessObject"
:type="elementType"
/>
</CollapsePanel>
<CollapsePanel key="form" header="表单" v-if="formVisible">
<template #extra>
<IconifyIcon icon="ep:list" />
</template>
<ElementForm :id="elementId" :type="elementType" />
</CollapsePanel>
<CollapsePanel
key="task"
:header="getTaskCollapseItemName(elementType)"
v-if="isTaskCollapseItemShow(elementType)"
>
<template #extra>
<IconifyIcon icon="ep:checked" />
</template>
<ElementTask :id="elementId" :type="elementType" />
</CollapsePanel>
<CollapsePanel
key="multiInstance"
header="多人审批方式"
v-if="elementType.includes('Task')"
>
<template #extra>
<IconifyIcon icon="ep:help-filled" />
</template>
<ElementMultiInstance
:id="elementId"
:business-object="elementBusinessObject"
:type="elementType"
/>
</CollapsePanel>
<CollapsePanel key="listeners" header="执行监听器">
<template #extra>
<IconifyIcon icon="ep:bell-filled" />
</template>
<ElementListeners :id="elementId" :type="elementType" />
</CollapsePanel>
<CollapsePanel
key="taskListeners"
header="任务监听器"
v-if="elementType === 'UserTask'"
>
<template #extra>
<IconifyIcon icon="ep:bell-filled" />
</template>
<UserTaskListeners :id="elementId" :type="elementType" />
</CollapsePanel>
<CollapsePanel key="extensions" header="扩展属性">
<template #extra>
<IconifyIcon icon="ep:circle-plus-filled" />
</template>
<ElementProperties :id="elementId" :type="elementType" />
</CollapsePanel>
<CollapsePanel key="other" header="其他">
<template #extra>
<IconifyIcon icon="ep:promotion" />
</template>
<ElementOtherConfig :id="elementId" />
</CollapsePanel>
<CollapsePanel key="customConfig" header="自定义配置">
<template #extra>
<IconifyIcon icon="ep:tools" />
</template>
<ElementCustomConfig
:id="elementId"
:type="elementType"
:business-object="elementBusinessObject"
/>
</CollapsePanel>
<!-- 新增的时间事件配置项 -->
<CollapsePanel
key="timeEvent"
header="时间事件"
v-if="elementType === 'IntermediateCatchEvent'"
>
<template #extra>
<IconifyIcon icon="ep:timer" />
</template>
<TimeEventConfig
:business-object="bpmnElement.value?.businessObject"
:key="elementId"
/>
</CollapsePanel>
</Collapse>
</div>
</template>

View File

@@ -0,0 +1,221 @@
<script lang="ts" setup>
import { onBeforeUnmount, reactive, ref, toRaw, watch } from 'vue';
import { Form, FormItem, Input } from 'ant-design-vue';
defineOptions({ name: 'ElementBaseInfo' });
const props = defineProps<{
businessObject?: BusinessObject;
model?: Model;
}>();
interface BusinessObject {
id?: string;
name?: string;
$type: string;
[key: string]: any;
}
interface Model {
key?: string;
name?: string;
[key: string]: any;
}
const needProps = ref<Record<string, any>>({});
const bpmnElement = ref<any>();
const elementBaseInfo = ref<BusinessObject>({} as any);
// 流程表单的下拉框的数据
// const forms = ref([])
// 流程模型的校验
const rules = reactive<any>({
id: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }],
});
const bpmnInstances = () =>
(window as any)?.bpmnInstances as {
bpmnElement: any;
modeling: {
updateProperties: (element: any, properties: any) => void;
};
};
const resetBaseInfo = () => {
// console.log(window, 'window');
// console.log(bpmnElement.value, 'bpmnElement');
bpmnElement.value = bpmnInstances()?.bpmnElement;
// console.log(bpmnElement.value, 'resetBaseInfo11111111111')
if (bpmnElement.value?.businessObject) {
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 = (event: Event) => {
const target = event.target as HTMLInputElement;
const value = target.value;
// 校验 value 的值,只有 XML NCName 通过的情况下,才进行赋值。否则,会导致流程图报错,无法绘制的问题
if (!value) {
return;
}
if (!/[a-z_][-\w.$]*/i.test(value)) {
// 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 = (event: Event) => {
const target = event.target as HTMLInputElement;
const value = target.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: string) => {
// console.log(key, 'key');
// 触发 elementBaseInfo 对应的字段
const attrObj: Record<string, any> = 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 as any);
handleNameUpdate(props.model?.name as any);
}
},
{
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>
<template>
<div class="panel-tab__content">
<Form :model="needProps" :rules="rules" layout="vertical">
<div v-if="needProps.type === 'bpmn:Process'">
<!-- 如果是 Process 信息的时候使用自定义表单 -->
<FormItem label="流程标识" name="id">
<Input
v-model:value="needProps.id"
placeholder="请输入流标标识"
:disabled="needProps.id !== undefined && needProps.id.length > 0"
@change="handleKeyUpdate"
/>
</FormItem>
<FormItem label="流程名称" name="name">
<Input
v-model:value="needProps.name"
placeholder="请输入流程名称"
allow-clear
@change="handleNameUpdate"
/>
</FormItem>
</div>
<div v-else>
<FormItem label="ID">
<Input
v-model:value="elementBaseInfo.id"
allow-clear
@change="updateBaseInfo('id')"
/>
</FormItem>
<FormItem label="名称">
<Input
v-model:value="elementBaseInfo.name"
allow-clear
@change="updateBaseInfo('name')"
/>
</FormItem>
</div>
</Form>
</div>
</template>

View File

@@ -0,0 +1,58 @@
<script lang="ts" setup>
import type { Component } from 'vue';
import { defineOptions, defineProps, ref, watch } from 'vue';
import { CustomConfigMap } from './data';
defineOptions({ name: 'ElementCustomConfig' });
const props = defineProps({
id: {
type: String,
default: '',
},
type: {
type: String,
default: '',
},
businessObject: {
type: Object as () => BusinessObject,
default: () => ({}),
},
});
interface BusinessObject {
eventDefinitions?: Array<{ $type: string }>;
[key: string]: any;
}
// const bpmnInstances = () => (window as any)?.bpmnInstances;
const customConfigComponent = ref<Component | null>(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] || '';
}
// @ts-ignore
customConfigComponent.value = (
CustomConfigMap as Record<string, { component: Component }>
)[val]?.component;
}
},
{ immediate: true },
);
</script>
<template>
<div class="panel-tab__content">
<component :is="customConfigComponent" v-bind="$props" />
</div>
</template>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,307 @@
<script lang="ts" setup>
import {
defineOptions,
defineProps,
inject,
nextTick,
ref,
toRaw,
watch,
} from 'vue';
import {
Divider,
FormItem,
InputNumber,
RadioButton,
RadioGroup,
Select,
SelectOption,
Switch,
} from 'ant-design-vue';
import { convertTimeUnit } from '#/components/simple-process-design/components/nodes-config/utils';
import {
TIME_UNIT_TYPES,
TIMEOUT_HANDLER_TYPES,
TimeUnitType,
} from '#/components/simple-process-design/consts';
defineOptions({ name: 'ElementCustomConfig4BoundaryEventTimer' });
const props = defineProps({
id: {
type: String,
default: '',
},
type: {
type: String,
default: '',
},
});
const prefix = inject('prefix');
const bpmnElement = ref<any>();
const bpmnInstances = () => (window as Record<string, any>)?.bpmnInstances;
const timeoutHandlerEnable = ref(false);
const boundaryEventType = ref<any>();
const timeoutHandlerType = ref<{
value: number | undefined;
}>({
value: undefined,
});
const timeModdle = ref<any>();
const timeDuration = ref(6);
const timeUnit = ref(TimeUnitType.HOUR);
const maxRemindCount = ref(1);
const elExtensionElements = ref<any>();
const otherExtensions = ref<any[]>();
const configExtensions = ref<any[]>([]);
const eventDefinition = ref<any>();
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: any) => 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: any) => 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];
maxRemindCount.value = Number.parseInt(maxRemindCountStr.slice(1));
timeDuration.value = Number.parseInt(timeDurationStr.slice(2, -1));
timeUnit.value = convertTimeUnit(timeDurationStr.slice(-1));
timeModdle.value = eventDefinition.value.timeCycle;
}
if (eventDefinition.value.timeDuration) {
const timeDurationStr = eventDefinition.value.timeDuration.body;
timeDuration.value = Number.parseInt(timeDurationStr.slice(2, -1));
timeUnit.value = convertTimeUnit(timeDurationStr.slice(-1));
timeModdle.value = eventDefinition.value.timeDuration;
}
}
// 保留剩余扩展元素,便于后面更新该元素对应属性
otherExtensions.value =
elExtensionElements.value.values?.filter(
(ex: any) =>
ex.$type !== `${prefix}:BoundaryEventType` &&
ex.$type !== `${prefix}:TimeoutHandlerType`,
) ?? [];
};
const timeoutHandlerChange = (checked: any) => {
timeoutHandlerEnable.value = checked;
if (checked) {
// 启用自定义用户任务超时处理
// 边界事件类型 --- 超时
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 > 0 &&
nextTick(() => {
resetElement();
});
},
{ immediate: true },
);
</script>
<template>
<div>
<Divider orientation="left">审批人超时未处理时</Divider>
<FormItem label="启用开关" name="timeoutHandlerEnable">
<Switch
v-model:checked="timeoutHandlerEnable"
checked-children="开启"
un-checked-children="关闭"
@change="timeoutHandlerChange"
/>
</FormItem>
<FormItem
label="执行动作"
name="timeoutHandlerType"
v-if="timeoutHandlerEnable"
>
<RadioGroup
v-model:value="timeoutHandlerType.value"
@change="onTimeoutHandlerTypeChanged"
>
<RadioButton
v-for="item in TIMEOUT_HANDLER_TYPES"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</RadioButton>
</RadioGroup>
</FormItem>
<FormItem label="超时时间设置" v-if="timeoutHandlerEnable">
<span class="mr-2">当超过</span>
<FormItem name="timeDuration">
<InputNumber
class="mr-2"
:style="{ width: '100px' }"
v-model:value="timeDuration"
:min="1"
:controls="true"
@change="
() => {
updateTimeModdle();
updateElementExtensions();
}
"
/>
</FormItem>
<Select
v-model:value="timeUnit"
class="mr-2"
:style="{ width: '100px' }"
@change="onTimeUnitChange"
>
<SelectOption
v-for="item in TIME_UNIT_TYPES"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</SelectOption>
</Select>
未处理
</FormItem>
<FormItem
label="最大提醒次数"
name="maxRemindCount"
v-if="timeoutHandlerEnable && timeoutHandlerType.value === 1"
>
<InputNumber
v-model:value="maxRemindCount"
:min="1"
:max="10"
@change="
() => {
updateTimeModdle();
updateElementExtensions();
}
"
/>
</FormItem>
</div>
</template>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,783 @@
<!-- UserTask 自定义配置
1. 审批人与提交人为同一人时
2. 审批人拒绝时
3. 审批人为空时
4. 操作按钮
5. 字段权限
6. 审批类型
7. 是否需要签名
-->
<script lang="ts" setup>
import type { SystemUserApi } from '#/api/system/user';
import type { ButtonSetting } from '#/components/simple-process-design/consts';
import { inject, nextTick, onMounted, ref, toRaw, watch } from 'vue';
import { BpmModelFormType } from '@vben/constants';
import {
Button,
Divider,
Form,
Radio,
RadioGroup,
Select,
SelectOption,
Switch,
} from 'ant-design-vue';
import { getSimpleUserList } from '#/api/system/user';
import {
APPROVE_TYPE,
ApproveType,
ASSIGN_EMPTY_HANDLER_TYPES,
ASSIGN_START_USER_HANDLER_TYPES,
AssignEmptyHandlerType,
DEFAULT_BUTTON_SETTING,
FieldPermissionType,
OPERATION_BUTTON_NAME,
REJECT_HANDLER_TYPES,
RejectHandlerType,
} from '#/components/simple-process-design/consts';
import { useFormFieldsPermission } from '#/components/simple-process-design/helpers';
defineOptions({ name: 'ElementCustomConfig4UserTask' });
const props = defineProps({
id: {
type: String,
required: false,
default: '',
},
type: {
type: String,
required: false,
default: '',
},
});
const prefix = inject('prefix');
// 审批人与提交人为同一人时
const assignStartUserHandlerTypeEl = ref<any>();
const assignStartUserHandlerType = ref<any>();
// 审批人拒绝时
const rejectHandlerTypeEl = ref<any>();
const rejectHandlerType = ref<any>();
const returnNodeIdEl = ref<any>();
const returnNodeId = ref<any>();
const returnTaskList = ref<any[]>([]);
// 审批人为空时
const assignEmptyHandlerTypeEl = ref<any>();
const assignEmptyHandlerType = ref<any>();
const assignEmptyUserIdsEl = ref<any>();
const assignEmptyUserIds = ref<any>();
// 操作按钮
const buttonsSettingEl = ref<any>();
const { btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } =
useButtonsSetting();
// 字段权限
const fieldsPermissionEl = ref<any[]>([]);
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<any>();
const otherExtensions = ref<any>();
const bpmnElement = ref<any>();
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: any) => ex.$type === `${prefix}:ApproveType`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:ApproveType`, {
value: ApproveType.USER,
});
// 审批人与提交人为同一人时
assignStartUserHandlerTypeEl.value =
elExtensionElements.value.values?.filter(
(ex: any) => ex.$type === `${prefix}:AssignStartUserHandlerType`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:AssignStartUserHandlerType`, {
value: 1,
});
assignStartUserHandlerType.value = assignStartUserHandlerTypeEl.value.value;
// 审批人拒绝时
rejectHandlerTypeEl.value =
elExtensionElements.value.values?.filter(
(ex: any) => ex.$type === `${prefix}:RejectHandlerType`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:RejectHandlerType`, { value: 1 });
rejectHandlerType.value = rejectHandlerTypeEl.value.value;
returnNodeIdEl.value =
elExtensionElements.value.values?.filter(
(ex: any) => ex.$type === `${prefix}:RejectReturnTaskId`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:RejectReturnTaskId`, {
value: '',
});
returnNodeId.value = returnNodeIdEl.value.value;
// 审批人为空时
assignEmptyHandlerTypeEl.value =
elExtensionElements.value.values?.filter(
(ex: any) => ex.$type === `${prefix}:AssignEmptyHandlerType`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:AssignEmptyHandlerType`, {
value: 1,
});
assignEmptyHandlerType.value = assignEmptyHandlerTypeEl.value.value;
assignEmptyUserIdsEl.value =
elExtensionElements.value.values?.filter(
(ex: any) => ex.$type === `${prefix}:AssignEmptyUserIds`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:AssignEmptyUserIds`, {
value: '',
});
assignEmptyUserIds.value = assignEmptyUserIdsEl.value.value
?.split(',')
.map((item: string) => {
// 如果数字超出了最大安全整数范围,则将其作为字符串处理
const num = Number(item);
return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER
? item
: num;
});
// 操作按钮
buttonsSettingEl.value = elExtensionElements.value.values?.filter(
(ex: any) => 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: any) => ex.$type === `${prefix}:FieldsPermission`,
);
fieldsPermissionEl.value = [];
getNodeConfigFormFields();
fieldsPermissionConfig.value.forEach((element: any) => {
element.permission =
fieldsPermissionList?.find((obj: any) => obj.field === element.field)
?.permission ?? '1';
fieldsPermissionEl.value.push(
bpmnInstances().moddle.create(`${prefix}:FieldsPermission`, element),
);
});
}
// 是否需要签名
signEnable.value =
elExtensionElements.value.values?.filter(
(ex: any) => ex.$type === `${prefix}:SignEnable`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:SignEnable`, { value: false });
// 审批意见
reasonRequire.value =
elExtensionElements.value.values?.filter(
(ex: any) => ex.$type === `${prefix}:ReasonRequire`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:ReasonRequire`, { value: false });
// 保留剩余扩展元素,便于后面更新该元素对应属性
otherExtensions.value =
elExtensionElements.value.values?.filter(
(ex: any) =>
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 > 0 &&
nextTick(() => {
resetCustomConfigList();
});
},
{ immediate: true },
);
function findAllPredecessorsExcludingStart(elementId: string, modeler: any) {
const elementRegistry = modeler.get('elementRegistry');
const allConnections = elementRegistry.filter(
(element: any) => element.type === 'bpmn:SequenceFlow',
);
const predecessors = new Set(); // 使用 Set 来避免重复节点
const visited = new Set(); // 用于记录已访问的节点
// 检查是否是开始事件节点
function isStartEvent(element: any) {
return element.type === 'bpmn:StartEvent';
}
function findPredecessorsRecursively(element: any) {
// 如果该节点已经访问过,直接返回,避免循环
if (visited.has(element)) {
return;
}
// 标记当前节点为已访问
visited.add(element);
// 获取与当前节点相连的所有连接
const incomingConnections = allConnections.filter(
(connection: any) => connection.target === element,
);
incomingConnections.forEach((connection: any) => {
const source = connection.source; // 获取前置节点
// 只添加不是开始事件的前置节点
if (!isStartEvent(source)) {
predecessors.add(source.businessObject);
// 递归查找前置节点
findPredecessorsRecursively(source);
}
});
}
const targetElement = elementRegistry.get(elementId);
if (targetElement) {
findPredecessorsRecursively(targetElement);
}
return [...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];
if (buttonItem) {
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: any) => {
if (type === 'READ') {
field.permission = FieldPermissionType.READ;
} else if (type === 'WRITE') {
field.permission = FieldPermissionType.WRITE;
} else {
field.permission = FieldPermissionType.NONE;
}
});
};
const userOptions = ref<SystemUserApi.User[]>([]); // 用户列表
onMounted(async () => {
// 获得用户列表
userOptions.value = await getSimpleUserList();
});
</script>
<template>
<div>
<Divider orientation="left">审批类型</Divider>
<Form.Item name="approveType" label="审批类型">
<RadioGroup v-model:value="approveType.value">
<Radio
v-for="(item, index) in APPROVE_TYPE"
:key="index"
:value="item.value"
>
{{ item.label }}
</Radio>
</RadioGroup>
</Form.Item>
<Divider orientation="left">审批人拒绝时</Divider>
<Form.Item name="rejectHandlerType" label="处理方式">
<RadioGroup
v-model:value="rejectHandlerType"
:disabled="returnTaskList.length === 0"
@change="updateRejectHandlerType"
>
<div class="flex-col">
<div v-for="(item, index) in REJECT_HANDLER_TYPES" :key="index">
<Radio :key="item.value" :value="item.value">
{{ item.label }}
</Radio>
</div>
</div>
</RadioGroup>
</Form.Item>
<Form.Item
v-if="rejectHandlerType === RejectHandlerType.RETURN_USER_TASK"
name="returnNodeId"
label="驳回节点"
>
<Select
v-model:value="returnNodeId"
allow-clear
style="width: 100%"
@change="updateReturnNodeId"
placeholder="请选择驳回节点"
>
<SelectOption
v-for="item in returnTaskList"
:key="item.id"
:value="item.id"
>
{{ item.name }}
</SelectOption>
</Select>
</Form.Item>
<Divider orientation="left">审批人为空时</Divider>
<Form.Item prop="assignEmptyHandlerType">
<RadioGroup
v-model:value="assignEmptyHandlerType"
@change="updateAssignEmptyHandlerType"
>
<div class="flex-col">
<div v-for="(item, index) in ASSIGN_EMPTY_HANDLER_TYPES" :key="index">
<Radio :key="item.value" :value="item.value">
{{ item.label }}
</Radio>
</div>
</div>
</RadioGroup>
</Form.Item>
<Form.Item
v-if="assignEmptyHandlerType === AssignEmptyHandlerType.ASSIGN_USER"
label="指定用户"
prop="assignEmptyHandlerUserIds"
>
<Select
v-model:value="assignEmptyUserIds"
allow-clear
mode="multiple"
style="width: 100%"
@change="updateAssignEmptyUserIds"
>
<SelectOption
v-for="item in userOptions"
:key="item.id"
:value="item.id"
>
{{ item.nickname }}
</SelectOption>
</Select>
</Form.Item>
<Divider orientation="left">审批人与提交人为同一人时</Divider>
<RadioGroup
v-model:value="assignStartUserHandlerType"
@change="updateAssignStartUserHandlerType"
>
<div class="flex-col">
<div
v-for="(item, index) in ASSIGN_START_USER_HANDLER_TYPES"
:key="index"
>
<Radio :key="item.value" :value="item.value">
{{ item.label }}
</Radio>
</div>
</div>
</RadioGroup>
<Divider orientation="left">操作按钮</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-mounted-focus
v-model="item.displayName"
:placeholder="item.displayName"
v-if="btnDisplayNameEdit[index]"
/>
<Button v-else type="text" @click="changeBtnDisplayName(index)">
{{ item.displayName }}
</Button>
</div>
<div class="button-setting-item-label">
<Switch v-model:checked="item.enable" />
</div>
</div>
</div>
<Divider orientation="left">字段权限</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>
<RadioGroup
class="field-setting-item-group"
v-model:value="item.permission"
>
<div class="item-radio-wrap">
<Radio
:value="FieldPermissionType.READ"
size="large"
@change="updateElementExtensions"
>
<span></span>
</Radio>
</div>
<div class="item-radio-wrap">
<Radio
:value="FieldPermissionType.WRITE"
size="large"
@change="updateElementExtensions"
>
<span></span>
</Radio>
</div>
<div class="item-radio-wrap">
<Radio
:value="FieldPermissionType.NONE"
size="large"
@change="updateElementExtensions"
>
<span></span>
</Radio>
</div>
</RadioGroup>
</div>
</div>
<Divider orientation="left">是否需要签名</Divider>
<Form.Item prop="signEnable">
<Switch
v-model:checked="signEnable.value"
checked-children=""
un-checked-children=""
@change="updateElementExtensions"
/>
</Form.Item>
<Divider orientation="left">审批意见</Divider>
<Form.Item prop="reasonRequire">
<Switch
v-model:checked="reasonRequire.value"
checked-children="必填"
un-checked-children="非必填"
@change="updateElementExtensions"
/>
</Form.Item>
</div>
</template>
<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: '用户任务',
component: UserTaskCustomConfig,
},
BoundaryEventTimerEventDefinition: {
name: '定时边界事件(非中断)',
component: BoundaryEventTimer,
},
};

View File

@@ -0,0 +1,238 @@
<script lang="ts" setup>
import { nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
import { Form, Input, Select } from 'ant-design-vue';
defineOptions({ name: 'FlowCondition' });
const props = defineProps({
businessObject: {
type: Object,
default: () => ({}),
},
type: {
type: String,
default: '',
},
});
const { TextArea } = Input;
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) {
// 带条件
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';
} else {
// 普通
flowConditionForm.value = { type: 'normal' };
}
};
const updateFlowType = (flowType: any) => {
// 正常条件类
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 = () => {
const { 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,
(_) => {
// console.log(val, 'val');
nextTick(() => {
resetFlowCondition();
});
},
{
immediate: true,
},
);
</script>
<template>
<div class="panel-tab__content">
<Form
:model="flowConditionForm"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<Form.Item label="流转类型">
<Select v-model:value="flowConditionForm.type" @change="updateFlowType">
<Select.Option value="normal">普通流转路径</Select.Option>
<Select.Option value="default">默认流转路径</Select.Option>
<Select.Option value="condition">条件流转路径</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="条件格式"
v-if="flowConditionForm.type === 'condition'"
key="condition"
>
<Select v-model:value="flowConditionForm.conditionType">
<Select.Option value="expression">表达式</Select.Option>
<Select.Option value="script">脚本</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="表达式"
v-if="
flowConditionForm.conditionType &&
flowConditionForm.conditionType === 'expression'
"
key="express"
>
<Input
v-model:value="flowConditionForm.body"
style="width: 192px"
allow-clear
@change="updateFlowCondition"
/>
</Form.Item>
<template
v-if="
flowConditionForm.conditionType &&
flowConditionForm.conditionType === 'script'
"
>
<Form.Item label="脚本语言" key="language">
<Input
v-model:value="flowConditionForm.language"
allow-clear
@change="updateFlowCondition"
/>
</Form.Item>
<Form.Item label="脚本类型" key="scriptType">
<Select v-model:value="flowConditionForm.scriptType">
<Select.Option value="inlineScript">内联脚本</Select.Option>
<Select.Option value="externalScript">外部脚本</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="脚本"
v-if="flowConditionForm.scriptType === 'inlineScript'"
key="body"
>
<TextArea
v-model:value="flowConditionForm.body"
:auto-size="{ minRows: 2, maxRows: 6 }"
allow-clear
@change="updateFlowCondition"
/>
</Form.Item>
<Form.Item
label="资源地址"
v-if="flowConditionForm.scriptType === 'externalScript'"
key="resource"
>
<Input
v-model:value="flowConditionForm.resource"
allow-clear
@change="updateFlowCondition"
/>
</Form.Item>
</template>
</Form>
</div>
</template>

View File

@@ -0,0 +1,536 @@
<script lang="ts" setup>
import { computed, inject, nextTick, onMounted, ref, toRaw, watch } from 'vue';
import { Form, FormItem, Select } from 'ant-design-vue';
import { getFormSimpleList } from '#/api/bpm/form';
defineOptions({ name: 'ElementForm' });
const props = defineProps({
id: {
type: String,
default: '',
},
type: {
type: String,
default: '',
},
});
const prefix = inject('prefix');
const formKey = ref<number | string | undefined>(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.find(
(ex: any) => ex.$type === `${prefix}:FormData`,
) || bpmnInstances().moddle.create(`${prefix}:FormData`, { fields: [] });
// 业务标识 businessKey 绑定在 formData 中
businessKey.value = formData.value.businessKey;
// 保留剩余扩展元素,便于后面更新该元素对应属性
otherExtensions.value = elExtensionElements.value.values.filter(
(ex: any) => ex.$type !== `${prefix}:FormData`,
);
// 复制原始值,填充表格
fieldList.value = structuredClone(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: any) => {
formFieldForm.value.type = type === 'custom' ? '' : type;
};
// 打开字段详情侧边栏
const _openFieldForm = (field: any, index: any) => {
formFieldIndex.value = index;
if (index === -1) {
formFieldForm.value = {};
// 初始化枚举值列表
fieldEnumList.value = [];
// 初始化约束条件列表
fieldConstraintsList.value = [];
// 初始化自定义属性列表
fieldPropertiesList.value = [];
} else {
const FieldObject = formData.value.fields[index];
formFieldForm.value = structuredClone(field);
// 设置自定义类型
// this.$set(this.formFieldForm, "typeType", !this.fieldType[field.type] ? "custom" : field.type);
formFieldForm.value.typeType = fieldType.value[
field.type as keyof typeof fieldType.value
]
? field.type
: 'custom';
// 初始化枚举值列表
field.type === 'enum' &&
(fieldEnumList.value = structuredClone(FieldObject?.values || []));
// 初始化约束条件列表
fieldConstraintsList.value = structuredClone(
FieldObject?.validation?.constraints || [],
);
// 初始化自定义属性列表
fieldPropertiesList.value = structuredClone(
FieldObject?.properties?.values || [],
);
}
fieldModelVisible.value = true;
};
// 打开字段 某个 配置项 弹窗
const _openFieldOptionForm = (option: any, index: any, type: any) => {
fieldOptionModelVisible.value = true;
fieldOptionType.value = type;
formFieldOptionIndex.value = index;
if (type === 'property') {
fieldOptionForm.value = option ? structuredClone(option) : {};
return (optionModelTitle.value = '属性配置');
}
if (type === 'enum') {
fieldOptionForm.value = option ? structuredClone(option) : {};
return (optionModelTitle.value = '枚举值配置');
}
fieldOptionForm.value = option ? structuredClone(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 > 0) {
const fieldPropertyList = fieldPropertiesList.value.map((fp: any) => {
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 > 0) {
const fieldConstraintList = fieldConstraintsList.value.map((fc: any) => {
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 > 0) {
Field.values = fieldEnumList.value.map((fe: any) => {
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: any, index: any, type: any) => {
// 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: any, index: any) => {
console.warn(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, formData.value],
},
);
// 更新到元素上
bpmnInstances().modeling.updateProperties(toRaw(bpmnELement.value), {
extensionElements: newElExtensionElements,
});
};
const formList = ref<any[]>([]); // 流程表单的下拉框的数据
const formOptions = computed(() => {
return formList.value.map((form: any) => ({
value: form.id,
label: form.name,
}));
});
onMounted(async () => {
formList.value = await getFormSimpleList();
formKey.value = formKey.value
? Number.parseInt(formKey.value as string)
: undefined;
});
watch(
() => props.id,
(val: any) => {
val &&
val.length > 0 &&
nextTick(() => {
resetFormList();
});
},
{ immediate: true },
);
</script>
<template>
<div class="panel-tab__content">
<Form :label-col="{ style: { width: '80px' } }">
<FormItem label="流程表单">
<!-- <Input v-model:value="formKey" @change="updateElementFormKey" />-->
<Select
v-model:value="formKey"
allow-clear
@change="updateElementFormKey"
:options="formOptions"
/>
</FormItem>
<FormItem label="业务标识">
<Select
v-model:value="businessKey"
@change="_updateElementBusinessKey"
allow-clear
>
<Select.Option v-for="i in fieldList" :key="i.id" :value="i.id">
{{ i.label }}
</Select.Option>
<Select.Option value=""></Select.Option>
</Select>
</FormItem>
</Form>
<!--字段列表-->
<!-- <div class="element-property list-property">-->
<!-- <Divider><Icon icon="ep:coin" /> 表单字段</Divider>-->
<!-- <Table :data-source="fieldList" :scroll="{ y: 240 }" bordered>-->
<!-- <TableColumn title="序号" type="index" width="50px" />-->
<!-- <TableColumn title="字段名称" dataIndex="label" width="80px" :ellipsis="true" />-->
<!-- <TableColumn-->
<!-- title="字段类型"-->
<!-- dataIndex="type"-->
<!-- width="80px"-->
<!-- :customRender="({ text }) => fieldType[text] || text"-->
<!-- :ellipsis="true"-->
<!-- />-->
<!-- <TableColumn-->
<!-- title="默认值"-->
<!-- dataIndex="defaultValue"-->
<!-- width="80px"-->
<!-- :ellipsis="true"-->
<!-- />-->
<!-- <TableColumn title="操作" width="90px">-->
<!-- <template #default="scope">-->
<!-- <Button type="link" @click="openFieldForm(scope, scope.$index)">-->
<!-- 编辑-->
<!-- </Button>-->
<!-- <Divider type="vertical" />-->
<!-- <Button-->
<!-- type="link"-->
<!-- danger-->
<!-- @click="removeField(scope, scope.$index)"-->
<!-- >-->
<!-- 移除-->
<!-- </Button>-->
<!-- </template>-->
<!-- </TableColumn>-->
<!-- </Table>-->
<!-- </div>-->
<!-- <div class="element-drawer__button">-->
<!-- <Button type="primary" @click="openFieldForm(null, -1)">添加字段</Button>-->
<!-- </div>-->
<!--字段配置侧边栏-->
<!-- <Drawer-->
<!-- v-model:open="fieldModelVisible"-->
<!-- title="字段配置"-->
<!-- :width="`${width}px`"-->
<!-- destroyOnClose-->
<!-- >-->
<!-- <Form :model="formFieldForm" :label-col="{ style: { width: '90px' } }">-->
<!-- <FormItem label="字段ID">-->
<!-- <Input v-model:value="formFieldForm.id" allowClear />-->
<!-- </FormItem>-->
<!-- <FormItem label="类型">-->
<!-- <Select-->
<!-- v-model:value="formFieldForm.typeType"-->
<!-- placeholder="请选择字段类型"-->
<!-- allowClear-->
<!-- @change="changeFieldTypeType"-->
<!-- >-->
<!-- <SelectOption v-for="(value, key) of fieldType" :key="key" :value="key">{{ value }}</SelectOption>-->
<!-- </Select>-->
<!-- </FormItem>-->
<!-- <FormItem label="类型名称" v-if="formFieldForm.typeType === 'custom'">-->
<!-- <Input v-model:value="formFieldForm.type" allowClear />-->
<!-- </FormItem>-->
<!-- <FormItem label="名称">-->
<!-- <Input v-model:value="formFieldForm.label" allowClear />-->
<!-- </FormItem>-->
<!-- <FormItem label="时间格式" v-if="formFieldForm.typeType === 'date'">-->
<!-- <Input v-model:value="formFieldForm.datePattern" allowClear />-->
<!-- </FormItem>-->
<!-- <FormItem label="默认值">-->
<!-- <Input v-model:value="formFieldForm.defaultValue" allowClear />-->
<!-- </FormItem>-->
<!-- </Form>-->
<!-- &lt;!&ndash; 枚举值设置 &ndash;&gt;-->
<!-- <template v-if="formFieldForm.type === 'enum'">-->
<!-- <Divider key="enum-divider" />-->
<!-- <p class="listener-filed__title" key="enum-title">-->
<!-- <span><Icon icon="ep:menu" />枚举值列表</span>-->
<!-- <Button type="primary" @click="openFieldOptionForm(null, -1, 'enum')"-->
<!-- >添加枚举值</Button-->
<!-- >-->
<!-- </p>-->
<!-- <Table :data-source="fieldEnumList" key="enum-table" :scroll="{ y: 240 }" bordered>-->
<!-- <TableColumn title="序号" width="50px" type="index" />-->
<!-- <TableColumn title="枚举值编号" dataIndex="id" width="100px" :ellipsis="true" />-->
<!-- <TableColumn title="枚举值名称" dataIndex="name" width="100px" :ellipsis="true" />-->
<!-- <TableColumn title="操作" width="90px">-->
<!-- <template #default="scope">-->
<!-- <Button-->
<!-- type="link"-->
<!-- @click="openFieldOptionForm(scope, scope.$index, 'enum')"-->
<!-- >-->
<!-- 编辑-->
<!-- </Button>-->
<!-- <Divider type="vertical" />-->
<!-- <Button-->
<!-- type="link"-->
<!-- danger-->
<!-- @click="removeFieldOptionItem(scope, scope.$index, 'enum')"-->
<!-- >-->
<!-- 移除-->
<!-- </Button>-->
<!-- </template>-->
<!-- </TableColumn>-->
<!-- </Table>-->
<!-- </template>-->
<!-- &lt;!&ndash; 校验规则 &ndash;&gt;-->
<!-- <Divider key="validation-divider" />-->
<!-- <p class="listener-filed__title" key="validation-title">-->
<!-- <span><Icon icon="ep:menu" />约束条件列表</span>-->
<!-- <Button type="primary" @click="openFieldOptionForm(null, -1, 'constraint')"-->
<!-- >添加约束</Button-->
<!-- >-->
<!-- </p>-->
<!-- <Table :data-source="fieldConstraintsList" key="validation-table" :scroll="{ y: 240 }" bordered>-->
<!-- <TableColumn title="序号" width="50px" type="index" />-->
<!-- <TableColumn title="约束名称" dataIndex="name" width="100px" :ellipsis="true" />-->
<!-- <TableColumn title="约束配置" dataIndex="config" width="100px" :ellipsis="true" />-->
<!-- <TableColumn title="操作" width="90px">-->
<!-- <template #default="scope">-->
<!-- <Button-->
<!-- type="link"-->
<!-- @click="openFieldOptionForm(scope, scope.$index, 'constraint')"-->
<!-- >-->
<!-- 编辑-->
<!-- </Button>-->
<!-- <Divider type="vertical" />-->
<!-- <Button-->
<!-- type="link"-->
<!-- danger-->
<!-- @click="removeFieldOptionItem(scope, scope.$index, 'constraint')"-->
<!-- >-->
<!-- 移除-->
<!-- </Button>-->
<!-- </template>-->
<!-- </TableColumn>-->
<!-- </Table>-->
<!-- &lt;!&ndash; 表单属性 &ndash;&gt;-->
<!-- <Divider key="property-divider" />-->
<!-- <p class="listener-filed__title" key="property-title">-->
<!-- <span><Icon icon="ep:menu" />字段属性列表</span>-->
<!-- <Button type="primary" @click="openFieldOptionForm(null, -1, 'property')"-->
<!-- >添加属性</Button-->
<!-- >-->
<!-- </p>-->
<!-- <Table :data-source="fieldPropertiesList" key="property-table" :scroll="{ y: 240 }" bordered>-->
<!-- <TableColumn title="序号" width="50px" type="index" />-->
<!-- <TableColumn title="属性编号" dataIndex="id" width="100px" :ellipsis="true" />-->
<!-- <TableColumn title="属性值" dataIndex="value" width="100px" :ellipsis="true" />-->
<!-- <TableColumn title="操作" width="90px">-->
<!-- <template #default="scope">-->
<!-- <Button-->
<!-- type="link"-->
<!-- @click="openFieldOptionForm(scope, scope.$index, 'property')"-->
<!-- >-->
<!-- 编辑-->
<!-- </Button>-->
<!-- <Divider type="vertical" />-->
<!-- <Button-->
<!-- type="link"-->
<!-- danger-->
<!-- @click="removeFieldOptionItem(scope, scope.$index, 'property')"-->
<!-- >-->
<!-- 移除-->
<!-- </Button>-->
<!-- </template>-->
<!-- </TableColumn>-->
<!-- </Table>-->
<!-- &lt;!&ndash; 底部按钮 &ndash;&gt;-->
<!-- <div class="element-drawer__button">-->
<!-- <Button> </Button>-->
<!-- <Button type="primary" @click="saveField"> </Button>-->
<!-- </div>-->
<!-- </Drawer>-->
<!-- <Modal-->
<!-- v-model:open="fieldOptionModelVisible"-->
<!-- :title="optionModelTitle"-->
<!-- width="600px"-->
<!-- destroyOnClose-->
<!-- >-->
<!-- <Form :model="fieldOptionForm" :label-col="{ style: { width: '96px' } }">-->
<!-- <FormItem label="编号/ID" v-if="fieldOptionType !== 'constraint'" key="option-id">-->
<!-- <Input v-model:value="fieldOptionForm.id" allowClear />-->
<!-- </FormItem>-->
<!-- <FormItem label="名称" v-if="fieldOptionType !== 'property'" key="option-name">-->
<!-- <Input v-model:value="fieldOptionForm.name" allowClear />-->
<!-- </FormItem>-->
<!-- <FormItem label="配置" v-if="fieldOptionType === 'constraint'" key="option-config">-->
<!-- <Input v-model:value="fieldOptionForm.config" allowClear />-->
<!-- </FormItem>-->
<!-- <FormItem label="值" v-if="fieldOptionType === 'property'" key="option-value">-->
<!-- <Input v-model:value="fieldOptionForm.value" allowClear />-->
<!-- </FormItem>-->
<!-- </Form>-->
<!-- <template #footer>-->
<!-- <Button @click="fieldOptionModelVisible = false"> </Button>-->
<!-- <Button type="primary" @click="saveFieldOption"> </Button>-->
<!-- </template>-->
<!-- </Modal>-->
</div>
</template>

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,621 @@
<script lang="ts" setup>
import { inject, nextTick, ref, watch } from 'vue';
import { IconifyIcon, PlusOutlined } from '@vben/icons';
import {
Button,
Divider,
Drawer,
Form,
FormItem,
Input,
Modal,
Select,
SelectOption,
Table,
TableColumn,
} from 'ant-design-vue';
import { createListenerObject, updateElementExtensions } from '../../utils';
import ProcessListenerDialog from './ProcessListenerDialog.vue';
import {
fieldType,
initListenerForm,
initListenerForm2,
initListenerType,
listenerType,
} from './utilSelf';
defineOptions({ name: 'ElementListeners' });
const props = defineProps({
id: {
type: String,
default: '',
},
type: {
type: String,
default: '',
},
});
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: any) => ex.$type === `${prefix}:ExecutionListener`,
) ?? [];
elementListenersList.value = bpmnElementListeners.value.map((listener: any) =>
initListenerType(listener),
);
};
// 打开 监听器详情 侧边栏
const openListenerForm = (listener: any, index: number) => {
// 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: any) => ({
...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: any, index: number) => {
listenerFieldForm.value = field ? structuredClone(field) : {};
editingListenerFieldIndex.value = field ? index : -1;
listenerFieldFormModelVisible.value = true;
nextTick(() => {
if (listenerFieldFormRef.value) {
listenerFieldFormRef.value.clearValidate();
}
});
};
// 保存监听器注入字段
const saveListenerFiled = async () => {
// debugger
const 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: number) => {
// debugger
Modal.confirm({
title: '确认移除该字段吗?',
content: '此操作不可撤销',
okText: '确 认',
cancelText: '取 消',
onOk() {
fieldsListOfListener.value.splice(index, 1);
listenerForm.value.fields.splice(index, 1);
},
onCancel() {
console.warn('操作取消');
},
});
};
// 移除监听器
const removeListener = (index: number) => {
Modal.confirm({
title: '确认移除该监听器吗?',
content: '此操作不可撤销',
okText: '确 认',
cancelText: '取 消',
onOk() {
bpmnElementListeners.value.splice(index, 1);
elementListenersList.value.splice(index, 1);
updateElementExtensions(bpmnElement.value, [
...otherExtensionList.value,
...bpmnElementListeners.value,
]);
},
onCancel() {
console.warn('操作取消');
},
});
};
// 保存监听器配置
const saveListenerConfig = async () => {
// debugger
const 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: any) => ex.$type !== `${prefix}:ExecutionListener`,
) ?? [];
updateElementExtensions(bpmnElement.value, [
...otherExtensionList.value,
...bpmnElementListeners.value,
]);
// 4. 隐藏侧边栏
listenerFormModelVisible.value = false;
listenerForm.value = {};
};
// 打开监听器弹窗
const processListenerDialogRef = ref();
const openProcessListenerDialog = async () => {
processListenerDialogRef.value.open('execution');
};
const selectProcessListener = (listener: any) => {
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: any) => ex.$type !== `${prefix}:ExecutionListener`,
) ?? [];
updateElementExtensions(bpmnElement.value, [
...otherExtensionList.value,
...bpmnElementListeners.value,
]);
};
watch(
() => props.id,
(val: string) => {
if (val && val.length > 0) {
nextTick(() => {
resetListenersList();
});
}
},
{ immediate: true },
);
</script>
<template>
<div class="panel-tab__content">
<Table
:data-source="elementListenersList"
size="small"
bordered
:pagination="false"
>
<TableColumn title="序号" width="50px">
<template #default="{ index }">
{{ index + 1 }}
</template>
</TableColumn>
<TableColumn title="事件类型" width="100px" data-index="event" />
<TableColumn
title="监听器类型"
width="100px"
:custom-render="
({ record }: any) =>
listenerTypeObject[record.listenerType as keyof typeof listenerType]
"
/>
<TableColumn title="操作" width="100px">
<template #default="{ record, index }">
<Button
size="small"
type="link"
@click="openListenerForm(record, index)"
>
编辑
</Button>
<Divider type="vertical" />
<Button
size="small"
type="link"
danger
@click="removeListener(index)"
>
移除
</Button>
</template>
</TableColumn>
</Table>
<div class="element-drawer__button">
<Button type="primary" size="small" @click="openListenerForm(null, -1)">
<template #icon>
<PlusOutlined />
</template>
添加监听器
</Button>
<Button size="small" @click="openProcessListenerDialog">
<template #icon>
<IconifyIcon icon="ep:select" />
</template>
选择监听器
</Button>
</div>
<!-- 监听器 编辑/创建 部分 -->
<Drawer
v-model:open="listenerFormModelVisible"
title="执行监听器"
:width="width as any"
:destroy-on-close="true"
>
<Form
:model="listenerForm"
ref="listenerFormRef"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<FormItem
label="事件类型"
name="event"
:rules="[
{
required: true,
message: '请选择事件类型',
trigger: ['blur', 'change'],
},
]"
>
<Select v-model:value="listenerForm.event">
<SelectOption value="start">start</SelectOption>
<SelectOption value="end">end</SelectOption>
</Select>
</FormItem>
<FormItem
label="监听器类型"
name="listenerType"
:rules="[
{
required: true,
message: '请选择监听器类型',
trigger: ['blur', 'change'],
},
]"
>
<Select v-model:value="listenerForm.listenerType">
<SelectOption
v-for="i in Object.keys(listenerTypeObject)"
:key="i"
:value="i"
>
{{ listenerTypeObject[i as keyof typeof listenerType] }}
</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="listenerForm.listenerType === 'classListener'"
label="Java类"
name="class"
key="listener-class"
:rules="[
{
required: true,
message: '请填写Java类',
trigger: ['blur', 'change'],
},
]"
>
<Input v-model:value="listenerForm.class" allow-clear />
</FormItem>
<FormItem
v-if="listenerForm.listenerType === 'expressionListener'"
label="表达式"
name="expression"
key="listener-expression"
:rules="[
{
required: true,
message: '请填写表达式',
trigger: ['blur', 'change'],
},
]"
>
<Input v-model:value="listenerForm.expression" allow-clear />
</FormItem>
<FormItem
v-if="listenerForm.listenerType === 'delegateExpressionListener'"
label="代理表达式"
name="delegateExpression"
key="listener-delegate"
:rules="[
{
required: true,
message: '请填写代理表达式',
trigger: ['blur', 'change'],
},
]"
>
<Input v-model:value="listenerForm.delegateExpression" allow-clear />
</FormItem>
<template v-if="listenerForm.listenerType === 'scriptListener'">
<FormItem
label="脚本格式"
name="scriptFormat"
key="listener-script-format"
:rules="[
{
required: true,
trigger: ['blur', 'change'],
message: '请填写脚本格式',
},
]"
>
<Input v-model:value="listenerForm.scriptFormat" allow-clear />
</FormItem>
<FormItem
label="脚本类型"
name="scriptType"
key="listener-script-type"
:rules="[
{
required: true,
trigger: ['blur', 'change'],
message: '请选择脚本类型',
},
]"
>
<Select v-model:value="listenerForm.scriptType">
<SelectOption value="inlineScript">内联脚本</SelectOption>
<SelectOption value="externalScript">外部脚本</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="listenerForm.scriptType === 'inlineScript'"
label="脚本内容"
name="value"
key="listener-script"
:rules="[
{
required: true,
trigger: ['blur', 'change'],
message: '请填写脚本内容',
},
]"
>
<Input v-model:value="listenerForm.value" allow-clear />
</FormItem>
<FormItem
v-if="listenerForm.scriptType === 'externalScript'"
label="资源地址"
name="resource"
key="listener-resource"
:rules="[
{
required: true,
trigger: ['blur', 'change'],
message: '请填写资源地址',
},
]"
>
<Input v-model:value="listenerForm.resource" allow-clear />
</FormItem>
</template>
</Form>
<Divider />
<p class="listener-filed__title">
<span><IconifyIcon icon="ep:menu" />注入字段:</span>
<Button type="primary" @click="openListenerFieldForm(null, -1)">
添加字段
</Button>
</p>
<Table
:data-source="fieldsListOfListener"
size="small"
:scroll="{ y: 240 }"
:pagination="false"
bordered
style="flex: none"
>
<TableColumn title="序号" width="50px">
<template #default="{ index }">
{{ index + 1 }}
</template>
</TableColumn>
<TableColumn title="字段名称" width="100px" data-index="name" />
<TableColumn
title="字段类型"
width="80px"
:custom-render="
({ record }: any) =>
fieldTypeObject[record.fieldType as keyof typeof fieldType]
"
/>
<TableColumn
title="字段值/表达式"
width="100px"
:custom-render="
({ record }: any) => record.string || record.expression
"
/>
<TableColumn title="操作" width="130px">
<template #default="{ record, index }">
<Button
size="small"
type="link"
@click="openListenerFieldForm(record, index)"
>
编辑
</Button>
<Divider type="vertical" />
<Button
size="small"
type="link"
danger
@click="removeListenerField(index)"
>
移除
</Button>
</template>
</TableColumn>
</Table>
<div class="element-drawer__button">
<Button @click="listenerFormModelVisible = false">取 消</Button>
<Button type="primary" @click="saveListenerConfig">保 存</Button>
</div>
</Drawer>
<!-- 注入字段 编辑/创建 部分 -->
<Modal
title="字段配置"
v-model:open="listenerFieldFormModelVisible"
width="600px"
:destroy-on-close="true"
>
<Form
:model="listenerFieldForm"
ref="listenerFieldFormRef"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
style="height: 136px"
>
<FormItem
label="字段名称"
name="name"
:rules="[
{
required: true,
message: '请填写字段名称',
trigger: ['blur', 'change'],
},
]"
>
<Input v-model:value="listenerFieldForm.name" allow-clear />
</FormItem>
<FormItem
label="字段类型"
name="fieldType"
:rules="[
{
required: true,
message: '请选择字段类型',
trigger: ['blur', 'change'],
},
]"
>
<Select v-model:value="listenerFieldForm.fieldType">
<SelectOption
v-for="i in Object.keys(fieldTypeObject)"
:key="i"
:value="i"
>
{{ fieldTypeObject[i as keyof typeof fieldType] }}
</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="listenerFieldForm.fieldType === 'string'"
label="字段值"
name="string"
key="field-string"
:rules="[
{
required: true,
message: '请填写字段值',
trigger: ['blur', 'change'],
},
]"
>
<Input v-model:value="listenerFieldForm.string" allow-clear />
</FormItem>
<FormItem
v-if="listenerFieldForm.fieldType === 'expression'"
label="表达式"
name="expression"
key="field-expression"
:rules="[
{
required: true,
message: '请填写表达式',
trigger: ['blur', 'change'],
},
]"
>
<Input v-model:value="listenerFieldForm.expression" allow-clear />
</FormItem>
</Form>
<template #footer>
<Button size="small" @click="listenerFieldFormModelVisible = false">
取 消
</Button>
<Button size="small" type="primary" @click="saveListenerFiled">
确 定
</Button>
</template>
</Modal>
</div>
<!-- 选择弹窗 -->
<ProcessListenerDialog
ref="processListenerDialogRef"
@select="selectProcessListener"
/>
</template>

View File

@@ -0,0 +1,111 @@
<!-- 执行器选择 -->
<script setup lang="ts">
import type { BpmProcessListenerApi } from '#/api/bpm/processListener';
import { reactive, ref } from 'vue';
import { CommonStatusEnum } from '@vben/constants';
import { Button, Modal, Pagination, Table } from 'ant-design-vue';
import { getProcessListenerPage } from '#/api/bpm/processListener';
import { ContentWrap } from '#/components/content-wrap';
import { DictTag } from '#/components/dict-tag';
import { DICT_TYPE } from '#/utils/dict';
/** BPM 流程 表单 */
defineOptions({ name: 'ProcessListenerDialog' });
/** 提交表单 */
const emit = defineEmits(['success', 'select']);
const dialogVisible = ref(false); // 弹窗的是否展示
const loading = ref(true); // 列表的加载中
const list = ref<BpmProcessListenerApi.ProcessListener[]>([]); // 列表的数据
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;
await getList();
dialogVisible.value = true;
};
defineExpose({ open }); // 提供 open 方法,用于打开弹窗
/** 查询列表 */
const getList = async () => {
loading.value = true;
try {
const data = await getProcessListenerPage(queryParams);
list.value = data.list;
total.value = data.total;
} finally {
loading.value = false;
}
};
// 定义 success 事件,用于操作成功后的回调
const select = async (row: BpmProcessListenerApi.ProcessListener) => {
dialogVisible.value = false;
// 发送操作成功的事件
emit('select', row);
};
</script>
<template>
<Modal
title="请选择监听器"
v-model:open="dialogVisible"
width="1024px"
:footer="null"
>
<ContentWrap>
<Table
:loading="loading"
:data-source="list"
:pagination="false"
:scroll="{ x: 'max-content' }"
>
<Table.Column title="名字" align="center" data-index="name" />
<Table.Column title="类型" align="center" data-index="type">
<template #default="{ record }">
<DictTag
:type="DICT_TYPE.BPM_PROCESS_LISTENER_TYPE"
:value="record.type"
/>
</template>
</Table.Column>
<Table.Column title="事件" align="center" data-index="event" />
<Table.Column title="值类型" align="center" data-index="valueType">
<template #default="{ record }">
<DictTag
:type="DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE"
:value="record.valueType"
/>
</template>
</Table.Column>
<Table.Column title="值" align="center" data-index="value" />
<Table.Column title="操作" align="center">
<template #default="{ record }">
<Button type="primary" @click="select(record)"> 选择 </Button>
</template>
</Table.Column>
</Table>
<!-- 分页 -->
<div class="mt-4 flex justify-end">
<Pagination
:total="total"
v-model:current="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
show-size-changer
@change="getList"
/>
</div>
</ContentWrap>
</Modal>
</template>

View File

@@ -0,0 +1,598 @@
<script lang="ts" setup>
import { inject, nextTick, ref, watch } from 'vue';
import { MenuOutlined, PlusOutlined, SelectOutlined } from '@vben/icons';
import {
Button,
Divider,
Drawer,
Form,
FormItem,
Input,
Modal,
Select,
SelectOption,
Table,
TableColumn,
} from 'ant-design-vue';
import ProcessListenerDialog from '#/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue';
import { createListenerObject, updateElementExtensions } from '../../utils';
import {
eventType,
fieldType,
initListenerForm,
initListenerForm2,
initListenerType,
listenerType,
} from './utilSelf';
defineOptions({ name: 'UserTaskListeners' });
const props = defineProps<Props>();
interface Props {
id?: string;
type?: string;
}
const prefix = inject<string>('prefix');
const width = inject<number>('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<any>(-1); // 字段所在下标,-1 为新增
const listenerFieldForm = ref<any>({}); // 监听器 注入字段 详情表单
const bpmnElement = ref<any>();
const bpmnElementListeners = ref<any[]>([]);
const otherExtensionList = ref<any[]>([]);
const listenerFormRef = ref<any>({});
const listenerFieldFormRef = ref<any>({});
interface BpmnInstances {
bpmnElement: any;
[key: string]: any;
}
declare global {
interface Window {
bpmnInstances?: BpmnInstances;
}
}
const bpmnInstances = () => window.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: any) => ex.$type === `${prefix}:TaskListener`,
) ?? [];
elementListenersList.value = bpmnElementListeners.value.map((listener) =>
initListenerType(listener),
);
};
const openListenerForm = (listener: any, index?: number) => {
if (listener) {
listenerForm.value = initListenerForm(listener);
editingListenerIndex.value = index || -1;
} else {
listenerForm.value = {};
editingListenerIndex.value = -1; // 标记为新增
}
if (listener && listener.fields) {
fieldsListOfListener.value = listener.fields.map((field: any) => ({
...field,
fieldType: field.string ? 'string' : 'expression',
}));
} else {
fieldsListOfListener.value = [];
listenerForm.value.fields = [];
}
// 打开侧边栏并清楚验证状态
listenerFormModelVisible.value = true;
nextTick(() => {
if (listenerFormRef.value) listenerFormRef.value.clearValidate();
});
};
// 移除监听器
const removeListener = (_: any, index: number) => {
// console.log(listener, 'listener');
Modal.confirm({
title: '提示',
content: '确认移除该监听器吗?',
okText: '确 认',
cancelText: '取 消',
onOk() {
bpmnElementListeners.value.splice(index, 1);
elementListenersList.value.splice(index, 1);
updateElementExtensions(bpmnElement.value, [
...otherExtensionList.value,
...bpmnElementListeners.value,
]);
},
onCancel() {
// console.info('操作取消');
},
});
};
// 保存监听器
const saveListenerConfig = async () => {
const 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: any) => ex.$type !== `${prefix}:TaskListener`,
) ?? [];
updateElementExtensions(bpmnElement.value, [
...otherExtensionList.value,
...bpmnElementListeners.value,
]);
// 4. 隐藏侧边栏
listenerFormModelVisible.value = false;
listenerForm.value = {};
};
// 打开监听器字段编辑弹窗
const openListenerFieldForm = (field: any, index?: number) => {
listenerFieldForm.value = field ? structuredClone(field) : {};
editingListenerFieldIndex.value = field ? index : -1;
listenerFieldFormModelVisible.value = true;
nextTick(() => {
if (listenerFieldFormRef.value) listenerFieldFormRef.value.clearValidate();
});
};
// 保存监听器注入字段
const saveListenerFiled = async () => {
const 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 = (_: any, index: number) => {
// console.log(field, 'field');
Modal.confirm({
title: '提示',
content: '确认移除该字段吗?',
okText: '确 认',
cancelText: '取 消',
onOk() {
fieldsListOfListener.value.splice(index, 1);
listenerForm.value.fields.splice(index, 1);
},
onCancel() {
// console.info('操作取消');
},
});
};
// 打开监听器弹窗
const processListenerDialogRef = ref<any>();
const openProcessListenerDialog = async () => {
processListenerDialogRef.value.open('task');
};
const selectProcessListener = (listener: any) => {
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: any) => ex.$type !== `${prefix}:TaskListener`,
) ?? [];
updateElementExtensions(
bpmnElement.value,
otherExtensionList.value?.concat(bpmnElementListeners.value),
);
};
watch(
() => props.id,
(val) => {
val &&
val.length > 0 &&
nextTick(() => {
resetListenersList();
});
},
{ immediate: true },
);
</script>
<template>
<div class="panel-tab__content">
<Table :data="elementListenersList" size="small" bordered>
<TableColumn title="序号" width="50px" type="index" />
<TableColumn
title="事件类型"
width="80px"
:ellipsis="{ showTitle: true }"
:custom-render="
({ record }: any) =>
listenerEventTypeObject[record.event as keyof typeof eventType]
"
/>
<TableColumn
title="事件id"
width="80px"
data-index="id"
:ellipsis="{ showTitle: true }"
/>
<TableColumn
title="监听器类型"
width="80px"
:ellipsis="{ showTitle: true }"
:custom-render="
({ record }: any) =>
listenerTypeObject[record.listenerType as keyof typeof listenerType]
"
/>
<TableColumn title="操作" width="90px">
<template #default="{ record, index }">
<Button
size="small"
type="link"
@click="openListenerForm(record, index)"
>
编辑
</Button>
<Divider type="vertical" />
<Button
size="small"
type="link"
danger
@click="removeListener(record, index)"
>
移除
</Button>
</template>
</TableColumn>
</Table>
<div class="element-drawer__button">
<Button size="small" type="primary" @click="openListenerForm(null)">
<template #icon><PlusOutlined /></template>
添加监听器
</Button>
<Button size="small" @click="openProcessListenerDialog">
<template #icon><SelectOutlined /></template>
选择监听器
</Button>
</div>
<!-- 监听器 编辑/创建 部分 -->
<Drawer
v-model:open="listenerFormModelVisible"
title="任务监听器"
:width="width"
:destroy-on-close="true"
>
<Form
:model="listenerForm"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
ref="listenerFormRef"
>
<FormItem
label="事件类型"
name="event"
:rules="[{ required: true, message: '请选择事件类型' }]"
>
<Select v-model:value="listenerForm.event">
<SelectOption
v-for="i in Object.keys(listenerEventTypeObject)"
:key="i"
:value="i"
>
{{ listenerEventTypeObject[i as keyof typeof eventType] }}
</SelectOption>
</Select>
</FormItem>
<FormItem
label="监听器ID"
name="id"
:rules="[{ required: true, message: '请输入监听器ID' }]"
>
<Input v-model:value="listenerForm.id" allow-clear />
</FormItem>
<FormItem
label="监听器类型"
name="listenerType"
:rules="[{ required: true, message: '请选择监听器类型' }]"
>
<Select v-model:value="listenerForm.listenerType">
<SelectOption
v-for="i in Object.keys(listenerTypeObject)"
:key="i"
:value="i"
>
{{ listenerTypeObject[i as keyof typeof listenerType] }}
</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="listenerForm.listenerType === 'classListener'"
label="Java类"
name="class"
key="listener-class"
:rules="[{ required: true, message: '请输入Java类' }]"
>
<Input v-model:value="listenerForm.class" allow-clear />
</FormItem>
<FormItem
v-if="listenerForm.listenerType === 'expressionListener'"
label="表达式"
name="expression"
key="listener-expression"
:rules="[{ required: true, message: '请输入表达式' }]"
>
<Input v-model:value="listenerForm.expression" allow-clear />
</FormItem>
<FormItem
v-if="listenerForm.listenerType === 'delegateExpressionListener'"
label="代理表达式"
name="delegateExpression"
key="listener-delegate"
:rules="[{ required: true, message: '请输入代理表达式' }]"
>
<Input v-model:value="listenerForm.delegateExpression" allow-clear />
</FormItem>
<template v-if="listenerForm.listenerType === 'scriptListener'">
<FormItem
label="脚本格式"
name="scriptFormat"
key="listener-script-format"
:rules="[{ required: true, message: '请填写脚本格式' }]"
>
<Input v-model:value="listenerForm.scriptFormat" allow-clear />
</FormItem>
<FormItem
label="脚本类型"
name="scriptType"
key="listener-script-type"
:rules="[{ required: true, message: '请选择脚本类型' }]"
>
<Select v-model:value="listenerForm.scriptType">
<SelectOption value="inlineScript">内联脚本</SelectOption>
<SelectOption value="externalScript">外部脚本</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="listenerForm.scriptType === 'inlineScript'"
label="脚本内容"
name="value"
key="listener-script"
:rules="[{ required: true, message: '请填写脚本内容' }]"
>
<Input v-model:value="listenerForm.value" allow-clear />
</FormItem>
<FormItem
v-if="listenerForm.scriptType === 'externalScript'"
label="资源地址"
name="resource"
key="listener-resource"
:rules="[{ required: true, message: '请填写资源地址' }]"
>
<Input v-model:value="listenerForm.resource" allow-clear />
</FormItem>
</template>
<template v-if="listenerForm.event === 'timeout'">
<FormItem
label="定时器类型"
name="eventDefinitionType"
key="eventDefinitionType"
>
<Select v-model:value="listenerForm.eventDefinitionType">
<SelectOption value="date">日期</SelectOption>
<SelectOption value="duration">持续时长</SelectOption>
<SelectOption value="cycle">循环</SelectOption>
<SelectOption value="null">无</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="
!!listenerForm.eventDefinitionType &&
listenerForm.eventDefinitionType !== 'null'
"
label="定时器"
name="eventTimeDefinitions"
key="eventTimeDefinitions"
:rules="[{ required: true, message: '请填写定时器配置' }]"
>
<Input
v-model:value="listenerForm.eventTimeDefinitions"
allow-clear
/>
</FormItem>
</template>
</Form>
<Divider />
<p class="listener-filed__title">
<span><MenuOutlined />注入字段:</span>
<Button
size="small"
type="primary"
@click="openListenerFieldForm(null)"
>
添加字段
</Button>
</p>
<Table
:data="fieldsListOfListener"
size="small"
:scroll="{ y: 240 }"
bordered
style="flex: none"
>
<TableColumn title="序号" width="50px" type="index" />
<TableColumn title="字段名称" width="100px" data-index="name" />
<TableColumn
title="字段类型"
width="80px"
:ellipsis="{ showTitle: true }"
:custom-render="
({ record }: any) =>
fieldTypeObject[record.fieldType as keyof typeof fieldType]
"
/>
<TableColumn
title="字段值/表达式"
width="100px"
:ellipsis="{ showTitle: true }"
:custom-render="
({ record }: any) => record.string || record.expression
"
/>
<TableColumn title="操作" width="100px">
<template #default="{ record, index }">
<Button
size="small"
type="link"
@click="openListenerFieldForm(record, index)"
>
编辑
</Button>
<Divider type="vertical" />
<Button
size="small"
type="link"
danger
@click="removeListenerField(record, index)"
>
移除
</Button>
</template>
</TableColumn>
</Table>
<div class="element-drawer__button">
<Button size="small" @click="listenerFormModelVisible = false">
取 消
</Button>
<Button size="small" type="primary" @click="saveListenerConfig">
保 存
</Button>
</div>
</Drawer>
<!-- 注入字段 编辑/创建 部分 -->
<Modal
title="字段配置"
v-model:open="listenerFieldFormModelVisible"
:width="600"
:destroy-on-close="true"
>
<Form
:model="listenerFieldForm"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
ref="listenerFieldFormRef"
style="height: 136px"
>
<FormItem
label="字段名称"
name="name"
:rules="[{ required: true, message: '请输入字段名称' }]"
>
<Input v-model:value="listenerFieldForm.name" allow-clear />
</FormItem>
<FormItem
label="字段类型"
name="fieldType"
:rules="[{ required: true, message: '请选择字段类型' }]"
>
<Select v-model:value="listenerFieldForm.fieldType">
<SelectOption
v-for="i in Object.keys(fieldTypeObject)"
:key="i"
:value="i"
>
{{ fieldTypeObject[i as keyof typeof fieldType] }}
</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="listenerFieldForm.fieldType === 'string'"
label="字段值"
name="string"
key="field-string"
:rules="[{ required: true, message: '请输入字段值' }]"
>
<Input v-model:value="listenerFieldForm.string" allow-clear />
</FormItem>
<FormItem
v-if="listenerFieldForm.fieldType === 'expression'"
label="表达式"
name="expression"
key="field-expression"
:rules="[{ required: true, message: '请输入表达式' }]"
>
<Input v-model:value="listenerFieldForm.expression" allow-clear />
</FormItem>
</Form>
<template #footer>
<Button size="small" @click="listenerFieldFormModelVisible = false">
取 消
</Button>
<Button size="small" type="primary" @click="saveListenerFiled">
确 定
</Button>
</template>
</Modal>
</div>
<!-- 选择弹窗 -->
<ProcessListenerDialog
ref="processListenerDialogRef"
@select="selectProcessListener"
/>
</template>

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: any) {
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: any) {
let listenerType;
if (listener.class) listenerType = 'classListener';
if (listener.expression) listenerType = 'expressionListener';
if (listener.delegateExpression) listenerType = 'delegateExpressionListener';
if (listener.script) listenerType = 'scriptListener';
return {
...structuredClone(listener),
...listener.script,
listenerType,
};
}
/** 将 ProcessListenerDO 转换成 initListenerForm 想同的 Form 对象 */
export function initListenerForm2(processListener: any) {
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,526 @@
<script lang="ts" setup>
import { inject, nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
import {
Button,
Checkbox,
Form,
FormItem,
Input,
InputNumber,
Radio,
RadioGroup,
Select,
} from 'ant-design-vue';
import {
APPROVE_METHODS,
ApproveMethodType,
} from '#/components/simple-process-design/consts';
defineOptions({ name: 'ElementMultiInstance' });
const props = defineProps({
businessObject: {
type: Object,
required: false,
default: () => ({}),
},
type: {
type: String,
required: false,
default: '',
},
id: {
type: String,
required: false,
default: '',
},
});
const prefix = inject<string>('prefix');
const loopCharacteristics = ref('');
// 默认配置,用来覆盖原始不存在的选项,避免报错
const defaultLoopInstanceForm = ref({
completionCondition: '',
loopCardinality: '',
extensionElements: [],
asyncAfter: false,
asyncBefore: false,
exclusive: false,
});
interface LoopInstanceForm {
completionCondition?: string;
loopCardinality?: string;
extensionElements?: any[];
asyncAfter?: boolean;
asyncBefore?: boolean;
exclusive?: boolean;
collection?: string;
elementVariable?: string;
timeCycle?: string;
}
const loopInstanceForm = ref<LoopInstanceForm>({});
const bpmnElement = ref<any>(null);
const multiLoopInstance = ref<any>(null);
declare global {
interface Window {
bpmnInstances?: () => any;
}
}
const bpmnInstances = () => (window as any)?.bpmnInstances;
const getElementLoop = (businessObject: any): void => {
if (!businessObject.loopCharacteristics) {
loopCharacteristics.value = 'Null';
loopInstanceForm.value = {};
return;
}
if (
businessObject.loopCharacteristics.$type ===
'bpmn:StandardLoopCharacteristics'
) {
loopCharacteristics.value = 'StandardLoop';
loopInstanceForm.value = {};
return;
}
loopCharacteristics.value = businessObject.loopCharacteristics.isSequential
? 'SequentialMultiInstance'
: '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 > 0
) {
loopInstanceForm.value.timeCycle =
businessObject.loopCharacteristics.extensionElements.values[0].body;
}
};
const changeLoopCharacteristicsType = (type: any): void => {
// 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;
}
// 时序
multiLoopInstance.value =
type === 'SequentialMultiInstance'
? bpmnInstances().moddle.create('bpmn:MultiInstanceLoopCharacteristics', {
isSequential: true,
})
: bpmnInstances().moddle.create('bpmn:MultiInstanceLoopCharacteristics', {
// eslint-disable-next-line no-template-curly-in-string
collection: '${coll_userList}',
});
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
loopCharacteristics: toRaw(multiLoopInstance.value),
});
};
// 循环基数
const updateLoopCardinality = (cardinality: string): void => {
let loopCardinality = null;
if (cardinality && cardinality.length > 0) {
loopCardinality = bpmnInstances().moddle.create('bpmn:FormalExpression', {
body: cardinality,
});
}
bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnElement.value),
multiLoopInstance.value,
{
loopCardinality,
},
);
};
// 完成条件
const updateLoopCondition = (condition: string): void => {
let completionCondition = null;
if (condition && condition.length > 0) {
completionCondition = bpmnInstances().moddle.create(
'bpmn:FormalExpression',
{
body: condition,
},
);
}
bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnElement.value),
multiLoopInstance.value,
{
completionCondition,
},
);
};
// 重试周期
const updateLoopTimeCycle = (timeCycle: string): void => {
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 = (): void => {
bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnElement.value),
multiLoopInstance.value,
{
collection: loopInstanceForm.value.collection || null,
elementVariable: loopInstanceForm.value.elementVariable || null,
},
);
};
// 各异步状态
const updateLoopAsync = (key: any): void => {
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 {
// @ts-ignore
asyncAttr[key] = loopInstanceForm.value[key];
}
bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnElement.value),
multiLoopInstance.value,
asyncAttr,
);
};
const changeConfig = (config: string): void => {
switch (config) {
case '会签': {
changeLoopCharacteristicsType('ParallelMultiInstance');
// eslint-disable-next-line no-template-curly-in-string
updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }');
break;
}
case '依次审批': {
changeLoopCharacteristicsType('SequentialMultiInstance');
updateLoopCardinality('1');
// eslint-disable-next-line no-template-curly-in-string
updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }');
break;
}
case '或签': {
changeLoopCharacteristicsType('ParallelMultiInstance');
// eslint-disable-next-line no-template-curly-in-string
updateLoopCondition('${ nrOfCompletedInstances > 0 }');
break;
}
// No default
}
};
/**
* -----新版本多实例-----
*/
const approveMethod = ref<ApproveMethodType | undefined>();
const approveRatio = ref<number>(100);
const otherExtensions = ref<any[]>([]);
const getElementLoopNew = (): void => {
if (props.type === 'UserTask') {
const extensionElements =
bpmnElement.value.businessObject?.extensionElements ??
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] });
approveMethod.value = extensionElements.values.find(
(ex: any) => ex.$type === `${prefix}:ApproveMethod`,
)?.value;
otherExtensions.value =
extensionElements.values.filter(
(ex: any) => ex.$type !== `${prefix}:ApproveMethod`,
) ?? [];
if (!approveMethod.value) {
approveMethod.value = ApproveMethodType.SEQUENTIAL_APPROVE;
updateLoopCharacteristics();
}
}
};
const onApproveMethodChange = (): void => {
approveRatio.value = 100;
updateLoopCharacteristics();
};
const onApproveRatioChange = (): void => {
updateLoopCharacteristics();
};
const updateLoopCharacteristics = (): void => {
// 根据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',
// eslint-disable-next-line no-template-curly-in-string
{ 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',
// eslint-disable-next-line no-template-curly-in-string
{ isSequential: false, collection: '${coll_userList}' },
);
multiLoopInstance.value.completionCondition =
bpmnInstances().moddle.create('bpmn:FormalExpression', {
// eslint-disable-next-line no-template-curly-in-string
body: '${ nrOfCompletedInstances > 0 }',
});
}
if (approveMethod.value === ApproveMethodType.SEQUENTIAL_APPROVE) {
multiLoopInstance.value = bpmnInstances().moddle.create(
'bpmn:MultiInstanceLoopCharacteristics',
// eslint-disable-next-line no-template-curly-in-string
{ isSequential: true, collection: '${coll_userList}' },
);
multiLoopInstance.value.loopCardinality = bpmnInstances().moddle.create(
'bpmn:FormalExpression',
{
body: '1',
},
);
multiLoopInstance.value.completionCondition =
bpmnInstances().moddle.create('bpmn:FormalExpression', {
// eslint-disable-next-line no-template-curly-in-string
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>
<template>
<div class="panel-tab__content">
<RadioGroup
v-if="type === 'UserTask'"
v-model:value="approveMethod"
@change="onApproveMethodChange"
>
<div class="flex-col">
<div v-for="(item, index) in APPROVE_METHODS" :key="index">
<Radio :value="item.value">
{{ item.label }}
</Radio>
<FormItem prop="approveRatio">
<InputNumber
v-model:value="approveRatio"
:min="10"
:max="100"
:step="10"
size="small"
v-if="
item.value === ApproveMethodType.APPROVE_BY_RATIO &&
approveMethod === ApproveMethodType.APPROVE_BY_RATIO
"
@change="onApproveRatioChange"
/>
</FormItem>
</div>
</div>
</RadioGroup>
<div v-else>除了UserTask以外节点的多实例待实现</div>
<!-- 与Simple设计器配置合并保留以前的代码 -->
<Form :label-col="{ span: 6 }" style="display: none">
<FormItem label="快捷配置">
<Button size="small" @click="() => changeConfig('依次审批')">
依次审批
</Button>
<Button size="small" @click="() => changeConfig('会签')">会签</Button>
<Button size="small" @click="() => changeConfig('或签')">或签</Button>
</FormItem>
<FormItem label="会签类型">
<Select
v-model:value="loopCharacteristics"
@change="changeLoopCharacteristicsType"
>
<Select.Option value="ParallelMultiInstance">
并行多重事件
</Select.Option>
<Select.Option value="SequentialMultiInstance">
时序多重事件
</Select.Option>
<Select.Option value="Null">无</Select.Option>
</Select>
</FormItem>
<template
v-if="
loopCharacteristics === 'ParallelMultiInstance' ||
loopCharacteristics === 'SequentialMultiInstance'
"
>
<FormItem label="循环数量" key="loopCardinality">
<Input
v-model:value="loopInstanceForm.loopCardinality"
allow-clear
@change="
() =>
updateLoopCardinality(loopInstanceForm.loopCardinality || '')
"
/>
</FormItem>
<FormItem label="集合" key="collection" v-show="false">
<Input
v-model:value="loopInstanceForm.collection"
allow-clear
@change="() => updateLoopBase()"
/>
</FormItem>
<!-- add by 芋艿:由于「元素变量」暂时用不到,所以这里 display 为 none -->
<FormItem label="元素变量" key="elementVariable" style="display: none">
<Input
v-model:value="loopInstanceForm.elementVariable"
allow-clear
@change="() => updateLoopBase()"
/>
</FormItem>
<FormItem label="完成条件" key="completionCondition">
<Input
v-model:value="loopInstanceForm.completionCondition"
allow-clear
@change="
() =>
updateLoopCondition(loopInstanceForm.completionCondition || '')
"
/>
</FormItem>
<!-- add by 芋艿:由于「异步状态」暂时用不到,所以这里 display 为 none -->
<FormItem label="异步状态" key="async" style="display: none">
<Checkbox
v-model:checked="loopInstanceForm.asyncBefore"
@change="() => updateLoopAsync('asyncBefore')"
>
异步前
</Checkbox>
<Checkbox
v-model:checked="loopInstanceForm.asyncAfter"
@change="() => updateLoopAsync('asyncAfter')"
>
异步后
</Checkbox>
<Checkbox
v-model:checked="loopInstanceForm.exclusive"
v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore"
@change="() => updateLoopAsync('exclusive')"
>
排除
</Checkbox>
</FormItem>
<FormItem
label="重试周期"
prop="timeCycle"
v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore"
key="timeCycle"
>
<Input
v-model:value="loopInstanceForm.timeCycle"
allow-clear
@change="
() => updateLoopTimeCycle(loopInstanceForm.timeCycle || '')
"
/>
</FormItem>
</template>
</Form>
</div>
</template>

View File

@@ -0,0 +1,74 @@
<script lang="ts" setup>
import { nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
import { Input } from 'ant-design-vue';
defineOptions({ name: 'ElementOtherConfig' });
const props = defineProps({
id: {
type: String,
default: '',
},
});
const { Textarea } = Input;
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 > 0) {
nextTick(() => {
const documentations =
bpmnInstances().bpmnElement.businessObject?.documentation;
documentation.value =
documentations && documentations.length > 0
? documentations[0].text
: '';
});
} else {
documentation.value = '';
}
},
{ immediate: true },
);
</script>
<template>
<div class="panel-tab__content">
<div class="element-property input-property">
<div class="element-property__label">元素文档</div>
<div class="element-property__value">
<Textarea
v-model:value="documentation"
:auto-size="{ minRows: 2, maxRows: 4 }"
@change="updateDocumentation"
@blur="updateDocumentation"
/>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,237 @@
<script lang="ts" setup>
import { inject, nextTick, ref, toRaw, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import {
Button,
Divider,
Form,
FormItem,
Input,
Modal,
Table,
TableColumn,
} from 'ant-design-vue';
defineOptions({ name: 'ElementProperties' });
const props = defineProps({
id: {
type: String,
default: '',
},
type: {
type: String,
default: '',
},
});
const prefix = inject('prefix');
// const width = inject('width')
const elementPropertyList = ref<Array<{ name: string; value: string }>>([]);
const propertyForm = ref<{ name?: string; value?: string }>({});
const editingPropertyIndex = ref(-1);
const propertyFormModelVisible = ref(false);
const bpmnElement = ref<any>();
const otherExtensionList = ref<any[]>([]);
const bpmnElementProperties = ref<any[]>([]);
const bpmnElementPropertyList = ref<any[]>([]);
const attributeFormRef = ref<any>();
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: any) => {
if (ex.$type !== `${prefix}:Properties`) {
otherExtensionList.value.push(ex);
}
return ex.$type === `${prefix}:Properties`;
},
) ?? [];
// 保存所有的 扩展属性字段
bpmnElementPropertyList.value = bpmnElementProperties.value.flatMap(
(current: any) => current.values,
);
// 复制 显示
elementPropertyList.value = structuredClone(
bpmnElementPropertyList.value ?? [],
);
};
const openAttributesForm = (
attr: null | { name: string; value: string },
index: number,
) => {
editingPropertyIndex.value = index;
// @ts-ignore
propertyForm.value = index === -1 ? {} : structuredClone(attr);
propertyFormModelVisible.value = true;
nextTick(() => {
if (attributeFormRef.value) attributeFormRef.value.clearValidate();
});
};
const removeAttributes = (
_attr: { name: string; value: string },
index: number,
) => {
Modal.confirm({
title: '提示',
content: '确认移除该属性吗?',
okText: '确 认',
cancelText: '取 消',
onOk() {
elementPropertyList.value.splice(index, 1);
bpmnElementPropertyList.value.splice(index, 1);
// 新建一个属性字段的保存列表
const propertiesObject = bpmnInstances().moddle.create(
`${prefix}:Properties`,
{
values: bpmnElementPropertyList.value,
},
);
updateElementExtensions(propertiesObject);
resetAttributesList();
},
onCancel() {
// console.info('操作取消');
},
});
};
const saveAttribute = () => {
// console.log(propertyForm.value, 'propertyForm.value');
const { name, value } = propertyForm.value;
if (editingPropertyIndex.value === -1) {
// 新建属性字段
const newPropertyObject = bpmnInstances().moddle.create(
`${prefix}:Property`,
{
name,
value,
},
);
// 新建一个属性字段的保存列表
const propertiesObject = bpmnInstances().moddle.create(
`${prefix}:Properties`,
{
values: [...bpmnElementPropertyList.value, newPropertyObject],
},
);
updateElementExtensions(propertiesObject);
} else {
bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnElement.value),
toRaw(bpmnElementPropertyList.value)[toRaw(editingPropertyIndex.value)],
{
name,
value,
},
);
}
propertyFormModelVisible.value = false;
resetAttributesList();
};
const updateElementExtensions = (properties: any) => {
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
values: [...otherExtensionList.value, properties],
});
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
extensionElements: extensions,
});
};
watch(
() => props.id,
(val) => {
if (val) {
val && val.length > 0 && resetAttributesList();
}
},
{ immediate: true },
);
</script>
<template>
<div class="panel-tab__content">
<Table :data="elementPropertyList" :scroll="{ y: 240 }" bordered>
<TableColumn title="序号" width="50">
<template #default="{ index }">
{{ index + 1 }}
</template>
</TableColumn>
<TableColumn
title="属性名"
data-index="name"
:min-width="100"
:ellipsis="{ showTitle: true }"
/>
<TableColumn
title="属性值"
data-index="value"
:min-width="100"
:ellipsis="{ showTitle: true }"
/>
<TableColumn title="操作" width="110">
<template #default="{ record, index }">
<Button
type="link"
@click="openAttributesForm(record, index)"
size="small"
>
编辑
</Button>
<Divider type="vertical" />
<Button
type="link"
size="small"
danger
@click="removeAttributes(record, index)"
>
移除
</Button>
</template>
</TableColumn>
</Table>
<div class="element-drawer__button">
<Button type="primary" @click="openAttributesForm(null, -1)">
<template #icon>
<IconifyIcon icon="ep:plus" />
</template>
添加属性
</Button>
</div>
<Modal
v-model:open="propertyFormModelVisible"
title="属性配置"
:width="600"
:destroy-on-close="true"
>
<Form
:model="propertyForm"
ref="attributeFormRef"
:label-col="{ span: 6 }"
>
<FormItem label="属性名:" name="name">
<Input v-model:value="propertyForm.name" allow-clear />
</FormItem>
<FormItem label="属性值:" name="value">
<Input v-model:value="propertyForm.value" allow-clear />
</FormItem>
</Form>
<template #footer>
<Button @click="propertyFormModelVisible = false"> </Button>
<Button type="primary" @click="saveAttribute"> </Button>
</template>
</Modal>
</div>
</template>

View File

@@ -0,0 +1,178 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import { IconifyIcon } from '@vben/icons';
import {
Button,
Form,
FormItem,
Input,
message,
Modal,
Table,
TableColumn,
} from 'ant-design-vue';
defineOptions({ name: 'SignalAndMassage' });
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(() => {
return modelType.value === 'message'
? { title: '创建消息', idLabel: '消息ID', nameLabel: '消息名称' }
: { 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: any) => {
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: any) => {
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>
<template>
<div class="panel-tab__content">
<div class="panel-tab__content--title">
<span>
<IconifyIcon icon="ep:menu" style="margin-right: 8px; color: #555" />
消息列表
</span>
<Button type="primary" title="创建新消息" @click="openModel('message')">
<template #icon>
<IconifyIcon icon="ep:plus" />
</template>
创建新消息
</Button>
</div>
<Table :data-source="messageList" :bordered="true" :pagination="false">
<TableColumn title="序号" width="60px">
<template #default="{ index }">
{{ index + 1 }}
</template>
</TableColumn>
<TableColumn
title="消息ID"
data-index="id"
:width="300"
:ellipsis="{ showTitle: true }"
/>
<TableColumn
title="消息名称"
data-index="name"
:width="300"
:ellipsis="{ showTitle: true }"
/>
</Table>
<div
class="panel-tab__content--title"
style="padding-top: 8px; margin-top: 8px; border-top: 1px solid #eee"
>
<span>
<IconifyIcon icon="ep:menu" style="margin-right: 8px; color: #555">
信号列表
</IconifyIcon>
</span>
<Button type="primary" title="创建新信号" @click="openModel('signal')">
<template #icon>
<IconifyIcon icon="ep:plus" />
</template>
创建新信号
</Button>
</div>
<Table :data-source="signalList" :bordered="true" :pagination="false">
<TableColumn title="序号" width="60px">
<template #default="{ index }">
{{ index + 1 }}
</template>
</TableColumn>
<TableColumn
title="信号ID"
data-index="id"
:width="300"
:ellipsis="{ showTitle: true }"
/>
<TableColumn
title="信号名称"
data-index="name"
:width="300"
:ellipsis="{ showTitle: true }"
/>
</Table>
<Modal
v-model:open="dialogVisible"
:title="modelConfig.title"
:mask-closable="false"
width="400px"
:destroy-on-close="true"
>
<Form
:model="modelObjectForm"
:label-col="{ span: 9 }"
:wrapper-col="{ span: 15 }"
>
<FormItem :label="modelConfig.idLabel">
<Input v-model:value="modelObjectForm.id" allow-clear />
</FormItem>
<FormItem :label="modelConfig.nameLabel">
<Input v-model:value="modelObjectForm.name" allow-clear />
</FormItem>
</Form>
<template #footer>
<Button @click="dialogVisible = false"> </Button>
<Button type="primary" @click="addNewObject"> </Button>
</template>
</Modal>
</div>
</template>

View File

@@ -0,0 +1,92 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { Checkbox, Form, FormItem } from 'ant-design-vue';
import { installedComponent } from './data';
defineOptions({ name: 'ElementTaskConfig' });
const props = defineProps({
id: {
type: String,
default: '',
},
type: {
type: String,
default: '',
},
});
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) {
// @ts-ignore
witchTaskComponent.value = installedComponent[props.type].component;
}
},
{ immediate: true },
);
</script>
<template>
<div class="panel-tab__content">
<Form :label-col="{ span: 9 }" :wrapper-col="{ span: 15 }">
<!-- add by 芋艿由于异步延续暂时用不到所以这里 display none -->
<FormItem label="异步延续" style="display: none">
<Checkbox
v-model:checked="taskConfigForm.asyncBefore"
@change="changeTaskAsync"
>
异步前
</Checkbox>
<Checkbox
v-model:checked="taskConfigForm.asyncAfter"
@change="changeTaskAsync"
>
异步后
</Checkbox>
<Checkbox
v-model:checked="taskConfigForm.exclusive"
v-if="taskConfigForm.asyncAfter || taskConfigForm.asyncBefore"
@change="changeTaskAsync"
>
排除
</Checkbox>
</FormItem>
<component :is="witchTaskComponent" v-bind="$props" />
</Form>
</div>
</template>

View File

@@ -0,0 +1,40 @@
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: keyof typeof installedComponent,
) => {
return installedComponent[elementType].name;
};
export const isTaskCollapseItemShow = (
elementType: keyof typeof installedComponent,
) => {
return installedComponent[elementType];
};

View File

@@ -0,0 +1,361 @@
<script lang="ts" setup>
import { inject, nextTick, ref, toRaw, watch } from 'vue';
import { alert } from '@vben/common-ui';
import { PlusOutlined } from '@vben/icons';
import {
Button,
Divider,
Form,
FormItem,
Input,
Modal,
Switch,
Table,
TableColumn,
} from 'ant-design-vue';
interface FormData {
processInstanceName: string;
calledElement: string;
inheritVariables: boolean;
businessKey: string;
inheritBusinessKey: boolean;
calledElementType: string;
}
defineOptions({ name: 'CallActivity' });
const props = defineProps({
id: { type: String, default: '' },
type: { type: String, default: '' },
});
const prefix = inject('prefix');
const formData = ref<FormData>({
processInstanceName: '',
calledElement: '',
inheritVariables: false,
businessKey: '',
inheritBusinessKey: false,
calledElementType: 'key',
});
const inVariableList = ref<any[]>([]);
const outVariableList = ref<any[]>([]);
const variableType = ref<string>(); // 参数类型
const editingVariableIndex = ref<number>(-1); // 编辑参数下标
const variableDialogVisible = ref<boolean>(false);
const varialbeFormRef = ref<any>();
const varialbeFormData = ref<{
source: string;
target: string;
}>({
source: '',
target: '',
});
const bpmnInstances = () => (window as any)?.bpmnInstances;
const bpmnElement = ref<any>();
const otherExtensionList = ref<any[]>([]);
const initCallActivity = () => {
bpmnElement.value = bpmnInstances().bpmnElement;
// console.log(bpmnElement.value.businessObject, 'callActivity');
// 初始化所有配置项
Object.keys(formData.value).forEach((key: string) => {
// @ts-ignore
formData.value[key] =
bpmnElement.value.businessObject[key] ??
formData.value[key as keyof FormData];
});
otherExtensionList.value = []; // 其他扩展配置
inVariableList.value.length = 0;
outVariableList.value.length = 0;
// 初始化输入参数
bpmnElement.value.businessObject?.extensionElements?.values?.forEach(
(ex: any) => {
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: keyof FormData) => {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
[attr]: formData.value[attr],
});
};
const openVariableForm = (type: string, data: any, index: number) => {
editingVariableIndex.value = index;
variableType.value = type;
varialbeFormData.value = index === -1 ? {} : { ...data };
variableDialogVisible.value = true;
};
const removeVariable = async (type: string, index: number) => {
try {
await alert('是否确认删除?');
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 > 0 &&
nextTick(() => {
initCallActivity();
});
},
{ immediate: true },
);
</script>
<template>
<div>
<Form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<FormItem label="实例名称">
<Input
v-model:value="formData.processInstanceName"
allow-clear
placeholder="请输入实例名称"
@change="updateCallActivityAttr('processInstanceName')"
/>
</FormItem>
<!-- TODO 需要可选择已存在的流程 -->
<FormItem label="被调用流程">
<Input
v-model:value="formData.calledElement"
allow-clear
placeholder="请输入被调用流程"
@change="updateCallActivityAttr('calledElement')"
/>
</FormItem>
<FormItem label="继承变量">
<Switch
v-model:checked="formData.inheritVariables"
@change="updateCallActivityAttr('inheritVariables')"
/>
</FormItem>
<FormItem label="继承业务键">
<Switch
v-model:checked="formData.inheritBusinessKey"
@change="updateCallActivityAttr('inheritBusinessKey')"
/>
</FormItem>
<FormItem v-if="!formData.inheritBusinessKey" label="业务键表达式">
<Input
v-model:value="formData.businessKey"
allow-clear
placeholder="请输入业务键表达式"
@change="updateCallActivityAttr('businessKey')"
/>
</FormItem>
<Divider />
<div>
<div class="mb-10px flex">
<span>输入参数</span>
<Button
class="ml-auto"
type="primary"
:icon="PlusOutlined"
title="添加参数"
size="small"
@click="openVariableForm('in', null, -1)"
/>
</div>
<Table
:data-source="inVariableList"
:scroll="{ y: 240 }"
bordered
:pagination="false"
>
<TableColumn
title="源"
data-index="source"
:min-width="100"
:ellipsis="true"
/>
<TableColumn
title="目标"
data-index="target"
:min-width="100"
:ellipsis="true"
/>
<TableColumn title="操作" :width="110">
<template #default="{ record, index }">
<Button
type="link"
@click="openVariableForm('in', record, index)"
size="small"
>
编辑
</Button>
<Divider type="vertical" />
<Button
type="link"
size="small"
danger
@click="removeVariable('in', index)"
>
移除
</Button>
</template>
</TableColumn>
</Table>
</div>
<Divider />
<div>
<div class="mb-10px flex">
<span>输出参数</span>
<Button
class="ml-auto"
type="primary"
:icon="PlusOutlined"
title="添加参数"
size="small"
@click="openVariableForm('out', null, -1)"
/>
</div>
<Table
:data-source="outVariableList"
:scroll="{ y: 240 }"
bordered
:pagination="false"
>
<TableColumn
title="源"
data-index="source"
:min-width="100"
:ellipsis="true"
/>
<TableColumn
title="目标"
data-index="target"
:min-width="100"
:ellipsis="true"
/>
<TableColumn title="操作" :width="110">
<template #default="{ record, index }">
<Button
type="link"
@click="openVariableForm('out', record, index)"
size="small"
>
编辑
</Button>
<Divider type="vertical" />
<Button
type="link"
size="small"
danger
@click="removeVariable('out', index)"
>
移除
</Button>
</template>
</TableColumn>
</Table>
</div>
</Form>
<!-- 添加或修改参数 -->
<Modal
v-model:open="variableDialogVisible"
title="参数配置"
:width="600"
:destroy-on-close="true"
@ok="saveVariable"
@cancel="variableDialogVisible = false"
>
<Form
:model="varialbeFormData"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
ref="varialbeFormRef"
>
<FormItem label="源:" name="source">
<Input v-model:value="varialbeFormData.source" allow-clear />
</FormItem>
<FormItem label="目标:" name="target">
<Input v-model:value="varialbeFormData.target" allow-clear />
</FormItem>
</Form>
</Modal>
</div>
</template>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,96 @@
<!-- 表达式选择 -->
<script setup lang="ts">
import type { BpmProcessExpressionApi } from '#/api/bpm/processExpression';
import { reactive, ref } from 'vue';
import { CommonStatusEnum } from '@vben/constants';
import { Button, Modal, Pagination, Table, TableColumn } from 'ant-design-vue';
import { getProcessExpressionPage } from '#/api/bpm/processExpression';
import { ContentWrap } from '#/components/content-wrap';
/** BPM 流程 表单 */
defineOptions({ name: 'ProcessExpressionDialog' });
/** 提交表单 */
const emit = defineEmits(['select']);
const dialogVisible = ref(false); // 弹窗的是否展示
const loading = ref(true); // 列表的加载中
const list = ref<BpmProcessExpressionApi.ProcessExpression[]>([]); // 列表的数据
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 getProcessExpressionPage(queryParams);
list.value = data.list;
total.value = data.total;
} finally {
loading.value = false;
}
};
// 定义 select 事件,用于操作成功后的回调
const select = async (row: BpmProcessExpressionApi.ProcessExpression) => {
dialogVisible.value = false;
// 发送操作成功的事件
emit('select', row);
};
// const handleCancel = () => {
// dialogVisible.value = false;
// };
</script>
<template>
<Modal
title="请选择表达式"
v-model:open="dialogVisible"
width="1024px"
:footer="null"
>
<ContentWrap>
<Table
:loading="loading"
:data-source="list"
:pagination="false"
:scroll="{ x: 'max-content' }"
>
<TableColumn title="名字" align="center" data-index="name" />
<TableColumn title="表达式" align="center" data-index="expression" />
<TableColumn title="操作" align="center">
<template #default="{ record }">
<Button type="primary" @click="select(record)"> 选择 </Button>
</template>
</TableColumn>
</Table>
<!-- 分页 -->
<div class="mt-4 flex justify-end">
<Pagination
:total="total"
v-model:current="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
show-size-changer
@change="getList"
/>
</div>
</ContentWrap>
</Modal>
</template>

View File

@@ -0,0 +1,156 @@
<script lang="ts" setup>
import {
h,
nextTick,
onBeforeUnmount,
onMounted,
ref,
toRaw,
watch,
} from 'vue';
import { PlusOutlined } from '@vben/icons';
import {
Button,
Form,
Input,
message,
Modal,
Select,
SelectOption,
} from 'ant-design-vue';
defineOptions({ name: 'ReceiveTask' });
const props = defineProps({
id: { type: String, default: '' },
type: { type: String, default: '' },
});
const bindMessageId = ref('');
const newMessageForm = ref<Record<string, any>>({});
const messageMap = ref<Record<string, any>>({});
const messageModelVisible = ref(false);
const bpmnElement = ref<any>();
const bpmnMessageRefsMap = ref<Record<string, 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;
// @ts-ignore
bpmnMessageRefsMap.value?.[newMessageForm.value.id] = newMessage;
messageModelVisible.value = false;
};
const updateTaskMessage = (messageId: string) => {
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: any) => el.$type === 'bpmn:Message')
.forEach((m: any) => {
// @ts-ignore
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>
<template>
<div style="margin-top: 16px">
<Form.Item label="消息实例">
<div
style="
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
"
>
<Select
v-model:value="bindMessageId"
@change="(value: any) => updateTaskMessage(value)"
>
<SelectOption
v-for="key in Object.keys(messageMap)"
:value="key"
:label="messageMap[key]"
:key="key"
/>
</Select>
<Button
type="primary"
:icon="h(PlusOutlined)"
style="margin-left: 8px"
@click="openMessageModel"
/>
</div>
</Form.Item>
<Modal
v-model:open="messageModelVisible"
:mask-closable="false"
title="创建新消息"
width="400px"
:destroy-on-close="true"
>
<Form :model="newMessageForm" size="small" :label-col="{ span: 6 }">
<Form.Item label="消息ID">
<Input v-model:value="newMessageForm.id" allow-clear />
</Form.Item>
<Form.Item label="消息名称">
<Input v-model:value="newMessageForm.name" allow-clear />
</Form.Item>
</Form>
<template #footer>
<Button size="small" type="primary" @click="createNewMessage">
</Button>
</template>
</Modal>
</div>
</template>

View File

@@ -0,0 +1,129 @@
<script lang="ts" setup>
import {
defineOptions,
defineProps,
nextTick,
onBeforeUnmount,
ref,
toRaw,
watch,
} from 'vue';
import {
FormItem,
Input,
Select,
SelectOption,
Textarea,
} from 'ant-design-vue';
defineOptions({ name: 'ScriptTask' });
const props = defineProps({
id: {
type: String,
default: '',
},
type: {
type: String,
default: '',
},
});
const defaultTaskForm = ref({
scriptFormat: '',
script: '',
resource: '',
resultVariable: '',
});
const scriptTaskForm = ref<any>({});
const bpmnElement = ref();
const bpmnInstances = () => (window as any)?.bpmnInstances;
const resetTaskForm = () => {
for (const key in defaultTaskForm.value) {
// @ts-ignore
scriptTaskForm.value[key] =
bpmnElement.value?.businessObject[
key as keyof typeof defaultTaskForm.value
] || defaultTaskForm.value[key as keyof typeof defaultTaskForm.value];
}
scriptTaskForm.value.scriptType = scriptTaskForm.value.script
? 'inline'
: 'external';
};
const updateElementTask = () => {
const 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>
<template>
<div class="mt-4">
<FormItem label="脚本格式">
<Input
v-model:value="scriptTaskForm.scriptFormat"
allow-clear
@input="updateElementTask()"
@change="updateElementTask()"
/>
</FormItem>
<FormItem label="脚本类型">
<Select v-model:value="scriptTaskForm.scriptType">
<SelectOption value="inline">内联脚本</SelectOption>
<SelectOption value="external">外部资源</SelectOption>
</Select>
</FormItem>
<FormItem label="脚本" v-show="scriptTaskForm.scriptType === 'inline'">
<Textarea
v-model:value="scriptTaskForm.script"
:auto-size="{ minRows: 2, maxRows: 4 }"
allow-clear
@input="updateElementTask()"
@change="updateElementTask()"
/>
</FormItem>
<FormItem
label="资源地址"
v-show="scriptTaskForm.scriptType === 'external'"
>
<Input
v-model:value="scriptTaskForm.resource"
allow-clear
@input="updateElementTask()"
@change="updateElementTask()"
/>
</FormItem>
<FormItem label="结果变量">
<Input
v-model:value="scriptTaskForm.resultVariable"
allow-clear
@input="updateElementTask()"
@change="updateElementTask()"
/>
</FormItem>
</div>
</template>

View File

@@ -0,0 +1,111 @@
<script lang="ts" setup>
import { nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
import { FormItem, Input, Select } from 'ant-design-vue';
defineOptions({ name: 'ServiceTask' });
const props = defineProps({
id: { type: String, default: '' },
type: { type: String, default: '' },
});
const defaultTaskForm = ref({
executeType: '',
class: '',
expression: '',
delegateExpression: '',
});
const serviceTaskForm = ref<any>({});
const bpmnElement = ref();
const bpmnInstances = () => (window as any)?.bpmnInstances;
const resetTaskForm = () => {
for (const key in defaultTaskForm.value) {
const value =
// @ts-ignore
bpmnElement.value?.businessObject[key] || defaultTaskForm.value[key];
serviceTaskForm.value[key] = value;
if (value) {
serviceTaskForm.value.executeType = key;
}
}
};
const updateElementTask = () => {
const taskAttr = Object.create(null);
const type = serviceTaskForm.value.executeType;
for (const 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>
<template>
<div>
<FormItem label="执行类型" key="executeType">
<Select
v-model:value="serviceTaskForm.executeType"
:options="[
{ label: 'Java类', value: 'class' },
{ label: '表达式', value: 'expression' },
{ label: '代理表达式', value: 'delegateExpression' },
]"
/>
</FormItem>
<FormItem
v-if="serviceTaskForm.executeType === 'class'"
label="Java类"
name="class"
key="execute-class"
>
<Input
v-model:value="serviceTaskForm.class"
allow-clear
@change="updateElementTask"
/>
</FormItem>
<FormItem
v-if="serviceTaskForm.executeType === 'expression'"
label="表达式"
name="expression"
key="execute-expression"
>
<Input
v-model:value="serviceTaskForm.expression"
allow-clear
@change="updateElementTask"
/>
</FormItem>
<FormItem
v-if="serviceTaskForm.executeType === 'delegateExpression'"
label="代理表达式"
name="delegateExpression"
key="execute-delegate"
>
<Input
v-model:value="serviceTaskForm.delegateExpression"
allow-clear
@change="updateElementTask"
/>
</FormItem>
</div>
</template>

View File

@@ -0,0 +1,563 @@
<script lang="ts" setup>
import type { BpmProcessExpressionApi } from '#/api/bpm/processExpression';
import type { BpmUserGroupApi } from '#/api/bpm/userGroup';
import type { SystemPostApi } from '#/api/system/post';
import type { SystemRoleApi } from '#/api/system/role';
import type { SystemUserApi } from '#/api/system/user';
import {
computed,
h,
inject,
nextTick,
onBeforeUnmount,
onMounted,
ref,
toRaw,
watch,
} from 'vue';
import { SelectOutlined } from '@vben/icons';
import { handleTree } from '@vben/utils';
import {
Button,
Form,
FormItem,
Select,
SelectOption,
Textarea,
TreeSelect,
} from 'ant-design-vue';
import { getUserGroupSimpleList } from '#/api/bpm/userGroup';
import { getSimpleDeptList } from '#/api/system/dept';
import { getSimplePostList } from '#/api/system/post';
import { getSimpleRoleList } from '#/api/system/role';
import { getSimpleUserList } from '#/api/system/user';
import {
CANDIDATE_STRATEGY,
CandidateStrategy,
FieldPermissionType,
MULTI_LEVEL_DEPT,
} from '#/components/simple-process-design/consts';
import { useFormFieldsPermission } from '#/components/simple-process-design/helpers';
import ProcessExpressionDialog from './ProcessExpressionDialog.vue';
defineOptions({ name: 'UserTask' });
const props = defineProps({
id: {
type: String,
default: '',
},
type: {
type: String,
default: '',
},
});
const prefix = inject('prefix');
const userTaskForm = ref({
candidateStrategy: undefined, // 分配规则
candidateParam: [], // 分配选项
skipExpression: '', // 跳过表达式
});
const bpmnElement = ref<any>();
const bpmnInstances = () => (window as Record<string, any>)?.bpmnInstances;
const roleOptions = ref<SystemRoleApi.Role[]>([]); // 角色列表
const deptTreeOptions = ref<any>(); // 部门树
const postOptions = ref<SystemPostApi.Post[]>([]); // 岗位列表
const userOptions = ref<SystemUserApi.User[]>([]); // 用户列表
const userGroupOptions = ref<BpmUserGroupApi.UserGroup[]>([]); // 用户组列表
const treeRef = ref<any>();
const { formFieldOptions } = useFormFieldsPermission(FieldPermissionType.READ);
// 定义 TreeSelect 的默认属性映射
const defaultProps = {
children: 'children',
label: 'name',
value: 'id',
};
// 表单内用户字段选项, 必须是必填和用户选择器
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<any>();
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: any) => ex.$type === `${prefix}:CandidateStrategy`,
)?.[0]?.value;
const candidateParamStr = extensionElements.values?.filter(
(ex: any) => ex.$type === `${prefix}:CandidateParam`,
)?.[0]?.value;
if (candidateParamStr && candidateParamStr.length > 0) {
// eslint-disable-next-line unicorn/prefer-switch
if (userTaskForm.value.candidateStrategy === CandidateStrategy.EXPRESSION) {
// 特殊:流程表达式,只有一个 input 输入框
// @ts-ignore
userTaskForm.value.candidateParam = [candidateParamStr];
} else if (
userTaskForm.value.candidateStrategy ===
CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
) {
// 特殊:多级不部门负责人,需要通过'|'分割
userTaskForm.value.candidateParam = candidateParamStr
.split('|')[0]
.split(',')
.map((item: any) => {
// 如果数字超出了最大安全整数范围,则将其作为字符串处理
const 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
) {
// @ts-ignore
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: any) => {
// 如果数字超出了最大安全整数范围,则将其作为字符串处理
const 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: any) =>
ex.$type !== `${prefix}:CandidateStrategy` &&
ex.$type !== `${prefix}:CandidateParam`,
) ?? [];
// 跳过表达式
userTaskForm.value.skipExpression =
businessObject.skipExpression === undefined
? ''
: businessObject.skipExpression;
// 改用通过extensionElements来存储数据
// 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 = Array.isArray(userTaskForm.value.candidateParam)
? 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<any>();
const openProcessExpressionDialog = async () => {
processExpressionDialogRef.value.open();
};
const selectProcessExpression = (
expression: BpmProcessExpressionApi.ProcessExpression,
) => {
// @ts-ignore
userTaskForm.value.candidateParam = [expression.expression];
updateElementTask();
};
const handleFormUserChange = (e: any) => {
if (e === 'PROCESS_START_USER_ID') {
userTaskForm.value.candidateParam = [];
// @ts-ignore
userTaskForm.value.candidateStrategy = CandidateStrategy.START_USER;
}
updateElementTask();
};
watch(
() => props.id,
() => {
bpmnElement.value = bpmnInstances().bpmnElement;
nextTick(() => {
resetTaskForm();
});
},
{ immediate: true },
);
onMounted(async () => {
// 获得角色列表
roleOptions.value = await getSimpleRoleList();
// 获得部门列表
const deptOptions = await getSimpleDeptList();
deptTreeOptions.value = handleTree(deptOptions, 'id');
// 获得岗位列表
postOptions.value = await getSimplePostList();
// 获得用户列表
userOptions.value = await getSimpleUserList();
// 获得用户组列表
userGroupOptions.value = await getUserGroupSimpleList();
});
onBeforeUnmount(() => {
bpmnElement.value = null;
});
</script>
<template>
<Form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<FormItem label="规则类型" name="candidateStrategy">
<Select
v-model:value="userTaskForm.candidateStrategy"
allow-clear
style="width: 100%"
@change="changeCandidateStrategy"
>
<SelectOption
v-for="(dict, index) in CANDIDATE_STRATEGY"
:key="index"
:label="dict.label"
:value="dict.value"
/>
</Select>
</FormItem>
<FormItem
v-if="userTaskForm.candidateStrategy === CandidateStrategy.ROLE"
label="指定角色"
name="candidateParam"
>
<Select
v-model:value="userTaskForm.candidateParam"
allow-clear
mode="multiple"
style="width: 100%"
@change="updateElementTask"
>
<SelectOption
v-for="item in roleOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</Select>
</FormItem>
<FormItem
v-if="
userTaskForm.candidateStrategy === CandidateStrategy.DEPT_MEMBER ||
userTaskForm.candidateStrategy === CandidateStrategy.DEPT_LEADER ||
userTaskForm.candidateStrategy ===
CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
"
label="指定部门"
name="candidateParam"
>
<TreeSelect
ref="treeRef"
v-model:value="userTaskForm.candidateParam"
:tree-data="deptTreeOptions"
:field-names="defaultProps"
placeholder="加载中,请稍后"
multiple
tree-checkable
@change="updateElementTask"
/>
</FormItem>
<FormItem
v-if="userTaskForm.candidateStrategy === CandidateStrategy.POST"
label="指定岗位"
name="candidateParam"
>
<Select
v-model:value="userTaskForm.candidateParam"
allow-clear
mode="multiple"
style="width: 100%"
@change="updateElementTask"
>
<SelectOption
v-for="item in postOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</Select>
</FormItem>
<FormItem
v-if="userTaskForm.candidateStrategy === CandidateStrategy.USER"
label="指定用户"
name="candidateParam"
>
<Select
v-model:value="userTaskForm.candidateParam"
allow-clear
mode="multiple"
style="width: 100%"
@change="updateElementTask"
>
<SelectOption
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</Select>
</FormItem>
<FormItem
v-if="userTaskForm.candidateStrategy === CandidateStrategy.USER_GROUP"
label="指定用户组"
name="candidateParam"
>
<Select
v-model:value="userTaskForm.candidateParam"
allow-clear
mode="multiple"
style="width: 100%"
@change="updateElementTask"
>
<SelectOption
v-for="item in userGroupOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</Select>
</FormItem>
<FormItem
v-if="userTaskForm.candidateStrategy === CandidateStrategy.FORM_USER"
label="表单内用户字段"
name="formUser"
>
<Select
v-model:value="userTaskForm.candidateParam"
allow-clear
style="width: 100%"
@change="handleFormUserChange"
>
<SelectOption
v-for="(item, idx) in userFieldOnFormOptions"
:key="idx"
:label="item.title"
:value="item.field"
:disabled="!item.required"
/>
</Select>
</FormItem>
<FormItem
v-if="
userTaskForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER
"
label="表单内部门字段"
name="formDept"
>
<Select
v-model:value="userTaskForm.candidateParam"
allow-clear
style="width: 100%"
@change="updateElementTask"
>
<SelectOption
v-for="(item, idx) in deptFieldOnFormOptions"
:key="idx"
:label="item.title"
:value="item.field"
:disabled="!item.required"
/>
</Select>
</FormItem>
<FormItem
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!"
name="deptLevel"
>
<Select v-model:value="deptLevel" allow-clear @change="updateElementTask">
<SelectOption
v-for="(item, index) in MULTI_LEVEL_DEPT"
:key="index"
:label="item.label"
:value="item.value"
/>
</Select>
</FormItem>
<FormItem
v-if="userTaskForm.candidateStrategy === CandidateStrategy.EXPRESSION"
label="流程表达式"
name="candidateParam"
>
<Textarea
v-model:value="userTaskForm.candidateParam[0]"
allow-clear
style="width: 100%"
@change="updateElementTask"
/>
<Button
class="!w-1/1 mt-5px"
type="primary"
:icon="h(SelectOutlined)"
@click="openProcessExpressionDialog"
>
选择表达式
</Button>
<!-- 选择弹窗 -->
<ProcessExpressionDialog
ref="processExpressionDialogRef"
@select="selectProcessExpression"
/>
</FormItem>
<FormItem label="跳过表达式" name="skipExpression">
<Textarea
v-model:value="userTaskForm.skipExpression"
allow-clear
style="width: 100%"
@change="updateSkipExpression"
/>
</FormItem>
</Form>
</template>

View File

@@ -0,0 +1,380 @@
<script setup>
import { ref, watch } from 'vue';
import {
Button,
Checkbox,
DatePicker,
Input,
InputNumber,
Radio,
Tabs,
} from 'ant-design-vue';
const props = defineProps({
value: {
type: String,
default: '',
},
});
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表达式
const 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 = d.replace(new RegExp(`\\d+${type}`), val + type);
} else {
d += 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>
<template>
<Tabs v-model:active-key="tab">
<Tabs.TabPane key="cron" tab="CRON表达式">
<div style="margin-bottom: 10px">
<Input
v-model:value="cronStr"
readonly
style="width: 400px; font-weight: bold"
key="cronStr"
/>
</div>
<div style="display: flex; gap: 8px; margin-bottom: 8px">
<Input
v-model:value="fields.second"
placeholder="秒"
style="width: 80px"
key="second"
/>
<Input
v-model:value="fields.minute"
placeholder="分"
style="width: 80px"
key="minute"
/>
<Input
v-model:value="fields.hour"
placeholder="时"
style="width: 80px"
key="hour"
/>
<Input
v-model:value="fields.day"
placeholder="天"
style="width: 80px"
key="day"
/>
<Input
v-model:value="fields.month"
placeholder="月"
style="width: 80px"
key="month"
/>
<Input
v-model:value="fields.week"
placeholder="周"
style="width: 80px"
key="week"
/>
<Input
v-model:value="fields.year"
placeholder="年"
style="width: 80px"
key="year"
/>
</div>
<Tabs
v-model:active-key="activeField"
type="card"
style="margin-bottom: 8px"
>
<Tabs.TabPane v-for="f in cronFieldList" :key="f.key" :tab="f.label">
<div style="margin-bottom: 8px">
<Radio.Group
v-model:value="cronMode[f.key]"
:key="`radio-${f.key}`"
>
<Radio value="every" :key="`every-${f.key}`">
{{ f.label }}
</Radio>
<Radio value="range" :key="`range-${f.key}`">
<InputNumber
v-model:value="cronRange[f.key][0]"
:min="f.min"
:max="f.max"
size="small"
style="width: 60px"
:key="`range0-${f.key}`"
/>
<InputNumber
v-model:value="cronRange[f.key][1]"
:min="f.min"
:max="f.max"
size="small"
style="width: 60px"
:key="`range1-${f.key}`"
/>
之间每{{ f.label }}
</Radio>
<Radio value="step" :key="`step-${f.key}`">
从第
<InputNumber
v-model:value="cronStep[f.key][0]"
:min="f.min"
:max="f.max"
size="small"
style="width: 60px"
:key="`step0-${f.key}`"
/>
开始每
<InputNumber
v-model:value="cronStep[f.key][1]"
:min="1"
:max="f.max"
size="small"
style="width: 60px"
:key="`step1-${f.key}`"
/>
{{ f.label }}
</Radio>
<Radio value="appoint" :key="`appoint-${f.key}`"> 指定 </Radio>
</Radio.Group>
</div>
<div v-if="cronMode[f.key] === 'appoint'">
<Checkbox.Group
v-model:value="cronAppoint[f.key]"
:key="`group-${f.key}`"
>
<Checkbox
v-for="n in f.max + 1"
:key="`cb-${f.key}-${n - 1}`"
:value="pad(n - 1)"
>
{{ pad(n - 1) }}
</Checkbox>
</Checkbox.Group>
</div>
</Tabs.TabPane>
</Tabs>
</Tabs.TabPane>
<Tabs.TabPane key="iso" title="标准格式" tab="iso-tab">
<div style="margin-bottom: 10px">
<Input
v-model:value="isoStr"
placeholder="如R1/2025-05-21T21:59:54/P3DT30M30S"
style="width: 400px; font-weight: bold"
key="isoStr"
/>
</div>
<div style="margin-bottom: 10px">
循环次数<InputNumber
v-model:value="repeat"
:min="1"
style="width: 100px"
key="repeat"
/>
</div>
<div style="margin-bottom: 10px">
日期时间<DatePicker
v-model:value="isoDate"
show-time
placeholder="选择日期时间"
style="width: 200px"
key="isoDate"
/>
</div>
<div style="margin-bottom: 10px">
当前时长<Input
v-model:value="isoDuration"
placeholder="如P3DT30M30S"
style="width: 200px"
key="isoDuration"
/>
</div>
<div>
<div>
<Button
v-for="s in [5, 10, 30, 50]"
@click="setDuration('S', s)"
:key="`sec-${s}`"
>
{{ s }}
</Button>
自定义
</div>
<div>
<Button
v-for="m in [5, 10, 30, 50]"
@click="setDuration('M', m)"
:key="`min-${m}`"
>
{{ m }}
</Button>
自定义
</div>
<div>
小时
<Button
v-for="h in [4, 8, 12, 24]"
@click="setDuration('H', h)"
:key="`hour-${h}`"
>
{{ h }}
</Button>
自定义
</div>
<div>
<Button
v-for="d in [1, 2, 3, 4]"
@click="setDuration('D', d)"
:key="`day-${d}`"
>
{{ d }}
</Button>
自定义
</div>
<div>
<Button
v-for="mo in [1, 2, 3, 4]"
@click="setDuration('M', mo)"
:key="`mon-${mo}`"
>
{{ mo }}
</Button>
自定义
</div>
<div>
<Button
v-for="y in [1, 2, 3, 4]"
@click="setDuration('Y', y)"
:key="`year-${y}`"
>
{{ y }}
</Button>
自定义
</div>
</div>
</Tabs.TabPane>
</Tabs>
</template>

View File

@@ -0,0 +1,99 @@
<script setup>
import { ref, watch } from 'vue';
import { Button, Input } from 'ant-design-vue';
const props = defineProps({
value: {
type: String,
default: '',
},
});
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 || Number.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>
<template>
<div>
<div style="margin-bottom: 10px">
当前选择<Input
v-model:value="isoString"
readonly
style="width: 300px"
/>
</div>
<div v-for="unit in units" :key="unit.key" style="margin-bottom: 8px">
<span>{{ unit.label }}</span>
<Button.Group>
<Button
v-for="val in unit.presets"
:key="val"
size="small"
@click="setUnit(unit.key, val)"
>
{{ val }}
</Button>
<Input
v-model:value="custom[unit.key]"
size="small"
style="width: 60px; margin-left: 8px"
placeholder="自定义"
@change="setUnit(unit.key, custom[unit.key])"
/>
</Button.Group>
</div>
</div>
</template>

View File

@@ -0,0 +1,357 @@
<script lang="ts" setup>
import type { Ref } from 'vue';
import { computed, nextTick, onMounted, ref, toRaw, watch } from 'vue';
import {
CheckCircleFilled,
ExclamationCircleFilled,
IconifyIcon,
QuestionCircleFilled,
} from '@vben/icons';
import { Button, DatePicker, Input, Modal, Tooltip } from 'ant-design-vue';
import CycleConfig from './CycleConfig.vue';
import DurationConfig from './DurationConfig.vue';
const props = defineProps({
businessObject: {
type: Object,
default: () => ({}),
},
});
const bpmnInstances = () => (window as any).bpmnInstances;
const type: Ref<string> = ref('time');
const condition: Ref<string> = ref('');
const valid: Ref<boolean> = ref(true);
const showDatePicker: Ref<boolean> = ref(false);
const showDurationDialog: Ref<boolean> = ref(false);
const showCycleDialog: Ref<boolean> = ref(false);
const showHelp: Ref<boolean> = ref(false);
const dateValue: Ref<Date | null> = ref(null);
// const bpmnElement = ref(null);
const placeholder = computed<string>(() => {
if (type.value === 'time') return '请输入时间';
if (type.value === 'duration') return '请输入持续时长';
if (type.value === 'cycle') return '请输入循环表达式';
return '';
});
const helpText = computed<string>(() => {
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<string>(() => {
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(): void {
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: string) {
type.value = t;
condition.value = '';
updateNode();
}
// 输入校验
watch([type, condition], () => {
valid.value = validate();
// updateNode() // 可以注释掉,避免频繁触发
});
function validate(): boolean {
if (type.value === 'time') {
return !!condition.value && !Number.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: any) {
dateValue.value = val;
}
function onDateConfirm(): void {
if (dateValue.value) {
condition.value = new Date(dateValue.value).toISOString();
showDatePicker.value = false;
updateNode();
}
}
// 持续时长
function onDurationChange(val: string) {
condition.value = val;
}
function onDurationConfirm(): void {
showDurationDialog.value = false;
updateNode();
}
// 循环
function onCycleChange(val: string) {
condition.value = val;
}
function onCycleConfirm(): void {
showCycleDialog.value = false;
updateNode();
}
// 输入框聚焦时弹窗(可选)
function handleInputFocus(): void {
if (type.value === 'time') showDatePicker.value = true;
if (type.value === 'duration') showDurationDialog.value = true;
if (type.value === 'cycle') showCycleDialog.value = true;
}
// 同步到节点
function updateNode(): void {
const moddle = (window.bpmnInstances as any)?.moddle;
const modeling = (window.bpmnInstances as any)?.modeling;
const elementRegistry = (window.bpmnInstances as any)?.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>
<template>
<div class="panel-tab__content">
<div style="margin-top: 10px">
<span>类型</span>
<Button.Group>
<Button
size="small"
:type="type === 'time' ? 'primary' : 'default'"
@click="setType('time')"
>
时间
</Button>
<Button
size="small"
:type="type === 'duration' ? 'primary' : 'default'"
@click="setType('duration')"
>
持续
</Button>
<Button
size="small"
:type="type === 'cycle' ? 'primary' : 'default'"
@click="setType('cycle')"
>
循环
</Button>
</Button.Group>
<CheckCircleFilled v-if="valid" style="color: green; margin-left: 8px" />
</div>
<div style="display: flex; align-items: center; margin-top: 10px">
<span>条件</span>
<Input
v-model:value="condition"
:placeholder="placeholder"
style="width: calc(100% - 100px)"
:readonly="type !== 'duration' && type !== 'cycle'"
@focus="handleInputFocus"
@blur="updateNode"
>
<template #suffix>
<Tooltip v-if="!valid" title="格式错误" placement="top">
<ExclamationCircleFilled style="color: orange" />
</Tooltip>
<Tooltip :title="helpText" placement="top">
<QuestionCircleFilled
style="color: #409eff; cursor: pointer"
@click="showHelp = true"
/>
</Tooltip>
<Button
v-if="type === 'time'"
@click="showDatePicker = true"
style="margin-left: 4px"
shape="circle"
size="small"
>
<IconifyIcon icon="ep:calendar" />
</Button>
<Button
v-if="type === 'duration'"
@click="showDurationDialog = true"
style="margin-left: 4px"
shape="circle"
size="small"
>
<IconifyIcon icon="ep:timer" />
</Button>
<Button
v-if="type === 'cycle'"
@click="showCycleDialog = true"
style="margin-left: 4px"
shape="circle"
size="small"
>
<IconifyIcon icon="ep:setting" />
</Button>
</template>
</Input>
</div>
<!-- 时间选择器 -->
<Modal
v-model:open="showDatePicker"
title="选择时间"
width="400px"
@cancel="showDatePicker = false"
>
<DatePicker
v-model:value="dateValue"
show-time
placeholder="选择日期时间"
style="width: 100%"
@change="onDateChange"
/>
<template #footer>
<Button @click="showDatePicker = false">取消</Button>
<Button type="primary" @click="onDateConfirm">确定</Button>
</template>
</Modal>
<!-- 持续时长选择器 -->
<Modal
v-model:open="showDurationDialog"
title="时间配置"
width="600px"
@cancel="showDurationDialog = false"
>
<DurationConfig :value="condition" @change="onDurationChange" />
<template #footer>
<Button @click="showDurationDialog = false">取消</Button>
<Button type="primary" @click="onDurationConfirm">确定</Button>
</template>
</Modal>
<!-- 循环配置器 -->
<Modal
v-model:open="showCycleDialog"
title="时间配置"
width="800px"
@cancel="showCycleDialog = false"
>
<CycleConfig :value="condition" @change="onCycleChange" />
<template #footer>
<Button @click="showCycleDialog = false">取消</Button>
<Button type="primary" @click="onCycleConfirm">确定</Button>
</template>
</Modal>
<!-- 帮助说明 -->
<Modal
v-model:open="showHelp"
title="格式说明"
width="600px"
@cancel="showHelp = false"
>
<div v-html="helpHtml"></div>
<template #footer>
<Button @click="showHelp = false">关闭</Button>
</template>
</Modal>
</div>
</template>
<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;

View File

@@ -3,6 +3,7 @@
"extends": "@vben/tsconfig/web-app.json",
"compilerOptions": {
"baseUrl": ".",
"allowJs": true,
"paths": {
"#/*": ["./src/*"]
}