From 7222e320e2509e068583f0e3ef40d069e5b6acb7 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 14 Jun 2026 02:48:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(bpm):=20vben5=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=AE=A1=E6=89=B9=E9=99=84=E4=BB=B6=E4=B8=8A=E4=BC=A0=E4=B8=8E?= =?UTF-8?q?=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - web-antd、web-ele 审批通过和拒绝弹窗补齐附件上传约束 - 限制支持常用文档和图片格式 - 限制单文件最大 5MB、最多上传 10 个 - 统一上传目录为 bpm/task-attachment - 开启上传说明展示 - web-antdv-next 补齐 BPM 审批附件完整能力 - ApprovalTaskInfo 增加 attachments 字段 - 审批通过、审批拒绝表单支持上传附件 - 提交 approveTask/rejectTask 时携带 attachments - 弹窗关闭或提交成功后重置附件表单数据 - 支持图片附件预览,非图片附件新窗口打开 - 三端时间线支持展示审批附件 - 审批意见和附件统一展示在任务节点下 - 图片附件展示缩略图并支持预览 - 普通附件展示文件名并支持点击打开 - 兼容带 query/hash 的附件 URL 文件名解析和图片类型识别 --- .../detail/modules/operation-button.vue | 30 ++++- .../detail/modules/time-line.vue | 12 +- .../src/api/bpm/processInstance/index.ts | 1 + .../detail/modules/operation-button.vue | 105 ++++++++++++++++++ .../detail/modules/time-line.vue | 74 +++++++++++- .../detail/modules/operation-button.vue | 30 ++++- .../detail/modules/time-line.vue | 12 +- 7 files changed, 250 insertions(+), 14 deletions(-) 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 e10a87ba6..12c6d9049 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 @@ -95,6 +95,24 @@ const popOverVisible: any = ref({ deleteSign: false, }); // 气泡卡是否展示 const returnList = ref([] as any); // 退回节点 +const APPROVAL_ATTACHMENT_FILE_TYPES = [ + 'doc', + 'docx', + 'xls', + 'xlsx', + 'ppt', + 'pptx', + 'txt', + 'pdf', + 'jpg', + 'jpeg', + 'png', + 'gif', + 'bmp', + 'webp', +]; +const APPROVAL_ATTACHMENT_FILE_SIZE = 5; +const APPROVAL_ATTACHMENT_DIRECTORY = 'bpm/task-attachment'; /** 创建流程表达式 */ function openSignatureModal() { @@ -772,7 +790,9 @@ const imagePreviewUrl = ref(''); /** 判断文件是否为图片类型 */ function isImageUrl(url: string) { - return /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(url); + return /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test( + url.split(/[?#]/)[0] || '', + ); } /** 处理文件预览 */ @@ -892,8 +912,12 @@ defineExpose({ loadTodoTask }); 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 0e9f2ab7e..50f8dfddb 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 @@ -197,7 +197,7 @@ function shouldShowCustomUserSelect( /** 判断是否需要显示审批意见和附件 */ function shouldShowReasonAndAttachment(task: any, nodeType: BpmNodeTypeEnum) { return ( - (task.reason || task.attachments?.length > 0) && + Boolean(task.reason || task.attachments?.length > 0) && [BpmNodeTypeEnum.START_USER_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes( nodeType, ) @@ -205,11 +205,17 @@ function shouldShowReasonAndAttachment(task: any, nodeType: BpmNodeTypeEnum) { } function getAttachmentName(url: string) { - return decodeURIComponent(url.slice(url.lastIndexOf('/') + 1)); + const cleanUrl = url.split(/[?#]/)[0] || ''; + const fileName = cleanUrl.slice(cleanUrl.lastIndexOf('/') + 1); + try { + return decodeURIComponent(fileName); + } catch { + return fileName; + } } function isImageAttachment(url: string) { - const ext = url.split('.').pop()?.toLowerCase(); + const ext = url.split(/[?#]/)[0]?.split('.').pop()?.toLowerCase(); return ['bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp'].includes(ext || ''); } diff --git a/apps/web-antdv-next/src/api/bpm/processInstance/index.ts b/apps/web-antdv-next/src/api/bpm/processInstance/index.ts index 7ee5421f8..8ed2116c5 100644 --- a/apps/web-antdv-next/src/api/bpm/processInstance/index.ts +++ b/apps/web-antdv-next/src/api/bpm/processInstance/index.ts @@ -79,6 +79,7 @@ export namespace BpmProcessInstanceApi { export interface ApprovalTaskInfo { id: number; assigneeUser: User; + attachments?: string[]; ownerUser: User; reason: string; signPicUrl: string; diff --git a/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/operation-button.vue b/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/operation-button.vue index 423cd8daf..09b55a03f 100644 --- a/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/operation-button.vue +++ b/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/operation-button.vue @@ -55,6 +55,7 @@ import { transferTask, } from '#/api/bpm/task'; import { setConfAndFields2 } from '#/components/form-create'; +import { FileUpload } from '#/components/upload'; import { $t } from '#/locales'; import Signature from './signature.vue'; @@ -94,6 +95,24 @@ const popOverVisible: any = ref({ deleteSign: false, }); // 气泡卡是否展示 const returnList = ref([] as any); // 退回节点 +const APPROVAL_ATTACHMENT_FILE_TYPES = [ + 'doc', + 'docx', + 'xls', + 'xlsx', + 'ppt', + 'pptx', + 'txt', + 'pdf', + 'jpg', + 'jpeg', + 'png', + 'gif', + 'bmp', + 'webp', +]; +const APPROVAL_ATTACHMENT_FILE_SIZE = 5; +const APPROVAL_ATTACHMENT_DIRECTORY = 'bpm/task-attachment'; /** 创建流程表达式 */ function openSignatureModal() { @@ -120,6 +139,7 @@ const approveReasonForm: any = reactive({ reason: '', signPicUrl: '', nextAssignees: {}, + attachments: [], }); const approveReasonRule: Record = computed(() => { return { @@ -142,6 +162,7 @@ const approveReasonRule: Record = computed(() => { const rejectFormRef = ref(); const rejectReasonForm = reactive({ reason: '', + attachments: [], }); // 拒绝表单 const rejectReasonRule: any = computed(() => { return { @@ -290,6 +311,14 @@ function closePopover(type: string, formRef: any | FormInstance) { if (formRef) { formRef.resetFields(); } + if (type === 'approve') { + approveReasonForm.reason = ''; + approveReasonForm.attachments = []; + approveReasonForm.signPicUrl = ''; + } else if (type === 'reject') { + rejectReasonForm.reason = ''; + rejectReasonForm.attachments = []; + } if (popOverVisible.value[type]) popOverVisible.value[type] = false; nextAssigneesActivityNode.value = []; // 清理 Timeline 组件中的自定义审批人数据 @@ -401,6 +430,7 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) { const data = { id: runningTask.value.id, reason: approveReasonForm.reason, + attachments: approveReasonForm.attachments, variables, // 审批通过, 把修改的字段值赋于流程实例变量 nextAssignees: approveReasonForm.nextAssignees, // 下个自选节点选择的审批人信息 } as any; @@ -414,6 +444,9 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) { await formCreateApi.validate(); } await approveTask(data); + approveReasonForm.reason = ''; + approveReasonForm.attachments = []; + approveReasonForm.signPicUrl = ''; popOverVisible.value.approve = false; nextAssigneesActivityNode.value = []; // 清理 Timeline 组件中的自定义审批人数据 @@ -426,8 +459,11 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) { const data = { id: runningTask.value.id, reason: rejectReasonForm.reason, + attachments: rejectReasonForm.attachments, }; await rejectTask(data); + rejectReasonForm.reason = ''; + rejectReasonForm.attachments = []; popOverVisible.value.reject = false; message.success('审批不通过成功'); } @@ -748,6 +784,36 @@ function handleSignFinish(url: string) { approveFormRef.value?.validateFields(['signPicUrl']); } +/** 附件图片预览 */ +const imagePreviewVisible = ref(false); +const imagePreviewUrl = ref(''); + +/** 判断文件是否为图片类型 */ +function isImageUrl(url: string) { + return /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test( + url.split(/[?#]/)[0] || '', + ); +} + +/** 处理文件预览 */ +function handleFilePreview(file: any) { + if (!file?.url && !file?.response) { + message.warning('文件地址不存在,无法预览'); + return; + } + const url = file.url || file?.response?.url || file?.response; + if (!url) { + message.warning('文件地址不存在,无法预览'); + return; + } + if (isImageUrl(url)) { + imagePreviewUrl.value = url; + imagePreviewVisible.value = true; + } else { + window.open(url, '_blank'); + } +} + /** 处理弹窗可见性 */ function handlePopoverVisible(visible: boolean) { if (!visible) { @@ -843,6 +909,19 @@ defineExpose({ loadTodoTask }); :rows="4" /> + + +