From 44136d310b438a17a60a0daeefe6a484388d79aa Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 14 Jun 2026 02:48:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(bpm):=20=E6=94=AF=E6=8C=81=E5=AE=A1?= =?UTF-8?q?=E6=89=B9=E4=BB=BB=E5=8A=A1=E9=99=84=E4=BB=B6=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E4=B8=8E=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 审批通过、审批拒绝弹窗新增附件/图片上传 - 审批提交时携带 attachments 字段 - 审批完成或关闭弹窗后清理附件表单状态 - 审批流时间线支持展示审批附件 - 图片附件支持预览,非图片附件支持链接打开 - 统一附件上传目录、文件类型白名单和 5MB 大小限制 - ApprovalTaskInfo 增加 attachments 字段 --- src/api/bpm/processInstance/index.ts | 1 + .../detail/ProcessInstanceOperationButton.vue | 54 +++++++++++++++++- .../detail/ProcessInstanceTimeline.vue | 57 +++++++++++++++++-- 3 files changed, 105 insertions(+), 7 deletions(-) diff --git a/src/api/bpm/processInstance/index.ts b/src/api/bpm/processInstance/index.ts index 1e8f04dac..7d024b1d3 100644 --- a/src/api/bpm/processInstance/index.ts +++ b/src/api/bpm/processInstance/index.ts @@ -36,6 +36,7 @@ export type ApprovalTaskInfo = { assigneeUser: User status: number reason: string + attachments?: string[] signPicUrl: string } diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue b/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue index abfc479bf..7f6134c15 100644 --- a/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue +++ b/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue @@ -44,6 +44,16 @@ :rows="4" /> + + + + + + () // 运行中的任务 @@ -580,6 +618,7 @@ let pendingNextNodesTask: Promise | null = null // 跟踪 onChange 触 const approveReasonForm = reactive({ reason: '', signPicUrl: '', + attachments: [] as string[], nextAssignees: {} }) const approveReasonRule = computed(() => { @@ -599,7 +638,8 @@ const approveReasonRule = computed(() => { // 拒绝表单 const rejectFormRef = ref() const rejectReasonForm = reactive({ - reason: '' + reason: '', + attachments: [] as string[] }) const rejectReasonRule = computed(() => { return { @@ -736,6 +776,12 @@ const closePopover = (type: string, formRef: FormInstance | undefined) => { if (formRef) { formRef.resetFields() } + if (type === 'approve') { + approveReasonForm.attachments = [] + } + if (type === 'reject') { + rejectReasonForm.attachments = [] + } popOverVisible.value[type] = false nextAssigneesActivityNode.value = [] // 清理 Timeline 组件中的自定义审批人数据 @@ -837,6 +883,7 @@ const handleAudit = async (pass: boolean, formRef: FormInstance | undefined) => const data = { id: runningTask.value.id, reason: approveReasonForm.reason, + attachments: approveReasonForm.attachments, variables, // 审批通过, 把修改的字段值赋于流程实例变量 nextAssignees: approveReasonForm.nextAssignees // 下个自选节点选择的审批人信息 } as any @@ -861,7 +908,8 @@ const handleAudit = async (pass: boolean, formRef: FormInstance | undefined) => // 审批不通过数据 const data = { id: runningTask.value.id, - reason: rejectReasonForm.reason + reason: rejectReasonForm.reason, + attachments: rejectReasonForm.attachments } await TaskApi.rejectTask(data) popOverVisible.value.reject = false @@ -869,6 +917,8 @@ const handleAudit = async (pass: boolean, formRef: FormInstance | undefined) => } // 重置表单 formRef.resetFields() + approveReasonForm.attachments = [] + rejectReasonForm.attachments = [] // 加载最新数据 reload() } finally { diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue b/src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue index 91f333e8e..a2d6bd2e5 100644 --- a/src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue +++ b/src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue @@ -129,14 +129,33 @@
- 审批意见:{{ task.reason }} +
审批意见:{{ task.reason }}
+
+ +
{ } } +/** 是否展示审批意见和附件 */ +const shouldShowReasonAndAttachment = ( + task: ProcessInstanceApi.ApprovalTaskInfo, + nodeType: NodeType +) => { + return ( + Boolean(task.reason || task.attachments?.length) && + [NodeType.START_USER_NODE, NodeType.USER_TASK_NODE, NodeType.END_EVENT_NODE].includes(nodeType) + ) +} + +/** 获取附件名 */ +const getAttachmentName = (url: string) => { + const cleanUrl = url.split(/[?#]/)[0] + const fileName = cleanUrl.slice(cleanUrl.lastIndexOf('/') + 1) + try { + return decodeURIComponent(fileName) + } catch { + return fileName + } +} + +/** 是否图片附件 */ +const isImageAttachment = (url: string) => { + const ext = url.split(/[?#]/)[0]?.split('.').pop()?.toLowerCase() + return ['bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp'].includes(ext || '') +} + // 选择自定义审批人 const userSelectFormRef = ref() const handleSelectUser = (activityId, selectedList) => {