From 088e3c62aeeb79f045e42914c39f667121388623 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Thu, 2 Jul 2026 19:47:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(bpm):=20=E5=B1=95=E7=A4=BA=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E4=BB=BB=E5=8A=A1=E9=99=84=E4=BB=B6=E5=92=8C=E7=AD=BE?= =?UTF-8?q?=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 统一在流程 timeline、流转记录、任务管理中展示附件和签名 - 办理节点展示办理意见,审批节点展示审批意见 - 新增任务附件/签名展示组件,复用文件名解析和图片判断逻辑 - 接入流程评论列表并补齐评论 API 与字典类型 - 清理冗余 helper、临时常量和 form-create ref 类型写法 --- apps/web-antd/src/api/bpm/task/index.ts | 2 + .../detail/modules/operation-button.vue | 5 +- .../detail/modules/task-evidence.vue | 116 ++++++++++++++++++ .../detail/modules/task-list.vue | 17 +++ .../detail/modules/time-line.vue | 109 ++++------------ .../src/views/bpm/task/manager/data.ts | 6 + .../src/views/bpm/task/manager/index.vue | 8 ++ apps/web-antdv-next/src/api/bpm/task/index.ts | 2 + .../detail/modules/operation-button.vue | 5 +- .../detail/modules/task-evidence.vue | 116 ++++++++++++++++++ .../detail/modules/task-list.vue | 17 +++ .../detail/modules/time-line.vue | 108 ++++------------ .../src/views/bpm/task/manager/data.ts | 6 + .../src/views/bpm/task/manager/index.vue | 8 ++ apps/web-ele/src/api/bpm/task/index.ts | 2 + .../detail/modules/operation-button.vue | 5 +- .../detail/modules/task-evidence.vue | 116 ++++++++++++++++++ .../detail/modules/task-list.vue | 17 +++ .../detail/modules/time-line.vue | 106 ++++------------ .../src/views/bpm/task/manager/data.ts | 6 + .../src/views/bpm/task/manager/index.vue | 8 ++ .../@core/base/shared/src/utils/upload.ts | 3 +- 22 files changed, 525 insertions(+), 263 deletions(-) create mode 100644 apps/web-antd/src/views/bpm/processInstance/detail/modules/task-evidence.vue create mode 100644 apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/task-evidence.vue create mode 100644 apps/web-ele/src/views/bpm/processInstance/detail/modules/task-evidence.vue diff --git a/apps/web-antd/src/api/bpm/task/index.ts b/apps/web-antd/src/api/bpm/task/index.ts index b396594f7..9d4f83aa7 100644 --- a/apps/web-antd/src/api/bpm/task/index.ts +++ b/apps/web-antd/src/api/bpm/task/index.ts @@ -14,6 +14,8 @@ export namespace BpmTaskApi { endTime: number; // 结束时间 durationInMillis: number; // 持续时间 reason: string; // 审批理由 + attachments?: string[]; // 审批附件 + signPicUrl?: string; // 签名图片 ownerUser: any; // 负责人 assigneeUser: any; // 处理人 taskDefinitionKey: string; // 任务定义的标识 diff --git a/apps/web-antd/src/views/bpm/processInstance/detail/modules/operation-button.vue b/apps/web-antd/src/views/bpm/processInstance/detail/modules/operation-button.vue index 99595240a..743252004 100644 --- a/apps/web-antd/src/views/bpm/processInstance/detail/modules/operation-button.vue +++ b/apps/web-antd/src/views/bpm/processInstance/detail/modules/operation-button.vue @@ -40,11 +40,11 @@ import { Textarea, } from 'ant-design-vue'; +import { createComment } from '#/api/bpm/comment'; import { cancelProcessInstanceByStartUser, getNextApprovalNodes, } from '#/api/bpm/processInstance'; -import { createComment } from '#/api/bpm/comment'; import { approveTask, copyTask, @@ -114,7 +114,6 @@ const APPROVAL_ATTACHMENT_FILE_TYPES = [ 'webp', ]; const APPROVAL_ATTACHMENT_FILE_SIZE = 5; -const APPROVAL_ATTACHMENT_DIRECTORY = 'bpm/task-attachment'; /** 创建流程表达式 */ function openSignatureModal() { @@ -951,7 +950,6 @@ defineExpose({ loadTodoTask }); +import { computed } from 'vue'; + +import { IconifyIcon } from '@vben/icons'; +import { getFileNameFromUrl, isImage } from '@vben/utils'; + +defineOptions({ name: 'BpmTaskEvidence' }); + +const props = withDefaults( + defineProps<{ + attachments?: null | string | string[]; + compact?: boolean; + emptyText?: string; + reason?: string; + reasonLabel?: string; + signPicUrl?: null | string; + }>(), + { + attachments: () => [], + emptyText: '-', + reason: '', + reasonLabel: '审批意见', + signPicUrl: '', + }, +); + +const reasonText = computed(() => props.reason?.trim() || ''); +const attachmentList = computed(() => normalizeAttachments(props.attachments)); // 附件列表 +const signPicUrlValue = computed(() => props.signPicUrl?.trim() || ''); // 签名图片地址 +const hasEvidence = computed( + () => + !!reasonText.value || + attachmentList.value.length > 0 || + !!signPicUrlValue.value, +); // 是否存在审批凭证 + +/** 标准化附件列表 */ +function normalizeAttachments(attachments?: null | string | string[]) { + if (!attachments) { + return []; + } + const list = Array.isArray(attachments) + ? attachments + : String(attachments).split(','); + return list.map((item) => item.trim()).filter(Boolean); +} + +/** 判断是否图片附件 */ +function isImageAttachment(url: string) { + return isImage(getFileNameFromUrl(url)); +} + + + diff --git a/apps/web-antd/src/views/bpm/processInstance/detail/modules/task-list.vue b/apps/web-antd/src/views/bpm/processInstance/detail/modules/task-list.vue index 0565fcb9b..1ca5a8b94 100644 --- a/apps/web-antd/src/views/bpm/processInstance/detail/modules/task-list.vue +++ b/apps/web-antd/src/views/bpm/processInstance/detail/modules/task-list.vue @@ -16,6 +16,8 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { getTaskListByProcessInstanceId } from '#/api/bpm/task'; import { setConfAndFields2 } from '#/components/form-create'; +import TaskEvidence from './task-evidence.vue'; + defineOptions({ name: 'BpmProcessInstanceTaskList', }); @@ -79,6 +81,14 @@ function useGridColumns(): VxeTableGridOptions['columns'] { }, minWidth: 200, }, + { + field: 'evidence', + title: '附件/签名', + slots: { + default: 'slot-evidence', + }, + minWidth: 220, + }, { field: 'durationInMillis', title: '耗时', @@ -186,6 +196,13 @@ defineExpose({ + diff --git a/apps/web-antd/src/views/bpm/processInstance/detail/modules/time-line.vue b/apps/web-antd/src/views/bpm/processInstance/detail/modules/time-line.vue index 2ad3faeb8..2a639d1be 100644 --- a/apps/web-antd/src/views/bpm/processInstance/detail/modules/time-line.vue +++ b/apps/web-antd/src/views/bpm/processInstance/detail/modules/time-line.vue @@ -14,10 +14,12 @@ import { import { IconifyIcon } from '@vben/icons'; import { formatDateTime, isEmpty } from '@vben/utils'; -import { Avatar, Button, Image, Timeline, Tooltip } from 'ant-design-vue'; +import { Avatar, Button, Timeline, Tooltip } from 'ant-design-vue'; import { UserSelectModal } from '#/views/system/user/components'; +import TaskEvidence from './task-evidence.vue'; + defineOptions({ name: 'BpmProcessInstanceTimeline' }); const props = withDefaults( @@ -196,7 +198,7 @@ function shouldShowCustomUserSelect( ); } -/** 判断是否需要显示审批意见和附件 */ +/** 判断是否需要显示审批凭证 */ function shouldShowReasonAndAttachment( task: any, nodeType: BpmNodeTypeEnum, @@ -207,26 +209,25 @@ function shouldShowReasonAndAttachment( return false; } return ( - Boolean(task.reason || task.attachments?.length > 0) && - [BpmNodeTypeEnum.START_USER_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes( - nodeType, - ) + hasTaskEvidence(task) && + [ + BpmNodeTypeEnum.START_USER_NODE, + BpmNodeTypeEnum.TRANSACTOR_NODE, + BpmNodeTypeEnum.USER_TASK_NODE, + ].includes(nodeType) ); } -function getAttachmentName(url: string) { - const cleanUrl = url.split(/[?#]/)[0] || ''; - const fileName = cleanUrl.slice(cleanUrl.lastIndexOf('/') + 1); - try { - return decodeURIComponent(fileName); - } catch { - return fileName; - } +/** 判断是否存在审批凭证 */ +function hasTaskEvidence(task: any) { + return Boolean( + task?.reason || task?.attachments?.length > 0 || task?.signPicUrl, + ); } -function isImageAttachment(url: string) { - const ext = url.split(/[?#]/)[0]?.split('.').pop()?.toLowerCase(); - return ['bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp'].includes(ext || ''); +/** 获取意见文案 */ +function getReasonLabel(nodeType: BpmNodeTypeEnum) { + return nodeType === BpmNodeTypeEnum.TRANSACTOR_NODE ? '办理意见' : '审批意见'; } /** 用户选择弹窗关闭 */ @@ -428,76 +429,16 @@ defineExpose({ setCustomApproveUsers, batchSetCustomApproveUsers }); - -
+ -
审批意见:{{ task.reason }}
-
-
- 附件列表: -
-
- -
-
-
-
- 签名: - -
+ :attachments="task.attachments" + :reason="task.reason" + :reason-label="getReasonLabel(activity.nodeType)" + :sign-pic-url="task.signPicUrl" + /> diff --git a/apps/web-antd/src/views/bpm/task/manager/data.ts b/apps/web-antd/src/views/bpm/task/manager/data.ts index 947982b23..bef7b26c4 100644 --- a/apps/web-antd/src/views/bpm/task/manager/data.ts +++ b/apps/web-antd/src/views/bpm/task/manager/data.ts @@ -78,6 +78,12 @@ export function useGridColumns(): VxeTableGridOptions['columns'] { title: '审批建议', minWidth: 180, }, + { + field: 'evidence', + title: '附件/签名', + minWidth: 220, + slots: { default: 'evidence' }, + }, { field: 'durationInMillis', title: '耗时', diff --git a/apps/web-antd/src/views/bpm/task/manager/index.vue b/apps/web-antd/src/views/bpm/task/manager/index.vue index 829570b9a..77b939648 100644 --- a/apps/web-antd/src/views/bpm/task/manager/index.vue +++ b/apps/web-antd/src/views/bpm/task/manager/index.vue @@ -7,6 +7,7 @@ import { DocAlert, Page } from '@vben/common-ui'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { getTaskManagerPage } from '#/api/bpm/task'; import { router } from '#/router'; +import TaskEvidence from '#/views/bpm/processInstance/detail/modules/task-evidence.vue'; import { useGridColumns, useGridFormSchema } from './data'; @@ -60,6 +61,13 @@ const [Grid] = useVbenVxeGrid({ + + diff --git a/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/time-line.vue b/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/time-line.vue index df3bedf13..85d4144cb 100644 --- a/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/time-line.vue +++ b/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/time-line.vue @@ -17,7 +17,6 @@ import { formatDateTime, isEmpty } from '@vben/utils'; import { Avatar, Button, - Image, Timeline, TimelineItem, Tooltip, @@ -25,6 +24,8 @@ import { import { UserSelectModal } from '#/views/system/user/components'; +import TaskEvidence from './task-evidence.vue'; + defineOptions({ name: 'BpmProcessInstanceTimeline' }); const props = withDefaults( @@ -203,7 +204,7 @@ function shouldShowCustomUserSelect( ); } -/** 判断是否需要显示审批意见和附件 */ +/** 判断是否需要显示审批凭证 */ function shouldShowReasonAndAttachment( task: any, nodeType: BpmNodeTypeEnum, @@ -214,26 +215,25 @@ function shouldShowReasonAndAttachment( return false; } return ( - Boolean(task.reason || task.attachments?.length > 0) && - [BpmNodeTypeEnum.START_USER_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes( - nodeType, - ) + hasTaskEvidence(task) && + [ + BpmNodeTypeEnum.START_USER_NODE, + BpmNodeTypeEnum.TRANSACTOR_NODE, + BpmNodeTypeEnum.USER_TASK_NODE, + ].includes(nodeType) ); } -function getAttachmentName(url: string) { - const cleanUrl = url.split(/[?#]/)[0] || ''; - const fileName = cleanUrl.slice(cleanUrl.lastIndexOf('/') + 1); - try { - return decodeURIComponent(fileName); - } catch { - return fileName; - } +/** 判断是否存在审批凭证 */ +function hasTaskEvidence(task: any) { + return Boolean( + task?.reason || task?.attachments?.length > 0 || task?.signPicUrl, + ); } -function isImageAttachment(url: string) { - const ext = url.split(/[?#]/)[0]?.split('.').pop()?.toLowerCase(); - return ['bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp'].includes(ext || ''); +/** 获取意见文案 */ +function getReasonLabel(nodeType: BpmNodeTypeEnum) { + return nodeType === BpmNodeTypeEnum.TRANSACTOR_NODE ? '办理意见' : '审批意见'; } /** 用户选择弹窗关闭 */ @@ -435,76 +435,16 @@ defineExpose({ setCustomApproveUsers, batchSetCustomApproveUsers }); - -
+ -
审批意见:{{ task.reason }}
-
-
- 附件列表: -
-
- -
-
-
-
- 签名: - -
+ :attachments="task.attachments" + :reason="task.reason" + :reason-label="getReasonLabel(activity.nodeType)" + :sign-pic-url="task.signPicUrl" + /> diff --git a/apps/web-antdv-next/src/views/bpm/task/manager/data.ts b/apps/web-antdv-next/src/views/bpm/task/manager/data.ts index 947982b23..bef7b26c4 100644 --- a/apps/web-antdv-next/src/views/bpm/task/manager/data.ts +++ b/apps/web-antdv-next/src/views/bpm/task/manager/data.ts @@ -78,6 +78,12 @@ export function useGridColumns(): VxeTableGridOptions['columns'] { title: '审批建议', minWidth: 180, }, + { + field: 'evidence', + title: '附件/签名', + minWidth: 220, + slots: { default: 'evidence' }, + }, { field: 'durationInMillis', title: '耗时', diff --git a/apps/web-antdv-next/src/views/bpm/task/manager/index.vue b/apps/web-antdv-next/src/views/bpm/task/manager/index.vue index 829570b9a..77b939648 100644 --- a/apps/web-antdv-next/src/views/bpm/task/manager/index.vue +++ b/apps/web-antdv-next/src/views/bpm/task/manager/index.vue @@ -7,6 +7,7 @@ import { DocAlert, Page } from '@vben/common-ui'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { getTaskManagerPage } from '#/api/bpm/task'; import { router } from '#/router'; +import TaskEvidence from '#/views/bpm/processInstance/detail/modules/task-evidence.vue'; import { useGridColumns, useGridFormSchema } from './data'; @@ -60,6 +61,13 @@ const [Grid] = useVbenVxeGrid({ + + diff --git a/apps/web-ele/src/views/bpm/processInstance/detail/modules/time-line.vue b/apps/web-ele/src/views/bpm/processInstance/detail/modules/time-line.vue index 86512f654..1b257a1de 100644 --- a/apps/web-ele/src/views/bpm/processInstance/detail/modules/time-line.vue +++ b/apps/web-ele/src/views/bpm/processInstance/detail/modules/time-line.vue @@ -17,7 +17,6 @@ import { formatDateTime, isEmpty } from '@vben/utils'; import { ElAvatar, ElButton, - ElImage, ElTimeline, ElTimelineItem, ElTooltip, @@ -25,6 +24,8 @@ import { import { UserSelectModal } from '#/views/system/user/components'; +import TaskEvidence from './task-evidence.vue'; + defineOptions({ name: 'BpmProcessInstanceTimeline' }); const props = withDefaults( @@ -203,7 +204,7 @@ function shouldShowCustomUserSelect( ); } -/** 判断是否需要显示审批意见和附件 */ +/** 判断是否需要显示审批凭证 */ function shouldShowReasonAndAttachment( task: any, nodeType: BpmNodeTypeEnum, @@ -214,26 +215,25 @@ function shouldShowReasonAndAttachment( return false; } return ( - Boolean(task.reason || task.attachments?.length > 0) && - [BpmNodeTypeEnum.START_USER_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes( - nodeType, - ) + hasTaskEvidence(task) && + [ + BpmNodeTypeEnum.START_USER_NODE, + BpmNodeTypeEnum.TRANSACTOR_NODE, + BpmNodeTypeEnum.USER_TASK_NODE, + ].includes(nodeType) ); } -function getAttachmentName(url: string) { - const cleanUrl = url.split(/[?#]/)[0] || ''; - const fileName = cleanUrl.slice(cleanUrl.lastIndexOf('/') + 1); - try { - return decodeURIComponent(fileName); - } catch { - return fileName; - } +/** 判断是否存在审批凭证 */ +function hasTaskEvidence(task: any) { + return Boolean( + task?.reason || task?.attachments?.length > 0 || task?.signPicUrl, + ); } -function isImageAttachment(url: string) { - const ext = url.split(/[?#]/)[0]?.split('.').pop()?.toLowerCase(); - return ['bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp'].includes(ext || ''); +/** 获取意见文案 */ +function getReasonLabel(nodeType: BpmNodeTypeEnum) { + return nodeType === BpmNodeTypeEnum.TRANSACTOR_NODE ? '办理意见' : '审批意见'; } /** 用户选择弹窗关闭 */ @@ -433,74 +433,16 @@ defineExpose({ setCustomApproveUsers, batchSetCustomApproveUsers }); - -
+ -
审批意见:{{ task.reason }}
-
-
- 附件列表: -
-
- -
-
-
-
- 签名: - -
+ :attachments="task.attachments" + :reason="task.reason" + :reason-label="getReasonLabel(activity.nodeType)" + :sign-pic-url="task.signPicUrl" + /> diff --git a/apps/web-ele/src/views/bpm/task/manager/data.ts b/apps/web-ele/src/views/bpm/task/manager/data.ts index 947982b23..bef7b26c4 100644 --- a/apps/web-ele/src/views/bpm/task/manager/data.ts +++ b/apps/web-ele/src/views/bpm/task/manager/data.ts @@ -78,6 +78,12 @@ export function useGridColumns(): VxeTableGridOptions['columns'] { title: '审批建议', minWidth: 180, }, + { + field: 'evidence', + title: '附件/签名', + minWidth: 220, + slots: { default: 'evidence' }, + }, { field: 'durationInMillis', title: '耗时', diff --git a/apps/web-ele/src/views/bpm/task/manager/index.vue b/apps/web-ele/src/views/bpm/task/manager/index.vue index 2c320a0e7..146506aee 100644 --- a/apps/web-ele/src/views/bpm/task/manager/index.vue +++ b/apps/web-ele/src/views/bpm/task/manager/index.vue @@ -7,6 +7,7 @@ import { DocAlert, Page } from '@vben/common-ui'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { getTaskManagerPage } from '#/api/bpm/task'; import { router } from '#/router'; +import TaskEvidence from '#/views/bpm/processInstance/detail/modules/task-evidence.vue'; import { useGridColumns, useGridFormSchema } from './data'; @@ -60,6 +61,13 @@ const [Grid] = useVbenVxeGrid({ +