liyanbo il y a 2 mois
Parent
commit
ecd211b977
53 fichiers modifiés avec 10114 ajouts et 20 suppressions
  1. 53 0
      src/api/bpm/category/index.ts
  2. 28 0
      src/api/bpm/definition/index.ts
  3. 56 0
      src/api/bpm/form/index.ts
  4. 27 0
      src/api/bpm/leave/index.ts
  5. 78 0
      src/api/bpm/model/index.ts
  6. 42 0
      src/api/bpm/processExpression/index.ts
  7. 109 0
      src/api/bpm/processInstance/index.ts
  8. 40 0
      src/api/bpm/processListener/index.ts
  9. 15 0
      src/api/bpm/simple/index.ts
  10. 113 0
      src/api/bpm/task/index.ts
  11. 47 0
      src/api/bpm/userGroup/index.ts
  12. 111 0
      src/router/modules/remaining.ts
  13. 10 20
      src/types/auto-components.d.ts
  14. 4 0
      src/utils/dict.ts
  15. 130 0
      src/views/bpm/category/CategoryForm.vue
  16. 198 0
      src/views/bpm/category/index.vue
  17. 174 0
      src/views/bpm/form/editor/index.vue
  18. 204 0
      src/views/bpm/form/index.vue
  19. 132 0
      src/views/bpm/group/UserGroupForm.vue
  20. 190 0
      src/views/bpm/group/index.vue
  21. 663 0
      src/views/bpm/model/CategoryDraggableModel.vue
  22. 173 0
      src/views/bpm/model/definition/index.vue
  23. 344 0
      src/views/bpm/model/form/BasicInfo.vue
  24. 442 0
      src/views/bpm/model/form/ExtraSettings.vue
  25. 129 0
      src/views/bpm/model/form/FormDesign.vue
  26. 72 0
      src/views/bpm/model/form/ProcessDesign.vue
  27. 124 0
      src/views/bpm/model/form/editor/index.vue
  28. 442 0
      src/views/bpm/model/form/index.vue
  29. 225 0
      src/views/bpm/model/index.vue
  30. 231 0
      src/views/bpm/oa/leave/create.vue
  31. 51 0
      src/views/bpm/oa/leave/detail.vue
  32. 256 0
      src/views/bpm/oa/leave/index.vue
  33. 114 0
      src/views/bpm/processExpression/ProcessExpressionForm.vue
  34. 181 0
      src/views/bpm/processExpression/index.vue
  35. 331 0
      src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue
  36. 321 0
      src/views/bpm/processInstance/create/index.vue
  37. 61 0
      src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue
  38. 1116 0
      src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue
  39. 174 0
      src/views/bpm/processInstance/detail/ProcessInstanceSimpleViewer.vue
  40. 103 0
      src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
  41. 330 0
      src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue
  42. 50 0
      src/views/bpm/processInstance/detail/SignDialog.vue
  43. 355 0
      src/views/bpm/processInstance/detail/index.vue
  44. 332 0
      src/views/bpm/processInstance/index.vue
  45. 258 0
      src/views/bpm/processInstance/manager/index.vue
  46. 273 0
      src/views/bpm/processInstance/report/index.vue
  47. 162 0
      src/views/bpm/processListener/ProcessListenerForm.vue
  48. 184 0
      src/views/bpm/processListener/index.vue
  49. 40 0
      src/views/bpm/simple/SimpleModelDesign.vue
  50. 157 0
      src/views/bpm/task/copy/index.vue
  51. 265 0
      src/views/bpm/task/done/index.vue
  52. 165 0
      src/views/bpm/task/manager/index.vue
  53. 229 0
      src/views/bpm/task/todo/index.vue

+ 53 - 0
src/api/bpm/category/index.ts

@@ -0,0 +1,53 @@
+import request from '@/config/axios'
+
+// BPM 流程分类 VO
+export interface CategoryVO {
+  id: number // 分类编号
+  name: string // 分类名
+  code: string // 分类标志
+  status: number // 分类状态
+  sort: number // 分类排序
+}
+
+// BPM 流程分类 API
+export const CategoryApi = {
+  // 查询流程分类分页
+  getCategoryPage: async (params: any) => {
+    return await request.get({ url: `/bpm/category/page`, params })
+  },
+
+  // 查询流程分类列表
+  getCategorySimpleList: async () => {
+    return await request.get({ url: `/bpm/category/simple-list` })
+  },
+
+  // 查询流程分类详情
+  getCategory: async (id: number) => {
+    return await request.get({ url: `/bpm/category/get?id=` + id })
+  },
+
+  // 新增流程分类
+  createCategory: async (data: CategoryVO) => {
+    return await request.post({ url: `/bpm/category/create`, data })
+  },
+
+  // 修改流程分类
+  updateCategory: async (data: CategoryVO) => {
+    return await request.put({ url: `/bpm/category/update`, data })
+  },
+
+  // 批量修改流程分类的排序
+  updateCategorySortBatch: async (ids: number[]) => {
+    return await request.put({
+      url: `/bpm/category/update-sort-batch`,
+      params: {
+        ids: ids.join(',')
+      }
+    })
+  },
+
+  // 删除流程分类
+  deleteCategory: async (id: number) => {
+    return await request.delete({ url: `/bpm/category/delete?id=` + id })
+  }
+}

+ 28 - 0
src/api/bpm/definition/index.ts

@@ -0,0 +1,28 @@
+import request from '@/config/axios'
+
+export const getProcessDefinition = async (id?: string, key?: string) => {
+  return await request.get({
+    url: '/bpm/process-definition/get',
+    params: { id, key }
+  })
+}
+
+export const getProcessDefinitionPage = async (params) => {
+  return await request.get({
+    url: '/bpm/process-definition/page',
+    params
+  })
+}
+
+export const getProcessDefinitionList = async (params) => {
+  return await request.get({
+    url: '/bpm/process-definition/list',
+    params
+  })
+}
+
+export const getSimpleProcessDefinitionList = async () => {
+  return await request.get({
+    url: '/bpm/process-definition/simple-list'
+  })
+}

+ 56 - 0
src/api/bpm/form/index.ts

@@ -0,0 +1,56 @@
+import request from '@/config/axios'
+
+export type FormVO = {
+  id: number
+  name: string
+  conf: string
+  fields: string[]
+  status: number
+  remark: string
+  createTime: string
+}
+
+// 创建工作流的表单定义
+export const createForm = async (data: FormVO) => {
+  return await request.post({
+    url: '/bpm/form/create',
+    data: data
+  })
+}
+
+// 更新工作流的表单定义
+export const updateForm = async (data: FormVO) => {
+  return await request.put({
+    url: '/bpm/form/update',
+    data: data
+  })
+}
+
+// 删除工作流的表单定义
+export const deleteForm = async (id: number) => {
+  return await request.delete({
+    url: '/bpm/form/delete?id=' + id
+  })
+}
+
+// 获得工作流的表单定义
+export const getForm = async (id: number) => {
+  return await request.get({
+    url: '/bpm/form/get?id=' + id
+  })
+}
+
+// 获得工作流的表单定义分页
+export const getFormPage = async (params) => {
+  return await request.get({
+    url: '/bpm/form/page',
+    params
+  })
+}
+
+// 获得动态表单的精简列表
+export const getFormSimpleList = async () => {
+  return await request.get({
+    url: '/bpm/form/simple-list'
+  })
+}

+ 27 - 0
src/api/bpm/leave/index.ts

@@ -0,0 +1,27 @@
+import request from '@/config/axios'
+
+export type LeaveVO = {
+  id: number
+  status: number
+  type: number
+  reason: string
+  processInstanceId: string
+  startTime: string
+  endTime: string
+  createTime: string
+}
+
+// 创建请假申请
+export const createLeave = async (data: LeaveVO) => {
+  return await request.post({ url: '/bpm/oa/leave/create', data: data })
+}
+
+// 获得请假申请
+export const getLeave = async (id: number) => {
+  return await request.get({ url: '/bpm/oa/leave/get?id=' + id })
+}
+
+// 获得请假申请分页
+export const getLeavePage = async (params: PageParam) => {
+  return await request.get({ url: '/bpm/oa/leave/page', params })
+}

+ 78 - 0
src/api/bpm/model/index.ts

@@ -0,0 +1,78 @@
+import request from '@/config/axios'
+
+export type ProcessDefinitionVO = {
+  id: string
+  version: number
+  deploymentTIme: string
+  suspensionState: number
+  formType?: number
+}
+
+export type ModelVO = {
+  id: number
+  formName: string
+  key: string
+  name: string
+  description: string
+  category: string
+  formType: number
+  formId: number
+  formCustomCreatePath: string
+  formCustomViewPath: string
+  processDefinition: ProcessDefinitionVO
+  status: number
+  remark: string
+  createTime: string
+  bpmnXml: string
+}
+
+export const getModelList = async (name: string | undefined) => {
+  return await request.get({ url: '/bpm/model/list', params: { name } })
+}
+
+export const getModel = async (id: string) => {
+  return await request.get({ url: '/bpm/model/get?id=' + id })
+}
+
+export const updateModel = async (data: ModelVO) => {
+  return await request.put({ url: '/bpm/model/update', data: data })
+}
+
+// 批量修改流程分类的排序
+export const updateModelSortBatch = async (ids: number[]) => {
+  return await request.put({
+    url: `/bpm/model/update-sort-batch`,
+    params: {
+      ids: ids.join(',')
+    }
+  })
+}
+
+export const updateModelBpmn = async (data: ModelVO) => {
+  return await request.put({ url: '/bpm/model/update-bpmn', data: data })
+}
+
+// 任务状态修改
+export const updateModelState = async (id: number, state: number) => {
+  const data = {
+    id: id,
+    state: state
+  }
+  return await request.put({ url: '/bpm/model/update-state', data: data })
+}
+
+export const createModel = async (data: ModelVO) => {
+  return await request.post({ url: '/bpm/model/create', data: data })
+}
+
+export const deleteModel = async (id: number) => {
+  return await request.delete({ url: '/bpm/model/delete?id=' + id })
+}
+
+export const deployModel = async (id: number) => {
+  return await request.post({ url: '/bpm/model/deploy?id=' + id })
+}
+
+export const cleanModel = async (id: number) => {
+  return await request.delete({ url: '/bpm/model/clean?id=' + id })
+}

+ 42 - 0
src/api/bpm/processExpression/index.ts

@@ -0,0 +1,42 @@
+import request from '@/config/axios'
+
+// BPM 流程表达式 VO
+export interface ProcessExpressionVO {
+  id: number // 编号
+  name: string // 表达式名字
+  status: number // 表达式状态
+  expression: string // 表达式
+}
+
+// BPM 流程表达式 API
+export const ProcessExpressionApi = {
+  // 查询BPM 流程表达式分页
+  getProcessExpressionPage: async (params: any) => {
+    return await request.get({ url: `/bpm/process-expression/page`, params })
+  },
+
+  // 查询BPM 流程表达式详情
+  getProcessExpression: async (id: number) => {
+    return await request.get({ url: `/bpm/process-expression/get?id=` + id })
+  },
+
+  // 新增BPM 流程表达式
+  createProcessExpression: async (data: ProcessExpressionVO) => {
+    return await request.post({ url: `/bpm/process-expression/create`, data })
+  },
+
+  // 修改BPM 流程表达式
+  updateProcessExpression: async (data: ProcessExpressionVO) => {
+    return await request.put({ url: `/bpm/process-expression/update`, data })
+  },
+
+  // 删除BPM 流程表达式
+  deleteProcessExpression: async (id: number) => {
+    return await request.delete({ url: `/bpm/process-expression/delete?id=` + id })
+  },
+
+  // 导出BPM 流程表达式 Excel
+  exportProcessExpression: async (params) => {
+    return await request.download({ url: `/bpm/process-expression/export-excel`, params })
+  }
+}

+ 109 - 0
src/api/bpm/processInstance/index.ts

@@ -0,0 +1,109 @@
+import request from '@/config/axios'
+import { ProcessDefinitionVO } from '@/api/bpm/model'
+import { NodeType, CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts'
+export type Task = {
+  id: string
+  name: string
+}
+
+export type ProcessInstanceVO = {
+  id: number
+  name: string
+  processDefinitionId: string
+  category: string
+  result: number
+  tasks: Task[]
+  fields: string[]
+  status: number
+  remark: string
+  businessKey: string
+  createTime: string
+  endTime: string
+  processDefinition?: ProcessDefinitionVO
+}
+
+// 用户信息
+export type User = {
+  id: number
+  nickname: string
+  avatar: string
+}
+
+// 审批任务信息
+export type ApprovalTaskInfo = {
+  id: number
+  ownerUser: User
+  assigneeUser: User
+  status: number
+  reason: string
+  signPicUrl: string
+}
+
+// 审批节点信息
+export type ApprovalNodeInfo = {
+  id: number
+  name: string
+  nodeType: NodeType
+  candidateStrategy?: CandidateStrategy
+  status: number
+  startTime?: Date
+  endTime?: Date
+  candidateUsers?: User[]
+  tasks: ApprovalTaskInfo[]
+}
+
+export const getProcessInstanceMyPage = async (params: any) => {
+  return await request.get({ url: '/bpm/process-instance/my-page', params })
+}
+
+export const getProcessInstanceManagerPage = async (params: any) => {
+  return await request.get({ url: '/bpm/process-instance/manager-page', params })
+}
+
+export const createProcessInstance = async (data) => {
+  return await request.post({ url: '/bpm/process-instance/create', data: data })
+}
+
+export const cancelProcessInstanceByStartUser = async (id: number, reason: string) => {
+  const data = {
+    id: id,
+    reason: reason
+  }
+  return await request.delete({ url: '/bpm/process-instance/cancel-by-start-user', data: data })
+}
+
+export const cancelProcessInstanceByAdmin = async (id: number, reason: string) => {
+  const data = {
+    id: id,
+    reason: reason
+  }
+  return await request.delete({ url: '/bpm/process-instance/cancel-by-admin', data: data })
+}
+
+export const getProcessInstance = async (id: string) => {
+  return await request.get({ url: '/bpm/process-instance/get?id=' + id })
+}
+
+export const getProcessInstanceCopyPage = async (params: any) => {
+  return await request.get({ url: '/bpm/process-instance/copy/page', params })
+}
+
+// 获取审批详情
+export const getApprovalDetail = async (params: any) => {
+  return await request.get({ url: '/bpm/process-instance/get-approval-detail', params })
+}
+
+// 获取下一个执行的流程节点
+export const getNextApprovalNodes = async (params: any) => {
+  return await request.get({ url: '/bpm/process-instance/get-next-approval-nodes', params })
+}
+
+// 获取表单字段权限
+export const getFormFieldsPermission = async (params: any) => {
+  return await request.get({ url: '/bpm/process-instance/get-form-fields-permission', params })
+}
+
+// 获取流程实例的 BPMN 模型视图
+export const getProcessInstanceBpmnModelView = async (id: string) => {
+  return await request.get({ url: '/bpm/process-instance/get-bpmn-model-view?id=' + id })
+}

+ 40 - 0
src/api/bpm/processListener/index.ts

@@ -0,0 +1,40 @@
+import request from '@/config/axios'
+
+// BPM 流程监听器 VO
+export interface ProcessListenerVO {
+  id: number // 编号
+  name: string // 监听器名字
+  type: string // 监听器类型
+  status: number // 监听器状态
+  event: string // 监听事件
+  valueType: string // 监听器值类型
+  value: string // 监听器值
+}
+
+// BPM 流程监听器 API
+export const ProcessListenerApi = {
+  // 查询流程监听器分页
+  getProcessListenerPage: async (params: any) => {
+    return await request.get({ url: `/bpm/process-listener/page`, params })
+  },
+
+  // 查询流程监听器详情
+  getProcessListener: async (id: number) => {
+    return await request.get({ url: `/bpm/process-listener/get?id=` + id })
+  },
+
+  // 新增流程监听器
+  createProcessListener: async (data: ProcessListenerVO) => {
+    return await request.post({ url: `/bpm/process-listener/create`, data })
+  },
+
+  // 修改流程监听器
+  updateProcessListener: async (data: ProcessListenerVO) => {
+    return await request.put({ url: `/bpm/process-listener/update`, data })
+  },
+
+  // 删除流程监听器
+  deleteProcessListener: async (id: number) => {
+    return await request.delete({ url: `/bpm/process-listener/delete?id=` + id })
+  }
+}

+ 15 - 0
src/api/bpm/simple/index.ts

@@ -0,0 +1,15 @@
+import request from '@/config/axios'
+
+
+export const updateBpmSimpleModel = async (data) => {
+  return await request.post({
+    url: '/bpm/model/simple/update',
+    data: data
+  })
+}
+
+export const getBpmSimpleModel = async (id) => {
+  return await request.get({
+    url: '/bpm/model/simple/get?id=' + id
+  })
+}

+ 113 - 0
src/api/bpm/task/index.ts

@@ -0,0 +1,113 @@
+import request from '@/config/axios'
+
+/**
+ * 任务状态枚举
+ */
+export enum TaskStatusEnum {
+  /**
+   * 未开始
+   */
+  NOT_START = -1,
+
+  /**
+   * 待审批
+   */
+  WAIT = 0,
+  /**
+   * 审批中
+   */
+  RUNNING = 1,
+  /**
+   * 审批通过
+   */
+  APPROVE = 2,
+
+  /**
+   * 审批不通过
+   */
+  REJECT = 3,
+
+  /**
+   * 已取消
+   */
+  CANCEL = 4,
+  /**
+   * 已退回
+   */
+  RETURN = 5,
+  /**
+   * 审批通过中
+   */
+  APPROVING = 7
+}
+
+export const getTaskTodoPage = async (params: any) => {
+  return await request.get({ url: '/bpm/task/todo-page', params })
+}
+
+export const getTaskDonePage = async (params: any) => {
+  return await request.get({ url: '/bpm/task/done-page', params })
+}
+
+export const getTaskManagerPage = async (params: any) => {
+  return await request.get({ url: '/bpm/task/manager-page', params })
+}
+
+export const approveTask = async (data: any) => {
+  return await request.put({ url: '/bpm/task/approve', data })
+}
+
+export const rejectTask = async (data: any) => {
+  return await request.put({ url: '/bpm/task/reject', data })
+}
+
+export const getTaskListByProcessInstanceId = async (processInstanceId: string) => {
+  return await request.get({
+    url: '/bpm/task/list-by-process-instance-id?processInstanceId=' + processInstanceId
+  })
+}
+
+// 获取所有可退回的节点
+export const getTaskListByReturn = async (id: string) => {
+  return await request.get({ url: '/bpm/task/list-by-return', params: { id } })
+}
+
+// 退回
+export const returnTask = async (data: any) => {
+  return await request.put({ url: '/bpm/task/return', data })
+}
+
+// 委派
+export const delegateTask = async (data: any) => {
+  return await request.put({ url: '/bpm/task/delegate', data })
+}
+
+// 转派
+export const transferTask = async (data: any) => {
+  return await request.put({ url: '/bpm/task/transfer', data })
+}
+
+// 加签
+export const signCreateTask = async (data: any) => {
+  return await request.put({ url: '/bpm/task/create-sign', data })
+}
+
+// 减签
+export const signDeleteTask = async (data: any) => {
+  return await request.delete({ url: '/bpm/task/delete-sign', data })
+}
+
+// 抄送
+export const copyTask = async (data: any) => {
+  return await request.put({ url: '/bpm/task/copy', data })
+}
+
+// 获取我的待办任务
+export const myTodoTask = async (processInstanceId: string) => {
+  return await request.get({ url: '/bpm/task/my-todo?processInstanceId=' + processInstanceId })
+}
+
+// 获取减签任务列表
+export const getChildrenTaskList = async (id: string) => {
+  return await request.get({ url: '/bpm/task/list-by-parent-task-id?parentTaskId=' + id })
+}

+ 47 - 0
src/api/bpm/userGroup/index.ts

@@ -0,0 +1,47 @@
+import request from '@/config/axios'
+
+export type UserGroupVO = {
+  id: number
+  name: string
+  description: string
+  userIds: number[]
+  status: number
+  remark: string
+  createTime: string
+}
+
+// 创建用户组
+export const createUserGroup = async (data: UserGroupVO) => {
+  return await request.post({
+    url: '/bpm/user-group/create',
+    data: data
+  })
+}
+
+// 更新用户组
+export const updateUserGroup = async (data: UserGroupVO) => {
+  return await request.put({
+    url: '/bpm/user-group/update',
+    data: data
+  })
+}
+
+// 删除用户组
+export const deleteUserGroup = async (id: number) => {
+  return await request.delete({ url: '/bpm/user-group/delete?id=' + id })
+}
+
+// 获得用户组
+export const getUserGroup = async (id: number) => {
+  return await request.get({ url: '/bpm/user-group/get?id=' + id })
+}
+
+// 获得用户组分页
+export const getUserGroupPage = async (params) => {
+  return await request.get({ url: '/bpm/user-group/page', params })
+}
+
+// 获取用户组精简信息列表
+export const getUserGroupSimpleList = async (): Promise<UserGroupVO[]> => {
+  return await request.get({ url: '/bpm/user-group/simple-list' })
+}

+ 111 - 0
src/router/modules/remaining.ts

@@ -387,6 +387,117 @@ const remainingRouter: AppRouteRecordRaw[] = [
       }
     ]
   },
+  {
+    path: '/bpm',
+    component: Layout,
+    name: 'bpm',
+    meta: {
+      hidden: true
+    },
+    children: [
+      {
+        path: 'manager/form/edit',
+        component: () => import('@/views/bpm/form/editor/index.vue'),
+        name: 'BpmFormEditor',
+        meta: {
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          title: '设计流程表单',
+          activeMenu: '/bpm/manager/form'
+        }
+      },
+      {
+        path: 'manager/definition',
+        component: () => import('@/views/bpm/model/definition/index.vue'),
+        name: 'BpmProcessDefinition',
+        meta: {
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          title: '流程定义',
+          activeMenu: '/bpm/manager/model'
+        }
+      },
+      {
+        path: 'process-instance/detail',
+        component: () => import('@/views/bpm/processInstance/detail/index.vue'),
+        name: 'BpmProcessInstanceDetail',
+        meta: {
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          title: '流程详情',
+          activeMenu: '/bpm/task/my'
+        },
+        props: (route) => ({
+          id: route.query.id,
+          taskId: route.query.taskId,
+          activityId: route.query.activityId
+        })
+      },
+      {
+        path: 'process-instance/report',
+        component: () => import('@/views/bpm/processInstance/report/index.vue'),
+        name: 'BpmProcessInstanceReport',
+        meta: {
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          title: '数据报表',
+          activeMenu: '/bpm/manager/model'
+        }
+      },
+      {
+        path: 'oa/leave/create',
+        component: () => import('@/views/bpm/oa/leave/create.vue'),
+        name: 'OALeaveCreate',
+        meta: {
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          title: '发起 OA 请假',
+          activeMenu: '/bpm/oa/leave'
+        }
+      },
+      {
+        path: 'oa/leave/detail',
+        component: () => import('@/views/bpm/oa/leave/detail.vue'),
+        name: 'OALeaveDetail',
+        meta: {
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          title: '查看 OA 请假',
+          activeMenu: '/bpm/oa/leave'
+        }
+      },
+      {
+        path: 'manager/model/create',
+        component: () => import('@/views/bpm/model/form/index.vue'),
+        name: 'BpmModelCreate',
+        meta: {
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          title: '创建流程',
+          activeMenu: '/bpm/manager/model'
+        }
+      },
+      {
+        path: 'manager/model/:type/:id',
+        component: () => import('@/views/bpm/model/form/index.vue'),
+        name: 'BpmModelUpdate',
+        meta: {
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          title: '修改流程',
+          activeMenu: '/bpm/manager/model'
+        }
+      }
+    ]
+  },
 ]
 
 export default remainingRouter

+ 10 - 20
src/types/auto-components.d.ts

