!280 Merge remote-tracking branch 'yudao/dev' into dev
Merge pull request !280 from Jason/dev
This commit is contained in:
@@ -57,6 +57,8 @@
|
|||||||
"vue": "catalog:",
|
"vue": "catalog:",
|
||||||
"vue-dompurify-html": "catalog:",
|
"vue-dompurify-html": "catalog:",
|
||||||
"vue-router": "catalog:",
|
"vue-router": "catalog:",
|
||||||
|
"vue3-print-nb": "catalog:",
|
||||||
|
"vue3-signature": "catalog:",
|
||||||
"vuedraggable": "catalog:"
|
"vuedraggable": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -15,6 +15,33 @@ export namespace BpmTaskApi {
|
|||||||
valueType: string; // 监听器值类型
|
valueType: string; // 监听器值类型
|
||||||
processInstance?: BpmProcessInstanceApi.ProcessInstance; // 流程实例
|
processInstance?: BpmProcessInstanceApi.ProcessInstance; // 流程实例
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 流程任务
|
||||||
|
export interface TaskManager {
|
||||||
|
id: string; // 编号
|
||||||
|
name: string; // 任务名称
|
||||||
|
createTime: number; // 创建时间
|
||||||
|
endTime: number; // 结束时间
|
||||||
|
durationInMillis: number; // 持续时间
|
||||||
|
status: number; // 状态
|
||||||
|
reason: string; // 原因
|
||||||
|
ownerUser: any; // 负责人
|
||||||
|
assigneeUser: any; // 处理人
|
||||||
|
taskDefinitionKey: string; // 任务定义key
|
||||||
|
processInstanceId: string; // 流程实例id
|
||||||
|
processInstance: BpmProcessInstanceApi.ProcessInstance; // 流程实例
|
||||||
|
parentTaskId: any; // 父任务id
|
||||||
|
children: any; // 子任务
|
||||||
|
formId: any; // 表单id
|
||||||
|
formName: any; // 表单名称
|
||||||
|
formConf: any; // 表单配置
|
||||||
|
formFields: any; // 表单字段
|
||||||
|
formVariables: any; // 表单变量
|
||||||
|
buttonsSetting: any; // 按钮设置
|
||||||
|
signEnable: any; // 签名设置
|
||||||
|
reasonRequire: any; // 原因设置
|
||||||
|
nodeType: any; // 节点类型
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询待办任务分页 */
|
/** 查询待办任务分页 */
|
||||||
|
|||||||
@@ -27,25 +27,25 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// path: 'process-instance/detail',
|
path: 'process-instance/detail',
|
||||||
// component: () => import('#/views/bpm/processInstance/detail/index.vue'),
|
component: () => import('#/views/bpm/processInstance/detail/index.vue'),
|
||||||
// name: 'BpmProcessInstanceDetail',
|
name: 'BpmProcessInstanceDetail',
|
||||||
// meta: {
|
meta: {
|
||||||
// title: '流程详情',
|
title: '流程详情',
|
||||||
// activePath: '/bpm/task/my',
|
activePath: '/bpm/task/my',
|
||||||
// icon: 'ant-design:history-outlined',
|
icon: 'ant-design:history-outlined',
|
||||||
// keepAlive: false,
|
keepAlive: false,
|
||||||
// hideInMenu: true,
|
hideInMenu: true,
|
||||||
// },
|
},
|
||||||
// props: (route) => {
|
props: (route) => {
|
||||||
// return {
|
return {
|
||||||
// id: route.query.id,
|
id: route.query.id,
|
||||||
// taskId: route.query.taskId,
|
taskId: route.query.taskId,
|
||||||
// activityId: route.query.activityId,
|
activityId: route.query.activityId,
|
||||||
// };
|
};
|
||||||
// },
|
},
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
path: '/bpm/manager/form/edit',
|
path: '/bpm/manager/form/edit',
|
||||||
name: 'BpmFormEditor',
|
name: 'BpmFormEditor',
|
||||||
|
|||||||
423
apps/web-ele/src/views/bpm/processInstance/detail/index.vue
Normal file
423
apps/web-ele/src/views/bpm/processInstance/detail/index.vue
Normal file
@@ -0,0 +1,423 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
|
||||||
|
import type { SystemUserApi } from '#/api/system/user';
|
||||||
|
|
||||||
|
import { nextTick, onMounted, ref, shallowRef, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import {
|
||||||
|
BpmFieldPermissionType,
|
||||||
|
BpmModelFormType,
|
||||||
|
BpmModelType,
|
||||||
|
BpmTaskStatusEnum,
|
||||||
|
DICT_TYPE,
|
||||||
|
} from '@vben/constants';
|
||||||
|
import {
|
||||||
|
IconifyIcon,
|
||||||
|
SvgBpmApproveIcon,
|
||||||
|
SvgBpmCancelIcon,
|
||||||
|
SvgBpmRejectIcon,
|
||||||
|
SvgBpmRunningIcon,
|
||||||
|
} from '@vben/icons';
|
||||||
|
import { formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ElAvatar,
|
||||||
|
ElCard,
|
||||||
|
ElCol,
|
||||||
|
ElMessage,
|
||||||
|
ElRow,
|
||||||
|
ElTabPane,
|
||||||
|
ElTabs,
|
||||||
|
} from 'element-plus';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getApprovalDetail as getApprovalDetailApi,
|
||||||
|
getProcessInstanceBpmnModelView,
|
||||||
|
} from '#/api/bpm/processInstance';
|
||||||
|
import { getSimpleUserList } from '#/api/system/user';
|
||||||
|
import DictTag from '#/components/dict-tag/dict-tag.vue';
|
||||||
|
import { setConfAndFields2 } from '#/components/form-create';
|
||||||
|
import { registerComponent } from '#/utils';
|
||||||
|
|
||||||
|
import ProcessInstanceBpmnViewer from './modules/bpm-viewer.vue';
|
||||||
|
import ProcessInstanceOperationButton from './modules/operation-button.vue';
|
||||||
|
import ProcessssPrint from './modules/process-print.vue';
|
||||||
|
import ProcessInstanceSimpleViewer from './modules/simple-bpm-viewer.vue';
|
||||||
|
import BpmProcessInstanceTaskList from './modules/task-list.vue';
|
||||||
|
import ProcessInstanceTimeline from './modules/time-line.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'BpmProcessInstanceDetail' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
activityId?: string; // 流程活动编号,用于抄送查看
|
||||||
|
id: string; // 流程实例的编号
|
||||||
|
taskId?: string; // 任务编号
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const processInstanceLoading = ref(false); // 流程实例的加载中
|
||||||
|
const processInstance = ref<BpmProcessInstanceApi.ProcessInstance>(); // 流程实例
|
||||||
|
const processDefinition = ref<any>({}); // 流程定义
|
||||||
|
const processModelView = ref<any>({}); // 流程模型视图
|
||||||
|
const operationButtonRef = ref(); // 操作按钮组件 ref
|
||||||
|
const activeTab = ref('form');
|
||||||
|
const taskListRef = ref();
|
||||||
|
const auditIconsMap: {
|
||||||
|
[key: string]:
|
||||||
|
| typeof SvgBpmApproveIcon
|
||||||
|
| typeof SvgBpmCancelIcon
|
||||||
|
| typeof SvgBpmRejectIcon
|
||||||
|
| typeof SvgBpmRunningIcon;
|
||||||
|
} = {
|
||||||
|
[BpmTaskStatusEnum.RUNNING]: SvgBpmRunningIcon,
|
||||||
|
[BpmTaskStatusEnum.APPROVE]: SvgBpmApproveIcon,
|
||||||
|
[BpmTaskStatusEnum.REJECT]: SvgBpmRejectIcon,
|
||||||
|
[BpmTaskStatusEnum.CANCEL]: SvgBpmCancelIcon,
|
||||||
|
[BpmTaskStatusEnum.APPROVING]: SvgBpmApproveIcon,
|
||||||
|
[BpmTaskStatusEnum.RETURN]: SvgBpmRejectIcon,
|
||||||
|
[BpmTaskStatusEnum.WAIT]: SvgBpmRunningIcon,
|
||||||
|
};
|
||||||
|
const activityNodes = ref<BpmProcessInstanceApi.ApprovalNodeInfo[]>([]); // 审批节点信息
|
||||||
|
const userOptions = ref<SystemUserApi.User[]>([]); // 用户列表
|
||||||
|
|
||||||
|
const fApi = ref<any>();
|
||||||
|
const detailForm = ref({
|
||||||
|
rule: [],
|
||||||
|
option: {},
|
||||||
|
value: {},
|
||||||
|
}); // 流程实例的表单详情
|
||||||
|
const writableFields: Array<string> = []; // 表单可以编辑的字段
|
||||||
|
|
||||||
|
const BusinessFormComponent = shallowRef<any>(null); // 异步组件(业务表单)
|
||||||
|
|
||||||
|
/** 获取详情 */
|
||||||
|
async function getDetail() {
|
||||||
|
// 获得审批详情
|
||||||
|
await getApprovalDetail();
|
||||||
|
// 获得流程模型视图
|
||||||
|
await getProcessModelView();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得审批详情 */
|
||||||
|
async function getApprovalDetail() {
|
||||||
|
processInstanceLoading.value = true;
|
||||||
|
try {
|
||||||
|
const param = {
|
||||||
|
processInstanceId: props.id,
|
||||||
|
activityId: props.activityId,
|
||||||
|
taskId: props.taskId,
|
||||||
|
};
|
||||||
|
const data = await getApprovalDetailApi(param);
|
||||||
|
if (!data) {
|
||||||
|
ElMessage.error('查询不到审批详情信息!');
|
||||||
|
}
|
||||||
|
if (!data.processDefinition || !data.processInstance) {
|
||||||
|
ElMessage.error('查询不到流程信息!');
|
||||||
|
}
|
||||||
|
|
||||||
|
processInstance.value = data.processInstance;
|
||||||
|
processDefinition.value = data.processDefinition;
|
||||||
|
|
||||||
|
// 设置表单信息
|
||||||
|
if (processDefinition.value.formType === BpmModelFormType.NORMAL) {
|
||||||
|
// 获取表单字段权限
|
||||||
|
const formFieldsPermission = data.formFieldsPermission;
|
||||||
|
// 清空可编辑字段为空
|
||||||
|
writableFields.splice(0);
|
||||||
|
if (detailForm.value.rule?.length > 0) {
|
||||||
|
// 避免刷新 form-create 显示不了
|
||||||
|
detailForm.value.value = processInstance.value.formVariables;
|
||||||
|
} else {
|
||||||
|
setConfAndFields2(
|
||||||
|
detailForm,
|
||||||
|
processDefinition.value.formConf,
|
||||||
|
processDefinition.value.formFields,
|
||||||
|
processInstance.value.formVariables,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
fApi.value?.btn.show(false);
|
||||||
|
fApi.value?.resetBtn.show(false);
|
||||||
|
fApi.value?.disabled(true);
|
||||||
|
// 设置表单字段权限
|
||||||
|
if (formFieldsPermission) {
|
||||||
|
Object.keys(data.formFieldsPermission).forEach((item) => {
|
||||||
|
setFieldPermission(item, formFieldsPermission[item]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 注意:data.processDefinition.formCustomViewPath 是组件的全路径,例如说:/crm/contract/detail/index.vue
|
||||||
|
BusinessFormComponent.value = registerComponent(
|
||||||
|
data?.processDefinition?.formCustomViewPath || '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取审批节点,显示 Timeline 的数据
|
||||||
|
activityNodes.value = data.activityNodes;
|
||||||
|
|
||||||
|
// 获取待办任务显示操作按钮
|
||||||
|
operationButtonRef.value?.loadTodoTask(data.todoTask);
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('获取审批详情失败!');
|
||||||
|
} finally {
|
||||||
|
processInstanceLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取流程模型视图*/
|
||||||
|
async function getProcessModelView() {
|
||||||
|
if (BpmModelType.BPMN === processDefinition.value?.modelType) {
|
||||||
|
// 重置,解决 BPMN 流程图刷新不会重新渲染问题
|
||||||
|
processModelView.value = {
|
||||||
|
bpmnXml: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const data = await getProcessInstanceBpmnModelView(props.id);
|
||||||
|
if (data) {
|
||||||
|
processModelView.value = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 设置表单权限 */
|
||||||
|
function setFieldPermission(field: string, permission: string) {
|
||||||
|
if (permission === BpmFieldPermissionType.READ) {
|
||||||
|
fApi.value?.disabled(true, field);
|
||||||
|
}
|
||||||
|
if (permission === BpmFieldPermissionType.WRITE) {
|
||||||
|
fApi.value?.disabled(false, field);
|
||||||
|
// 加入可以编辑的字段
|
||||||
|
writableFields.push(field);
|
||||||
|
}
|
||||||
|
if (permission === BpmFieldPermissionType.NONE) {
|
||||||
|
fApi.value?.hidden(true, field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 操作成功后刷新 */
|
||||||
|
const refresh = () => {
|
||||||
|
// 重新获取详情
|
||||||
|
getDetail();
|
||||||
|
};
|
||||||
|
|
||||||
|
const [PrintModal, printModalApi] = useVbenModal({
|
||||||
|
connectedComponent: ProcessssPrint,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 打开打印对话框 */
|
||||||
|
function handlePrint() {
|
||||||
|
printModalApi.setData({ processInstanceId: props.id }).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 监听 Tab 切换,当切换到 "record" 标签时刷新任务列表 */
|
||||||
|
watch(
|
||||||
|
() => activeTab.value,
|
||||||
|
async (newVal) => {
|
||||||
|
if (newVal === 'record') {
|
||||||
|
// 如果切换到流转记录标签,刷新任务列表
|
||||||
|
await nextTick();
|
||||||
|
taskListRef.value?.refresh();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(async () => {
|
||||||
|
await getDetail();
|
||||||
|
// 获得用户列表
|
||||||
|
userOptions.value = await getSimpleUserList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<ElCard
|
||||||
|
class="flex h-full flex-col"
|
||||||
|
:body-style="{
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
paddingTop: '12px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span class="text-gray-500">编号:{{ id || '-' }}</span>
|
||||||
|
<IconifyIcon
|
||||||
|
icon="lucide:printer"
|
||||||
|
class="hover:text-primary cursor-pointer"
|
||||||
|
@click="handlePrint"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="flex h-full flex-col">
|
||||||
|
<!-- 流程基本信息 -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex h-10 items-center gap-5">
|
||||||
|
<div class="mb-1 text-2xl font-bold">
|
||||||
|
{{ processInstance?.name }}
|
||||||
|
</div>
|
||||||
|
<DictTag
|
||||||
|
v-if="processInstance?.status"
|
||||||
|
:type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS"
|
||||||
|
:value="processInstance.status"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex h-12 items-center gap-5 text-sm">
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2 rounded-3xl bg-gray-100 px-2.5 py-1 dark:bg-gray-600"
|
||||||
|
>
|
||||||
|
<ElAvatar
|
||||||
|
:size="28"
|
||||||
|
v-if="processInstance?.startUser?.avatar"
|
||||||
|
:src="processInstance?.startUser?.avatar"
|
||||||
|
/>
|
||||||
|
<ElAvatar
|
||||||
|
:size="28"
|
||||||
|
v-else-if="processInstance?.startUser?.nickname"
|
||||||
|
>
|
||||||
|
{{ processInstance?.startUser?.nickname.substring(0, 1) }}
|
||||||
|
</ElAvatar>
|
||||||
|
<span class="text-sm">
|
||||||
|
{{ processInstance?.startUser?.nickname }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-gray-500">
|
||||||
|
{{ formatDateTime(processInstance?.startTime) }} 提交
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<component
|
||||||
|
v-if="processInstance?.status"
|
||||||
|
:is="auditIconsMap[processInstance?.status]"
|
||||||
|
class="absolute right-5 top-2.5 size-36"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 流程操作 -->
|
||||||
|
<div class="flex h-full flex-1 flex-col">
|
||||||
|
<ElTabs v-model="activeTab">
|
||||||
|
<ElTabPane label="审批详情" name="form" class="pb-20 pr-3">
|
||||||
|
<ElRow :gutter="24">
|
||||||
|
<ElCol
|
||||||
|
:xs="24"
|
||||||
|
:sm="24"
|
||||||
|
:md="18"
|
||||||
|
:lg="18"
|
||||||
|
:xl="16"
|
||||||
|
class="h-full"
|
||||||
|
>
|
||||||
|
<!-- 流程表单 -->
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
processDefinition?.formType === BpmModelFormType.NORMAL
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<form-create
|
||||||
|
v-model="detailForm.value"
|
||||||
|
v-model:api="fApi"
|
||||||
|
:option="detailForm.option"
|
||||||
|
:rule="detailForm.rule"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="
|
||||||
|
processDefinition?.formType === BpmModelFormType.CUSTOM
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<BusinessFormComponent :id="processInstance?.businessKey" />
|
||||||
|
</div>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :xs="24" :sm="24" :md="6" :lg="6" :xl="8">
|
||||||
|
<div class="mt-2">
|
||||||
|
<ProcessInstanceTimeline :activity-nodes="activityNodes" />
|
||||||
|
</div>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
</ElTabPane>
|
||||||
|
<ElTabPane label="流程图" name="diagram" class="pb-20 pr-3">
|
||||||
|
<div>
|
||||||
|
<ProcessInstanceSimpleViewer
|
||||||
|
v-show="
|
||||||
|
processDefinition.modelType &&
|
||||||
|
processDefinition.modelType === BpmModelType.SIMPLE
|
||||||
|
"
|
||||||
|
:loading="processInstanceLoading"
|
||||||
|
:model-view="processModelView"
|
||||||
|
/>
|
||||||
|
<ProcessInstanceBpmnViewer
|
||||||
|
v-show="
|
||||||
|
processDefinition.modelType &&
|
||||||
|
processDefinition.modelType === BpmModelType.BPMN
|
||||||
|
"
|
||||||
|
:loading="processInstanceLoading"
|
||||||
|
:model-view="processModelView"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ElTabPane>
|
||||||
|
<ElTabPane label="流转记录" name="record" class="pb-20 pr-3">
|
||||||
|
<BpmProcessInstanceTaskList
|
||||||
|
ref="taskListRef"
|
||||||
|
:loading="processInstanceLoading"
|
||||||
|
:id="id"
|
||||||
|
/>
|
||||||
|
</ElTabPane>
|
||||||
|
<!-- TODO 待开发 -->
|
||||||
|
<ElTabPane
|
||||||
|
label="流转评论"
|
||||||
|
name="comment"
|
||||||
|
v-if="false"
|
||||||
|
class="pr-3"
|
||||||
|
>
|
||||||
|
<div>待开发</div>
|
||||||
|
</ElTabPane>
|
||||||
|
</ElTabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="px-4">
|
||||||
|
<ProcessInstanceOperationButton
|
||||||
|
ref="operationButtonRef"
|
||||||
|
:process-instance="processInstance"
|
||||||
|
:process-definition="processDefinition"
|
||||||
|
:user-options="userOptions"
|
||||||
|
:normal-form="detailForm"
|
||||||
|
:normal-form-api="fApi"
|
||||||
|
:writable-fields="writableFields"
|
||||||
|
@success="refresh"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElCard>
|
||||||
|
<!-- 打印对话框 -->
|
||||||
|
<PrintModal />
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.el-tabs) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs__content) {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
// import { MyProcessViewer } from '#/views/bpm/components/bpmn-process-designer/package';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ProcessInstanceBpmnViewer' });
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
bpmnXml?: string;
|
||||||
|
loading?: boolean; // 是否加载中
|
||||||
|
modelView?: Object;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
loading: false,
|
||||||
|
modelView: () => ({}),
|
||||||
|
bpmnXml: '',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// BPMN 流程图数据
|
||||||
|
const view = ref({
|
||||||
|
bpmnXml: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 监控 modelView 更新 */
|
||||||
|
watch(
|
||||||
|
() => props.modelView,
|
||||||
|
async (newModelView) => {
|
||||||
|
// 加载最新
|
||||||
|
if (newModelView) {
|
||||||
|
// @ts-ignore
|
||||||
|
view.value = newModelView;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 监听 bpmnXml */
|
||||||
|
watch(
|
||||||
|
() => props.bpmnXml,
|
||||||
|
(value) => {
|
||||||
|
view.value.bpmnXml = value;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-loading="loading"
|
||||||
|
class="h-full w-full overflow-auto rounded-lg border border-gray-200 bg-white p-4"
|
||||||
|
>
|
||||||
|
<!-- <MyProcessViewer
|
||||||
|
key="processViewer"
|
||||||
|
:xml="view.bpmnXml"
|
||||||
|
:view="view"
|
||||||
|
class="h-full min-h-[500px] w-full"
|
||||||
|
/> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,299 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictLabel } from '@vben/hooks';
|
||||||
|
import { useUserStore } from '@vben/stores';
|
||||||
|
import { formatDate } from '@vben/utils';
|
||||||
|
|
||||||
|
// @ts-ignore - 安装 vue3-print-nb 局部指令 v-print
|
||||||
|
import vPrint from 'vue3-print-nb';
|
||||||
|
|
||||||
|
import { getProcessInstancePrintData } from '#/api/bpm/processInstance';
|
||||||
|
import { decodeFields } from '#/components/form-create';
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
const printData = ref<BpmProcessInstanceApi.ProcessPrintDataRespVO>();
|
||||||
|
const userName = computed(() => userStore.userInfo?.nickname ?? '');
|
||||||
|
const printTime = ref(formatDate(new Date(), 'YYYY-MM-DD HH:mm'));
|
||||||
|
const formFields = ref<any[]>([]);
|
||||||
|
const printDataMap = ref<Record<string, any>>({});
|
||||||
|
|
||||||
|
/** 打印配置 */
|
||||||
|
const printObj = ref({
|
||||||
|
id: 'printDivTag',
|
||||||
|
popTitle: ' ',
|
||||||
|
extraCss: '/print.css',
|
||||||
|
extraHead: '',
|
||||||
|
zIndex: 20_003,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
closable: true,
|
||||||
|
footer: false,
|
||||||
|
title: '打印流程',
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
const { processInstanceId } = modalApi.getData<{
|
||||||
|
processInstanceId: string;
|
||||||
|
}>();
|
||||||
|
if (processInstanceId) {
|
||||||
|
await fetchPrintData(processInstanceId);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 获取打印数据 */
|
||||||
|
async function fetchPrintData(id: string) {
|
||||||
|
printData.value = await getProcessInstancePrintData(id);
|
||||||
|
initPrintDataMap();
|
||||||
|
parseFormFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析表单字段 */
|
||||||
|
function parseFormFields() {
|
||||||
|
if (!printData.value) return;
|
||||||
|
|
||||||
|
const formFieldsObj = decodeFields(
|
||||||
|
printData.value.processInstance.processDefinition?.formFields || [],
|
||||||
|
);
|
||||||
|
const processVariables = printData.value.processInstance.formVariables;
|
||||||
|
const res: any = [];
|
||||||
|
|
||||||
|
for (const item of formFieldsObj) {
|
||||||
|
const id = item.field;
|
||||||
|
const name = item.title;
|
||||||
|
const fieldKey = item.field as string;
|
||||||
|
const variable = processVariables[fieldKey];
|
||||||
|
let html = variable;
|
||||||
|
|
||||||
|
switch (item.type) {
|
||||||
|
case 'checkbox':
|
||||||
|
case 'radio':
|
||||||
|
case 'select': {
|
||||||
|
const options = item.options;
|
||||||
|
const temp: any = [];
|
||||||
|
if (Array.isArray(options)) {
|
||||||
|
if (Array.isArray(variable)) {
|
||||||
|
const labels = options
|
||||||
|
.filter((o: any) => variable.includes(o.value))
|
||||||
|
.map((o: any) => o.label);
|
||||||
|
temp.push(...labels);
|
||||||
|
} else {
|
||||||
|
const opt = options.find((o: any) => o.value === variable);
|
||||||
|
if (opt) {
|
||||||
|
temp.push(opt.label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html = temp.join(',');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'UploadImg': {
|
||||||
|
const imgEl = document.createElement('img');
|
||||||
|
imgEl.setAttribute('src', variable);
|
||||||
|
imgEl.setAttribute('style', 'max-width: 600px;');
|
||||||
|
html = imgEl.outerHTML;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// TODO 更多表单打印展示
|
||||||
|
}
|
||||||
|
|
||||||
|
printDataMap.value[fieldKey] = html;
|
||||||
|
res.push({ id, name, html });
|
||||||
|
}
|
||||||
|
|
||||||
|
formFields.value = res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化打印数据映射 */
|
||||||
|
function initPrintDataMap() {
|
||||||
|
if (!printData.value) return;
|
||||||
|
|
||||||
|
printDataMap.value.startUser =
|
||||||
|
printData.value.processInstance.startUser?.nickname || '';
|
||||||
|
printDataMap.value.startUserDept =
|
||||||
|
printData.value.processInstance.startUser?.deptName || '';
|
||||||
|
printDataMap.value.processName = printData.value.processInstance.name;
|
||||||
|
printDataMap.value.processNum = printData.value.processInstance.id;
|
||||||
|
printDataMap.value.startTime = formatDate(
|
||||||
|
printData.value.processInstance.startTime,
|
||||||
|
);
|
||||||
|
printDataMap.value.endTime = formatDate(
|
||||||
|
printData.value.processInstance.endTime,
|
||||||
|
);
|
||||||
|
printDataMap.value.processStatus = getDictLabel(
|
||||||
|
DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS,
|
||||||
|
printData.value.processInstance.status,
|
||||||
|
);
|
||||||
|
printDataMap.value.printUser = userName.value;
|
||||||
|
printDataMap.value.printTime = printTime.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取打印模板 HTML */
|
||||||
|
function getPrintTemplateHTML() {
|
||||||
|
if (!printData.value?.printTemplateHtml) return '';
|
||||||
|
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(
|
||||||
|
printData.value.printTemplateHtml,
|
||||||
|
'text/html',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 替换 mentions
|
||||||
|
const mentions = doc.querySelectorAll('[data-w-e-type="mention"]');
|
||||||
|
mentions.forEach((item) => {
|
||||||
|
const htmlElement = item as HTMLElement;
|
||||||
|
const mentionId = JSON.parse(
|
||||||
|
decodeURIComponent(htmlElement.dataset.info ?? ''),
|
||||||
|
).id;
|
||||||
|
item.innerHTML = printDataMap.value[mentionId] ?? '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 替换流程记录
|
||||||
|
const processRecords = doc.querySelectorAll(
|
||||||
|
'[data-w-e-type="process-record"]',
|
||||||
|
);
|
||||||
|
const processRecordTable: Element = document.createElement('table');
|
||||||
|
|
||||||
|
if (processRecords.length > 0) {
|
||||||
|
// 构建流程记录 html
|
||||||
|
processRecordTable.setAttribute('class', 'w-full border-collapse');
|
||||||
|
|
||||||
|
const headTr = document.createElement('tr');
|
||||||
|
const headTd = document.createElement('td');
|
||||||
|
headTd.setAttribute('colspan', '2');
|
||||||
|
headTd.setAttribute('class', 'border border-black p-1.5 text-center');
|
||||||
|
headTd.innerHTML = '流程记录';
|
||||||
|
headTr.append(headTd);
|
||||||
|
processRecordTable.append(headTr);
|
||||||
|
|
||||||
|
printData.value?.tasks.forEach((item) => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
const td1 = document.createElement('td');
|
||||||
|
td1.setAttribute('class', 'border border-black p-1.5');
|
||||||
|
td1.innerHTML = item.name;
|
||||||
|
const td2 = document.createElement('td');
|
||||||
|
td2.setAttribute('class', 'border border-black p-1.5');
|
||||||
|
td2.innerHTML = item.description;
|
||||||
|
tr.append(td1);
|
||||||
|
tr.append(td2);
|
||||||
|
processRecordTable.append(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
processRecords.forEach((item) => {
|
||||||
|
item.innerHTML = processRecordTable.outerHTML;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 返回 html
|
||||||
|
return doc.body.innerHTML;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal class="w-2/3">
|
||||||
|
<div id="printDivTag" class="break-all">
|
||||||
|
<!-- eslint-disable vue/no-v-html 使用自定义打印模板 -->
|
||||||
|
<div
|
||||||
|
v-if="printData?.printTemplateEnable"
|
||||||
|
v-html="getPrintTemplateHTML()"
|
||||||
|
></div>
|
||||||
|
<div v-else-if="printData">
|
||||||
|
<h2 class="mb-3 text-center text-xl font-bold">
|
||||||
|
{{ printData.processInstance.name }}
|
||||||
|
</h2>
|
||||||
|
<div class="mb-2 flex justify-between text-sm">
|
||||||
|
<div>
|
||||||
|
{{ `流程编号: ${printData.processInstance.id}` }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ `打印人员: ${userName}` }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table class="mt-3 w-full border-collapse">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="w-1/4 border border-black p-1.5">发起人</td>
|
||||||
|
<td class="w-1/4 border border-black p-1.5">
|
||||||
|
{{ printData.processInstance.startUser?.nickname }}
|
||||||
|
</td>
|
||||||
|
<td class="w-1/4 border border-black p-1.5">发起时间</td>
|
||||||
|
<td class="w-1/4 border border-black p-1.5">
|
||||||
|
{{ formatDate(printData.processInstance.startTime) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="w-1/4 border border-black p-1.5">所属部门</td>
|
||||||
|
<td class="w-1/4 border border-black p-1.5">
|
||||||
|
{{ printData.processInstance.startUser?.deptName }}
|
||||||
|
</td>
|
||||||
|
<td class="w-1/4 border border-black p-1.5">流程状态</td>
|
||||||
|
<td class="w-1/4 border border-black p-1.5">
|
||||||
|
{{
|
||||||
|
getDictLabel(
|
||||||
|
DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS,
|
||||||
|
printData.processInstance.status,
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
class="w-full border border-black p-1.5 text-center"
|
||||||
|
colspan="4"
|
||||||
|
>
|
||||||
|
<h4>表单内容</h4>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="item in formFields" :key="item.id">
|
||||||
|
<td class="w-1/5 border border-black p-1.5">
|
||||||
|
{{ item.name }}
|
||||||
|
</td>
|
||||||
|
<td class="w-4/5 border border-black p-1.5" colspan="3">
|
||||||
|
<div v-html="item.html"></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
class="w-full border border-black p-1.5 text-center"
|
||||||
|
colspan="4"
|
||||||
|
>
|
||||||
|
<h4>流程记录</h4>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="item in printData.tasks" :key="item.id">
|
||||||
|
<td class="w-1/5 border border-black p-1.5">
|
||||||
|
{{ item.name }}
|
||||||
|
</td>
|
||||||
|
<td class="w-4/5 border border-black p-1.5" colspan="3">
|
||||||
|
{{ item.description }}
|
||||||
|
<div v-if="item.signPicUrl && item.signPicUrl.length > 0">
|
||||||
|
<img class="h-10 w-[90px]" :src="item.signPicUrl" alt="" />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<ElButton @click="modalApi.close()">取 消</ElButton>
|
||||||
|
<ElButton v-print="printObj" type="primary">打 印</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
import { base64ToFile } from '@vben/utils';
|
||||||
|
|
||||||
|
import Vue3Signature from 'vue3-signature';
|
||||||
|
|
||||||
|
import { uploadFile } from '#/api/infra/file';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'BpmProcessInstanceSignature',
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(['success']);
|
||||||
|
|
||||||
|
const signature = ref<InstanceType<typeof Vue3Signature>>();
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
const signFileUrl = await uploadFile({
|
||||||
|
file: base64ToFile(signature?.value?.save('image/jpeg') || '', '签名'),
|
||||||
|
});
|
||||||
|
emits('success', signFileUrl);
|
||||||
|
await modalApi.close();
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal title="流程签名" class="w-3/5">
|
||||||
|
<div class="mb-2 flex justify-end">
|
||||||
|
<ElSpace>
|
||||||
|
<ElTooltip content="撤销上一步操作">
|
||||||
|
<ElButton @click="signature?.undo()">
|
||||||
|
<template #icon>
|
||||||
|
<IconifyIcon icon="lucide:undo" class="mb-1 size-4" />
|
||||||
|
</template>
|
||||||
|
撤销
|
||||||
|
</ElButton>
|
||||||
|
</ElTooltip>
|
||||||
|
<ElTooltip content="清空画布">
|
||||||
|
<ElButton @click="signature?.clear()">
|
||||||
|
<template #icon>
|
||||||
|
<IconifyIcon icon="lucide:trash" class="mb-1 size-4" />
|
||||||
|
</template>
|
||||||
|
<span>清除</span>
|
||||||
|
</ElButton>
|
||||||
|
</ElTooltip>
|
||||||
|
</ElSpace>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Vue3Signature
|
||||||
|
class="mx-auto !h-80 border border-solid border-gray-300"
|
||||||
|
ref="signature"
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SimpleFlowNode } from '#/views/bpm/components/simple-process-design';
|
||||||
|
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { BpmNodeTypeEnum, BpmTaskStatusEnum } from '@vben/constants';
|
||||||
|
|
||||||
|
import { SimpleProcessViewer } from '#/views/bpm/components/simple-process-design';
|
||||||
|
|
||||||
|
defineOptions({ name: 'BpmProcessInstanceSimpleViewer' });
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
loading?: boolean; // 是否加载中
|
||||||
|
modelView?: any;
|
||||||
|
simpleJson?: string; // Simple 模型结构数据 (json 格式)
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
loading: false,
|
||||||
|
modelView: () => ({}),
|
||||||
|
simpleJson: '',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const simpleModel = ref<any>({});
|
||||||
|
const tasks = ref([]); // 用户任务
|
||||||
|
const processInstance = ref(); // 流程实例
|
||||||
|
|
||||||
|
/** 监控模型视图 包括任务列表、进行中的活动节点编号等 */
|
||||||
|
watch(
|
||||||
|
() => props.modelView,
|
||||||
|
async (newModelView) => {
|
||||||
|
if (newModelView) {
|
||||||
|
tasks.value = newModelView.tasks;
|
||||||
|
processInstance.value = newModelView.processInstance;
|
||||||
|
// 已经拒绝的活动节点编号集合,只包括 UserTask
|
||||||
|
const rejectedTaskActivityIds: string[] =
|
||||||
|
newModelView.rejectedTaskActivityIds;
|
||||||
|
// 进行中的活动节点编号集合, 只包括 UserTask
|
||||||
|
const unfinishedTaskActivityIds: string[] =
|
||||||
|
newModelView.unfinishedTaskActivityIds;
|
||||||
|
// 已经完成的活动节点编号集合, 包括 UserTask、Gateway 等
|
||||||
|
const finishedActivityIds: string[] =
|
||||||
|
newModelView.finishedTaskActivityIds;
|
||||||
|
// 已经完成的连线节点编号集合,只包括 SequenceFlow
|
||||||
|
const finishedSequenceFlowActivityIds: string[] =
|
||||||
|
newModelView.finishedSequenceFlowActivityIds;
|
||||||
|
setSimpleModelNodeTaskStatus(
|
||||||
|
newModelView.simpleModel,
|
||||||
|
newModelView.processInstance?.status,
|
||||||
|
rejectedTaskActivityIds,
|
||||||
|
unfinishedTaskActivityIds,
|
||||||
|
finishedActivityIds,
|
||||||
|
finishedSequenceFlowActivityIds,
|
||||||
|
);
|
||||||
|
simpleModel.value = newModelView.simpleModel || {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 监控模型结构数据 */
|
||||||
|
watch(
|
||||||
|
() => props.simpleJson,
|
||||||
|
async (value) => {
|
||||||
|
if (value) {
|
||||||
|
simpleModel.value = JSON.parse(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const setSimpleModelNodeTaskStatus = (
|
||||||
|
simpleModel: SimpleFlowNode | undefined,
|
||||||
|
processStatus: number,
|
||||||
|
rejectedTaskActivityIds: string[],
|
||||||
|
unfinishedTaskActivityIds: string[],
|
||||||
|
finishedActivityIds: string[],
|
||||||
|
finishedSequenceFlowActivityIds: string[],
|
||||||
|
) => {
|
||||||
|
if (!simpleModel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 结束节点
|
||||||
|
if (simpleModel.type === BpmNodeTypeEnum.END_EVENT_NODE) {
|
||||||
|
simpleModel.activityStatus = finishedActivityIds.includes(simpleModel.id)
|
||||||
|
? processStatus
|
||||||
|
: BpmTaskStatusEnum.NOT_START;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 审批节点
|
||||||
|
if (
|
||||||
|
simpleModel.type === BpmNodeTypeEnum.START_USER_NODE ||
|
||||||
|
simpleModel.type === BpmNodeTypeEnum.USER_TASK_NODE ||
|
||||||
|
simpleModel.type === BpmNodeTypeEnum.TRANSACTOR_NODE ||
|
||||||
|
simpleModel.type === BpmNodeTypeEnum.CHILD_PROCESS_NODE
|
||||||
|
) {
|
||||||
|
simpleModel.activityStatus = BpmTaskStatusEnum.NOT_START;
|
||||||
|
if (rejectedTaskActivityIds.includes(simpleModel.id)) {
|
||||||
|
simpleModel.activityStatus = BpmTaskStatusEnum.REJECT;
|
||||||
|
} else if (unfinishedTaskActivityIds.includes(simpleModel.id)) {
|
||||||
|
simpleModel.activityStatus = BpmTaskStatusEnum.RUNNING;
|
||||||
|
} else if (finishedActivityIds.includes(simpleModel.id)) {
|
||||||
|
simpleModel.activityStatus = BpmTaskStatusEnum.APPROVE;
|
||||||
|
}
|
||||||
|
// TODO 是不是还缺一个 cancel 的状态 @jason:
|
||||||
|
}
|
||||||
|
// 抄送节点
|
||||||
|
if (simpleModel.type === BpmNodeTypeEnum.COPY_TASK_NODE) {
|
||||||
|
// 抄送节点,只有通过和未执行状态
|
||||||
|
simpleModel.activityStatus = finishedActivityIds.includes(simpleModel.id)
|
||||||
|
? BpmTaskStatusEnum.APPROVE
|
||||||
|
: BpmTaskStatusEnum.NOT_START;
|
||||||
|
}
|
||||||
|
// 延迟器节点
|
||||||
|
if (simpleModel.type === BpmNodeTypeEnum.DELAY_TIMER_NODE) {
|
||||||
|
// 延迟器节点,只有通过和未执行状态
|
||||||
|
simpleModel.activityStatus = finishedActivityIds.includes(simpleModel.id)
|
||||||
|
? BpmTaskStatusEnum.APPROVE
|
||||||
|
: BpmTaskStatusEnum.NOT_START;
|
||||||
|
}
|
||||||
|
// 触发器节点
|
||||||
|
if (simpleModel.type === BpmNodeTypeEnum.TRIGGER_NODE) {
|
||||||
|
// 触发器节点,只有通过和未执行状态
|
||||||
|
simpleModel.activityStatus = finishedActivityIds.includes(simpleModel.id)
|
||||||
|
? BpmTaskStatusEnum.APPROVE
|
||||||
|
: BpmTaskStatusEnum.NOT_START;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 条件节点对应 SequenceFlow
|
||||||
|
if (simpleModel.type === BpmNodeTypeEnum.CONDITION_NODE) {
|
||||||
|
// 条件节点,只有通过和未执行状态
|
||||||
|
simpleModel.activityStatus = finishedSequenceFlowActivityIds.includes(
|
||||||
|
simpleModel.id,
|
||||||
|
)
|
||||||
|
? BpmTaskStatusEnum.APPROVE
|
||||||
|
: BpmTaskStatusEnum.NOT_START;
|
||||||
|
}
|
||||||
|
// 网关节点
|
||||||
|
if (
|
||||||
|
simpleModel.type === BpmNodeTypeEnum.CONDITION_BRANCH_NODE ||
|
||||||
|
simpleModel.type === BpmNodeTypeEnum.PARALLEL_BRANCH_NODE ||
|
||||||
|
simpleModel.type === BpmNodeTypeEnum.INCLUSIVE_BRANCH_NODE ||
|
||||||
|
simpleModel.type === BpmNodeTypeEnum.ROUTER_BRANCH_NODE
|
||||||
|
) {
|
||||||
|
// 网关节点。只有通过和未执行状态
|
||||||
|
simpleModel.activityStatus = finishedActivityIds.includes(simpleModel.id)
|
||||||
|
? BpmTaskStatusEnum.APPROVE
|
||||||
|
: BpmTaskStatusEnum.NOT_START;
|
||||||
|
simpleModel.conditionNodes?.forEach((node) => {
|
||||||
|
setSimpleModelNodeTaskStatus(
|
||||||
|
node,
|
||||||
|
processStatus,
|
||||||
|
rejectedTaskActivityIds,
|
||||||
|
unfinishedTaskActivityIds,
|
||||||
|
finishedActivityIds,
|
||||||
|
finishedSequenceFlowActivityIds,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setSimpleModelNodeTaskStatus(
|
||||||
|
simpleModel.childNode,
|
||||||
|
processStatus,
|
||||||
|
rejectedTaskActivityIds,
|
||||||
|
unfinishedTaskActivityIds,
|
||||||
|
finishedActivityIds,
|
||||||
|
finishedSequenceFlowActivityIds,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div v-loading="loading">
|
||||||
|
<SimpleProcessViewer
|
||||||
|
:flow-node="simpleModel"
|
||||||
|
:tasks="tasks"
|
||||||
|
:process-instance="processInstance"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { BpmTaskApi } from '#/api/bpm/task';
|
||||||
|
|
||||||
|
import { nextTick, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
import FormCreate from '@form-create/element-ui';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { getTaskListByProcessInstanceId } from '#/api/bpm/task';
|
||||||
|
import { setConfAndFields2 } from '#/components/form-create';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'BpmProcessInstanceTaskList',
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
id: string;
|
||||||
|
loading: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
/** 表单类型定义 */
|
||||||
|
interface TaskForm {
|
||||||
|
rule: any[];
|
||||||
|
option: Record<string, any>;
|
||||||
|
value: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取表格列配置 */
|
||||||
|
function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '审批节点',
|
||||||
|
minWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'approver',
|
||||||
|
title: '审批人',
|
||||||
|
slots: {
|
||||||
|
default: ({ row }: { row: BpmTaskApi.TaskManager }) => {
|
||||||
|
return row.assigneeUser?.nickname || row.ownerUser?.nickname;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '开始时间',
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'endTime',
|
||||||
|
title: '结束时间',
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '审批状态',
|
||||||
|
minWidth: 150,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.BPM_TASK_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'reason',
|
||||||
|
title: '审批建议',
|
||||||
|
slots: {
|
||||||
|
default: 'slot-reason',
|
||||||
|
},
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'durationInMillis',
|
||||||
|
title: '耗时',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatPast2',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const taskForm = ref<TaskForm>({
|
||||||
|
rule: [],
|
||||||
|
option: {},
|
||||||
|
value: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
title: '查看表单',
|
||||||
|
footer: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 显示表单详情 */
|
||||||
|
async function handleShowFormDetail(row: BpmTaskApi.TaskManager) {
|
||||||
|
// 设置表单配置和表单字段
|
||||||
|
taskForm.value = {
|
||||||
|
rule: [],
|
||||||
|
option: {},
|
||||||
|
value: row,
|
||||||
|
};
|
||||||
|
setConfAndFields2(
|
||||||
|
taskForm,
|
||||||
|
row.formConf,
|
||||||
|
row.formFields || [],
|
||||||
|
row.formVariables || {},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 打开弹窗
|
||||||
|
modalApi.open();
|
||||||
|
// 等待表单渲染
|
||||||
|
await nextTick();
|
||||||
|
// 获取表单 API 实例
|
||||||
|
const formApi = formRef.value?.fapi;
|
||||||
|
if (!formApi) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 设置表单不可编辑
|
||||||
|
formApi.btn.show(false);
|
||||||
|
formApi.resetBtn.show(false);
|
||||||
|
formApi.disabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
keepSource: true,
|
||||||
|
showFooter: true,
|
||||||
|
border: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async () => {
|
||||||
|
return await getTaskListByProcessInstanceId(props.id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
pagerConfig: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<BpmTaskApi.TaskManager>,
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
refresh: handleRefresh,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-full">
|
||||||
|
<Grid>
|
||||||
|
<template #slot-reason="{ row }">
|
||||||
|
<div class="flex flex-wrap items-center justify-center">
|
||||||
|
<span v-if="row.reason">{{ row.reason }}</span>
|
||||||
|
<span v-else>-</span>
|
||||||
|
<ElButton
|
||||||
|
v-if="row.formId > 0"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
plain
|
||||||
|
class="ml-1"
|
||||||
|
@click="handleShowFormDetail(row)"
|
||||||
|
>
|
||||||
|
<IconifyIcon icon="lucide:file-text" />
|
||||||
|
<span class="!ml-0.5 text-xs">查看表单</span>
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
<Modal class="w-[800px]">
|
||||||
|
<FormCreate
|
||||||
|
ref="formRef"
|
||||||
|
v-model="taskForm.value"
|
||||||
|
:option="taskForm.option"
|
||||||
|
:rule="taskForm.rule"
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,486 @@
|
|||||||
|
<!-- 审批详情的右侧:审批流 -->
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
import {
|
||||||
|
BpmCandidateStrategyEnum,
|
||||||
|
BpmNodeTypeEnum,
|
||||||
|
BpmTaskStatusEnum,
|
||||||
|
} from '@vben/constants';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
import { formatDateTime, isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ElAvatar,
|
||||||
|
ElButton,
|
||||||
|
ElImage,
|
||||||
|
ElTimeline,
|
||||||
|
ElTimelineItem,
|
||||||
|
ElTooltip,
|
||||||
|
} from 'element-plus';
|
||||||
|
|
||||||
|
import { UserSelectModal } from '#/views/system/user/components';
|
||||||
|
|
||||||
|
defineOptions({ name: 'BpmProcessInstanceTimeline' });
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
activityNodes: BpmProcessInstanceApi.ApprovalNodeInfo[]; // 审批节点信息
|
||||||
|
enableApproveUserSelect?: boolean; // 是否开启审批人自选功能
|
||||||
|
showStatusIcon?: boolean; // 是否显示头像右下角状态图标
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
showStatusIcon: true, // 默认值为 true
|
||||||
|
enableApproveUserSelect: false, // 默认值为 false
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
selectUserConfirm: [activityId: string, userList: any[]];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { push } = useRouter();
|
||||||
|
|
||||||
|
const statusIconMap: Record<
|
||||||
|
string,
|
||||||
|
{ animation?: string; color: string; icon: string }
|
||||||
|
> = {
|
||||||
|
'-2': { color: '#909398', icon: 'mdi:skip-forward-outline' }, // 跳过
|
||||||
|
'-1': { color: '#909398', icon: 'mdi:clock-outline' }, // 审批未开始
|
||||||
|
'0': { color: '#ff943e', icon: 'mdi:loading', animation: 'animate-spin' }, // 待审批
|
||||||
|
'1': { color: '#448ef7', icon: 'mdi:loading', animation: 'animate-spin' }, // 审批中
|
||||||
|
'2': { color: '#00b32a', icon: 'mdi:check' }, // 审批通过
|
||||||
|
'3': { color: '#f46b6c', icon: 'mdi:close' }, // 审批不通过
|
||||||
|
'4': { color: '#cccccc', icon: 'mdi:trash-can-outline' }, // 已取消
|
||||||
|
'5': { color: '#f46b6c', icon: 'mdi:arrow-left' }, // 退回
|
||||||
|
'6': { color: '#448ef7', icon: 'mdi:clock-outline' }, // 委派中
|
||||||
|
'7': { color: '#00b32a', icon: 'mdi:check' }, // 审批通过中
|
||||||
|
}; // 状态图标映射
|
||||||
|
const nodeTypeSvgMap = {
|
||||||
|
// 结束节点
|
||||||
|
[BpmNodeTypeEnum.END_EVENT_NODE]: {
|
||||||
|
color: '#909398',
|
||||||
|
icon: 'mdi:power',
|
||||||
|
},
|
||||||
|
// 开始节点
|
||||||
|
[BpmNodeTypeEnum.START_USER_NODE]: {
|
||||||
|
color: '#909398',
|
||||||
|
icon: 'mdi:account-outline',
|
||||||
|
},
|
||||||
|
// 用户任务节点
|
||||||
|
[BpmNodeTypeEnum.USER_TASK_NODE]: {
|
||||||
|
color: '#ff943e',
|
||||||
|
icon: 'tdesign:seal',
|
||||||
|
},
|
||||||
|
// 事务节点
|
||||||
|
[BpmNodeTypeEnum.TRANSACTOR_NODE]: {
|
||||||
|
color: '#ff943e',
|
||||||
|
icon: 'mdi:file-edit-outline',
|
||||||
|
},
|
||||||
|
// 复制任务节点
|
||||||
|
[BpmNodeTypeEnum.COPY_TASK_NODE]: {
|
||||||
|
color: '#3296fb',
|
||||||
|
icon: 'mdi:content-copy',
|
||||||
|
},
|
||||||
|
// 条件分支节点
|
||||||
|
[BpmNodeTypeEnum.CONDITION_NODE]: {
|
||||||
|
color: '#14bb83',
|
||||||
|
icon: 'carbon:flow',
|
||||||
|
},
|
||||||
|
// 并行分支节点
|
||||||
|
[BpmNodeTypeEnum.PARALLEL_BRANCH_NODE]: {
|
||||||
|
color: '#14bb83',
|
||||||
|
icon: 'si:flow-parallel-line',
|
||||||
|
},
|
||||||
|
// 子流程节点
|
||||||
|
[BpmNodeTypeEnum.CHILD_PROCESS_NODE]: {
|
||||||
|
color: '#14bb83',
|
||||||
|
icon: 'icon-park-outline:tree-diagram',
|
||||||
|
},
|
||||||
|
} as Record<BpmNodeTypeEnum, { color: string; icon: string }>; // 节点类型图标映射
|
||||||
|
const onlyStatusIconShow = [-1, 0, 1]; // 只有状态是 -1、0、1 才展示头像右小角状态小 icon
|
||||||
|
|
||||||
|
/** 获取审批节点类型图标 */
|
||||||
|
function getApprovalNodeTypeIcon(nodeType: BpmNodeTypeEnum) {
|
||||||
|
return nodeTypeSvgMap[nodeType]?.icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取审批节点图标 */
|
||||||
|
function getApprovalNodeIcon(taskStatus: number, nodeType: BpmNodeTypeEnum) {
|
||||||
|
if (taskStatus === BpmTaskStatusEnum.NOT_START) {
|
||||||
|
return statusIconMap[taskStatus]?.icon || 'mdi:clock-outline';
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
BpmNodeTypeEnum.CHILD_PROCESS_NODE,
|
||||||
|
BpmNodeTypeEnum.END_EVENT_NODE,
|
||||||
|
BpmNodeTypeEnum.START_USER_NODE,
|
||||||
|
BpmNodeTypeEnum.TRANSACTOR_NODE,
|
||||||
|
BpmNodeTypeEnum.USER_TASK_NODE,
|
||||||
|
].includes(nodeType)
|
||||||
|
) {
|
||||||
|
return statusIconMap[taskStatus]?.icon || 'mdi:clock-outline';
|
||||||
|
}
|
||||||
|
return 'mdi:clock-outline';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取审批节点颜色 */
|
||||||
|
function getApprovalNodeColor(taskStatus: number) {
|
||||||
|
return statusIconMap[taskStatus]?.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取审批节点时间 */
|
||||||
|
function getApprovalNodeTime(node: BpmProcessInstanceApi.ApprovalNodeInfo) {
|
||||||
|
if (node.nodeType === BpmNodeTypeEnum.START_USER_NODE && node.startTime) {
|
||||||
|
return formatDateTime(node.startTime);
|
||||||
|
}
|
||||||
|
if (node.endTime) {
|
||||||
|
return formatDateTime(node.endTime);
|
||||||
|
}
|
||||||
|
if (node.startTime) {
|
||||||
|
return formatDateTime(node.startTime);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const [UserSelectModalComp, userSelectModalApi] = useVbenModal({
|
||||||
|
connectedComponent: UserSelectModal,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
const selectedActivityNodeId = ref<string>();
|
||||||
|
const customApproveUsers = ref<Record<string, any[]>>({}); // key:activityId,value:用户列表
|
||||||
|
|
||||||
|
/** 打开选择用户弹窗 */
|
||||||
|
const handleSelectUser = (activityId: string, selectedList: any[]) => {
|
||||||
|
selectedActivityNodeId.value = activityId;
|
||||||
|
userSelectModalApi
|
||||||
|
.setData({ userIds: selectedList.map((item) => item.id) })
|
||||||
|
.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 选择用户完成 */
|
||||||
|
const selectedUsers = ref<number[]>([]);
|
||||||
|
function handleUserSelectConfirm(userList: any[]) {
|
||||||
|
if (!selectedActivityNodeId.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
customApproveUsers.value[selectedActivityNodeId.value] = userList || [];
|
||||||
|
|
||||||
|
emit('selectUserConfirm', selectedActivityNodeId.value, userList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 跳转子流程 */
|
||||||
|
function handleChildProcess(activity: any) {
|
||||||
|
if (!activity.processInstanceId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
push({
|
||||||
|
name: 'BpmProcessInstanceDetail',
|
||||||
|
query: {
|
||||||
|
id: activity.processInstanceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断是否需要显示自定义选择审批人 */
|
||||||
|
function shouldShowCustomUserSelect(
|
||||||
|
activity: BpmProcessInstanceApi.ApprovalNodeInfo,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
isEmpty(activity.tasks) &&
|
||||||
|
((BpmCandidateStrategyEnum.START_USER_SELECT ===
|
||||||
|
activity.candidateStrategy &&
|
||||||
|
isEmpty(activity.candidateUsers)) ||
|
||||||
|
(props.enableApproveUserSelect &&
|
||||||
|
BpmCandidateStrategyEnum.APPROVE_USER_SELECT ===
|
||||||
|
activity.candidateStrategy))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断是否需要显示审批意见 */
|
||||||
|
function shouldShowApprovalReason(task: any, nodeType: BpmNodeTypeEnum) {
|
||||||
|
return (
|
||||||
|
task.reason &&
|
||||||
|
[BpmNodeTypeEnum.END_EVENT_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes(
|
||||||
|
nodeType,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用户选择弹窗关闭 */
|
||||||
|
function handleUserSelectClosed() {
|
||||||
|
selectedUsers.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用户选择弹窗取消 */
|
||||||
|
function handleUserSelectCancel() {
|
||||||
|
selectedUsers.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 设置自定义审批人 */
|
||||||
|
const setCustomApproveUsers = (activityId: string, users: any[]) => {
|
||||||
|
customApproveUsers.value[activityId] = users || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 批量设置多个节点的自定义审批人 */
|
||||||
|
const batchSetCustomApproveUsers = (data: Record<string, any[]>) => {
|
||||||
|
Object.keys(data).forEach((activityId) => {
|
||||||
|
customApproveUsers.value[activityId] = data[activityId] || [];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ setCustomApproveUsers, batchSetCustomApproveUsers });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ElTimeline class="pt-5">
|
||||||
|
<!-- 遍历每个审批节点 -->
|
||||||
|
<ElTimelineItem
|
||||||
|
v-for="(activity, index) in activityNodes"
|
||||||
|
:key="index"
|
||||||
|
:color="getApprovalNodeColor(activity.status)"
|
||||||
|
>
|
||||||
|
<template #dot>
|
||||||
|
<div class="relative">
|
||||||
|
<div
|
||||||
|
class="absolute -left-2.5 -top-1.5 flex h-8 w-8 items-center justify-center rounded-full border border-solid border-gray-200 bg-blue-500 p-1.5"
|
||||||
|
>
|
||||||
|
<IconifyIcon
|
||||||
|
:icon="getApprovalNodeTypeIcon(activity.nodeType)"
|
||||||
|
class="size-6 text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="showStatusIcon"
|
||||||
|
class="absolute left-1.5 top-2.5 flex size-4 items-center rounded-full border-2 border-solid border-white p-0.5"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: getApprovalNodeColor(activity.status),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<IconifyIcon
|
||||||
|
:icon="getApprovalNodeIcon(activity.status, activity.nodeType)"
|
||||||
|
class="text-white"
|
||||||
|
:class="[statusIconMap[activity.status]?.animation]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="ml-2 flex flex-col items-start gap-2"
|
||||||
|
:id="`activity-task-${activity.id}-${index}`"
|
||||||
|
>
|
||||||
|
<!-- 第一行:节点名称、时间 -->
|
||||||
|
<div class="flex w-full items-center">
|
||||||
|
<div class="font-bold">
|
||||||
|
{{ activity.name }}
|
||||||
|
<span v-if="activity.status === BpmTaskStatusEnum.SKIP">
|
||||||
|
【跳过】
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- 信息:时间 -->
|
||||||
|
<div
|
||||||
|
v-if="activity.status !== BpmTaskStatusEnum.NOT_START"
|
||||||
|
class="ml-auto text-sm text-gray-500"
|
||||||
|
>
|
||||||
|
{{ getApprovalNodeTime(activity) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 子流程节点 -->
|
||||||
|
<div v-if="activity.nodeType === BpmNodeTypeEnum.CHILD_PROCESS_NODE">
|
||||||
|
<ElButton
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
@click="handleChildProcess(activity)"
|
||||||
|
:disabled="!activity.processInstanceId"
|
||||||
|
>
|
||||||
|
查看子流程
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 需要自定义选择审批人 -->
|
||||||
|
<div
|
||||||
|
v-if="shouldShowCustomUserSelect(activity)"
|
||||||
|
class="flex flex-wrap items-center gap-2"
|
||||||
|
>
|
||||||
|
<ElTooltip content="添加用户" placement="left">
|
||||||
|
<ElButton
|
||||||
|
type="primary"
|
||||||
|
size="default"
|
||||||
|
plain
|
||||||
|
class="flex items-center justify-center"
|
||||||
|
@click="
|
||||||
|
handleSelectUser(
|
||||||
|
activity.id,
|
||||||
|
customApproveUsers[activity.id] ?? [],
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<IconifyIcon icon="lucide:user-plus" class="size-4" />
|
||||||
|
</ElButton>
|
||||||
|
</ElTooltip>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="(user, userIndex) in customApproveUsers[activity.id]"
|
||||||
|
:key="user.id || userIndex"
|
||||||
|
class="relative flex h-9 items-center gap-2 rounded-3xl bg-gray-100 pr-2 dark:bg-gray-600"
|
||||||
|
>
|
||||||
|
<ElAvatar
|
||||||
|
class="!m-1"
|
||||||
|
:size="28"
|
||||||
|
v-if="user.avatar"
|
||||||
|
:src="user.avatar"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ElAvatar class="!m-1" :size="28" v-else>
|
||||||
|
<span>{{ user.nickname.substring(0, 1) }}</span>
|
||||||
|
</ElAvatar>
|
||||||
|
<span class="text-sm">{{ user.nickname }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="mt-1 flex flex-wrap items-center gap-2">
|
||||||
|
<!-- 情况一:遍历每个审批节点下的【进行中】task 任务 -->
|
||||||
|
<div
|
||||||
|
v-for="(task, idx) in activity.tasks"
|
||||||
|
:key="idx"
|
||||||
|
class="flex flex-col gap-2 pr-2"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="relative flex flex-wrap gap-2"
|
||||||
|
v-if="task.assigneeUser || task.ownerUser"
|
||||||
|
>
|
||||||
|
<!-- 信息:头像昵称 -->
|
||||||
|
<div class="relative flex h-8 items-center rounded-3xl pr-2">
|
||||||
|
<template
|
||||||
|
v-if="
|
||||||
|
task.assigneeUser?.avatar || task.assigneeUser?.nickname
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<ElAvatar
|
||||||
|
class="!m-1"
|
||||||
|
:size="28"
|
||||||
|
v-if="task.assigneeUser?.avatar"
|
||||||
|
:src="task.assigneeUser?.avatar"
|
||||||
|
/>
|
||||||
|
<ElAvatar class="!m-1" :size="28" v-else>
|
||||||
|
{{ task.assigneeUser?.nickname.substring(0, 1) }}
|
||||||
|
</ElAvatar>
|
||||||
|
{{ task.assigneeUser?.nickname }}
|
||||||
|
</template>
|
||||||
|
<template
|
||||||
|
v-else-if="
|
||||||
|
task.ownerUser?.avatar || task.ownerUser?.nickname
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<ElAvatar
|
||||||
|
class="!m-1"
|
||||||
|
:size="28"
|
||||||
|
v-if="task.ownerUser?.avatar"
|
||||||
|
:src="task.ownerUser?.avatar"
|
||||||
|
/>
|
||||||
|
<ElAvatar class="!m-1" :size="28" v-else>
|
||||||
|
{{ task.ownerUser?.nickname.substring(0, 1) }}
|
||||||
|
</ElAvatar>
|
||||||
|
{{ task.ownerUser?.nickname }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 信息:任务状态图标 -->
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
showStatusIcon && onlyStatusIconShow.includes(task.status)
|
||||||
|
"
|
||||||
|
class="absolute left-5 top-5 flex items-center rounded-full border-2 border-solid border-white p-1"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: statusIconMap[task.status]?.color,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<IconifyIcon
|
||||||
|
:icon="statusIconMap[task.status]?.icon || 'lucide:clock'"
|
||||||
|
class="size-1.5 text-white"
|
||||||
|
:class="[statusIconMap[task.status]?.animation]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 审批意见和签名 -->
|
||||||
|
<teleport defer :to="`#activity-task-${activity.id}-${index}`">
|
||||||
|
<div
|
||||||
|
v-if="shouldShowApprovalReason(task, activity.nodeType)"
|
||||||
|
class="mt-1 w-full rounded-md bg-gray-100 p-2 text-sm text-gray-500"
|
||||||
|
>
|
||||||
|
审批意见:{{ task.reason }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
task.signPicUrl &&
|
||||||
|
activity.nodeType === BpmNodeTypeEnum.USER_TASK_NODE
|
||||||
|
"
|
||||||
|
class="mt-1 w-full rounded-md bg-gray-100 p-2 text-sm text-gray-500"
|
||||||
|
>
|
||||||
|
签名:
|
||||||
|
<ElImage
|
||||||
|
class="ml-1 h-10 w-24"
|
||||||
|
:src="task.signPicUrl"
|
||||||
|
:preview-src-list="[task.signPicUrl]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</teleport>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 情况二:遍历每个审批节点下的【候选的】task 任务 -->
|
||||||
|
<div
|
||||||
|
v-for="(user, userIndex) in activity.candidateUsers"
|
||||||
|
:key="userIndex"
|
||||||
|
class="relative flex h-8 items-center rounded-3xl pr-2"
|
||||||
|
>
|
||||||
|
<ElAvatar
|
||||||
|
class="!m-1"
|
||||||
|
:size="28"
|
||||||
|
v-if="user.avatar"
|
||||||
|
:src="user.avatar"
|
||||||
|
/>
|
||||||
|
<ElAvatar class="!m-1" :size="28" v-else>
|
||||||
|
{{ user.nickname.substring(0, 1) }}
|
||||||
|
</ElAvatar>
|
||||||
|
<span class="text-sm">
|
||||||
|
{{ user.nickname }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- 候选任务状态图标 -->
|
||||||
|
<div
|
||||||
|
v-if="showStatusIcon"
|
||||||
|
class="absolute left-6 top-5 flex items-center rounded-full border-2 border-solid border-white p-1"
|
||||||
|
:style="{ backgroundColor: statusIconMap['-1']?.color }"
|
||||||
|
>
|
||||||
|
<IconifyIcon
|
||||||
|
class="text-xs text-white"
|
||||||
|
:icon="statusIconMap['-1']?.icon || 'lucide:clock'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElTimelineItem>
|
||||||
|
</ElTimeline>
|
||||||
|
|
||||||
|
<!-- 用户选择弹窗 -->
|
||||||
|
<UserSelectModalComp
|
||||||
|
class="w-3/5"
|
||||||
|
v-model="selectedUsers"
|
||||||
|
:multiple="true"
|
||||||
|
title="选择用户"
|
||||||
|
@confirm="handleUserSelectConfirm"
|
||||||
|
@closed="handleUserSelectClosed"
|
||||||
|
@cancel="handleUserSelectCancel"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -938,6 +938,12 @@ importers:
|
|||||||
vue-router:
|
vue-router:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 4.6.3(vue@3.5.24(typescript@5.9.3))
|
version: 4.6.3(vue@3.5.24(typescript@5.9.3))
|
||||||
|
vue3-print-nb:
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 0.1.4(typescript@5.9.3)
|
||||||
|
vue3-signature:
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 0.2.4(vue@3.5.24(typescript@5.9.3))
|
||||||
vuedraggable:
|
vuedraggable:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 4.1.0(vue@3.5.24(typescript@5.9.3))
|
version: 4.1.0(vue@3.5.24(typescript@5.9.3))
|
||||||
|
|||||||
Reference in New Issue
Block a user