refactor: 重构 bpmnProcessDesigner => bpmn-process-designer

This commit is contained in:
puhui999
2025-09-15 09:43:52 +08:00
parent 85de19a422
commit 26f00f3d37
67 changed files with 35 additions and 11 deletions

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>