diff --git a/apps/web-antd/src/api/bpm/processInstance/index.ts b/apps/web-antd/src/api/bpm/processInstance/index.ts new file mode 100644 index 00000000..cec0a1fd --- /dev/null +++ b/apps/web-antd/src/api/bpm/processInstance/index.ts @@ -0,0 +1,163 @@ +import type { PageParam, PageResult } from '@vben/request'; + +import type { BpmModelApi } from '#/api/bpm/model'; +import type { CandidateStrategy, NodeType } from '#/utils'; + +import { requestClient } from '#/api/request'; + +export namespace BpmProcessInstanceApi { + export type Task = { + id: number; + name: string; + }; + + export type User = { + avatar: string; + id: number; + nickname: string; + }; + + // 审批任务信息 + export type ApprovalTaskInfo = { + assigneeUser: User; + id: number; + ownerUser: User; + reason: string; + signPicUrl: string; + status: number; + }; + + // 审批节点信息 + export type ApprovalNodeInfo = { + candidateStrategy?: CandidateStrategy; + candidateUsers?: User[]; + endTime?: Date; + id: number; + name: string; + nodeType: NodeType; + startTime?: Date; + status: number; + tasks: ApprovalTaskInfo[]; + }; + + export type ProcessInstanceVO = { + businessKey: string; + category: string; + createTime: string; + endTime: string; + fields: string[]; + id: number; + name: string; + processDefinition?: BpmModelApi.ProcessDefinitionVO; + processDefinitionId: string; + remark: string; + result: number; + status: number; + tasks: BpmProcessInstanceApi.Task[]; + }; +} + +/** 查询我的流程实例分页 */ +export async function getProcessInstanceMyPage(params: PageParam) { + return requestClient.get>( + '/bpm/process-instance/my-page', + { params }, + ); +} + +/** 查询管理员流程实例分页 */ +export async function getProcessInstanceManagerPage(params: PageParam) { + return requestClient.get>( + '/bpm/process-instance/manager-page', + { params }, + ); +} + +/** 新增流程实例 */ +export async function createProcessInstance( + data: BpmProcessInstanceApi.ProcessInstanceVO, +) { + return requestClient.post( + '/bpm/process-instance/create', + data, + ); +} + +/** 申请人主动取消流程实例 */ +export async function cancelProcessInstanceByStartUser( + id: number, + reason: string, +) { + return requestClient.delete( + '/bpm/process-instance/cancel-by-start-user', + { + data: { id, reason }, + }, + ); +} + +/** 管理员取消流程实例 */ +export async function cancelProcessInstanceByAdmin(id: number, reason: string) { + return requestClient.delete( + '/bpm/process-instance/cancel-by-admin', + { + data: { id, reason }, + }, + ); +} + +/** 查询流程实例详情 */ +export async function getProcessInstance(id: number) { + return requestClient.get( + `/bpm/process-instance/get?id=${id}`, + ); +} + +/** 查询复制流程实例分页 */ +export async function getProcessInstanceCopyPage(params: PageParam) { + return requestClient.get>( + '/bpm/process-instance/copy/page', + { params }, + ); +} + +/** 更新流程实例 */ +export async function updateProcessInstance( + data: BpmProcessInstanceApi.ProcessInstanceVO, +) { + return requestClient.put( + '/bpm/process-instance/update', + data, + ); +} + +/** 获取审批详情 */ +export async function getApprovalDetail(params: any) { + return requestClient.get( + `/bpm/process-instance/get-approval-detail`, + { params }, + ); +} + +/** 获取下一个执行的流程节点 */ +export async function getNextApprovalNodes(params: any) { + return requestClient.get( + `/bpm/process-instance/get-next-approval-nodes`, + { params }, + ); +} + +/** 获取表单字段权限 */ +export async function getFormFieldsPermission(params: any) { + return requestClient.get( + `/bpm/process-instance/get-form-fields-permission`, + { params }, + ); +} + +/** 获取流程实例 BPMN 模型视图 */ +export async function getProcessInstanceBpmnModelView(id: number) { + return requestClient.get( + `/bpm/process-instance/get-bpmn-model-view?id=${id}`, + ); +} diff --git a/apps/web-antd/src/utils/constants.ts b/apps/web-antd/src/utils/constants.ts index ac1d8120..ec58eec8 100644 --- a/apps/web-antd/src/utils/constants.ts +++ b/apps/web-antd/src/utils/constants.ts @@ -464,3 +464,132 @@ export const BpmAutoApproveType = { APPROVE_ALL: 1, // 仅审批一次,后续重复的审批节点均自动通过 APPROVE_SEQUENT: 2, // 仅针对连续审批的节点自动通过 }; + +// 候选人策略枚举 ( 用于审批节点。抄送节点 ) +export enum CandidateStrategy { + /** + * 审批人自选 + */ + APPROVE_USER_SELECT = 34, + /** + * 部门的负责人 + */ + DEPT_LEADER = 21, + /** + * 部门成员 + */ + DEPT_MEMBER = 20, + /** + * 流程表达式 + */ + EXPRESSION = 60, + /** + * 表单内部门负责人 + */ + FORM_DEPT_LEADER = 51, + /** + * 表单内用户字段 + */ + FORM_USER = 50, + /** + * 连续多级部门的负责人 + */ + MULTI_LEVEL_DEPT_LEADER = 23, + /** + * 指定岗位 + */ + POST = 22, + /** + * 指定角色 + */ + ROLE = 10, + /** + * 发起人自己 + */ + START_USER = 36, + /** + * 发起人部门负责人 + */ + START_USER_DEPT_LEADER = 37, + /** + * 发起人连续多级部门的负责人 + */ + START_USER_MULTI_LEVEL_DEPT_LEADER = 38, + /** + * 发起人自选 + */ + START_USER_SELECT = 35, + /** + * 指定用户 + */ + USER = 30, + /** + * 指定用户组 + */ + USER_GROUP = 40, +} + +/** + * 节点类型 + */ +export enum NodeType { + /** + * 子流程节点 + */ + CHILD_PROCESS_NODE = 20, + /** + * 条件分支节点 (对应排他网关) + */ + CONDITION_BRANCH_NODE = 51, + /** + * 条件节点 + */ + CONDITION_NODE = 50, + + /** + * 抄送人节点 + */ + COPY_TASK_NODE = 12, + + /** + * 延迟器节点 + */ + DELAY_TIMER_NODE = 14, + + /** + * 结束节点 + */ + END_EVENT_NODE = 1, + + /** + * 包容分支节点 (对应包容网关) + */ + INCLUSIVE_BRANCH_NODE = 53, + + /** + * 并行分支节点 (对应并行网关) + */ + PARALLEL_BRANCH_NODE = 52, + + /** + * 路由分支节点 + */ + ROUTER_BRANCH_NODE = 54, + /** + * 发起人节点 + */ + START_USER_NODE = 10, + /** + * 办理人节点 + */ + TRANSACTOR_NODE = 13, + + /** + * 触发器节点 + */ + TRIGGER_NODE = 15, + /** + * 审批人节点 + */ + USER_TASK_NODE = 11, +} diff --git a/apps/web-antd/src/utils/formatTime.ts b/apps/web-antd/src/utils/formatTime.ts new file mode 100644 index 00000000..3d4e4722 --- /dev/null +++ b/apps/web-antd/src/utils/formatTime.ts @@ -0,0 +1,31 @@ +/** + * 将毫秒,转换成时间字符串。例如说,xx 分钟 + * + * @param ms 毫秒 + * @returns {string} 字符串 + */ +export function formatPast2(ms: number): string { + // 定义时间单位常量,便于维护 + const SECOND = 1000; + const MINUTE = 60 * SECOND; + const HOUR = 60 * MINUTE; + const DAY = 24 * HOUR; + + // 计算各时间单位 + const day = Math.floor(ms / DAY); + const hour = Math.floor((ms % DAY) / HOUR); + const minute = Math.floor((ms % HOUR) / MINUTE); + const second = Math.floor((ms % MINUTE) / SECOND); + + // 根据时间长短返回不同格式 + if (day > 0) { + return `${day} 天${hour} 小时 ${minute} 分钟`; + } + if (hour > 0) { + return `${hour} 小时 ${minute} 分钟`; + } + if (minute > 0) { + return `${minute} 分钟`; + } + return second > 0 ? `${second} 秒` : `${0} 秒`; +} diff --git a/apps/web-antd/src/utils/index.ts b/apps/web-antd/src/utils/index.ts index 9dc13ef1..dfcc6046 100644 --- a/apps/web-antd/src/utils/index.ts +++ b/apps/web-antd/src/utils/index.ts @@ -1,4 +1,5 @@ export * from './constants'; export * from './dict'; +export * from './formatTime'; export * from './rangePickerProps'; export * from './validator'; diff --git a/apps/web-antd/src/views/bpm/processInstance/manager/data.ts b/apps/web-antd/src/views/bpm/processInstance/manager/data.ts new file mode 100644 index 00000000..2821c582 --- /dev/null +++ b/apps/web-antd/src/views/bpm/processInstance/manager/data.ts @@ -0,0 +1,227 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; +import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance'; + +import { h } from 'vue'; + +import { useAccess } from '@vben/access'; + +import { Button } from 'ant-design-vue'; + +import { getCategorySimpleList } from '#/api/bpm/category'; +import { getSimpleUserList } from '#/api/system/user'; +import { $t } from '#/locales'; +import { + DICT_TYPE, + formatPast2, + getDictOptions, + getRangePickerDefaultProps, +} from '#/utils'; + +import { BpmProcessInstanceStatus } from '../../../../utils/constants'; + +const { hasAccessByCodes } = useAccess(); + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'startUserId', + label: '发起人', + component: 'ApiSelect', + componentProps: { + placeholder: '请选择发起人', + allowClear: true, + api: getSimpleUserList, + labelField: 'nickname', + valueField: 'id', + }, + }, + { + fieldName: 'name', + label: '流程名称', + component: 'Input', + componentProps: { + placeholder: '请输入流程名称', + allowClear: true, + }, + }, + { + fieldName: 'processDefinitionId', + label: '所属流程', + component: 'Input', + componentProps: { + placeholder: '请输入流程定义的编号', + allowClear: true, + }, + }, + // 流程分类 + { + fieldName: 'category', + label: '流程分类', + component: 'ApiSelect', + componentProps: { + placeholder: '请输入流程分类', + allowClear: true, + api: getCategorySimpleList, + labelField: 'name', + valueField: 'code', + }, + }, + // 流程状态 + { + fieldName: 'status', + label: '流程状态', + component: 'Select', + componentProps: { + options: getDictOptions( + DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS, + 'number', + ), + placeholder: '请选择流程状态', + allowClear: true, + }, + }, + // 发起时间 + { + fieldName: 'createTime', + label: '发起时间', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns( + onActionClick: OnActionClickFn, + onTaskClick: (task: BpmProcessInstanceApi.Task) => void, +): VxeTableGridOptions['columns'] { + return [ + { + field: 'name', + title: '流程名称', + minWidth: 200, + fixed: 'left', + }, + + { + field: 'categoryName', + title: '流程分类', + minWidth: 120, + fixed: 'left', + }, + + { + field: 'startUser.nickname', + title: '流程发起人', + minWidth: 120, + }, + + { + field: 'startUser.deptName', + title: '发起部门', + minWidth: 120, + }, + + // 流程状态 + { + field: 'status', + title: '流程状态', + minWidth: 120, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS }, + }, + }, + + { + field: 'startTime', + title: '发起时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + field: 'endTime', + title: '结束时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + field: 'durationInMillis', + title: '流程耗时', + minWidth: 180, + slots: { + default: ({ row }) => { + return row.durationInMillis > 0 + ? formatPast2(row.durationInMillis) + : '-'; + }, + }, + }, + + // 当前审批任务 tasks + { + field: 'tasks', + title: '当前审批任务', + minWidth: 320, + slots: { + default: ({ row }) => { + if (!row?.tasks?.length) return '-'; + + return row.tasks.map((task: BpmProcessInstanceApi.Task) => + h( + Button, + { + type: 'link', + size: 'small', + onClick: () => onTaskClick(task), + }, + { default: () => task.name }, + ), + ); + }, + }, + }, + + { + field: 'id', + title: '流程编号', + minWidth: 320, + }, + { + field: 'operation', + title: '操作', + minWidth: 180, + align: 'center', + fixed: 'right', + cellRender: { + attrs: { + nameField: 'name', + nameTitle: '流程分类', + onClick: onActionClick, + }, + name: 'CellOperation', + options: [ + { + code: 'detail', + text: $t('ui.actionTitle.detail'), + show: hasAccessByCodes(['bpm:process-instance:query']), + }, + { + code: 'cancel', + text: $t('ui.actionTitle.cancel'), + show: (row: BpmProcessInstanceApi.ProcessInstanceVO) => { + return ( + row.status === BpmProcessInstanceStatus.RUNNING && + hasAccessByCodes(['bpm:process-instance:cancel']) + ); + }, + }, + ], + }, + }, + ]; +} diff --git a/apps/web-antd/src/views/bpm/processInstance/manager/index.vue b/apps/web-antd/src/views/bpm/processInstance/manager/index.vue index 9dd48e7c..3b3ce81b 100644 --- a/apps/web-antd/src/views/bpm/processInstance/manager/index.vue +++ b/apps/web-antd/src/views/bpm/processInstance/manager/index.vue @@ -1,31 +1,128 @@ diff --git a/packages/locales/src/langs/en-US/ui.json b/packages/locales/src/langs/en-US/ui.json index 8d91dc41..a9fa1717 100644 --- a/packages/locales/src/langs/en-US/ui.json +++ b/packages/locales/src/langs/en-US/ui.json @@ -11,6 +11,7 @@ "mobile": "Please input a valid {0}" }, "actionTitle": { + "cancel": "Cancel {0}", "edit": "Modify {0}", "create": "Create {0}", "delete": "Delete {0}", diff --git a/packages/locales/src/langs/zh-CN/ui.json b/packages/locales/src/langs/zh-CN/ui.json index 652d43f7..446cfe75 100644 --- a/packages/locales/src/langs/zh-CN/ui.json +++ b/packages/locales/src/langs/zh-CN/ui.json @@ -11,6 +11,7 @@ "mobile": "请输入正确的{0}" }, "actionTitle": { + "cancel": "取消{0}", "edit": "修改{0}", "create": "新增{0}", "delete": "删除{0}",