@@ -13,6 +13,7 @@ declare module 'vue' {
     BoundaryEventTimer: typeof import('./../components/bpmnProcessDesigner/package/penal/custom-config/components/BoundaryEventTimer.vue')['default']
     CallActivity: typeof import('./../components/bpmnProcessDesigner/package/penal/task/task-components/CallActivity.vue')['default']
     CardTitle: typeof import('./../components/Card/src/CardTitle.vue')['default']
+    Category: typeof import('./../api/bpm/category/index.ts')['default']
     ChildProcessNode: typeof import('./../components/SimpleProcessDesignerV2/src/nodes/ChildProcessNode.vue')['default']
     ChildProcessNodeConfig: typeof import('./../components/SimpleProcessDesignerV2/src/nodes-config/ChildProcessNodeConfig.vue')['default']
     ColorInput: typeof import('./../components/ColorInput/index.vue')['default']
@@ -32,6 +33,7 @@ declare module 'vue' {
     Crontab: typeof import('./../components/Crontab/src/Crontab.vue')['default']
     Cropper: typeof import('./../components/Cropper/src/Cropper.vue')['default']
     CropperAvatar: typeof import('./../components/Cropper/src/CropperAvatar.vue')['default']
+    Definition: typeof import('./../api/bpm/definition/index.ts')['default']
     DelayTimerNode: typeof import('./../components/SimpleProcessDesignerV2/src/nodes/DelayTimerNode.vue')['default']
     DelayTimerNodeConfig: typeof import('./../components/SimpleProcessDesignerV2/src/nodes-config/DelayTimerNodeConfig.vue')['default']
     DeptSelectForm: typeof import('./../components/DeptSelectForm/index.vue')['default']
@@ -45,21 +47,15 @@ declare module 'vue' {
     Draggable: typeof import('./../components/Draggable/index.vue')['default']
     Echart: typeof import('./../components/Echart/src/Echart.vue')['default']
     Editor: typeof import('./../components/Editor/src/Editor.vue')['default']
-    ElAside: typeof import('element-plus/es')['ElAside']
-    ElAutoResizer: typeof import('element-plus/es')['ElAutoResizer']
     ElAvatar: typeof import('element-plus/es')['ElAvatar']
     ElBadge: typeof import('element-plus/es')['ElBadge']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElCard: typeof import('element-plus/es')['ElCard']
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
-    ElCheckboxButton: typeof import('element-plus/es')['ElCheckboxButton']
     ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElCol: typeof import('element-plus/es')['ElCol']
     ElCollapseTransition: typeof import('element-plus/es')['ElCollapseTransition']
-    ElContainer: typeof import('element-plus/es')['ElContainer']
     ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
-    ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
-    ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
     ElDialog: typeof import('element-plus/es')['ElDialog']
     ElDivider: typeof import('element-plus/es')['ElDivider']
     ElDrawer: typeof import('element-plus/es')['ElDrawer']
@@ -74,45 +70,31 @@ declare module 'vue' {
     ElementOtherConfig: typeof import('./../components/bpmnProcessDesigner/package/penal/other/ElementOtherConfig.vue')['default']
     ElementProperties: typeof import('./../components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue')['default']
     ElementTask: typeof import('./../components/bpmnProcessDesigner/package/penal/task/ElementTask.vue')['default']
-    ElEmpty: typeof import('element-plus/es')['ElEmpty']
-    ElFooter: typeof import('element-plus/es')['ElFooter']
     ElForm: typeof import('element-plus/es')['ElForm']
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
-    ElHeader: typeof import('element-plus/es')['ElHeader']
     ElIcon: typeof import('element-plus/es')['ElIcon']
     ElImage: typeof import('element-plus/es')['ElImage']
     ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
     ElInput: typeof import('element-plus/es')['ElInput']
     ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
     ElLink: typeof import('element-plus/es')['ElLink']
-    ElMain: typeof import('element-plus/es')['ElMain']
     ElOption: typeof import('element-plus/es')['ElOption']
     ElPagination: typeof import('element-plus/es')['ElPagination']
     ElPopover: typeof import('element-plus/es')['ElPopover']
-    ElProgress: typeof import('element-plus/es')['ElProgress']
     ElRadio: typeof import('element-plus/es')['ElRadio']
-    ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElRow: typeof import('element-plus/es')['ElRow']
     ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
-    ElSegmented: typeof import('element-plus/es')['ElSegmented']
     ElSelect: typeof import('element-plus/es')['ElSelect']
     ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
-    ElSlider: typeof import('element-plus/es')['ElSlider']
-    ElSpace: typeof import('element-plus/es')['ElSpace']
     ElSwitch: typeof import('element-plus/es')['ElSwitch']
     ElTable: typeof import('element-plus/es')['ElTable']
     ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
-    ElTableV2: typeof import('element-plus/es')['ElTableV2']
     ElTabPane: typeof import('element-plus/es')['ElTabPane']
     ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTag: typeof import('element-plus/es')['ElTag']
     ElText: typeof import('element-plus/es')['ElText']
-    ElTimeline: typeof import('element-plus/es')['ElTimeline']
-    ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
-    ElTree: typeof import('element-plus/es')['ElTree']
-    ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
     ElUpload: typeof import('element-plus/es')['ElUpload']
     EndEventNode: typeof import('./../components/SimpleProcessDesignerV2/src/nodes/EndEventNode.vue')['default']
     Error: typeof import('./../components/Error/src/Error.vue')['default']
@@ -131,14 +113,19 @@ declare module 'vue' {
     InputPassword: typeof import('./../components/InputPassword/src/InputPassword.vue')['default']
     InputWithColor: typeof import('./../components/InputWithColor/index.vue')['default']
     LazyJsonViewer: typeof import('./../components/Json/LazyJsonViewer.vue')['default']
+    Leave: typeof import('./../api/bpm/leave/index.ts')['default']
     MagicCubeEditor: typeof import('./../components/MagicCubeEditor/index.vue')['default']
     MarkdownView: typeof import('./../components/MarkdownView/index.vue')['default']
+    Model: typeof import('./../api/bpm/model/index.ts')['default']
     NodeHandler: typeof import('./../components/SimpleProcessDesignerV2/src/NodeHandler.vue')['default']
     OperateLogV2: typeof import('./../components/OperateLogV2/src/OperateLogV2.vue')['default']
     Pagination: typeof import('./../components/Pagination/index.vue')['default']
     ParallelNode: typeof import('./../components/SimpleProcessDesignerV2/src/nodes/ParallelNode.vue')['default']
     ProcessDesigner: typeof import('./../components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue')['default']
+    ProcessExpression: typeof import('./../api/bpm/processExpression/index.ts')['default']
     ProcessExpressionDialog: typeof import('./../components/bpmnProcessDesigner/package/penal/task/task-components/ProcessExpressionDialog.vue')['default']
+    ProcessInstance: typeof import('./../api/bpm/processInstance/index.ts')['default']
+    ProcessListener: typeof import('./../api/bpm/processListener/index.ts')['default']
     ProcessListenerDialog: typeof import('./../components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue')['default']
     ProcessNodeTree: typeof import('./../components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue')['default']
     ProcessPalette: typeof import('./../components/bpmnProcessDesigner/package/palette/ProcessPalette.vue')['default']
@@ -156,6 +143,7 @@ declare module 'vue' {
     ServiceTask: typeof import('./../components/bpmnProcessDesigner/package/penal/task/task-components/ServiceTask.vue')['default']
     ShortcutDateRangePicker: typeof import('./../components/ShortcutDateRangePicker/index.vue')['default']
     SignalAndMessage: typeof import('./../components/bpmnProcessDesigner/package/penal/signal-message/SignalAndMessage.vue')['default']
+    Simple: typeof import('./../api/bpm/simple/index.ts')['default']
     SimpleProcessDesigner: typeof import('./../components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue')['default']
     SimpleProcessModel: typeof import('./../components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue')['default']
     SimpleProcessViewer: typeof import('./../components/SimpleProcessDesignerV2/src/SimpleProcessViewer.vue')['default']
@@ -165,6 +153,7 @@ declare module 'vue' {
     SummaryCard: typeof import('./../components/SummaryCard/index.vue')['default']
     Table: typeof import('./../components/Table/src/Table.vue')['default']
     TableSelectForm: typeof import('./../components/Table/src/TableSelectForm.vue')['default']
+    Task: typeof import('./../api/bpm/task/index.ts')['default']
     Tinyflow: typeof import('./../components/Tinyflow/Tinyflow.vue')['default']
     Tooltip: typeof import('./../components/Tooltip/src/Tooltip.vue')['default']
     TriggerNode: typeof import('./../components/SimpleProcessDesignerV2/src/nodes/TriggerNode.vue')['default']
@@ -175,6 +164,7 @@ declare module 'vue' {
     UploadModel: typeof import('./../components/UploadFile/src/UploadModel.vue')['default']
     UploadMusic: typeof import('./../components/UploadFile/src/UploadMusic.vue')['default']
     UploadVideo: typeof import('./../components/UploadFile/src/UploadVideo.vue')['default']
+    UserGroup: typeof import('./../api/bpm/userGroup/index.ts')['default']
     UserSelectForm: typeof import('./../components/UserSelectForm/index.vue')['default']
     UserTask: typeof import('./../components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue')['default']
     UserTaskCustomConfig: typeof import('./../components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue')['default']

+ 4 - 0
src/utils/dict.ts

@@ -143,8 +143,12 @@ export enum DICT_TYPE {
   INFRA_OPERATE_TYPE = 'infra_operate_type',
 
   // ========== BPM 模块 ==========
+  BPM_MODEL_TYPE = 'bpm_model_type',
+  BPM_MODEL_FORM_TYPE = 'bpm_model_form_type',
+  BPM_TASK_CANDIDATE_STRATEGY = 'bpm_task_candidate_strategy',
   BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status',
   BPM_TASK_STATUS = 'bpm_task_status',
+  BPM_OA_LEAVE_TYPE = 'bpm_oa_leave_type',
   BPM_PROCESS_LISTENER_TYPE = 'bpm_process_listener_type',
   BPM_PROCESS_LISTENER_VALUE_TYPE = 'bpm_process_listener_value_type',
 

+ 130 - 0
src/views/bpm/category/CategoryForm.vue

@@ -0,0 +1,130 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="分类名" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入分类名" />
+      </el-form-item>
+      <el-form-item label="分类标志" prop="code">
+        <el-input v-model="formData.code" placeholder="请输入分类标志" />
+      </el-form-item>
+      <el-form-item label="分类描述" prop="description">
+        <el-input v-model="formData.description" type="textarea" placeholder="请输入分类描述" />
+      </el-form-item>
+      <el-form-item label="分类状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :value="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="分类排序" prop="sort">
+        <el-input-number
+          v-model="formData.sort"
+          placeholder="请输入分类排序"
+          class="!w-1/1"
+          :precision="0"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** BPM 流程分类 表单 */
+defineOptions({ name: 'CategoryForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  code: undefined,
+  description: undefined,
+  status: CommonStatusEnum.ENABLE,
+  sort: undefined
+})
+const formRules = reactive({
+  name: [{ required: true, message: '分类名不能为空', trigger: 'blur' }],
+  code: [{ required: true, message: '分类标志不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '分类状态不能为空', trigger: 'blur' }],
+  sort: [{ required: true, message: '分类排序不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await CategoryApi.getCategory(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as CategoryVO
+    if (formType.value === 'create') {
+      await CategoryApi.createCategory(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await CategoryApi.updateCategory(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    code: undefined,
+    description: undefined,
+    status: CommonStatusEnum.ENABLE,
+    sort: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 198 - 0
src/views/bpm/category/index.vue

@@ -0,0 +1,198 @@
+<template>
+
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="分类名" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入分类名"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="分类标志" prop="code">
+        <el-input
+          v-model="queryParams.code"
+          placeholder="请输入分类标志"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="分类状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择分类状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['bpm:category:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="分类编号" align="center" prop="id" />
+      <el-table-column label="分类名" align="center" prop="name" />
+      <el-table-column label="分类标志" align="center" prop="code" />
+      <el-table-column label="分类描述" align="center" prop="description" />
+      <el-table-column label="分类状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column label="分类排序" align="center" prop="sort" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['bpm:category:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['bpm:category:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <CategoryForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+import CategoryForm from './CategoryForm.vue'
+
+/** BPM 流程分类 列表 */
+defineOptions({ name: 'BpmCategory' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<CategoryVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  code: undefined,
+  status: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await CategoryApi.getCategoryPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await CategoryApi.deleteCategory(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 174 - 0
src/views/bpm/form/editor/index.vue

@@ -0,0 +1,174 @@
+<template>
+  <ContentWrap :body-style="{ padding: '0px' }" class="!mb-0">
+    <!-- 表单设计器 -->
+    <div
+      class="h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-2px)]"
+    >
+      <fc-designer class="my-designer" ref="designer" :config="designerConfig">
+        <template #handle>
+          <el-button size="small" type="success" plain @click="handleSave">
+            <Icon class="mr-5px" icon="ep:plus" />
+            保存
+          </el-button>
+        </template>
+      </fc-designer>
+    </div>
+  </ContentWrap>
+
+  <!-- 表单保存的弹窗 -->
+  <Dialog v-model="dialogVisible" title="保存表单" width="600">
+    <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
+      <el-form-item label="表单名" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入表单名" />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :value="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="备注" prop="remark">
+        <el-input v-model="formData.remark" placeholder="请输入备注" type="textarea" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+import * as FormApi from '@/api/bpm/form'
+import FcDesigner from '@form-create/designer'
+import { encodeConf, encodeFields, setConfAndFields } from '@/utils/formCreate'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { useFormCreateDesigner } from '@/components/FormCreate'
+import { useRoute } from 'vue-router'
+
+defineOptions({ name: 'BpmFormEditor' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息
+const route = useRoute() // 路由
+const { push, currentRoute } = useRouter() // 路由
+const { query } = useRoute() // 路由信息
+const { delView } = useTagsViewStore() // 视图操作
+
+// 表单设计器配置
+const designerConfig = ref({
+  switchType: [], // 是否可以切换组件类型,或者可以相互切换的字段
+  autoActive: true, // 是否自动选中拖入的组件
+  useTemplate: false, // 是否生成vue2语法的模板组件
+  formOptions: {
+    form: {
+      labelWidth: '100px' // 设置默认的 label 宽度为 100px
+    }
+  }, // 定义表单配置默认值
+  fieldReadonly: false, // 配置field是否可以编辑
+  hiddenDragMenu: false, // 隐藏拖拽操作按钮
+  hiddenDragBtn: false, // 隐藏拖拽按钮
+  hiddenMenu: [], // 隐藏部分菜单
+  hiddenItem: [], // 隐藏部分组件
+  hiddenItemConfig: {}, // 隐藏组件的部分配置项
+  disabledItemConfig: {}, // 禁用组件的部分配置项
+  showSaveBtn: false, // 是否显示保存按钮
+  showConfig: true, // 是否显示右侧的配置界面
+  showBaseForm: true, // 是否显示组件的基础配置表单
+  showControl: true, // 是否显示组件联动
+  showPropsForm: true, // 是否显示组件的属性配置表单
+  showEventForm: true, // 是否显示组件的事件配置表单
+  showValidateForm: true, // 是否显示组件的验证配置表单
+  showFormConfig: true, // 是否显示表单配置
+  showInputData: true, // 是否显示录入按钮
+  showDevice: true, // 是否显示多端适配选项
+  appendConfigData: [] // 定义渲染规则所需的formData
+})
+const designer = ref() // 表单设计器
+useFormCreateDesigner(designer) // 表单设计器增强
+const dialogVisible = ref(false) // 弹窗是否展示
+const formLoading = ref(false) // 表单的加载中:提交的按钮禁用
+const formData = ref({
+  name: '',
+  status: CommonStatusEnum.ENABLE,
+  remark: ''
+})
+const formRules = reactive({
+  name: [{ required: true, message: '表单名不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '开启状态不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 处理保存按钮 */
+const handleSave = () => {
+  dialogVisible.value = true
+}
+
+/** 提交表单 */
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as FormApi.FormVO
+    data.conf = encodeConf(designer) // 表单配置
+    data.fields = encodeFields(designer) // 表单字段
+    if (!data.id) {
+      await FormApi.createForm(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await FormApi.updateForm(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    close()
+  } finally {
+    formLoading.value = false
+  }
+}
+/** 关闭按钮 */
+const close = () => {
+  delView(unref(currentRoute))
+  push('/bpm/manager/form')
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  // 场景一:新增表单
+  const id = query.id as unknown as number
+  if (!id) {
+    return
+  }
+  // 场景二:修改表单
+  const data = await FormApi.getForm(id)
+  formData.value = data
+  setConfAndFields(designer, data.conf, data.fields)
+
+  if (route.query.type !== 'copy') {
+    return
+  }
+  // 场景三: 复制表单
+  const { id: foo, ...copied } = data
+  formData.value = copied
+  formData.value.name += '_copy'
+})
+</script>
+
+<style>
+.my-designer {
+  ._fc-l,
+  ._fc-m,
+  ._fc-r {
+    border-top: none;
+  }
+}
+</style>

+ 204 - 0
src/views/bpm/form/index.vue

@@ -0,0 +1,204 @@
+<template>
+
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="68px"
+    >
+      <el-form-item label="表单名" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          class="!w-240px"
+          clearable
+          placeholder="请输入表单名"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon class="mr-5px" icon="ep:search" />
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon class="mr-5px" icon="ep:refresh" />
+          重置
+        </el-button>
+        <el-button v-hasPermi="['bpm:form:create']" plain type="primary" @click="openForm">
+          <Icon class="mr-5px" icon="ep:plus" />
+          新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column align="center" label="编号" prop="id" />
+      <el-table-column align="center" label="表单名" prop="name" />
+      <el-table-column align="center" label="状态" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="备注" prop="remark" />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="创建时间"
+        prop="createTime"
+      />
+      <el-table-column align="center" label="操作">
+        <template #default="scope">
+          <el-button
+            v-hasPermi="['bpm:form:update']"
+            link
+            type="primary"
+            @click="openForm('copy', scope.row.id)"
+          >
+            复制
+          </el-button>
+          <el-button
+            v-hasPermi="['bpm:form:update']"
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+          >
+            编辑
+          </el-button>
+          <el-button v-hasPermi="['bpm:form:query']" link @click="openDetail(scope.row.id)">
+            详情
+          </el-button>
+          <el-button
+            v-hasPermi="['bpm:form:delete']"
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单详情的弹窗 -->
+  <Dialog v-model="detailVisible" title="表单详情" width="800">
+    <form-create :option="detailData.option" :rule="detailData.rule" />
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as FormApi from '@/api/bpm/form'
+import { setConfAndFields2 } from '@/utils/formCreate'
+
+defineOptions({ name: 'BpmForm' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+const { currentRoute, push } = useRouter() // 路由
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: null
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await FormApi.getFormPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const openForm = (type: string, id?: number) => {
+  const toRouter: { name: string; query: { type: string; id?: number } } = {
+    name: 'BpmFormEditor',
+    query: {
+      type
+    }
+  }
+  console.log(typeof id)
+  // 表单新建的时候id传的是event需要排除
+  if (typeof id === 'number' || typeof id === 'string') {
+    toRouter.query.id = id
+  }
+  push(toRouter)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await FormApi.deleteForm(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 详情操作 */
+const detailVisible = ref(false)
+const detailData = ref({
+  rule: [],
+  option: {}
+})
+const openDetail = async (rowId: number) => {
+  // 设置表单
+  const data = await FormApi.getForm(rowId)
+  setConfAndFields2(detailData, data.conf, data.fields)
+  // 弹窗打开
+  detailVisible.value = true
+}
+/**表单保存返回后重新加载列表 */
+watch(
+  () => currentRoute.value,
+  () => {
+    getList()
+  },
+  {
+    immediate: true
+  }
+)
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 132 - 0
src/views/bpm/group/UserGroupForm.vue

@@ -0,0 +1,132 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+    >
+      <el-form-item label="组名" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入组名" />
+      </el-form-item>
+      <el-form-item label="描述">
+        <el-input v-model="formData.description" placeholder="请输入描述" type="textarea" />
+      </el-form-item>
+      <el-form-item label="成员" prop="userIds">
+        <el-select v-model="formData.userIds" multiple placeholder="请选择成员">
+          <el-option
+            v-for="user in userList"
+            :key="user.id"
+            :label="user.nickname"
+            :value="user.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :value="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+import * as UserGroupApi from '@/api/bpm/userGroup'
+import * as UserApi from '@/api/system/user'
+
+defineOptions({ name: 'UserGroupForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  description: undefined,
+  userIds: undefined,
+  status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive({
+  name: [{ required: true, message: '组名不能为空', trigger: 'blur' }],
+  description: [{ required: true, message: '描述不能为空', trigger: 'blur' }],
+  userIds: [{ required: true, message: '成员不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+const userList = ref<any[]>([]) // 用户列表
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await UserGroupApi.getUserGroup(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+  // 加载用户列表
+  userList.value = await UserApi.getSimpleUserList()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as UserGroupApi.UserGroupVO
+    if (formType.value === 'create') {
+      await UserGroupApi.createUserGroup(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await UserGroupApi.updateUserGroup(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    description: undefined,
+    userIds: undefined,
+    status: CommonStatusEnum.ENABLE
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 190 - 0
src/views/bpm/group/index.vue

@@ -0,0 +1,190 @@
+<template>
+
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="组名" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入组名"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['bpm:user-group:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="组名" align="center" prop="name" />
+      <el-table-column label="描述" align="center" prop="description" />
+      <el-table-column label="成员" align="center">
+        <template #default="scope">
+          <span v-for="userId in scope.row.userIds" :key="userId" class="pr-5px">
+            {{ userList.find((user) => user.id === userId)?.nickname }}
+          </span>
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['bpm:user-group:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['bpm:user-group:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <UserGroupForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as UserGroupApi from '@/api/bpm/userGroup'
+import * as UserApi from '@/api/system/user'
+import UserGroupForm from './UserGroupForm.vue'
+import { UserVO } from '@/api/system/user'
+
+defineOptions({ name: 'BpmUserGroup' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: null,
+  status: null,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const userList = ref<UserVO[]>([]) // 用户列表
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await UserGroupApi.getUserGroupPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await UserGroupApi.deleteUserGroup(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+  // 加载用户列表
+  userList.value = await UserApi.getSimpleUserList()
+})
+</script>

+ 663 - 0
src/views/bpm/model/CategoryDraggableModel.vue

@@ -0,0 +1,663 @@
+<template>
+  <div class="flex items-center h-50px" v-memo="[categoryInfo.name, isCategorySorting]">
+    <!-- 头部:分类名 -->
+    <div class="flex items-center">
+      <el-tooltip content="拖动排序" v-if="isCategorySorting">
+        <Icon
+          :size="22"
+          icon="ic:round-drag-indicator"
+          class="ml-10px category-drag-icon cursor-move text-#8a909c"
+        />
+      </el-tooltip>
+      <h3 class="ml-20px mr-8px text-18px">{{ categoryInfo.name }}</h3>
+      <div class="color-gray-600 text-16px"> ({{ categoryInfo.modelList?.length || 0 }}) </div>
+    </div>
+    <!-- 头部:操作 -->
+    <div class="flex-1 flex" v-show="!isCategorySorting">
+      <div
+        v-if="categoryInfo.modelList.length > 0"
+        class="ml-20px flex items-center"
+        :class="[
+          'transition-transform duration-300 cursor-pointer',
+          isExpand ? 'rotate-180' : 'rotate-0'
+        ]"
+        @click="isExpand = !isExpand"
+      >
+        <Icon icon="ep:arrow-down-bold" color="#999" />
+      </div>
+      <div class="ml-auto flex items-center" :class="isModelSorting ? 'mr-15px' : 'mr-45px'">
+        <template v-if="!isModelSorting">
+          <el-button
+            v-if="categoryInfo.modelList.length > 0"
+            link
+            type="info"
+            class="mr-20px"
+            @click.stop="handleModelSort"
+          >
+            <Icon icon="fa:sort-amount-desc" class="mr-5px" />
+            排序
+          </el-button>
+          <el-button v-else link type="info" class="mr-20px" @click.stop="openModelForm('create')">
+            <Icon icon="fa:plus" class="mr-5px" />
+            新建
+          </el-button>
+          <el-dropdown
+            @command="(command) => handleCategoryCommand(command, categoryInfo)"
+            placement="bottom"
+          >
+            <el-button link type="info">
+              <Icon icon="ep:setting" class="mr-5px" />
+              分类
+            </el-button>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item command="handleRename"> 重命名 </el-dropdown-item>
+                <el-dropdown-item command="handleDeleteCategory"> 删除该类 </el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+        </template>
+        <template v-else>
+          <el-button @click.stop="handleModelSortCancel"> 取 消 </el-button>
+          <el-button type="primary" @click.stop="handleModelSortSubmit"> 保存排序 </el-button>
+        </template>
+      </div>
+    </div>
+  </div>
+
+  <!-- 模型列表 -->
+  <el-collapse-transition>
+    <div v-show="isExpand">
+      <el-table
+        v-if="modelList && modelList.length > 0"
+        :class="categoryInfo.name"
+        ref="tableRef"
+        :data="modelList"
+        row-key="id"
+        :header-cell-style="tableHeaderStyle"
+        :cell-style="tableCellStyle"
+        :row-style="{ height: '68px' }"
+      >
+        <el-table-column label="流程名" prop="name" min-width="150">
+          <template #default="{ row }">
+            <div class="flex items-center">
+              <el-tooltip content="拖动排序" v-if="isModelSorting">
+                <Icon
+                  icon="ic:round-drag-indicator"
+                  class="drag-icon cursor-move text-#8a909c mr-10px"
+                />
+              </el-tooltip>
+              <el-image v-if="row.icon" :src="row.icon" class="h-38px w-38px mr-10px rounded" />
+              <div v-else class="flow-icon">
+                <span style="font-size: 12px; color: #fff">{{ subString(row.name, 0, 2) }}</span>
+              </div>
+              {{ row.name }}
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="可见范围" prop="startUserIds" min-width="150">
+          <template #default="{ row }">
+            <el-text v-if="!row.startUsers?.length && !row.startDepts?.length"> 全部可见 </el-text>
+            <el-text v-else-if="row.startUsers.length === 1">
+              {{ row.startUsers[0].nickname }}
+            </el-text>
+            <el-text v-else-if="row.startDepts?.length === 1">
+              {{ row.startDepts[0].name }}
+            </el-text>
+            <el-text v-else-if="row.startDepts?.length > 1">
+              <el-tooltip
+                class="box-item"
+                effect="dark"
+                placement="top"
+                :content="row.startDepts.map((dept: any) => dept.name).join('、')"
+              >
+                {{ row.startDepts[0].name }}等 {{ row.startDepts.length }} 个部门可见
+              </el-tooltip>
+            </el-text>
+            <el-text v-else>
+              <el-tooltip
+                class="box-item"
+                effect="dark"
+                placement="top"
+                :content="row.startUsers.map((user: any) => user.nickname).join('、')"
+              >
+                {{ row.startUsers[0].nickname }}等 {{ row.startUsers.length }} 人可见
+              </el-tooltip>
+            </el-text>
+          </template>
+        </el-table-column>
+        <el-table-column label="流程类型" prop="type" min-width="120">
+          <template #default="{ row }">
+            <dict-tag :value="row.type" :type="DICT_TYPE.BPM_MODEL_TYPE" />
+          </template>
+        </el-table-column>
+        <el-table-column label="表单信息" prop="formType" min-width="150">
+          <template #default="scope">
+            <el-button
+              v-if="scope.row.formType === BpmModelFormType.NORMAL"
+              type="primary"
+              link
+              @click="handleFormDetail(scope.row)"
+            >
+              <span>{{ scope.row.formName }}</span>
+            </el-button>
+            <el-button
+              v-else-if="scope.row.formType === BpmModelFormType.CUSTOM"
+              type="primary"
+              link
+              @click="handleFormDetail(scope.row)"
+            >
+              <span>{{ scope.row.formCustomCreatePath }}</span>
+            </el-button>
+            <label v-else>暂无表单</label>
+          </template>
+        </el-table-column>
+        <el-table-column label="最后发布" prop="deploymentTime" min-width="250">
+          <template #default="scope">
+            <div class="flex items-center">
+              <span v-if="scope.row.processDefinition" class="w-150px">
+                {{ formatDate(scope.row.processDefinition.deploymentTime) }}
+              </span>
+              <el-tag v-if="scope.row.processDefinition">
+                v{{ scope.row.processDefinition.version }}
+              </el-tag>
+              <el-tag v-else type="warning">未部署</el-tag>
+              <el-tag
+                v-if="scope.row.processDefinition?.suspensionState === 2"
+                type="warning"
+                class="ml-10px"
+              >
+                已停用
+              </el-tag>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="200" fixed="right">
+          <template #default="scope">
+            <el-button
+              link
+              type="primary"
+              @click="openModelForm('update', scope.row.id)"
+              v-if="hasPermiUpdate"
+              :disabled="!isManagerUser(scope.row)"
+            >
+              修改
+            </el-button>
+            <el-button
+              link
+              type="primary"
+              @click="openModelForm('copy', scope.row.id)"
+              v-if="hasPermiUpdate"
+              :disabled="!isManagerUser(scope.row)"
+            >
+              复制
+            </el-button>
+            <el-button
+              link
+              class="!ml-5px"
+              type="primary"
+              @click="handleDeploy(scope.row)"
+              v-if="hasPermiDeploy"
+              :disabled="!isManagerUser(scope.row)"
+            >
+              发布
+            </el-button>
+            <el-dropdown
+              class="!align-middle ml-5px"
+              @command="(command) => handleModelCommand(command, scope.row)"
+              v-if="hasPermiMore"
+            >
+              <el-button type="primary" link>更多</el-button>
+              <template #dropdown>
+                <el-dropdown-menu>
+                  <el-dropdown-item command="handleDefinitionList" v-if="hasPermiPdQuery">
+                    历史
+                  </el-dropdown-item>
+                  <el-dropdown-item
+                    command="handleReport"
+                    v-if="
+                      checkPermi(['bpm:process-instance:manager-query']) &&
+                      scope.row.processDefinition
+                    "
+                    :disabled="!isManagerUser(scope.row)"
+                  >
+                    报表
+                  </el-dropdown-item>
+                  <el-dropdown-item
+                    command="handleChangeState"
+                    v-if="hasPermiUpdate && scope.row.processDefinition"
+                    :disabled="!isManagerUser(scope.row)"
+                  >
+                    {{ scope.row.processDefinition.suspensionState === 1 ? '停用' : '启用' }}
+                  </el-dropdown-item>
+                  <el-dropdown-item
+                    type="danger"
+                    command="handleClean"
+                    v-if="checkPermi(['bpm:model:clean'])"
+                    :disabled="!isManagerUser(scope.row)"
+                  >
+                    清理
+                  </el-dropdown-item>
+                  <el-dropdown-item
+                    type="danger"
+                    command="handleDelete"
+                    v-if="hasPermiDelete"
+                    :disabled="!isManagerUser(scope.row)"
+                  >
+                    删除
+                  </el-dropdown-item>
+                </el-dropdown-menu>
+              </template>
+            </el-dropdown>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+  </el-collapse-transition>
+
+  <!-- 弹窗:重命名分类 -->
+  <Dialog :fullscreen="false" class="rename-dialog" v-model="renameCategoryVisible" width="400">
+    <template #title>
+      <div class="pl-10px font-bold text-18px"> 重命名分类 </div>
+    </template>
+    <div class="px-30px">
+      <el-input v-model="renameCategoryForm.name" />
+    </div>
+    <template #footer>
+      <div class="pr-25px pb-25px">
+        <el-button @click="renameCategoryVisible = false">取 消</el-button>
+        <el-button type="primary" @click="handleRenameConfirm">确 定</el-button>
+      </div>
+    </template>
+  </Dialog>
+
+  <!-- 弹窗:表单详情 -->
+  <Dialog title="表单详情" :fullscreen="true" v-model="formDetailVisible">
+    <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" />
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+import Sortable from 'sortablejs'
+import { formatDate } from '@/utils/formatTime'
+import * as ModelApi from '@/api/bpm/model'
+import * as FormApi from '@/api/bpm/form'
+import { setConfAndFields2 } from '@/utils/formCreate'
+import { BpmModelFormType } from '@/utils/constants'
+import { checkPermi } from '@/utils/permission'
+import { useUserStoreWithOut } from '@/store/modules/user'
+import { useAppStore } from '@/store/modules/app'
+import { cloneDeep, isEqual } from 'lodash-es'
+import { useDebounceFn } from '@vueuse/core'
+import { subString } from '@/utils/index'
+
+defineOptions({ name: 'BpmModel' })
+
+// 优化 Props 类型定义
+interface UserInfo {
+  nickname: string
+  [key: string]: any
+}
+
+interface ProcessDefinition {
+  deploymentTime: string
+  version: number
+  suspensionState: number
+}
+
+interface ModelInfo {
+  id: number
+  name: string
+  icon?: string
+  startUsers?: UserInfo[]
+  processDefinition?: ProcessDefinition
+  formType?: number
+  formId?: number
+  formName?: string
+  formCustomCreatePath?: string
+  managerUserIds?: number[]
+  [key: string]: any
+}
+
+interface CategoryInfoProps {
+  id: number
+  name: string
+  modelList: ModelInfo[]
+}
+
+const props = defineProps<{
+  categoryInfo: CategoryInfoProps
+  isCategorySorting: boolean
+}>()
+
+const emit = defineEmits(['success'])
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+const { push } = useRouter() // 路由
+const userStore = useUserStoreWithOut() // 用户信息缓存
+const isDark = computed(() => useAppStore().getIsDark) // 是否黑暗模式
+const router = useRouter() // 路由
+
+const isModelSorting = ref(false) // 是否正处于排序状态
+const originalData = ref<ModelInfo[]>([]) // 原始数据
+const modelList = ref<ModelInfo[]>([]) // 模型列表
+const isExpand = ref(false) // 是否处于展开状态
+
+// 使用 computed 优化表格样式计算
+const tableHeaderStyle = computed(() => ({
+  backgroundColor: isDark.value ? '' : '#edeff0',
+  paddingLeft: '10px'
+}))
+
+const tableCellStyle = computed(() => ({
+  paddingLeft: '10px'
+}))
+
+/** 权限校验:通过 computed 解决列表的卡顿问题 */
+const hasPermiUpdate = computed(() => {
+  return checkPermi(['bpm:model:update'])
+})
+const hasPermiDelete = computed(() => {
+  return checkPermi(['bpm:model:delete'])
+})
+const hasPermiDeploy = computed(() => {
+  return checkPermi(['bpm:model:deploy'])
+})
+const hasPermiMore = computed(() => {
+  return checkPermi(['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete'])
+})
+const hasPermiPdQuery = computed(() => {
+  return checkPermi(['bpm:process-definition:query'])
+})
+
+/** '更多'操作按钮 */
+const handleModelCommand = (command: string, row: any) => {
+  switch (command) {
+    case 'handleDefinitionList':
+      handleDefinitionList(row)
+      break
+    case 'handleDelete':
+      handleDelete(row)
+      break
+    case 'handleChangeState':
+      handleChangeState(row)
+      break
+    case 'handleClean':
+      handleClean(row)
+      break
+    case 'handleReport':
+      router.push({
+        name: 'BpmProcessInstanceReport',
+        query: {
+          processDefinitionId: row.processDefinition.id,
+          processDefinitionKey: row.key
+        }
+      })
+      break
+    default:
+      break
+  }
+}
+
+/** '分类'操作按钮 */
+const handleCategoryCommand = async (command: string, row: any) => {
+  switch (command) {
+    case 'handleRename':
+      renameCategoryForm.value = await CategoryApi.getCategory(row.id)
+      renameCategoryVisible.value = true
+      break
+    case 'handleDeleteCategory':
+      await handleDeleteCategory()
+      break
+    default:
+      break
+  }
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (row: any) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await ModelApi.deleteModel(row.id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    emit('success')
+  } catch {}
+}
+
+/** 清理按钮操作 */
+const handleClean = async (row: any) => {
+  try {
+    // 清理的二次确认
+    await message.confirm('是否确认清理流程名字为"' + row.name + '"的数据项?')
+    // 发起清理
+    await ModelApi.cleanModel(row.id)
+    message.success('清理成功')
+    // 刷新列表
+    emit('success')
+  } catch {}
+}
+
+/** 更新状态操作 */
+const handleChangeState = async (row: any) => {
+  const state = row.processDefinition.suspensionState
+  const newState = state === 1 ? 2 : 1
+  try {
+    // 修改状态的二次确认
+    const id = row.id
+    const statusState = state === 1 ? '停用' : '启用'
+    const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?'
+    await message.confirm(content)
+    // 发起修改状态
+    await ModelApi.updateModelState(id, newState)
+    message.success(statusState + '成功')
+    // 刷新列表
+    emit('success')
+  } catch {}
+}
+
+/** 发布流程 */
+const handleDeploy = async (row: any) => {
+  try {
+    await message.confirm('是否确认发布该流程?')
+    // 发起部署
+    await ModelApi.deployModel(row.id)
+    message.success(t('发布成功'))
+    // 刷新列表
+    emit('success')
+  } catch {}
+}
+
+/** 跳转到指定流程定义列表 */
+const handleDefinitionList = (row: any) => {
+  push({
+    name: 'BpmProcessDefinition',
+    query: {
+      key: row.key
+    }
+  })
+}
+
+/** 流程表单的详情按钮操作 */
+const formDetailVisible = ref(false)
+const formDetailPreview = ref({
+  rule: [],
+  option: {}
+})
+const handleFormDetail = async (row: any) => {
+  if (row.formType == BpmModelFormType.NORMAL) {
+    // 设置表单
+    const data = await FormApi.getForm(row.formId)
+    setConfAndFields2(formDetailPreview, data.conf, data.fields)
+    // 弹窗打开
+    formDetailVisible.value = true
+  } else {
+    await push({
+      path: row.formCustomCreatePath
+    })
+  }
+}
+
+/** 判断是否可以操作 */
+const isManagerUser = (row: any) => {
+  const userId = userStore.getUser.id
+  return row.managerUserIds && row.managerUserIds.includes(userId)
+}
+
+/** 处理模型的排序 **/
+const handleModelSort = () => {
+  // 保存初始数据
+  originalData.value = cloneDeep(props.categoryInfo.modelList)
+  isModelSorting.value = true
+  initSort()
+}
+
+/** 处理模型的排序提交 */
+const handleModelSortSubmit = async () => {
+  // 保存排序
+  const ids = modelList.value.map((item: any) => item.id)
+  await ModelApi.updateModelSortBatch(ids)
+  // 刷新列表
+  isModelSorting.value = false
+  message.success('排序模型成功')
+  emit('success')
+}
+
+/** 处理模型的排序取消 */
+const handleModelSortCancel = () => {
+  // 恢复初始数据
+  modelList.value = cloneDeep(originalData.value)
+  isModelSorting.value = false
+}
+
+/** 创建拖拽实例 */
+const tableRef = ref()
+const initSort = useDebounceFn(() => {
+  const table = document.querySelector(`.${props.categoryInfo.name} .el-table__body-wrapper tbody`)
+  if (!table) return
+
+  Sortable.create(table, {
+    group: 'shared',
+    animation: 150,
+    draggable: '.el-table__row',
+    handle: '.drag-icon',
+    onEnd: ({ newDraggableIndex, oldDraggableIndex }) => {
+      if (oldDraggableIndex !== newDraggableIndex) {
+        modelList.value.splice(
+          newDraggableIndex,
+          0,
+          modelList.value.splice(oldDraggableIndex, 1)[0]
+        )
+      }
+    }
+  })
+}, 200)
+
+/** 更新 modelList 模型列表 */
+const updateModeList = useDebounceFn(() => {
+  const newModelList = props.categoryInfo.modelList
+  if (!isEqual(modelList.value, newModelList)) {
+    modelList.value = cloneDeep(newModelList)
+    if (newModelList?.length > 0) {
+      isExpand.value = true
+    }
+  }
+}, 100)
+
+/** 重命名弹窗确定 */
+const renameCategoryVisible = ref(false)
+const renameCategoryForm = ref({
+  name: ''
+})
+const handleRenameConfirm = async () => {
+  if (renameCategoryForm.value?.name.length === 0) {
+    return message.warning('请输入名称')
+  }
+  // 发起修改
+  await CategoryApi.updateCategory(renameCategoryForm.value as CategoryVO)
+  message.success('重命名成功')
+  // 刷新列表
+  renameCategoryVisible.value = false
+  emit('success')
+}
+
+/** 删除分类 */
+const handleDeleteCategory = async () => {
+  try {
+    if (props.categoryInfo.modelList.length > 0) {
+      return message.warning('该分类下仍有流程定义,不允许删除')
+    }
+    await message.confirm('确认删除分类吗?')
+    // 发起删除
+    await CategoryApi.deleteCategory(props.categoryInfo.id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    emit('success')
+  } catch {}
+}
+
+/** 添加/修改/复制流程模型弹窗 */
+const openModelForm = async (type: string, id?: number) => {
+  if (type === 'create') {
+    await push({ name: 'BpmModelCreate' })
+  } else {
+    await push({
+      name: 'BpmModelUpdate',
+      params: { id, type }
+    })
+  }
+}
+
+watchEffect(() => {
+  if (props.categoryInfo?.modelList) {
+    updateModeList()
+  }
+
+  if (props.isCategorySorting) {
+    isExpand.value = false
+  }
+})
+</script>
+
+<style lang="scss">
+.rename-dialog.el-dialog {
+  padding: 0 !important;
+
+  .el-dialog__header {
+    border-bottom: none;
+  }
+
+  .el-dialog__footer {
+    border-top: none !important;
+  }
+}
+</style>
+<style lang="scss" scoped>
+.flow-icon {
+  display: flex;
+  width: 38px;
+  height: 38px;
+  margin-right: 10px;
+  background-color: var(--el-color-primary);
+  border-radius: 0.25rem;
+  align-items: center;
+  justify-content: center;
+}
+
+.category-draggable-model {
+  :deep(.el-table__cell) {
+    overflow: hidden;
+    border-bottom: none !important;
+  }
+
+  // 优化表格渲染性能
+  :deep(.el-table__body) {
+    will-change: transform;
+    transform: translateZ(0);
+  }
+}
+</style>

+ 173 - 0
src/views/bpm/model/definition/index.vue

@@ -0,0 +1,173 @@
+<template>
+
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="定义编号" align="center" prop="id" min-width="250" />
+      <el-table-column label="流程名称" align="center" prop="name" min-width="150" />
+      <el-table-column label="流程图标" align="center" min-width="50">
+        <template #default="{ row }">
+          <el-image v-if="row.icon" :src="row.icon" class="h-24px w-24pxrounded" />
+        </template>
+      </el-table-column>
+      <el-table-column label="可见范围" prop="startUserIds" min-width="100">
+        <template #default="{ row }">
+          <el-text v-if="!row.startUsers?.length"> 全部可见 </el-text>
+          <el-text v-else-if="row.startUsers.length === 1">
+            {{ row.startUsers[0].nickname }}
+          </el-text>
+          <el-text v-else>
+            <el-tooltip
+              class="box-item"
+              effect="dark"
+              placement="top"
+              :content="row.startUsers.map((user: any) => user.nickname).join('、')"
+            >
+              {{ row.startUsers[0].nickname }}等 {{ row.startUsers.length }} 人可见
+            </el-tooltip>
+          </el-text>
+        </template>
+      </el-table-column>
+      <el-table-column label="流程类型" prop="modelType" min-width="120">
+        <template #default="{ row }">
+          <dict-tag :value="row.modelType" :type="DICT_TYPE.BPM_MODEL_TYPE" />
+        </template>
+      </el-table-column>
+      <el-table-column label="表单信息" prop="formType" min-width="150">
+        <template #default="scope">
+          <el-button
+            v-if="scope.row.formType === BpmModelFormType.NORMAL"
+            type="primary"
+            link
+            @click="handleFormDetail(scope.row)"
+          >
+            <span>{{ scope.row.formName }}</span>
+          </el-button>
+          <el-button
+            v-else-if="scope.row.formType === BpmModelFormType.CUSTOM"
+            type="primary"
+            link
+            @click="handleFormDetail(scope.row)"
+          >
+            <span>{{ scope.row.formCustomCreatePath }}</span>
+          </el-button>
+          <label v-else>暂无表单</label>
+        </template>
+      </el-table-column>
+      <el-table-column label="流程版本" align="center" min-width="80">
+        <template #default="scope">
+          <el-tag>v{{ scope.row.version }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="部署时间"
+        align="center"
+        prop="deploymentTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openModelForm(scope.row.id)"
+            v-hasPermi="['bpm:model:update']"
+          >
+            恢复
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 弹窗:表单详情 -->
+  <Dialog title="表单详情" v-model="formDetailVisible" width="800">
+    <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" />
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as DefinitionApi from '@/api/bpm/definition'
+import { setConfAndFields2 } from '@/utils/formCreate'
+import { DICT_TYPE } from '@/utils/dict'
+import { BpmModelFormType } from '@/utils/constants'
+
+defineOptions({ name: 'BpmProcessDefinition' })
+
+const { push } = useRouter() // 路由
+const { query } = useRoute() // 查询参数
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  key: query.key
+})
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await DefinitionApi.getProcessDefinitionPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 流程表单的详情按钮操作 */
+const formDetailVisible = ref(false)
+const formDetailPreview = ref({
+  rule: [],
+  option: {}
+})
+const handleFormDetail = async (row: any) => {
+  if (row.formType == BpmModelFormType.NORMAL) {
+    // 设置表单
+    setConfAndFields2(formDetailPreview, row.formConf, row.formFields)
+    // 弹窗打开
+    formDetailVisible.value = true
+  } else {
+    await push({
+      path: row.formCustomCreatePath
+    })
+  }
+}
+
+/** 恢复流程模型弹窗 */
+const openModelForm = async (id?: number) => {
+  await push({
+    name: 'BpmModelUpdate',
+    params: { id, type: 'definition' }
+  })
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style lang="scss" scoped>
+.flow-icon {
+  display: flex;
+  width: 38px;
+  height: 38px;
+  margin-right: 10px;
+  background-color: var(--el-color-primary);
+  border-radius: 0.25rem;
+  align-items: center;
+  justify-content: center;
+}
+</style>

+ 344 - 0
src/views/bpm/model/form/BasicInfo.vue

@@ -0,0 +1,344 @@
+<template>
+  <el-form ref="formRef" :model="modelData" :rules="rules" label-width="120px" class="mt-20px">
+    <el-form-item label="流程标识" prop="key" class="mb-20px">
+      <div class="flex items-center">
+        <el-input
+          class="!w-440px"
+          v-model="modelData.key"
+          :disabled="!!modelData.id"
+          placeholder="请输入流程标识,以字母或下划线开头"
+        />
+        <el-tooltip
+          class="item"
+          :content="modelData.id ? '流程标识不可修改!' : '新建后,流程标识不可修改!'"
+          effect="light"
+          placement="top"
+        >
+          <Icon icon="ep:question-filled" class="ml-5px" />
+        </el-tooltip>
+      </div>
+    </el-form-item>
+    <el-form-item label="流程名称" prop="name" class="mb-20px">
+      <el-input
+        v-model="modelData.name"
+        :disabled="!!modelData.id"
+        clearable
+        placeholder="请输入流程名称"
+      />
+    </el-form-item>
+    <el-form-item label="流程分类" prop="category" class="mb-20px">
+      <el-select
+        class="!w-full"
+        v-model="modelData.category"
+        clearable
+        placeholder="请选择流程分类"
+      >
+        <el-option
+          v-for="category in categoryList"
+          :key="category.code"
+          :label="category.name"
+          :value="category.code"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item label="流程图标" class="mb-20px">
+      <UploadImg v-model="modelData.icon" :limit="1" height="64px" width="64px" />
+    </el-form-item>
+    <el-form-item label="流程描述" prop="description" class="mb-20px">
+      <el-input v-model="modelData.description" clearable type="textarea" />
+    </el-form-item>
+    <el-form-item label="流程类型" prop="type" class="mb-20px">
+      <el-radio-group v-model="modelData.type">
+        <el-radio
+          v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_TYPE)"
+          :key="dict.value"
+          :value="dict.value"
+        >
+          {{ dict.label }}
+        </el-radio>
+      </el-radio-group>
+    </el-form-item>
+    <el-form-item label="是否可见" prop="visible" class="mb-20px">
+      <el-radio-group v-model="modelData.visible">
+        <el-radio
+          v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+          :key="dict.value as string"
+          :value="dict.value"
+        >
+          {{ dict.label }}
+        </el-radio>
+      </el-radio-group>
+    </el-form-item>
+    <el-form-item label="谁可以发起" prop="startUserType" class="mb-20px">
+      <el-select
+        v-model="modelData.startUserType"
+        placeholder="请选择谁可以发起"
+        @change="handleStartUserTypeChange"
+      >
+        <el-option label="全员" :value="0" />
+        <el-option label="指定人员" :value="1" />
+        <el-option label="指定部门" :value="2" />
+      </el-select>
+      <div v-if="modelData.startUserType === 1" class="mt-2 flex flex-wrap gap-2">
+        <div
+          v-for="user in selectedStartUsers"
+          :key="user.id"
+          class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
+        >
+          <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
+          <el-avatar class="!m-5px" :size="28" v-else>
+            {{ user.nickname.substring(0, 1) }}
+          </el-avatar>
+          {{ user.nickname }}
+          <Icon
+            icon="ep:close"
+            class="ml-2 cursor-pointer hover:text-red-500"
+            @click="handleRemoveStartUser(user)"
+          />
+        </div>
+        <el-button type="primary" link @click="openStartUserSelect">
+          <Icon icon="ep:plus" /> 选择人员
+        </el-button>
+      </div>
+      <div v-if="modelData.startUserType === 2" class="mt-2 flex flex-wrap gap-2">
+        <div
+          v-for="dept in selectedStartDepts" 
+          :key="dept.id"
+          class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
+        >
+          <Icon icon="ep:office-building" class="!m-5px text-20px" />
+          {{ dept.name }}
+          <Icon
+            icon="ep:close"
+            class="ml-2 cursor-pointer hover:text-red-500"
+            @click="handleRemoveStartDept(dept)"
+          />
+        </div>
+        <el-button type="primary" link @click="openStartDeptSelect">
+          <Icon icon="ep:plus" /> 选择部门
+        </el-button>
+      </div>
+    </el-form-item>
+    <el-form-item label="流程管理员" prop="managerUserIds" class="mb-20px">
+      <div class="flex flex-wrap gap-2">
+        <div
+          v-for="user in selectedManagerUsers"
+          :key="user.id"
+          class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
+        >
+          <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
+          <el-avatar class="!m-5px" :size="28" v-else>
+            {{ user.nickname.substring(0, 1) }}
+          </el-avatar>
+          {{ user.nickname }}
+          <Icon
+            icon="ep:close"
+            class="ml-2 cursor-pointer hover:text-red-500"
+            @click="handleRemoveManagerUser(user)"
+          />
+        </div>
+        <el-button type="primary" link @click="openManagerUserSelect">
+          <Icon icon="ep:plus" />选择人员
+        </el-button>
+      </div>
+    </el-form-item>
+  </el-form>
+
+  <!-- 用户选择弹窗 -->
+  <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
+  <!-- 部门选择弹窗 -->
+  <DeptSelectForm
+    ref="deptSelectFormRef"
+    :multiple="true"
+    :check-strictly="true"
+    @confirm="handleDeptSelectConfirm"
+  />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
+import { UserVO } from '@/api/system/user'
+import { DeptVO } from '@/api/system/dept'
+import { CategoryVO } from '@/api/bpm/category'
+
+const props = defineProps({
+  categoryList: {
+    type: Array as PropType<CategoryVO[]>,
+    required: true
+  },
+  userList: {
+    type: Array,
+    required: true
+  },
+  deptList: {
+    type: Array,
+    required: true
+  }
+})
+
+const formRef = ref()
+const selectedStartUsers = ref<UserVO[]>([])
+const selectedStartDepts = ref<DeptVO[]>([])
+const selectedManagerUsers = ref<UserVO[]>([])
+const userSelectFormRef = ref()
+const deptSelectFormRef = ref()
+const currentSelectType = ref<'start' | 'manager'>('start')
+
+const rules = {
+  name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }],
+  key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
+  category: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }],
+  type: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
+  visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
+  managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }]
+}
+
+// 创建本地数据副本
+const modelData = defineModel<any>()
+
+// 初始化选中的用户
+watch(
+  () => modelData.value,
+  (newVal) => {
+    if (newVal.startUserIds?.length) {
+      selectedStartUsers.value = props.userList.filter((user: UserVO) =>
+        newVal.startUserIds.includes(user.id)
+      ) as UserVO[]
+    } else {
+      selectedStartUsers.value = []
+    }
+    if (newVal.startDeptIds?.length) {
+      selectedStartDepts.value = props.deptList.filter((dept: DeptVO) =>
+        newVal.startDeptIds.includes(dept.id)
+      ) as DeptVO[]
+    } else {
+      selectedStartDepts.value = []
+    }
+    if (newVal.managerUserIds?.length) {
+      selectedManagerUsers.value = props.userList.filter((user: UserVO) =>
+        newVal.managerUserIds.includes(user.id)
+      ) as UserVO[]
+    } else {
+      selectedManagerUsers.value = []
+    }
+  },
+  {
+    immediate: true
+  }
+)
+
+/** 打开发起人选择 */
+const openStartUserSelect = () => {
+  currentSelectType.value = 'start'
+  userSelectFormRef.value.open(0, selectedStartUsers.value)
+}
+
+/** 打开部门选择 */
+const openStartDeptSelect = () => {
+  deptSelectFormRef.value.open(selectedStartDepts.value)
+}
+
+/** 打开管理员选择 */
+const openManagerUserSelect = () => {
+  currentSelectType.value = 'manager'
+  userSelectFormRef.value.open(0, selectedManagerUsers.value)
+}
+
+/** 处理用户选择确认 */
+const handleUserSelectConfirm = (_, users: UserVO[]) => {
+  if (currentSelectType.value === 'start') {
+    modelData.value = {
+      ...modelData.value,
+      startUserIds: users.map((u) => u.id)
+    }
+  } else {
+    modelData.value = {
+      ...modelData.value,
+      managerUserIds: users.map((u) => u.id)
+    }
+  }
+}
+
+/** 处理部门选择确认 */
+const handleDeptSelectConfirm = (depts: DeptVO[]) => {
+  modelData.value = {
+    ...modelData.value,
+    startDeptIds: depts.map((d) => d.id)
+  }
+}
+
+/** 处理发起人类型变化 */
+const handleStartUserTypeChange = (value: number) => {
+  if (value === 0) {
+    modelData.value = {
+      ...modelData.value,
+      startUserIds: [],
+      startDeptIds: []
+    }
+  } else if (value === 1) {
+    modelData.value = {
+      ...modelData.value,
+      startDeptIds: []
+    }
+  } else if (value === 2) {
+    modelData.value = {
+      ...modelData.value,
+      startUserIds: []
+    }
+  }
+}
+
+/** 移除发起人 */
+const handleRemoveStartUser = (user: UserVO) => {
+  modelData.value = {
+    ...modelData.value,
+    startUserIds: modelData.value.startUserIds.filter((id: number) => id !== user.id)
+  }
+}
+
+/** 移除部门 */
+const handleRemoveStartDept = (dept: DeptVO) => {
+  modelData.value = {
+    ...modelData.value,
+    startDeptIds: modelData.value.startDeptIds.filter((id: number) => id !== dept.id)
+  }
+}
+
+/** 移除管理员 */
+const handleRemoveManagerUser = (user: UserVO) => {
+  modelData.value = {
+    ...modelData.value,
+    managerUserIds: modelData.value.managerUserIds.filter((id: number) => id !== user.id)
+  }
+}
+
+/** 表单校验 */
+const validate = async () => {
+  await formRef.value?.validate()
+}
+
+defineExpose({
+  validate
+})
+</script>
+
+<style lang="scss" scoped>
+.bg-gray-100 {
+  background-color: #f5f7fa;
+  transition: all 0.3s;
+
+  &:hover {
+    background-color: #e6e8eb;
+  }
+
+  .ep-close {
+    font-size: 14px;
+    color: #909399;
+    transition: color 0.3s;
+
+    &:hover {
+      color: #f56c6c;
+    }
+  }
+}
+</style>

+ 442 - 0
src/views/bpm/model/form/ExtraSettings.vue

@@ -0,0 +1,442 @@
+<template>
+  <el-form ref="formRef" :model="modelData" label-width="120px" class="mt-20px">
+    <el-form-item class="mb-20px">
+      <template #label>
+        <el-text size="large" tag="b">提交人权限</el-text>
+      </template>
+      <div class="flex flex-col">
+        <el-checkbox v-model="modelData.allowCancelRunningProcess" label="允许撤销审批中的申请" />
+        <div class="ml-22px">
+          <el-text type="info"> 第一个审批节点通过后,提交人仍可撤销申请 </el-text>
+        </div>
+      </div>
+    </el-form-item>
+    <el-form-item v-if="modelData.processIdRule" class="mb-20px">
+      <template #label>
+        <el-text size="large" tag="b">流程编码</el-text>
+      </template>
+      <div class="flex flex-col">
+        <div>
+          <el-input
+            v-model="modelData.processIdRule.prefix"
+            class="w-130px!"
+            placeholder="前缀"
+            :disabled="!modelData.processIdRule.enable"
+          >
+            <template #prepend>
+              <el-checkbox v-model="modelData.processIdRule.enable" />
+            </template>
+          </el-input>
+          <el-select
+            v-model="modelData.processIdRule.infix"
+            class="w-130px! ml-5px"
+            placeholder="中缀"
+            :disabled="!modelData.processIdRule.enable"
+          >
+            <el-option
+              v-for="item in timeOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+          <el-input
+            v-model="modelData.processIdRule.postfix"
+            class="w-80px! ml-5px"
+            placeholder="后缀"
+            :disabled="!modelData.processIdRule.enable"
+          />
+          <el-input-number
+            v-model="modelData.processIdRule.length"
+            class="w-120px! ml-5px"
+            :min="5"
+            :disabled="!modelData.processIdRule.enable"
+          />
+        </div>
+        <div class="ml-22px" v-if="modelData.processIdRule.enable">
+          <el-text type="info"> 编码示例:{{ numberExample }} </el-text>
+        </div>
+      </div>
+    </el-form-item>
+    <el-form-item class="mb-20px">
+      <template #label>
+        <el-text size="large" tag="b">自动去重</el-text>
+      </template>
+      <div class="flex flex-col">
+        <div>
+          <el-text> 同一审批人在流程中重复出现时: </el-text>
+        </div>
+        <el-radio-group v-model="modelData.autoApprovalType">
+          <div class="flex flex-col">
+            <el-radio :value="0">不自动通过</el-radio>
+            <el-radio :value="1">仅审批一次,后续重复的审批节点均自动通过</el-radio>
+            <el-radio :value="2">仅针对连续审批的节点自动通过</el-radio>
+          </div>
+        </el-radio-group>
+      </div>
+    </el-form-item>
+    <el-form-item v-if="modelData.titleSetting" class="mb-20px">
+      <template #label>
+        <el-text size="large" tag="b">标题设置</el-text>
+      </template>
+      <div class="flex flex-col">
+        <el-radio-group v-model="modelData.titleSetting.enable">
+          <div class="flex flex-col">
+            <el-radio :value="false"
+              >系统默认 <el-text type="info"> 展示流程名称 </el-text></el-radio
+            >
+            <el-radio :value="true">
+              自定义标题
+              <el-text>
+                <el-tooltip content="输入字符 '{' 即可插入表单字段" effect="light" placement="top">
+                  <Icon icon="ep:question-filled" class="ml-5px" />
+                </el-tooltip>
+              </el-text>
+            </el-radio>
+          </div>
+        </el-radio-group>
+        <el-mention
+          v-if="modelData.titleSetting.enable"
+          v-model="modelData.titleSetting.title"
+          type="textarea"
+          prefix="{"
+          split="}"
+          whole
+          :options="formFieldOptions4Title"
+          placeholder="请插入表单字段(输入 '{' 可以选择表单字段)或输入文本"
+          class="w-600px!"
+        />
+      </div>
+    </el-form-item>
+    <el-form-item
+      v-if="modelData.summarySetting && modelData.formType === BpmModelFormType.NORMAL"
+      class="mb-20px"
+    >
+      <template #label>
+        <el-text size="large" tag="b">摘要设置</el-text>
+      </template>
+      <div class="flex flex-col">
+        <el-radio-group v-model="modelData.summarySetting.enable">
+          <div class="flex flex-col">
+            <el-radio :value="false">
+              系统默认 <el-text type="info"> 展示表单前 3 个字段 </el-text>
+            </el-radio>
+            <el-radio :value="true"> 自定义摘要 </el-radio>
+          </div>
+        </el-radio-group>
+        <el-select
+          class="w-500px!"
+          v-if="modelData.summarySetting.enable"
+          v-model="modelData.summarySetting.summary"
+          multiple
+          placeholder="请选择要展示的表单字段"
+        >
+          <el-option
+            v-for="item in formFieldOptions4Summary"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </div>
+    </el-form-item>
+    <el-form-item class="mb-20px">
+      <template #label>
+        <el-text size="large" tag="b">流程前置通知</el-text>
+      </template>
+      <div class="flex flex-col w-100%">
+        <div class="flex">
+          <el-switch
+            v-model="processBeforeTriggerEnable"
+            @change="handleProcessBeforeTriggerEnableChange"
+          />
+          <div class="ml-80px">流程启动后通知</div>
+        </div>
+        <HttpRequestSetting
+          v-if="processBeforeTriggerEnable"
+          v-model:setting="modelData.processBeforeTriggerSetting"
+          :responseEnable="true"
+          :formItemPrefix="'processBeforeTriggerSetting'"
+        />
+      </div>
+    </el-form-item>
+    <el-form-item class="mb-20px">
+      <template #label>
+        <el-text size="large" tag="b">流程后置通知</el-text>
+      </template>
+      <div class="flex flex-col w-100%">
+        <div class="flex">
+          <el-switch
+            v-model="processAfterTriggerEnable"
+            @change="handleProcessAfterTriggerEnableChange"
+          />
+          <div class="ml-80px">流程结束后通知</div>
+        </div>
+        <HttpRequestSetting
+          v-if="processAfterTriggerEnable"
+          v-model:setting="modelData.processAfterTriggerSetting"
+          :responseEnable="true"
+          :formItemPrefix="'processAfterTriggerSetting'"
+        />
+      </div>
+    </el-form-item>
+    <el-form-item class="mb-20px">
+      <template #label>
+        <el-text size="large" tag="b">任务前置通知</el-text>
+      </template>
+      <div class="flex flex-col w-100%">
+        <div class="flex">
+          <el-switch
+            v-model="taskBeforeTriggerEnable"
+            @change="handleTaskBeforeTriggerEnableChange"
+          />
+          <div class="ml-80px">任务执行时通知</div>
+        </div>
+        <HttpRequestSetting
+          v-if="taskBeforeTriggerEnable"
+          v-model:setting="modelData.taskBeforeTriggerSetting"
+          :responseEnable="true"
+          :formItemPrefix="'taskBeforeTriggerSetting'"
+        />
+      </div>
+    </el-form-item>
+    <el-form-item class="mb-20px">
+      <template #label>
+        <el-text size="large" tag="b">任务后置通知</el-text>
+      </template>
+      <div class="flex flex-col w-100%">
+        <div class="flex">
+          <el-switch
+            v-model="taskAfterTriggerEnable"
+            @change="handleTaskAfterTriggerEnableChange"
+          />
+          <div class="ml-80px">任务结束后通知</div>
+        </div>
+        <HttpRequestSetting
+          v-if="taskAfterTriggerEnable"
+          v-model:setting="modelData.taskAfterTriggerSetting"
+          :responseEnable="true"
+          :formItemPrefix="'taskAfterTriggerSetting'"
+        />
+      </div>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import dayjs from 'dayjs'
+import { BpmAutoApproveType, BpmModelFormType } from '@/utils/constants'
+import * as FormApi from '@/api/bpm/form'
+import { parseFormFields } from '@/components/FormCreate/src/utils'
+import { ProcessVariableEnum } from '@/components/SimpleProcessDesignerV2/src/consts'
+import HttpRequestSetting from '@/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestSetting.vue'
+
+const modelData = defineModel<any>()
+
+/** 自定义 ID 流程编码 */
+const timeOptions = ref([
+  {
+    value: '',
+    label: '无'
+  },
+  {
+    value: 'DAY',
+    label: '精确到日'
+  },
+  {
+    value: 'HOUR',
+    label: '精确到时'
+  },
+  {
+    value: 'MINUTE',
+    label: '精确到分'
+  },
+  {
+    value: 'SECOND',
+    label: '精确到秒'
+  }
+])
+const numberExample = computed(() => {
+  if (modelData.value.processIdRule.enable) {
+    let infix = ''
+    switch (modelData.value.processIdRule.infix) {
+      case 'DAY':
+        infix = dayjs().format('YYYYMMDD')
+        break
+      case 'HOUR':
+        infix = dayjs().format('YYYYMMDDHH')
+        break
+      case 'MINUTE':
+        infix = dayjs().format('YYYYMMDDHHmm')
+        break
+      case 'SECOND':
+        infix = dayjs().format('YYYYMMDDHHmmss')
+        break
+      default:
+        break
+    }
+    return (
+      modelData.value.processIdRule.prefix +
+      infix +
+      modelData.value.processIdRule.postfix +
+      '1'.padStart(modelData.value.processIdRule.length - 1, '0')
+    )
+  } else {
+    return ''
+  }
+})
+
+/** 是否开启流程前置通知 */
+const processBeforeTriggerEnable = ref(false)
+const handleProcessBeforeTriggerEnableChange = (val: boolean | string | number) => {
+  if (val) {
+    modelData.value.processBeforeTriggerSetting = {
+      url: '',
+      header: [],
+      body: [],
+      response: []
+    }
+  } else {
+    modelData.value.processBeforeTriggerSetting = null
+  }
+}
+
+/** 是否开启流程后置通知 */
+const processAfterTriggerEnable = ref(false)
+const handleProcessAfterTriggerEnableChange = (val: boolean | string | number) => {
+  if (val) {
+    modelData.value.processAfterTriggerSetting = {
+      url: '',
+      header: [],
+      body: [],
+      response: []
+    }
+  } else {
+    modelData.value.processAfterTriggerSetting = null
+  }
+}
+
+/** 是否开启任务前置通知 */
+const taskBeforeTriggerEnable = ref(false)
+const handleTaskBeforeTriggerEnableChange = (val: boolean | string | number) => {
+  if (val) {
+    modelData.value.taskBeforeTriggerSetting = {
+      url: '',
+      header: [],
+      body: [],
+      response: []
+    }
+  } else {
+    modelData.value.taskBeforeTriggerSetting = null
+  }
+}
+
+/** 是否开启任务后置通知 */
+const taskAfterTriggerEnable = ref(false)
+const handleTaskAfterTriggerEnableChange = (val: boolean | string | number) => {
+  if (val) {
+    modelData.value.taskAfterTriggerSetting = {
+      url: '',
+      header: [],
+      body: [],
+      response: []
+    }
+  } else {
+    modelData.value.taskAfterTriggerSetting = null
+  }
+}
+
+/** 表单选项 */
+const formField = ref<Array<{ field: string; title: string }>>([])
+const formFieldOptions4Title = computed(() => {
+  let cloneFormField = formField.value.map((item) => {
+    return {
+      label: item.title,
+      value: item.field
+    }
+  })
+  // 固定添加发起人 ID 字段
+  cloneFormField.unshift({
+    label: '流程名称',
+    value: ProcessVariableEnum.PROCESS_DEFINITION_NAME
+  })
+  cloneFormField.unshift({
+    label: '发起时间',
+    value: ProcessVariableEnum.START_TIME
+  })
+  cloneFormField.unshift({
+    label: '发起人',
+    value: ProcessVariableEnum.START_USER_ID
+  })
+  return cloneFormField
+})
+const formFieldOptions4Summary = computed(() => {
+  return formField.value.map((item) => {
+    return {
+      label: item.title,
+      value: item.field
+    }
+  })
+})
+
+/** 兼容以前未配置更多设置的流程 */
+const initData = () => {
+  if (!modelData.value.processIdRule) {
+    modelData.value.processIdRule = {
+      enable: false,
+      prefix: '',
+      infix: '',
+      postfix: '',
+      length: 5
+    }
+  }
+  if (!modelData.value.autoApprovalType) {
+    modelData.value.autoApprovalType = BpmAutoApproveType.NONE
+  }
+  if (!modelData.value.titleSetting) {
+    modelData.value.titleSetting = {
+      enable: false,
+      title: ''
+    }
+  }
+  if (!modelData.value.summarySetting) {
+    modelData.value.summarySetting = {
+      enable: false,
+      summary: []
+    }
+  }
+  if (modelData.value.processBeforeTriggerSetting) {
+    processBeforeTriggerEnable.value = true
+  }
+  if (modelData.value.processAfterTriggerSetting) {
+    processAfterTriggerEnable.value = true
+  }
+  if (modelData.value.taskBeforeTriggerSetting) {
+    taskBeforeTriggerEnable.value = true
+  }
+  if (modelData.value.taskAfterTriggerSetting) {
+    taskAfterTriggerEnable.value = true
+  }
+}
+defineExpose({ initData })
+
+/** 监听表单 ID 变化,加载表单数据 */
+watch(
+  () => modelData.value.formId,
+  async (newFormId) => {
+    if (newFormId && modelData.value.formType === BpmModelFormType.NORMAL) {
+      const data = await FormApi.getForm(newFormId)
+      const result: Array<{ field: string; title: string }> = []
+      if (data.fields) {
+        data.fields.forEach((fieldStr: string) => {
+          parseFormFields(JSON.parse(fieldStr), result)
+        })
+      }
+      formField.value = result
+    } else {
+      formField.value = []
+    }
+  },
+  { immediate: true }
+)
+</script>

+ 129 - 0
src/views/bpm/model/form/FormDesign.vue

@@ -0,0 +1,129 @@
+<template>
+  <el-form ref="formRef" :model="modelData" :rules="rules" label-width="120px" class="mt-20px">
+    <el-form-item label="表单类型" prop="formType" class="mb-20px">
+      <el-radio-group v-model="modelData.formType">
+        <el-radio
+          v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)"
+          :key="dict.value"
+          :value="dict.value"
+        >
+          {{ dict.label }}
+        </el-radio>
+      </el-radio-group>
+    </el-form-item>
+    <el-form-item v-if="modelData.formType === BpmModelFormType.NORMAL" label="流程表单" prop="formId">
+      <el-select v-model="modelData.formId" clearable style="width: 100%">
+        <el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" />
+      </el-select>
+    </el-form-item>
+    <el-form-item v-if="modelData.formType === BpmModelFormType.CUSTOM" label="表单提交路由" prop="formCustomCreatePath">
+      <el-input
+        v-model="modelData.formCustomCreatePath"
+        placeholder="请输入表单提交路由"
+        style="width: 330px"
+      />
+      <el-tooltip
+        class="item"
+        content="自定义表单的提交路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/create.vue"
+        effect="light"
+        placement="top"
+      >
+        <Icon icon="ep:question" class="ml-5px" />
+      </el-tooltip>
+    </el-form-item>
+    <el-form-item v-if="modelData.formType === BpmModelFormType.CUSTOM" label="表单查看地址" prop="formCustomViewPath">
+      <el-input
+        v-model="modelData.formCustomViewPath"
+        placeholder="请输入表单查看的组件地址"
+        style="width: 330px"
+      />
+      <el-tooltip
+        class="item"
+        content="自定义表单的查看组件地址,使用 Vue 的组件地址,例如说:bpm/oa/leave/detail.vue"
+        effect="light"
+        placement="top"
+      >
+        <Icon icon="ep:question" class="ml-5px" />
+      </el-tooltip>
+    </el-form-item>
+    <!-- 表单预览 -->
+    <div
+      v-if="modelData.formType === BpmModelFormType.NORMAL && modelData.formId && formPreview.rule.length > 0"
+      class="mt-20px"
+    >
+      <div class="flex items-center mb-15px">
+        <div class="h-15px w-4px bg-[#1890ff] mr-10px"></div>
+        <span class="text-15px font-bold">表单预览</span>
+      </div>
+      <form-create
+        v-model="formPreview.formData"
+        :rule="formPreview.rule"
+        :option="formPreview.option"
+      />
+    </div>
+  </el-form>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as FormApi from '@/api/bpm/form'
+import { setConfAndFields2 } from '@/utils/formCreate'
+import { BpmModelFormType } from '@/utils/constants'
+
+const props = defineProps({
+  formList: {
+    type: Array,
+    required: true
+  }
+})
+
+const formRef = ref()
+
+// 创建本地数据副本
+const modelData = defineModel<any>()
+
+// 表单预览数据
+const formPreview = ref({
+  formData: {},
+  rule: [],
+  option: {
+    submitBtn: false,
+    resetBtn: false,
+    formData: {}
+  }
+})
+
+// 监听表单ID变化,加载表单数据
+watch(
+  () => modelData.value.formId,
+  async (newFormId) => {
+    if (newFormId && modelData.value.formType === BpmModelFormType.NORMAL) {
+      const data = await FormApi.getForm(newFormId)
+      setConfAndFields2(formPreview.value, data.conf, data.fields)
+      // 设置只读
+      formPreview.value.rule.forEach((item: any) => {
+        item.props = { ...item.props, disabled: true }
+      })
+    } else {
+      formPreview.value.rule = []
+    }
+  },
+  { immediate: true }
+)
+
+const rules = {
+  formType: [{ required: true, message: '表单类型不能为空', trigger: 'blur' }],
+  formId: [{ required: true, message: '流程表单不能为空', trigger: 'blur' }],
+  formCustomCreatePath: [{ required: true, message: '表单提交路由不能为空', trigger: 'blur' }],
+  formCustomViewPath: [{ required: true, message: '表单查看地址不能为空', trigger: 'blur' }]
+}
+
+/** 表单校验 */
+const validate = async () => {
+  await formRef.value?.validate()
+}
+
+defineExpose({
+  validate
+})
+</script>

+ 72 - 0
src/views/bpm/model/form/ProcessDesign.vue

@@ -0,0 +1,72 @@
+<template>
+  <!-- BPMN设计器 -->
+  <template v-if="modelData.type === BpmModelType.BPMN">
+    <BpmModelEditor
+      v-if="showDesigner"
+      :model-id="modelData.id"
+      :model-key="modelData.key"
+      :model-name="modelData.name"
+      @success="handleDesignSuccess"
+    />
+  </template>
+
+  <!-- Simple设计器 -->
+  <template v-else>
+    <SimpleModelDesign
+      v-if="showDesigner"
+      :model-id="modelData.id"
+      :model-key="modelData.key"
+      :model-name="modelData.name"
+      :start-user-ids="modelData.startUserIds"
+      :start-dept-ids="modelData.startDeptIds"
+      @success="handleDesignSuccess"
+    />
+  </template>
+</template>
+
+<script lang="ts" setup>
+import { BpmModelType } from '@/utils/constants'
+import BpmModelEditor from './editor/index.vue'
+import SimpleModelDesign from '../../simple/SimpleModelDesign.vue'
+
+// 创建本地数据副本
+const modelData = defineModel<any>()
+
+const processData = inject('processData') as Ref
+
+/** 表单校验 */
+const validate = async () => {
+  try {
+    // 获取最新的流程数据
+    if (!processData.value) {
+      throw new Error('请设计流程')
+    }
+    return true
+  } catch (error) {
+    throw error
+  }
+}
+/** 处理设计器保存成功 */
+const handleDesignSuccess = async (data?: any) => {
+  if (data) {
+    // 创建新的对象以触发响应式更新
+    const newModelData = {
+      ...modelData.value,
+      bpmnXml: modelData.value.type === BpmModelType.BPMN ? data : null,
+      simpleModel: modelData.value.type === BpmModelType.BPMN ? null : data
+    }
+    // 使用emit更新父组件的数据
+    await nextTick()
+    //更新表单的模型数据部分
+    modelData.value = newModelData
+  }
+}
+
+/** 是否显示设计器 */
+const showDesigner = computed(() => {
+  return Boolean(modelData.value?.key && modelData.value?.name)
+})
+defineExpose({
+  validate
+})
+</script>

+ 124 - 0
src/views/bpm/model/form/editor/index.vue

@@ -0,0 +1,124 @@
+<template>
+  <ContentWrap>
+    <!-- 流程设计器,负责绘制流程等 -->
+    <MyProcessDesigner
+      key="designer"
+      v-model="xmlString"
+      :value="xmlString"
+      v-bind="controlForm"
+      keyboard
+      ref="processDesigner"
+      @init-finished="initModeler"
+      :additionalModel="controlForm.additionalModel"
+      :model="model"
+      @save="save"
+      :process-id="modelKey"
+      :process-name="modelName"
+    />
+    <!-- 流程属性器,负责编辑每个流程节点的属性 -->
+    <MyProcessPenal
+      v-if="modeler"
+      key="penal"
+      :bpmnModeler="modeler"
+      :prefix="controlForm.prefix"
+      class="process-panel"
+      :model="model"
+    />
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { MyProcessDesigner, MyProcessPenal } from '@/components/bpmnProcessDesigner/package'
+// 自定义元素选中时的弹出菜单(修改 默认任务 为 用户任务)
+import CustomContentPadProvider from '@/components/bpmnProcessDesigner/package/designer/plugins/content-pad'
+// 自定义左侧菜单(修改 默认任务 为 用户任务)
+import CustomPaletteProvider from '@/components/bpmnProcessDesigner/package/designer/plugins/palette'
+import * as ModelApi from '@/api/bpm/model'
+import { BpmModelFormType } from '@/utils/constants'
+import * as FormApi from '@/api/bpm/form'
+
+defineOptions({ name: 'BpmModelEditor' })
+
+defineProps<{
+  modelId?: string
+  modelKey: string
+  modelName: string
+  value?: string
+}>()
+
+const emit = defineEmits(['success', 'init-finished'])
+const message = useMessage() // 国际化
+
+// 表单信息
+const formFields = ref<string[]>([])
+// 表单类型,暂仅限流程表单
+const formType = ref(BpmModelFormType.NORMAL)
+provide('formFields', formFields)
+provide('formType', formType)
+
+// 注入流程数据
+const xmlString = inject('processData') as Ref
+// 注入模型数据
+const modelData = inject('modelData') as Ref
+
+const modeler = shallowRef() // BPMN Modeler
+const processDesigner = ref()
+const controlForm = ref({
+  simulation: true,
+  labelEditing: false,
+  labelVisible: false,
+  prefix: 'flowable',
+  headerButtonSize: 'mini',
+  additionalModel: [CustomContentPadProvider, CustomPaletteProvider]
+})
+const model = ref<ModelApi.ModelVO>() // 流程模型的信息
+
+/** 初始化 modeler */
+const initModeler = async (item: any) => {
+  // 先初始化模型数据
+  model.value = modelData.value
+  modeler.value = item
+}
+
+/** 添加/修改模型 */
+const save = async (bpmnXml: string) => {
+  try {
+    xmlString.value = bpmnXml
+    emit('success', bpmnXml)
+  } catch (error) {
+    console.error('保存失败:', error)
+    message.error('保存失败')
+  }
+}
+
+/** 监听表单 ID 变化,加载表单数据 */
+watch(
+  () => modelData.value.formId,
+  async (newFormId) => {
+    if (newFormId && modelData.value.formType === BpmModelFormType.NORMAL) {
+      const data = await FormApi.getForm(newFormId)
+      formFields.value = data.fields
+    } else {
+      formFields.value = []
+    }
+  },
+  { immediate: true }
+)
+
+// 在组件卸载时清理
+onBeforeUnmount(() => {
+  modeler.value = null
+  // 清理全局实例
+  const w = window as any
+  if (w.bpmnInstances) {
+    w.bpmnInstances = null
+  }
+})
+</script>
+<style lang="scss">
+.process-panel__container {
+  position: absolute;
+  top: 172px;
+  right: 70px;
+}
+</style>

+ 442 - 0
src/views/bpm/model/form/index.vue

@@ -0,0 +1,442 @@
+<template>
+  <ContentWrap>
+    <div class="mx-auto">
+      <!-- 头部导航栏 -->
+      <div
+        class="absolute top-0 left-0 right-0 h-50px bg-white border-bottom z-10 flex items-center px-20px"
+      >
+        <!-- 左侧标题 -->
+        <div class="w-200px flex items-center overflow-hidden">
+          <Icon icon="ep:arrow-left" class="cursor-pointer flex-shrink-0" @click="handleBack" />
+          <span class="ml-10px text-16px truncate" :title="formData.name || '创建流程'">
+            {{ formData.name || '创建流程' }}
+          </span>
+        </div>
+
+        <!-- 步骤条 -->
+        <div class="flex-1 flex items-center justify-center h-full">
+          <div class="w-400px flex items-center justify-between h-full">
+            <div
+              v-for="(step, index) in steps"
+              :key="index"
+              class="flex items-center cursor-pointer mx-15px relative h-full"
+              :class="[
+                currentStep === index
+                  ? 'text-[#3473ff] border-[#3473ff] border-b-2 border-b-solid'
+                  : 'text-gray-500'
+              ]"
+              @click="handleStepClick(index)"
+            >
+              <div
+                class="w-28px h-28px rounded-full flex items-center justify-center mr-8px border-2 border-solid text-15px"
+                :class="[
+                  currentStep === index
+                    ? 'bg-[#3473ff] text-white border-[#3473ff]'
+                    : 'border-gray-300 bg-white text-gray-500'
+                ]"
+              >
+                {{ index + 1 }}
+              </div>
+              <span class="text-16px font-bold whitespace-nowrap">{{ step.title }}</span>
+            </div>
+          </div>
+        </div>
+
+        <!-- 右侧按钮 -->
+        <div class="w-200px flex items-center justify-end gap-2">
+          <el-button v-if="actionType === 'update'" type="success" @click="handleDeploy">
+            发 布
+          </el-button>
+          <el-button type="primary" @click="handleSave">
+            <span v-if="actionType === 'definition'">恢 复</span>
+            <span v-else>保 存</span>
+          </el-button>
+        </div>
+      </div>
+
+      <!-- 主体内容 -->
+      <div class="mt-50px">
+        <!-- 第一步:基本信息 -->
+        <div v-if="currentStep === 0" class="mx-auto w-560px">
+          <BasicInfo
+            v-model="formData"
+            :categoryList="categoryList"
+            :userList="userList"
+            :deptList="deptList"
+            ref="basicInfoRef"
+          />
+        </div>
+
+        <!-- 第二步:表单设计 -->
+        <div v-if="currentStep === 1" class="mx-auto w-560px">
+          <FormDesign v-model="formData" :formList="formList" ref="formDesignRef" />
+        </div>
+
+        <!-- 第三步:流程设计 -->
+        <ProcessDesign v-if="currentStep === 2" v-model="formData" ref="processDesignRef" />
+
+        <!-- 第四步:更多设置 -->
+        <div v-show="currentStep === 3" class="mx-auto w-700px">
+          <ExtraSettings v-model="formData" ref="extraSettingsRef" />
+        </div>
+      </div>
+    </div>
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { useRoute, useRouter } from 'vue-router'
+import { useMessage } from '@/hooks/web/useMessage'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { useUserStoreWithOut } from '@/store/modules/user'
+import * as ModelApi from '@/api/bpm/model'
+import * as FormApi from '@/api/bpm/form'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+import * as UserApi from '@/api/system/user'
+import * as DeptApi from '@/api/system/dept'
+import * as DefinitionApi from '@/api/bpm/definition'
+import { BpmModelFormType, BpmModelType, BpmAutoApproveType } from '@/utils/constants'
+import BasicInfo from './BasicInfo.vue'
+import FormDesign from './FormDesign.vue'
+import ProcessDesign from './ProcessDesign.vue'
+import ExtraSettings from './ExtraSettings.vue'
+import { useTagsView } from '@/hooks/web/useTagsView'
+
+const router = useRouter()
+const { delView } = useTagsViewStore() // 视图操作
+const tagsView = useTagsView()
+const route = useRoute()
+const message = useMessage()
+const userStore = useUserStoreWithOut()
+
+// 组件引用
+const basicInfoRef = ref()
+const formDesignRef = ref()
+const processDesignRef = ref()
+const extraSettingsRef = ref()
+
+/** 步骤校验函数 */
+const validateBasic = async () => {
+  await basicInfoRef.value?.validate()
+}
+
+/** 表单设计校验 */
+const validateForm = async () => {
+  await formDesignRef.value?.validate()
+}
+
+/** 流程设计校验 */
+const validateProcess = async () => {
+  await processDesignRef.value?.validate()
+}
+
+const currentStep = ref(-1) // 步骤控制。-1 用于,一开始全部不展示等当前页面数据初始化完成
+
+const steps = [
+  { title: '基本信息', validator: validateBasic },
+  { title: '表单设计', validator: validateForm },
+  { title: '流程设计', validator: validateProcess },
+  { title: '更多设置', validator: null }
+]
+
+// 表单数据
+const formData: any = ref({
+  id: undefined,
+  name: '',
+  key: '',
+  category: undefined,
+  icon: undefined,
+  description: '',
+  type: BpmModelType.BPMN,
+  formType: BpmModelFormType.NORMAL,
+  formId: '',
+  formCustomCreatePath: '',
+  formCustomViewPath: '',
+  visible: true,
+  startUserType: undefined,
+  startUserIds: [],
+  startDeptIds: [],
+  managerUserIds: [],
+  allowCancelRunningProcess: true,
+  processIdRule: {
+    enable: false,
+    prefix: '',
+    infix: '',
+    postfix: '',
+    length: 5
+  },
+  autoApprovalType: BpmAutoApproveType.NONE,
+  titleSetting: {
+    enable: false,
+    title: ''
+  },
+  summarySetting: {
+    enable: false,
+    summary: []
+  }
+})
+
+// 流程数据
+const processData = ref<any>()
+
+provide('processData', processData)
+provide('modelData', formData)
+
+// 数据列表
+const formList = ref([])
+const categoryList = ref<CategoryVO[]>([])
+const userList = ref<UserApi.UserVO[]>([])
+const deptList = ref<DeptApi.DeptVO[]>([])
+
+/** 初始化数据 */
+const actionType = route.params.type as string
+const initData = async () => {
+  if (actionType === 'definition') {
+    // 情况一:流程定义场景(恢复)
+    const definitionId = route.params.id as string
+    const data = await DefinitionApi.getProcessDefinition(definitionId)
+    // 将 definition => model,最终赋值
+    data.type = data.modelType
+    delete data.modelType
+    data.id = data.modelId
+    delete data.modelId
+    if (data.simpleModel) {
+      data.simpleModel = JSON.parse(data.simpleModel)
+    }
+    formData.value = data
+    formData.value.startUserType =
+      formData.value.startUserIds?.length > 0 ? 1 : formData.value?.startDeptIds?.length > 0 ? 2 : 0
+  } else if (['update', 'copy'].includes(actionType)) {
+    // 情况二:修改场景/复制场景
+    const modelId = route.params.id as string
+    formData.value = await ModelApi.getModel(modelId)
+    formData.value.startUserType =
+      formData.value.startUserIds?.length > 0 ? 1 : formData.value?.startDeptIds?.length > 0 ? 2 : 0
+
+    // 特殊:复制场景
+    if (route.params.type === 'copy') {
+      delete formData.value.id
+      formData.value.name += '副本'
+      formData.value.key += '_copy'
+      tagsView.setTitle('复制流程')
+    }
+  } else {
+    // 情况三:新增场景
+    formData.value.startUserType = 0 // 全体
+    formData.value.managerUserIds.push(userStore.getUser.id)
+  }
+
+  // 获取表单列表
+  formList.value = await FormApi.getFormSimpleList()
+  // 获取分类列表
+  categoryList.value = await CategoryApi.getCategorySimpleList()
+  // 获取用户列表
+  userList.value = await UserApi.getSimpleUserList()
+  // 获取部门列表
+  deptList.value = await DeptApi.getSimpleDeptList()
+
+  // 最终,设置 currentStep 切换到第一步
+  currentStep.value = 0
+
+  // 兼容,以前未配置更多设置的流程
+  extraSettingsRef.value.initData()
+}
+
+/** 根据类型切换流程数据 */
+watch(
+  async () => formData.value.type,
+  () => {
+    if (formData.value.type === BpmModelType.BPMN) {
+      processData.value = formData.value.bpmnXml
+    } else if (formData.value.type === BpmModelType.SIMPLE) {
+      processData.value = formData.value.simpleModel
+    }
+    console.log('加载流程数据', processData.value)
+  },
+  {
+    immediate: true
+  }
+)
+
+/** 校验所有步骤数据是否完整 */
+const validateAllSteps = async () => {
+  try {
+    // 基本信息校验
+    try {
+      await validateBasic()
+    } catch (error) {
+      currentStep.value = 0
+      throw new Error('请完善基本信息')
+    }
+
+    // 表单设计校验
+    try {
+      await validateForm()
+    } catch (error) {
+      currentStep.value = 1
+      throw new Error('请完善自定义表单信息')
+    }
+
+    // 流程设计校验
+
+    // 表单设计校验
+    try {
+      await validateProcess()
+    } catch (error) {
+      currentStep.value = 2
+      throw new Error('请设计流程')
+    }
+
+    return true
+  } catch (error) {
+    throw error
+  }
+}
+
+/** 保存操作 */
+const handleSave = async () => {
+  try {
+    // 保存前校验所有步骤的数据
+    await validateAllSteps()
+
+    // 更新表单数据
+    const modelData = {
+      ...formData.value
+    }
+
+    if (actionType === 'definition') {
+      // 情况一:流程定义场景(恢复)
+      await ModelApi.updateModel(modelData)
+      // 提示成功
+      message.success('恢复成功,可点击【发布】按钮,进行发布模型')
+    } else if (actionType === 'update') {
+      // 修改场景
+      await ModelApi.updateModel(modelData)
+      // 提示成功
+      message.success('修改成功,可点击【发布】按钮,进行发布模型')
+    } else if (actionType === 'copy') {
+      // 情况三:复制场景
+      formData.value.id = await ModelApi.createModel(modelData)
+      // 提示成功
+      message.success('复制成功,可点击【发布】按钮,进行发布模型')
+    } else {
+      // 情况四:新增场景
+      formData.value.id = await ModelApi.createModel(modelData)
+      // 提示成功
+      message.success('新建成功,可点击【发布】按钮,进行发布模型')
+    }
+
+    // 返回列表页(排除更新的情况)
+    if (actionType !== 'update') {
+      await router.push({ name: 'BpmModel' })
+    }
+  } catch (error: any) {
+    console.error('保存失败:', error)
+    message.warning(error.message || '请完善所有步骤的必填信息')
+  }
+}
+
+/** 发布操作 */
+const handleDeploy = async () => {
+  try {
+    // 修改场景下直接发布,新增场景下需要先确认
+    if (!formData.value.id) {
+      await message.confirm('是否确认发布该流程?')
+    }
+    // 校验所有步骤
+    await validateAllSteps()
+
+    // 更新表单数据
+    const modelData = {
+      ...formData.value
+    }
+
+    // 先保存所有数据
+    if (formData.value.id) {
+      await ModelApi.updateModel(modelData)
+    } else {
+      const result = await ModelApi.createModel(modelData)
+      formData.value.id = result.id
+    }
+
+    // 发布
+    await ModelApi.deployModel(formData.value.id)
+    message.success('发布成功')
+    // 返回列表页
+    await router.push({ name: 'BpmModel' })
+  } catch (error: any) {
+    console.error('发布失败:', error)
+    message.warning(error.message || '发布失败')
+  }
+}
+
+/** 步骤切换处理 */
+const handleStepClick = async (index: number) => {
+  try {
+    if (index !== 0) {
+      await validateBasic()
+    }
+    if (index !== 1) {
+      await validateForm()
+    }
+    if (index !== 2) {
+      await validateProcess()
+    }
+
+    // 切换步骤
+    currentStep.value = index
+
+    // 如果切换到流程设计步骤,等待组件渲染完成后刷新设计器
+    if (index === 2) {
+      await nextTick()
+      // 等待更长时间确保组件完全初始化
+      await new Promise((resolve) => setTimeout(resolve, 200))
+      if (processDesignRef.value?.refresh) {
+        await processDesignRef.value.refresh()
+      }
+    }
+  } catch (error) {
+    console.error('步骤切换失败:', error)
+    message.warning('请先完善当前步骤必填信息')
+  }
+}
+
+/** 返回列表页 */
+const handleBack = () => {
+  // 先删除当前页签
+  delView(unref(router.currentRoute))
+  // 跳转到列表页
+  router.push({ name: 'BpmModel' })
+}
+
+/** 初始化 */
+onMounted(async () => {
+  await initData()
+})
+
+// 添加组件卸载前的清理代码
+onBeforeUnmount(() => {
+  // 清理所有的引用
+  basicInfoRef.value = null
+  formDesignRef.value = null
+  processDesignRef.value = null
+})
+</script>
+
+<style lang="scss" scoped>
+.border-bottom {
+  border-bottom: 1px solid #dcdfe6;
+}
+
+.text-primary {
+  color: #3473ff;
+}
+
+.bg-primary {
+  background-color: #3473ff;
+}
+
+.border-primary {
+  border-color: #3473ff;
+}
+</style>

+ 225 - 0
src/views/bpm/model/index.vue

@@ -0,0 +1,225 @@
+<template>
+  <ContentWrap>
+    <div class="flex justify-between pl-20px items-center">
+      <h3 class="font-extrabold">流程模型</h3>
+      <!-- 搜索工作栏 -->
+      <el-form
+        v-if="!isCategorySorting"
+        class="-mb-15px flex mr-10px"
+        :model="queryParams"
+        ref="queryFormRef"
+        :inline="true"
+        label-width="68px"
+        @submit.prevent
+      >
+        <el-form-item prop="name" class="ml-auto">
+          <el-input
+            v-model="queryParams.name"
+            placeholder="搜索流程"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-240px"
+          >
+            <template #prefix>
+              <Icon icon="ep:search" class="mx-10px" />
+            </template>
+          </el-input>
+        </el-form-item>
+        <!-- 右上角:新建模型、更多操作 -->
+        <el-form-item>
+          <el-button type="primary" @click="openForm('create')" v-hasPermi="['bpm:model:create']">
+            <Icon icon="ep:plus" class="mr-5px" /> 新建模型
+          </el-button>
+        </el-form-item>
+        <el-form-item>
+          <el-dropdown @command="(command) => handleCommand(command)" placement="bottom-end">
+            <el-button class="w-30px" plain>
+              <Icon icon="ep:setting" />
+            </el-button>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item command="handleCategoryAdd">
+                  <Icon icon="ep:circle-plus" :size="13" class="mr-5px" />
+                  新建分类
+                </el-dropdown-item>
+                <el-dropdown-item command="handleCategorySort">
+                  <Icon icon="fa:sort-amount-desc" :size="13" class="mr-5px" />
+                  分类排序
+                </el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+        </el-form-item>
+      </el-form>
+      <div class="mr-20px" v-else>
+        <el-button @click="handleCategorySortCancel"> 取 消 </el-button>
+        <el-button type="primary" @click="handleCategorySortSubmit"> 保存排序 </el-button>
+      </div>
+    </div>
+
+    <el-divider />
+
+    <!-- 按照分类,展示其所属的模型列表 -->
+    <div class="px-15px">
+      <draggable
+        :disabled="!isCategorySorting"
+        v-model="categoryGroup"
+        item-key="id"
+        :animation="400"
+      >
+        <template #item="{ element }">
+          <ContentWrap
+            class="rounded-lg transition-all duration-300 ease-in-out hover:shadow-xl"
+            v-loading="loading"
+            :body-style="{ padding: 0 }"
+            :key="element.id"
+          >
+            <CategoryDraggableModel
+              :isCategorySorting="isCategorySorting"
+              :categoryInfo="element"
+              @success="getList"
+            />
+          </ContentWrap>
+        </template>
+      </draggable>
+    </div>
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加分类 -->
+  <CategoryForm ref="categoryFormRef" @success="getList" />
+  <!-- 弹窗:表单详情 -->
+  <Dialog title="表单详情" v-model="formDetailVisible" width="800">
+    <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" />
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import draggable from 'vuedraggable'
+import { CategoryApi } from '@/api/bpm/category'
+import * as ModelApi from '@/api/bpm/model'
+import CategoryForm from '../category/CategoryForm.vue'
+import { cloneDeep } from 'lodash-es'
+import CategoryDraggableModel from './CategoryDraggableModel.vue'
+
+defineOptions({ name: 'BpmModel' })
+
+const { push } = useRouter()
+const message = useMessage() // 消息弹窗
+const loading = ref(true) // 列表的加载中
+const isCategorySorting = ref(false) // 是否 category 正处于排序状态
+const queryParams = reactive({
+  name: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+const categoryGroup: any = ref([]) // 按照 category 分组的数据
+const originalData: any = ref([]) // 原始数据
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  getList()
+}
+
+/** 添加/修改操作 */
+const openForm = (type: string, id?: number) => {
+  if (type === 'create') {
+    push({ name: 'BpmModelCreate' })
+  } else {
+    push({
+      name: 'BpmModelUpdate',
+      params: { id }
+    })
+  }
+}
+
+/** 流程表单的详情按钮操作 */
+const formDetailVisible = ref(false)
+const formDetailPreview = ref({
+  rule: [],
+  option: {}
+})
+
+/** 右上角设置按钮 */
+const handleCommand = (command: string) => {
+  switch (command) {
+    case 'handleCategoryAdd':
+      handleCategoryAdd()
+      break
+    case 'handleCategorySort':
+      handleCategorySort()
+      break
+    default:
+      break
+  }
+}
+
+/** 新建分类 */
+const categoryFormRef = ref()
+const handleCategoryAdd = () => {
+  categoryFormRef.value.open('create')
+}
+
+/** 分类排序的提交 */
+const handleCategorySort = () => {
+  // 保存初始数据
+  originalData.value = cloneDeep(categoryGroup.value)
+  isCategorySorting.value = true
+}
+
+/** 分类排序的取消 */
+const handleCategorySortCancel = () => {
+  // 恢复初始数据
+  categoryGroup.value = cloneDeep(originalData.value)
+  isCategorySorting.value = false
+}
+
+/** 分类排序的保存 */
+const handleCategorySortSubmit = async () => {
+  // 保存排序
+  const ids = categoryGroup.value.map((item: any) => item.id)
+  await CategoryApi.updateCategorySortBatch(ids)
+  // 刷新列表
+  isCategorySorting.value = false
+  message.success('排序分类成功')
+  await getList()
+}
+
+/** 加载数据 */
+const getList = async () => {
+  loading.value = true
+  try {
+    // 查询模型 + 分裂的列表
+    const modelList = await ModelApi.getModelList(queryParams.name)
+    const categoryList = await CategoryApi.getCategorySimpleList()
+    // 按照 category 聚合
+    // 注意:必须一次性赋值给 categoryGroup,否则每次操作后,列表会重新渲染,滚动条的位置会偏离!!!
+    categoryGroup.value = categoryList.map((category: any) => ({
+      ...category,
+      modelList: modelList.filter((model: any) => model.categoryName == category.name)
+    }))
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 初始化 **/
+onActivated(() => {
+  getList()
+})
+</script>
+
+<style lang="scss" scoped>
+:deep() {
+  .el-table--fit .el-table__inner-wrapper:before {
+    height: 0;
+  }
+  .el-card {
+    border-radius: 8px;
+  }
+  .el-form--inline .el-form-item {
+    margin-right: 10px;
+  }
+  .el-divider--horizontal {
+    margin-top: 6px;
+  }
+}
+</style>

+ 231 - 0
src/views/bpm/oa/leave/create.vue

@@ -0,0 +1,231 @@
+<template>
+  <el-row :gutter="20">
+    <el-col :span="16">
+      <ContentWrap title="申请信息">
+        <el-form
+          ref="formRef"
+          v-loading="formLoading"
+          :model="formData"
+          :rules="formRules"
+          label-width="80px"
+        >
+          <el-form-item label="请假类型" prop="type">
+            <el-select v-model="formData.type" clearable placeholder="请选择请假类型">
+              <el-option
+                v-for="dict in getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="开始时间" prop="startTime">
+            <el-date-picker
+              v-model="formData.startTime"
+              clearable
+              placeholder="请选择开始时间"
+              type="datetime"
+              value-format="x"
+            />
+          </el-form-item>
+          <el-form-item label="结束时间" prop="endTime">
+            <el-date-picker
+              v-model="formData.endTime"
+              clearable
+              placeholder="请选择结束时间"
+              type="datetime"
+              value-format="x"
+            />
+          </el-form-item>
+          <el-form-item label="原因" prop="reason">
+            <el-input v-model="formData.reason" placeholder="请输入请假原因" type="textarea" />
+          </el-form-item>
+          <el-form-item>
+            <el-button :disabled="formLoading" type="primary" @click="submitForm">
+              确 定
+            </el-button>
+          </el-form-item>
+        </el-form>
+      </ContentWrap>
+    </el-col>
+
+    <!-- 审批相关:流程信息 -->
+    <el-col :span="8">
+      <ContentWrap title="审批流程" :bodyStyle="{ padding: '0 20px 0' }">
+        <ProcessInstanceTimeline
+          ref="timelineRef"
+          :activity-nodes="activityNodes"
+          :show-status-icon="false"
+          @select-user-confirm="selectUserConfirm"
+        />
+      </ContentWrap>
+    </el-col>
+  </el-row>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as LeaveApi from '@/api/bpm/leave'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+
+// 审批相关:import
+import * as DefinitionApi from '@/api/bpm/definition'
+import ProcessInstanceTimeline from '@/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import { CandidateStrategy, NodeId } from '@/components/SimpleProcessDesignerV2/src/consts'
+import { ApprovalNodeInfo } from '@/api/bpm/processInstance'
+
+defineOptions({ name: 'BpmOALeaveCreate' })
+
+const message = useMessage() // 消息弹窗
+const { delView } = useTagsViewStore() // 视图操作
+const { push, currentRoute } = useRouter() // 路由
+
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref({
+  type: undefined,
+  reason: undefined,
+  startTime: undefined,
+  endTime: undefined
+})
+const formRules = reactive({
+  type: [{ required: true, message: '请假类型不能为空', trigger: 'blur' }],
+  reason: [{ required: true, message: '请假原因不能为空', trigger: 'change' }],
+  startTime: [{ required: true, message: '请假开始时间不能为空', trigger: 'change' }],
+  endTime: [{ required: true, message: '请假结束时间不能为空', trigger: 'change' }]
+})
+const formRef = ref() // 表单 Ref
+
+// 审批相关:变量
+const processDefineKey = 'oa_leave' // 流程定义 Key
+const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表
+const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
+const tempStartUserSelectAssignees = ref({}) // 历史发起人选择审批人的数据,用于每次表单变更时,临时保存
+const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) // 审批节点信息
+const processDefinitionId = ref('')
+
+/** 提交表单 */
+const submitForm = async () => {
+  // 1.1 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 1.2 审批相关:校验指定审批人
+  if (startUserSelectTasks.value?.length > 0) {
+    for (const userTask of startUserSelectTasks.value) {
+      if (
+        Array.isArray(startUserSelectAssignees.value[userTask.id]) &&
+        startUserSelectAssignees.value[userTask.id].length === 0
+      ) {
+        return message.warning(`请选择${userTask.name}的审批人`)
+      }
+    }
+  }
+
+  // 2. 提交请求
+  formLoading.value = true
+  try {
+    const data = { ...formData.value } as unknown as LeaveApi.LeaveVO
+    // 审批相关:设置指定审批人
+    if (startUserSelectTasks.value?.length > 0) {
+      data.startUserSelectAssignees = startUserSelectAssignees.value
+    }
+    await LeaveApi.createLeave(data)
+    message.success('发起成功')
+    // 关闭当前 Tab
+    delView(unref(currentRoute))
+    await push({ name: 'BpmOALeave' })
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 审批相关:获取审批详情 */
+const getApprovalDetail = async () => {
+  try {
+    const data = await ProcessInstanceApi.getApprovalDetail({
+      processDefinitionId: processDefinitionId.value,
+      // TODO 小北:可以支持 processDefinitionKey 查询
+      activityId: NodeId.START_USER_NODE_ID,
+      processVariablesStr: JSON.stringify({ day: daysDifference() }) // 解决 GET 无法传递对象的问题,后端 String 再转 JSON
+    })
+
+    if (!data) {
+      message.error('查询不到审批详情信息!')
+      return
+    }
+    // 获取审批节点,显示 Timeline 的数据
+    activityNodes.value = data.activityNodes
+
+    // 获取发起人自选的任务
+    startUserSelectTasks.value = data.activityNodes?.filter(
+      (node: ApprovalNodeInfo) => CandidateStrategy.START_USER_SELECT === node.candidateStrategy
+    )
+    // 恢复之前的选择审批人
+    if (startUserSelectTasks.value?.length > 0) {
+      for (const node of startUserSelectTasks.value) {
+        if (
+          tempStartUserSelectAssignees.value[node.id] &&
+          tempStartUserSelectAssignees.value[node.id].length > 0
+        ) {
+          startUserSelectAssignees.value[node.id] = tempStartUserSelectAssignees.value[node.id]
+        } else {
+          startUserSelectAssignees.value[node.id] = []
+        }
+      }
+    }
+  } finally {
+  }
+}
+
+/** 审批相关:选择发起人 */
+const selectUserConfirm = (id: string, userList: any[]) => {
+  startUserSelectAssignees.value[id] = userList?.map((item: any) => item.id)
+}
+
+// 计算天数差
+// TODO @小北:可以搞到 formatTime 里面去,然后看看 dayjs 里面有没有现成的方法,或者辅助计算的方法。
+const daysDifference = () => {
+  const oneDay = 24 * 60 * 60 * 1000 // 一天的毫秒数
+  const diffTime = Math.abs(Number(formData.value.endTime) - Number(formData.value.startTime))
+  return Math.floor(diffTime / oneDay)
+}
+
+/** 初始化 */
+onMounted(async () => {
+  // TODO @小北:这里可以简化,统一通过 getApprovalDetail 处理么?
+  const processDefinitionDetail = await DefinitionApi.getProcessDefinition(
+    undefined,
+    processDefineKey
+  )
+
+  if (!processDefinitionDetail) {
+    message.error('OA 请假的流程模型未配置,请检查!')
+    return
+  }
+  processDefinitionId.value = processDefinitionDetail.id
+  startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
+
+  // 审批相关:加载最新的审批详情,主要用于节点预测
+  await getApprovalDetail()
+})
+
+/** 审批相关:预测流程节点会因为输入的参数值而产生新的预测结果值,所以需重新预测一次, formData.value可改成实际业务中的特定字段 */
+watch(
+  formData.value,
+  (newValue, oldValue) => {
+    if (!oldValue) {
+      return
+    }
+    if (newValue && Object.keys(newValue).length > 0) {
+      // 记录之前的节点审批人
+      tempStartUserSelectAssignees.value = startUserSelectAssignees.value
+      startUserSelectAssignees.value = {}
+      // 加载最新的审批详情,主要用于节点预测
+      getApprovalDetail()
+    }
+  },
+  {
+    immediate: true
+  }
+)
+</script>

+ 51 - 0
src/views/bpm/oa/leave/detail.vue

@@ -0,0 +1,51 @@
+<template>
+  <ContentWrap>
+    <el-descriptions :column="1" border>
+      <el-descriptions-item label="请假类型">
+        <dict-tag :type="DICT_TYPE.BPM_OA_LEAVE_TYPE" :value="detailData.type" />
+      </el-descriptions-item>
+      <el-descriptions-item label="开始时间">
+        {{ formatDate(detailData.startTime, 'YYYY-MM-DD') }}
+      </el-descriptions-item>
+      <el-descriptions-item label="结束时间">
+        {{ formatDate(detailData.endTime, 'YYYY-MM-DD') }}
+      </el-descriptions-item>
+      <el-descriptions-item label="原因">
+        {{ detailData.reason }}
+      </el-descriptions-item>
+    </el-descriptions>
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import { propTypes } from '@/utils/propTypes'
+import * as LeaveApi from '@/api/bpm/leave'
+
+defineOptions({ name: 'BpmOALeaveDetail' })
+
+const { query } = useRoute() // 查询参数
+
+const props = defineProps({
+  id: propTypes.number.def(undefined)
+})
+const detailLoading = ref(false) // 表单的加载中
+const detailData = ref<any>({}) // 详情数据
+const queryId = query.id as unknown as number // 从 URL 传递过来的 id 编号
+
+/** 获得数据 */
+const getInfo = async () => {
+  detailLoading.value = true
+  try {
+    detailData.value = await LeaveApi.getLeave(props.id || queryId)
+  } finally {
+    detailLoading.value = false
+  }
+}
+defineExpose({ open: getInfo }) // 提供 open 方法,用于打开弹窗
+
+/** 初始化 **/
+onMounted(() => {
+  getInfo()
+})
+</script>

+ 256 - 0
src/views/bpm/oa/leave/index.vue

@@ -0,0 +1,256 @@
+<template>
+
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="68px"
+    >
+      <el-form-item label="请假类型" prop="type">
+        <el-select
+          v-model="queryParams.type"
+          class="!w-240px"
+          clearable
+          placeholder="请选择请假类型"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="申请时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+          end-placeholder="结束日期"
+          start-placeholder="开始日期"
+          type="daterange"
+          value-format="YYYY-MM-DD HH:mm:ss"
+        />
+      </el-form-item>
+      <el-form-item label="审批结果" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          class="!w-240px"
+          clearable
+          placeholder="请选择审批结果"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="原因" prop="reason">
+        <el-input
+          v-model="queryParams.reason"
+          class="!w-240px"
+          clearable
+          placeholder="请输入原因"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon class="mr-5px" icon="ep:search" />
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon class="mr-5px" icon="ep:refresh" />
+          重置
+        </el-button>
+        <el-button plain type="primary" @click="handleCreate()">
+          <Icon class="mr-5px" icon="ep:plus" />
+          发起请假
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column align="center" label="申请编号" prop="id" />
+      <el-table-column align="center" label="状态" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="开始时间"
+        prop="startTime"
+        width="180"
+      />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="结束时间"
+        prop="endTime"
+        width="180"
+      />
+      <el-table-column align="center" label="请假类型" prop="type">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.BPM_OA_LEAVE_TYPE" :value="scope.row.type" />
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="原因" prop="reason" />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="申请时间"
+        prop="createTime"
+        width="180"
+      />
+      <el-table-column align="center" label="操作" width="200">
+        <template #default="scope">
+          <el-button
+            v-hasPermi="['bpm:oa-leave:query']"
+            link
+            type="primary"
+            @click="handleDetail(scope.row)"
+          >
+            详情
+          </el-button>
+          <el-button
+            v-hasPermi="['bpm:oa-leave:query']"
+            link
+            type="primary"
+            @click="handleProcessDetail(scope.row)"
+          >
+            进度
+          </el-button>
+          <el-button
+            v-if="scope.row.result === 1"
+            v-hasPermi="['bpm:oa-leave:create']"
+            link
+            type="danger"
+            @click="cancelLeave(scope.row)"
+          >
+            取消
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as LeaveApi from '@/api/bpm/leave'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+
+defineOptions({ name: 'BpmOALeave' })
+
+const message = useMessage() // 消息弹窗
+const router = useRouter() // 路由
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  type: undefined,
+  status: undefined,
+  reason: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await LeaveApi.getLeavePage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加操作 */
+const handleCreate = () => {
+  router.push({ name: 'OALeaveCreate' })
+}
+
+/** 详情操作 */
+const handleDetail = (row: LeaveApi.LeaveVO) => {
+  router.push({
+    name: 'OALeaveDetail',
+    query: {
+      id: row.id
+    }
+  })
+}
+
+/** 取消请假操作 */
+const cancelLeave = async (row) => {
+  // 二次确认
+  const { value } = await ElMessageBox.prompt('请输入取消原因', '取消流程', {
+    confirmButtonText: t('common.ok'),
+    cancelButtonText: t('common.cancel'),
+    inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 判断非空,且非空格
+    inputErrorMessage: '取消原因不能为空'
+  })
+  // 发起取消
+  await ProcessInstanceApi.cancelProcessInstanceByStartUser(row.id, value)
+  message.success('取消成功')
+  // 刷新列表
+  await getList()
+}
+
+/** 审批进度 */
+const handleProcessDetail = (row) => {
+  router.push({
+    name: 'BpmProcessInstanceDetail',
+    query: {
+      id: row.processInstanceId
+    }
+  })
+}
+
+// fix: 列表不刷新的问题。
+watch(
+  () => router.currentRoute.value,
+  () => {
+    getList()
+  }
+)
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 114 - 0
src/views/bpm/processExpression/ProcessExpressionForm.vue

@@ -0,0 +1,114 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="名字" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入名字" />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :value="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="表达式" prop="expression">
+        <el-input type="textarea" v-model="formData.expression" placeholder="请输入表达式" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { ProcessExpressionApi, ProcessExpressionVO } from '@/api/bpm/processExpression'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** BPM 流程 表单 */
+defineOptions({ name: 'ProcessExpressionForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  status: undefined,
+  expression: undefined
+})
+const formRules = reactive({
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
+  expression: [{ required: true, message: '表达式不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await ProcessExpressionApi.getProcessExpression(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as ProcessExpressionVO
+    if (formType.value === 'create') {
+      await ProcessExpressionApi.createProcessExpression(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await ProcessExpressionApi.updateProcessExpression(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    status: CommonStatusEnum.ENABLE,
+    expression: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 181 - 0
src/views/bpm/processExpression/index.vue

@@ -0,0 +1,181 @@
+<template>
+
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="名字" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入名字"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['bpm:process-expression:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column label="表达式" align="center" prop="expression" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['bpm:process-expression:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['bpm:process-expression:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <ProcessExpressionForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { ProcessExpressionApi, ProcessExpressionVO } from '@/api/bpm/processExpression'
+import ProcessExpressionForm from './ProcessExpressionForm.vue'
+
+/** BPM 流程表达式列表 */
+defineOptions({ name: 'BpmProcessExpression' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<ProcessExpressionVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  status: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ProcessExpressionApi.getProcessExpressionPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await ProcessExpressionApi.deleteProcessExpression(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 331 - 0
src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue

@@ -0,0 +1,331 @@
+<template>
+  <ContentWrap :bodyStyle="{ padding: '10px 20px 0' }">
+    <div class="processInstance-wrap-main">
+      <el-scrollbar>
+        <div class="text-#878c93 h-15px">流程:{{ selectProcessDefinition.name }}</div>
+        <el-divider class="!my-8px" />
+
+        <!-- 中间主要内容 tab 栏 -->
+        <el-tabs v-model="activeTab">
+          <!-- 表单信息 -->
+          <el-tab-pane label="表单填写" name="form">
+            <div class="form-scroll-area" v-loading="processInstanceStartLoading">
+              <el-scrollbar>
+                <el-row>
+                  <el-col :span="17">
+                    <form-create
+                      :rule="detailForm.rule"
+                      v-model:api="fApi"
+                      v-model="detailForm.value"
+                      :option="detailForm.option"
+                      @submit="submitForm"
+                    />
+                  </el-col>
+
+                  <el-col :span="6" :offset="1">
+                    <!-- 流程时间线 -->
+                    <ProcessInstanceTimeline
+                      ref="timelineRef"
+                      :activity-nodes="activityNodes"
+                      :show-status-icon="false"
+                      @select-user-confirm="selectUserConfirm"
+                    />
+                  </el-col>
+                </el-row>
+              </el-scrollbar>
+            </div>
+          </el-tab-pane>
+          <!-- 流程图 -->
+          <el-tab-pane label="流程图" name="diagram">
+            <div class="form-scroll-area">
+              <!-- BPMN 流程图预览 -->
+              <ProcessInstanceBpmnViewer
+                :bpmn-xml="bpmnXML"
+                v-if="BpmModelType.BPMN === selectProcessDefinition.modelType"
+              />
+
+              <!-- Simple 流程图预览 -->
+              <ProcessInstanceSimpleViewer
+                :simple-json="simpleJson"
+                v-if="BpmModelType.SIMPLE === selectProcessDefinition.modelType"
+              />
+            </div>
+          </el-tab-pane>
+        </el-tabs>
+
+        <!-- 底部操作栏 -->
+        <div class="b-t-solid border-t-1px border-[var(--el-border-color)]">
+          <!-- 操作栏按钮 -->
+          <div
+            v-if="activeTab === 'form'"
+            class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
+          >
+            <el-button plain type="success" @click="submitForm">
+              <Icon icon="ep:select" />&nbsp; 发起
+            </el-button>
+            <el-button plain type="danger" @click="handleCancel">
+              <Icon icon="ep:close" />&nbsp; 取消
+            </el-button>
+          </div>
+        </div>
+      </el-scrollbar>
+    </div>
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { decodeFields, setConfAndFields2 } from '@/utils/formCreate'
+import { BpmModelType, BpmModelFormType } from '@/utils/constants'
+import {
+  CandidateStrategy,
+  NodeId,
+  FieldPermissionType
+} from '@/components/SimpleProcessDesignerV2/src/consts'
+import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
+import ProcessInstanceSimpleViewer from '../detail/ProcessInstanceSimpleViewer.vue'
+import ProcessInstanceTimeline from '../detail/ProcessInstanceTimeline.vue'
+import type { ApiAttrs } from '@form-create/element-ui/types/config'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import * as DefinitionApi from '@/api/bpm/definition'
+import { ApprovalNodeInfo } from '@/api/bpm/processInstance'
+
+defineOptions({ name: 'ProcessDefinitionDetail' })
+const props = defineProps<{
+  selectProcessDefinition: any
+}>()
+const emit = defineEmits(['cancel'])
+const processInstanceStartLoading = ref(false) // 流程实例发起中
+const { push, currentRoute } = useRouter() // 路由
+const message = useMessage() // 消息弹窗
+const { delView } = useTagsViewStore() // 视图操作
+
+const detailForm: any = ref({
+  rule: [],
+  option: {},
+  value: {}
+}) // 流程表单详情
+const fApi = ref<ApiAttrs>()
+// 指定审批人
+const startUserSelectTasks: any = ref([]) // 发起人需要选择审批人或抄送人的任务列表
+const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
+const tempStartUserSelectAssignees = ref({}) // 历史发起人选择审批人的数据,用于每次表单变更时,临时保存
+const bpmnXML: any = ref(null) // BPMN 数据
+const simpleJson = ref<string | undefined>() // Simple 设计器数据 json 格式
+
+const activeTab = ref('form') // 当前的 Tab
+const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) // 审批节点信息
+
+/** 设置表单信息、获取流程图数据 **/
+const initProcessInfo = async (row: any, formVariables?: any) => {
+  // 重置指定审批人
+  startUserSelectTasks.value = []
+  startUserSelectAssignees.value = {}
+
+  // 情况一:流程表单
+  if (row.formType == BpmModelFormType.NORMAL) {
+    // 设置表单
+    // 注意:需要从 formVariables 中,移除不在 row.formFields 的值。
+    // 原因是:后端返回的 formVariables 里面,会有一些非表单的信息。例如说,某个流程节点的审批人。
+    //        这样,就可能导致一个流程被审批不通过后,重新发起时,会直接后端报错!!!
+    const allowedFields = decodeFields(row.formFields).map((fieldObj: any) => fieldObj.field)
+    for (const key in formVariables) {
+      if (!allowedFields.includes(key)) {
+        delete formVariables[key]
+      }
+    }
+    setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
+
+    await nextTick()
+    fApi.value?.btn.show(false) // 隐藏提交按钮
+
+    // 获取流程审批信息,当再次发起时,流程审批节点要根据原始表单参数预测出来
+    await getApprovalDetail({
+      id: row.id,
+      processVariablesStr: JSON.stringify(formVariables)
+    })
+
+    // 加载流程图
+    const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id)
+    if (processDefinitionDetail) {
+      bpmnXML.value = processDefinitionDetail.bpmnXml
+      simpleJson.value = processDefinitionDetail.simpleModel
+    }
+    // 情况二:业务表单
+  } else if (row.formCustomCreatePath) {
+    await push({
+      path: row.formCustomCreatePath
+    })
+    // 这里暂时无需加载流程图,因为跳出到另外个 Tab;
+  }
+}
+
+/** 预测流程节点会因为输入的参数值而产生新的预测结果值,所以需重新预测一次 */
+watch(
+  detailForm.value,
+  (newValue) => {
+    if (newValue && Object.keys(newValue.value).length > 0) {
+      // 记录之前的节点审批人
+      tempStartUserSelectAssignees.value = startUserSelectAssignees.value
+      startUserSelectAssignees.value = {}
+      // 加载最新的审批详情
+      getApprovalDetail({
+        id: props.selectProcessDefinition.id,
+        processVariablesStr: JSON.stringify(newValue.value) // 解决 GET 无法传递对象的问题,后端 String 再转 JSON
+      })
+    }
+  },
+  {
+    immediate: true
+  }
+)
+
+/** 获取审批详情 */
+const getApprovalDetail = async (row: any) => {
+  try {
+    // TODO 获取审批详情,设置 activityId 为发起人节点(为了获取字段权限。暂时只对 Simple 设计器有效);@jason:这里可以去掉 activityId 么?
+    const data = await ProcessInstanceApi.getApprovalDetail({
+      processDefinitionId: row.id,
+      activityId: NodeId.START_USER_NODE_ID,
+      processVariablesStr: row.processVariablesStr // 解决 GET 无法传递对象的问题,后端 String 再转 JSON
+    })
+
+    if (!data) {
+      message.error('查询不到审批详情信息!')
+      return
+    }
+    // 获取审批节点,显示 Timeline 的数据
+    activityNodes.value = data.activityNodes
+
+    // 获取发起人自选的任务
+    startUserSelectTasks.value = data.activityNodes?.filter(
+      (node: ApprovalNodeInfo) => CandidateStrategy.START_USER_SELECT === node.candidateStrategy
+    )
+    // 恢复之前的选择审批人
+    if (startUserSelectTasks.value?.length > 0) {
+      for (const node of startUserSelectTasks.value) {
+        if (
+          tempStartUserSelectAssignees.value[node.id] &&
+          tempStartUserSelectAssignees.value[node.id].length > 0
+        ) {
+          startUserSelectAssignees.value[node.id] = tempStartUserSelectAssignees.value[node.id]
+        } else {
+          startUserSelectAssignees.value[node.id] = []
+        }
+      }
+    }
+
+    // 获取表单字段权限
+    const formFieldsPermission = data.formFieldsPermission
+    // 设置表单字段权限
+    if (formFieldsPermission) {
+      Object.keys(formFieldsPermission).forEach((item) => {
+        setFieldPermission(item, formFieldsPermission[item])
+      })
+    }
+  } finally {
+  }
+}
+
+/**
+ * 设置表单权限
+ */
+const setFieldPermission = (field: string, permission: string) => {
+  if (permission === FieldPermissionType.READ) {
+    //@ts-ignore
+    fApi.value?.disabled(true, field)
+  }
+  if (permission === FieldPermissionType.WRITE) {
+    //@ts-ignore
+    fApi.value?.disabled(false, field)
+  }
+  if (permission === FieldPermissionType.NONE) {
+    //@ts-ignore
+    fApi.value?.hidden(true, field)
+  }
+}
+
+/** 提交按钮 */
+const submitForm = async () => {
+  if (!fApi.value || !props.selectProcessDefinition) {
+    return
+  }
+  // 流程表单校验
+  await fApi.value.validate()
+  // 如果有指定审批人,需要校验
+  if (startUserSelectTasks.value?.length > 0) {
+    for (const userTask of startUserSelectTasks.value) {
+      if (
+        Array.isArray(startUserSelectAssignees.value[userTask.id]) &&
+        startUserSelectAssignees.value[userTask.id].length === 0
+      )
+        return message.warning(`请选择${userTask.name}的候选人`)
+    }
+  }
+
+  // 提交请求
+  processInstanceStartLoading.value = true
+  try {
+    await ProcessInstanceApi.createProcessInstance({
+      processDefinitionId: props.selectProcessDefinition.id,
+      variables: detailForm.value.value,
+      startUserSelectAssignees: startUserSelectAssignees.value
+    })
+    // 提示
+    message.success('发起流程成功')
+    // 跳转回去
+    delView(unref(currentRoute))
+    await push({
+      name: 'BpmProcessInstanceMy'
+    })
+  } finally {
+    processInstanceStartLoading.value = false
+  }
+}
+
+/** 取消发起审批 */
+const handleCancel = () => {
+  emit('cancel')
+}
+
+/** 选择发起人 */
+const selectUserConfirm = (id: string, userList: any[]) => {
+  startUserSelectAssignees.value[id] = userList?.map((item: any) => item.id)
+}
+
+defineExpose({ initProcessInfo })
+</script>
+
+<style lang="scss" scoped>
+$wrap-padding-height: 20px;
+$wrap-margin-height: 15px;
+$button-height: 51px;
+$process-header-height: 105px;
+
+.processInstance-wrap-main {
+  height: calc(
+    100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
+  );
+  max-height: calc(
+    100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
+  );
+  overflow: auto;
+
+  .form-scroll-area {
+    height: calc(
+      100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
+        $process-header-height - 40px
+    );
+    max-height: calc(
+      100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
+        $process-header-height - 40px
+    );
+    overflow: auto;
+  }
+}
+
+.form-box {
+  :deep(.el-card) {
+    border: none;
+  }
+}
+</style>

+ 321 - 0
src/views/bpm/processInstance/create/index.vue

@@ -0,0 +1,321 @@
+<template>
+  <!-- 第一步,通过流程定义的列表,选择对应的流程 -->
+  <template v-if="!selectProcessDefinition">
+    <el-input
+      v-model="searchName"
+      class="!w-50% mb-15px"
+      placeholder="请输入流程名称"
+      clearable
+      @input="handleQuery"
+      @clear="handleQuery"
+    >
+      <template #prefix>
+        <Icon icon="ep:search" />
+      </template>
+    </el-input>
+    <ContentWrap
+      :class="{ 'process-definition-container': filteredProcessDefinitionList?.length }"
+      class="position-relative pb-20px h-700px"
+      v-loading="loading"
+    >
+      <el-row v-if="filteredProcessDefinitionList?.length" :gutter="20" class="!flex-nowrap">
+        <el-col :span="5">
+          <div class="flex flex-col">
+            <div
+              v-for="category in availableCategories"
+              :key="category.code"
+              class="flex items-center p-10px cursor-pointer text-14px rounded-md"
+              :class="categoryActive.code === category.code ? 'text-#3e7bff bg-#e8eeff' : ''"
+              @click="handleCategoryClick(category)"
+            >
+              {{ category.name }}
+            </div>
+          </div>
+        </el-col>
+        <el-col :span="19">
+          <el-scrollbar ref="scrollWrapper" height="700" @scroll="handleScroll">
+            <div
+              class="mb-20px pl-10px"
+              v-for="(definitions, categoryCode) in processDefinitionGroup"
+              :key="categoryCode"
+              :ref="`category-${categoryCode}`"
+            >
+              <h3 class="text-18px font-bold mb-10px mt-5px">
+                {{ getCategoryName(categoryCode as any) }}
+              </h3>
+              <div class="grid grid-cols-3 gap3">
+                <el-tooltip
+                  v-for="definition in definitions"
+                  :key="definition.id"
+                  :content="definition.description"
+                  :disabled="!definition.description || definition.description.trim().length === 0"
+                  placement="top"
+                >
+                  <el-card
+                    shadow="hover"
+                    class="cursor-pointer definition-item-card"
+                    @click="handleSelect(definition)"
+                  >
+                    <template #default>
+                      <div class="flex">
+                        <el-image
+                          v-if="definition.icon"
+                          :src="definition.icon"
+                          class="w-32px h-32px"
+                        />
+                        <div v-else class="flow-icon">
+                          <span style="font-size: 12px; color: #fff">
+                            {{ subString(definition.name, 0, 2) }}
+                          </span>
+                        </div>
+                        <el-text class="!ml-10px" size="large">{{ definition.name }}</el-text>
+                      </div>
+                    </template>
+                  </el-card>
+                </el-tooltip>
+              </div>
+            </div>
+          </el-scrollbar>
+        </el-col>
+      </el-row>
+      <el-empty class="!py-200px" :image-size="200" description="没有找到搜索结果" v-else />
+    </ContentWrap>
+  </template>
+
+  <!-- 第二步,填写表单,进行流程的提交 -->
+  <ProcessDefinitionDetail
+    v-else
+    ref="processDefinitionDetailRef"
+    :selectProcessDefinition="selectProcessDefinition"
+    @cancel="selectProcessDefinition = undefined"
+  />
+</template>
+
+<script lang="ts" setup>
+import * as DefinitionApi from '@/api/bpm/definition'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+import ProcessDefinitionDetail from './ProcessDefinitionDetail.vue'
+import { groupBy } from 'lodash-es'
+import { subString } from '@/utils/index'
+
+defineOptions({ name: 'BpmProcessInstanceCreate' })
+
+const { proxy } = getCurrentInstance() as any
+const route = useRoute() // 路由
+const message = useMessage() // 消息
+
+const searchName = ref('') // 当前搜索关键字
+const processInstanceId: any = route.query.processInstanceId // 流程实例编号。场景:重新发起时
+const loading = ref(true) // 加载中
+const categoryList: any = ref([]) // 分类的列表
+const categoryActive: any = ref({}) // 选中的分类
+const processDefinitionList = ref([]) // 流程定义的列表
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    // 所有流程分类数据
+    await getCategoryList()
+    // 所有流程定义数据
+    await getProcessDefinitionList()
+
+    // 如果 processInstanceId 非空,说明是重新发起
+    if (processInstanceId?.length > 0) {
+      const processInstance = await ProcessInstanceApi.getProcessInstance(processInstanceId)
+      if (!processInstance) {
+        message.error('重新发起流程失败,原因:流程实例不存在')
+        return
+      }
+      const processDefinition = processDefinitionList.value.find(
+        (item: any) => item.key == processInstance.processDefinition?.key
+      )
+      if (!processDefinition) {
+        message.error('重新发起流程失败,原因:流程定义不存在')
+        return
+      }
+      await handleSelect(processDefinition, processInstance.formVariables)
+    }
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 获取所有流程分类数据 */
+const getCategoryList = async () => {
+  try {
+    // 流程分类
+    categoryList.value = await CategoryApi.getCategorySimpleList()
+  } finally {
+  }
+}
+
+/** 获取所有流程定义数据 */
+const getProcessDefinitionList = async () => {
+  try {
+    // 流程定义
+    processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
+      suspensionState: 1
+    })
+    // 初始化过滤列表为全部流程定义
+    filteredProcessDefinitionList.value = processDefinitionList.value
+
+    // 在获取完所有数据后,设置第一个有效分类为激活状态
+    if (availableCategories.value.length > 0 && !categoryActive.value?.code) {
+      categoryActive.value = availableCategories.value[0]
+    }
+  } finally {
+  }
+}
+
+/** 搜索流程 */
+const filteredProcessDefinitionList = ref([]) // 用于存储搜索过滤后的流程定义
+const handleQuery = () => {
+  if (searchName.value.trim()) {
+    // 如果有搜索关键字,进行过滤
+    filteredProcessDefinitionList.value = processDefinitionList.value.filter(
+      (definition: any) => definition.name.toLowerCase().includes(searchName.value.toLowerCase()) // 假设搜索依据是流程定义的名称
+    )
+  } else {
+    // 如果没有搜索关键字,恢复所有数据
+    filteredProcessDefinitionList.value = processDefinitionList.value
+  }
+}
+
+/** 流程定义的分组 */
+const processDefinitionGroup: any = computed(() => {
+  if (!processDefinitionList.value?.length) {
+    return {}
+  }
+
+  const grouped = groupBy(filteredProcessDefinitionList.value, 'category')
+  // 按照 categoryList 的顺序重新组织数据
+  const orderedGroup = {}
+  categoryList.value.forEach((category: any) => {
+    if (grouped[category.code]) {
+      orderedGroup[category.code] = grouped[category.code]
+    }
+  })
+  return orderedGroup
+})
+
+/** 左侧分类切换 */
+const handleCategoryClick = (category: any) => {
+  categoryActive.value = category
+  const categoryRef = proxy.$refs[`category-${category.code}`] // 获取点击分类对应的 DOM 元素
+  if (categoryRef?.length) {
+    const scrollWrapper = proxy.$refs.scrollWrapper // 获取右侧滚动容器
+    const categoryOffsetTop = categoryRef[0].offsetTop
+
+    // 滚动到对应位置
+    scrollWrapper.scrollTo({ top: categoryOffsetTop, behavior: 'smooth' })
+  }
+}
+
+/** 通过分类 code 获取对应的名称 */
+const getCategoryName = (categoryCode: string) => {
+  return categoryList.value?.find((ctg: any) => ctg.code === categoryCode)?.name
+}
+
+// ========== 表单相关 ==========
+const selectProcessDefinition = ref() // 选择的流程定义
+const processDefinitionDetailRef = ref()
+
+/** 处理选择流程的按钮操作 **/
+const handleSelect = async (row, formVariables?) => {
+  // 设置选择的流程
+  selectProcessDefinition.value = row
+  // 初始化流程定义详情
+  await nextTick()
+  processDefinitionDetailRef.value?.initProcessInfo(row, formVariables)
+}
+
+/** 处理滚动事件,和左侧分类联动 */
+const handleScroll = (e: any) => {
+  // 直接使用事件对象获取滚动位置
+  const scrollTop = e.scrollTop
+
+  // 获取所有分类区域的位置信息
+  const categoryPositions = categoryList.value
+    .map((category: CategoryVO) => {
+      const categoryRef = proxy.$refs[`category-${category.code}`]
+      if (categoryRef?.[0]) {
+        return {
+          code: category.code,
+          offsetTop: categoryRef[0].offsetTop,
+          height: categoryRef[0].offsetHeight
+        }
+      }
+      return null
+    })
+    .filter(Boolean)
+
+  // 查找当前滚动位置对应的分类
+  let currentCategory = categoryPositions[0]
+  for (const position of categoryPositions) {
+    // 为了更好的用户体验,可以添加一个缓冲区域(比如 50px)
+    if (scrollTop >= position.offsetTop - 50) {
+      currentCategory = position
+    } else {
+      break
+    }
+  }
+
+  // 更新当前 active 的分类
+  if (currentCategory && categoryActive.value.code !== currentCategory.code) {
+    categoryActive.value = categoryList.value.find(
+      (c: CategoryVO) => c.code === currentCategory.code
+    )
+  }
+}
+
+/** 过滤出有流程的分类列表。目的:只展示有流程的分类 */
+const availableCategories = computed(() => {
+  if (!categoryList.value?.length || !processDefinitionGroup.value) {
+    return []
+  }
+
+  // 获取所有有流程的分类代码
+  const availableCategoryCodes = Object.keys(processDefinitionGroup.value)
+
+  // 过滤出有流程的分类
+  return categoryList.value.filter((category: CategoryVO) =>
+    availableCategoryCodes.includes(category.code)
+  )
+})
+
+/** 初始化 */
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style lang="scss" scoped>
+.flow-icon {
+  display: flex;
+  width: 32px;
+  height: 32px;
+  margin-right: 10px;
+  background-color: var(--el-color-primary);
+  border-radius: 0.25rem;
+  align-items: center;
+  justify-content: center;
+}
+
+.process-definition-container::before {
+  position: absolute;
+  left: 20.8%;
+  height: 100%;
+  border-left: 1px solid #e6e6e6;
+  content: '';
+}
+
+:deep() {
+  .definition-item-card {
+    .el-card__body {
+      padding: 14px;
+    }
+  }
+}
+</style>

+ 61 - 0
src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue

@@ -0,0 +1,61 @@
+<template>
+  <el-card v-loading="loading" class="box-card">
+    <MyProcessViewer key="designer" :xml="view.bpmnXml" :view="view" class="process-viewer" />
+  </el-card>
+</template>
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
+
+defineOptions({ name: 'BpmProcessInstanceBpmnViewer' })
+
+const props = defineProps({
+  loading: propTypes.bool.def(false), // 是否加载中
+  bpmnXml: propTypes.string, // BPMN XML
+  modelView: propTypes.object
+})
+
+const view = ref({
+  bpmnXml: ''
+}) // BPMN 流程图数据
+
+
+/** 只有 loading 完成时,才去加载流程列表 */
+watch(
+  () => props.modelView,
+  async (newModelView) => {
+    // 加载最新
+    if (newModelView) {
+      //@ts-ignore
+      view.value = newModelView
+    }
+  }
+)
+
+/** 监听 bpmnXml */
+watch(
+  () => props.bpmnXml,
+  (value) => {
+    view.value.bpmnXml = value
+  }
+)
+</script>
+<style lang="scss" scoped>
+.box-card {
+  height: 100%;
+  width: 100%;
+  margin-bottom: 0;
+
+  :deep(.el-card__body) {
+    height: 100%;
+    padding: 0;
+  }
+
+  :deep(.process-viewer) {
+    height: 100% !important;
+    min-height: 100%;
+    width: 100%;
+    overflow: auto;
+  }
+}
+</style>

+ 1116 - 0
src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue

@@ -0,0 +1,1116 @@
+<template>
+  <div
+    class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
+  >
+    <!-- 【通过】按钮 -->
+    <el-popover
+      :visible="popOverVisible.approve"
+      placement="top-end"
+      :width="420"
+      trigger="click"
+      v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.APPROVE)"
+    >
+      <template #reference>
+        <el-button plain type="success" @click="openPopover('approve')">
+          <Icon icon="ep:select" />&nbsp; {{ getButtonDisplayName(OperationButtonType.APPROVE) }}
+        </el-button>
+      </template>
+      <!-- 审批表单 -->
+      <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
+        <el-form
+          label-position="top"
+          class="mb-auto"
+          ref="approveFormRef"
+          :model="approveReasonForm"
+          :rules="approveReasonRule"
+          label-width="100px"
+        >
+          <el-card v-if="runningTask?.formId > 0" class="mb-15px !-mt-10px">
+            <template #header>
+              <span class="el-icon-picture-outline"> 填写表单【{{ runningTask?.formName }}】 </span>
+            </template>
+            <form-create
+              v-model="approveForm.value"
+              v-model:api="approveFormFApi"
+              :option="approveForm.option"
+              :rule="approveForm.rule"
+            />
+          </el-card>
+          <el-form-item :label="`${nodeTypeName}意见`" prop="reason">
+            <el-input
+              v-model="approveReasonForm.reason"
+              :placeholder="`请输入${nodeTypeName}意见`"
+              type="textarea"
+              :rows="4"
+            />
+          </el-form-item>
+          <el-form-item
+            label="下一个节点的审批人"
+            prop="nextAssignees"
+            v-if="nextAssigneesActivityNode.length > 0"
+          >
+            <div class="ml-10px -mt-15px -mb-35px">
+              <ProcessInstanceTimeline
+                :activity-nodes="nextAssigneesActivityNode"
+                :show-status-icon="false"
+                @select-user-confirm="selectNextAssigneesConfirm"
+              />
+            </div>
+          </el-form-item>
+          <el-form-item
+            v-if="runningTask.signEnable"
+            label="签名"
+            prop="signPicUrl"
+            ref="approveSignFormRef"
+          >
+            <el-button @click="signRef.open()">点击签名</el-button>
+            <el-image
+              class="w-90px h-40px ml-5px"
+              v-if="approveReasonForm.signPicUrl"
+              :src="approveReasonForm.signPicUrl"
+              :preview-src-list="[approveReasonForm.signPicUrl]"
+            />
+          </el-form-item>
+          <el-form-item>
+            <el-button
+              :disabled="formLoading"
+              type="success"
+              @click="handleAudit(true, approveFormRef)"
+            >
+              {{ getButtonDisplayName(OperationButtonType.APPROVE) }}
+            </el-button>
+            <el-button @click="closePopover('approve', approveFormRef)"> 取消 </el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+    </el-popover>
+
+    <!-- 【拒绝】按钮 -->
+    <el-popover
+      :visible="popOverVisible.reject"
+      placement="top-end"
+      :width="420"
+      trigger="click"
+      v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.REJECT)"
+    >
+      <template #reference>
+        <el-button class="mr-20px" plain type="danger" @click="openPopover('reject')">
+          <Icon icon="ep:close" />&nbsp; {{ getButtonDisplayName(OperationButtonType.REJECT) }}
+        </el-button>
+      </template>
+      <!-- 审批表单 -->
+      <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
+        <el-form
+          label-position="top"
+          class="mb-auto"
+          ref="rejectFormRef"
+          :model="rejectReasonForm"
+          :rules="rejectReasonRule"
+          label-width="100px"
+        >
+          <el-form-item label="审批意见" prop="reason">
+            <el-input
+              v-model="rejectReasonForm.reason"
+              placeholder="请输入审批意见"
+              type="textarea"
+              :rows="4"
+            />
+          </el-form-item>
+          <el-form-item>
+            <el-button
+              :disabled="formLoading"
+              type="danger"
+              @click="handleAudit(false, rejectFormRef)"
+            >
+              {{ getButtonDisplayName(OperationButtonType.REJECT) }}
+            </el-button>
+            <el-button @click="closePopover('reject', rejectFormRef)"> 取消 </el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+    </el-popover>
+
+    <!-- 【抄送】按钮 -->
+    <el-popover
+      :visible="popOverVisible.copy"
+      placement="top-start"
+      :width="420"
+      trigger="click"
+      v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.COPY)"
+    >
+      <template #reference>
+        <div @click="openPopover('copy')" class="hover-bg-gray-100 rounded-xl p-6px">
+          <Icon :size="14" icon="svg-icon:send" />&nbsp;
+          {{ getButtonDisplayName(OperationButtonType.COPY) }}
+        </div>
+      </template>
+      <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
+        <el-form
+          label-position="top"
+          class="mb-auto"
+          ref="copyFormRef"
+          :model="copyForm"
+          :rules="copyFormRule"
+          label-width="100px"
+        >
+          <el-form-item label="抄送人" prop="copyUserIds">
+            <el-select
+              v-model="copyForm.copyUserIds"
+              clearable
+              style="width: 100%"
+              multiple
+              placeholder="请选择抄送人"
+            >
+              <el-option
+                v-for="item in userOptions"
+                :key="item.id"
+                :label="item.nickname"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="抄送意见" prop="copyReason">
+            <el-input
+              v-model="copyForm.copyReason"
+              clearable
+              placeholder="请输入抄送意见"
+              type="textarea"
+              :rows="3"
+            />
+          </el-form-item>
+          <el-form-item>
+            <el-button :disabled="formLoading" type="primary" @click="handleCopy">
+              {{ getButtonDisplayName(OperationButtonType.COPY) }}
+            </el-button>
+            <el-button @click="closePopover('copy', copyFormRef)"> 取消 </el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+    </el-popover>
+
+    <!-- 【转办】按钮 -->
+    <el-popover
+      :visible="popOverVisible.transfer"
+      placement="top-start"
+      :width="420"
+      trigger="click"
+      v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.TRANSFER)"
+    >
+      <template #reference>
+        <div @click="openPopover('transfer')" class="hover-bg-gray-100 rounded-xl p-6px">
+          <Icon :size="14" icon="fa:share-square-o" />&nbsp;
+          {{ getButtonDisplayName(OperationButtonType.TRANSFER) }}
+        </div>
+      </template>
+      <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
+        <el-form
+          label-position="top"
+          class="mb-auto"
+          ref="transferFormRef"
+          :model="transferForm"
+          :rules="transferFormRule"
+          label-width="100px"
+        >
+          <el-form-item label="新审批人" prop="assigneeUserId">
+            <el-select v-model="transferForm.assigneeUserId" clearable style="width: 100%">
+              <el-option
+                v-for="item in userOptions"
+                :key="item.id"
+                :label="item.nickname"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="审批意见" prop="reason">
+            <el-input
+              v-model="transferForm.reason"
+              clearable
+              placeholder="请输入审批意见"
+              type="textarea"
+              :rows="3"
+            />
+          </el-form-item>
+          <el-form-item>
+            <el-button :disabled="formLoading" type="primary" @click="handleTransfer()">
+              {{ getButtonDisplayName(OperationButtonType.TRANSFER) }}
+            </el-button>
+            <el-button @click="closePopover('transfer', transferFormRef)"> 取消 </el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+    </el-popover>
+
+    <!-- 【委派】按钮 -->
+    <el-popover
+      :visible="popOverVisible.delegate"
+      placement="top-start"
+      :width="420"
+      trigger="click"
+      v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.DELEGATE)"
+    >
+      <template #reference>
+        <div @click="openPopover('delegate')" class="hover-bg-gray-100 rounded-xl p-6px">
+          <Icon :size="14" icon="ep:position" />&nbsp;
+          {{ getButtonDisplayName(OperationButtonType.DELEGATE) }}
+        </div>
+      </template>
+      <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
+        <el-form
+          label-position="top"
+          class="mb-auto"
+          ref="delegateFormRef"
+          :model="delegateForm"
+          :rules="delegateFormRule"
+          label-width="100px"
+        >
+          <el-form-item label="接收人" prop="delegateUserId">
+            <el-select v-model="delegateForm.delegateUserId" clearable style="width: 100%">
+              <el-option
+                v-for="item in userOptions"
+                :key="item.id"
+                :label="item.nickname"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="审批意见" prop="reason">
+            <el-input
+              v-model="delegateForm.reason"
+              clearable
+              placeholder="请输入审批意见"
+              type="textarea"
+              :rows="3"
+            />
+          </el-form-item>
+          <el-form-item>
+            <el-button :disabled="formLoading" type="primary" @click="handleDelegate()">
+              {{ getButtonDisplayName(OperationButtonType.DELEGATE) }}
+            </el-button>
+            <el-button @click="closePopover('delegate', delegateFormRef)"> 取消 </el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+    </el-popover>
+
+    <!-- 【加签】按钮 当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 -->
+    <el-popover
+      :visible="popOverVisible.addSign"
+      placement="top-start"
+      :width="420"
+      trigger="click"
+      v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.ADD_SIGN)"
+    >
+      <template #reference>
+        <div @click="openPopover('addSign')" class="hover-bg-gray-100 rounded-xl p-6px">
+          <Icon :size="14" icon="ep:plus" />&nbsp;
+          {{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }}
+        </div>
+      </template>
+      <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
+        <el-form
+          label-position="top"
+          class="mb-auto"
+          ref="addSignFormRef"
+          :model="addSignForm"
+          :rules="addSignFormRule"
+          label-width="100px"
+        >
+          <el-form-item label="加签处理人" prop="addSignUserIds">
+            <el-select v-model="addSignForm.addSignUserIds" multiple clearable style="width: 100%">
+              <el-option
+                v-for="item in userOptions"
+                :key="item.id"
+                :label="item.nickname"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="审批意见" prop="reason">
+            <el-input
+              v-model="addSignForm.reason"
+              clearable
+              placeholder="请输入审批意见"
+              type="textarea"
+              :rows="3"
+            />
+          </el-form-item>
+          <el-form-item>
+            <el-button :disabled="formLoading" type="primary" @click="handlerAddSign('before')">
+              向前{{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }}
+            </el-button>
+            <el-button :disabled="formLoading" type="primary" @click="handlerAddSign('after')">
+              向后{{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }}
+            </el-button>
+            <el-button @click="closePopover('addSign', addSignFormRef)"> 取消 </el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+    </el-popover>
+
+    <!-- 【减签】按钮 -->
+    <el-popover
+      :visible="popOverVisible.deleteSign"
+      placement="top-start"
+      :width="420"
+      trigger="click"
+      v-if="runningTask?.children.length > 0"
+    >
+      <template #reference>
+        <div @click="openPopover('deleteSign')" class="hover-bg-gray-100 rounded-xl p-6px">
+          <Icon :size="14" icon="ep:semi-select" />&nbsp; 减签
+        </div>
+      </template>
+      <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
+        <el-form
+          label-position="top"
+          class="mb-auto"
+          ref="deleteSignFormRef"
+          :model="deleteSignForm"
+          :rules="deleteSignFormRule"
+          label-width="100px"
+        >
+          <el-form-item label="减签人员" prop="deleteSignTaskId">
+            <el-select v-model="deleteSignForm.deleteSignTaskId" clearable style="width: 100%">
+              <el-option
+                v-for="item in runningTask.children"
+                :key="item.id"
+                :label="getDeleteSignUserLabel(item)"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="审批意见" prop="reason">
+            <el-input
+              v-model="deleteSignForm.reason"
+              clearable
+              placeholder="请输入审批意见"
+              type="textarea"
+              :rows="3"
+            />
+          </el-form-item>
+          <el-form-item>
+            <el-button :disabled="formLoading" type="primary" @click="handlerDeleteSign()">
+              减签
+            </el-button>
+            <el-button @click="closePopover('deleteSign', deleteSignFormRef)"> 取消 </el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+    </el-popover>
+
+    <!-- 【退回】按钮 -->
+    <el-popover
+      :visible="popOverVisible.return"
+      placement="top-start"
+      :width="420"
+      trigger="click"
+      v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.RETURN)"
+    >
+      <template #reference>
+        <div @click="openPopover('return')" class="hover-bg-gray-100 rounded-xl p-6px">
+          <Icon :size="14" icon="ep:back" />&nbsp;
+          {{ getButtonDisplayName(OperationButtonType.RETURN) }}
+        </div>
+      </template>
+      <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
+        <el-form
+          label-position="top"
+          class="mb-auto"
+          ref="returnFormRef"
+          :model="returnForm"
+          :rules="returnFormRule"
+          label-width="100px"
+        >
+          <el-form-item label="退回节点" prop="targetTaskDefinitionKey">
+            <el-select v-model="returnForm.targetTaskDefinitionKey" clearable style="width: 100%">
+              <el-option
+                v-for="item in returnList"
+                :key="item.taskDefinitionKey"
+                :label="item.name"
+                :value="item.taskDefinitionKey"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="退回理由" prop="returnReason">
+            <el-input
+              v-model="returnForm.returnReason"
+              clearable
+              placeholder="请输入退回理由"
+              type="textarea"
+              :rows="3"
+            />
+          </el-form-item>
+          <el-form-item>
+            <el-button :disabled="formLoading" type="primary" @click="handleReturn()">
+              {{ getButtonDisplayName(OperationButtonType.RETURN) }}
+            </el-button>
+            <el-button @click="closePopover('return', returnFormRef)"> 取消 </el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+    </el-popover>
+
+    <!--【取消】按钮 这个对应发起人的取消, 只有发起人可以取消 -->
+    <el-popover
+      :visible="popOverVisible.cancel"
+      placement="top-start"
+      :width="420"
+      trigger="click"
+      v-if="
+        userId === processInstance?.startUser?.id && !isEndProcessStatus(processInstance?.status)
+      "
+    >
+      <template #reference>
+        <div @click="openPopover('cancel')" class="hover-bg-gray-100 rounded-xl p-6px">
+          <Icon :size="14" icon="fa:mail-reply" />&nbsp; 取消
+        </div>
+      </template>
+      <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
+        <el-form
+          label-position="top"
+          class="mb-auto"
+          ref="cancelFormRef"
+          :model="cancelForm"
+          :rules="cancelFormRule"
+          label-width="100px"
+        >
+          <el-form-item label="取消理由" prop="cancelReason">
+            <span class="text-#878c93 text-12px">&nbsp; 取消后,该审批流程将自动结束</span>
+            <el-input
+              v-model="cancelForm.cancelReason"
+              clearable
+              placeholder="请输入取消理由"
+              type="textarea"
+              :rows="3"
+            />
+          </el-form-item>
+          <el-form-item>
+            <el-button :disabled="formLoading" type="primary" @click="handleCancel()">
+              确认
+            </el-button>
+            <el-button @click="closePopover('cancel', cancelFormRef)"> 取消 </el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+    </el-popover>
+    <!-- 【再次提交】 按钮-->
+    <div
+      @click="handleReCreate()"
+      class="hover-bg-gray-100 rounded-xl p-6px"
+      v-if="
+        userId === processInstance?.startUser?.id &&
+        isEndProcessStatus(processInstance?.status) &&
+        processDefinition?.formType === 10
+      "
+    >
+      <Icon :size="14" icon="ep:refresh" />&nbsp; 再次提交
+    </div>
+  </div>
+
+  <!-- 签名弹窗 -->
+  <SignDialog ref="signRef" @success="handleSignFinish" />
+</template>
+<script lang="ts" setup>
+import { useUserStoreWithOut } from '@/store/modules/user'
+import { setConfAndFields2 } from '@/utils/formCreate'
+import * as TaskApi from '@/api/bpm/task'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import * as UserApi from '@/api/system/user'
+import {
+  NodeType,
+  OPERATION_BUTTON_NAME,
+  OperationButtonType,
+  CandidateStrategy
+} from '@/components/SimpleProcessDesignerV2/src/consts'
+import { BpmModelFormType, BpmProcessInstanceStatus } from '@/utils/constants'
+import type { FormInstance, FormRules } from 'element-plus'
+import SignDialog from './SignDialog.vue'
+import ProcessInstanceTimeline from '../detail/ProcessInstanceTimeline.vue'
+import { isEmpty } from '@/utils/is'
+
+defineOptions({ name: 'ProcessInstanceBtnContainer' })
+
+const router = useRouter() // 路由
+const message = useMessage() // 消息弹窗
+
+const userId = useUserStoreWithOut().getUser.id // 当前登录的编号
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+
+const props = defineProps<{
+  processInstance: any // 流程实例信息
+  processDefinition: any // 流程定义信息
+  userOptions: UserApi.UserVO[]
+  normalForm: any // 流程表单 formCreate
+  normalFormApi: any // 流程表单 formCreate Api
+  writableFields: string[] // 流程表单可以编辑的字段
+}>()
+
+const formLoading = ref(false) // 表单加载中
+const popOverVisible = ref({
+  approve: false,
+  reject: false,
+  transfer: false,
+  delegate: false,
+  addSign: false,
+  return: false,
+  copy: false,
+  cancel: false,
+  deleteSign: false
+}) // 气泡卡是否展示
+const returnList = ref([] as any) // 退回节点
+
+// ========== 审批信息 ==========
+const runningTask = ref<any>() // 运行中的任务
+const approveForm = ref<any>({}) // 审批通过时,额外的补充信息
+const approveFormFApi = ref<any>({}) // approveForms 的 fAPi
+const nodeTypeName = ref('审批') // 节点类型名称
+
+// 审批通过意见表单
+const reasonRequire = ref()
+const approveFormRef = ref<FormInstance>()
+const signRef = ref()
+const approveSignFormRef = ref()
+const nextAssigneesActivityNode = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) // 下一个审批节点信息
+const approveReasonForm = reactive({
+  reason: '',
+  signPicUrl: '',
+  nextAssignees: {}
+})
+const approveReasonRule = computed(() => {
+  return {
+    reason: [
+      { required: reasonRequire.value, message: nodeTypeName + '意见不能为空', trigger: 'blur' }
+    ],
+    signPicUrl: [{ required: true, message: '签名不能为空', trigger: 'change' }],
+    nextAssignees: [{ required: true, message: '审批人不能为空', trigger: 'blur' }]
+  }
+})
+
+// 拒绝表单
+const rejectFormRef = ref<FormInstance>()
+const rejectReasonForm = reactive({
+  reason: ''
+})
+const rejectReasonRule = computed(() => {
+  return {
+    reason: [{ required: reasonRequire.value, message: '审批意见不能为空', trigger: 'blur' }]
+  }
+})
+
+// 抄送表单
+const copyFormRef = ref<FormInstance>()
+const copyForm = reactive({
+  copyUserIds: [],
+  copyReason: ''
+})
+const copyFormRule = reactive<FormRules<typeof copyForm>>({
+  copyUserIds: [{ required: true, message: '抄送人不能为空', trigger: 'change' }]
+})
+
+// 转办表单
+const transferFormRef = ref<FormInstance>()
+const transferForm = reactive({
+  assigneeUserId: undefined,
+  reason: ''
+})
+const transferFormRule = reactive<FormRules<typeof transferForm>>({
+  assigneeUserId: [{ required: true, message: '新审批人不能为空', trigger: 'change' }],
+  reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }]
+})
+
+// 委派表单
+const delegateFormRef = ref<FormInstance>()
+const delegateForm = reactive({
+  delegateUserId: undefined,
+  reason: ''
+})
+const delegateFormRule = reactive<FormRules<typeof delegateForm>>({
+  delegateUserId: [{ required: true, message: '接收人不能为空', trigger: 'change' }],
+  reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }]
+})
+
+// 加签表单
+const addSignFormRef = ref<FormInstance>()
+const addSignForm = reactive({
+  addSignUserIds: undefined,
+  reason: ''
+})
+const addSignFormRule = reactive<FormRules<typeof addSignForm>>({
+  addSignUserIds: [{ required: true, message: '加签处理人不能为空', trigger: 'change' }],
+  reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }]
+})
+
+// 减签表单
+const deleteSignFormRef = ref<FormInstance>()
+const deleteSignForm = reactive({
+  deleteSignTaskId: undefined,
+  reason: ''
+})
+const deleteSignFormRule = reactive<FormRules<typeof deleteSignForm>>({
+  deleteSignTaskId: [{ required: true, message: '减签人员不能为空', trigger: 'change' }],
+  reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }]
+})
+
+// 退回表单
+const returnFormRef = ref<FormInstance>()
+const returnForm = reactive({
+  targetTaskDefinitionKey: undefined,
+  returnReason: ''
+})
+const returnFormRule = reactive<FormRules<typeof returnForm>>({
+  targetTaskDefinitionKey: [{ required: true, message: '退回节点不能为空', trigger: 'change' }],
+  returnReason: [{ required: true, message: '退回理由不能为空', trigger: 'blur' }]
+})
+
+// 取消表单
+const cancelFormRef = ref<FormInstance>()
+const cancelForm = reactive({
+  cancelReason: ''
+})
+const cancelFormRule = reactive<FormRules<typeof cancelForm>>({
+  cancelReason: [{ required: true, message: '取消理由不能为空', trigger: 'blur' }]
+})
+
+/** 监听 approveFormFApis,实现它对应的 form-create 初始化后,隐藏掉对应的表单提交按钮 */
+watch(
+  () => approveFormFApi.value,
+  (val) => {
+    val?.btn?.show(false)
+    val?.resetBtn?.show(false)
+  },
+  {
+    deep: true
+  }
+)
+
+/** 弹出气泡卡 */
+const openPopover = async (type: string) => {
+  if (type === 'approve') {
+    // 校验流程表单
+    const valid = await validateNormalForm()
+    if (!valid) {
+      message.warning('表单校验不通过,请先完善表单!!')
+      return
+    }
+    initNextAssigneesFormField()
+  }
+  if (type === 'return') {
+    // 获取退回节点
+    returnList.value = await TaskApi.getTaskListByReturn(runningTask.value.id)
+    if (returnList.value.length === 0) {
+      message.warning('当前没有可退回的节点')
+      return
+    }
+  }
+  Object.keys(popOverVisible.value).forEach((item) => {
+    popOverVisible.value[item] = item === type
+  })
+  // await nextTick()
+  // formRef.value.resetFields()
+}
+
+/** 关闭气泡卡 */
+const closePopover = (type: string, formRef: FormInstance | undefined) => {
+  if (formRef) {
+    formRef.resetFields()
+  }
+  popOverVisible.value[type] = false
+  nextAssigneesActivityNode.value = []
+}
+
+/** 流程通过时,根据表单变量查询新的流程节点,判断下一个节点类型是否为自选审批人 */
+const initNextAssigneesFormField = async () => {
+  // 获取修改的流程变量, 暂时只支持流程表单
+  const variables = getUpdatedProcessInstanceVariables()
+  const data = await ProcessInstanceApi.getNextApprovalNodes({
+    processInstanceId: props.processInstance.id,
+    taskId: runningTask.value.id,
+    processVariablesStr: JSON.stringify(variables)
+  })
+  if (data && data.length > 0) {
+    data.forEach((node: any) => {
+      if (
+        // 情况一:当前节点没有审批人,并且是发起人自选
+        (isEmpty(node.tasks) &&
+          isEmpty(node.candidateUsers) &&
+          CandidateStrategy.START_USER_SELECT === node.candidateStrategy) ||
+        // 情况二:当前节点是审批人自选
+        CandidateStrategy.APPROVE_USER_SELECT === node.candidateStrategy
+      ) {
+        nextAssigneesActivityNode.value.push(node)
+      }
+    })
+  }
+}
+
+/** 选择下一个节点的审批人 */
+const selectNextAssigneesConfirm = (id: string, userList: any[]) => {
+  approveReasonForm.nextAssignees[id] = userList?.map((item: any) => item.id)
+}
+/** 审批通过时,校验每个自选审批人的节点是否都已配置了审批人 */
+const validateNextAssignees = () => {
+  if (Object.keys(nextAssigneesActivityNode.value).length === 0) {
+    return true
+  }
+  // 如果需要自选审批人,则校验每个节点是否都已配置审批人
+  for (const item of nextAssigneesActivityNode.value) {
+    if (isEmpty(approveReasonForm.nextAssignees[item.id])) {
+      message.warning('下一个节点的审批人不能为空!')
+      return false
+    }
+  }
+  return true
+}
+
+/** 处理审批通过和不通过的操作 */
+const handleAudit = async (pass: boolean, formRef: FormInstance | undefined) => {
+  formLoading.value = true
+  try {
+    // 校验表单
+    if (!formRef) return
+    await formRef.validate()
+    // 校验流程表单必填字段
+    const valid = await validateNormalForm()
+    if (!valid) {
+      message.warning('表单校验不通过,请先完善表单!!')
+      return
+    }
+
+    if (pass) {
+      const nextAssigneesValid = validateNextAssignees()
+      if (!nextAssigneesValid) return
+      const variables = getUpdatedProcessInstanceVariables()
+      // 审批通过数据
+      const data = {
+        id: runningTask.value.id,
+        reason: approveReasonForm.reason,
+        variables, // 审批通过, 把修改的字段值赋于流程实例变量
+        nextAssignees: approveReasonForm.nextAssignees // 下个自选节点选择的审批人信息
+      } as any
+      // 签名
+      if (runningTask.value.signEnable) {
+        data.signPicUrl = approveReasonForm.signPicUrl
+      }
+      // 多表单处理,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交
+      // TODO 芋艿 任务有多表单这里要如何处理,会和可编辑的字段冲突
+      const formCreateApi = approveFormFApi.value
+      if (Object.keys(formCreateApi)?.length > 0) {
+        await formCreateApi.validate()
+        // @ts-ignore
+        data.variables = approveForm.value.value
+      }
+      await TaskApi.approveTask(data)
+      popOverVisible.value.approve = false
+      nextAssigneesActivityNode.value = []
+      message.success('审批通过成功')
+    } else {
+      // 审批不通过数据
+      const data = {
+        id: runningTask.value.id,
+        reason: rejectReasonForm.reason
+      }
+      await TaskApi.rejectTask(data)
+      popOverVisible.value.reject = false
+      message.success('审批不通过成功')
+    }
+    // 重置表单
+    formRef.resetFields()
+    // 加载最新数据
+    reload()
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 处理抄送 */
+const handleCopy = async () => {
+  formLoading.value = true
+  try {
+    // 1. 校验表单
+    if (!copyFormRef.value) return
+    await copyFormRef.value.validate()
+    // 2. 提交抄送
+    const data = {
+      id: runningTask.value.id,
+      reason: copyForm.copyReason,
+      copyUserIds: copyForm.copyUserIds
+    }
+    await TaskApi.copyTask(data)
+    copyFormRef.value.resetFields()
+    popOverVisible.value.copy = false
+    message.success('操作成功')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 处理转交 */
+const handleTransfer = async () => {
+  formLoading.value = true
+  try {
+    // 1.1 校验表单
+    if (!transferFormRef.value) return
+    await transferFormRef.value.validate()
+    // 1.2 提交转交
+    const data = {
+      id: runningTask.value.id,
+      reason: transferForm.reason,
+      assigneeUserId: transferForm.assigneeUserId
+    }
+    await TaskApi.transferTask(data)
+    transferFormRef.value.resetFields()
+    popOverVisible.value.transfer = false
+    message.success('操作成功')
+    // 2. 加载最新数据
+    reload()
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 处理委派 */
+const handleDelegate = async () => {
+  formLoading.value = true
+  try {
+    // 1.1 校验表单
+    if (!delegateFormRef.value) return
+    await delegateFormRef.value.validate()
+    // 1.2 处理委派
+    const data = {
+      id: runningTask.value.id,
+      reason: delegateForm.reason,
+      delegateUserId: delegateForm.delegateUserId
+    }
+
+    await TaskApi.delegateTask(data)
+    popOverVisible.value.delegate = false
+    delegateFormRef.value.resetFields()
+    message.success('操作成功')
+    // 2. 加载最新数据
+    reload()
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 处理加签 */
+const handlerAddSign = async (type: string) => {
+  formLoading.value = true
+  try {
+    // 1.1 校验表单
+    if (!addSignFormRef.value) return
+    await addSignFormRef.value.validate()
+    // 1.2 提交加签
+    const data = {
+      id: runningTask.value.id,
+      type,
+      reason: addSignForm.reason,
+      userIds: addSignForm.addSignUserIds
+    }
+    await TaskApi.signCreateTask(data)
+    message.success('操作成功')
+    addSignFormRef.value.resetFields()
+    popOverVisible.value.addSign = false
+    // 2 加载最新数据
+    reload()
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 处理退回 */
+const handleReturn = async () => {
+  formLoading.value = true
+  try {
+    // 1.1 校验表单
+    if (!returnFormRef.value) return
+    await returnFormRef.value.validate()
+    // 1.2 提交退回
+    const data = {
+      id: runningTask.value.id,
+      reason: returnForm.returnReason,
+      targetTaskDefinitionKey: returnForm.targetTaskDefinitionKey
+    }
+
+    await TaskApi.returnTask(data)
+    popOverVisible.value.return = false
+    returnFormRef.value.resetFields()
+    message.success('操作成功')
+    // 2 重新加载数据
+    reload()
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 处理取消 */
+const handleCancel = async () => {
+  formLoading.value = true
+  try {
+    // 1.1 校验表单
+    if (!cancelFormRef.value) return
+    await cancelFormRef.value.validate()
+    // 1.2 提交取消
+    await ProcessInstanceApi.cancelProcessInstanceByStartUser(
+      props.processInstance.id,
+      cancelForm.cancelReason
+    )
+    popOverVisible.value.return = false
+    message.success('操作成功')
+    cancelFormRef.value.resetFields()
+    // 2 重新加载数据
+    reload()
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 处理再次提交 */
+const handleReCreate = async () => {
+  // 跳转发起流程界面
+  await router.push({
+    name: 'BpmProcessInstanceCreate',
+    query: { processInstanceId: props.processInstance?.id }
+  })
+}
+
+/** 获取减签人员标签 */
+const getDeleteSignUserLabel = (task: any): string => {
+  const deptName = task?.assigneeUser?.deptName || task?.ownerUser?.deptName
+  const nickname = task?.assigneeUser?.nickname || task?.ownerUser?.nickname
+  return `${nickname} ( 所属部门:${deptName} )`
+}
+/** 处理减签 */
+const handlerDeleteSign = async () => {
+  formLoading.value = true
+  try {
+    // 1.1 校验表单
+    if (!deleteSignFormRef.value) return
+    await deleteSignFormRef.value.validate()
+    // 1.2 提交减签
+    const data = {
+      id: deleteSignForm.deleteSignTaskId,
+      reason: deleteSignForm.reason
+    }
+    await TaskApi.signDeleteTask(data)
+    message.success('减签成功')
+    deleteSignFormRef.value.resetFields()
+    popOverVisible.value.deleteSign = false
+    // 2 加载最新数据
+    reload()
+  } finally {
+    formLoading.value = false
+  }
+}
+/** 重新加载数据 */
+const reload = () => {
+  emit('success')
+}
+
+/** 任务是否为处理中状态 */
+const isHandleTaskStatus = () => {
+  let canHandle = false
+  if (TaskApi.TaskStatusEnum.RUNNING === runningTask.value?.status) {
+    canHandle = true
+  }
+  return canHandle
+}
+
+/** 流程状态是否为结束状态 */
+const isEndProcessStatus = (status: number) => {
+  let isEndStatus = false
+  if (
+    BpmProcessInstanceStatus.APPROVE === status ||
+    BpmProcessInstanceStatus.REJECT === status ||
+    BpmProcessInstanceStatus.CANCEL === status
+  ) {
+    isEndStatus = true
+  }
+  return isEndStatus
+}
+
+/** 是否显示按钮 */
+const isShowButton = (btnType: OperationButtonType): boolean => {
+  let isShow = true
+  if (runningTask.value?.buttonsSetting && runningTask.value?.buttonsSetting[btnType]) {
+    isShow = runningTask.value.buttonsSetting[btnType].enable
+  }
+  return isShow
+}
+
+/** 获取按钮的显示名称 */
+const getButtonDisplayName = (btnType: OperationButtonType) => {
+  let displayName = OPERATION_BUTTON_NAME.get(btnType)
+  if (runningTask.value?.buttonsSetting && runningTask.value?.buttonsSetting[btnType]) {
+    displayName = runningTask.value.buttonsSetting[btnType].displayName
+  }
+  return displayName
+}
+
+const loadTodoTask = (task: any) => {
+  approveForm.value = {}
+  runningTask.value = task
+  approveFormFApi.value = {}
+  reasonRequire.value = task?.reasonRequire ?? false
+  nodeTypeName.value = task?.nodeType === NodeType.TRANSACTOR_NODE ? '办理' : '审批'
+  // 处理 approve 表单.
+  if (task && task.formId && task.formConf) {
+    const tempApproveForm = {}
+    setConfAndFields2(tempApproveForm, task.formConf, task.formFields, task.formVariables)
+    approveForm.value = tempApproveForm
+  } else {
+    approveForm.value = {} // 占位,避免为空
+  }
+}
+
+/** 校验流程表单 */
+const validateNormalForm = async () => {
+  if (props.processDefinition?.formType === BpmModelFormType.NORMAL) {
+    let valid = true
+    try {
+      await props.normalFormApi?.validate()
+    } catch {
+      valid = false
+    }
+    return valid
+  } else {
+    return true
+  }
+}
+
+/** 从可以编辑的流程表单字段,获取需要修改的流程实例的变量 */
+const getUpdatedProcessInstanceVariables = () => {
+  const variables = {}
+  props.writableFields.forEach((field) => {
+    variables[field] = props.normalFormApi.getValue(field)
+  })
+  return variables
+}
+
+/** 处理签名完成 */
+const handleSignFinish = (url: string) => {
+  approveReasonForm.signPicUrl = url
+  approveSignFormRef.value.validate('change')
+}
+
+defineExpose({ loadTodoTask })
+</script>
+
+<style lang="scss" scoped>
+:deep(.el-affix--fixed) {
+  background-color: var(--el-bg-color);
+}
+
+.btn-container {
+  > div {
+    display: flex;
+    margin: 0 8px;
+    cursor: pointer;
+    align-items: center;
+
+    &:hover {
+      color: #6db5ff;
+    }
+  }
+}
+</style>

+ 174 - 0
src/views/bpm/processInstance/detail/ProcessInstanceSimpleViewer.vue

@@ -0,0 +1,174 @@
+<template>
+  <div v-loading="loading" class="process-viewer-container">
+    <SimpleProcessViewer
+      :flow-node="simpleModel"
+      :tasks="tasks"
+      :process-instance="processInstance"
+    />
+  </div>
+</template>
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { TaskStatusEnum } from '@/api/bpm/task'
+import { SimpleFlowNode, NodeType } from '@/components/SimpleProcessDesignerV2/src/consts'
+import { SimpleProcessViewer } from '@/components/SimpleProcessDesignerV2/src/'
+defineOptions({ name: 'BpmProcessInstanceSimpleViewer' })
+
+const props = defineProps({
+  loading: propTypes.bool.def(false), // 是否加载中
+  modelView: propTypes.object,
+  simpleJson: propTypes.string // Simple 模型结构数据 (json 格式)
+})
+const simpleModel = ref<any>({})
+// 用户任务
+const tasks = ref([])
+// 流程实例
+const processInstance = ref()
+
+/** 监控模型视图 包括任务列表、进行中的活动节点编号等 */
+watch(
+  () => props.modelView,
+  async (newModelView) => {
+    if (newModelView) {
+      tasks.value = newModelView.tasks
+      processInstance.value = newModelView.processInstance
+      // 已经拒绝的活动节点编号集合,只包括 UserTask
+      const rejectedTaskActivityIds: string[] = newModelView.rejectedTaskActivityIds
+      // 进行中的活动节点编号集合, 只包括 UserTask
+      const unfinishedTaskActivityIds: string[] = newModelView.unfinishedTaskActivityIds
+      // 已经完成的活动节点编号集合, 包括 UserTask、Gateway 等
+      const finishedActivityIds: string[] = newModelView.finishedTaskActivityIds
+      // 已经完成的连线节点编号集合,只包括 SequenceFlow
+      const finishedSequenceFlowActivityIds: string[] = newModelView.finishedSequenceFlowActivityIds
+      setSimpleModelNodeTaskStatus(
+        newModelView.simpleModel,
+        newModelView.processInstance?.status,
+        rejectedTaskActivityIds,
+        unfinishedTaskActivityIds,
+        finishedActivityIds,
+        finishedSequenceFlowActivityIds
+      )
+      simpleModel.value = newModelView.simpleModel ? newModelView.simpleModel : {}
+    }
+  }
+)
+/** 监控模型结构数据 */
+watch(
+  () => props.simpleJson,
+  async (value) => {
+    if (value) {
+      simpleModel.value = JSON.parse(value)
+    }
+  }
+)
+const setSimpleModelNodeTaskStatus = (
+  simpleModel: SimpleFlowNode | undefined,
+  processStatus: number,
+  rejectedTaskActivityIds: string[],
+  unfinishedTaskActivityIds: string[],
+  finishedActivityIds: string[],
+  finishedSequenceFlowActivityIds: string[]
+) => {
+  if (!simpleModel) {
+    return
+  }
+  // 结束节点
+  if (simpleModel.type === NodeType.END_EVENT_NODE) {
+    if (finishedActivityIds.includes(simpleModel.id)) {
+      simpleModel.activityStatus = processStatus
+    } else {
+      simpleModel.activityStatus = TaskStatusEnum.NOT_START
+    }
+    return
+  }
+  // 审批节点
+  if (
+    simpleModel.type === NodeType.START_USER_NODE ||
+    simpleModel.type === NodeType.USER_TASK_NODE ||
+    simpleModel.type === NodeType.TRANSACTOR_NODE ||
+    simpleModel.type === NodeType.CHILD_PROCESS_NODE
+  ) {
+    simpleModel.activityStatus = TaskStatusEnum.NOT_START
+    if (rejectedTaskActivityIds.includes(simpleModel.id)) {
+      simpleModel.activityStatus = TaskStatusEnum.REJECT
+    } else if (unfinishedTaskActivityIds.includes(simpleModel.id)) {
+      simpleModel.activityStatus = TaskStatusEnum.RUNNING
+    } else if (finishedActivityIds.includes(simpleModel.id)) {
+      simpleModel.activityStatus = TaskStatusEnum.APPROVE
+    }
+    // TODO 是不是还缺一个 cancel 的状态
+  }
+  // 抄送节点
+  if (simpleModel.type === NodeType.COPY_TASK_NODE) {
+    // 抄送节点,只有通过和未执行状态
+    if (finishedActivityIds.includes(simpleModel.id)) {
+      simpleModel.activityStatus = TaskStatusEnum.APPROVE
+    } else {
+      simpleModel.activityStatus = TaskStatusEnum.NOT_START
+    }
+  }
+  // 延迟器节点
+  if (simpleModel.type === NodeType.DELAY_TIMER_NODE) {
+    // 延迟器节点,只有通过和未执行状态
+    if (finishedActivityIds.includes(simpleModel.id)) {
+      simpleModel.activityStatus = TaskStatusEnum.APPROVE
+    } else {
+      simpleModel.activityStatus = TaskStatusEnum.NOT_START
+    }
+  }
+  // 触发器节点
+  if (simpleModel.type === NodeType.TRIGGER_NODE) {
+    // 触发器节点,只有通过和未执行状态
+    if (finishedActivityIds.includes(simpleModel.id)) {
+      simpleModel.activityStatus = TaskStatusEnum.APPROVE
+    } else {
+      simpleModel.activityStatus = TaskStatusEnum.NOT_START
+    }
+  }
+
+  // 条件节点对应 SequenceFlow
+  if (simpleModel.type === NodeType.CONDITION_NODE) {
+    // 条件节点,只有通过和未执行状态
+    if (finishedSequenceFlowActivityIds.includes(simpleModel.id)) {
+      simpleModel.activityStatus = TaskStatusEnum.APPROVE
+    } else {
+      simpleModel.activityStatus = TaskStatusEnum.NOT_START
+    }
+  }
+  // 网关节点
+  if (
+    simpleModel.type === NodeType.CONDITION_BRANCH_NODE ||
+    simpleModel.type === NodeType.PARALLEL_BRANCH_NODE ||
+    simpleModel.type === NodeType.INCLUSIVE_BRANCH_NODE ||
+    simpleModel.type === NodeType.ROUTER_BRANCH_NODE
+  ) {
+    // 网关节点。只有通过和未执行状态
+    if (finishedActivityIds.includes(simpleModel.id)) {
+      simpleModel.activityStatus = TaskStatusEnum.APPROVE
+    } else {
+      simpleModel.activityStatus = TaskStatusEnum.NOT_START
+    }
+    simpleModel.conditionNodes?.forEach((node) => {
+      setSimpleModelNodeTaskStatus(
+        node,
+        processStatus,
+        rejectedTaskActivityIds,
+        unfinishedTaskActivityIds,
+        finishedActivityIds,
+        finishedSequenceFlowActivityIds
+      )
+    })
+  }
+
+  setSimpleModelNodeTaskStatus(
+    simpleModel.childNode,
+    processStatus,
+    rejectedTaskActivityIds,
+    unfinishedTaskActivityIds,
+    finishedActivityIds,
+    finishedSequenceFlowActivityIds
+  )
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 103 - 0
src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue

@@ -0,0 +1,103 @@
+<template>
+  <el-table :data="tasks" border header-cell-class-name="table-header-gray">
+    <el-table-column label="审批节点" prop="name" min-width="120" align="center" />
+    <el-table-column label="审批人" min-width="100" align="center">
+      <template #default="scope">
+        {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
+      </template>
+    </el-table-column>
+    <el-table-column
+      :formatter="dateFormatter"
+      align="center"
+      label="开始时间"
+      prop="createTime"
+      min-width="140"
+    />
+    <el-table-column
+      :formatter="dateFormatter"
+      align="center"
+      label="结束时间"
+      prop="endTime"
+      min-width="140"
+    />
+    <el-table-column align="center" label="审批状态" prop="status" min-width="90">
+      <template #default="scope">
+        <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="审批建议" prop="reason" min-width="200">
+      <template #default="scope">
+        {{ scope.row.reason }}
+        <el-button
+          class="ml-10px"
+          size="small"
+          v-if="scope.row.formId > 0"
+          @click="handleFormDetail(scope.row)"
+        >
+          <Icon icon="ep:document" /> 查看表单
+        </el-button>
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="耗时" prop="durationInMillis" min-width="100">
+      <template #default="scope">
+        {{ formatPast2(scope.row.durationInMillis) }}
+      </template>
+    </el-table-column>
+  </el-table>
+
+  <!-- 弹窗:表单 -->
+  <Dialog title="表单详情" v-model="taskFormVisible" width="600">
+    <form-create
+      ref="fApi"
+      v-model="taskForm.value"
+      :option="taskForm.option"
+      :rule="taskForm.rule"
+    />
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
+import { propTypes } from '@/utils/propTypes'
+import { DICT_TYPE } from '@/utils/dict'
+import type { ApiAttrs } from '@form-create/element-ui/types/config'
+import { setConfAndFields2 } from '@/utils/formCreate'
+import * as TaskApi from '@/api/bpm/task'
+
+defineOptions({ name: 'BpmProcessInstanceTaskList' })
+
+const props = defineProps({
+  loading: propTypes.bool.def(false), // 是否加载中
+  id: propTypes.string // 流程实例的编号
+})
+const tasks = ref([]) // 流程任务的数组
+
+/** 查看表单 */
+const fApi = ref<ApiAttrs>() // form-create 的 API 操作类
+const taskForm = ref({
+  rule: [],
+  option: {},
+  value: {}
+}) // 流程任务的表单详情
+const taskFormVisible = ref(false)
+const handleFormDetail = async (row: any) => {
+  // 设置表单
+  setConfAndFields2(taskForm, row.formConf, row.formFields, row.formVariables)
+  // 弹窗打开
+  taskFormVisible.value = true
+  // 隐藏提交、重置按钮,设置禁用只读
+  await nextTick()
+  fApi.value.fapi.btn.show(false)
+  fApi.value?.fapi?.resetBtn.show(false)
+  fApi.value?.fapi?.disabled(true)
+}
+
+/** 只有 loading 完成时,才去加载流程列表 */
+watch(
+  () => props.loading,
+  async (value) => {
+    if (value) {
+      tasks.value = await TaskApi.getTaskListByProcessInstanceId(props.id)
+    }
+  }
+)
+</script>

+ 330 - 0
src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue

@@ -0,0 +1,330 @@
+<!-- 审批详情的右侧:审批流 -->
+<template>
+  <el-timeline class="pt-20px">
+    <!-- 遍历每个审批节点 -->
+    <el-timeline-item
+      v-for="(activity, index) in activityNodes"
+      :key="index"
+      size="large"
+      :icon="getApprovalNodeIcon(activity.status, activity.nodeType)"
+      :color="getApprovalNodeColor(activity.status)"
+    >
+      <template #dot>
+        <div
+          class="position-absolute left--10px top--6px rounded-full border border-solid border-#dedede w-30px h-30px flex justify-center items-center bg-#3f73f7 p-5px"
+        >
+          <img class="w-full h-full" :src="getApprovalNodeImg(activity.nodeType)" alt="" />
+          <div
+            v-if="showStatusIcon"
+            class="position-absolute top-17px left-17px rounded-full flex items-center p-1px border-2 border-white border-solid"
+            :style="{ backgroundColor: getApprovalNodeColor(activity.status) }"
+          >
+            <el-icon :size="11" color="#fff">
+              <component :is="getApprovalNodeIcon(activity.status, activity.nodeType)" />
+            </el-icon>
+          </div>
+        </div>
+      </template>
+      <div class="flex flex-col items-start gap2" :id="`activity-task-${activity.id}-${index}`">
+        <!-- 第一行:节点名称、时间 -->
+        <div class="flex w-full">
+          <div class="font-bold"> {{ activity.name }}</div>
+          <!-- 信息:时间 -->
+          <div
+            v-if="activity.status !== TaskStatusEnum.NOT_START"
+            class="text-#a5a5a5 text-13px mt-1 ml-auto"
+          >
+            {{ getApprovalNodeTime(activity) }}
+          </div>
+        </div>
+        <div v-if="activity.nodeType === NodeType.CHILD_PROCESS_NODE">
+          <el-button type="primary" plain size="small" @click="handleChildProcess(activity)">
+            查看子流程
+          </el-button>
+        </div>
+        <!-- 需要自定义选择审批人 -->
+        <div
+          class="flex flex-wrap gap2 items-center"
+          v-if="
+            isEmpty(activity.tasks) &&
+            isEmpty(activity.candidateUsers) &&
+            (CandidateStrategy.START_USER_SELECT === activity.candidateStrategy ||
+              CandidateStrategy.APPROVE_USER_SELECT === activity.candidateStrategy)
+          "
+        >
+          <!--  && activity.nodeType === NodeType.USER_TASK_NODE -->
+
+          <el-tooltip content="添加用户" placement="left">
+            <el-button
+              class="!px-6px"
+              @click="handleSelectUser(activity.id, customApproveUsers[activity.id])"
+            >
+              <img class="w-18px text-#ccc" src="@/assets/svgs/bpm/add-user.svg" alt="" />
+            </el-button>
+          </el-tooltip>
+          <div
+            v-for="(user, idx1) in customApproveUsers[activity.id]"
+            :key="idx1"
+            class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
+          >
+            <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
+            <el-avatar class="!m-5px" :size="28" v-else>
+              {{ user.nickname.substring(0, 1) }}
+            </el-avatar>
+            {{ user.nickname }}
+          </div>
+        </div>
+        <div v-else class="flex items-center flex-wrap mt-1 gap2">
+          <!-- 情况一:遍历每个审批节点下的【进行中】task 任务 -->
+          <div v-for="(task, idx) in activity.tasks" :key="idx" class="flex flex-col pr-2 gap2">
+            <div
+              class="position-relative flex flex-wrap gap2"
+              v-if="task.assigneeUser || task.ownerUser"
+            >
+              <!-- 信息:头像昵称 -->
+              <div
+                class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
+              >
+                <template v-if="task.assigneeUser?.avatar || task.assigneeUser?.nickname">
+                  <el-avatar
+                    class="!m-5px"
+                    :size="28"
+                    v-if="task.assigneeUser?.avatar"
+                    :src="task.assigneeUser?.avatar"
+                  />
+                  <el-avatar class="!m-5px" :size="28" v-else>
+                    {{ task.assigneeUser?.nickname.substring(0, 1) }}
+                  </el-avatar>
+                  {{ task.assigneeUser?.nickname }}
+                </template>
+                <template v-else-if="task.ownerUser?.avatar || task.ownerUser?.nickname">
+                  <el-avatar
+                    class="!m-5px"
+                    :size="28"
+                    v-if="task.ownerUser?.avatar"
+                    :src="task.ownerUser?.avatar"
+                  />
+                  <el-avatar class="!m-5px" :size="28" v-else>
+                    {{ task.ownerUser?.nickname.substring(0, 1) }}
+                  </el-avatar>
+                  {{ task.ownerUser?.nickname }}
+                </template>
+                <!-- 信息:任务 ICON -->
+                <div
+                  v-if="showStatusIcon && onlyStatusIconShow.includes(task.status)"
+                  class="position-absolute top-19px left-23px rounded-full flex items-center p-1px border-2 border-white border-solid"
+                  :style="{ backgroundColor: statusIconMap2[task.status]?.color }"
+                >
+                  <Icon :size="11" :icon="statusIconMap2[task.status]?.icon" color="#FFFFFF" />
+                </div>
+              </div>
+            </div>
+            <teleport defer :to="`#activity-task-${activity.id}-${index}`">
+              <div
+                v-if="
+                  task.reason &&
+                  [NodeType.USER_TASK_NODE, NodeType.END_EVENT_NODE].includes(activity.nodeType)
+                "
+                class="text-#a5a5a5 text-13px mt-1 w-full bg-#f8f8fa p2 rounded-md"
+              >
+                <!-- TODO lesan:这里如果是办理,需要是办理意见 -->
+                审批意见:{{ task.reason }}
+              </div>
+              <div
+                v-if="task.signPicUrl && activity.nodeType === NodeType.USER_TASK_NODE"
+                class="text-#a5a5a5 text-13px mt-1 w-full bg-#f8f8fa p2 rounded-md"
+              >
+                签名:
+                <el-image
+                  class="w-90px h-40px ml-5px"
+                  :src="task.signPicUrl"
+                  :preview-src-list="[task.signPicUrl]"
+                />
+              </div>
+            </teleport>
+          </div>
+          <!-- 情况二:遍历每个审批节点下的【候选的】task 任务。例如说,1)依次审批,2)未来的审批任务等 -->
+          <div
+            v-for="(user, idx1) in activity.candidateUsers"
+            :key="idx1"
+            class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
+          >
+            <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
+            <el-avatar class="!m-5px" :size="28" v-else>
+              {{ user.nickname.substring(0, 1) }}
+            </el-avatar>
+            {{ user.nickname }}
+
+            <!-- 信息:任务 ICON -->
+            <div
+              v-if="showStatusIcon"
+              class="position-absolute top-20px left-24px rounded-full flex items-center p-1px border-2 border-white border-solid"
+              :style="{ backgroundColor: statusIconMap2['-1']?.color }"
+            >
+              <Icon :size="11" :icon="statusIconMap2['-1']?.icon" color="#FFFFFF" />
+            </div>
+          </div>
+        </div>
+      </div>
+    </el-timeline-item>
+  </el-timeline>
+
+  <!-- 用户选择弹窗 -->
+  <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
+</template>
+
+<script lang="ts" setup>
+import { formatDate } from '@/utils/formatTime'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import { TaskStatusEnum } from '@/api/bpm/task'
+import { NodeType, CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts'
+import { isEmpty } from '@/utils/is'
+import { Check, Close, Loading, Clock, Minus, Delete } from '@element-plus/icons-vue'
+import starterSvg from '@/assets/svgs/bpm/starter.svg'
+import auditorSvg from '@/assets/svgs/bpm/auditor.svg'
+import copySvg from '@/assets/svgs/bpm/copy.svg'
+import conditionSvg from '@/assets/svgs/bpm/condition.svg'
+import parallelSvg from '@/assets/svgs/bpm/parallel.svg'
+import finishSvg from '@/assets/svgs/bpm/finish.svg'
+import transactorSvg from '@/assets/svgs/bpm/transactor.svg'
+import childProcessSvg from '@/assets/svgs/bpm/child-process.svg'
+
+defineOptions({ name: 'BpmProcessInstanceTimeline' })
+withDefaults(
+  defineProps<{
+    activityNodes: ProcessInstanceApi.ApprovalNodeInfo[] // 审批节点信息
+    showStatusIcon?: boolean // 是否显示头像右下角状态图标
+  }>(),
+  {
+    showStatusIcon: true // 默认值为 true
+  }
+)
+const { push } = useRouter() // 路由
+
+// 审批节点
+const statusIconMap2 = {
+  // 未开始
+  '-1': { color: '#909398', icon: 'ep-clock' },
+  // 待审批
+  '0': { color: '#00b32a', icon: 'ep:loading' },
+  // 审批中
+  '1': { color: '#448ef7', icon: 'ep:loading' },
+  // 审批通过
+  '2': { color: '#00b32a', icon: 'ep:circle-check-filled' },
+  // 审批不通过
+  '3': { color: '#f46b6c', icon: 'fa-solid:times-circle' },
+  // 取消
+  '4': { color: '#cccccc', icon: 'ep:delete-filled' },
+  // 退回
+  '5': { color: '#f46b6c', icon: 'ep:remove-filled' },
+  // 委派中
+  '6': { color: '#448ef7', icon: 'ep:loading' },
+  // 审批通过中
+  '7': { color: '#00b32a', icon: 'ep:circle-check-filled' }
+}
+
+const statusIconMap = {
+  // 审批未开始
+  '-1': { color: '#909398', icon: Clock },
+  '0': { color: '#00b32a', icon: Clock },
+  // 审批中
+  '1': { color: '#448ef7', icon: Loading },
+  // 审批通过
+  '2': { color: '#00b32a', icon: Check },
+  // 审批不通过
+  '3': { color: '#f46b6c', icon: Close },
+  // 已取消
+  '4': { color: '#cccccc', icon: Delete },
+  // 退回
+  '5': { color: '#f46b6c', icon: Minus },
+  // 委派中
+  '6': { color: '#448ef7', icon: Loading },
+  // 审批通过中
+  '7': { color: '#00b32a', icon: Check }
+}
+
+const nodeTypeSvgMap = {
+  // 结束节点
+  [NodeType.END_EVENT_NODE]: { color: '#909398', svg: finishSvg },
+  // 发起人节点
+  [NodeType.START_USER_NODE]: { color: '#909398', svg: starterSvg },
+  // 审批人节点
+  [NodeType.USER_TASK_NODE]: { color: '#ff943e', svg: auditorSvg },
+  // 办理人节点
+  [NodeType.TRANSACTOR_NODE]: { color: '#ff943e', svg: transactorSvg },
+  // 抄送人节点
+  [NodeType.COPY_TASK_NODE]: { color: '#3296fb', svg: copySvg },
+  // 条件分支节点
+  [NodeType.CONDITION_NODE]: { color: '#14bb83', svg: conditionSvg },
+  // 并行分支节点
+  [NodeType.PARALLEL_BRANCH_NODE]: { color: '#14bb83', svg: parallelSvg },
+  // 子流程节点
+  [NodeType.CHILD_PROCESS_NODE]: { color: '#14bb83', svg: childProcessSvg }
+}
+
+// 只有只有状态是 -1、0、1 才展示头像右小角状态小icon
+const onlyStatusIconShow = [-1, 0, 1]
+
+// timeline时间线上icon图标
+const getApprovalNodeImg = (nodeType: NodeType) => {
+  return nodeTypeSvgMap[nodeType]?.svg
+}
+
+const getApprovalNodeIcon = (taskStatus: number, nodeType: NodeType) => {
+  if (taskStatus == TaskStatusEnum.NOT_START) {
+    return statusIconMap[taskStatus]?.icon
+  }
+
+  if (
+    nodeType === NodeType.START_USER_NODE ||
+    nodeType === NodeType.USER_TASK_NODE ||
+    nodeType === NodeType.TRANSACTOR_NODE ||
+    nodeType === NodeType.CHILD_PROCESS_NODE ||
+    nodeType === NodeType.END_EVENT_NODE
+  ) {
+    return statusIconMap[taskStatus]?.icon
+  }
+}
+
+const getApprovalNodeColor = (taskStatus: number) => {
+  return statusIconMap[taskStatus]?.color
+}
+
+const getApprovalNodeTime = (node: ProcessInstanceApi.ApprovalNodeInfo) => {
+  if (node.nodeType === NodeType.START_USER_NODE && node.startTime) {
+    return `${formatDate(node.startTime)}`
+  }
+  if (node.endTime) {
+    return `${formatDate(node.endTime)}`
+  }
+  if (node.startTime) {
+    return `${formatDate(node.startTime)}`
+  }
+}
+
+// 选择自定义审批人
+const userSelectFormRef = ref()
+const handleSelectUser = (activityId, selectedList) => {
+  userSelectFormRef.value.open(activityId, selectedList)
+}
+const emit = defineEmits<{
+  selectUserConfirm: [id: any, userList: any[]]
+}>()
+const customApproveUsers: any = ref({}) // key:activityId,value:用户列表
+// 选择完成
+const handleUserSelectConfirm = (activityId: string, userList: any[]) => {
+  customApproveUsers.value[activityId] = userList || []
+  emit('selectUserConfirm', activityId, userList)
+}
+
+/** 跳转子流程 */
+const handleChildProcess = (activity: any) => {
+  // TODO @lesan:貌似跳不过去?!
+  push({
+    name: 'BpmProcessInstanceDetail',
+    query: {
+      id: activity.processInstanceId
+    }
+  })
+}
+</script>

+ 50 - 0
src/views/bpm/processInstance/detail/SignDialog.vue

@@ -0,0 +1,50 @@
+<template>
+  <el-dialog v-model="signDialogVisible" title="签名" width="935">
+    <div class="position-relative">
+      <Vue3Signature class="b b-solid b-gray" ref="signature" w="900px" h="400px" />
+      <el-button
+        class="pos-absolute bottom-20px right-10px"
+        type="primary"
+        text
+        size="small"
+        @click="signature.clear()"
+      >
+        <Icon icon="ep:delete" class="mr-5px" />
+        清除
+      </el-button>
+    </div>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="signDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submit"> 提交 </el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import Vue3Signature from 'vue3-signature'
+import * as FileApi from '@/api/infra/file'
+import download from '@/utils/download'
+
+const message = useMessage() // 消息弹窗
+const signDialogVisible = ref(false)
+const signature = ref()
+
+const open = async () => {
+  signDialogVisible.value = true
+}
+defineExpose({ open })
+
+const emits = defineEmits(['success'])
+const submit = async () => {
+  message.success('签名上传中请稍等。。。')
+  const res = await FileApi.updateFile({
+    file: download.base64ToFile(signature.value.save('image/png'), '签名')
+  })
+  emits('success', res.data)
+  signDialogVisible.value = false
+}
+</script>
+
+<style scoped></style>

+ 355 - 0
src/views/bpm/processInstance/detail/index.vue

@@ -0,0 +1,355 @@
+<template>
+  <ContentWrap :bodyStyle="{ padding: '10px 20px 0' }" class="position-relative">
+    <div class="processInstance-wrap-main">
+      <el-scrollbar>
+        <img
+          class="position-absolute right-20px"
+          width="150"
+          :src="auditIconsMap[processInstance.status]"
+          alt=""
+        />
+        <div class="text-#878c93 h-15px">编号:{{ id }}</div>
+        <el-divider class="!my-8px" />
+        <div class="flex items-center gap-5 mb-10px h-40px">
+          <div class="text-26px font-bold mb-5px">{{ processInstance.name }}</div>
+          <dict-tag
+            v-if="processInstance.status"
+            :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS"
+            :value="processInstance.status"
+          />
+        </div>
+
+        <div class="flex items-center gap-5 mb-10px text-13px h-35px">
+          <div
+            class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600"
+          >
+            <el-avatar
+              :size="28"
+              v-if="processInstance?.startUser?.avatar"
+              :src="processInstance?.startUser?.avatar"
+            />
+            <el-avatar :size="28" v-else-if="processInstance?.startUser?.nickname">
+              {{ processInstance?.startUser?.nickname.substring(0, 1) }}
+            </el-avatar>
+            {{ processInstance?.startUser?.nickname }}
+          </div>
+          <div class="text-#878c93"> {{ formatDate(processInstance.startTime) }} 提交 </div>
+        </div>
+
+        <el-tabs v-model="activeTab">
+          <!-- 表单信息 -->
+          <el-tab-pane label="审批详情" name="form">
+            <div class="form-scroll-area">
+              <el-scrollbar>
+                <el-row>
+                  <el-col :span="17" class="!flex !flex-col formCol">
+                    <!-- 表单信息 -->
+                    <div
+                      v-loading="processInstanceLoading"
+                      class="form-box flex flex-col mb-30px flex-1"
+                    >
+                      <!-- 情况一:流程表单 -->
+                      <el-col v-if="processDefinition?.formType === BpmModelFormType.NORMAL">
+                        <form-create
+                          v-model="detailForm.value"
+                          v-model:api="fApi"
+                          :option="detailForm.option"
+                          :rule="detailForm.rule"
+                        />
+                      </el-col>
+                      <!-- 情况二:业务表单 -->
+                      <div v-if="processDefinition?.formType === BpmModelFormType.CUSTOM">
+                        <BusinessFormComponent :id="processInstance.businessKey" />
+                      </div>
+                    </div>
+                  </el-col>
+                  <el-col :span="7">
+                    <!-- 审批记录时间线 -->
+                    <ProcessInstanceTimeline :activity-nodes="activityNodes" />
+                  </el-col>
+                </el-row>
+              </el-scrollbar>
+            </div>
+          </el-tab-pane>
+
+          <!-- 流程图 -->
+          <el-tab-pane label="流程图" name="diagram">
+            <div class="form-scroll-area">
+              <ProcessInstanceSimpleViewer
+                v-show="
+                  processDefinition.modelType && processDefinition.modelType === BpmModelType.SIMPLE
+                "
+                :loading="processInstanceLoading"
+                :model-view="processModelView"
+              />
+              <ProcessInstanceBpmnViewer
+                v-show="
+                  processDefinition.modelType && processDefinition.modelType === BpmModelType.BPMN
+                "
+                :loading="processInstanceLoading"
+                :model-view="processModelView"
+              />
+            </div>
+          </el-tab-pane>
+
+          <!-- 流转记录 -->
+          <el-tab-pane label="流转记录" name="record">
+            <div class="form-scroll-area">
+              <el-scrollbar>
+                <ProcessInstanceTaskList :loading="processInstanceLoading" :id="id" />
+              </el-scrollbar>
+            </div>
+          </el-tab-pane>
+
+          <!-- 流转评论 TODO 待开发 -->
+          <el-tab-pane label="流转评论" name="comment" v-if="false">
+            <div class="form-scroll-area">
+              <el-scrollbar> 流转评论 </el-scrollbar>
+            </div>
+          </el-tab-pane>
+        </el-tabs>
+
+        <div class="b-t-solid border-t-1px border-[var(--el-border-color)]">
+          <!-- 操作栏按钮 -->
+          <ProcessInstanceOperationButton
+            ref="operationButtonRef"
+            :process-instance="processInstance"
+            :process-definition="processDefinition"
+            :userOptions="userOptions"
+            :normal-form="detailForm"
+            :normal-form-api="fApi"
+            :writable-fields="writableFields"
+            @success="refresh"
+          />
+        </div>
+      </el-scrollbar>
+    </div>
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { formatDate } from '@/utils/formatTime'
+import { DICT_TYPE } from '@/utils/dict'
+import { BpmModelType, BpmModelFormType } from '@/utils/constants'
+import { setConfAndFields2 } from '@/utils/formCreate'
+import { registerComponent } from '@/utils/routerHelper'
+import type { ApiAttrs } from '@form-create/element-ui/types/config'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import * as UserApi from '@/api/system/user'
+import ProcessInstanceBpmnViewer from './ProcessInstanceBpmnViewer.vue'
+import ProcessInstanceSimpleViewer from './ProcessInstanceSimpleViewer.vue'
+import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue'
+import ProcessInstanceOperationButton from './ProcessInstanceOperationButton.vue'
+import ProcessInstanceTimeline from './ProcessInstanceTimeline.vue'
+import { FieldPermissionType } from '@/components/SimpleProcessDesignerV2/src/consts'
+import { TaskStatusEnum } from '@/api/bpm/task'
+import runningSvg from '@/assets/svgs/bpm/running.svg'
+import approveSvg from '@/assets/svgs/bpm/approve.svg'
+import rejectSvg from '@/assets/svgs/bpm/reject.svg'
+import cancelSvg from '@/assets/svgs/bpm/cancel.svg'
+
+defineOptions({ name: 'BpmProcessInstanceDetail' })
+const props = defineProps<{
+  id: string // 流程实例的编号
+  taskId?: string // 任务编号
+  activityId?: string //流程活动编号,用于抄送查看
+}>()
+const message = useMessage() // 消息弹窗
+const processInstanceLoading = ref(false) // 流程实例的加载中
+const processInstance = ref<any>({}) // 流程实例
+const processDefinition = ref<any>({}) // 流程定义
+const processModelView = ref<any>({}) // 流程模型视图
+const operationButtonRef = ref() // 操作按钮组件 ref
+const auditIconsMap = {
+  [TaskStatusEnum.RUNNING]: runningSvg,
+  [TaskStatusEnum.APPROVE]: approveSvg,
+  [TaskStatusEnum.REJECT]: rejectSvg,
+  [TaskStatusEnum.CANCEL]: cancelSvg
+}
+
+// ========== 申请信息 ==========
+const fApi = ref<ApiAttrs>() //
+const detailForm = ref({
+  rule: [],
+  option: {},
+  value: {}
+}) // 流程实例的表单详情
+
+const writableFields: Array<string> = [] // 表单可以编辑的字段
+
+/** 获得详情 */
+const getDetail = () => {
+  // 获得审批详情
+  getApprovalDetail()
+  // 获得流程模型视图
+  getProcessModelView()
+}
+
+/** 加载流程实例 */
+const BusinessFormComponent = ref<any>(null) // 异步组件
+/** 获取审批详情 */
+const getApprovalDetail = async () => {
+  processInstanceLoading.value = true
+  try {
+    const param = {
+      processInstanceId: props.id,
+      activityId: props.activityId,
+      taskId: props.taskId
+    }
+    const data = await ProcessInstanceApi.getApprovalDetail(param)
+    if (!data) {
+      message.error('查询不到审批详情信息!')
+      return
+    }
+    if (!data.processDefinition || !data.processInstance) {
+      message.error('查询不到流程信息!')
+      return
+    }
+    processInstance.value = data.processInstance
+    processDefinition.value = data.processDefinition
+
+    // 设置表单信息
+    if (processDefinition.value.formType === BpmModelFormType.NORMAL) {
+      // 获取表单字段权限
+      const formFieldsPermission = data.formFieldsPermission
+      // 清空可编辑字段为空
+      writableFields.splice(0)
+      if (detailForm.value.rule?.length > 0) {
+        // 避免刷新 form-create 显示不了
+        detailForm.value.value = processInstance.value.formVariables
+      } else {
+        setConfAndFields2(
+          detailForm,
+          processDefinition.value.formConf,
+          processDefinition.value.formFields,
+          processInstance.value.formVariables
+        )
+      }
+      nextTick().then(() => {
+        fApi.value?.btn.show(false)
+        fApi.value?.resetBtn.show(false)
+        //@ts-ignore
+        fApi.value?.disabled(true)
+        // 设置表单字段权限
+        if (formFieldsPermission) {
+          Object.keys(data.formFieldsPermission).forEach((item) => {
+            setFieldPermission(item, formFieldsPermission[item])
+          })
+        }
+      })
+    } else {
+      // 注意:data.processDefinition.formCustomViewPath 是组件的全路径,例如说:/crm/contract/detail/index.vue
+      BusinessFormComponent.value = registerComponent(data.processDefinition.formCustomViewPath)
+    }
+
+    // 获取审批节点,显示 Timeline 的数据
+    activityNodes.value = data.activityNodes
+
+    // 获取待办任务显示操作按钮
+    operationButtonRef.value?.loadTodoTask(data.todoTask)
+  } finally {
+    processInstanceLoading.value = false
+  }
+}
+
+/** 获取流程模型视图*/
+const getProcessModelView = async () => {
+  if (BpmModelType.BPMN === processDefinition.value?.modelType) {
+    // 重置,解决 BPMN 流程图刷新不会重新渲染问题
+    processModelView.value = {
+      bpmnXml: ''
+    }
+  }
+  const data = await ProcessInstanceApi.getProcessInstanceBpmnModelView(props.id)
+  if (data) {
+    processModelView.value = data
+  }
+}
+
+// 审批节点信息
+const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([])
+/**
+ * 设置表单权限
+ */
+const setFieldPermission = (field: string, permission: string) => {
+  if (permission === FieldPermissionType.READ) {
+    //@ts-ignore
+    fApi.value?.disabled(true, field)
+  }
+  if (permission === FieldPermissionType.WRITE) {
+    //@ts-ignore
+    fApi.value?.disabled(false, field)
+    // 加入可以编辑的字段
+    writableFields.push(field)
+  }
+  if (permission === FieldPermissionType.NONE) {
+    //@ts-ignore
+    fApi.value?.hidden(true, field)
+  }
+}
+
+/**
+ * 操作成功后刷新
+ */
+const refresh = () => {
+  // 重新获取详情
+  getDetail()
+}
+
+/** 当前的Tab */
+const activeTab = ref('form')
+
+/** 初始化 */
+const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
+onMounted(async () => {
+  getDetail()
+  // 获得用户列表
+  userOptions.value = await UserApi.getSimpleUserList()
+})
+</script>
+
+<style lang="scss" scoped>
+$wrap-padding-height: 20px;
+$wrap-margin-height: 15px;
+$button-height: 51px;
+$process-header-height: 194px;
+
+.processInstance-wrap-main {
+  height: calc(
+    100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
+  );
+  max-height: calc(
+    100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
+  );
+  overflow: auto;
+
+  .form-scroll-area {
+    display: flex;
+    height: calc(
+      100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
+        $process-header-height - 40px
+    );
+    max-height: calc(
+      100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
+        $process-header-height - 40px
+    );
+    overflow: auto;
+    flex-direction: column;
+
+    :deep(.box-card) {
+      height: 100%;
+      flex: 1;
+
+      .el-card__body {
+        height: 100%;
+        padding: 0;
+      }
+    }
+  }
+}
+
+.form-box {
+  :deep(.el-card) {
+    border: none;
+  }
+}
+</style>

+ 332 - 0
src/views/bpm/processInstance/index.vue

@@ -0,0 +1,332 @@
+<template>
+
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入流程名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+      </el-form-item>
+
+      <el-form-item label="" prop="category" class="absolute right-[300px]">
+        <el-select
+          v-model="queryParams.category"
+          placeholder="请选择流程分类"
+          clearable
+          class="!w-155px"
+          @change="handleQuery"
+        >
+          <el-option
+            v-for="category in categoryList"
+            :key="category.code"
+            :label="category.name"
+            :value="category.code"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="" prop="status" class="absolute right-[130px]">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择流程状态"
+          clearable
+          class="!w-155px"
+          @change="handleQuery"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+
+      <!-- 高级筛选 -->
+      <el-form-item class="absolute right-0">
+        <el-popover
+          :visible="showPopover"
+          persistent
+          :width="400"
+          :show-arrow="false"
+          placement="bottom-end"
+        >
+          <template #reference>
+            <el-button @click="showPopover = !showPopover">
+              <Icon icon="ep:plus" class="mr-5px" />高级筛选
+            </el-button>
+          </template>
+          <el-form-item
+            label="所属流程"
+            class="font-bold"
+            label-position="top"
+            prop="processDefinitionKey"
+          >
+            <el-select
+              v-model="queryParams.processDefinitionKey"
+              placeholder="请选择流程定义"
+              clearable
+              class="!w-390px"
+              @change="handleQuery"
+            >
+              <el-option
+                v-for="item in processDefinitionList"
+                :key="item.key"
+                :label="item.name"
+                :value="item.key"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="发起时间" class="font-bold" label-position="top" prop="createTime">
+            <el-date-picker
+              v-model="queryParams.createTime"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              type="daterange"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item class="font-bold" label-position="top">
+            <div class="flex justify-end w-full">
+              <el-button @click="resetQuery">清空</el-button>
+              <el-button @click="showPopover = false">取消</el-button>
+              <el-button type="primary" @click="handleQuery">确认</el-button>
+            </div>
+          </el-form-item>
+        </el-popover>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="流程名称" align="center" prop="name" min-width="200px" fixed="left" />
+      <el-table-column label="摘要" prop="summary" width="180" fixed="left">
+        <template #default="scope">
+          <div class="flex flex-col" v-if="scope.row.summary && scope.row.summary.length > 0">
+            <div v-for="(item, index) in scope.row.summary" :key="index">
+              <el-text type="info"> {{ item.key }} : {{ item.value }} </el-text>
+            </div>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="流程分类"
+        align="center"
+        prop="categoryName"
+        min-width="100"
+        fixed="left"
+      />
+      <el-table-column label="流程状态" prop="status" min-width="200">
+        <template #default="scope">
+          <!-- 审批中状态 -->
+          <template
+            v-if="
+              scope.row.status === BpmProcessInstanceStatus.RUNNING && scope.row.tasks?.length > 0
+            "
+          >
+            <!-- 单人审批 -->
+            <template v-if="scope.row.tasks.length === 1">
+              <span>
+                <el-button link type="primary" @click="handleDetail(scope.row)">
+                  {{ scope.row.tasks[0].assigneeUser?.nickname }}
+                </el-button>
+                ({{ scope.row.tasks[0].name }}) 审批中
+              </span>
+            </template>
+            <!-- 多人审批 -->
+            <template v-else>
+              <span>
+                <el-button link type="primary" @click="handleDetail(scope.row)">
+                  {{ scope.row.tasks[0].assigneeUser?.nickname }}
+                </el-button>
+                等 {{ scope.row.tasks.length }} 人 ({{ scope.row.tasks[0].name }})审批中
+              </span>
+            </template>
+          </template>
+          <!-- 非审批中状态 -->
+          <template v-else>
+            <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
+          </template>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="发起时间"
+        align="center"
+        prop="startTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column
+        label="结束时间"
+        align="center"
+        prop="endTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="操作" align="center" fixed="right" width="180">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            v-hasPermi="['bpm:process-instance:cancel']"
+            @click="handleDetail(scope.row)"
+          >
+            详情
+          </el-button>
+          <el-button
+            link
+            type="primary"
+            v-if="scope.row.status === 1"
+            v-hasPermi="['bpm:process-instance:query']"
+            @click="handleCancel(scope.row)"
+          >
+            取消
+          </el-button>
+          <el-button link type="primary" v-else @click="handleCreate(scope.row)">
+            重新发起
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { ElMessageBox } from 'element-plus'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+import { ProcessInstanceVO } from '@/api/bpm/processInstance'
+import * as DefinitionApi from '@/api/bpm/definition'
+import { BpmProcessInstanceStatus } from '@/utils/constants'
+
+defineOptions({ name: 'BpmProcessInstanceMy' })
+
+const router = useRouter() // 路由
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const processDefinitionList = ref<any[]>([]) // 流程定义列表
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: '',
+  processDefinitionKey: undefined,
+  category: undefined,
+  status: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const categoryList = ref<CategoryVO[]>([]) // 流程分类列表
+const showPopover = ref(false) // 高级筛选是否展示
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ProcessInstanceApi.getProcessInstanceMyPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 发起流程操作 **/
+const handleCreate = async (row?: ProcessInstanceVO) => {
+  // 如果是【业务表单】,不支持重新发起
+  if (row?.id) {
+    const processDefinitionDetail = await DefinitionApi.getProcessDefinition(
+      row.processDefinitionId
+    )
+    if (processDefinitionDetail.formType === 20) {
+      message.error('重新发起流程失败,原因:该流程使用业务表单,不支持重新发起')
+      return
+    }
+  }
+  // 跳转发起流程界面
+  await router.push({
+    name: 'BpmProcessInstanceCreate',
+    query: { processInstanceId: row?.id }
+  })
+}
+
+/** 查看详情 */
+const handleDetail = (row: ProcessInstanceVO) => {
+  router.push({
+    name: 'BpmProcessInstanceDetail',
+    query: {
+      id: row.id
+    }
+  })
+}
+
+/** 取消按钮操作 */
+const handleCancel = async (row: ProcessInstanceVO) => {
+  // 二次确认
+  const { value } = await ElMessageBox.prompt('请输入取消原因', '取消流程', {
+    confirmButtonText: t('common.ok'),
+    cancelButtonText: t('common.cancel'),
+    inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 判断非空,且非空格
+    inputErrorMessage: '取消原因不能为空'
+  })
+  // 发起取消
+  await ProcessInstanceApi.cancelProcessInstanceByStartUser(row.id, value)
+  message.success('取消成功')
+  // 刷新列表
+  await getList()
+}
+
+/** 激活时 **/
+onActivated(() => {
+  getList()
+})
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+  categoryList.value = await CategoryApi.getCategorySimpleList()
+  // 获取流程定义列表
+  processDefinitionList.value = await DefinitionApi.getSimpleProcessDefinitionList()
+})
+</script>

+ 258 - 0
src/views/bpm/processInstance/manager/index.vue

@@ -0,0 +1,258 @@
+<template>
+
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="发起人" prop="startUserId">
+        <el-select v-model="queryParams.startUserId" placeholder="请选择发起人" class="!w-240px">
+          <el-option
+            v-for="user in userList"
+            :key="user.id"
+            :label="user.nickname"
+            :value="user.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="流程名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入流程名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="所属流程" prop="processDefinitionId">
+        <el-input
+          v-model="queryParams.processDefinitionId"
+          placeholder="请输入流程定义的编号"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="流程分类" prop="category">
+        <el-select
+          v-model="queryParams.category"
+          placeholder="请选择流程分类"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="category in categoryList"
+            :key="category.code"
+            :label="category.name"
+            :value="category.code"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="流程状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择流程状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="发起时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="流程名称" align="center" prop="name" min-width="200px" fixed="left" />
+      <el-table-column
+        label="流程分类"
+        align="center"
+        prop="categoryName"
+        min-width="100"
+        fixed="left"
+      />
+      <el-table-column label="流程发起人" align="center" prop="startUser.nickname" width="120" />
+      <el-table-column label="发起部门" align="center" prop="startUser.deptName" width="120" />
+      <el-table-column label="流程状态" prop="status" width="120">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="发起时间"
+        align="center"
+        prop="startTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column
+        label="结束时间"
+        align="center"
+        prop="endTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column align="center" label="耗时" prop="durationInMillis" width="169">
+        <template #default="scope">
+          {{ scope.row.durationInMillis > 0 ? formatPast2(scope.row.durationInMillis) : '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="当前审批任务" align="center" prop="tasks" min-width="120px">
+        <template #default="scope">
+          <el-button type="primary" v-for="task in scope.row.tasks" :key="task.id" link>
+            <span>{{ task.name }}</span>
+          </el-button>
+        </template>
+      </el-table-column>
+      <el-table-column label="流程编号" align="center" prop="id" min-width="320px" />
+      <el-table-column label="操作" align="center" fixed="right" width="180">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            v-hasPermi="['bpm:process-instance:cancel']"
+            @click="handleDetail(scope.row)"
+          >
+            详情
+          </el-button>
+          <el-button
+            link
+            type="primary"
+            v-if="scope.row.status === 1"
+            v-hasPermi="['bpm:process-instance:query']"
+            @click="handleCancel(scope.row)"
+          >
+            取消
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
+import { ElMessageBox } from 'element-plus'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import { CategoryApi } from '@/api/bpm/category'
+import * as UserApi from '@/api/system/user'
+import { cancelProcessInstanceByAdmin } from '@/api/bpm/processInstance'
+
+// 它和【我的流程】的差异是,该菜单可以看全部的流程实例
+defineOptions({ name: 'BpmProcessInstanceManager' })
+
+const router = useRouter() // 路由
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  startUserId: undefined,
+  name: '',
+  processDefinitionId: undefined,
+  category: undefined,
+  status: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const categoryList = ref([]) // 流程分类列表
+const userList = ref<any[]>([]) // 用户列表
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ProcessInstanceApi.getProcessInstanceManagerPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 查看详情 */
+const handleDetail = (row) => {
+  router.push({
+    name: 'BpmProcessInstanceDetail',
+    query: {
+      id: row.id
+    }
+  })
+}
+
+/** 取消按钮操作 */
+const handleCancel = async (row) => {
+  // 二次确认
+  const { value } = await ElMessageBox.prompt('请输入取消原因', '取消流程', {
+    confirmButtonText: t('common.ok'),
+    cancelButtonText: t('common.cancel'),
+    inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 判断非空,且非空格
+    inputErrorMessage: '取消原因不能为空'
+  })
+  // 发起取消
+  await ProcessInstanceApi.cancelProcessInstanceByAdmin(row.id, value)
+  message.success('取消成功')
+  // 刷新列表
+  await getList()
+}
+
+/** 激活时 **/
+onActivated(() => {
+  getList()
+})
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+  categoryList.value = await CategoryApi.getCategorySimpleList()
+  userList.value = await UserApi.getSimpleUserList()
+})
+</script>

+ 273 - 0
src/views/bpm/processInstance/report/index.vue

@@ -0,0 +1,273 @@
+<template>
+
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="发起人" prop="startUserId">
+        <el-select v-model="queryParams.startUserId" placeholder="请选择发起人" class="!w-240px">
+          <el-option
+            v-for="user in userList"
+            :key="user.id"
+            :label="user.nickname"
+            :value="user.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="流程名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入流程名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="流程状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择流程状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="发起时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="结束时间" prop="endTime">
+        <el-date-picker
+          v-model="queryParams.endTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item
+        v-for="(item, index) in formFields"
+        :key="index"
+        :label="item.title"
+        :prop="item.field"
+      >
+        <!-- TODO @lesan:目前只支持input类型的字符串搜索 -->
+        <el-input
+          :disabled="item.type !== 'input'"
+          v-model="queryParams.formFieldsParams[item.field]"
+          :placeholder="`请输入${item.title}`"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" border :data="list">
+      <el-table-column label="流程名称" align="center" prop="name" fixed="left" width="200" />
+      <el-table-column label="流程发起人" align="center" prop="startUser.nickname" width="120" />
+      <el-table-column label="流程状态" prop="status" width="120">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="发起时间"
+        align="center"
+        prop="startTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column
+        label="结束时间"
+        align="center"
+        prop="endTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column
+        v-for="(item, index) in formFields"
+        :key="index"
+        :label="item.title"
+        :prop="item.field"
+        width="120"
+      >
+        <!-- TODO @lesan:可以根据formField的type进行展示方式的控制,现在全部以字符串 -->
+        <template #default="scope">
+          {{ scope.row.formVariables[item.field] ?? '' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" fixed="right" width="180">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            v-hasPermi="['bpm:process-instance:cancel']"
+            @click="handleDetail(scope.row)"
+          >
+            详情
+          </el-button>
+          <el-button
+            link
+            type="primary"
+            v-if="scope.row.status === 1"
+            v-hasPermi="['bpm:process-instance:query']"
+            @click="handleCancel(scope.row)"
+          >
+            取消
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import * as UserApi from '@/api/system/user'
+import * as DefinitionApi from '@/api/bpm/definition'
+import { parseFormFields } from '@/components/FormCreate/src/utils'
+import { ElMessageBox } from 'element-plus'
+
+defineOptions({ name: 'BpmProcessInstanceReport' })
+
+const router = useRouter() // 路由
+const { query } = useRoute()
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const formFields = ref()
+const processDefinitionId = query.processDefinitionId as string
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  startUserId: undefined,
+  name: '',
+  processDefinitionKey: query.processDefinitionKey,
+  status: undefined,
+  createTime: [],
+  endTime: [],
+  formFieldsParams: {}
+})
+const queryFormRef = ref() // 搜索的表单
+const userList = ref<any[]>([]) // 用户列表
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ProcessInstanceApi.getProcessInstanceManagerPage({
+      ...queryParams,
+      formFieldsParams: JSON.stringify(queryParams.formFieldsParams)
+    })
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 获取流程定义 */
+const getProcessDefinition = async () => {
+  const processDefinition = await DefinitionApi.getProcessDefinition(processDefinitionId)
+  formFields.value = parseFormCreateFields(processDefinition.formFields)
+}
+
+/** 解析表单字段 */
+const parseFormCreateFields = (formFields?: string[]) => {
+  const result: Array<Record<string, any>> = []
+  if (formFields) {
+    formFields.forEach((fieldStr: string) => {
+      parseFormFields(JSON.parse(fieldStr), result)
+    })
+  }
+  return result
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  queryParams.formFieldsParams = {}
+  handleQuery()
+}
+
+/** 查看详情 */
+const handleDetail = (row) => {
+  router.push({
+    name: 'BpmProcessInstanceDetail',
+    query: {
+      id: row.id
+    }
+  })
+}
+
+/** 取消按钮操作 */
+const handleCancel = async (row) => {
+  // 二次确认
+  const { value } = await ElMessageBox.prompt('请输入取消原因', '取消流程', {
+    confirmButtonText: t('common.ok'),
+    cancelButtonText: t('common.cancel'),
+    inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 判断非空,且非空格
+    inputErrorMessage: '取消原因不能为空'
+  })
+  // 发起取消
+  await ProcessInstanceApi.cancelProcessInstanceByAdmin(row.id, value)
+  message.success('取消成功')
+  // 刷新列表
+  await getList()
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  // 获取流程定义,用于 table column 的展示
+  await getProcessDefinition()
+  // 获取流程列表
+  await getList()
+  // 获取用户列表
+  userList.value = await UserApi.getSimpleUserList()
+})
+</script>

+ 162 - 0
src/views/bpm/processListener/ProcessListenerForm.vue

@@ -0,0 +1,162 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="110px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="名字" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入名字" />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :value="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="类型" prop="type">
+        <el-select
+          v-model="formData.type"
+          placeholder="请选择类型"
+          @change="formData.event = undefined"
+        >
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.BPM_PROCESS_LISTENER_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="事件" prop="event">
+        <el-select v-model="formData.event" placeholder="请选择事件">
+          <el-option
+            v-for="event in formData.type == 'execution'
+              ? ['start', 'end']
+              : ['create', 'assignment', 'complete', 'delete', 'update', 'timeout']"
+            :label="event"
+            :value="event"
+            :key="event"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="值类型" prop="valueType">
+        <el-select v-model="formData.valueType" placeholder="请选择值类型">
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="类路径" prop="value" v-if="formData.type == 'class'">
+        <el-input v-model="formData.value" placeholder="请输入类路径" />
+      </el-form-item>
+      <el-form-item label="表达式" prop="value" v-else>
+        <el-input v-model="formData.value" placeholder="请输入表达式" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, getStrDictOptions, DICT_TYPE } from '@/utils/dict'
+import { ProcessListenerApi, ProcessListenerVO } from '@/api/bpm/processListener'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** BPM 流程 表单 */
+defineOptions({ name: 'ProcessListenerForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  type: undefined,
+  status: undefined,
+  event: undefined,
+  valueType: undefined,
+  value: undefined
+})
+const formRules = reactive({
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  type: [{ required: true, message: '类型不能为空', trigger: 'change' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
+  event: [{ required: true, message: '监听事件不能为空', trigger: 'blur' }],
+  valueType: [{ required: true, message: '值类型不能为空', trigger: 'change' }],
+  value: [{ required: true, message: '值不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await ProcessListenerApi.getProcessListener(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as ProcessListenerVO
+    if (formType.value === 'create') {
+      await ProcessListenerApi.createProcessListener(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await ProcessListenerApi.updateProcessListener(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    type: undefined,
+    status: CommonStatusEnum.ENABLE,
+    event: undefined,
+    valueType: undefined,
+    value: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 184 - 0
src/views/bpm/processListener/index.vue

@@ -0,0 +1,184 @@
+<template>
+
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="85px"
+    >
+      <el-form-item label="名字" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入名字"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="类型" prop="type">
+        <el-select v-model="queryParams.type" placeholder="请选择类型" clearable class="!w-240px">
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.BPM_PROCESS_LISTENER_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['bpm:process-listener:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="类型" align="center" prop="type">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.BPM_PROCESS_LISTENER_TYPE" :value="scope.row.type" />
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column label="事件" align="center" prop="event" />
+      <el-table-column label="值类型" align="center" prop="valueType">
+        <template #default="scope">
+          <dict-tag
+            :type="DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE"
+            :value="scope.row.valueType"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="值" align="center" prop="value" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['bpm:process-listener:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['bpm:process-listener:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <ProcessListenerForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getStrDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { ProcessListenerApi, ProcessListenerVO } from '@/api/bpm/processListener'
+import ProcessListenerForm from './ProcessListenerForm.vue'
+
+/** BPM 流程 列表 */
+defineOptions({ name: 'BpmProcessListener' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<ProcessListenerVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  type: undefined,
+  event: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ProcessListenerApi.getProcessListenerPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await ProcessListenerApi.deleteProcessListener(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 40 - 0
src/views/bpm/simple/SimpleModelDesign.vue

@@ -0,0 +1,40 @@
+<template>
+  <ContentWrap :bodyStyle="{ padding: '20px 16px' }">
+    <SimpleProcessDesigner
+      :model-id="modelId"
+      :model-key="modelKey"
+      :model-name="modelName"
+      @success="handleSuccess"
+      :start-user-ids="startUserIds"
+      :start-dept-ids="startDeptIds"
+      ref="designerRef"
+    />
+  </ContentWrap>
+</template>
+<script setup lang="ts">
+import { SimpleProcessDesigner } from '@/components/SimpleProcessDesignerV2/src/'
+
+defineOptions({
+  name: 'SimpleModelDesign'
+})
+
+defineProps<{
+  modelId?: string
+  modelKey?: string
+  modelName?: string
+  startUserIds?: number[]
+  startDeptIds?: number[]
+}>()
+
+const emit = defineEmits(['success'])
+const designerRef = ref()
+
+// 修改成功回调
+const handleSuccess = (data?: any) => {
+  console.info('handleSuccess', data)
+  if (data) {
+    emit('success', data)
+  }
+}
+</script>
+<style lang="scss" scoped></style>

+ 157 - 0
src/views/bpm/task/copy/index.vue

@@ -0,0 +1,157 @@
+<!-- 工作流 - 抄送我的流程 -->
+<template>
+
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form ref="queryFormRef" :inline="true" class="-mb-15px" label-width="68px">
+      <el-form-item label="流程名称" prop="name">
+        <el-input
+          v-model="queryParams.processInstanceName"
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+          clearable
+          placeholder="请输入流程名称"
+        />
+      </el-form-item>
+      <el-form-item label="抄送时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+          end-placeholder="结束日期"
+          start-placeholder="开始日期"
+          type="daterange"
+          value-format="YYYY-MM-DD HH:mm:ss"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon class="mr-5px" icon="ep:search" />
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon class="mr-5px" icon="ep:refresh" />
+          重置
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <!-- TODO 芋艿:增加摘要 -->
+      <el-table-column align="center" label="流程名" prop="processInstanceName" min-width="180" />
+      <el-table-column label="摘要" prop="summary" min-width="180">
+        <template #default="scope">
+          <div class="flex flex-col" v-if="scope.row.summary && scope.row.summary.length > 0">
+            <div v-for="(item, index) in scope.row.summary" :key="index">
+              <el-text type="info"> {{ item.key }} : {{ item.value }} </el-text>
+            </div>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column
+        align="center"
+        label="流程发起人"
+        prop="startUser.nickname"
+        min-width="100"
+      />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="流程发起时间"
+        prop="processInstanceStartTime"
+        width="180"
+      />
+      <el-table-column align="center" label="抄送节点" prop="activityName" min-width="180" />
+      <el-table-column align="center" label="抄送人" min-width="100">
+        <template #default="scope"> {{ scope.row.createUser?.nickname || '系统' }} </template>
+      </el-table-column>
+      <el-table-column align="center" label="抄送意见" prop="reason" width="150" />
+      <el-table-column
+        align="center"
+        label="抄送时间"
+        prop="createTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column align="center" label="操作" fixed="right" width="80">
+        <template #default="scope">
+          <el-button link type="primary" @click="handleAudit(scope.row)">详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+
+defineOptions({ name: 'BpmProcessInstanceCopy' })
+
+const { push } = useRouter() // 路由
+
+const loading = ref(false) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  processInstanceId: '',
+  processInstanceName: '',
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询任务列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ProcessInstanceApi.getProcessInstanceCopyPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 处理审批按钮 */
+const handleAudit = (row: any) => {
+  const query = {
+    id: row.processInstanceId,
+    activityId: undefined
+  }
+  if (row.activityId) {
+    query.activityId = row.activityId
+  }
+  push({
+    name: 'BpmProcessInstanceDetail',
+    query: query
+  })
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 265 - 0
src/views/bpm/task/done/index.vue

@@ -0,0 +1,265 @@
+<template>
+
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="68px"
+    >
+      <el-form-item label="" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          class="!w-240px"
+          clearable
+          placeholder="请输入任务名称"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon class="mr-5px" icon="ep:search" />
+          搜索
+        </el-button>
+      </el-form-item>
+
+      <el-form-item label="" prop="category" :style="{ position: 'absolute', right: '300px' }">
+        <el-select
+          v-model="queryParams.category"
+          placeholder="请选择流程分类"
+          clearable
+          class="!w-155px"
+          @change="handleQuery"
+        >
+          <el-option
+            v-for="category in categoryList"
+            :key="category.code"
+            :label="category.name"
+            :value="category.code"
+          />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="" prop="status" :style="{ position: 'absolute', right: '130px' }">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择流程状态"
+          clearable
+          class="!w-155px"
+          @change="handleQuery"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+
+      <!-- 高级筛选 -->
+      <el-form-item :style="{ position: 'absolute', right: '0px' }">
+        <el-popover
+          :visible="showPopover"
+          persistent
+          :width="400"
+          :show-arrow="false"
+          placement="bottom-end"
+        >
+          <template #reference>
+            <el-button @click="showPopover = !showPopover">
+              <Icon icon="ep:plus" class="mr-5px" />高级筛选
+            </el-button>
+          </template>
+          <el-form-item
+            label="所属流程"
+            class="font-bold"
+            label-position="top"
+            prop="processDefinitionKey"
+          >
+            <el-select
+              v-model="queryParams.processDefinitionKey"
+              placeholder="请选择流程定义"
+              clearable
+              @change="handleQuery"
+              class="!w-390px"
+            >
+              <el-option
+                v-for="item in processDefinitionList"
+                :key="item.key"
+                :label="item.name"
+                :value="item.key"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="发起时间" class="bold-label" label-position="top" prop="createTime">
+            <el-date-picker
+              v-model="queryParams.createTime"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              type="daterange"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item class="bold-label" label-position="top">
+            <el-button @click="handleQuery"> 确认</el-button>
+            <el-button @click="showPopover = false"> 取消</el-button>
+            <el-button @click="resetQuery"> 清空</el-button>
+          </el-form-item>
+        </el-popover>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column align="center" label="流程" prop="processInstance.name" width="180" />
+      <el-table-column label="摘要" prop="processInstance.summary" width="180">
+        <template #default="scope">
+          <div
+            class="flex flex-col"
+            v-if="scope.row.processInstance.summary && scope.row.processInstance.summary.length > 0"
+          >
+            <div v-for="(item, index) in scope.row.processInstance.summary" :key="index">
+              <el-text type="info"> {{ item.key }} : {{ item.value }} </el-text>
+            </div>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column
+        align="center"
+        label="发起人"
+        prop="processInstance.startUser.nickname"
+        width="100"
+      />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="发起时间"
+        prop="createTime"
+        width="180"
+      />
+      <el-table-column align="center" label="当前任务" prop="name" width="180" />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="任务开始时间"
+        prop="createTime"
+        width="180"
+      />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="任务结束时间"
+        prop="endTime"
+        width="180"
+      />
+      <el-table-column align="center" label="审批状态" prop="status" width="120">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="审批建议" prop="reason" min-width="180" />
+      <el-table-column align="center" label="耗时" prop="durationInMillis" width="160">
+        <template #default="scope">
+          {{ formatPast2(scope.row.durationInMillis) }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        align="center"
+        label="流程编号"
+        prop="processInstanceId"
+        :show-overflow-tooltip="true"
+      />
+      <el-table-column align="center" label="任务编号" prop="id" :show-overflow-tooltip="true" />
+      <el-table-column align="center" label="操作" fixed="right" width="80">
+        <template #default="scope">
+          <el-button link type="primary" @click="handleAudit(scope.row)">历史</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
+import * as TaskApi from '@/api/bpm/task'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+import * as DefinitionApi from '@/api/bpm/definition'
+
+defineOptions({ name: 'BpmDoneTask' })
+
+const { push } = useRouter() // 路由
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const processDefinitionList = ref<any[]>([]) // 流程定义列表
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: '',
+  category: undefined,
+  status: undefined,
+  processDefinitionKey: '',
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const categoryList = ref<CategoryVO[]>([]) // 流程分类列表
+const showPopover = ref(false) // 高级筛选是否展示
+
+/** 查询任务列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await TaskApi.getTaskDonePage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 处理审批按钮 */
+const handleAudit = (row: any) => {
+  push({
+    name: 'BpmProcessInstanceDetail',
+    query: {
+      id: row.processInstance.id,
+      taskId: row.id
+    }
+  })
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+  categoryList.value = await CategoryApi.getCategorySimpleList()
+  // 获取流程定义列表
+  processDefinitionList.value = await DefinitionApi.getSimpleProcessDefinitionList()
+})
+</script>

+ 165 - 0
src/views/bpm/task/manager/index.vue

@@ -0,0 +1,165 @@
+<template>
+
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="68px"
+    >
+      <el-form-item label="任务名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          class="!w-240px"
+          clearable
+          placeholder="请输入任务名称"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+          end-placeholder="结束日期"
+          start-placeholder="开始日期"
+          type="daterange"
+          value-format="YYYY-MM-DD HH:mm:ss"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon class="mr-5px" icon="ep:search" />
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon class="mr-5px" icon="ep:refresh" />
+          重置
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column align="center" label="流程" prop="processInstance.name" width="180" />
+      <el-table-column
+        align="center"
+        label="发起人"
+        prop="processInstance.startUser.nickname"
+        width="100"
+      />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="发起时间"
+        prop="createTime"
+        width="180"
+      />
+      <el-table-column align="center" label="当前任务" prop="name" width="180" />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="任务开始时间"
+        prop="createTime"
+        width="180"
+      />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="任务结束时间"
+        prop="endTime"
+        width="180"
+      />
+      <el-table-column align="center" label="审批人" prop="assigneeUser.nickname" width="100" />
+      <el-table-column align="center" label="审批状态" prop="status" width="120">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="审批建议" prop="reason" min-width="180" />
+      <el-table-column align="center" label="耗时" prop="durationInMillis" width="160">
+        <template #default="scope">
+          {{ formatPast2(scope.row.durationInMillis) }}
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="流程编号" prop="processInstanceId" :show-overflow-tooltip="true" />
+      <el-table-column align="center" label="任务编号" prop="id" :show-overflow-tooltip="true" />
+      <el-table-column align="center" label="操作" fixed="right" width="80">
+        <template #default="scope">
+          <el-button link type="primary" @click="handleAudit(scope.row)">历史</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
+import * as TaskApi from '@/api/bpm/task'
+
+// 它和【待办任务】【已办任务】的差异是,该菜单可以看全部的流程任务
+defineOptions({ name: 'BpmManagerTask' })
+
+const { push } = useRouter() // 路由
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: '',
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询任务列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await TaskApi.getTaskManagerPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 处理审批按钮 */
+const handleAudit = (row: any) => {
+  push({
+    name: 'BpmProcessInstanceDetail',
+    query: {
+      id: row.processInstance.id
+    }
+  })
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 229 - 0
src/views/bpm/task/todo/index.vue

@@ -0,0 +1,229 @@
+<template>
+
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="68px"
+    >
+      <el-form-item label="" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          class="!w-240px"
+          clearable
+          placeholder="请输入任务名称"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon class="mr-5px" icon="ep:search" />
+          搜索
+        </el-button>
+      </el-form-item>
+      <el-form-item label="" prop="category" class="absolute right-130px">
+        <el-select
+          v-model="queryParams.category"
+          placeholder="请选择流程分类"
+          clearable
+          class="!w-155px"
+          @change="handleQuery"
+        >
+          <el-option
+            v-for="category in categoryList"
+            :key="category.code"
+            :label="category.name"
+            :value="category.code"
+          />
+        </el-select>
+      </el-form-item>
+      <!-- 高级筛选 -->
+      <el-form-item class="absolute right-0">
+        <el-popover
+          :visible="showPopover"
+          persistent
+          :width="400"
+          :show-arrow="false"
+          placement="bottom-end"
+        >
+          <template #reference>
+            <el-button @click="showPopover = !showPopover">
+              <Icon icon="ep:plus" class="mr-5px" />高级筛选
+            </el-button>
+          </template>
+          <el-form-item
+            label="所属流程"
+            class="font-bold"
+            label-position="top"
+            prop="processDefinitionKey"
+          >
+            <el-select
+              v-model="queryParams.processDefinitionKey"
+              placeholder="请选择流程定义"
+              clearable
+              @change="handleQuery"
+              class="!w-390px"
+            >
+              <el-option
+                v-for="item in processDefinitionList"
+                :key="item.key"
+                :label="item.name"
+                :value="item.key"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="发起时间" class="font-bold" label-position="top" prop="createTime">
+            <el-date-picker
+              v-model="queryParams.createTime"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              type="daterange"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+              class="w-240px!"
+            />
+          </el-form-item>
+          <el-form-item class="font-bold" label-position="top">
+            <div class="flex justify-end w-full">
+              <el-button @click="resetQuery">清空</el-button>
+              <el-button @click="showPopover = false">取消</el-button>
+              <el-button type="primary" @click="handleQuery">确认</el-button>
+            </div>
+          </el-form-item>
+        </el-popover>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column align="center" label="流程" prop="processInstance.name" width="180" />
+      <el-table-column label="摘要" prop="processInstance.summary" width="180">
+        <template #default="scope">
+          <div
+            class="flex flex-col"
+            v-if="scope.row.processInstance.summary && scope.row.processInstance.summary.length > 0"
+          >
+            <div v-for="(item, index) in scope.row.processInstance.summary" :key="index">
+              <el-text type="info"> {{ item.key }} : {{ item.value }} </el-text>
+            </div>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column
+        align="center"
+        label="发起人"
+        prop="processInstance.startUser.nickname"
+        width="100"
+      />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="发起时间"
+        prop="createTime"
+        width="180"
+      />
+      <el-table-column align="center" label="当前任务" prop="name" width="180" />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="任务时间"
+        prop="createTime"
+        width="180"
+      />
+      <el-table-column
+        align="center"
+        label="流程编号"
+        prop="processInstanceId"
+        :show-overflow-tooltip="true"
+      />
+      <el-table-column align="center" label="任务编号" prop="id" :show-overflow-tooltip="true" />
+      <el-table-column align="center" label="操作" fixed="right" width="80">
+        <template #default="scope">
+          <el-button link type="primary" @click="handleAudit(scope.row)">办理</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as TaskApi from '@/api/bpm/task'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+import * as DefinitionApi from '@/api/bpm/definition'
+
+defineOptions({ name: 'BpmTodoTask' })
+
+const { push } = useRouter() // 路由
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const processDefinitionList = ref<any[]>([]) // 流程定义列表
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: '',
+  category: undefined,
+  processDefinitionKey: '',
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const categoryList = ref<CategoryVO[]>([]) // 流程分类列表
+const showPopover = ref(false) // 高级筛选是否展示
+
+/** 查询任务列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await TaskApi.getTaskTodoPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 处理审批按钮 */
+const handleAudit = (row: any) => {
+  push({
+    name: 'BpmProcessInstanceDetail',
+    query: {
+      id: row.processInstance.id,
+      taskId: row.id
+    }
+  })
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+  categoryList.value = await CategoryApi.getCategorySimpleList()
+  // 获取流程定义列表
+  processDefinitionList.value = await DefinitionApi.getSimpleProcessDefinitionList()
+})
+</script>