From 01f929e10fc188f73d93fdcf2a03fa1ad50c7ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=90=E5=A4=9C?= <278898052@qq.com> Date: Sat, 24 May 2025 15:10:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E3=80=90BPM=20=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E6=B5=81=E3=80=91=E5=AE=8C=E5=96=84=E6=93=8D=E4=BD=9C=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E3=80=81=E6=B5=81=E7=A8=8B=E7=AD=BE=E5=90=8D=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-antd/package.json | 3 +- apps/web-antd/src/api/bpm/definition/index.ts | 11 +- apps/web-antd/src/api/bpm/model/index.ts | 1 + .../src/api/bpm/processInstance/index.ts | 17 +- apps/web-antd/src/api/bpm/task/index.ts | 4 +- apps/web-antd/src/utils/constants.ts | 106 +- apps/web-antd/src/utils/download.ts | 214 +++ apps/web-antd/src/utils/index.ts | 1 + .../bpm/processInstance/create/index.vue | 39 +- .../bpm/processInstance/detail/index.vue | 125 +- .../detail/modules/bpm-viewer.vue | 9 + .../detail/modules/operation-button.vue | 1392 +++++++++++++++++ .../detail/modules/signature.vue | 88 ++ .../detail/modules/simple-bpm-viewer.vue | 9 + .../detail/modules/time-line.vue | 2 +- .../src/views/bpm/task/copy/index.vue | 4 +- pnpm-lock.yaml | 27 + pnpm-workspace.yaml | 1 + 18 files changed, 1962 insertions(+), 91 deletions(-) create mode 100644 apps/web-antd/src/utils/download.ts create mode 100644 apps/web-antd/src/views/bpm/processInstance/detail/modules/bpm-viewer.vue create mode 100644 apps/web-antd/src/views/bpm/processInstance/detail/modules/operation-button.vue create mode 100644 apps/web-antd/src/views/bpm/processInstance/detail/modules/signature.vue create mode 100644 apps/web-antd/src/views/bpm/processInstance/detail/modules/simple-bpm-viewer.vue diff --git a/apps/web-antd/package.json b/apps/web-antd/package.json index fdb068a51..f9a0862ed 100644 --- a/apps/web-antd/package.json +++ b/apps/web-antd/package.json @@ -53,7 +53,8 @@ "pinia": "catalog:", "vue": "catalog:", "vue-dompurify-html": "catalog:", - "vue-router": "catalog:" + "vue-router": "catalog:", + "vue3-signature": "catalog:" }, "devDependencies": { "@types/crypto-js": "catalog:" diff --git a/apps/web-antd/src/api/bpm/definition/index.ts b/apps/web-antd/src/api/bpm/definition/index.ts index 2dc9f625b..795caa1b5 100644 --- a/apps/web-antd/src/api/bpm/definition/index.ts +++ b/apps/web-antd/src/api/bpm/definition/index.ts @@ -37,11 +37,12 @@ export async function getProcessDefinitionPage(params: PageParam) { /** 查询流程定义列表 */ export async function getProcessDefinitionList(params: any) { - return requestClient.get< - PageResult - >('/bpm/process-definition/list', { - params, - }); + return requestClient.get( + '/bpm/process-definition/list', + { + params, + }, + ); } /** 查询流程定义列表(简单列表) */ diff --git a/apps/web-antd/src/api/bpm/model/index.ts b/apps/web-antd/src/api/bpm/model/index.ts index 713fedc47..455981782 100644 --- a/apps/web-antd/src/api/bpm/model/index.ts +++ b/apps/web-antd/src/api/bpm/model/index.ts @@ -14,6 +14,7 @@ export namespace BpmModelApi { /** 流程定义 VO */ export interface ProcessDefinitionVO { id: string; + key?: string; version: number; deploymentTime: number; suspensionState: number; diff --git a/apps/web-antd/src/api/bpm/processInstance/index.ts b/apps/web-antd/src/api/bpm/processInstance/index.ts index 70d218469..4e8329c94 100644 --- a/apps/web-antd/src/api/bpm/processInstance/index.ts +++ b/apps/web-antd/src/api/bpm/processInstance/index.ts @@ -1,5 +1,7 @@ import type { PageParam, PageResult } from '@vben/request'; +import type { BpmTaskApi } from '../task'; + import type { BpmModelApi } from '#/api/bpm/model'; import type { BpmCandidateStrategyEnum, BpmNodeTypeEnum } from '#/utils'; @@ -67,25 +69,26 @@ export namespace BpmProcessInstanceApi { processDefinition: BpmModelApi.ProcessDefinitionVO; processInstance: BpmProcessInstanceApi.ProcessInstanceVO; status: number; + todoTask: BpmTaskApi.TaskVO; }; // 抄送流程实例 VO export type CopyVO = { + activityId: string; + activityName: string; + createTime: number; + createUser: User; id: number; - startUser: User; processInstanceId: string; processInstanceName: string; processInstanceStartTime: number; - activityId: string; - activityName: string; - taskId: string; reason: string; - createUser: User; - createTime: number; + startUser: User; summary: { key: string; value: string; }[]; + taskId: string; }; } @@ -173,7 +176,7 @@ export async function getApprovalDetail(params: any) { /** 获取下一个执行的流程节点 */ export async function getNextApprovalNodes(params: any) { - return requestClient.get( + return requestClient.get( `/bpm/process-instance/get-next-approval-nodes`, { params }, ); diff --git a/apps/web-antd/src/api/bpm/task/index.ts b/apps/web-antd/src/api/bpm/task/index.ts index 17987704a..19cbede85 100644 --- a/apps/web-antd/src/api/bpm/task/index.ts +++ b/apps/web-antd/src/api/bpm/task/index.ts @@ -89,8 +89,8 @@ export const getTaskListByProcessInstanceId = async (id: string) => { }; /** 获取所有可退回的节点 */ -export const getTaskListByReturn = async (data: any) => { - return await requestClient.get('/bpm/task/list-by-return', data); +export const getTaskListByReturn = async (id: string) => { + return await requestClient.get(`/bpm/task/list-by-return?id=${id}`); }; /** 退回 */ diff --git a/apps/web-antd/src/utils/constants.ts b/apps/web-antd/src/utils/constants.ts index affb6b1bb..ed4f97a8e 100644 --- a/apps/web-antd/src/utils/constants.ts +++ b/apps/web-antd/src/utils/constants.ts @@ -441,30 +441,6 @@ export const ErpBizType = { // ========== BPM 模块 ========== -export const BpmModelType = { - BPMN: 10, // BPMN 设计器 - SIMPLE: 20, // 简易设计器 -}; - -export const BpmModelFormType = { - NORMAL: 10, // 流程表单 - CUSTOM: 20, // 业务表单 -}; - -export const BpmProcessInstanceStatus = { - NOT_START: -1, // 未开始 - RUNNING: 1, // 审批中 - APPROVE: 2, // 审批通过 - REJECT: 3, // 审批不通过 - CANCEL: 4, // 已取消 -}; - -export const BpmAutoApproveType = { - NONE: 0, // 不自动通过 - APPROVE_ALL: 1, // 仅审批一次,后续重复的审批节点均自动通过 - APPROVE_SEQUENT: 2, // 仅针对连续审批的节点自动通过 -}; - // 候选人策略枚举 ( 用于审批节点。抄送节点 ) export enum BpmCandidateStrategyEnum { /** @@ -594,6 +570,40 @@ export enum BpmNodeTypeEnum { USER_TASK_NODE = 11, } +/** + * 流程任务操作按钮 + */ +export enum BpmTaskOperationButtonTypeEnum { + /** + * 加签 + */ + ADD_SIGN = 5, + /** + * 通过 + */ + APPROVE = 1, + /** + * 抄送 + */ + COPY = 7, + /** + * 委派 + */ + DELEGATE = 4, + /** + * 拒绝 + */ + REJECT = 2, + /** + * 退回 + */ + RETURN = 6, + /** + * 转办 + */ + TRANSFER = 3, +} + /** * 任务状态枚举 */ @@ -667,3 +677,51 @@ export enum BpmFieldPermissionType { */ WRITE = '2', } + +/** + * 流程模型类型 + */ +export const BpmModelType = { + BPMN: 10, // BPMN 设计器 + SIMPLE: 20, // 简易设计器 +}; + +/** + * 流程模型表单类型 + */ +export const BpmModelFormType = { + NORMAL: 10, // 流程表单 + CUSTOM: 20, // 业务表单 +}; + +/** + * 流程实例状态 + */ +export const BpmProcessInstanceStatus = { + NOT_START: -1, // 未开始 + RUNNING: 1, // 审批中 + APPROVE: 2, // 审批通过 + REJECT: 3, // 审批不通过 + CANCEL: 4, // 已取消 +}; + +/** + * 自动审批类型 + */ +export const BpmAutoApproveType = { + NONE: 0, // 不自动通过 + APPROVE_ALL: 1, // 仅审批一次,后续重复的审批节点均自动通过 + APPROVE_SEQUENT: 2, // 仅针对连续审批的节点自动通过 +}; + +/** + * 审批操作按钮名称 + */ +export const OPERATION_BUTTON_NAME = new Map(); +OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.APPROVE, '通过'); +OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.REJECT, '拒绝'); +OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.TRANSFER, '转办'); +OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.DELEGATE, '委派'); +OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.ADD_SIGN, '加签'); +OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.RETURN, '退回'); +OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.COPY, '抄送'); diff --git a/apps/web-antd/src/utils/download.ts b/apps/web-antd/src/utils/download.ts new file mode 100644 index 000000000..acb0129c8 --- /dev/null +++ b/apps/web-antd/src/utils/download.ts @@ -0,0 +1,214 @@ +/** + * 下载工具模块 + * 提供多种文件格式的下载功能 + */ + +/** + * 图片下载配置接口 + */ +interface ImageDownloadOptions { + /** 图片 URL */ + url: string; + /** 指定画布宽度 */ + canvasWidth?: number; + /** 指定画布高度 */ + canvasHeight?: number; + /** 将图片绘制在画布上时带上图片的宽高值,默认为 true */ + drawWithImageSize?: boolean; +} + +/** + * 基础文件下载函数 + * @param data - 文件数据 Blob + * @param fileName - 文件名 + * @param mimeType - MIME 类型 + */ +export const download0 = (data: Blob, fileName: string, mimeType: string) => { + try { + // 创建 blob + const blob = new Blob([data], { type: mimeType }); + // 创建 href 超链接,点击进行下载 + window.URL = window.URL || window.webkitURL; + const href = URL.createObjectURL(blob); + const downA = document.createElement('a'); + downA.href = href; + downA.download = fileName; + downA.click(); + // 销毁超链接 + window.URL.revokeObjectURL(href); + } catch (error) { + console.error('文件下载失败:', error); + throw new Error( + `文件下载失败: ${error instanceof Error ? error.message : '未知错误'}`, + ); + } +}; + +/** + * 触发文件下载的通用方法 + * @param url - 下载链接 + * @param fileName - 文件名 + */ +const triggerDownload = (url: string, fileName: string) => { + const a = document.createElement('a'); + a.href = url; + a.download = fileName; + a.click(); +}; + +export const download = { + /** + * 下载 Excel 文件 + * @param data - 文件数据 Blob + * @param fileName - 文件名 + */ + excel: (data: Blob, fileName: string) => { + download0(data, fileName, 'application/vnd.ms-excel'); + }, + + /** + * 下载 Word 文件 + * @param data - 文件数据 Blob + * @param fileName - 文件名 + */ + word: (data: Blob, fileName: string) => { + download0(data, fileName, 'application/msword'); + }, + + /** + * 下载 Zip 文件 + * @param data - 文件数据 Blob + * @param fileName - 文件名 + */ + zip: (data: Blob, fileName: string) => { + download0(data, fileName, 'application/zip'); + }, + + /** + * 下载 HTML 文件 + * @param data - 文件数据 Blob + * @param fileName - 文件名 + */ + html: (data: Blob, fileName: string) => { + download0(data, fileName, 'text/html'); + }, + + /** + * 下载 Markdown 文件 + * @param data - 文件数据 Blob + * @param fileName - 文件名 + */ + markdown: (data: Blob, fileName: string) => { + download0(data, fileName, 'text/markdown'); + }, + + /** + * 下载 JSON 文件 + * @param data - 文件数据 Blob + * @param fileName - 文件名 + */ + json: (data: Blob, fileName: string) => { + download0(data, fileName, 'application/json'); + }, + + /** + * 下载图片(允许跨域) + * @param options - 图片下载配置 + */ + image: (options: ImageDownloadOptions) => { + const { + url, + canvasWidth, + canvasHeight, + drawWithImageSize = true, + } = options; + + const image = new Image(); + // image.setAttribute('crossOrigin', 'anonymous') + image.src = url; + image.addEventListener('load', () => { + try { + const canvas = document.createElement('canvas'); + canvas.width = canvasWidth || image.width; + canvas.height = canvasHeight || image.height; + const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; + ctx?.clearRect(0, 0, canvas.width, canvas.height); + + if (drawWithImageSize) { + ctx.drawImage(image, 0, 0, image.width, image.height); + } else { + ctx.drawImage(image, 0, 0); + } + + const dataUrl = canvas.toDataURL('image/png'); + triggerDownload(dataUrl, 'image.png'); + } catch (error) { + console.error('图片下载失败:', error); + throw new Error( + `图片下载失败: ${error instanceof Error ? error.message : '未知错误'}`, + ); + } + }); + + image.addEventListener('error', () => { + throw new Error('图片加载失败'); + }); + }, + + /** + * 将 Base64 字符串转换为文件对象 + * @param base64 - Base64 字符串 + * @param fileName - 文件名 + * @returns File 对象 + */ + base64ToFile: (base64: string, fileName: string): File => { + // 输入验证 + if (!base64 || typeof base64 !== 'string') { + throw new Error('base64 参数必须是非空字符串'); + } + + // 将 base64 按照逗号进行分割,将前缀与后续内容分隔开 + const data = base64.split(','); + if (data.length !== 2 || !data[0] || !data[1]) { + throw new Error('无效的 base64 格式'); + } + + // 利用正则表达式从前缀中获取类型信息(image/png、image/jpeg、image/webp等) + const typeMatch = data[0].match(/:(.*?);/); + if (!typeMatch || !typeMatch[1]) { + throw new Error('无法解析 base64 类型信息'); + } + const type = typeMatch[1]; + + // 从类型信息中获取具体的文件格式后缀(png、jpeg、webp) + const typeParts = type.split('/'); + if (typeParts.length !== 2 || !typeParts[1]) { + throw new Error('无效的 MIME 类型格式'); + } + const suffix = typeParts[1]; + + try { + // 使用 atob() 对 base64 数据进行解码,结果是一个文件数据流以字符串的格式输出 + const bstr = window.atob(data[1]); + + // 获取解码结果字符串的长度 + const n = bstr.length; + // 根据解码结果字符串的长度创建一个等长的整型数字数组 + const u8arr = new Uint8Array(n); + + // 优化的 Uint8Array 填充逻辑 + for (let i = 0; i < n; i++) { + // 使用 charCodeAt() 获取字符对应的字节值(Base64 解码后的字符串是字节级别的) + // eslint-disable-next-line unicorn/prefer-code-point + u8arr[i] = bstr.charCodeAt(i); + } + + // 返回 File 文件对象 + return new File([u8arr], `${fileName}.${suffix}`, { type }); + } catch (error) { + throw new Error( + `Base64 解码失败: ${error instanceof Error ? error.message : '未知错误'}`, + ); + } + }, +}; diff --git a/apps/web-antd/src/utils/index.ts b/apps/web-antd/src/utils/index.ts index 022e6441d..101e381e5 100644 --- a/apps/web-antd/src/utils/index.ts +++ b/apps/web-antd/src/utils/index.ts @@ -1,5 +1,6 @@ export * from './constants'; export * from './dict'; +export * from './download'; export * from './formatTime'; export * from './formCreate'; export * from './rangePickerProps'; diff --git a/apps/web-antd/src/views/bpm/processInstance/create/index.vue b/apps/web-antd/src/views/bpm/processInstance/create/index.vue index a8ceeb353..08274b6b5 100644 --- a/apps/web-antd/src/views/bpm/processInstance/create/index.vue +++ b/apps/web-antd/src/views/bpm/processInstance/create/index.vue @@ -1,4 +1,5 @@ + + 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 new file mode 100644 index 000000000..f00c58ab1 --- /dev/null +++ b/apps/web-antd/src/views/bpm/processInstance/detail/modules/operation-button.vue @@ -0,0 +1,1392 @@ + +