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" /> + + + + + + + + + + + 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 cd677e373..9e82e82ad 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 @@ -201,16 +201,31 @@ function shouldShowCustomUserSelect( ); } -/** 判断是否需要显示审批意见 */ -function shouldShowApprovalReason(task: any, nodeType: BpmNodeTypeEnum) { +/** 判断是否需要显示审批意见和附件 */ +function shouldShowReasonAndAttachment(task: any, nodeType: BpmNodeTypeEnum) { return ( - task.reason && + Boolean(task.reason || task.attachments?.length > 0) && [BpmNodeTypeEnum.START_USER_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 isImageAttachment(url: string) { + const ext = url.split(/[?#]/)[0]?.split('.').pop()?.toLowerCase(); + return ['bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp'].includes(ext || ''); +} + /** 用户选择弹窗关闭 */ function handleUserSelectClosed() { selectedUsers.value = []; @@ -413,13 +428,60 @@ defineExpose({ setCustomApproveUsers, batchSetCustomApproveUsers }); - + - 审批意见:{{ task.reason }} + 审批意见:{{ task.reason }} + + + 附件列表: + + + + + + + + {{ getAttachmentName(attachment) }} + + + + + @@ -966,8 +990,12 @@ defineExpose({ loadTodoTask }); 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 a7c19037f..b999c8a89 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 @@ -204,7 +204,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, ) @@ -212,11 +212,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 || ''); }