Browse Source

Merge branch 'feature/bpm' of https://gitee.com/yudaocode/yudao-ui-admin-vue3

YunaiV 1 year ago
parent
commit
77971f9c96
100 changed files with 6426 additions and 1764 deletions
  1. 43 0
      src/api/bpm/category/index.ts
  2. 3 2
      src/api/bpm/definition/index.ts
  3. 2 2
      src/api/bpm/form/index.ts
  4. 1 1
      src/api/bpm/leave/index.ts
  5. 42 0
      src/api/bpm/processExpression/index.ts
  6. 26 28
      src/api/bpm/processInstance/index.ts
  7. 40 0
      src/api/bpm/processListener/index.ts
  8. 25 40
      src/api/bpm/task/index.ts
  9. 0 29
      src/api/bpm/taskAssignRule/index.ts
  10. 3 3
      src/api/bpm/userGroup/index.ts
  11. 1 1
      src/api/crm/business/index.ts
  12. 1 1
      src/api/crm/clue/index.ts
  13. 1 1
      src/api/crm/contact/index.ts
  14. 1 1
      src/api/crm/contract/index.ts
  15. 1 6
      src/api/crm/customer/index.ts
  16. 8 0
      src/api/crm/permission/index.ts
  17. 6 4
      src/api/crm/receivable/index.ts
  18. 1 1
      src/api/crm/receivable/plan/index.ts
  19. 116 0
      src/api/crm/statistics/customer.ts
  20. 237 0
      src/components/SimpleProcessDesigner/src/addNode.vue
  21. 297 0
      src/components/SimpleProcessDesigner/src/nodeWrap.vue
  22. 165 0
      src/components/SimpleProcessDesigner/src/util.ts
  23. 1292 0
      src/components/SimpleProcessDesigner/theme/workflow.css
  24. 4 4
      src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue
  25. 47 30
      src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue
  26. 10 0
      src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json
  27. 10 0
      src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/camundaDescriptor.json
  28. 10 0
      src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json
  29. 3 8
      src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue
  30. 20 9
      src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
  31. 220 207
      src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue
  32. 46 1
      src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue
  33. 83 0
      src/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue
  34. 41 1
      src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue
  35. 27 0
      src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts
  36. 31 5
      src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue
  37. 2 1
      src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue
  38. 68 0
      src/components/bpmnProcessDesigner/package/penal/task/task-components/ProcessExpressionDialog.vue
  39. 193 59
      src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
  40. 1 0
      src/components/bpmnProcessDesigner/package/utils.ts
  41. 15 26
      src/router/modules/remaining.ts
  42. 55 0
      src/store/modules/simpleWorkflow.ts
  43. 5 5
      src/utils/dict.ts
  44. 8 5
      src/utils/formCreate.ts
  45. 5 5
      src/utils/formatTime.ts
  46. 9 3
      src/views/Login/components/LoginForm.vue
  47. 124 0
      src/views/bpm/category/CategoryForm.vue
  48. 198 0
      src/views/bpm/category/index.vue
  49. 5 32
      src/views/bpm/definition/index.vue
  50. 5 5
      src/views/bpm/group/UserGroupForm.vue
  51. 1 1
      src/views/bpm/group/index.vue
  52. 18 9
      src/views/bpm/model/ModelForm.vue
  53. 1 0
      src/views/bpm/model/ModelImportForm.vue
  54. 1 1
      src/views/bpm/model/editor/index.vue
  55. 25 22
      src/views/bpm/model/index.vue
  56. 79 3
      src/views/bpm/oa/leave/create.vue
  57. 11 6
      src/views/bpm/oa/leave/index.vue
  58. 114 0
      src/views/bpm/processExpression/ProcessExpressionForm.vue
  59. 180 0
      src/views/bpm/processExpression/index.vue
  60. 170 48
      src/views/bpm/processInstance/create/index.vue
  61. 10 13
      src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue
  62. 0 96
      src/views/bpm/processInstance/detail/ProcessInstanceChildrenTaskList.vue
  63. 93 46
      src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
  64. 0 242
      src/views/bpm/processInstance/detail/TaskCCDialogForm.vue
  65. 6 3
      src/views/bpm/processInstance/detail/dialog/TaskDelegateForm.vue
  66. 10 10
      src/views/bpm/processInstance/detail/dialog/TaskReturnForm.vue
  67. 12 10
      src/views/bpm/processInstance/detail/dialog/TaskSignCreateForm.vue
  68. 11 7
      src/views/bpm/processInstance/detail/dialog/TaskSignDeleteForm.vue
  69. 106 0
      src/views/bpm/processInstance/detail/dialog/TaskSignList.vue
  70. 12 6
      src/views/bpm/processInstance/detail/dialog/TaskTransferForm.vue
  71. 104 36
      src/views/bpm/processInstance/detail/index.vue
  72. 59 51
      src/views/bpm/processInstance/index.vue
  73. 255 0
      src/views/bpm/processInstance/manager/index.vue
  74. 162 0
      src/views/bpm/processListener/ProcessListenerForm.vue
  75. 183 0
      src/views/bpm/processListener/index.vue
  76. 28 0
      src/views/bpm/simpleWorkflow/index.vue
  77. 17 20
      src/views/bpm/task/copy/index.vue
  78. 0 51
      src/views/bpm/task/done/TaskDetail.vue
  79. 41 27
      src/views/bpm/task/done/index.vue
  80. 166 0
      src/views/bpm/task/manager/index.vue
  81. 23 25
      src/views/bpm/task/todo/index.vue
  82. 0 250
      src/views/bpm/taskAssignRule/TaskAssignRuleForm.vue
  83. 0 136
      src/views/bpm/taskAssignRule/index.vue
  84. 11 11
      src/views/crm/business/detail/index.vue
  85. 3 3
      src/views/crm/clue/detail/index.vue
  86. 24 21
      src/views/crm/contact/components/ContactList.vue
  87. 28 22
      src/views/crm/contact/components/ContactListModal.vue
  88. 8 8
      src/views/crm/contact/detail/index.vue
  89. 2 3
      src/views/crm/contract/detail/index.vue
  90. 2 2
      src/views/crm/customer/detail/index.vue
  91. 10 7
      src/views/crm/permission/components/PermissionForm.vue
  92. 57 23
      src/views/crm/permission/components/TransferForm.vue
  93. 14 13
      src/views/crm/receivable/ReceivableForm.vue
  94. 9 6
      src/views/crm/receivable/plan/ReceivablePlanForm.vue
  95. 130 0
      src/views/crm/statistics/customer/components/CustomerConversionStat.vue
  96. 127 0
      src/views/crm/statistics/customer/components/CustomerDealCycle.vue
  97. 124 0
      src/views/crm/statistics/customer/components/CustomerFollowupSummary.vue
  98. 105 0
      src/views/crm/statistics/customer/components/CustomerFollowupType.vue
  99. 151 0
      src/views/crm/statistics/customer/components/CustomerSummary.vue
  100. 166 0
      src/views/crm/statistics/customer/index.vue

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

@@ -0,0 +1,43 @@
+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 })
+  },
+
+  // 删除流程分类
+  deleteCategory: async (id: number) => {
+    return await request.delete({ url: `/bpm/category/delete?id=` + id })
+  }
+}

+ 3 - 2
src/api/bpm/definition/index.ts

@@ -1,8 +1,9 @@
 import request from '@/config/axios'
 
-export const getProcessDefinitionBpmnXML = async (id: number) => {
+export const getProcessDefinition = async (id: number, key: string) => {
   return await request.get({
-    url: '/bpm/process-definition/get-bpmn-xml?id=' + id
+    url: '/bpm/process-definition/get',
+    params: { id, key }
   })
 }
 

+ 2 - 2
src/api/bpm/form/index.ts

@@ -49,8 +49,8 @@ export const getFormPage = async (params) => {
 }
 
 // 获得动态表单的精简列表
-export const getSimpleFormList = async () => {
+export const getFormSimpleList = async () => {
   return await request.get({
-    url: '/bpm/form/list-all-simple'
+    url: '/bpm/form/simple-list'
   })
 }

+ 1 - 1
src/api/bpm/leave/index.ts

@@ -2,7 +2,7 @@ import request from '@/config/axios'
 
 export type LeaveVO = {
   id: number
-  result: number
+  status: number
   type: number
   reason: string
   processInstanceId: string

+ 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 })
+  }
+}

+ 26 - 28
src/api/bpm/processInstance/index.ts

@@ -20,51 +20,49 @@ export type ProcessInstanceVO = {
   endTime: string
 }
 
-export type ProcessInstanceCCVO = {
-  type: number,
-  taskName: string,
-  taskKey: string,
-  processInstanceName: string,
-  processInstanceKey: string,
-  startUserId: string,
-  options:string [],
+export type ProcessInstanceCopyVO = {
+  type: number
+  taskName: string
+  taskKey: string
+  processInstanceName: string
+  processInstanceKey: string
+  startUserId: string
+  options: string[]
   reason: string
 }
 
-export const getMyProcessInstancePage = async (params) => {
+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 cancelProcessInstance = async (id: number, reason: string) => {
+export const cancelProcessInstanceByStartUser = async (id: number, reason: string) => {
   const data = {
     id: id,
     reason: reason
   }
-  return await request.delete({ url: '/bpm/process-instance/cancel', data: data })
+  return await request.delete({ url: '/bpm/process-instance/cancel-by-start-user', data: data })
 }
 
-export const getProcessInstance = async (id: number) => {
-  return await request.get({ url: '/bpm/process-instance/get?id=' + id })
+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 })
 }
 
-/**
- * 抄送
- * @param data 抄送数据
- * @returns 是否抄送成功
- */
-export const createProcessInstanceCC = async (data) => {
-  return await request.post({ url: '/bpm/process-instance/cc/create', data: data })
+export const getProcessInstance = async (id: string) => {
+  return await request.get({ url: '/bpm/process-instance/get?id=' + id })
 }
 
-/**
- * 抄送列表
- * @param params 
- * @returns 
- */
-export const getProcessInstanceCCPage = async (params) => {
-  return await request.get({ url: '/bpm/process-instance/cc/my-page', params })
-}
+export const getProcessInstanceCopyPage = async (params: any) => {
+  return await request.get({ url: '/bpm/process-instance/copy/page', params })
+}

+ 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 })
+  }
+}

+ 25 - 40
src/api/bpm/task/index.ts

@@ -4,78 +4,63 @@ export type TaskVO = {
   id: number
 }
 
-export const getTodoTaskPage = async (params) => {
+export const getTaskTodoPage = async (params: any) => {
   return await request.get({ url: '/bpm/task/todo-page', params })
 }
 
-export const getDoneTaskPage = async (params) => {
+export const getTaskDonePage = async (params: any) => {
   return await request.get({ url: '/bpm/task/done-page', params })
 }
 
-export const completeTask = async (data) => {
-  return await request.put({ url: '/bpm/task/complete', data })
+export const getTaskManagerPage = async (params: any) => {
+  return await request.get({ url: '/bpm/task/manager-page', params })
 }
 
-export const approveTask = async (data) => {
+export const approveTask = async (data: any) => {
   return await request.put({ url: '/bpm/task/approve', data })
 }
 
-export const rejectTask = async (data) => {
+export const rejectTask = async (data: any) => {
   return await request.put({ url: '/bpm/task/reject', data })
 }
-export const backTask = async (data) => {
-  return await request.put({ url: '/bpm/task/back', data })
-}
-
-export const updateTaskAssignee = async (data) => {
-  return await request.put({ url: '/bpm/task/update-assignee', data })
-}
 
-export const getTaskListByProcessInstanceId = async (processInstanceId) => {
+export const getTaskListByProcessInstanceId = async (processInstanceId: string) => {
   return await request.get({
     url: '/bpm/task/list-by-process-instance-id?processInstanceId=' + processInstanceId
   })
 }
 
-// 导出任务
-export const exportTask = async (params) => {
-  return await request.download({ url: '/bpm/task/export', params })
-}
-
 // 获取所有可回退的节点
-export const getReturnList = async (params) => {
-  return await request.get({ url: '/bpm/task/return-list', params })
+export const getTaskListByReturn = async (id: string) => {
+  return await request.get({ url: '/bpm/task/list-by-return', params: { id } })
 }
 
 // 回退
-export const returnTask = async (data) => {
+export const returnTask = async (data: any) => {
   return await request.put({ url: '/bpm/task/return', data })
 }
 
-/**
- * 委派
- */
-export const delegateTask = async (data) => {
+// 委派
+export const delegateTask = async (data: any) => {
   return await request.put({ url: '/bpm/task/delegate', data })
 }
 
-/**
- * 加签
- */
-export const taskAddSign = async (data) => {
-  return await request.put({ url: '/bpm/task/create-sign', data })
+// 转派
+export const transferTask = async (data: any) => {
+  return await request.put({ url: '/bpm/task/transfer', data })
 }
 
-/**
- * 获取减签任务列表
- */
-export const getChildrenTaskList = async (id: string) => {
-  return await request.get({ url: '/bpm/task/children-list?taskId=' + id })
+// 加签
+export const signCreateTask = async (data: any) => {
+  return await request.put({ url: '/bpm/task/create-sign', data })
 }
 
-/**
- * 减签
- */
-export const taskSubSign = async (data) => {
+// 减签
+export const signDeleteTask = async (data: any) => {
   return await request.delete({ url: '/bpm/task/delete-sign', data })
 }
+
+// 获取减签任务列表
+export const getChildrenTaskList = async (id: string) => {
+  return await request.get({ url: '/bpm/task/list-by-parent-task-id?parentTaskId=' + id })
+}

+ 0 - 29
src/api/bpm/taskAssignRule/index.ts

@@ -1,29 +0,0 @@
-import request from '@/config/axios'
-
-export type TaskAssignVO = {
-  id: number
-  modelId: string
-  processDefinitionId: string
-  taskDefinitionKey: string
-  taskDefinitionName: string
-  options: string[]
-  type: number
-}
-
-export const getTaskAssignRuleList = async (params) => {
-  return await request.get({ url: '/bpm/task-assign-rule/list', params })
-}
-
-export const createTaskAssignRule = async (data: TaskAssignVO) => {
-  return await request.post({
-    url: '/bpm/task-assign-rule/create',
-    data: data
-  })
-}
-
-export const updateTaskAssignRule = async (data: TaskAssignVO) => {
-  return await request.put({
-    url: '/bpm/task-assign-rule/update',
-    data: data
-  })
-}

+ 3 - 3
src/api/bpm/userGroup/index.ts

@@ -4,7 +4,7 @@ export type UserGroupVO = {
   id: number
   name: string
   description: string
-  memberUserIds: number[]
+  userIds: number[]
   status: number
   remark: string
   createTime: string
@@ -42,6 +42,6 @@ export const getUserGroupPage = async (params) => {
 }
 
 // 获取用户组精简信息列表
-export const getSimpleUserGroupList = async (): Promise<UserGroupVO[]> => {
-  return await request.get({ url: '/bpm/user-group/list-all-simple' })
+export const getUserGroupSimpleList = async (): Promise<UserGroupVO[]> => {
+  return await request.get({ url: '/bpm/user-group/simple-list' })
 }

+ 1 - 1
src/api/crm/business/index.ts

@@ -1,5 +1,5 @@
 import request from '@/config/axios'
-import { TransferReqVO } from '@/api/crm/customer'
+import { TransferReqVO } from '@/api/crm/permission'
 
 export interface BusinessVO {
   id: number

+ 1 - 1
src/api/crm/clue/index.ts

@@ -1,5 +1,5 @@
 import request from '@/config/axios'
-import { TransferReqVO } from '@/api/crm/customer'
+import { TransferReqVO } from '@/api/crm/permission'
 
 export interface ClueVO {
   id: number // 编号

+ 1 - 1
src/api/crm/contact/index.ts

@@ -1,5 +1,5 @@
 import request from '@/config/axios'
-import { TransferReqVO } from '@/api/crm/customer'
+import { TransferReqVO } from '@/api/crm/permission'
 
 export interface ContactVO {
   id: number // 编号

+ 1 - 1
src/api/crm/contract/index.ts

@@ -1,5 +1,5 @@
 import request from '@/config/axios'
-import { TransferReqVO } from '@/api/crm/customer'
+import { TransferReqVO } from '@/api/crm/permission'
 
 export interface ContractVO {
   id: number

+ 1 - 6
src/api/crm/customer/index.ts

@@ -1,4 +1,5 @@
 import request from '@/config/axios'
+import { TransferReqVO } from '@/api/crm/permission'
 
 export interface CustomerVO {
   id: number // 编号
@@ -102,12 +103,6 @@ export const getCustomerSimpleList = async () => {
 
 // ======================= 业务操作 =======================
 
-export interface TransferReqVO {
-  id: number | undefined // 客户编号
-  newOwnerUserId: number | undefined // 新负责人的用户编号
-  oldOwnerPermissionLevel: number | undefined // 老负责人加入团队后的权限级别
-}
-
 // 客户转移
 export const transferCustomer = async (data: TransferReqVO) => {
   return await request.put({ url: '/crm/customer/transfer', data })

+ 8 - 0
src/api/crm/permission/index.ts

@@ -6,6 +6,7 @@ export interface PermissionVO {
   bizType: number // Crm 类型
   bizId: number // Crm 类型数据编号
   level: number // 权限级别
+  toBizTypes?: number[] // 同时添加至
   deptName?: string // 部门名称
   nickname?: string // 用户昵称
   postNames?: string[] // 岗位名称数组
@@ -13,6 +14,13 @@ export interface PermissionVO {
   ids?: number[]
 }
 
+export interface TransferReqVO {
+  bizId: number // 模块编号
+  newOwnerUserId: number // 新负责人的用户编号
+  oldOwnerPermissionLevel: number // 老负责人加入团队后的权限级别
+  toBizTypes?: number[] // 转移客户时,需要额外有【联系人】【商机】【合同】的 checkbox 选择
+}
+
 /**
  * CRM 业务类型枚举
  *

+ 6 - 4
src/api/crm/receivable/index.ts

@@ -3,18 +3,20 @@ import request from '@/config/axios'
 export interface ReceivableVO {
   id: number
   no: string
-  planId: number
-  customerId: number
+  planId?: number
+  customerId?: number
   customerName?: string
-  contractId: number
+  contractId?: number
   contract?: {
+    id?: number
+    name?: string
     no: string
     totalPrice: number
   }
   auditStatus: number
   processInstanceId: number
   returnTime: Date
-  returnType: string
+  returnType: number
   price: number
   ownerUserId: number
   ownerUserName?: string

+ 1 - 1
src/api/crm/receivable/plan/index.ts

@@ -11,7 +11,7 @@ export interface ReceivablePlanVO {
   remindTime: Date
   customerId: number
   customerName?: string
-  contractId: number
+  contractId?: number
   contractNo?: string
   ownerUserId: number
   ownerUserName?: string

+ 116 - 0
src/api/crm/statistics/customer.ts

@@ -0,0 +1,116 @@
+import request from '@/config/axios'
+
+export interface CrmStatisticsCustomerSummaryByDateRespVO {
+  time: string
+  customerCreateCount: number
+  customerDealCount: number
+}
+
+export interface CrmStatisticsCustomerSummaryByUserRespVO {
+  ownerUserName: string
+  customerCreateCount: number
+  customerDealCount: number
+  contractPrice: number
+  receivablePrice: number
+}
+
+export interface CrmStatisticsFollowupSummaryByDateRespVO {
+  time: string
+  followupRecordCount: number
+  followupCustomerCount: number
+}
+
+export interface CrmStatisticsFollowupSummaryByUserRespVO {
+  ownerUserName: string
+  followupRecordCount: number
+  followupCustomerCount: number
+}
+
+export interface CrmStatisticsFollowupSummaryByTypeRespVO {
+  followupType: string
+  followupRecordCount: number
+}
+
+export interface CrmStatisticsCustomerContractSummaryRespVO {
+  customerName: string
+  contractName: string
+  totalPrice: number
+  receivablePrice: number
+  customerType: string
+  customerSource: string
+  ownerUserName: string
+  creatorUserName: string
+  createTime: Date
+  orderDate: Date
+}
+
+export interface CrmStatisticsCustomerDealCycleByDateRespVO {
+  time: string
+  customerDealCycle: number
+}
+
+export interface CrmStatisticsCustomerDealCycleByUserRespVO {
+  ownerUserName: string
+  customerDealCycle: number
+  customerDealCount: number
+}
+
+// 客户分析 API
+export const StatisticsCustomerApi = {
+  // 1.1 客户总量分析(按日期)
+  getCustomerSummaryByDate: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-customer/get-customer-summary-by-date',
+      params
+    })
+  },
+  // 1.2 客户总量分析(按用户)
+  getCustomerSummaryByUser: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-customer/get-customer-summary-by-user',
+      params
+    })
+  },
+  // 2.1 客户跟进次数分析(按日期)
+  getFollowupSummaryByDate: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-customer/get-followup-summary-by-date',
+      params
+    })
+  },
+  // 2.2 客户跟进次数分析(按用户)
+  getFollowupSummaryByUser: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-customer/get-followup-summary-by-user',
+      params
+    })
+  },
+  // 3.1 获取客户跟进方式统计数
+  getFollowupSummaryByType: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-customer/get-followup-summary-by-type',
+      params
+    })
+  },
+  // 4.1 合同摘要信息(客户转化率页面)
+  getContractSummary: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-customer/get-contract-summary',
+      params
+    })
+  },
+  // 5.1 获取客户成交周期(按日期)
+  getCustomerDealCycleByDate: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-customer/get-customer-deal-cycle-by-date',
+      params
+    })
+  },
+  // 5.2 获取客户成交周期(按用户)
+  getCustomerDealCycleByUser: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-customer/get-customer-deal-cycle-by-user',
+      params
+    })
+  }
+}

+ 237 - 0
src/components/SimpleProcessDesigner/src/addNode.vue

@@ -0,0 +1,237 @@
+/* stylelint-disable order/properties-order */
+<template>
+  <div class="add-node-btn-box">
+    <div class="add-node-btn">
+      <el-popover placement="right-start" v-model="visible" width="auto">
+        <div class="add-node-popover-body">
+          <a class="add-node-popover-item approver" @click="addType(1)">
+            <div class="item-wrapper">
+              <span class="iconfont"></span>
+            </div>
+            <p>审批人</p>
+          </a>
+          <a class="add-node-popover-item notifier" @click="addType(2)">
+            <div class="item-wrapper">
+              <span class="iconfont"></span>
+            </div>
+            <p>抄送人</p>
+          </a>
+          <a class="add-node-popover-item condition" @click="addType(4)">
+            <div class="item-wrapper">
+              <span class="iconfont"></span>
+            </div>
+            <p>条件分支</p>
+          </a>
+        </div>
+        <template #reference>
+          <button class="btn" type="button">
+            <span class="iconfont"></span>
+          </button>
+        </template>
+      </el-popover>
+    </div>
+  </div>
+</template>
+<script setup>
+import { ref } from 'vue'
+let props = defineProps({
+  childNodeP: {
+    type: Object,
+    default: () => ({})
+  }
+})
+let emits = defineEmits(['update:childNodeP'])
+let visible = ref(false)
+const addType = (type) => {
+  visible.value = false
+  if (type != 4) {
+    var data
+    if (type == 1) {
+      data = {
+        nodeName: '审核人',
+        error: true,
+        type: 1,
+        settype: 1,
+        selectMode: 0,
+        selectRange: 0,
+        directorLevel: 1,
+        examineMode: 1,
+        noHanderAction: 1,
+        examineEndDirectorLevel: 0,
+        childNode: props.childNodeP,
+        nodeUserList: []
+      }
+    } else if (type == 2) {
+      data = {
+        nodeName: '抄送人',
+        type: 2,
+        ccSelfSelectFlag: 1,
+        childNode: props.childNodeP,
+        nodeUserList: []
+      }
+    }
+    emits('update:childNodeP', data)
+  } else {
+    emits('update:childNodeP', {
+      nodeName: '路由',
+      type: 4,
+      childNode: null,
+      conditionNodes: [
+        {
+          nodeName: '条件1',
+          error: true,
+          type: 3,
+          priorityLevel: 1,
+          conditionList: [],
+          nodeUserList: [],
+          childNode: props.childNodeP
+        },
+        {
+          nodeName: '条件2',
+          type: 3,
+          priorityLevel: 2,
+          conditionList: [],
+          nodeUserList: [],
+          childNode: null
+        }
+      ]
+    })
+  }
+}
+</script>
+<style scoped lang="scss">
+.add-node-btn-box {
+  width: 240px;
+  display: inline-flex;
+  -ms-flex-negative: 0;
+  flex-shrink: 0;
+  -webkit-box-flex: 1;
+  -ms-flex-positive: 1;
+  position: relative;
+
+  &:before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: -1;
+    margin: auto;
+    width: 2px;
+    height: 100%;
+    background-color: #cacaca;
+  }
+
+  .add-node-btn {
+    user-select: none;
+    width: 240px;
+    padding: 20px 0 32px;
+    display: flex;
+    -webkit-box-pack: center;
+    justify-content: center;
+    flex-shrink: 0;
+    -webkit-box-flex: 1;
+    flex-grow: 1;
+
+    .btn {
+      outline: none;
+      box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
+      width: 30px;
+      height: 30px;
+      background: #3296fa;
+      border-radius: 50%;
+      position: relative;
+      border: none;
+      line-height: 30px;
+      -webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+      transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+
+      .iconfont {
+        color: #fff;
+        font-size: 16px;
+      }
+
+      &:hover {
+        transform: scale(1.3);
+        box-shadow: 0 13px 27px 0 rgba(0, 0, 0, 0.1);
+      }
+
+      &:active {
+        transform: none;
+        background: #1e83e9;
+        box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
+      }
+    }
+  }
+}
+
+.add-node-popover-body {
+  display: flex;
+
+  .add-node-popover-item {
+    margin-right: 10px;
+    cursor: pointer;
+    text-align: center;
+    flex: 1;
+    color: #191f25 !important;
+
+    .item-wrapper {
+      user-select: none;
+      display: inline-block;
+      width: 80px;
+      height: 80px;
+      margin-bottom: 5px;
+      background: #fff;
+      border: 1px solid #e2e2e2;
+      border-radius: 50%;
+      transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+
+      .iconfont {
+        font-size: 35px;
+        line-height: 80px;
+      }
+    }
+
+    &.approver {
+      .item-wrapper {
+        color: #ff943e;
+      }
+    }
+
+    &.notifier {
+      .item-wrapper {
+        color: #3296fa;
+      }
+    }
+
+    &.condition {
+      .item-wrapper {
+        color: #15bc83;
+      }
+    }
+
+    &:hover {
+      .item-wrapper {
+        background: #3296fa;
+        box-shadow: 0 10px 20px 0 rgba(50, 150, 250, 0.4);
+      }
+
+      .iconfont {
+        color: #fff;
+      }
+    }
+
+    &:active {
+      .item-wrapper {
+        box-shadow: none;
+        background: #eaeaea;
+      }
+
+      .iconfont {
+        color: inherit;
+      }
+    }
+  }
+}
+</style>

+ 297 - 0
src/components/SimpleProcessDesigner/src/nodeWrap.vue

@@ -0,0 +1,297 @@
+<!-- eslint-disable vue/no-mutating-props -->
+<!--
+ * @Date: 2022-09-21 14:41:53
+ * @LastEditors: StavinLi 495727881@qq.com
+ * @LastEditTime: 2023-05-24 15:20:24
+ * @FilePath: /Workflow-Vue3/src/components/nodeWrap.vue
+-->
+<template>
+     <div class="node-wrap" v-if="nodeConfig.type < 3">
+      <div class="node-wrap-box" :class="(nodeConfig.type == 0 ? 'start-node ' : '') +(isTried && nodeConfig.error ? 'active error' : '')">
+          <div class="title" :style="`background: rgb(${bgColors[nodeConfig.type]});`">
+            <span v-if="nodeConfig.type == 0">{{ nodeConfig.nodeName }}</span>
+            <template v-else>
+              <span class="iconfont">{{nodeConfig.type == 1?'':''}}</span>
+              <input
+                v-if="isInput"
+                type="text"
+                class="ant-input editable-title-input"
+                @blur="blurEvent()"
+                @focus="$event.currentTarget.select()"
+                v-focus
+                v-model="nodeConfig.nodeName"
+                :placeholder="defaultText"
+              />
+              <span v-else class="editable-title" @click="clickEvent()">{{ nodeConfig.nodeName }}</span>
+              <i class="anticon anticon-close close" @click="delNode"></i>
+            </template>
+          </div>
+          <div class="content" @click="setPerson">
+            <div class="text">
+                <span class="placeholder" v-if="!showText">请选择{{defaultText}}</span>
+                {{showText}}
+            </div>
+            <i class="anticon anticon-right arrow"></i>
+          </div>
+          <div class="error_tip" v-if="isTried && nodeConfig.error">
+            <i class="anticon anticon-exclamation-circle"></i>
+          </div>
+      </div>
+      <addNode v-model:childNodeP="nodeConfig.childNode" />
+    </div>
+    <div class="branch-wrap" v-if="nodeConfig.type == 4">
+    <div class="branch-box-wrap">
+      <div class="branch-box">
+        <button class="add-branch" @click="addTerm">添加条件</button>
+        <div class="col-box" v-for="(item, index) in nodeConfig.conditionNodes" :key="index">
+          <div class="condition-node">
+            <div class="condition-node-box">
+              <div class="auto-judge" :class="isTried && item.error ? 'error active' : ''">
+                <div class="sort-left" v-if="index != 0" @click="arrTransfer(index, -1)">&lt;</div>
+                <div class="title-wrapper">
+                  <input
+                    v-if="isInputList[index]"
+                    type="text"
+                    class="ant-input editable-title-input"
+                    @blur="blurEvent(index)"
+                    @focus="$event.currentTarget.select()"
+                    v-model="item.nodeName"
+                  />
+                  <span v-else class="editable-title" @click="clickEvent(index)">{{ item.nodeName }}</span>
+                  <span class="priority-title" @click="setPerson(item.priorityLevel)">优先级{{ item.priorityLevel }}</span>
+                  <i class="anticon anticon-close close" @click="delTerm(index)"></i>
+                </div>
+                <div class="sort-right" v-if="index != nodeConfig.conditionNodes.length - 1" @click="arrTransfer(index)">&gt;</div>
+                <div class="content" @click="setPerson(item.priorityLevel)">{{ conditionStr(nodeConfig, index) }}</div>
+                <div class="error_tip" v-if="isTried && item.error">
+                    <i class="anticon anticon-exclamation-circle"></i>
+                </div>
+              </div>
+              <addNode v-model:childNodeP="item.childNode" />
+            </div>
+          </div>
+          <nodeWrap v-if="item.childNode" v-model:nodeConfig="item.childNode" />
+          <template v-if="index == 0">
+            <div class="top-left-cover-line"></div>
+            <div class="bottom-left-cover-line"></div>
+          </template>
+          <template v-if="index == nodeConfig.conditionNodes.length - 1">
+            <div class="top-right-cover-line"></div>
+            <div class="bottom-right-cover-line"></div>
+          </template>
+        </div>
+      </div>
+      <addNode v-model:childNodeP="nodeConfig.childNode" />
+    </div>
+  </div>
+    <nodeWrap v-if="nodeConfig.childNode" v-model:nodeConfig="nodeConfig.childNode" />
+</template>
+<script  setup>
+import addNode from './addNode.vue'
+import { onMounted, ref, watch, getCurrentInstance, computed } from 'vue'
+import {
+  arrToStr,
+  conditionStr,
+  setApproverStr,
+  copyerStr,
+  bgColors,
+  placeholderList
+} from './util'
+import { useWorkFlowStoreWithOut } from '@/store/modules/simpleWorkflow'
+let _uid = getCurrentInstance().uid
+
+let props = defineProps({
+  nodeConfig: {
+    type: Object,
+    default: () => ({})
+  },
+  flowPermission: {
+    type: Object,
+    // eslint-disable-next-line vue/require-valid-default-prop
+    default: () => []
+  }
+})
+
+let defaultText = computed(() => {
+  return placeholderList[props.nodeConfig.type]
+})
+let showText = computed(() => {
+  if (props.nodeConfig.type == 0) return arrToStr(props.flowPermission) || '所有人'
+  if (props.nodeConfig.type == 1) return setApproverStr(props.nodeConfig)
+  return copyerStr(props.nodeConfig)
+})
+
+let isInputList = ref([])
+let isInput = ref(false)
+const resetConditionNodesErr = () => {
+  for (var i = 0; i < props.nodeConfig.conditionNodes.length; i++) {
+    // eslint-disable-next-line vue/no-mutating-props
+    props.nodeConfig.conditionNodes[i].error =
+      conditionStr(props.nodeConfig, i) == '请设置条件' &&
+      i != props.nodeConfig.conditionNodes.length - 1
+  }
+}
+onMounted(() => {
+  if (props.nodeConfig.type == 1) {
+    // eslint-disable-next-line vue/no-mutating-props
+    props.nodeConfig.error = !setApproverStr(props.nodeConfig)
+  } else if (props.nodeConfig.type == 2) {
+    // eslint-disable-next-line vue/no-mutating-props
+    props.nodeConfig.error = !copyerStr(props.nodeConfig)
+  } else if (props.nodeConfig.type == 4) {
+    resetConditionNodesErr()
+  }
+})
+let emits = defineEmits(['update:flowPermission', 'update:nodeConfig'])
+let store = useWorkFlowStoreWithOut()
+let {
+  setPromoter,
+  setApprover,
+  setCopyer,
+  setCondition,
+  setFlowPermission,
+  setApproverConfig,
+  setCopyerConfig,
+  setConditionsConfig
+} = store
+let isTried = computed(() => store.isTried)
+let flowPermission1 = computed(() => store.flowPermission1)
+let approverConfig1 = computed(() => store.approverConfig1)
+let copyerConfig1 = computed(() => store.copyerConfig1)
+let conditionsConfig1 = computed(() => store.conditionsConfig1)
+watch(flowPermission1, (flow) => {
+  if (flow.flag && flow.id === _uid) {
+    emits('update:flowPermission', flow.value)
+  }
+})
+watch(approverConfig1, (approver) => {
+  if (approver.flag && approver.id === _uid) {
+    emits('update:nodeConfig', approver.value)
+  }
+})
+watch(copyerConfig1, (copyer) => {
+  if (copyer.flag && copyer.id === _uid) {
+    emits('update:nodeConfig', copyer.value)
+  }
+})
+watch(conditionsConfig1, (condition) => {
+  if (condition.flag && condition.id === _uid) {
+    emits('update:nodeConfig', condition.value)
+  }
+})
+
+const clickEvent = (index) => {
+  if (index || index === 0) {
+    isInputList.value[index] = true
+  } else {
+    isInput.value = true
+  }
+}
+const blurEvent = (index) => {
+  if (index || index === 0) {
+    isInputList.value[index] = false
+    // eslint-disable-next-line vue/no-mutating-props
+    props.nodeConfig.conditionNodes[index].nodeName =
+      props.nodeConfig.conditionNodes[index].nodeName || '条件'
+  } else {
+    isInput.value = false
+    // eslint-disable-next-line vue/no-mutating-props
+    props.nodeConfig.nodeName = props.nodeConfig.nodeName || defaultText
+  }
+}
+const delNode = () => {
+  emits('update:nodeConfig', props.nodeConfig.childNode)
+}
+const addTerm = () => {
+  let len = props.nodeConfig.conditionNodes.length + 1
+  // eslint-disable-next-line vue/no-mutating-props
+  props.nodeConfig.conditionNodes.push({
+    nodeName: '条件' + len,
+    type: 3,
+    priorityLevel: len,
+    conditionList: [],
+    nodeUserList: [],
+    childNode: null
+  })
+  resetConditionNodesErr()
+  emits('update:nodeConfig', props.nodeConfig)
+}
+const delTerm = (index) => {
+  // eslint-disable-next-line vue/no-mutating-props
+  props.nodeConfig.conditionNodes.splice(index, 1)
+  props.nodeConfig.conditionNodes.map((item, index) => {
+    item.priorityLevel = index + 1
+    item.nodeName = `条件${index + 1}`
+  })
+  resetConditionNodesErr()
+  emits('update:nodeConfig', props.nodeConfig)
+  if (props.nodeConfig.conditionNodes.length == 1) {
+    if (props.nodeConfig.childNode) {
+      if (props.nodeConfig.conditionNodes[0].childNode) {
+        reData(props.nodeConfig.conditionNodes[0].childNode, props.nodeConfig.childNode)
+      } else {
+        // eslint-disable-next-line vue/no-mutating-props
+        props.nodeConfig.conditionNodes[0].childNode = props.nodeConfig.childNode
+      }
+    }
+    emits('update:nodeConfig', props.nodeConfig.conditionNodes[0].childNode)
+  }
+}
+const reData = (data, addData) => {
+  if (!data.childNode) {
+    data.childNode = addData
+  } else {
+    reData(data.childNode, addData)
+  }
+}
+const setPerson = (priorityLevel) => {
+  var { type } = props.nodeConfig
+  if (type == 0) {
+    setPromoter(true)
+    setFlowPermission({
+      value: props.flowPermission,
+      flag: false,
+      id: _uid
+    })
+  } else if (type == 1) {
+    setApprover(true)
+    setApproverConfig({
+      value: {
+        ...JSON.parse(JSON.stringify(props.nodeConfig)),
+        ...{ settype: props.nodeConfig.settype ? props.nodeConfig.settype : 1 }
+      },
+      flag: false,
+      id: _uid
+    })
+  } else if (type == 2) {
+    setCopyer(true)
+    setCopyerConfig({
+      value: JSON.parse(JSON.stringify(props.nodeConfig)),
+      flag: false,
+      id: _uid
+    })
+  } else {
+    setCondition(true)
+    setConditionsConfig({
+      value: JSON.parse(JSON.stringify(props.nodeConfig)),
+      priorityLevel,
+      flag: false,
+      id: _uid
+    })
+  }
+}
+const arrTransfer = (index, type = 1) => {
+  //向左-1,向右1
+  // eslint-disable-next-line vue/no-mutating-props
+  props.nodeConfig.conditionNodes[index] = props.nodeConfig.conditionNodes.splice(
+    index + type,
+    1,
+    props.nodeConfig.conditionNodes[index]
+  )[0]
+  props.nodeConfig.conditionNodes.map((item, index) => {
+    item.priorityLevel = index + 1
+  })
+  resetConditionNodesErr()
+  emits('update:nodeConfig', props.nodeConfig)
+}
+</script>

+ 165 - 0
src/components/SimpleProcessDesigner/src/util.ts

@@ -0,0 +1,165 @@
+/**
+ * todo
+ */
+export const arrToStr = (arr?: [{ name: string }]) => {
+  if (arr) {
+    return arr
+      .map((item) => {
+        return item.name
+      })
+      .toString()
+  }
+}
+
+export const setApproverStr = (nodeConfig: any) => {
+  if (nodeConfig.settype == 1) {
+    if (nodeConfig.nodeUserList.length == 1) {
+      return nodeConfig.nodeUserList[0].name
+    } else if (nodeConfig.nodeUserList.length > 1) {
+      if (nodeConfig.examineMode == 1) {
+        return arrToStr(nodeConfig.nodeUserList)
+      } else if (nodeConfig.examineMode == 2) {
+        return nodeConfig.nodeUserList.length + '人会签'
+      }
+    }
+  } else if (nodeConfig.settype == 2) {
+    const level =
+      nodeConfig.directorLevel == 1 ? '直接主管' : '第' + nodeConfig.directorLevel + '级主管'
+    if (nodeConfig.examineMode == 1) {
+      return level
+    } else if (nodeConfig.examineMode == 2) {
+      return level + '会签'
+    }
+  } else if (nodeConfig.settype == 4) {
+    if (nodeConfig.selectRange == 1) {
+      return '发起人自选'
+    } else {
+      if (nodeConfig.nodeUserList.length > 0) {
+        if (nodeConfig.selectRange == 2) {
+          return '发起人自选'
+        } else {
+          return '发起人从' + nodeConfig.nodeUserList[0].name + '中自选'
+        }
+      } else {
+        return ''
+      }
+    }
+  } else if (nodeConfig.settype == 5) {
+    return '发起人自己'
+  } else if (nodeConfig.settype == 7) {
+    return '从直接主管到通讯录中级别最高的第' + nodeConfig.examineEndDirectorLevel + '个层级主管'
+  }
+}
+
+export const copyerStr = (nodeConfig: any) => {
+  if (nodeConfig.nodeUserList.length != 0) {
+    return arrToStr(nodeConfig.nodeUserList)
+  } else {
+    if (nodeConfig.ccSelfSelectFlag == 1) {
+      return '发起人自选'
+    }
+  }
+}
+export const conditionStr = (nodeConfig, index) => {
+  const { conditionList, nodeUserList } = nodeConfig.conditionNodes[index]
+  if (conditionList.length == 0) {
+    return index == nodeConfig.conditionNodes.length - 1 &&
+      nodeConfig.conditionNodes[0].conditionList.length != 0
+      ? '其他条件进入此流程'
+      : '请设置条件'
+  } else {
+    let str = ''
+    for (let i = 0; i < conditionList.length; i++) {
+      const {
+        columnId,
+        columnType,
+        showType,
+        showName,
+        optType,
+        zdy1,
+        opt1,
+        zdy2,
+        opt2,
+        fixedDownBoxValue
+      } = conditionList[i]
+      if (columnId == 0) {
+        if (nodeUserList.length != 0) {
+          str += '发起人属于:'
+          str +=
+            nodeUserList
+              .map((item) => {
+                return item.name
+              })
+              .join('或') + ' 并且 '
+        }
+      }
+      if (columnType == 'String' && showType == '3') {
+        if (zdy1) {
+          str += showName + '属于:' + dealStr(zdy1, JSON.parse(fixedDownBoxValue)) + ' 并且 '
+        }
+      }
+      if (columnType == 'Double') {
+        if (optType != 6 && zdy1) {
+          const optTypeStr = ['', '<', '>', '≤', '=', '≥'][optType]
+          str += `${showName} ${optTypeStr} ${zdy1} 并且 `
+        } else if (optType == 6 && zdy1 && zdy2) {
+          str += `${zdy1} ${opt1} ${showName} ${opt2} ${zdy2} 并且 `
+        }
+      }
+    }
+    return str ? str.substring(0, str.length - 4) : '请设置条件'
+  }
+}
+
+export const dealStr = (str: string, obj) => {
+  const arr = []
+  const list = str.split(',')
+  for (const elem in obj) {
+    list.map((item) => {
+      if (item == elem) {
+        arr.push(obj[elem].value)
+      }
+    })
+  }
+  return arr.join('或')
+}
+
+export const removeEle = (arr, elem, key = 'id') => {
+  let includesIndex
+  arr.map((item, index) => {
+    if (item[key] == elem[key]) {
+      includesIndex = index
+    }
+  })
+  arr.splice(includesIndex, 1)
+}
+
+export const bgColors = ['87, 106, 149', '255, 148, 62', '50, 150, 250']
+export const placeholderList = ['发起人', '审核人', '抄送人']
+export const setTypes = [
+  { value: 1, label: '指定成员' },
+  { value: 2, label: '主管' },
+  { value: 4, label: '发起人自选' },
+  { value: 5, label: '发起人自己' },
+  { value: 7, label: '连续多级主管' }
+]
+
+export const selectModes = [
+  { value: 1, label: '选一个人' },
+  { value: 2, label: '选多个人' }
+]
+
+export const selectRanges = [
+  { value: 1, label: '全公司' },
+  { value: 2, label: '指定成员' },
+  { value: 3, label: '指定角色' }
+]
+
+export const optTypes = [
+  { value: '1', label: '小于' },
+  { value: '2', label: '大于' },
+  { value: '3', label: '小于等于' },
+  { value: '4', label: '等于' },
+  { value: '5', label: '大于等于' },
+  { value: '6', label: '介于两个数之间' }
+]

+ 1292 - 0
src/components/SimpleProcessDesigner/theme/workflow.css

@@ -0,0 +1,1292 @@
+
+.clearfix {
+    zoom: 1
+}
+
+.clearfix:after,
+.clearfix:before {
+    content: "";
+    display: table
+}
+
+.clearfix:after {
+    clear: both
+}
+
+@font-face {
+    font-family: anticon;
+    font-display: fallback;
+    src: url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.eot");
+    src: url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.woff") format("woff"), url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.ttf") format("truetype"), url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.svg#iconfont") format("svg")
+}
+
+.anticon {
+    display: inline-block;
+    font-style: normal;
+    vertical-align: baseline;
+    text-align: center;
+    text-transform: none;
+    line-height: 1;
+    text-rendering: optimizeLegibility;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale
+}
+
+.anticon:before {
+    display: block;
+    font-family: anticon!important
+}
+.anticon-close:before {
+  content: "\E633"
+}
+.anticon-right:before {
+    content: "\E61F"
+}
+.anticon-exclamation-circle{
+    color: rgb(242, 86, 67)
+}
+.anticon-exclamation-circle:before {
+    content: "\E62C"
+}
+
+.anticon-left:before {
+    content: "\E620"
+}
+
+.anticon-close-circle:before {
+    content: "\E62E"
+}
+  
+.ant-btn {
+    line-height: 1.5;
+    display: inline-block;
+    font-weight: 400;
+    text-align: center;
+    touch-action: manipulation;
+    cursor: pointer;
+    background-image: none;
+    border: 1px solid transparent;
+    white-space: nowrap;
+    padding: 0 15px;
+    font-size: 14px;
+    border-radius: 4px;
+    height: 32px;
+    user-select: none;
+    transition: all .3s cubic-bezier(.645, .045, .355, 1);
+    position: relative;
+    color: rgba(0, 0, 0, .65);
+    background-color: #fff;
+    border-color: #d9d9d9
+}
+
+.ant-btn>.anticon {
+    line-height: 1
+}
+
+.ant-btn,
+.ant-btn:active,
+.ant-btn:focus {
+    outline: 0
+}
+
+.ant-btn>a:only-child {
+    color: currentColor
+}
+
+.ant-btn>a:only-child:after {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    right: 0;
+    background: transparent
+}
+
+.ant-btn:focus,
+.ant-btn:hover {
+    color: #40a9ff;
+    background-color: #fff;
+    border-color: #40a9ff
+}
+
+.ant-btn:focus>a:only-child,
+.ant-btn:hover>a:only-child {
+    color: currentColor
+}
+
+.ant-btn:focus>a:only-child:after,
+.ant-btn:hover>a:only-child:after {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    right: 0;
+    background: transparent
+}
+
+.ant-btn.active,
+.ant-btn:active {
+    color: #096dd9;
+    background-color: #fff;
+    border-color: #096dd9
+}
+
+.ant-btn.active>a:only-child,
+.ant-btn:active>a:only-child {
+    color: currentColor
+}
+
+.ant-btn.active>a:only-child:after,
+.ant-btn:active>a:only-child:after {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    right: 0;
+    background: transparent
+}
+
+.ant-btn.active,
+.ant-btn:active,
+.ant-btn:focus,
+.ant-btn:hover {
+    background: #fff;
+    text-decoration: none
+}
+
+.ant-btn>i,
+.ant-btn>span {
+    pointer-events: none
+}
+
+.ant-btn:before {
+    position: absolute;
+    top: -1px;
+    left: -1px;
+    bottom: -1px;
+    right: -1px;
+    background: #fff;
+    opacity: .35;
+    content: "";
+    border-radius: inherit;
+    z-index: 1;
+    transition: opacity .2s;
+    pointer-events: none;
+    display: none
+}
+
+.ant-btn .anticon {
+    transition: margin-left .3s cubic-bezier(.645, .045, .355, 1)
+}
+
+.ant-btn:active>span,
+.ant-btn:focus>span {
+    position: relative
+}
+
+.ant-btn>.anticon+span,
+.ant-btn>span+.anticon {
+    margin-left: 8px
+}
+
+.ant-input {
+    font-family: Chinese Quote, -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif;
+    font-variant: tabular-nums;
+    box-sizing: border-box;
+    margin: 0;
+    padding: 0;
+    list-style: none;
+    position: relative;
+    display: inline-block;
+    padding: 4px 11px;
+    width: 100%;
+    height: 32px;
+    font-size: 14px;
+    line-height: 1.5;
+    color: rgba(0, 0, 0, .65);
+    background-color: #fff;
+    background-image: none;
+    border: 1px solid #d9d9d9;
+    border-radius: 4px;
+    transition: all .3s
+}
+
+.ant-input::-moz-placeholder {
+    color: #bfbfbf;
+    opacity: 1
+}
+
+.ant-input:-ms-input-placeholder {
+    color: #bfbfbf
+}
+
+.ant-input::-webkit-input-placeholder {
+    color: #bfbfbf
+}
+
+.ant-input:focus,
+.ant-input:hover {
+    border-color: #40a9ff;
+    border-right-width: 1px!important
+}
+
+.ant-input:focus {
+    outline: 0;
+    box-shadow: 0 0 0 2px rgba(24, 144, 255, .2)
+}
+
+textarea.ant-input {
+    max-width: 100%;
+    height: auto;
+    vertical-align: bottom;
+    transition: all .3s, height 0s;
+    min-height: 32px
+}
+
+a,
+abbr,
+acronym,
+address,
+applet,
+article,
+aside,
+audio,
+b,
+big,
+blockquote,
+body,
+canvas,
+caption,
+center,
+cite,
+code,
+dd,
+del,
+details,
+dfn,
+div,
+dl,
+dt,
+em,
+fieldset,
+figcaption,
+figure,
+footer,
+form,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+header,
+hgroup,
+html,
+i,
+iframe,
+img,
+ins,
+kbd,
+label,
+legend,
+li,
+mark,
+menu,
+nav,
+object,
+ol,
+p,
+pre,
+q,
+s,
+samp,
+section,
+small,
+span,
+strike,
+strong,
+sub,
+summary,
+sup,
+table,
+tbody,
+td,
+tfoot,
+th,
+thead,
+time,
+tr,
+tt,
+u,
+ul,
+var,
+video {
+    margin: 0;
+    padding: 0;
+    border: 0;
+    outline: 0;
+    font-size: 100%;
+    font: inherit;
+    vertical-align: baseline
+}
+
+*,
+:after,
+:before {
+    -webkit-box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    box-sizing: border-box
+}
+
+html {
+    font-family: sans-serif;
+    -ms-text-size-adjust: 100%;
+    -webkit-text-size-adjust: 100%
+}
+
+body,
+html {
+    font-size: 14px
+}
+
+body {
+    font-family: Microsoft Yahei, Lucida Grande, Lucida Sans Unicode, Helvetica, Arial, Verdana, sans-serif;
+    line-height: 1.6;
+    background-color: #fff;
+    position: static!important;
+    -webkit-tap-highlight-color: rgba(0, 0, 0, 0)
+}
+
+ol,
+ul {
+    list-style-type: none
+}
+
+b,
+strong {
+    font-weight: 700
+}
+
+img {
+    border: 0
+}
+
+button,
+input,
+select,
+textarea {
+    font-family: inherit;
+    font-size: 100%;
+    margin: 0
+}
+
+textarea {
+    overflow: auto;
+    vertical-align: top;
+    -webkit-appearance: none
+}
+
+button,
+input {
+    line-height: normal
+}
+
+button,
+select {
+    text-transform: none
+}
+
+button,
+html input[type=button],
+input[type=reset],
+input[type=submit] {
+    -webkit-appearance: button;
+    cursor: pointer
+}
+
+input[type=search] {
+    -webkit-appearance: textfield;
+    -moz-box-sizing: content-box;
+    -webkit-box-sizing: content-box;
+    box-sizing: content-box
+}
+
+input[type=search]::-webkit-search-cancel-button,
+input[type=search]::-webkit-search-decoration {
+    -webkit-appearance: none
+}
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+    border: 0;
+    padding: 0
+}
+
+table {
+    width: 100%;
+    border-spacing: 0;
+    border-collapse: collapse
+}
+
+table,
+td,
+th {
+    border: 0
+}
+
+td,
+th {
+    padding: 0;
+    vertical-align: top
+}
+
+th {
+    font-weight: 700;
+    text-align: left
+}
+
+thead th {
+    white-space: nowrap
+}
+
+a {
+    text-decoration: none;
+    cursor: pointer;
+    color: #3296fa
+}
+
+a:active,
+a:hover {
+    outline: 0;
+    color: #3296fa
+}
+
+small {
+    font-size: 80%
+}
+
+body,
+html {
+    font-size: 12px!important;
+    color: #191f25!important;
+    background: #f6f6f6!important
+}
+
+.wrap {
+    display: -webkit-box;
+    display: -ms-flexbox;
+    display: flex;
+    -webkit-box-orient: vertical;
+    -webkit-box-direction: normal;
+    -ms-flex-direction: column;
+    flex-direction: column;
+    height: 100%
+}
+
+@font-face {
+    font-family: IconFont;
+    src: url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.eot");
+    src: url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.woff") format("woff"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.ttf") format("truetype"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.svg#IconFont") format("svg")
+}
+
+.iconfont {
+    font-family: IconFont!important;
+    font-size: 16px;
+    font-style: normal;
+    -webkit-font-smoothing: antialiased;
+    -webkit-text-stroke-width: .2px;
+    -moz-osx-font-smoothing: grayscale
+}
+
+.fd-nav {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    z-index: 997;
+    width: 100%;
+    height: 60px;
+    font-size: 14px;
+    color: #fff;
+    background: #3296fa;
+    display: flex;
+    align-items: center
+}
+
+.fd-nav>* {
+    flex: 1;
+    width: 100%
+}
+
+.fd-nav .fd-nav-left {
+    display: -webkit-box;
+    display: flex;
+    align-items: center
+}
+
+.fd-nav .fd-nav-center {
+    flex: none;
+    width: 600px;
+    text-align: center
+}
+
+.fd-nav .fd-nav-right {
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+    text-align: right
+}
+
+.fd-nav .fd-nav-back {
+    display: inline-block;
+    width: 60px;
+    height: 60px;
+    font-size: 22px;
+    border-right: 1px solid #1583f2;
+    text-align: center;
+    cursor: pointer
+}
+
+.fd-nav .fd-nav-back:hover {
+    background: #5af
+}
+
+.fd-nav .fd-nav-back:active {
+    background: #1583f2
+}
+
+.fd-nav .fd-nav-back .anticon {
+    line-height: 60px
+}
+
+.fd-nav .fd-nav-title {
+    width: 0;
+    flex: 1;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    padding: 0 15px
+}
+
+.fd-nav a {
+    color: #fff;
+    margin-left: 12px
+}
+
+.fd-nav .button-publish {
+    min-width: 80px;
+    margin-left: 4px;
+    margin-right: 15px;
+    color: #3296fa;
+    border-color: #fff
+}
+
+.fd-nav .button-publish.ant-btn:focus,
+.fd-nav .button-publish.ant-btn:hover {
+    color: #3296fa;
+    border-color: #fff;
+    box-shadow: 0 10px 20px 0 rgba(0, 0, 0, .3)
+}
+
+.fd-nav .button-publish.ant-btn:active {
+    color: #3296fa;
+    background: #d6eaff;
+    box-shadow: none
+}
+
+.fd-nav .button-preview {
+    min-width: 80px;
+    margin-left: 16px;
+    margin-right: 4px;
+    color: #fff;
+    border-color: #fff;
+    background: transparent
+}
+
+.fd-nav .button-preview.ant-btn:focus,
+.fd-nav .button-preview.ant-btn:hover {
+    color: #fff;
+    border-color: #fff;
+    background: #59acfc
+}
+
+.fd-nav .button-preview.ant-btn:active {
+    color: #fff;
+    border-color: #fff;
+    background: #2186ef
+}
+
+.fd-nav-content {
+    position: fixed;
+    top: 60px;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 1;
+    overflow-x: hidden;
+    overflow-y: auto;
+    padding-bottom: 30px
+}
+
+.error-modal-desc {
+    font-size: 13px;
+    color: rgba(25, 31, 37, .56);
+    line-height: 22px;
+    margin-bottom: 14px
+}
+
+.error-modal-list {
+    height: 200px;
+    overflow-y: auto;
+    margin-right: -25px;
+    padding-right: 25px
+}
+
+.error-modal-item {
+    padding: 10px 20px;
+    line-height: 21px;
+    background: #f6f6f6;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 8px;
+    border-radius: 4px
+}
+
+.error-modal-item-label {
+    flex: none;
+    font-size: 15px;
+    color: rgba(25, 31, 37, .56);
+    padding-right: 10px
+}
+
+.error-modal-item-content {
+    text-align: right;
+    flex: 1;
+    font-size: 13px;
+    color: #191f25
+}
+
+#body.blur {
+    -webkit-filter: blur(3px);
+    filter: blur(3px)
+}
+
+.zoom {
+    display: flex;
+    position: fixed;
+    -webkit-box-align: center;
+    -ms-flex-align: center;
+    align-items: center;
+    -webkit-box-pack: justify;
+    -ms-flex-pack: justify;
+    justify-content: space-between;
+    height: 40px;
+    width: 125px;
+    right: 40px;
+    margin-top: 30px;
+    z-index: 10
+}
+
+.zoom .zoom-in,
+.zoom .zoom-out {
+    width: 30px;
+    height: 30px;
+    background: #fff;
+    color: #c1c1cd;
+    cursor: pointer;
+    background-size: 100%;
+    background-repeat: no-repeat
+}
+
+.zoom .zoom-out {
+    background-image: url(https://gw.alicdn.com/tfs/TB1s0qhBHGYBuNjy0FoXXciBFXa-90-90.png)
+}
+
+.zoom .zoom-out.disabled {
+    opacity: .5
+}
+
+.zoom .zoom-in {
+    background-image: url(https://gw.alicdn.com/tfs/TB1UIgJBTtYBeNjy1XdXXXXyVXa-90-90.png)
+}
+
+.zoom .zoom-in.disabled {
+    opacity: .5
+}
+
+.auto-judge:hover .editable-title,
+.node-wrap-box:hover .editable-title {
+    border-bottom: 1px dashed #fff
+}
+
+.auto-judge:hover .editable-title.editing,
+.node-wrap-box:hover .editable-title.editing {
+    text-decoration: none;
+    border: 1px solid #d9d9d9
+}
+
+.auto-judge:hover .editable-title {
+    border-color: #15bc83
+}
+
+.editable-title {
+    line-height: 15px;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    border-bottom: 1px dashed transparent
+}
+
+.editable-title:before {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    right: 40px
+}
+
+.editable-title:hover {
+    border-bottom: 1px dashed #fff
+}
+
+.editable-title-input {
+    flex: none;
+    height: 18px;
+    padding-left: 4px;
+    text-indent: 0;
+    font-size: 12px;
+    line-height: 18px;
+    z-index: 1
+}
+
+.editable-title-input:hover {
+    text-decoration: none
+}
+
+.ant-btn {
+    position: relative
+}
+
+.node-wrap-box {
+    display: -webkit-inline-box;
+    display: -ms-inline-flexbox;
+    display: inline-flex;
+    -webkit-box-orient: vertical;
+    -webkit-box-direction: normal;
+    -ms-flex-direction: column;
+    flex-direction: column;
+    position: relative;
+    width: 220px;
+    min-height: 72px;
+    -ms-flex-negative: 0;
+    flex-shrink: 0;
+    background: #fff;
+    border-radius: 4px;
+    cursor: pointer
+}
+
+.node-wrap-box:after {
+    pointer-events: none;
+    content: "";
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    z-index: 2;
+    border-radius: 4px;
+    border: 1px solid transparent;
+    transition: all .1s cubic-bezier(.645, .045, .355, 1);
+    box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1)
+}
+
+.node-wrap-box.active:after,
+.node-wrap-box:active:after,
+.node-wrap-box:hover:after {
+    border: 1px solid #3296fa;
+    box-shadow: 0 0 6px 0 rgba(50, 150, 250, .3)
+}
+
+.node-wrap-box.active .close,
+.node-wrap-box:active .close,
+.node-wrap-box:hover .close {
+    display: block
+}
+
+.node-wrap-box.error:after {
+    border: 1px solid #f25643;
+    box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1)
+}
+
+.node-wrap-box .title {
+    position: relative;
+    display: flex;
+    align-items: center;
+    padding-left: 16px;
+    padding-right: 30px;
+    width: 100%;
+    height: 24px;
+    line-height: 24px;
+    font-size: 12px;
+    color: #fff;
+    text-align: left;
+    background: #576a95;
+    border-radius: 4px 4px 0 0
+}
+
+.node-wrap-box .title .iconfont {
+    font-size: 12px;
+    margin-right: 5px
+}
+
+.node-wrap-box .placeholder {
+    color: #bfbfbf
+}
+
+.node-wrap-box .close {
+    display: none;
+    position: absolute;
+    right: 10px;
+    top: 50%;
+    transform: translateY(-50%);
+    width: 20px;
+    height: 20px;
+    font-size: 14px;
+    color: #fff;
+    border-radius: 50%;
+    text-align: center;
+    line-height: 20px
+}
+
+.node-wrap-box .content {
+    position: relative;
+    font-size: 14px;
+    padding: 16px;
+    padding-right: 30px
+}
+
+.node-wrap-box .content .text {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: -webkit-box;
+    -webkit-line-clamp: 3;
+    -webkit-box-orient: vertical
+}
+
+.node-wrap-box .content .arrow {
+    position: absolute;
+    right: 10px;
+    top: 50%;
+    transform: translateY(-50%);
+    width: 20px;
+    height: 14px;
+    font-size: 14px;
+    color: #979797
+}
+
+.start-node.node-wrap-box .content .text {
+    display: block;
+    white-space: nowrap
+}
+
+.node-wrap-box:before {
+    content: "";
+    position: absolute;
+    top: -12px;
+    left: 50%;
+    -webkit-transform: translateX(-50%);
+    transform: translateX(-50%);
+    width: 0;
+    height: 4px;
+    border-style: solid;
+    border-width: 8px 6px 4px;
+    border-color: #cacaca transparent transparent;
+    background: #f5f5f7
+}
+
+.node-wrap-box.start-node:before {
+    content: none
+}
+
+.top-left-cover-line {
+    left: -1px
+}
+
+.top-left-cover-line,
+.top-right-cover-line {
+    position: absolute;
+    height: 8px;
+    width: 50%;
+    background-color: #f5f5f7;
+    top: -4px
+}
+
+.top-right-cover-line {
+    right: -1px
+}
+
+.bottom-left-cover-line {
+    left: -1px
+}
+
+.bottom-left-cover-line,
+.bottom-right-cover-line {
+    position: absolute;
+    height: 8px;
+    width: 50%;
+    background-color: #f5f5f7;
+    bottom: -4px
+}
+
+.bottom-right-cover-line {
+    right: -1px
+}
+
+.dingflow-design {
+    width: 100%;
+    background-color: #f5f5f7;
+    overflow: auto;
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    top: 0
+}
+
+.dingflow-design .box-scale {
+    transform: scale(1);
+    display: inline-block;
+    position: relative;
+    width: 100%;
+    padding: 54.5px 0;
+    -webkit-box-align: start;
+    -ms-flex-align: start;
+    align-items: flex-start;
+    -webkit-box-pack: center;
+    -ms-flex-pack: center;
+    justify-content: center;
+    -ms-flex-wrap: wrap;
+    flex-wrap: wrap;
+    min-width: -webkit-min-content;
+    min-width: -moz-min-content;
+    min-width: min-content;
+    background-color: #f5f5f7;
+    transform-origin: 50% 0px 0px;
+}
+
+.dingflow-design .node-wrap {
+    flex-direction: column;
+    -webkit-box-pack: start;
+    -ms-flex-pack: start;
+    justify-content: flex-start;
+    -webkit-box-align: center;
+    -ms-flex-align: center;
+    align-items: center;
+    -ms-flex-wrap: wrap;
+    flex-wrap: wrap;
+    -webkit-box-flex: 1;
+    -ms-flex-positive: 1;
+    padding: 0 50px;
+    position: relative
+}
+
+.dingflow-design .branch-wrap,
+.dingflow-design .node-wrap {
+    display: inline-flex;
+    width: 100%
+}
+
+.dingflow-design .branch-box-wrap {
+    display: flex;
+    -webkit-box-orient: vertical;
+    -webkit-box-direction: normal;
+    -ms-flex-direction: column;
+    flex-direction: column;
+    -ms-flex-wrap: wrap;
+    flex-wrap: wrap;
+    -webkit-box-align: center;
+    -ms-flex-align: center;
+    align-items: center;
+    min-height: 270px;
+    width: 100%;
+    -ms-flex-negative: 0;
+    flex-shrink: 0
+}
+
+.dingflow-design .branch-box {
+    display: flex;
+    overflow: visible;
+    min-height: 180px;
+    height: auto;
+    border-bottom: 2px solid #ccc;
+    border-top: 2px solid #ccc;
+    position: relative;
+    margin-top: 15px
+}
+
+.dingflow-design .branch-box .col-box {
+    background: #f5f5f7
+}
+
+.dingflow-design .branch-box .col-box:before {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 0;
+    margin: auto;
+    width: 2px;
+    height: 100%;
+    background-color: #cacaca
+}
+
+.dingflow-design .add-branch {
+    border: none;
+    outline: none;
+    user-select: none;
+    justify-content: center;
+    font-size: 12px;
+    padding: 0 10px;
+    height: 30px;
+    line-height: 30px;
+    border-radius: 15px;
+    color: #3296fa;
+    background: #fff;
+    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .1);
+    position: absolute;
+    top: -16px;
+    left: 50%;
+    transform: translateX(-50%);
+    transform-origin: center center;
+    cursor: pointer;
+    z-index: 1;
+    display: inline-flex;
+    align-items: center;
+    -webkit-transition: all .3s cubic-bezier(.645, .045, .355, 1);
+    transition: all .3s cubic-bezier(.645, .045, .355, 1)
+}
+
+.dingflow-design .add-branch:hover {
+    transform: translateX(-50%) scale(1.1);
+    box-shadow: 0 8px 16px 0 rgba(0, 0, 0, .1)
+}
+
+.dingflow-design .add-branch:active {
+    transform: translateX(-50%);
+    box-shadow: none
+}
+
+.dingflow-design .col-box {
+    display: inline-flex;
+    -webkit-box-orient: vertical;
+    -webkit-box-direction: normal;
+    flex-direction: column;
+    -webkit-box-align: center;
+    align-items: center;
+    position: relative
+}
+
+.dingflow-design .condition-node {
+    min-height: 220px
+}
+
+.dingflow-design .condition-node,
+.dingflow-design .condition-node-box {
+    display: inline-flex;
+    -webkit-box-orient: vertical;
+    -webkit-box-direction: normal;
+    flex-direction: column;
+    -webkit-box-flex: 1
+}
+
+.dingflow-design .condition-node-box {
+    padding-top: 30px;
+    padding-right: 50px;
+    padding-left: 50px;
+    -webkit-box-pack: center;
+    justify-content: center;
+    -webkit-box-align: center;
+    align-items: center;
+    flex-grow: 1;
+    position: relative
+}
+
+.dingflow-design .condition-node-box:before {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    margin: auto;
+    width: 2px;
+    height: 100%;
+    background-color: #cacaca
+}
+
+.dingflow-design .auto-judge {
+    position: relative;
+    width: 220px;
+    min-height: 72px;
+    background: #fff;
+    border-radius: 4px;
+    padding: 14px 19px;
+    cursor: pointer
+}
+
+.dingflow-design .auto-judge:after {
+    pointer-events: none;
+    content: "";
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    z-index: 2;
+    border-radius: 4px;
+    border: 1px solid transparent;
+    transition: all .1s cubic-bezier(.645, .045, .355, 1);
+    box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1)
+}
+
+.dingflow-design .auto-judge.active:after,
+.dingflow-design .auto-judge:active:after,
+.dingflow-design .auto-judge:hover:after {
+    border: 1px solid #3296fa;
+    box-shadow: 0 0 6px 0 rgba(50, 150, 250, .3)
+}
+
+.dingflow-design .auto-judge.active .close,
+.dingflow-design .auto-judge:active .close,
+.dingflow-design .auto-judge:hover .close {
+    display: block
+}
+
+.dingflow-design .auto-judge.error:after {
+    border: 1px solid #f25643;
+    box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1)
+}
+
+.dingflow-design .auto-judge .title-wrapper {
+    position: relative;
+    font-size: 12px;
+    color: #15bc83;
+    text-align: left;
+    line-height: 16px
+}
+
+.dingflow-design .auto-judge .title-wrapper .editable-title {
+    display: inline-block;
+    max-width: 120px;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis
+}
+
+.dingflow-design .auto-judge .title-wrapper .priority-title {
+    display: inline-block;
+    float: right;
+    margin-right: 10px;
+    color: rgba(25, 31, 37, .56)
+}
+
+.dingflow-design .auto-judge .placeholder {
+    color: #bfbfbf
+}
+
+.dingflow-design .auto-judge .close {
+    display: none;
+    position: absolute;
+    right: -10px;
+    top: -10px;
+    width: 20px;
+    height: 20px;
+    font-size: 14px;
+    color: rgba(0, 0, 0, .25);
+    border-radius: 50%;
+    text-align: center;
+    line-height: 20px;
+    z-index: 2
+}
+
+.dingflow-design .auto-judge .content {
+    font-size: 14px;
+    color: #191f25;
+    text-align: left;
+    margin-top: 6px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: -webkit-box;
+    -webkit-line-clamp: 3;
+    -webkit-box-orient: vertical
+}
+
+.dingflow-design .auto-judge .sort-left,
+.dingflow-design .auto-judge .sort-right {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    display: none;
+    z-index: 1
+}
+
+.dingflow-design .auto-judge .sort-left {
+    left: 0;
+    border-right: 1px solid #f6f6f6
+}
+
+.dingflow-design .auto-judge .sort-right {
+    right: 0;
+    border-left: 1px solid #f6f6f6
+}
+
+.dingflow-design .auto-judge:hover .sort-left,
+.dingflow-design .auto-judge:hover .sort-right {
+    display: flex;
+    align-items: center
+}
+
+.dingflow-design .auto-judge .sort-left:hover,
+.dingflow-design .auto-judge .sort-right:hover {
+    background: #efefef
+}
+
+.dingflow-design .end-node {
+    border-radius: 50%;
+    font-size: 14px;
+    color: rgba(25, 31, 37, .4);
+    text-align: left
+}
+
+.dingflow-design .end-node .end-node-circle {
+    width: 10px;
+    height: 10px;
+    margin: auto;
+    border-radius: 50%;
+    background: #dbdcdc
+}
+
+.dingflow-design .end-node .end-node-text {
+    margin-top: 5px;
+    text-align: center
+}
+
+.approval-setting {
+    border-radius: 2px;
+    margin: 20px 0;
+    position: relative;
+    background: #fff
+}
+
+.ant-btn {
+    position: relative
+}
+
+

+ 4 - 4
src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue

@@ -436,7 +436,7 @@ const initBpmnModeler = () => {
 
   // bpmnModeler.createDiagram()
 
-  console.log(bpmnModeler, 'bpmnModeler111111')
+  // console.log(bpmnModeler, 'bpmnModeler111111')
   emit('init-finished', bpmnModeler)
   initModelListeners()
 }
@@ -666,10 +666,10 @@ const previewProcessJson = () => {
 }
 /* ------------------------------------------------ 芋道源码 methods ------------------------------------------------------ */
 const processSave = async () => {
-  console.log(bpmnModeler, 'bpmnModelerbpmnModelerbpmnModelerbpmnModeler')
+  // console.log(bpmnModeler, 'bpmnModelerbpmnModelerbpmnModelerbpmnModeler')
   const { err, xml } = await bpmnModeler.saveXML()
-  console.log(err, 'errerrerrerrerr')
-  console.log(xml, 'xmlxmlxmlxmlxml')
+  // console.log(err, 'errerrerrerrerr')
+  // console.log(xml, 'xmlxmlxmlxmlxml')
   // 读取异常时抛出异常
   if (err) {
     // this.$modal.msgError('保存模型失败,请重试!')

+ 47 - 30
src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue

@@ -115,19 +115,19 @@ const highlightDiagram = async () => {
       if (!task) {
         return
       }
-      //进行中的任务已经高亮过了,则不高亮后面的任务了
+      // 进行中的任务已经高亮过了,则不高亮后面的任务了
       if (findProcessTask) {
         removeTaskDefinitionKeyList.push(n.id)
         return
       }
       // 高亮任务
-      canvas.addMarker(n.id, getResultCss(task.result))
+      canvas.addMarker(n.id, getResultCss(task.status))
       //标记是否高亮了进行中任务
-      if (task.result === 1) {
+      if (task.status === 1) {
         findProcessTask = true
       }
       // 如果非通过,就不走后面的线条了
-      if (task.result !== 2) {
+      if (task.status !== 2) {
         return
       }
       // 处理 outgoing 出线
@@ -194,6 +194,7 @@ const highlightDiagram = async () => {
       })
     } else if (n.$type === 'bpmn:StartEvent') {
       // 开始节点
+      canvas.addMarker(n.id, 'highlight')
       n.outgoing?.forEach((nn) => {
         // outgoing 例如说【bpmn:SequenceFlow】连线
         // 获得连线是否有指向目标。如果有,则进行高亮
@@ -205,10 +206,10 @@ const highlightDiagram = async () => {
       })
     } else if (n.$type === 'bpmn:EndEvent') {
       // 结束节点
-      if (!processInstance.value || processInstance.value.result === 1) {
+      if (!processInstance.value || processInstance.value.status === 1) {
         return
       }
-      canvas.addMarker(n.id, getResultCss(processInstance.value.result))
+      canvas.addMarker(n.id, getResultCss(processInstance.value.status))
     } else if (n.$type === 'bpmn:ServiceTask') {
       //服务任务
       if (activity.startTime > 0 && activity.endTime === 0) {
@@ -223,39 +224,49 @@ const highlightDiagram = async () => {
           canvas.addMarker(out.id, getResultCss(2))
         })
       }
+    } else if (n.$type === 'bpmn:SequenceFlow') {
+      let targetActivity = activityList.find((m: any) => m.key === n.targetRef.id)
+      if (targetActivity) {
+        canvas.addMarker(n.id, getActivityHighlightCss(targetActivity))
+      }
     }
   })
   if (!isEmpty(removeTaskDefinitionKeyList)) {
     taskList.value = taskList.value.filter(
-      (item) => !removeTaskDefinitionKeyList.includes(item.definitionKey)
+      (item) => !removeTaskDefinitionKeyList.includes(item.taskDefinitionKey)
     )
   }
 }
+
 const getActivityHighlightCss = (activity) => {
   return activity.endTime ? 'highlight' : 'highlight-todo'
 }
-const getResultCss = (result) => {
-  if (result === 1) {
+
+const getResultCss = (status) => {
+  if (status === 1) {
     // 审批中
     return 'highlight-todo'
-  } else if (result === 2) {
+  } else if (status === 2) {
     // 已通过
     return 'highlight'
-  } else if (result === 3) {
+  } else if (status === 3) {
     // 不通过
     return 'highlight-reject'
-  } else if (result === 4) {
+  } else if (status === 4) {
     // 已取消
     return 'highlight-cancel'
-  } else if (result === 5) {
+  } else if (status === 5) {
     // 退回
     return 'highlight-return'
-  } else if (result === 6) {
+  } else if (status === 6) {
     // 委派
-    return 'highlight-return'
-  } else if (result === 7 || result === 8 || result === 9) {
-    // 待后加签任务完成/待前加签任务完成/待前置任务完成
-    return 'highlight-return'
+    return 'highlight-todo'
+  } else if (status === 7) {
+    // 审批通过中
+    return 'highlight-todo'
+  } else if (status === 0) {
+    // 待审批
+    return 'highlight-todo'
   }
   return ''
 }
@@ -296,10 +307,10 @@ const elementHover = (element) => {
   !elementOverlayIds.value && (elementOverlayIds.value = {})
   !overlays.value && (overlays.value = bpmnModeler.get('overlays'))
   // 展示信息
-  console.log(activityLists.value, 'activityLists.value')
-  console.log(element.value, 'element.value')
+  // console.log(activityLists.value, 'activityLists.value')
+  // console.log(element.value, 'element.value')
   const activity = activityLists.value.find((m) => m.key === element.value.id)
-  console.log(activity, 'activityactivityactivityactivity')
+  // console.log(activity, 'activityactivityactivityactivity')
   if (!activity) {
     return
   }
@@ -313,15 +324,14 @@ const elementHover = (element) => {
                   <p>部门:${processInstance.value.startUser.deptName}</p>
                   <p>创建时间:${formatDate(processInstance.value.createTime)}`
     } else if (element.value.type === 'bpmn:UserTask') {
-      // debugger
       let task = taskList.value.find((m) => m.id === activity.taskId) // 找到活动对应的 taskId
       if (!task) {
         return
       }
-      let optionData = getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT)
+      let optionData = getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS)
       let dataResult = ''
       optionData.forEach((element) => {
-        if (element.value == task.result) {
+        if (element.value == task.status) {
           dataResult = element.label
         }
       })
@@ -333,7 +343,7 @@ const elementHover = (element) => {
       //             <p>部门:${task.assigneeUser.deptName}</p>
       //             <p>结果:${getIntDictOptions(
       //               DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT,
-      //               task.result
+      //               task.status
       //             )}</p>
       //             <p>创建时间:${formatDate(task.createTime)}</p>`
       if (task.endTime) {
@@ -351,29 +361,30 @@ const elementHover = (element) => {
       }
       console.log(html)
     } else if (element.value.type === 'bpmn:EndEvent' && processInstance.value) {
-      let optionData = getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT)
+      let optionData = getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS)
       let dataResult = ''
       optionData.forEach((element) => {
-        if (element.value == processInstance.value.result) {
+        if (element.value == processInstance.value.status) {
           dataResult = element.label
         }
       })
       html = `<p>结果:${dataResult}</p>`
       // html = `<p>结果:${getIntDictOptions(
       //   DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT,
-      //   processInstance.value.result
+      //   processInstance.value.status
       // )}</p>`
       if (processInstance.value.endTime) {
         html += `<p>结束时间:${formatDate(processInstance.value.endTime)}</p>`
       }
     }
-    console.log(html, 'html111111111111111')
+    // console.log(html, 'html111111111111111')
     elementOverlayIds.value[element.value.id] = toRaw(overlays.value)?.add(element.value, {
       position: { left: 0, bottom: 0 },
       html: `<div class="element-overlays">${html}</div>`
     })
   }
 }
+
 // 流程图的元素被 out
 const elementOut = (element) => {
   toRaw(overlays.value).remove({ element })
@@ -389,6 +400,7 @@ onMounted(() => {
   // 初始模型的监听器
   initModelListeners()
 })
+
 onBeforeUnmount(() => {
   // this.$once('hook:beforeDestroy', () => {
   // })
@@ -427,7 +439,7 @@ watch(
 )
 </script>
 
-<style>
+<style lang="scss">
 /** 处理中 */
 .highlight-todo.djs-connection > .djs-visual > path {
   stroke: #1890ff !important;
@@ -501,6 +513,10 @@ watch(
   stroke: green !important;
 }
 
+.djs-element.highlight > .djs-visual > path {
+  stroke: green !important;
+}
+
 /** 不通过 */
 .highlight-reject.djs-shape .djs-visual > :nth-child(1) {
   fill: red !important;
@@ -520,6 +536,7 @@ watch(
 
 .highlight-reject.djs-connection > .djs-visual > path {
   stroke: red !important;
+  marker-end: url(#sequenceflow-end-white-success) !important;
 }
 
 .highlight-reject:not(.djs-connection) .djs-visual > :nth-child(1) {

+ 10 - 0
src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json

@@ -332,6 +332,16 @@
           "name": "multiinstance_condition",
           "isAttr": true,
           "type": "String"
+        },
+        {
+          "name": "candidateStrategy",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "candidateParam",
+          "isAttr": true,
+          "type": "String"
         }
       ]
     },

+ 10 - 0
src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/camundaDescriptor.json

@@ -319,6 +319,16 @@
           "name": "priority",
           "isAttr": true,
           "type": "String"
+        },
+        {
+          "name": "candidateStrategy",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "candidateParam",
+          "isAttr": true,
+          "type": "String"
         }
       ]
     },

+ 10 - 0
src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json

@@ -319,6 +319,16 @@
           "name": "priority",
           "isAttr": true,
           "type": "String"
+        },
+        {
+          "name": "candidateStrategy",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "candidateParam",
+          "isAttr": true,
+          "type": "String"
         }
       ]
     },

+ 3 - 8
src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue

@@ -24,15 +24,10 @@
       </el-collapse-item>
       <el-collapse-item name="condition" v-if="formVisible" key="form">
         <template #title><Icon icon="ep:list" />表单</template>
-        <!-- <element-form :id="elementId" :type="elementType" /> -->
-        友情提示:使用
-        <router-link :to="{ path: '/bpm/manager/form' }"
-          ><el-link type="danger">流程表单</el-link>
-        </router-link>
-        替代,提供更好的表单设计功能
+        <element-form :id="elementId" :type="elementType" />
       </el-collapse-item>
       <el-collapse-item name="task" v-if="elementType.indexOf('Task') !== -1" key="task">
-        <template #title><Icon icon="ep:checked" />任务</template>
+        <template #title><Icon icon="ep:checked" />任务(审批人)</template>
         <element-task :id="elementId" :type="elementType" />
       </el-collapse-item>
       <el-collapse-item
@@ -40,7 +35,7 @@
         v-if="elementType.indexOf('Task') !== -1"
         key="multiInstance"
       >
-        <template #title><Icon icon="ep:help-filled" />多实例</template>
+        <template #title><Icon icon="ep:help-filled" />多实例(会签配置)</template>
         <element-multi-instance :business-object="elementBusinessObject" :type="elementType" />
       </el-collapse-item>
       <el-collapse-item name="listeners" key="listeners">

+ 20 - 9
src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue

@@ -68,13 +68,13 @@ const resetBaseInfo = () => {
   console.log(bpmnElement.value, 'bpmnElement')
 
   bpmnElement.value = bpmnInstances()?.bpmnElement
-  console.log(bpmnElement.value, 'resetBaseInfo11111111111')
+  // console.log(bpmnElement.value, 'resetBaseInfo11111111111')
   elementBaseInfo.value = bpmnElement.value.businessObject
   needProps.value['type'] = bpmnElement.value.businessObject.$type
   // elementBaseInfo.value['typess'] = bpmnElement.value.businessObject.$type
 
   // elementBaseInfo.value = JSON.parse(JSON.stringify(bpmnElement.value.businessObject))
-  console.log(elementBaseInfo.value, 'elementBaseInfo22222222222')
+  // console.log(elementBaseInfo.value, 'elementBaseInfo22222222222')
 }
 const handleKeyUpdate = (value) => {
   // 校验 value 的值,只有 XML NCName 通过的情况下,才进行赋值。否则,会导致流程图报错,无法绘制的问题
@@ -121,11 +121,11 @@ const updateBaseInfo = (key) => {
   //   id: elementBaseInfo.value[key]
   //   // di: { id: `${elementBaseInfo.value[key]}_di` }
   // }
-  console.log(elementBaseInfo, 'elementBaseInfo11111111111')
+  // console.log(elementBaseInfo, 'elementBaseInfo11111111111')
   needProps.value = { ...elementBaseInfo.value, ...needProps.value }
 
   if (key === 'id') {
-    console.log('jinru')
+    // console.log('jinru')
     console.log(window, 'window')
     console.log(bpmnElement.value, 'bpmnElement')
     console.log(toRaw(bpmnElement.value), 'bpmnElement')
@@ -138,20 +138,19 @@ const updateBaseInfo = (key) => {
     bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), attrObj)
   }
 }
+
 onMounted(() => {
-  // 针对上传的 bpmn 流程图时,需要延迟 1 秒的时间,保证 key 和 name 的更新
+  // 针对上传的 bpmn 流程图时,需要延迟 1 秒的时间,保证 key 和 name 的更新
   setTimeout(() => {
-    console.log(props.model, 'props.model')
     handleKeyUpdate(props.model.key)
     handleNameUpdate(props.model.name)
-    console.log(props, 'propsssssssssssssssssssss')
-  }, 1000)
+  }, 110)
 })
 
 watch(
   () => props.businessObject,
   (val) => {
-    console.log(val, 'val11111111111111111111')
+    // console.log(val, 'val11111111111111111111')
     if (val) {
       // nextTick(() => {
       resetBaseInfo()
@@ -159,6 +158,18 @@ watch(
     }
   }
 )
+
+watch(
+  () => props.model?.key,
+  (val) => {
+    // 针对上传的 bpmn 流程图时,保证 key 和 name 的更新
+    if (val) {
+      handleKeyUpdate(props.model.key)
+      handleNameUpdate(props.model.name)
+    }
+  }
+)
+
 // watch(
 //   () => ({ ...props }),
 //   (oldVal, newVal) => {

+ 220 - 207
src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue

@@ -1,228 +1,233 @@
 <template>
   <div class="panel-tab__content">
     <el-form label-width="80px">
-      <el-form-item label="表单标识">
-        <el-input v-model="formKey" clearable @change="updateElementFormKey" />
-      </el-form-item>
-      <el-form-item label="业务标识">
-        <el-select v-model="businessKey" @change="updateElementBusinessKey">
-          <el-option v-for="i in fieldList" :key="i.id" :value="i.id" :label="i.label" />
-          <el-option label="无" value="" />
+      <el-form-item label="流程表单">
+        <!--        <el-input v-model="formKey" clearable @change="updateElementFormKey" />-->
+        <el-select v-model="formKey" clearable @change="updateElementFormKey">
+          <el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" />
         </el-select>
       </el-form-item>
+      <!--      <el-form-item label="业务标识">-->
+      <!--        <el-select v-model="businessKey" @change="updateElementBusinessKey">-->
+      <!--          <el-option v-for="i in fieldList" :key="i.id" :value="i.id" :label="i.label" />-->
+      <!--          <el-option label="无" value="" />-->
+      <!--        </el-select>-->
+      <!--      </el-form-item>-->
     </el-form>
 
     <!--字段列表-->
-    <div class="element-property list-property">
-      <el-divider><Icon icon="ep:coin" /> 表单字段</el-divider>
-      <el-table :data="fieldList" max-height="240" fit border>
-        <el-table-column label="序号" type="index" width="50px" />
-        <el-table-column label="字段名称" prop="label" min-width="80px" show-overflow-tooltip />
-        <el-table-column
-          label="字段类型"
-          prop="type"
-          min-width="80px"
-          :formatter="(row) => fieldType[row.type] || row.type"
-          show-overflow-tooltip
-        />
-        <el-table-column
-          label="默认值"
-          prop="defaultValue"
-          min-width="80px"
-          show-overflow-tooltip
-        />
-        <el-table-column label="操作" width="90px">
-          <template #default="scope">
-            <el-button type="primary" link @click="openFieldForm(scope, scope.$index)"
-              >编辑</el-button
-            >
-            <el-divider direction="vertical" />
-            <el-button
-              type="primary"
-              link
-              style="color: #ff4d4f"
-              @click="removeField(scope, scope.$index)"
-              >移除</el-button
-            >
-          </template>
-        </el-table-column>
-      </el-table>
-    </div>
-    <div class="element-drawer__button">
-      <XButton type="primary" proIcon="ep:plus" title="添加字段" @click="openFieldForm(null, -1)" />
-    </div>
+    <!--    <div class="element-property list-property">-->
+    <!--      <el-divider><Icon icon="ep:coin" /> 表单字段</el-divider>-->
+    <!--      <el-table :data="fieldList" max-height="240" fit border>-->
+    <!--        <el-table-column label="序号" type="index" width="50px" />-->
+    <!--        <el-table-column label="字段名称" prop="label" min-width="80px" show-overflow-tooltip />-->
+    <!--        <el-table-column-->
+    <!--          label="字段类型"-->
+    <!--          prop="type"-->
+    <!--          min-width="80px"-->
+    <!--          :formatter="(row) => fieldType[row.type] || row.type"-->
+    <!--          show-overflow-tooltip-->
+    <!--        />-->
+    <!--        <el-table-column-->
+    <!--          label="默认值"-->
+    <!--          prop="defaultValue"-->
+    <!--          min-width="80px"-->
+    <!--          show-overflow-tooltip-->
+    <!--        />-->
+    <!--        <el-table-column label="操作" width="90px">-->
+    <!--          <template #default="scope">-->
+    <!--            <el-button type="primary" link @click="openFieldForm(scope, scope.$index)"-->
+    <!--              >编辑</el-button-->
+    <!--            >-->
+    <!--            <el-divider direction="vertical" />-->
+    <!--            <el-button-->
+    <!--              type="primary"-->
+    <!--              link-->
+    <!--              style="color: #ff4d4f"-->
+    <!--              @click="removeField(scope, scope.$index)"-->
+    <!--              >移除</el-button-->
+    <!--            >-->
+    <!--          </template>-->
+    <!--        </el-table-column>-->
+    <!--      </el-table>-->
+    <!--    </div>-->
+    <!--    <div class="element-drawer__button">-->
+    <!--      <XButton type="primary" proIcon="ep:plus" title="添加字段" @click="openFieldForm(null, -1)" />-->
+    <!--    </div>-->
 
     <!--字段配置侧边栏-->
-    <el-drawer
-      v-model="fieldModelVisible"
-      title="字段配置"
-      :size="`${width}px`"
-      append-to-body
-      destroy-on-close
-    >
-      <el-form :model="formFieldForm" label-width="90px">
-        <el-form-item label="字段ID">
-          <el-input v-model="formFieldForm.id" clearable />
-        </el-form-item>
-        <el-form-item label="类型">
-          <el-select
-            v-model="formFieldForm.typeType"
-            placeholder="请选择字段类型"
-            clearable
-            @change="changeFieldTypeType"
-          >
-            <el-option v-for="(value, key) of fieldType" :label="value" :value="key" :key="key" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="类型名称" v-if="formFieldForm.typeType === 'custom'">
-          <el-input v-model="formFieldForm.type" clearable />
-        </el-form-item>
-        <el-form-item label="名称">
-          <el-input v-model="formFieldForm.label" clearable />
-        </el-form-item>
-        <el-form-item label="时间格式" v-if="formFieldForm.typeType === 'date'">
-          <el-input v-model="formFieldForm.datePattern" clearable />
-        </el-form-item>
-        <el-form-item label="默认值">
-          <el-input v-model="formFieldForm.defaultValue" clearable />
-        </el-form-item>
-      </el-form>
+    <!--    <el-drawer-->
+    <!--      v-model="fieldModelVisible"-->
+    <!--      title="字段配置"-->
+    <!--      :size="`${width}px`"-->
+    <!--      append-to-body-->
+    <!--      destroy-on-close-->
+    <!--    >-->
+    <!--      <el-form :model="formFieldForm" label-width="90px">-->
+    <!--        <el-form-item label="字段ID">-->
+    <!--          <el-input v-model="formFieldForm.id" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="类型">-->
+    <!--          <el-select-->
+    <!--            v-model="formFieldForm.typeType"-->
+    <!--            placeholder="请选择字段类型"-->
+    <!--            clearable-->
+    <!--            @change="changeFieldTypeType"-->
+    <!--          >-->
+    <!--            <el-option v-for="(value, key) of fieldType" :label="value" :value="key" :key="key" />-->
+    <!--          </el-select>-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="类型名称" v-if="formFieldForm.typeType === 'custom'">-->
+    <!--          <el-input v-model="formFieldForm.type" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="名称">-->
+    <!--          <el-input v-model="formFieldForm.label" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="时间格式" v-if="formFieldForm.typeType === 'date'">-->
+    <!--          <el-input v-model="formFieldForm.datePattern" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="默认值">-->
+    <!--          <el-input v-model="formFieldForm.defaultValue" clearable />-->
+    <!--        </el-form-item>-->
+    <!--      </el-form>-->
 
-      <!-- 枚举值设置 -->
-      <template v-if="formFieldForm.type === 'enum'">
-        <el-divider key="enum-divider" />
-        <p class="listener-filed__title" key="enum-title">
-          <span><Icon icon="ep:menu" />枚举值列表:</span>
-          <el-button type="primary" @click="openFieldOptionForm(null, -1, 'enum')"
-            >添加枚举值</el-button
-          >
-        </p>
-        <el-table :data="fieldEnumList" key="enum-table" max-height="240" fit border>
-          <el-table-column label="序号" width="50px" type="index" />
-          <el-table-column label="枚举值编号" prop="id" min-width="100px" show-overflow-tooltip />
-          <el-table-column label="枚举值名称" prop="name" min-width="100px" show-overflow-tooltip />
-          <el-table-column label="操作" width="90px">
-            <template #default="scope">
-              <el-button
-                type="primary"
-                link
-                @click="openFieldOptionForm(scope, scope.$index, 'enum')"
-                >编辑</el-button
-              >
-              <el-divider direction="vertical" />
-              <el-button
-                type="primary"
-                link
-                style="color: #ff4d4f"
-                @click="removeFieldOptionItem(scope, scope.$index, 'enum')"
-                >移除</el-button
-              >
-            </template>
-          </el-table-column>
-        </el-table>
-      </template>
+    <!--      &lt;!&ndash; 枚举值设置 &ndash;&gt;-->
+    <!--      <template v-if="formFieldForm.type === 'enum'">-->
+    <!--        <el-divider key="enum-divider" />-->
+    <!--        <p class="listener-filed__title" key="enum-title">-->
+    <!--          <span><Icon icon="ep:menu" />枚举值列表:</span>-->
+    <!--          <el-button type="primary" @click="openFieldOptionForm(null, -1, 'enum')"-->
+    <!--            >添加枚举值</el-button-->
+    <!--          >-->
+    <!--        </p>-->
+    <!--        <el-table :data="fieldEnumList" key="enum-table" max-height="240" fit border>-->
+    <!--          <el-table-column label="序号" width="50px" type="index" />-->
+    <!--          <el-table-column label="枚举值编号" prop="id" min-width="100px" show-overflow-tooltip />-->
+    <!--          <el-table-column label="枚举值名称" prop="name" min-width="100px" show-overflow-tooltip />-->
+    <!--          <el-table-column label="操作" width="90px">-->
+    <!--            <template #default="scope">-->
+    <!--              <el-button-->
+    <!--                type="primary"-->
+    <!--                link-->
+    <!--                @click="openFieldOptionForm(scope, scope.$index, 'enum')"-->
+    <!--                >编辑</el-button-->
+    <!--              >-->
+    <!--              <el-divider direction="vertical" />-->
+    <!--              <el-button-->
+    <!--                type="primary"-->
+    <!--                link-->
+    <!--                style="color: #ff4d4f"-->
+    <!--                @click="removeFieldOptionItem(scope, scope.$index, 'enum')"-->
+    <!--                >移除</el-button-->
+    <!--              >-->
+    <!--            </template>-->
+    <!--          </el-table-column>-->
+    <!--        </el-table>-->
+    <!--      </template>-->
 
-      <!-- 校验规则 -->
-      <el-divider key="validation-divider" />
-      <p class="listener-filed__title" key="validation-title">
-        <span><Icon icon="ep:menu" />约束条件列表:</span>
-        <el-button type="primary" @click="openFieldOptionForm(null, -1, 'constraint')"
-          >添加约束</el-button
-        >
-      </p>
-      <el-table :data="fieldConstraintsList" key="validation-table" max-height="240" fit border>
-        <el-table-column label="序号" width="50px" type="index" />
-        <el-table-column label="约束名称" prop="name" min-width="100px" show-overflow-tooltip />
-        <el-table-column label="约束配置" prop="config" min-width="100px" show-overflow-tooltip />
-        <el-table-column label="操作" width="90px">
-          <template #default="scope">
-            <el-button
-              type="primary"
-              link
-              @click="openFieldOptionForm(scope, scope.$index, 'constraint')"
-              >编辑</el-button
-            >
-            <el-divider direction="vertical" />
-            <el-button
-              type="primary"
-              link
-              style="color: #ff4d4f"
-              @click="removeFieldOptionItem(scope, scope.$index, 'constraint')"
-              >移除</el-button
-            >
-          </template>
-        </el-table-column>
-      </el-table>
+    <!--      &lt;!&ndash; 校验规则 &ndash;&gt;-->
+    <!--      <el-divider key="validation-divider" />-->
+    <!--      <p class="listener-filed__title" key="validation-title">-->
+    <!--        <span><Icon icon="ep:menu" />约束条件列表:</span>-->
+    <!--        <el-button type="primary" @click="openFieldOptionForm(null, -1, 'constraint')"-->
+    <!--          >添加约束</el-button-->
+    <!--        >-->
+    <!--      </p>-->
+    <!--      <el-table :data="fieldConstraintsList" key="validation-table" max-height="240" fit border>-->
+    <!--        <el-table-column label="序号" width="50px" type="index" />-->
+    <!--        <el-table-column label="约束名称" prop="name" min-width="100px" show-overflow-tooltip />-->
+    <!--        <el-table-column label="约束配置" prop="config" min-width="100px" show-overflow-tooltip />-->
+    <!--        <el-table-column label="操作" width="90px">-->
+    <!--          <template #default="scope">-->
+    <!--            <el-button-->
+    <!--              type="primary"-->
+    <!--              link-->
+    <!--              @click="openFieldOptionForm(scope, scope.$index, 'constraint')"-->
+    <!--              >编辑</el-button-->
+    <!--            >-->
+    <!--            <el-divider direction="vertical" />-->
+    <!--            <el-button-->
+    <!--              type="primary"-->
+    <!--              link-->
+    <!--              style="color: #ff4d4f"-->
+    <!--              @click="removeFieldOptionItem(scope, scope.$index, 'constraint')"-->
+    <!--              >移除</el-button-->
+    <!--            >-->
+    <!--          </template>-->
+    <!--        </el-table-column>-->
+    <!--      </el-table>-->
 
-      <!-- 表单属性 -->
-      <el-divider key="property-divider" />
-      <p class="listener-filed__title" key="property-title">
-        <span><Icon icon="ep:menu" />字段属性列表:</span>
-        <el-button type="primary" @click="openFieldOptionForm(null, -1, 'property')"
-          >添加属性</el-button
-        >
-      </p>
-      <el-table :data="fieldPropertiesList" key="property-table" max-height="240" fit border>
-        <el-table-column label="序号" width="50px" type="index" />
-        <el-table-column label="属性编号" prop="id" min-width="100px" show-overflow-tooltip />
-        <el-table-column label="属性值" prop="value" min-width="100px" show-overflow-tooltip />
-        <el-table-column label="操作" width="90px">
-          <template #default="scope">
-            <el-button
-              type="primary"
-              link
-              @click="openFieldOptionForm(scope, scope.$index, 'property')"
-              >编辑</el-button
-            >
-            <el-divider direction="vertical" />
-            <el-button
-              type="primary"
-              link
-              style="color: #ff4d4f"
-              @click="removeFieldOptionItem(scope, scope.$index, 'property')"
-              >移除</el-button
-            >
-          </template>
-        </el-table-column>
-      </el-table>
+    <!--      &lt;!&ndash; 表单属性 &ndash;&gt;-->
+    <!--      <el-divider key="property-divider" />-->
+    <!--      <p class="listener-filed__title" key="property-title">-->
+    <!--        <span><Icon icon="ep:menu" />字段属性列表:</span>-->
+    <!--        <el-button type="primary" @click="openFieldOptionForm(null, -1, 'property')"-->
+    <!--          >添加属性</el-button-->
+    <!--        >-->
+    <!--      </p>-->
+    <!--      <el-table :data="fieldPropertiesList" key="property-table" max-height="240" fit border>-->
+    <!--        <el-table-column label="序号" width="50px" type="index" />-->
+    <!--        <el-table-column label="属性编号" prop="id" min-width="100px" show-overflow-tooltip />-->
+    <!--        <el-table-column label="属性值" prop="value" min-width="100px" show-overflow-tooltip />-->
+    <!--        <el-table-column label="操作" width="90px">-->
+    <!--          <template #default="scope">-->
+    <!--            <el-button-->
+    <!--              type="primary"-->
+    <!--              link-->
+    <!--              @click="openFieldOptionForm(scope, scope.$index, 'property')"-->
+    <!--              >编辑</el-button-->
+    <!--            >-->
+    <!--            <el-divider direction="vertical" />-->
+    <!--            <el-button-->
+    <!--              type="primary"-->
+    <!--              link-->
+    <!--              style="color: #ff4d4f"-->
+    <!--              @click="removeFieldOptionItem(scope, scope.$index, 'property')"-->
+    <!--              >移除</el-button-->
+    <!--            >-->
+    <!--          </template>-->
+    <!--        </el-table-column>-->
+    <!--      </el-table>-->
 
-      <!-- 底部按钮 -->
-      <div class="element-drawer__button">
-        <el-button>取 消</el-button>
-        <el-button type="primary" @click="saveField">保 存</el-button>
-      </div>
-    </el-drawer>
+    <!--      &lt;!&ndash; 底部按钮 &ndash;&gt;-->
+    <!--      <div class="element-drawer__button">-->
+    <!--        <el-button>取 消</el-button>-->
+    <!--        <el-button type="primary" @click="saveField">保 存</el-button>-->
+    <!--      </div>-->
+    <!--    </el-drawer>-->
 
-    <el-dialog
-      v-model="fieldOptionModelVisible"
-      :title="optionModelTitle"
-      width="600px"
-      append-to-body
-      destroy-on-close
-    >
-      <el-form :model="fieldOptionForm" label-width="96px">
-        <el-form-item label="编号/ID" v-if="fieldOptionType !== 'constraint'" key="option-id">
-          <el-input v-model="fieldOptionForm.id" clearable />
-        </el-form-item>
-        <el-form-item label="名称" v-if="fieldOptionType !== 'property'" key="option-name">
-          <el-input v-model="fieldOptionForm.name" clearable />
-        </el-form-item>
-        <el-form-item label="配置" v-if="fieldOptionType === 'constraint'" key="option-config">
-          <el-input v-model="fieldOptionForm.config" clearable />
-        </el-form-item>
-        <el-form-item label="值" v-if="fieldOptionType === 'property'" key="option-value">
-          <el-input v-model="fieldOptionForm.value" clearable />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="fieldOptionModelVisible = false">取 消</el-button>
-        <el-button type="primary" @click="saveFieldOption">确 定</el-button>
-      </template>
-    </el-dialog>
+    <!--    <el-dialog-->
+    <!--      v-model="fieldOptionModelVisible"-->
+    <!--      :title="optionModelTitle"-->
+    <!--      width="600px"-->
+    <!--      append-to-body-->
+    <!--      destroy-on-close-->
+    <!--    >-->
+    <!--      <el-form :model="fieldOptionForm" label-width="96px">-->
+    <!--        <el-form-item label="编号/ID" v-if="fieldOptionType !== 'constraint'" key="option-id">-->
+    <!--          <el-input v-model="fieldOptionForm.id" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="名称" v-if="fieldOptionType !== 'property'" key="option-name">-->
+    <!--          <el-input v-model="fieldOptionForm.name" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="配置" v-if="fieldOptionType === 'constraint'" key="option-config">-->
+    <!--          <el-input v-model="fieldOptionForm.config" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="值" v-if="fieldOptionType === 'property'" key="option-value">-->
+    <!--          <el-input v-model="fieldOptionForm.value" clearable />-->
+    <!--        </el-form-item>-->
+    <!--      </el-form>-->
+    <!--      <template #footer>-->
+    <!--        <el-button @click="fieldOptionModelVisible = false">取 消</el-button>-->
+    <!--        <el-button type="primary" @click="saveFieldOption">确 定</el-button>-->
+    <!--      </template>-->
+    <!--    </el-dialog>-->
   </div>
 </template>
 
 <script lang="ts" setup>
+import * as FormApi from '@/api/bpm/form'
+
 defineOptions({ name: 'ElementForm' })
 
 const props = defineProps({
@@ -263,6 +268,9 @@ const bpmnInstances = () => (window as any)?.bpmnInstances
 const resetFormList = () => {
   bpmnELement.value = bpmnInstances().bpmnElement
   formKey.value = bpmnELement.value.businessObject.formKey
+  if (formKey.value?.length > 0) {
+    formKey.value = parseInt(formKey.value)
+  }
   // 获取元素扩展属性 或者 创建扩展属性
   elExtensionElements.value =
     bpmnELement.value.businessObject.get('extensionElements') ||
@@ -421,7 +429,7 @@ const saveField = () => {
 
 // 移除某个 字段的 配置项
 const removeFieldOptionItem = (option, index, type) => {
-  console.log(option, 'option')
+  // console.log(option, 'option')
   if (type === 'property') {
     fieldPropertiesList.value.splice(index, 1)
     return
@@ -451,6 +459,11 @@ const updateElementExtensions = () => {
   })
 }
 
+const formList = ref([]) // 流程表单的下拉框的数据
+onMounted(async () => {
+  formList.value = await FormApi.getFormSimpleList()
+})
+
 watch(
   () => props.id,
   (val) => {

+ 46 - 1
src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue

@@ -26,8 +26,16 @@
         type="primary"
         preIcon="ep:plus"
         title="添加监听器"
+        size="small"
         @click="openListenerForm(null)"
       />
+      <XButton
+        type="success"
+        preIcon="ep:select"
+        title="选择监听器"
+        size="small"
+        @click="openProcessListenerDialog"
+      />
     </div>
 
     <!-- 监听器 编辑/创建 部分 -->
@@ -240,11 +248,21 @@
       </template>
     </el-dialog>
   </div>
+
+  <!-- 选择弹窗 -->
+  <ProcessListenerDialog ref="processListenerDialogRef" @select="selectProcessListener" />
 </template>
 <script lang="ts" setup>
 import { ElMessageBox } from 'element-plus'
 import { createListenerObject, updateElementExtensions } from '../../utils'
-import { initListenerType, initListenerForm, listenerType, fieldType } from './utilSelf'
+import {
+  initListenerType,
+  initListenerForm,
+  listenerType,
+  fieldType,
+  initListenerForm2
+} from './utilSelf'
+import ProcessListenerDialog from './ProcessListenerDialog.vue'
 
 defineOptions({ name: 'ElementListeners' })
 
@@ -284,6 +302,7 @@ const resetListenersList = () => {
 }
 // 打开 监听器详情 侧边栏
 const openListenerForm = (listener, index?) => {
+  // debugger
   if (listener) {
     listenerForm.value = initListenerForm(listener)
     editingListenerIndex.value = index
@@ -321,6 +340,7 @@ const openListenerFieldForm = (field, index?) => {
 }
 // 保存监听器注入字段
 const saveListenerFiled = async () => {
+  // debugger
   let validateStatus = await listenerFieldFormRef.value.validate()
   if (!validateStatus) return // 验证不通过直接返回
   if (editingListenerFieldIndex.value === -1) {
@@ -337,6 +357,7 @@ const saveListenerFiled = async () => {
 }
 // 移除监听器字段
 const removeListenerField = (index) => {
+  // debugger
   ElMessageBox.confirm('确认移除该字段吗?', '提示', {
     confirmButtonText: '确 认',
     cancelButtonText: '取 消'
@@ -349,6 +370,7 @@ const removeListenerField = (index) => {
 }
 // 移除监听器
 const removeListener = (index) => {
+  debugger
   ElMessageBox.confirm('确认移除该监听器吗?', '提示', {
     confirmButtonText: '确 认',
     cancelButtonText: '取 消'
@@ -365,6 +387,7 @@ const removeListener = (index) => {
 }
 // 保存监听器配置
 const saveListenerConfig = async () => {
+  // debugger
   let validateStatus = await listenerFormRef.value.validate()
   if (!validateStatus) return // 验证不通过直接返回
   const listenerObject = createListenerObject(listenerForm.value, false, prefix)
@@ -389,6 +412,28 @@ const saveListenerConfig = async () => {
   listenerForm.value = {}
 }
 
+// 打开监听器弹窗
+const processListenerDialogRef = ref()
+const openProcessListenerDialog = async () => {
+  processListenerDialogRef.value.open('execution')
+}
+const selectProcessListener = (listener) => {
+  const listenerForm = initListenerForm2(listener)
+  const listenerObject = createListenerObject(listenerForm, false, prefix)
+  bpmnElementListeners.value.push(listenerObject)
+  elementListenersList.value.push(listenerForm)
+
+  // 保存其他配置
+  otherExtensionList.value =
+    bpmnElement.value.businessObject?.extensionElements?.values?.filter(
+      (ex) => ex.$type !== `${prefix}:ExecutionListener`
+    ) ?? []
+  updateElementExtensions(
+    bpmnElement.value,
+    otherExtensionList.value.concat(bpmnElementListeners.value)
+  )
+}
+
 watch(
   () => props.id,
   (val) => {

+ 83 - 0
src/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue

@@ -0,0 +1,83 @@
+<!-- 执行器选择 -->
+<template>
+  <Dialog title="请选择监听器" v-model="dialogVisible" width="1024px">
+    <ContentWrap>
+      <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+        <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="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">
+          <template #default="scope">
+            <el-button link type="primary" @click="select(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>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { ProcessListenerApi, ProcessListenerVO } from '@/api/bpm/processListener'
+import { DICT_TYPE } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** BPM 流程 表单 */
+defineOptions({ name: 'ProcessListenerDialog' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const loading = ref(true) // 列表的加载中
+const list = ref<ProcessListenerVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  type: undefined,
+  status: CommonStatusEnum.ENABLE
+})
+
+/** 打开弹窗 */
+const open = async (type: string) => {
+  dialogVisible.value = true
+  loading.value = true
+  try {
+    queryParams.pageNo = 1
+    queryParams.type = type
+    const data = await ProcessListenerApi.getProcessListenerPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const select = async (row) => {
+  dialogVisible.value = false
+  // 发送操作成功的事件
+  emit('select', row)
+}
+</script>

+ 41 - 1
src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue

@@ -39,6 +39,13 @@
         title="添加监听器"
         @click="openListenerForm(null)"
       />
+      <XButton
+        type="success"
+        preIcon="ep:select"
+        title="选择监听器"
+        size="small"
+        @click="openProcessListenerDialog"
+      />
     </div>
 
     <!-- 监听器 编辑/创建 部分 -->
@@ -286,11 +293,22 @@
       </template>
     </el-dialog>
   </div>
+
+  <!-- 选择弹窗 -->
+  <ProcessListenerDialog ref="processListenerDialogRef" @select="selectProcessListener" />
 </template>
 <script lang="ts" setup>
 import { ElMessageBox } from 'element-plus'
 import { createListenerObject, updateElementExtensions } from '../../utils'
-import { initListenerForm, initListenerType, eventType, listenerType, fieldType } from './utilSelf'
+import {
+  initListenerForm,
+  initListenerType,
+  eventType,
+  listenerType,
+  fieldType,
+  initListenerForm2
+} from './utilSelf'
+import ProcessListenerDialog from '@/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue'
 
 defineOptions({ name: 'UserTaskListeners' })
 
@@ -437,6 +455,28 @@ const removeListenerField = (field, index) => {
     .catch(() => console.info('操作取消'))
 }
 
+// 打开监听器弹窗
+const processListenerDialogRef = ref()
+const openProcessListenerDialog = async () => {
+  processListenerDialogRef.value.open('task')
+}
+const selectProcessListener = (listener) => {
+  const listenerForm = initListenerForm2(listener)
+  const listenerObject = createListenerObject(listenerForm, true, prefix)
+  bpmnElementListeners.value.push(listenerObject)
+  elementListenersList.value.push(listenerForm)
+
+  // 保存其他配置
+  otherExtensionList.value =
+    bpmnElement.value.businessObject?.extensionElements?.values?.filter(
+      (ex) => ex.$type !== `${prefix}:TaskListener`
+    ) ?? []
+  updateElementExtensions(
+    bpmnElement.value,
+    otherExtensionList.value.concat(bpmnElementListeners.value)
+  )
+}
+
 watch(
   () => props.id,
   (val) => {

+ 27 - 0
src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts

@@ -40,6 +40,33 @@ export function initListenerType(listener) {
   }
 }
 
+/** 将 ProcessListenerDO 转换成 initListenerForm 想同的 Form 对象 */
+export function initListenerForm2(processListener) {
+  if (processListener.valueType === 'class') {
+    return {
+      listenerType: 'classListener',
+      class: processListener.value,
+      event: processListener.event,
+      fields: []
+    }
+  } else if (processListener.valueType === 'expression') {
+    return {
+      listenerType: 'expressionListener',
+      expression: processListener.value,
+      event: processListener.event,
+      fields: []
+    }
+  } else if (processListener.valueType === 'delegateExpression') {
+    return {
+      listenerType: 'delegateExpressionListener',
+      delegateExpression: processListener.value,
+      event: processListener.event,
+      fields: []
+    }
+  }
+  throw new Error('未知的监听器类型')
+}
+
 export const listenerType = {
   classListener: 'Java 类',
   expressionListener: '表达式',

+ 31 - 5
src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue

@@ -1,11 +1,15 @@
 <template>
   <div class="panel-tab__content">
     <el-form label-width="90px">
-      <el-form-item label="回路特性">
+      <el-form-item label="快捷配置">
+        <el-button size="small" @click="changeConfig('依次审批')">依次审批</el-button>
+        <el-button size="small" @click="changeConfig('会签')">会签</el-button>
+        <el-button size="small" @click="changeConfig('或签')">或签</el-button>
+      </el-form-item>
+      <el-form-item label="会签类型">
         <el-select v-model="loopCharacteristics" @change="changeLoopCharacteristicsType">
           <el-option label="并行多重事件" value="ParallelMultiInstance" />
           <el-option label="时序多重事件" value="SequentialMultiInstance" />
-          <el-option label="循环事件" value="StandardLoop" />
           <el-option label="无" value="Null" />
         </el-select>
       </el-form-item>
@@ -15,7 +19,7 @@
           loopCharacteristics === 'SequentialMultiInstance'
         "
       >
-        <el-form-item label="循环数" key="loopCardinality">
+        <el-form-item label="循环数" key="loopCardinality">
           <el-input
             v-model="loopInstanceForm.loopCardinality"
             clearable
@@ -25,7 +29,8 @@
         <el-form-item label="集合" key="collection" v-show="false">
           <el-input v-model="loopInstanceForm.collection" clearable @change="updateLoopBase" />
         </el-form-item>
-        <el-form-item label="元素变量" key="elementVariable">
+        <!-- add by 芋艿:由于「元素变量」暂时用不到,所以这里 display 为 none -->
+        <el-form-item label="元素变量" key="elementVariable" style="display: none">
           <el-input v-model="loopInstanceForm.elementVariable" clearable @change="updateLoopBase" />
         </el-form-item>
         <el-form-item label="完成条件" key="completionCondition">
@@ -35,7 +40,8 @@
             @change="updateLoopCondition"
           />
         </el-form-item>
-        <el-form-item label="异步状态" key="async">
+        <!-- add by 芋艿:由于「异步状态」暂时用不到,所以这里 display 为 none -->
+        <el-form-item label="异步状态" key="async" style="display: none">
           <el-checkbox
             v-model="loopInstanceForm.asyncBefore"
             label="异步前"
@@ -124,6 +130,7 @@ const getElementLoop = (businessObject) => {
       businessObject.loopCharacteristics.extensionElements.values[0].body
   }
 }
+
 const changeLoopCharacteristicsType = (type) => {
   // this.loopInstanceForm = { ...this.defaultLoopInstanceForm }; // 切换类型取消原表单配置
   // 取消多实例配置
@@ -160,6 +167,7 @@ const changeLoopCharacteristicsType = (type) => {
     loopCharacteristics: toRaw(multiLoopInstance.value)
   })
 }
+
 // 循环基数
 const updateLoopCardinality = (cardinality) => {
   let loopCardinality = null
@@ -176,6 +184,7 @@ const updateLoopCardinality = (cardinality) => {
     }
   )
 }
+
 // 完成条件
 const updateLoopCondition = (condition) => {
   let completionCondition = null
@@ -192,6 +201,7 @@ const updateLoopCondition = (condition) => {
     }
   )
 }
+
 // 重试周期
 const updateLoopTimeCycle = (timeCycle) => {
   const extensionElements = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
@@ -209,6 +219,7 @@ const updateLoopTimeCycle = (timeCycle) => {
     }
   )
 }
+
 // 直接更新的基础信息
 const updateLoopBase = () => {
   bpmnInstances().modeling.updateModdleProperties(
@@ -220,6 +231,7 @@ const updateLoopBase = () => {
     }
   )
 }
+
 // 各异步状态
 const updateLoopAsync = (key) => {
   const { asyncBefore, asyncAfter } = loopInstanceForm.value
@@ -238,6 +250,20 @@ const updateLoopAsync = (key) => {
   )
 }
 
+const changeConfig = (config) => {
+  if (config === '依次审批') {
+    changeLoopCharacteristicsType('SequentialMultiInstance')
+    updateLoopCardinality('1')
+    updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }')
+  } else if (config === '会签') {
+    changeLoopCharacteristicsType('ParallelMultiInstance')
+    updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }')
+  } else if (config === '或签') {
+    changeLoopCharacteristicsType('ParallelMultiInstance')
+    updateLoopCondition('${ nrOfCompletedInstances > 0 }')
+  }
+}
+
 onBeforeUnmount(() => {
   multiLoopInstance.value = null
   bpmnElement.value = null

+ 2 - 1
src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue

@@ -1,7 +1,8 @@
 <template>
   <div class="panel-tab__content">
     <el-form size="small" label-width="90px">
-      <el-form-item label="异步延续">
+      <!-- add by 芋艿:由于「异步延续」暂时用不到,所以这里 display 为 none -->
+      <el-form-item label="异步延续" style="display: none">
         <el-checkbox
           v-model="taskConfigForm.asyncBefore"
           label="异步前"

+ 68 - 0
src/components/bpmnProcessDesigner/package/penal/task/task-components/ProcessExpressionDialog.vue

@@ -0,0 +1,68 @@
+<!-- 表达式选择 -->
+<template>
+  <Dialog title="请选择表达式" v-model="dialogVisible" width="1024px">
+    <ContentWrap>
+      <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+        <el-table-column label="名字" align="center" prop="name" />
+        <el-table-column label="表达式" align="center" prop="expression" />
+        <el-table-column label="操作" align="center">
+          <template #default="scope">
+            <el-button link type="primary" @click="select(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>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { CommonStatusEnum } from '@/utils/constants'
+import { ProcessExpressionApi, ProcessExpressionVO } from '@/api/bpm/processExpression'
+
+/** BPM 流程 表单 */
+defineOptions({ name: 'ProcessExpressionDialog' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const loading = ref(true) // 列表的加载中
+const list = ref<ProcessExpressionVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  type: undefined,
+  status: CommonStatusEnum.ENABLE
+})
+
+/** 打开弹窗 */
+const open = async (type: string) => {
+  dialogVisible.value = true
+  loading.value = true
+  try {
+    queryParams.pageNo = 1
+    queryParams.type = type
+    const data = await ProcessExpressionApi.getProcessExpressionPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const select = async (row) => {
+  dialogVisible.value = false
+  // 发送操作成功的事件
+  emit('select', row)
+}
+</script>

+ 193 - 59
src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue

@@ -1,85 +1,204 @@
 <template>
-  <div style="margin-top: 16px">
-    <!--    <el-form-item label="处理用户">-->
-    <!--      <el-select v-model="userTaskForm.assignee" @change="updateElementTask('assignee')">-->
-    <!--        <el-option v-for="ak in mockData" :key="'ass-' + ak" :label="`用户${ak}`" :value="`user${ak}`" />-->
-    <!--      </el-select>-->
-    <!--    </el-form-item>-->
-    <!--    <el-form-item label="候选用户">-->
-    <!--      <el-select v-model="userTaskForm.candidateUsers" multiple collapse-tags @change="updateElementTask('candidateUsers')">-->
-    <!--        <el-option v-for="uk in mockData" :key="'user-' + uk" :label="`用户${uk}`" :value="`user${uk}`" />-->
-    <!--      </el-select>-->
-    <!--    </el-form-item>-->
-    <!--    <el-form-item label="候选分组">-->
-    <!--      <el-select v-model="userTaskForm.candidateGroups" multiple collapse-tags @change="updateElementTask('candidateGroups')">-->
-    <!--        <el-option v-for="gk in mockData" :key="'ass-' + gk" :label="`分组${gk}`" :value="`group${gk}`" />-->
-    <!--      </el-select>-->
-    <!--    </el-form-item>-->
-    <el-form-item label="到期时间">
-      <el-input v-model="userTaskForm.dueDate" clearable @change="updateElementTask('dueDate')" />
+  <el-form label-width="100px">
+    <el-form-item label="规则类型" prop="candidateStrategy">
+      <el-select
+        v-model="userTaskForm.candidateStrategy"
+        clearable
+        style="width: 100%"
+        @change="changeCandidateStrategy"
+      >
+        <el-option
+          v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_CANDIDATE_STRATEGY)"
+          :key="dict.value"
+          :label="dict.label"
+          :value="dict.value"
+        />
+      </el-select>
     </el-form-item>
-    <el-form-item label="跟踪时间">
-      <el-input
-        v-model="userTaskForm.followUpDate"
+    <el-form-item
+      v-if="userTaskForm.candidateStrategy == 10"
+      label="指定角色"
+      prop="candidateParam"
+    >
+      <el-select
+        v-model="userTaskForm.candidateParam"
         clearable
-        @change="updateElementTask('followUpDate')"
+        multiple
+        style="width: 100%"
+        @change="updateElementTask"
+      >
+        <el-option v-for="item in roleOptions" :key="item.id" :label="item.name" :value="item.id" />
+      </el-select>
+    </el-form-item>
+    <el-form-item
+      v-if="userTaskForm.candidateStrategy == 20 || userTaskForm.candidateStrategy == 21"
+      label="指定部门"
+      prop="candidateParam"
+      span="24"
+    >
+      <el-tree-select
+        ref="treeRef"
+        v-model="userTaskForm.candidateParam"
+        :data="deptTreeOptions"
+        :props="defaultProps"
+        empty-text="加载中,请稍后"
+        multiple
+        node-key="id"
+        show-checkbox
+        @change="updateElementTask"
       />
     </el-form-item>
-    <el-form-item label="优先级">
-      <el-input v-model="userTaskForm.priority" clearable @change="updateElementTask('priority')" />
+    <el-form-item
+      v-if="userTaskForm.candidateStrategy == 22"
+      label="指定岗位"
+      prop="candidateParam"
+      span="24"
+    >
+      <el-select
+        v-model="userTaskForm.candidateParam"
+        clearable
+        multiple
+        style="width: 100%"
+        @change="updateElementTask"
+      >
+        <el-option v-for="item in postOptions" :key="item.id" :label="item.name" :value="item.id" />
+      </el-select>
+    </el-form-item>
+    <el-form-item
+      v-if="userTaskForm.candidateStrategy == 30"
+      label="指定用户"
+      prop="candidateParam"
+      span="24"
+    >
+      <el-select
+        v-model="userTaskForm.candidateParam"
+        clearable
+        multiple
+        style="width: 100%"
+        @change="updateElementTask"
+      >
+        <el-option
+          v-for="item in userOptions"
+          :key="item.id"
+          :label="item.nickname"
+          :value="item.id"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item
+      v-if="userTaskForm.candidateStrategy === 40"
+      label="指定用户组"
+      prop="candidateParam"
+    >
+      <el-select
+        v-model="userTaskForm.candidateParam"
+        clearable
+        multiple
+        style="width: 100%"
+        @change="updateElementTask"
+      >
+        <el-option
+          v-for="item in userGroupOptions"
+          :key="item.id"
+          :label="item.name"
+          :value="item.id"
+        />
+      </el-select>
     </el-form-item>
-    友情提示:任务的分配规则,使用
-    <router-link target="_blank" :to="{ path: '/bpm/manager/model' }"
-      ><el-link type="danger">流程模型</el-link>
-    </router-link>
-    下的【分配规则】替代,提供指定角色、部门负责人、部门成员、岗位、工作组、自定义脚本等 7
-    种维护的任务分配维度,更加灵活!
-  </div>
+    <el-form-item
+      v-if="userTaskForm.candidateStrategy === 60"
+      label="流程表达式"
+      prop="candidateParam"
+    >
+      <el-input
+        type="textarea"
+        v-model="userTaskForm.candidateParam[0]"
+        clearable
+        style="width: 72%"
+        @change="updateElementTask"
+      />
+      <el-button class="ml-5px" size="small" type="success" @click="openProcessExpressionDialog"
+        >选择表达式</el-button
+      >
+      <!-- 选择弹窗 -->
+      <ProcessExpressionDialog ref="processExpressionDialogRef" @select="selectProcessExpression" />
+    </el-form-item>
+  </el-form>
 </template>
 
 <script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as RoleApi from '@/api/system/role'
+import * as DeptApi from '@/api/system/dept'
+import * as PostApi from '@/api/system/post'
+import * as UserApi from '@/api/system/user'
+import * as UserGroupApi from '@/api/bpm/userGroup'
+import ProcessExpressionDialog from './ProcessExpressionDialog.vue'
+
 defineOptions({ name: 'UserTask' })
 const props = defineProps({
   id: String,
   type: String
 })
-const defaultTaskForm = ref({
-  assignee: '',
-  candidateUsers: [],
-  candidateGroups: [],
-  dueDate: '',
-  followUpDate: '',
-  priority: ''
+const userTaskForm = ref({
+  candidateStrategy: undefined, // 分配规则
+  candidateParam: [] // 分配选项
 })
-const userTaskForm = ref<any>({})
-// const mockData=ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
 const bpmnElement = ref()
 const bpmnInstances = () => (window as any)?.bpmnInstances
 
+const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
+const deptTreeOptions = ref() // 部门树
+const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表
+const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
+const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
+
 const resetTaskForm = () => {
-  for (let key in defaultTaskForm.value) {
-    let value
-    if (key === 'candidateUsers' || key === 'candidateGroups') {
-      value = bpmnElement.value?.businessObject[key]
-        ? bpmnElement.value.businessObject[key].split(',')
-        : []
+  const businessObject = bpmnElement.value.businessObject
+  if (!businessObject) {
+    return
+  }
+  if (businessObject.candidateStrategy != undefined) {
+    userTaskForm.value.candidateStrategy = parseInt(businessObject.candidateStrategy) as any
+  } else {
+    userTaskForm.value.candidateStrategy = undefined
+  }
+  if (businessObject.candidateParam && businessObject.candidateParam.length > 0) {
+    if (userTaskForm.value.candidateStrategy === 60) {
+      // 特殊:流程表达式,只有一个 input 输入框
+      userTaskForm.value.candidateParam = [businessObject.candidateParam]
     } else {
-      value = bpmnElement.value?.businessObject[key] || defaultTaskForm.value[key]
+      userTaskForm.value.candidateParam = businessObject.candidateParam
+        .split(',')
+        .map((item) => +item)
     }
-    userTaskForm.value[key] = value
-  }
-}
-const updateElementTask = (key) => {
-  const taskAttr = Object.create(null)
-  if (key === 'candidateUsers' || key === 'candidateGroups') {
-    taskAttr[key] =
-      userTaskForm.value[key] && userTaskForm.value[key].length
-        ? userTaskForm.value[key].join()
-        : null
   } else {
-    taskAttr[key] = userTaskForm.value[key] || null
+    userTaskForm.value.candidateParam = []
   }
-  bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr)
+}
+
+/** 更新 candidateStrategy 字段时,需要清空 candidateParam,并触发 bpmn 图更新 */
+const changeCandidateStrategy = () => {
+  userTaskForm.value.candidateParam = []
+  updateElementTask()
+}
+
+/** 选中某个 options 时候,更新 bpmn 图  */
+const updateElementTask = () => {
+  bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+    candidateStrategy: userTaskForm.value.candidateStrategy,
+    candidateParam: userTaskForm.value.candidateParam.join(',')
+  })
+}
+
+// 打开监听器弹窗
+const processExpressionDialogRef = ref()
+const openProcessExpressionDialog = async () => {
+  processExpressionDialogRef.value.open()
+}
+const selectProcessExpression = (expression) => {
+  userTaskForm.value.candidateParam = [expression.expression]
 }
 
 watch(
@@ -92,6 +211,21 @@ watch(
   },
   { immediate: true }
 )
+
+onMounted(async () => {
+  // 获得角色列表
+  roleOptions.value = await RoleApi.getSimpleRoleList()
+  // 获得部门列表
+  const deptOptions = await DeptApi.getSimpleDeptList()
+  deptTreeOptions.value = handleTree(deptOptions, 'id')
+  // 获得岗位列表
+  postOptions.value = await PostApi.getSimplePostList()
+  // 获得用户列表
+  userOptions.value = await UserApi.getSimpleUserList()
+  // 获得用户组列表
+  userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
+})
+
 onBeforeUnmount(() => {
   bpmnElement.value = null
 })

+ 1 - 0
src/components/bpmnProcessDesigner/package/utils.ts

@@ -2,6 +2,7 @@ import { toRaw } from 'vue'
 const bpmnInstances = () => (window as any)?.bpmnInstances
 // 创建监听器实例
 export function createListenerObject(options, isTask, prefix) {
+  debugger
   const listenerObj = Object.create(null)
   listenerObj.event = options.event
   isTask && (listenerObj.id = options.id) // 任务监听器特有的 id 字段

+ 15 - 26
src/router/modules/remaining.ts

@@ -243,7 +243,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
     },
     children: [
       {
-        path: '/manager/form/edit',
+        path: 'manager/form/edit',
         component: () => import('@/views/bpm/form/editor/index.vue'),
         name: 'BpmFormEditor',
         meta: {
@@ -255,7 +255,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
         }
       },
       {
-        path: '/manager/model/edit',
+        path: 'manager/model/edit',
         component: () => import('@/views/bpm/model/editor/index.vue'),
         name: 'BpmModelEditor',
         meta: {
@@ -267,42 +267,31 @@ const remainingRouter: AppRouteRecordRaw[] = [
         }
       },
       {
-        path: '/manager/definition',
-        component: () => import('@/views/bpm/definition/index.vue'),
-        name: 'BpmProcessDefinition',
+        path: 'manager/simple/workflow/model/edit',
+        component: () => import('@/views/bpm/simpleWorkflow/index.vue'),
+        name: 'SimpleWorkflowDesignEditor',
         meta: {
           noCache: true,
           hidden: true,
           canTo: true,
-          title: '流程定义',
+          title: '仿钉钉设计流程',
           activeMenu: '/bpm/manager/model'
         }
       },
       {
-        path: '/manager/task-assign-rule',
-        component: () => import('@/views/bpm/taskAssignRule/index.vue'),
-        name: 'BpmTaskAssignRuleList',
-        meta: {
-          noCache: true,
-          hidden: true,
-          canTo: true,
-          title: '任务分配规则'
-        }
-      },
-      {
-        path: '/process-instance/create',
-        component: () => import('@/views/bpm/processInstance/create/index.vue'),
-        name: 'BpmProcessInstanceCreate',
+        path: 'manager/definition',
+        component: () => import('@/views/bpm/definition/index.vue'),
+        name: 'BpmProcessDefinition',
         meta: {
           noCache: true,
           hidden: true,
           canTo: true,
-          title: '发起流程',
-          activeMenu: 'bpm/processInstance/create'
+          title: '流程定义',
+          activeMenu: '/bpm/manager/model'
         }
       },
       {
-        path: '/process-instance/detail',
+        path: 'process-instance/detail',
         component: () => import('@/views/bpm/processInstance/detail/index.vue'),
         name: 'BpmProcessInstanceDetail',
         meta: {
@@ -310,11 +299,11 @@ const remainingRouter: AppRouteRecordRaw[] = [
           hidden: true,
           canTo: true,
           title: '流程详情',
-          activeMenu: 'bpm/processInstance/detail'
+          activeMenu: '/bpm/task/my'
         }
       },
       {
-        path: '/bpm/oa/leave/create',
+        path: 'oa/leave/create',
         component: () => import('@/views/bpm/oa/leave/create.vue'),
         name: 'OALeaveCreate',
         meta: {
@@ -326,7 +315,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
         }
       },
       {
-        path: '/bpm/oa/leave/detail',
+        path: 'oa/leave/detail',
         component: () => import('@/views/bpm/oa/leave/detail.vue'),
         name: 'OALeaveDetail',
         meta: {

+ 55 - 0
src/store/modules/simpleWorkflow.ts

@@ -0,0 +1,55 @@
+import { store } from '../index'
+import { defineStore } from 'pinia'
+
+export const useWorkFlowStore = defineStore('simpleWorkflow', {
+  state: () => ({
+    tableId: '',
+    isTried: false,
+    promoterDrawer: false,
+    flowPermission1: {},
+    approverDrawer: false,
+    approverConfig1: {},
+    copyerDrawer: false,
+    copyerConfig1: {},
+    conditionDrawer: false,
+    conditionsConfig1: {
+      conditionNodes: []
+    }
+  }),
+  actions: {
+    setTableId(payload) {
+      this.tableId = payload
+    },
+    setIsTried(payload) {
+      this.isTried = payload
+    },
+    setPromoter(payload) {
+      this.promoterDrawer = payload
+    },
+    setFlowPermission(payload) {
+      this.flowPermission1 = payload
+    },
+    setApprover(payload) {
+      this.approverDrawer = payload
+    },
+    setApproverConfig(payload) {
+      this.approverConfig1 = payload
+    },
+    setCopyer(payload) {
+      this.copyerDrawer = payload
+    },
+    setCopyerConfig(payload) {
+      this.copyerConfig1 = payload
+    },
+    setCondition(payload) {
+      this.conditionDrawer = payload
+    },
+    setConditionsConfig(payload) {
+      this.conditionsConfig1 = payload
+    }
+  }
+})
+
+export const useWorkFlowStoreWithOut = () => {
+  return useWorkFlowStore(store)
+}

+ 5 - 5
src/utils/dict.ts

@@ -136,13 +136,13 @@ export enum DICT_TYPE {
   INFRA_FILE_STORAGE = 'infra_file_storage',
 
   // ========== BPM 模块 ==========
-  BPM_MODEL_CATEGORY = 'bpm_model_category',
   BPM_MODEL_FORM_TYPE = 'bpm_model_form_type',
-  BPM_TASK_ASSIGN_RULE_TYPE = 'bpm_task_assign_rule_type',
+  BPM_TASK_CANDIDATE_STRATEGY = 'bpm_task_candidate_strategy',
   BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status',
-  BPM_PROCESS_INSTANCE_RESULT = 'bpm_process_instance_result',
-  BPM_TASK_ASSIGN_SCRIPT = 'bpm_task_assign_script',
+  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',
 
   // ========== PAY 模块 ==========
   PAY_CHANNEL_CODE = 'pay_channel_code', // 支付渠道编码类型
@@ -157,7 +157,7 @@ export enum DICT_TYPE {
   MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 自动回复请求匹配类型
   MP_MESSAGE_TYPE = 'mp_message_type', // 消息类型
 
-  // ========== MALL - 会员模块 ==========
+  // ========== Member 会员模块 ==========
   MEMBER_POINT_BIZ_TYPE = 'member_point_biz_type', // 积分的业务类型
   MEMBER_EXPERIENCE_BIZ_TYPE = 'member_experience_biz_type', // 会员经验业务类型
 

+ 8 - 5
src/utils/formCreate.ts

@@ -28,7 +28,7 @@ export const decodeFields = (fields: string[]) => {
   return rule
 }
 
-// 设置表单的 Conf 和 Fields
+// 设置表单的 Conf 和 Fields,适用 FcDesigner 场景
 export const setConfAndFields = (designerRef: object, conf: string, fields: string) => {
   // @ts-ignore
   designerRef.value.setOption(JSON.parse(conf))
@@ -36,19 +36,22 @@ export const setConfAndFields = (designerRef: object, conf: string, fields: stri
   designerRef.value.setRule(decodeFields(fields))
 }
 
-// 设置表单的 Conf 和 Fields
+// 设置表单的 Conf 和 Fields,适用 form-create 场景
 export const setConfAndFields2 = (
   detailPreview: object,
   conf: string,
   fields: string,
   value?: object
 ) => {
+  if (isRef(detailPreview)) {
+    detailPreview = detailPreview.value
+  }
   // @ts-ignore
-  detailPreview.value.option = JSON.parse(conf)
+  detailPreview.option = JSON.parse(conf)
   // @ts-ignore
-  detailPreview.value.rule = decodeFields(fields)
+  detailPreview.rule = decodeFields(fields)
   if (value) {
     // @ts-ignore
-    detailPreview.value.value = value
+    detailPreview.value = value
   }
 }

+ 5 - 5
src/utils/formatTime.ts

@@ -175,18 +175,18 @@ export function formatPast2(ms: number): string {
   const minute = Math.floor(ms / (60 * 1000) - day * 24 * 60 - hour * 60)
   const second = Math.floor(ms / 1000 - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60)
   if (day > 0) {
-    return day + '天' + hour + '小时' + minute + '分钟'
+    return day + ' 天' + hour + ' 小时 ' + minute + ' 分钟'
   }
   if (hour > 0) {
-    return hour + '小时' + minute + '分钟'
+    return hour + ' 小时 ' + minute + ' 分钟'
   }
   if (minute > 0) {
-    return minute + '分钟'
+    return minute + ' 分钟'
   }
   if (second > 0) {
-    return second + '秒'
+    return second + ' 秒'
   } else {
-    return 0 + '秒'
+    return 0 + ' 秒'
   }
 }
 

+ 9 - 3
src/views/Login/components/LoginForm.vue

@@ -291,10 +291,16 @@ const doSocialLogin = async (type: number) => {
       await getTenantId()
       // 如果获取不到,则需要弹出提示,进行处理
       if (!authUtil.getTenantId()) {
-        await message.prompt('请输入租户名称', t('common.reminder')).then(async ({ value }) => {
-          const res = await LoginApi.getTenantIdByName(value)
+        try {
+          const data = await message.prompt('请输入租户名称', t('common.reminder'))
+          if (data?.action !== 'confirm') throw 'cancel'
+          const res = await LoginApi.getTenantIdByName(data.value)
           authUtil.setTenantId(res)
-        })
+        } catch (error) {
+          if (error === 'cancel') return
+        } finally {
+          loginLoading.value = false
+        }
       }
     }
     // 计算 redirectUri

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

@@ -0,0 +1,124 @@
+<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="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="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'
+
+/** 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,
+  status: undefined,
+  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,
+    status: undefined,
+    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 download from '@/utils/download'
+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>

+ 5 - 32
src/views/bpm/definition/index.vue

@@ -11,11 +11,7 @@
           </el-button>
         </template>
       </el-table-column>
-      <el-table-column label="定义分类" align="center" prop="category" width="100">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="scope.row.category" />
-        </template>
-      </el-table-column>
+      <el-table-column label="定义分类" align="center" prop="categoryName" width="100" />
       <el-table-column label="表单信息" align="center" prop="formType" width="200">
         <template #default="scope">
           <el-button
@@ -57,18 +53,6 @@
         width="300"
         show-overflow-tooltip
       />
-      <el-table-column label="操作" align="center" width="150" fixed="right">
-        <template #default="scope">
-          <el-button
-            link
-            type="primary"
-            @click="handleAssignRule(scope.row)"
-            v-hasPermi="['bpm:task-assign-rule:query']"
-          >
-            分配规则
-          </el-button>
-        </template>
-      </el-table-column>
     </el-table>
     <!-- 分页 -->
     <Pagination
@@ -88,8 +72,8 @@
   <Dialog title="流程图" v-model="bpmnDetailVisible" width="800">
     <MyProcessViewer
       key="designer"
-      v-model="bpmnXML"
-      :value="bpmnXML as any"
+      v-model="bpmnXml"
+      :value="bpmnXml as any"
       v-bind="bpmnControlForm"
       :prefix="bpmnControlForm.prefix"
     />
@@ -97,7 +81,6 @@
 </template>
 
 <script lang="ts" setup>
-import { DICT_TYPE } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
 import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
 import * as DefinitionApi from '@/api/bpm/definition'
@@ -129,16 +112,6 @@ const getList = async () => {
   }
 }
 
-/** 点击任务分配按钮 */
-const handleAssignRule = (row) => {
-  push({
-    name: 'BpmTaskAssignRuleList',
-    query: {
-      modelId: row.id
-    }
-  })
-}
-
 /** 流程表单的详情按钮操作 */
 const formDetailVisible = ref(false)
 const formDetailPreview = ref({
@@ -160,12 +133,12 @@ const handleFormDetail = async (row) => {
 
 /** 流程图的详情按钮操作 */
 const bpmnDetailVisible = ref(false)
-const bpmnXML = ref(null)
+const bpmnXml = ref(null)
 const bpmnControlForm = ref({
   prefix: 'flowable'
 })
 const handleBpmnDetail = async (row) => {
-  bpmnXML.value = await DefinitionApi.getProcessDefinitionBpmnXML(row.id)
+  bpmnXml.value = (await DefinitionApi.getProcessDefinition(row.id))?.bpmnXml
   bpmnDetailVisible.value = true
 }
 

+ 5 - 5
src/views/bpm/group/UserGroupForm.vue

@@ -13,8 +13,8 @@
       <el-form-item label="描述">
         <el-input v-model="formData.description" placeholder="请输入描述" type="textarea" />
       </el-form-item>
-      <el-form-item label="成员" prop="memberUserIds">
-        <el-select v-model="formData.memberUserIds" multiple placeholder="请选择成员">
+      <el-form-item label="成员" prop="userIds">
+        <el-select v-model="formData.userIds" multiple placeholder="请选择成员">
           <el-option
             v-for="user in userList"
             :key="user.id"
@@ -60,13 +60,13 @@ const formData = ref({
   id: undefined,
   name: undefined,
   description: undefined,
-  memberUserIds: undefined,
+  userIds: undefined,
   status: CommonStatusEnum.ENABLE
 })
 const formRules = reactive({
   name: [{ required: true, message: '组名不能为空', trigger: 'blur' }],
   description: [{ required: true, message: '描述不能为空', trigger: 'blur' }],
-  memberUserIds: [{ required: true, message: '成员不能为空', trigger: 'blur' }],
+  userIds: [{ required: true, message: '成员不能为空', trigger: 'blur' }],
   status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
@@ -124,7 +124,7 @@ const resetForm = () => {
     id: undefined,
     name: undefined,
     description: undefined,
-    memberUserIds: undefined,
+    userIds: undefined,
     status: CommonStatusEnum.ENABLE
   }
   formRef.value?.resetFields()

+ 1 - 1
src/views/bpm/group/index.vue

@@ -63,7 +63,7 @@
       <el-table-column label="描述" align="center" prop="description" />
       <el-table-column label="成员" align="center">
         <template #default="scope">
-          <span v-for="userId in scope.row.memberUserIds" :key="userId" class="pr-5px">
+          <span v-for="userId in scope.row.userIds" :key="userId" class="pr-5px">
             {{ userList.find((user) => user.id === userId)?.nickname }}
           </span>
         </template>

+ 18 - 9
src/views/bpm/model/ModelForm.vue

@@ -43,13 +43,16 @@
           style="width: 100%"
         >
           <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_CATEGORY)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
+            v-for="category in categoryList"
+            :key="category.code"
+            :label="category.name"
+            :value="category.code"
           />
         </el-select>
       </el-form-item>
+      <el-form-item v-if="formData.id" label="流程图标" prop="icon">
+        <UploadImg v-model="formData.icon" :limit="1" height="128px" width="128px" />
+      </el-form-item>
       <el-form-item label="流程描述" prop="description">
         <el-input v-model="formData.description" clearable type="textarea" />
       </el-form-item>
@@ -126,6 +129,7 @@ import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { ElMessageBox } from 'element-plus'
 import * as ModelApi from '@/api/bpm/model'
 import * as FormApi from '@/api/bpm/form'
+import { CategoryApi } from '@/api/bpm/category'
 
 defineOptions({ name: 'ModelForm' })
 
@@ -140,20 +144,23 @@ const formData = ref({
   formType: 10,
   name: '',
   category: undefined,
+  icon: undefined,
   description: '',
   formId: '',
   formCustomCreatePath: '',
   formCustomViewPath: ''
 })
 const formRules = reactive({
-  category: [{ required: true, message: '参数分类不能为空', trigger: 'blur' }],
   name: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }],
   key: [{ required: true, message: '参数键名不能为空', trigger: 'blur' }],
+  category: [{ required: true, message: '参数分类不能为空', trigger: 'blur' }],
+  icon: [{ required: true, message: '参数图标不能为空', trigger: 'blur' }],
   value: [{ required: true, message: '参数键值不能为空', trigger: 'blur' }],
   visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
 const formList = ref([]) // 流程表单的下拉框的数据
+const categoryList = ref([]) // 流程分类列表
 
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
@@ -171,7 +178,9 @@ const open = async (type: string, id?: number) => {
     }
   }
   // 获得流程表单的下拉框的数据
-  formList.value = await FormApi.getSimpleFormList()
+  formList.value = await FormApi.getFormSimpleList()
+  // 查询流程分类列表
+  categoryList.value = await CategoryApi.getCategorySimpleList()
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
@@ -190,11 +199,10 @@ const submitForm = async () => {
       await ModelApi.createModel(data)
       // 提示,引导用户做后续的操作
       await ElMessageBox.alert(
-        '<strong>新建模型成功!</strong>后续需要执行如下 4 个步骤:' +
+        '<strong>新建模型成功!</strong>后续需要执行如下 3 个步骤:' +
           '<div>1. 点击【修改流程】按钮,配置流程的分类、表单信息</div>' +
           '<div>2. 点击【设计流程】按钮,绘制流程图</div>' +
-          '<div>3. 点击【分配规则】按钮,设置每个用户任务的审批人</div>' +
-          '<div>4. 点击【发布流程】按钮,完成流程的最终发布</div>' +
+          '<div>3. 点击【发布流程】按钮,完成流程的最终发布</div>' +
           '另外,每次流程修改后,都需要点击【发布流程】按钮,才能正式生效!!!',
         '重要提示',
         {
@@ -220,6 +228,7 @@ const resetForm = () => {
     formType: 10,
     name: '',
     category: undefined,
+    icon: '',
     description: '',
     formId: '',
     formCustomCreatePath: '',

+ 1 - 0
src/views/bpm/model/ModelImportForm.vue

@@ -109,6 +109,7 @@ const submitFormSuccess = async (response: any) => {
   }
   // 提示成功
   message.success('导入流程成功!请点击【设计流程】按钮,进行编辑保存后,才可以进行【发布流程】')
+  dialogVisible.value = false
   // 发送操作成功的事件
   emit('success')
 }

+ 1 - 1
src/views/bpm/model/editor/index.vue

@@ -89,11 +89,11 @@ onMounted(async () => {
   }
   // 查询模型
   const data = await ModelApi.getModel(modelId)
-  xmlString.value = data.bpmnXml
   model.value = {
     ...data,
     bpmnXml: undefined // 清空 bpmnXml 属性
   }
+  xmlString.value = data.bpmnXml
 })
 </script>
 <style lang="scss">

+ 25 - 22
src/views/bpm/model/index.vue

@@ -36,10 +36,10 @@
           class="!w-240px"
         >
           <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_CATEGORY)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
+            v-for="category in categoryList"
+            :key="category.code"
+            :label="category.name"
+            :value="category.code"
           />
         </el-select>
       </el-form-item>
@@ -72,11 +72,12 @@
           </el-button>
         </template>
       </el-table-column>
-      <el-table-column label="流程分类" align="center" prop="category" width="100">
+      <el-table-column label="流程图标" align="center" prop="icon" width="100">
         <template #default="scope">
-          <dict-tag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="scope.row.category" />
+          <el-image :src="scope.row.icon" class="w-32px h-32px" />
         </template>
       </el-table-column>
+      <el-table-column label="流程分类" align="center" prop="categoryName" width="100" />
       <el-table-column label="表单信息" align="center" prop="formType" width="200">
         <template #default="scope">
           <el-button
@@ -164,10 +165,10 @@
           <el-button
             link
             type="primary"
-            @click="handleAssignRule(scope.row)"
-            v-hasPermi="['bpm:task-assign-rule:query']"
+            @click="handleSimpleDesign(scope.row.id)"
+            v-hasPermi="['bpm:model:update']"
           >
-            分配规则
+            仿钉钉设计流程
           </el-button>
           <el-button
             link
@@ -229,7 +230,6 @@
 </template>
 
 <script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { dateFormatter, formatDate } from '@/utils/formatTime'
 import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
 import * as ModelApi from '@/api/bpm/model'
@@ -237,6 +237,7 @@ import * as FormApi from '@/api/bpm/form'
 import ModelForm from './ModelForm.vue'
 import ModelImportForm from '@/views/bpm/model/ModelImportForm.vue'
 import { setConfAndFields2 } from '@/utils/formCreate'
+import { CategoryApi } from '@/api/bpm/category'
 
 defineOptions({ name: 'BpmModel' })
 
@@ -255,6 +256,7 @@ const queryParams = reactive({
   category: undefined
 })
 const queryFormRef = ref() // 搜索的表单
+const categoryList = ref([]) // 流程分类列表
 
 /** 查询列表 */
 const getList = async () => {
@@ -334,6 +336,15 @@ const handleDesign = (row) => {
   })
 }
 
+const handleSimpleDesign = (row) => {
+  push({
+    name: 'SimpleWorkflowDesignEditor',
+    query: {
+      modelId: row.id
+    }
+  })
+}
+
 /** 发布流程 */
 const handleDeploy = async (row) => {
   try {
@@ -347,16 +358,6 @@ const handleDeploy = async (row) => {
   } catch {}
 }
 
-/** 点击任务分配按钮 */
-const handleAssignRule = (row) => {
-  push({
-    name: 'BpmTaskAssignRuleList',
-    query: {
-      modelId: row.id
-    }
-  })
-}
-
 /** 跳转到指定流程定义列表 */
 const handleDefinitionList = (row) => {
   push({
@@ -400,7 +401,9 @@ const handleBpmnDetail = async (row) => {
 }
 
 /** 初始化 **/
-onMounted(() => {
-  getList()
+onMounted(async () => {
+  await getList()
+  // 查询流程分类列表
+  categoryList.value = await CategoryApi.getCategorySimpleList()
 })
 </script>

+ 79 - 3
src/views/bpm/oa/leave/create.vue

@@ -37,6 +37,36 @@
     <el-form-item label="原因" prop="reason">
       <el-input v-model="formData.reason" placeholder="请输请假原因" type="textarea" />
     </el-form-item>
+    <el-col v-if="startUserSelectTasks.length > 0">
+      <el-card class="mb-10px">
+        <template #header>指定审批人</template>
+        <el-form
+          :model="startUserSelectAssignees"
+          :rules="startUserSelectAssigneesFormRules"
+          ref="startUserSelectAssigneesFormRef"
+        >
+          <el-form-item
+            v-for="userTask in startUserSelectTasks"
+            :key="userTask.id"
+            :label="`任务【${userTask.name}】`"
+            :prop="userTask.id"
+          >
+            <el-select
+              v-model="startUserSelectAssignees[userTask.id]"
+              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>
+      </el-card>
+    </el-col>
     <el-form-item>
       <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
     </el-form-item>
@@ -46,10 +76,15 @@
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import * as LeaveApi from '@/api/bpm/leave'
 import { useTagsViewStore } from '@/store/modules/tagsView'
+import * as DefinitionApi from '@/api/bpm/definition'
+import * as UserApi from '@/api/system/user'
 
 defineOptions({ name: 'BpmOALeaveCreate' })
 
 const message = useMessage() // 消息弹窗
+const { delView } = useTagsViewStore() // 视图操作
+const { push, currentRoute } = useRouter() // 路由
+
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formData = ref({
   type: undefined,
@@ -64,18 +99,34 @@ const formRules = reactive({
   endTime: [{ required: true, message: '请假结束时间不能为空', trigger: 'change' }]
 })
 const formRef = ref() // 表单 Ref
-const { delView } = useTagsViewStore() // 视图操作
-const { push, currentRoute } = useRouter() // 路由
+
+// 指定审批人
+const processDefineKey = 'oa_leave' // 流程定义 Key
+const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表
+const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
+const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref
+const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules
+const userList = ref<any[]>([]) // 用户列表
+
 /** 提交表单 */
 const submitForm = async () => {
   // 校验表单
   if (!formRef) return
   const valid = await formRef.value.validate()
   if (!valid) return
+  // 校验指定审批人
+  if (startUserSelectTasks.value?.length > 0) {
+    await startUserSelectAssigneesFormRef.value.validate()
+  }
+
   // 提交请求
   formLoading.value = true
   try {
-    const data = formData.value as unknown as LeaveApi.LeaveVO
+    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
@@ -85,4 +136,29 @@ const submitForm = async () => {
     formLoading.value = false
   }
 }
+
+/** 初始化 */
+onMounted(async () => {
+  const processDefinitionDetail = await DefinitionApi.getProcessDefinition(
+    undefined,
+    processDefineKey
+  )
+  if (!processDefinitionDetail) {
+    message.error('OA 请假的流程模型未配置,请检查!')
+    return
+  }
+  startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
+  // 设置指定审批人
+  if (startUserSelectTasks.value?.length > 0) {
+    // 设置校验规则
+    for (const userTask of startUserSelectTasks.value) {
+      startUserSelectAssignees.value[userTask.id] = []
+      startUserSelectAssigneesFormRules.value[userTask.id] = [
+        { required: true, message: '请选择审批人', trigger: 'blur' }
+      ]
+    }
+    // 加载用户列表
+    userList.value = await UserApi.getSimpleUserList()
+  }
+})
 </script>

+ 11 - 6
src/views/bpm/oa/leave/index.vue

@@ -36,10 +36,15 @@
           value-format="YYYY-MM-DD HH:mm:ss"
         />
       </el-form-item>
-      <el-form-item label="结果" prop="result">
-        <el-select v-model="queryParams.result" class="!w-240px" clearable placeholder="请选择结果">
+      <el-form-item label="审批结果" prop="result">
+        <el-select
+          v-model="queryParams.result"
+          class="!w-240px"
+          clearable
+          placeholder="请选择审批结果"
+        >
           <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT)"
+            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
             :key="dict.value"
             :label="dict.label"
             :value="dict.value"
@@ -78,7 +83,7 @@
       <el-table-column align="center" label="申请编号" prop="id" />
       <el-table-column align="center" label="状态" prop="result">
         <template #default="scope">
-          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.result" />
+          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.result" />
         </template>
       </el-table-column>
       <el-table-column
@@ -166,7 +171,7 @@ const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   type: undefined,
-  result: undefined,
+  status: undefined,
   reason: undefined,
   createTime: []
 })
@@ -221,7 +226,7 @@ const cancelLeave = async (row) => {
     inputErrorMessage: '取消原因不能为空'
   })
   // 发起取消
-  await ProcessInstanceApi.cancelProcessInstance(row.id, value)
+  await ProcessInstanceApi.cancelProcessInstanceByStartUser(row.id, value)
   message.success('取消成功')
   // 刷新列表
   await getList()

+ 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"
+            :label="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>

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

@@ -0,0 +1,180 @@
+<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>

+ 170 - 48
src/views/bpm/processInstance/create/index.vue

@@ -1,35 +1,45 @@
 <template>
   <!-- 第一步,通过流程定义的列表,选择对应的流程 -->
-  <ContentWrap v-if="!selectProcessInstance">
-    <el-table v-loading="loading" :data="list">
-      <el-table-column label="流程名称" align="center" prop="name" />
-      <el-table-column label="流程分类" align="center" prop="category">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="scope.row.category" />
-        </template>
-      </el-table-column>
-      <el-table-column label="流程版本" align="center" prop="version">
-        <template #default="scope">
-          <el-tag>v{{ scope.row.version }}</el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column label="流程描述" align="center" prop="description" />
-      <el-table-column label="操作" align="center">
-        <template #default="scope">
-          <el-button link type="primary" @click="handleSelect(scope.row)">
-            <Icon icon="ep:plus" /> 选择
-          </el-button>
-        </template>
-      </el-table-column>
-    </el-table>
+  <ContentWrap v-if="!selectProcessDefinition" v-loading="loading">
+    <el-tabs tab-position="left" v-model="categoryActive">
+      <el-tab-pane
+        :label="category.name"
+        :name="category.code"
+        :key="category.code"
+        v-for="category in categoryList"
+      >
+        <el-row :gutter="20">
+          <el-col
+            :lg="6"
+            :sm="12"
+            :xs="24"
+            v-for="definition in categoryProcessDefinitionList"
+            :key="definition.id"
+          >
+            <el-card
+              shadow="hover"
+              class="mb-20px cursor-pointer"
+              @click="handleSelect(definition)"
+            >
+              <template #default>
+                <div class="flex">
+                  <el-image :src="definition.icon" class="w-32px h-32px" />
+                  <el-text class="!ml-10px" size="large">{{ definition.name }}</el-text>
+                </div>
+              </template>
+            </el-card>
+          </el-col>
+        </el-row>
+      </el-tab-pane>
+    </el-tabs>
   </ContentWrap>
 
   <!-- 第二步,填写表单,进行流程的提交 -->
   <ContentWrap v-else>
     <el-card class="box-card">
       <div class="clearfix">
-        <span class="el-icon-document">申请信息【{{ selectProcessInstance.name }}】</span>
-        <el-button style="float: right" type="primary" @click="selectProcessInstance = undefined">
+        <span class="el-icon-document">申请信息【{{ selectProcessDefinition.name }}】</span>
+        <el-button style="float: right" type="primary" @click="selectProcessDefinition = undefined">
           <Icon icon="ep:delete" /> 选择其它流程
         </el-button>
       </div>
@@ -37,9 +47,43 @@
         <form-create
           :rule="detailForm.rule"
           v-model:api="fApi"
+          v-model="detailForm.value"
           :option="detailForm.option"
           @submit="submitForm"
-        />
+        >
+          <template #type-startUserSelect>
+            <el-col :span="24">
+              <el-card class="mb-10px">
+                <template #header>指定审批人</template>
+                <el-form
+                  :model="startUserSelectAssignees"
+                  :rules="startUserSelectAssigneesFormRules"
+                  ref="startUserSelectAssigneesFormRef"
+                >
+                  <el-form-item
+                    v-for="userTask in startUserSelectTasks"
+                    :key="userTask.id"
+                    :label="`任务【${userTask.name}】`"
+                    :prop="userTask.id"
+                  >
+                    <el-select
+                      v-model="startUserSelectAssignees[userTask.id]"
+                      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>
+              </el-card>
+            </el-col>
+          </template>
+        </form-create>
       </el-col>
     </el-card>
     <!-- 流程图预览 -->
@@ -47,59 +91,127 @@
   </ContentWrap>
 </template>
 <script lang="ts" setup>
-import { DICT_TYPE } from '@/utils/dict'
 import * as DefinitionApi from '@/api/bpm/definition'
 import * as ProcessInstanceApi from '@/api/bpm/processInstance'
 import { setConfAndFields2 } from '@/utils/formCreate'
 import type { ApiAttrs } from '@form-create/element-ui/types/config'
 import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
+import { CategoryApi } from '@/api/bpm/category'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import * as UserApi from '@/api/system/user'
 
 defineOptions({ name: 'BpmProcessInstanceCreate' })
 
-const router = useRouter() // 路由
+const route = useRoute() // 路由
+const { push, currentRoute } = useRouter() // 路由
 const message = useMessage() // 消息
+const { delView } = useTagsViewStore() // 视图操作
 
-// ========== 列表相关 ==========
-const loading = ref(true) // 列表的加载中
-const list = ref([]) // 列表的数据
-const queryParams = reactive({
-  suspensionState: 1
-})
+const processInstanceId = route.query.processInstanceId
+const loading = ref(true) // 加载中
+const categoryList = ref([]) // 分类的列表
+const categoryActive = ref('') // 选中的分类
+const processDefinitionList = ref([]) // 流程定义的列表
 
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {
-    list.value = await DefinitionApi.getProcessDefinitionList(queryParams)
+    // 流程分类
+    categoryList.value = await CategoryApi.getCategorySimpleList()
+    if (categoryList.value.length > 0) {
+      categoryActive.value = categoryList.value[0].code
+    }
+    // 流程定义
+    processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
+      suspensionState: 1
+    })
+
+    // 如果 processInstanceId 非空,说明是重新发起
+    if (processInstanceId?.length > 0) {
+      const processInstance = await ProcessInstanceApi.getProcessInstance(processInstanceId)
+      if (!processInstance) {
+        message.error('重新发起流程失败,原因:流程实例不存在')
+        return
+      }
+      const processDefinition = processDefinitionList.value.find(
+        (item) => item.key == processInstance.processDefinition?.key
+      )
+      if (!processDefinition) {
+        message.error('重新发起流程失败,原因:流程定义不存在')
+        return
+      }
+      await handleSelect(processDefinition, processInstance.formVariables)
+    }
   } finally {
     loading.value = false
   }
 }
 
+/** 选中分类对应的流程定义列表 */
+const categoryProcessDefinitionList = computed(() => {
+  return processDefinitionList.value.filter((item) => item.category == categoryActive.value)
+})
+
 // ========== 表单相关 ==========
-const bpmnXML = ref(null) // BPMN 数据
 const fApi = ref<ApiAttrs>()
 const detailForm = ref({
-  // 流程表单详情
   rule: [],
-  option: {}
-})
-const selectProcessInstance = ref() // 选择的流程实例
+  option: {},
+  value: {}
+}) // 流程表单详情
+const selectProcessDefinition = ref() // 选择的流程定义
+
+// 指定审批人
+const bpmnXML = ref(null) // BPMN 数据
+const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表
+const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
+const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref
+const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules
+const userList = ref<any[]>([]) // 用户列表
 
 /** 处理选择流程的按钮操作 **/
-const handleSelect = async (row) => {
+const handleSelect = async (row, formVariables) => {
   // 设置选择的流程
-  selectProcessInstance.value = row
+  selectProcessDefinition.value = row
+
+  // 重置指定审批人
+  startUserSelectTasks.value = []
+  startUserSelectAssignees.value = {}
+  startUserSelectAssigneesFormRules.value = {}
 
   // 情况一:流程表单
   if (row.formType == 10) {
     // 设置表单
-    setConfAndFields2(detailForm, row.formConf, row.formFields)
+    setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
     // 加载流程图
-    bpmnXML.value = await DefinitionApi.getProcessDefinitionBpmnXML(row.id)
+    const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id)
+    if (processDefinitionDetail) {
+      bpmnXML.value = processDefinitionDetail.bpmnXml
+      startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
+
+      // 设置指定审批人
+      if (startUserSelectTasks.value?.length > 0) {
+        detailForm.value.rule.push({
+          type: 'startUserSelect',
+          props: {
+            title: '指定审批人'
+          }
+        })
+        // 设置校验规则
+        for (const userTask of startUserSelectTasks.value) {
+          startUserSelectAssignees.value[userTask.id] = []
+          startUserSelectAssigneesFormRules.value[userTask.id] = [
+            { required: true, message: '请选择审批人', trigger: 'blur' }
+          ]
+        }
+        // 加载用户列表
+        userList.value = await UserApi.getSimpleUserList()
+      }
+    }
     // 情况二:业务表单
   } else if (row.formCustomCreatePath) {
-    await router.push({
+    await push({
       path: row.formCustomCreatePath
     })
     // 这里暂时无需加载流程图,因为跳出到另外个 Tab;
@@ -108,19 +220,29 @@ const handleSelect = async (row) => {
 
 /** 提交按钮 */
 const submitForm = async (formData) => {
-  if (!fApi.value || !selectProcessInstance.value) {
+  if (!fApi.value || !selectProcessDefinition.value) {
     return
   }
+  // 如果有指定审批人,需要校验
+  if (startUserSelectTasks.value?.length > 0) {
+    await startUserSelectAssigneesFormRef.value.validate()
+  }
+
   // 提交请求
   fApi.value.btn.loading(true)
   try {
     await ProcessInstanceApi.createProcessInstance({
-      processDefinitionId: selectProcessInstance.value.id,
-      variables: formData
+      processDefinitionId: selectProcessDefinition.value.id,
+      variables: formData,
+      startUserSelectAssignees: startUserSelectAssignees.value
     })
     // 提示
     message.success('发起流程成功')
-    router.go(-1)
+    // 跳转回去
+    delView(unref(currentRoute))
+    await push({
+      name: 'BpmProcessInstanceMy'
+    })
   } finally {
     fApi.value.btn.loading(false)
   }

+ 10 - 13
src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue

@@ -33,21 +33,18 @@ const bpmnControlForm = ref({
   prefix: 'flowable'
 })
 const activityList = ref([]) // 任务列表
-// const bpmnXML = computed(() => { // TODO 芋艿:不晓得为啊哈不能这么搞
-//   if (!props.processInstance || !props.processInstance.processDefinition) {
-//     return
-//   }
-//   return DefinitionApi.getProcessDefinitionBpmnXML(props.processInstance.processDefinition.id)
-// })
 
-/** 初始化 */
-onMounted(async () => {
-  if (props.id) {
-    activityList.value = await ActivityApi.getActivityList({
-      processInstanceId: props.id
-    })
+/** 只有 loading 完成时,才去加载流程列表 */
+watch(
+  () => props.loading,
+  async (value) => {
+    if (value && props.id) {
+      activityList.value = await ActivityApi.getActivityList({
+        processInstanceId: props.id
+      })
+    }
   }
-})
+)
 </script>
 <style>
 .box-card {

+ 0 - 96
src/views/bpm/processInstance/detail/ProcessInstanceChildrenTaskList.vue

@@ -1,96 +0,0 @@
-<template>
-  <el-drawer v-model="drawerVisible" title="子任务" size="70%">
-    <!-- 当前任务 -->
-    <template #header>
-      <h4>【{{ baseTask.name }} 】审批人:{{ baseTask.assigneeUser?.nickname }}</h4>
-      <el-button
-        style="margin-left: 5px"
-        v-if="isSubSignButtonVisible(baseTask)"
-        type="danger"
-        plain
-        @click="handleSubSign(baseTask)"
-      >
-        <Icon icon="ep:remove" /> 减签
-      </el-button>
-    </template>
-    <!-- 子任务列表 -->
-    <el-table :data="baseTask.children" style="width: 100%" row-key="id" border>
-      <el-table-column prop="assigneeUser.nickname" label="审批人" />
-      <el-table-column prop="assigneeUser.deptName" label="所在部门" />
-      <el-table-column label="审批状态" prop="result">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.result" />
-        </template>
-      </el-table-column>
-      <el-table-column
-        label="提交时间"
-        align="center"
-        prop="createTime"
-        width="180"
-        :formatter="dateFormatter"
-      />
-      <el-table-column
-        label="结束时间"
-        align="center"
-        prop="endTime"
-        width="180"
-        :formatter="dateFormatter"
-      />
-      <el-table-column label="操作" prop="operation">
-        <template #default="scope">
-          <el-button
-            v-if="isSubSignButtonVisible(scope.row)"
-            type="danger"
-            plain
-            @click="handleSubSign(scope.row)"
-          >
-            <Icon icon="ep:remove" /> 减签
-          </el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-    <!-- 减签 -->
-    <TaskSubSignDialogForm ref="taskSubSignDialogForm" />
-  </el-drawer>
-</template>
-<script lang="ts" setup>
-import { isEmpty } from '@/utils/is'
-import { DICT_TYPE } from '@/utils/dict'
-import { dateFormatter } from '@/utils/formatTime'
-import TaskSubSignDialogForm from './TaskSubSignDialogForm.vue'
-
-defineOptions({ name: 'ProcessInstanceChildrenTaskList' })
-
-const message = useMessage() // 消息弹窗
-const drawerVisible = ref(false) // 抽屉的是否展示
-
-const baseTask = ref<object>({})
-/** 打开弹窗 */
-const open = async (task: any) => {
-  if (isEmpty(task.children)) {
-    message.warning('该任务没有子任务')
-    return
-  }
-  baseTask.value = task
-  // 展开抽屉
-  drawerVisible.value = true
-}
-defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗
-
-/** 发起减签 */
-const taskSubSignDialogForm = ref()
-const handleSubSign = (item) => {
-  taskSubSignDialogForm.value.open(item.id)
-  // TODO @海洋:减签后,需要刷新下界面哈
-}
-
-/** 是否显示减签按钮 */
-const isSubSignButtonVisible = (task: any) => {
-  if (task && task.children && !isEmpty(task.children)) {
-    // 有子任务,且子任务有任意一个是 待处理 和 待前置任务完成 则显示减签按钮
-    const subTask = task.children.find((item) => item.result === 1 || item.result === 9)
-    return !isEmpty(subTask)
-  }
-  return false
-}
-</script>

+ 93 - 46
src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue

@@ -3,25 +3,44 @@
     <template #header>
       <span class="el-icon-picture-outline">审批记录</span>
     </template>
-    <el-col :offset="4" :span="16">
+    <el-col :offset="3" :span="17">
       <div class="block">
         <el-timeline>
+          <el-timeline-item
+            v-if="processInstance.endTime"
+            :type="getProcessInstanceTimelineItemType(processInstance)"
+          >
+            <p style="font-weight: 700">
+              结束流程:在 {{ formatDate(processInstance?.endTime) }} 结束
+              <dict-tag
+                :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS"
+                :value="processInstance.status"
+              />
+            </p>
+          </el-timeline-item>
           <el-timeline-item
             v-for="(item, index) in tasks"
             :key="index"
-            :icon="getTimelineItemIcon(item)"
-            :type="getTimelineItemType(item)"
+            :type="getTaskTimelineItemType(item)"
           >
             <p style="font-weight: 700">
-              任务:{{ item.name }}
-              <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="item.result" />
+              审批任务:{{ item.name }}
+              <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="item.status" />
               <el-button
-                style="margin-left: 5px"
+                class="ml-10px"
                 v-if="!isEmpty(item.children)"
                 @click="openChildrenTask(item)"
+                size="small"
+              >
+                <Icon icon="ep:memo" /> 子任务
+              </el-button>
+              <el-button
+                class="ml-10px"
+                size="small"
+                v-if="item.formId > 0"
+                @click="handleFormDetail(item)"
               >
-                <Icon icon="ep:memo" />
-                子任务
+                <Icon icon="ep:document" /> 查看表单
               </el-button>
             </p>
             <el-card :body-style="{ padding: '10px' }">
@@ -45,84 +64,112 @@
               <label v-if="item.durationInMillis" style="font-weight: normal; color: #8a909c">
                 {{ formatPast2(item?.durationInMillis) }}
               </label>
-              <p v-if="item.reason">
-                <el-tag :type="getTimelineItemType(item)">{{ item.reason }}</el-tag>
-              </p>
+              <p v-if="item.reason"> 审批建议:{{ item.reason }} </p>
             </el-card>
           </el-timeline-item>
+          <el-timeline-item type="success">
+            <p style="font-weight: 700">
+              发起流程:【{{ processInstance.startUser?.nickname }}】在
+              {{ formatDate(processInstance?.startTime) }} 发起【 {{ processInstance.name }} 】流程
+            </p>
+          </el-timeline-item>
         </el-timeline>
       </div>
     </el-col>
-    <!-- 子任务  -->
-    <ProcessInstanceChildrenTaskList ref="processInstanceChildrenTaskList" />
   </el-card>
+
+  <!-- 弹窗:子任务  -->
+  <TaskSignList ref="taskSignListRef" @success="refresh" />
+  <!-- 弹窗:表单 -->
+  <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 { formatDate, formatPast2 } from '@/utils/formatTime'
 import { propTypes } from '@/utils/propTypes'
 import { DICT_TYPE } from '@/utils/dict'
 import { isEmpty } from '@/utils/is'
-import ProcessInstanceChildrenTaskList from './ProcessInstanceChildrenTaskList.vue'
+import TaskSignList from './dialog/TaskSignList.vue'
+import type { ApiAttrs } from '@form-create/element-ui/types/config'
+import { setConfAndFields2 } from '@/utils/formCreate'
 
 defineOptions({ name: 'BpmProcessInstanceTaskList' })
 
 defineProps({
   loading: propTypes.bool, // 是否加载中
+  processInstance: propTypes.object, // 流程实例
   tasks: propTypes.arrayOf(propTypes.object) // 流程任务的数组
 })
 
-/** 获得任务对应的 icon */
-const getTimelineItemIcon = (item) => {
-  if (item.result === 1) {
-    return 'el-icon-time'
-  }
-  if (item.result === 2) {
-    return 'el-icon-check'
-  }
-  if (item.result === 3) {
-    return 'el-icon-close'
+/** 获得流程实例对应的颜色 */
+const getProcessInstanceTimelineItemType = (item: any) => {
+  if (item.status === 2) {
+    return 'success'
   }
-  if (item.result === 4) {
-    return 'el-icon-remove-outline'
+  if (item.status === 3) {
+    return 'danger'
   }
-  if (item.result === 5) {
-    return 'el-icon-back'
+  if (item.status === 4) {
+    return 'warning'
   }
   return ''
 }
 
 /** 获得任务对应的颜色 */
-const getTimelineItemType = (item) => {
-  if (item.result === 1) {
+const getTaskTimelineItemType = (item: any) => {
+  if ([0, 1, 6, 7].includes(item.status)) {
     return 'primary'
   }
-  if (item.result === 2) {
+  if (item.status === 2) {
     return 'success'
   }
-  if (item.result === 3) {
+  if (item.status === 3) {
     return 'danger'
   }
-  if (item.result === 4) {
+  if (item.status === 4) {
     return 'info'
   }
-  if (item.result === 5) {
-    return 'warning'
-  }
-  if (item.result === 6) {
-    return 'default'
-  }
-  if (item.result === 7 || item.result === 8) {
+  if (item.status === 5) {
     return 'warning'
   }
   return ''
 }
 
-/**
- * 子任务
- */
-const processInstanceChildrenTaskList = ref()
+/** 子任务 */
+const taskSignListRef = ref()
+const openChildrenTask = (item: any) => {
+  taskSignListRef.value.open(item)
+}
+
+/** 查看表单 */
+const fApi = ref<ApiAttrs>() // form-create 的 API 操作类
+const taskForm = ref({
+  rule: [],
+  option: {},
+  value: {}
+}) // 流程任务的表单详情
+const taskFormVisible = ref(false)
+const handleFormDetail = async (row) => {
+  // 设置表单
+  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)
+}
 
-const openChildrenTask = (item) => {
-  processInstanceChildrenTaskList.value.open(item)
+/** 刷新数据 */
+const emit = defineEmits(['refresh']) // 定义 success 事件,用于操作成功后的回调
+const refresh = () => {
+  emit('refresh')
 }
 </script>

+ 0 - 242
src/views/bpm/processInstance/detail/TaskCCDialogForm.vue

@@ -1,242 +0,0 @@
-<!-- TODO @kyle:需要在讨论下;可能直接选人更合适 -->
-<template>
-  <Dialog v-model="dialogVisible" title="修改任务规则" width="600">
-    <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
-      <el-form-item label="任务名称" prop="taskName">
-        <el-input v-model="formData.taskName" disabled placeholder="请输入任务名称" />
-      </el-form-item>
-      <el-form-item label="任务标识" prop="taskKey">
-        <el-input v-model="formData.taskKey" disabled placeholder="请输入任务标识" />
-      </el-form-item>
-      <el-form-item label="流程名称" prop="processInstanceName">
-        <el-input v-model="formData.processInstanceName" disabled placeholder="请输入流程名称" />
-      </el-form-item>
-      <el-form-item label="流程标识" prop="processInstanceKey">
-        <el-input v-model="formData.processInstanceKey" disabled placeholder="请输入流程标识" />
-      </el-form-item>
-      <el-form-item label="规则类型" prop="type">
-        <el-select v-model="formData.type" clearable style="width: 100%">
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_RULE_TYPE)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item v-if="formData.type === 10" label="指定角色" prop="roleIds">
-        <el-select v-model="formData.roleIds" clearable multiple style="width: 100%">
-          <el-option
-            v-for="item in roleOptions"
-            :key="item.id"
-            :label="item.name"
-            :value="item.id"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item
-        v-if="formData.type === 20 || formData.type === 21"
-        label="指定部门"
-        prop="deptIds"
-        span="24"
-      >
-        <el-tree-select
-          ref="treeRef"
-          v-model="formData.deptIds"
-          :data="deptTreeOptions"
-          :props="defaultProps"
-          empty-text="加载中,请稍后"
-          multiple
-          node-key="id"
-          show-checkbox
-        />
-      </el-form-item>
-      <el-form-item v-if="formData.type === 22" label="指定岗位" prop="postIds" span="24">
-        <el-select v-model="formData.postIds" clearable multiple style="width: 100%">
-          <el-option
-            v-for="item in postOptions"
-            :key="parseInt(item.id)"
-            :label="item.name"
-            :value="parseInt(item.id)"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item
-        v-if="formData.type === 30 || formData.type === 31 || formData.type === 32"
-        label="指定用户"
-        prop="userIds"
-        span="24"
-      >
-        <el-select v-model="formData.userIds" clearable multiple style="width: 100%">
-          <el-option
-            v-for="item in userOptions"
-            :key="parseInt(item.id)"
-            :label="item.nickname"
-            :value="parseInt(item.id)"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item v-if="formData.type === 40" label="指定用户组" prop="userGroupIds">
-        <el-select v-model="formData.userGroupIds" clearable multiple style="width: 100%">
-          <el-option
-            v-for="item in userGroupOptions"
-            :key="parseInt(item.id)"
-            :label="item.name"
-            :value="parseInt(item.id)"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item v-if="formData.type === 50" label="指定脚本" prop="scripts">
-        <el-select v-model="formData.scripts" clearable multiple style="width: 100%">
-          <el-option
-            v-for="dict in taskAssignScriptDictDatas"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="抄送原因" prop="reason">
-        <el-input v-model="formData.reason" 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 { defaultProps, handleTree } from '@/utils/tree'
-import * as ProcessInstanceApi from '@/api/bpm/processInstance'
-import * as RoleApi from '@/api/system/role'
-import * as DeptApi from '@/api/system/dept'
-import * as PostApi from '@/api/system/post'
-import * as UserApi from '@/api/system/user'
-import * as UserGroupApi from '@/api/bpm/userGroup'
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formData = ref({
-  type: Number(undefined),
-  taskName: '',
-  taskKey: '',
-  processInstanceName: '',
-  processInstanceKey: '',
-  startUserId: '',
-  options: [],
-  roleIds: [],
-  deptIds: [],
-  postIds: [],
-  userIds: [],
-  userGroupIds: [],
-  scripts: [],
-  reason: ''
-})
-const formRules = reactive({
-  type: [{ required: true, message: '规则类型不能为空', trigger: 'change' }],
-  roleIds: [{ required: true, message: '指定角色不能为空', trigger: 'change' }],
-  deptIds: [{ required: true, message: '指定部门不能为空', trigger: 'change' }],
-  postIds: [{ required: true, message: '指定岗位不能为空', trigger: 'change' }],
-  userIds: [{ required: true, message: '指定用户不能为空', trigger: 'change' }],
-  userGroupIds: [{ required: true, message: '指定用户组不能为空', trigger: 'change' }],
-  scripts: [{ required: true, message: '指定脚本不能为空', trigger: 'change' }],
-  reason: [{ required: true, message: '抄送原因不能为空', trigger: 'change' }]
-})
-const formRef = ref() // 表单 Ref
-const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
-const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表
-const deptTreeOptions = ref() // 部门树
-const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表
-const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
-const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
-const taskAssignScriptDictDatas = getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_SCRIPT)
-
-/** 打开弹窗 */
-const open = async (row) => {
-  // 1. 先重置表单
-  resetForm()
-  // 2. 再设置表单
-  if (row != null) {
-    formData.value.type = undefined as unknown as number
-    formData.value.taskName = row.name
-    formData.value.taskKey = row.id
-    formData.value.processInstanceName = row.processInstance.name
-    formData.value.processInstanceKey = row.processInstance.id
-    formData.value.startUserId = row.processInstance.startUserId
-  }
-  // 打开弹窗
-  dialogVisible.value = true
-
-  // 获得角色列表
-  roleOptions.value = await RoleApi.getSimpleRoleList()
-  // 获得部门列表
-  deptOptions.value = await DeptApi.getSimpleDeptList()
-  deptTreeOptions.value = handleTree(deptOptions.value, 'id')
-  // 获得岗位列表
-  postOptions.value = await PostApi.getSimplePostList()
-  // 获得用户列表
-  userOptions.value = await UserApi.getSimpleUserList()
-  // 获得用户组列表
-  userGroupOptions.value = await UserGroupApi.getSimpleUserGroupList()
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
-
-  // 构建表单
-  const form = {
-    ...formData.value
-  }
-  // 将 roleIds 等选项赋值到 options 中
-  if (form.type === 10) {
-    form.options = form.roleIds
-  } else if (form.type === 20 || form.type === 21) {
-    form.options = form.deptIds
-  } else if (form.type === 22) {
-    form.options = form.postIds
-  } else if (form.type === 30 || form.type === 31 || form.type === 32) {
-    form.options = form.userIds
-  } else if (form.type === 40) {
-    form.options = form.userGroupIds
-  } else if (form.type === 50) {
-    form.options = form.scripts
-  }
-  form.roleIds = undefined
-  form.deptIds = undefined
-  form.postIds = undefined
-  form.userIds = undefined
-  form.userGroupIds = undefined
-  form.scripts = undefined
-
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = form as unknown as ProcessInstanceApi.ProcessInstanceCCVO
-    await ProcessInstanceApi.createProcessInstanceCC(data)
-    console.log(data)
-    message.success(t('common.createSuccess'))
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formRef.value?.resetFields()
-}
-</script>

+ 6 - 3
src/views/bpm/processInstance/detail/TaskDelegateForm.vue → src/views/bpm/processInstance/detail/dialog/TaskDelegateForm.vue

@@ -37,10 +37,12 @@ const dialogVisible = ref(false) // 弹窗的是否展示
 const formLoading = ref(false) // 表单的加载中
 const formData = ref({
   id: '',
-  delegateUserId: undefined
+  delegateUserId: undefined,
+  reason: ''
 })
 const formRules = ref({
-  delegateUserId: [{ required: true, message: '接收人不能为空', trigger: 'change' }]
+  delegateUserId: [{ required: true, message: '接收人不能为空', trigger: 'change' }],
+  reason: [{ required: true, message: '委派理由不能为空', trigger: 'blur' }]
 })
 
 const formRef = ref() // 表单 Ref
@@ -79,7 +81,8 @@ const submitForm = async () => {
 const resetForm = () => {
   formData.value = {
     id: '',
-    delegateUserId: undefined
+    delegateUserId: undefined,
+    reason: ''
   }
   formRef.value?.resetFields()
 }

+ 10 - 10
src/views/bpm/processInstance/detail/TaskReturnDialogForm.vue → src/views/bpm/processInstance/detail/dialog/TaskReturnForm.vue

@@ -1,5 +1,5 @@
 <template>
-  <Dialog v-model="dialogVisible" title="回退" width="500">
+  <Dialog v-model="dialogVisible" title="回退任务" width="500">
     <el-form
       ref="formRef"
       v-loading="formLoading"
@@ -7,13 +7,13 @@
       :rules="formRules"
       label-width="110px"
     >
-      <el-form-item label="退回节点" prop="targetDefinitionKey">
-        <el-select v-model="formData.targetDefinitionKey" clearable style="width: 100%">
+      <el-form-item label="退回节点" prop="targetTaskDefinitionKey">
+        <el-select v-model="formData.targetTaskDefinitionKey" clearable style="width: 100%">
           <el-option
             v-for="item in returnList"
-            :key="item.definitionKey"
+            :key="item.taskDefinitionKey"
             :label="item.name"
-            :value="item.definitionKey"
+            :value="item.taskDefinitionKey"
           />
         </el-select>
       </el-form-item>
@@ -35,19 +35,19 @@ const dialogVisible = ref(false) // 弹窗的是否展示
 const formLoading = ref(false) // 表单的加载中
 const formData = ref({
   id: '',
-  targetDefinitionKey: undefined,
+  targetTaskDefinitionKey: undefined,
   reason: ''
 })
 const formRules = ref({
-  targetDefinitionKey: [{ required: true, message: '必须选择回退节点', trigger: 'change' }],
+  targetTaskDefinitionKey: [{ required: true, message: '必须选择回退节点', trigger: 'change' }],
   reason: [{ required: true, message: '回退理由不能为空', trigger: 'blur' }]
 })
 
 const formRef = ref() // 表单 Ref
-const returnList = ref([])
+const returnList = ref([] as any)
 /** 打开弹窗 */
 const open = async (id: string) => {
-  returnList.value = await TaskApi.getReturnList({ taskId: id })
+  returnList.value = await TaskApi.getTaskListByReturn(id)
   if (returnList.value.length === 0) {
     message.warning('当前没有可回退的节点')
     return false
@@ -82,7 +82,7 @@ const submitForm = async () => {
 const resetForm = () => {
   formData.value = {
     id: '',
-    targetDefinitionKey: undefined,
+    targetTaskDefinitionKey: undefined,
     reason: ''
   }
   formRef.value?.resetFields()

+ 12 - 10
src/views/bpm/processInstance/detail/TaskAddSignDialogForm.vue → src/views/bpm/processInstance/detail/dialog/TaskSignCreateForm.vue

@@ -7,8 +7,8 @@
       :rules="formRules"
       label-width="110px"
     >
-      <el-form-item label="加签处理人" prop="userIdList">
-        <el-select v-model="formData.userIdList" multiple clearable style="width: 100%">
+      <el-form-item label="加签处理人" prop="userIds">
+        <el-select v-model="formData.userIds" multiple clearable style="width: 100%">
           <el-option
             v-for="item in userList"
             :key="item.id"
@@ -36,18 +36,19 @@
 import * as TaskApi from '@/api/bpm/task'
 import * as UserApi from '@/api/system/user'
 
-const message = useMessage() // 消息弹窗
-defineOptions({ name: 'BpmTaskUpdateAssigneeForm' })
+defineOptions({ name: 'TaskSignCreateForm' })
 
+const message = useMessage() // 消息弹窗
 const dialogVisible = ref(false) // 弹窗的是否展示
 const formLoading = ref(false) // 表单的加载中
 const formData = ref({
   id: '',
-  userIdList: [],
-  type: ''
+  userIds: [],
+  type: '',
+  reason: ''
 })
 const formRules = ref({
-  userIdList: [{ required: true, message: '加签处理人不能为空', trigger: 'change' }],
+  userIds: [{ required: true, message: '加签处理人不能为空', trigger: 'change' }],
   reason: [{ required: true, message: '加签理由不能为空', trigger: 'change' }]
 })
 
@@ -75,7 +76,7 @@ const submitForm = async (type: string) => {
   formLoading.value = true
   formData.value.type = type
   try {
-    await TaskApi.taskAddSign(formData.value)
+    await TaskApi.signCreateTask(formData.value)
     message.success('加签成功')
     dialogVisible.value = false
     // 发送操作成功的事件
@@ -89,8 +90,9 @@ const submitForm = async (type: string) => {
 const resetForm = () => {
   formData.value = {
     id: '',
-    userIdList: [],
-    type: ''
+    userIds: [],
+    type: '',
+    reason: ''
   }
   formRef.value?.resetFields()
 }

+ 11 - 7
src/views/bpm/processInstance/detail/TaskSubSignDialogForm.vue → src/views/bpm/processInstance/detail/dialog/TaskSignDeleteForm.vue

@@ -9,8 +9,10 @@
     >
       <el-form-item label="减签任务" prop="id">
         <el-radio-group v-model="formData.id">
-          <el-radio-button v-for="item in subTaskList" :key="item.id" :label="item.id">
-            {{ item.name }}({{ item.assigneeUser.deptName }}{{ item.assigneeUser.nickname }}--审批)
+          <el-radio-button v-for="item in childrenTaskList" :key="item.id" :label="item.id">
+            {{ item.name }}
+            ({{ item.assigneeUser?.deptName || item.ownerUser?.deptName }} -
+            {{ item.assigneeUser?.nickname || item.ownerUser?.nickname }})
           </el-radio-button>
         </el-radio-group>
       </el-form-item>
@@ -24,10 +26,12 @@
     </template>
   </Dialog>
 </template>
-<script lang="ts" name="TaskRollbackDialogForm" setup>
+<script lang="ts" setup>
 import * as TaskApi from '@/api/bpm/task'
 import { isEmpty } from '@/utils/is'
 
+defineOptions({ name: 'TaskSignDeleteForm' })
+
 const message = useMessage() // 消息弹窗
 const dialogVisible = ref(false) // 弹窗的是否展示
 const formLoading = ref(false) // 表单的加载中
@@ -41,11 +45,11 @@ const formRules = ref({
 })
 
 const formRef = ref() // 表单 Ref
-const subTaskList = ref([])
+const childrenTaskList = ref([])
 /** 打开弹窗 */
 const open = async (id: string) => {
-  subTaskList.value = await TaskApi.getChildrenTaskList(id)
-  if (isEmpty(subTaskList.value)) {
+  childrenTaskList.value = await TaskApi.getChildrenTaskList(id)
+  if (isEmpty(childrenTaskList.value)) {
     message.warning('当前没有可减签的任务')
     return false
   }
@@ -64,7 +68,7 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    await TaskApi.taskSubSign(formData.value)
+    await TaskApi.signDeleteTask(formData.value)
     message.success('减签成功')
     dialogVisible.value = false
     // 发送操作成功的事件

+ 106 - 0
src/views/bpm/processInstance/detail/dialog/TaskSignList.vue

@@ -0,0 +1,106 @@
+<template>
+  <el-drawer v-model="drawerVisible" title="子任务" size="880px">
+    <!-- 当前任务 -->
+    <template #header>
+      <h4>【{{ parentTask.name }} 】审批人:{{ parentTask?.assigneeUser?.nickname }}</h4>
+      <el-button
+        style="margin-left: 5px"
+        v-if="isSignDeleteButtonVisible(parentTask)"
+        type="danger"
+        plain
+        @click="handleSignDelete(parentTask)"
+      >
+        <Icon icon="ep:remove" /> 减签
+      </el-button>
+    </template>
+    <!-- 子任务列表 -->
+    <el-table :data="parentTask.children" style="width: 100%" row-key="id" border>
+      <el-table-column prop="assigneeUser.nickname" label="审批人" min-width="100">
+        <template #default="scope">
+          {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="assigneeUser.deptName" label="所在部门" min-width="100">
+        <template #default="scope">
+          {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
+        </template>
+      </el-table-column>
+      <el-table-column 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
+        label="提交时间"
+        align="center"
+        prop="createTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column
+        label="结束时间"
+        align="center"
+        prop="endTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="操作" prop="operation" width="90">
+        <template #default="scope">
+          <el-button
+            v-if="isSignDeleteButtonVisible(scope.row)"
+            type="danger"
+            plain
+            size="small"
+            @click="handleSignDelete(scope.row)"
+          >
+            <Icon icon="ep:remove" /> 减签
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 减签 -->
+    <TaskSignDeleteForm ref="taskSignDeleteFormRef" @success="handleSignDeleteSuccess" />
+  </el-drawer>
+</template>
+<script lang="ts" setup>
+import { isEmpty } from '@/utils/is'
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import TaskSignDeleteForm from './TaskSignDeleteForm.vue'
+
+defineOptions({ name: 'TaskSignList' })
+
+const message = useMessage() // 消息弹窗
+const drawerVisible = ref(false) // 抽屉的是否展示
+const parentTask = ref({} as any)
+
+/** 打开弹窗 */
+const open = async (task: any) => {
+  if (isEmpty(task.children)) {
+    message.warning('该任务没有子任务')
+    return
+  }
+  parentTask.value = task
+  // 展开抽屉
+  drawerVisible.value = true
+}
+defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗
+
+/** 发起减签 */
+const taskSignDeleteFormRef = ref()
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const handleSignDelete = (item: any) => {
+  taskSignDeleteFormRef.value.open(item.id)
+}
+const handleSignDeleteSuccess = () => {
+  emit('success')
+  // 关闭抽屉
+  drawerVisible.value = false
+}
+
+/** 是否显示减签按钮 */
+const isSignDeleteButtonVisible = (task: any) => {
+  return task && task.children && !isEmpty(task.children)
+}
+</script>

+ 12 - 6
src/views/bpm/processInstance/detail/TaskUpdateAssigneeForm.vue → src/views/bpm/processInstance/detail/dialog/TaskTransferForm.vue

@@ -1,5 +1,5 @@
 <template>
-  <Dialog v-model="dialogVisible" title="转派审批人" width="500">
+  <Dialog v-model="dialogVisible" title="转派任务" width="500">
     <el-form
       ref="formRef"
       v-loading="formLoading"
@@ -17,6 +17,9 @@
           />
         </el-select>
       </el-form-item>
+      <el-form-item label="转派理由" prop="reason">
+        <el-input v-model="formData.reason" clearable placeholder="请输入转派理由" />
+      </el-form-item>
     </el-form>
     <template #footer>
       <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
@@ -28,16 +31,18 @@
 import * as TaskApi from '@/api/bpm/task'
 import * as UserApi from '@/api/system/user'
 
-defineOptions({ name: 'BpmTaskUpdateAssigneeForm' })
+defineOptions({ name: 'TaskTransferForm' })
 
 const dialogVisible = ref(false) // 弹窗的是否展示
 const formLoading = ref(false) // 表单的加载中
 const formData = ref({
   id: '',
-  assigneeUserId: undefined
+  assigneeUserId: undefined,
+  reason: ''
 })
 const formRules = ref({
-  assigneeUserId: [{ required: true, message: '新审批人不能为空', trigger: 'change' }]
+  assigneeUserId: [{ required: true, message: '新审批人不能为空', trigger: 'change' }],
+  reason: [{ required: true, message: '转派理由不能为空', trigger: 'blur' }]
 })
 
 const formRef = ref() // 表单 Ref
@@ -63,7 +68,7 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    await TaskApi.updateTaskAssignee(formData.value)
+    await TaskApi.transferTask(formData.value)
     dialogVisible.value = false
     // 发送操作成功的事件
     emit('success')
@@ -76,7 +81,8 @@ const submitForm = async () => {
 const resetForm = () => {
   formData.value = {
     id: '',
-    assigneeUserId: undefined
+    assigneeUserId: undefined,
+    reason: ''
   }
   formRef.value?.resetFields()
 }

+ 104 - 36
src/views/bpm/processInstance/detail/index.vue

@@ -21,9 +21,22 @@
             {{ processInstance.name }}
           </el-form-item>
           <el-form-item v-if="processInstance && processInstance.startUser" label="流程发起人">
-            {{ processInstance.startUser.nickname }}
-            <el-tag size="small" type="info">{{ processInstance.startUser.deptName }}</el-tag>
+            {{ processInstance?.startUser.nickname }}
+            <el-tag size="small" type="info">{{ processInstance?.startUser.deptName }}</el-tag>
           </el-form-item>
+          <el-card class="mb-15px !-mt-10px" v-if="runningTasks[index].formId > 0">
+            <template #header>
+              <span class="el-icon-picture-outline">
+                填写表单【{{ runningTasks[index]?.formName }}】
+              </span>
+            </template>
+            <form-create
+              v-model:api="approveFormFApis[index]"
+              v-model="approveForms[index].value"
+              :option="approveForms[index].option"
+              :rule="approveForms[index].rule"
+            />
+          </el-card>
           <el-form-item label="审批建议" prop="reason">
             <el-input
               v-model="auditForms[index].reason"
@@ -31,6 +44,16 @@
               type="textarea"
             />
           </el-form-item>
+          <el-form-item label="抄送人" prop="copyUserIds">
+            <el-select v-model="auditForms[index].copyUserIds" 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>
         <div style="margin-bottom: 20px; margin-left: 10%; font-size: 14px">
           <el-button type="success" @click="handleAudit(item, true)">
@@ -82,25 +105,30 @@
     </el-card>
 
     <!-- 审批记录 -->
-    <ProcessInstanceTaskList :loading="tasksLoad" :tasks="tasks" />
+    <ProcessInstanceTaskList
+      :loading="tasksLoad"
+      :process-instance="processInstance"
+      :tasks="tasks"
+      @refresh="getTaskList"
+    />
 
     <!-- 高亮流程图 -->
     <ProcessInstanceBpmnViewer
       :id="`${id}`"
-      :bpmn-xml="bpmnXML"
+      :bpmn-xml="bpmnXml"
       :loading="processInstanceLoading"
       :process-instance="processInstance"
       :tasks="tasks"
     />
 
     <!-- 弹窗:转派审批人 -->
-    <TaskUpdateAssigneeForm ref="taskUpdateAssigneeFormRef" @success="getDetail" />
-    <!-- 弹窗回退节点 -->
-    <TaskReturnDialog ref="taskReturnDialogRef" @success="getDetail" />
-    <!-- 委派,将任务委派给别人处理,处理完成后,会重新回到原审批人手中-->
+    <TaskTransferForm ref="taskTransferFormRef" @success="getDetail" />
+    <!-- 弹窗回退节点 -->
+    <TaskReturnForm ref="taskReturnFormRef" @success="getDetail" />
+    <!-- 弹窗:委派,将任务委派给别人处理,处理完成后,会重新回到原审批人手中-->
     <TaskDelegateForm ref="taskDelegateForm" @success="getDetail" />
-    <!-- 加签,当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 -->
-    <TaskAddSignDialogForm ref="taskAddSignDialogForm" @success="getDetail" />
+    <!-- 弹窗:加签,当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 -->
+    <TaskSignCreateForm ref="taskSignCreateFormRef" @success="getDetail" />
   </ContentWrap>
 </template>
 <script lang="ts" setup>
@@ -110,14 +138,15 @@ import type { ApiAttrs } from '@form-create/element-ui/types/config'
 import * as DefinitionApi from '@/api/bpm/definition'
 import * as ProcessInstanceApi from '@/api/bpm/processInstance'
 import * as TaskApi from '@/api/bpm/task'
-import TaskUpdateAssigneeForm from './TaskUpdateAssigneeForm.vue'
 import ProcessInstanceBpmnViewer from './ProcessInstanceBpmnViewer.vue'
 import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue'
-import TaskReturnDialog from './TaskReturnDialogForm.vue'
-import TaskDelegateForm from './TaskDelegateForm.vue'
-import TaskAddSignDialogForm from './TaskAddSignDialogForm.vue'
+import TaskReturnForm from './dialog/TaskReturnForm.vue'
+import TaskDelegateForm from './dialog/TaskDelegateForm.vue'
+import TaskTransferForm from './dialog/TaskTransferForm.vue'
+import TaskSignCreateForm from './dialog/TaskSignCreateForm.vue'
 import { registerComponent } from '@/utils/routerHelper'
 import { isEmpty } from '@/utils/is'
+import * as UserApi from '@/api/system/user'
 
 defineOptions({ name: 'BpmProcessInstanceDetail' })
 
@@ -126,10 +155,10 @@ const message = useMessage() // 消息弹窗
 const { proxy } = getCurrentInstance() as any
 
 const userId = useUserStore().getUser.id // 当前登录的编号
-const id = query.id as unknown as number // 流程实例的编号
+const id = query.id as unknown as string // 流程实例的编号
 const processInstanceLoading = ref(false) // 流程实例的加载中
 const processInstance = ref<any>({}) // 流程实例
-const bpmnXML = ref('') // BPMN XML
+const bpmnXml = ref('') // BPMN XML
 const tasksLoad = ref(true) // 任务的加载中
 const tasks = ref<any[]>([]) // 任务列表
 // ========== 审批信息 ==========
@@ -138,14 +167,30 @@ const auditForms = ref<any[]>([]) // 审批任务的表单
 const auditRule = reactive({
   reason: [{ required: true, message: '审批建议不能为空', trigger: 'blur' }]
 })
+const approveForms = ref<any[]>([]) // 审批通过时,额外的补充信息
+const approveFormFApis = ref<ApiAttrs[]>([]) // approveForms 的 fAPi
+
 // ========== 申请信息 ==========
 const fApi = ref<ApiAttrs>() //
 const detailForm = ref({
-  // 流程表单详情
   rule: [],
   option: {},
   value: {}
-})
+}) // 流程实例的表单详情
+
+/** 监听 approveFormFApis,实现它对应的 form-create 初始化后,隐藏掉对应的表单提交按钮 */
+watch(
+  () => approveFormFApis.value,
+  (value) => {
+    value?.forEach((api) => {
+      api.btn.show(false)
+      api.resetBtn.show(false)
+    })
+  },
+  {
+    deep: true
+  }
+)
 
 /** 处理审批通过和不通过的操作 */
 const handleAudit = async (task, pass) => {
@@ -161,9 +206,16 @@ const handleAudit = async (task, pass) => {
   // 2.1 提交审批
   const data = {
     id: task.id,
-    reason: auditForms.value[index].reason
+    reason: auditForms.value[index].reason,
+    copyUserIds: auditForms.value[index].copyUserIds
   }
   if (pass) {
+    // 审批通过,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交
+    const formCreateApi = approveFormFApis.value[index]
+    if (formCreateApi) {
+      await formCreateApi.validate()
+      data.variables = approveForms.value[index].value
+    }
     await TaskApi.approveTask(data)
     message.success('审批通过成功')
   } else {
@@ -175,28 +227,27 @@ const handleAudit = async (task, pass) => {
 }
 
 /** 转派审批人 */
-const taskUpdateAssigneeFormRef = ref()
+const taskTransferFormRef = ref()
 const openTaskUpdateAssigneeForm = (id: string) => {
-  taskUpdateAssigneeFormRef.value.open(id)
+  taskTransferFormRef.value.open(id)
 }
 
-const taskDelegateForm = ref()
 /** 处理审批退回的操作 */
+const taskDelegateForm = ref()
 const handleDelegate = async (task) => {
   taskDelegateForm.value.open(task.id)
 }
 
-//回退弹框组件
-const taskReturnDialogRef = ref()
 /** 处理审批退回的操作 */
-const handleBack = async (task) => {
-  taskReturnDialogRef.value.open(task.id)
+const taskReturnFormRef = ref()
+const handleBack = async (task: any) => {
+  taskReturnFormRef.value.open(task.id)
 }
 
-const taskAddSignDialogForm = ref()
 /** 处理审批加签的操作 */
-const handleSign = async (task) => {
-  taskAddSignDialogForm.value.open(task.id)
+const taskSignCreateFormRef = ref()
+const handleSign = async (task: any) => {
+  taskSignCreateFormRef.value.open(task.id)
 }
 
 /** 获得详情 */
@@ -239,7 +290,9 @@ const getProcessInstance = async () => {
     }
 
     // 加载流程图
-    bpmnXML.value = await DefinitionApi.getProcessDefinitionBpmnXML(processDefinition.id as number)
+    bpmnXml.value = (
+      await DefinitionApi.getProcessDefinition(processDefinition.id as number)
+    )?.bpmnXml
   } finally {
     processInstanceLoading.value = false
   }
@@ -247,6 +300,10 @@ const getProcessInstance = async () => {
 
 /** 加载任务列表 */
 const getTaskList = async () => {
+  runningTasks.value = []
+  auditForms.value = []
+  approveForms.value = []
+  approveFormFApis.value = []
   try {
     // 获得未取消的任务
     tasksLoad.value = true
@@ -254,7 +311,7 @@ const getTaskList = async () => {
     tasks.value = []
     // 1.1 移除已取消的审批
     data.forEach((task) => {
-      if (task.result !== 4) {
+      if (task.status !== 4) {
         tasks.value.push(task)
       }
     })
@@ -274,8 +331,6 @@ const getTaskList = async () => {
     })
 
     // 获得需要自己审批的任务
-    runningTasks.value = []
-    auditForms.value = []
     loadRunningTask(tasks.value)
   } finally {
     tasksLoad.value = false
@@ -291,7 +346,7 @@ const loadRunningTask = (tasks) => {
       loadRunningTask(task.children)
     }
     // 2.1 只有待处理才需要
-    if (task.result !== 1 && task.result !== 6) {
+    if (task.status !== 1 && task.status !== 6) {
       return
     }
     // 2.2 自己不是处理人
@@ -301,13 +356,26 @@ const loadRunningTask = (tasks) => {
     // 2.3 添加到处理任务
     runningTasks.value.push({ ...task })
     auditForms.value.push({
-      reason: ''
+      reason: '',
+      copyUserIds: []
     })
+
+    // 2.4 处理 approve 表单
+    if (task.formId && task.formConf) {
+      const approveForm = {}
+      setConfAndFields2(approveForm, task.formConf, task.formFields, task.formVariable)
+      approveForms.value.push(approveForm)
+    } else {
+      approveForms.value.push({}) // 占位,避免为空
+    }
   })
 }
 
 /** 初始化 */
-onMounted(() => {
+const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
+onMounted(async () => {
   getDetail()
+  // 获得用户列表
+  userOptions.value = await UserApi.getSimpleUserList()
 })
 </script>

+ 59 - 51
src/views/bpm/processInstance/index.vue

@@ -36,15 +36,20 @@
           class="!w-240px"
         >
           <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_CATEGORY)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
+            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-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"
@@ -53,17 +58,7 @@
           />
         </el-select>
       </el-form-item>
-      <el-form-item label="结果" prop="result">
-        <el-select v-model="queryParams.result" placeholder="请选择结果" clearable class="!w-240px">
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="提交时间" prop="createTime">
+      <el-form-item label="发起时间" prop="createTime">
         <el-date-picker
           v-model="queryParams.createTime"
           value-format="YYYY-MM-DD HH:mm:ss"
@@ -81,7 +76,7 @@
           type="primary"
           plain
           v-hasPermi="['bpm:process-instance:query']"
-          @click="handleCreate"
+          @click="handleCreate()"
         >
           <Icon icon="ep:plus" class="mr-5px" /> 发起流程
         </el-button>
@@ -92,34 +87,23 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
-      <el-table-column label="流程编号" align="center" prop="id" width="300px" />
-      <el-table-column label="流程名称" align="center" prop="name" />
-      <el-table-column label="流程分类" align="center" prop="category">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="scope.row.category" />
-        </template>
-      </el-table-column>
-      <el-table-column label="当前审批任务" align="center" prop="tasks">
-        <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="状态" prop="status">
+      <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="流程状态" 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="结果" prop="result">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.result" />
-        </template>
-      </el-table-column>
       <el-table-column
-        label="提交时间"
+        label="发起时间"
         align="center"
-        prop="createTime"
+        prop="startTime"
         width="180"
         :formatter="dateFormatter"
       />
@@ -130,7 +114,20 @@
         width="180"
         :formatter="dateFormatter"
       />
-      <el-table-column label="操作" align="center">
+      <el-table-column align="center" label="耗时" prop="durationInMillis" width="160">
+        <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
@@ -143,12 +140,15 @@
           <el-button
             link
             type="primary"
-            v-if="scope.row.result === 1"
+            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.id)">
+            重新发起
+          </el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -163,11 +163,12 @@
 </template>
 <script lang="ts" setup>
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { dateFormatter } from '@/utils/formatTime'
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
 import { ElMessageBox } from 'element-plus'
 import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import { CategoryApi } from '@/api/bpm/category'
 
-defineOptions({ name: 'BpmProcessInstance' })
+defineOptions({ name: 'BpmProcessInstanceMy' })
 
 const router = useRouter() // 路由
 const message = useMessage() // 消息弹窗
@@ -183,16 +184,16 @@ const queryParams = reactive({
   processDefinitionId: undefined,
   category: undefined,
   status: undefined,
-  result: undefined,
   createTime: []
 })
 const queryFormRef = ref() // 搜索的表单
+const categoryList = ref([]) // 流程分类列表
 
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {
-    const data = await ProcessInstanceApi.getMyProcessInstancePage(queryParams)
+    const data = await ProcessInstanceApi.getProcessInstanceMyPage(queryParams)
     list.value = data.list
     total.value = data.total
   } finally {
@@ -213,9 +214,10 @@ const resetQuery = () => {
 }
 
 /** 发起流程操作 **/
-const handleCreate = () => {
+const handleCreate = (id) => {
   router.push({
-    name: 'BpmProcessInstanceCreate'
+    name: 'BpmProcessInstanceCreate',
+    query: { processInstanceId: id }
   })
 }
 
@@ -239,14 +241,20 @@ const handleCancel = async (row) => {
     inputErrorMessage: '取消原因不能为空'
   })
   // 发起取消
-  await ProcessInstanceApi.cancelProcessInstance(row.id, value)
+  await ProcessInstanceApi.cancelProcessInstanceByStartUser(row.id, value)
   message.success('取消成功')
   // 刷新列表
   await getList()
 }
 
-/** 初始化 **/
-onMounted(() => {
+/** 激活时 **/
+onActivated(() => {
   getList()
 })
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+  categoryList.value = await CategoryApi.getCategorySimpleList()
+})
 </script>

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

@@ -0,0 +1,255 @@
+<template>
+  <doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
+
+  <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-220px"
+        />
+      </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>

+ 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"
+            :label="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>

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

@@ -0,0 +1,183 @@
+<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>

+ 28 - 0
src/views/bpm/simpleWorkflow/index.vue

@@ -0,0 +1,28 @@
+<template>
+  <div>
+    <section class="dingflow-design">
+      <div class="box-scale">
+        <nodeWrap v-model:nodeConfig="nodeConfig" />
+        <div class="end-node">
+          <div class="end-node-circle"></div>
+          <div class="end-node-text">流程结束</div>
+        </div>
+      </div>
+    </section>
+  </div>
+</template>
+<script lang="ts" setup>
+import nodeWrap from '@/components/SimpleProcessDesigner/src/nodeWrap.vue'
+defineOptions({ name: 'SimpleWorkflowDesignEditor' })
+let nodeConfig = ref({
+  nodeName: '发起人',
+  type: 0,
+  id: 'root',
+  formPerms: {},
+  nodeUserList: [],
+  childNode: {}
+})
+</script>
+<style>
+@import url('@/components/SimpleProcessDesigner/theme/workflow.css');
+</style>

+ 17 - 20
src/views/bpm/task/cc/index.vue → src/views/bpm/task/copy/index.vue

@@ -11,14 +11,6 @@
           placeholder="请输入流程名称"
         />
       </el-form-item>
-      <el-form-item label="所属流程" prop="processDefinitionId">
-        <el-input
-          v-model="queryParams.processInstanceId"
-          placeholder="请输入流程定义的编号"
-          clearable
-          class="!w-240px"
-        />
-      </el-form-item>
       <el-form-item label="抄送时间" prop="createTime">
         <el-date-picker
           v-model="queryParams.createTime"
@@ -46,12 +38,17 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
-      <el-table-column align="center" label="所属流程" prop="processInstanceId" width="300px" />
-      <el-table-column align="center" label="流程名称" prop="processInstanceName" />
-      <el-table-column align="center" label="任务名称" prop="taskName" />
-      <el-table-column align="center" label="流程发起人" prop="startUserNickname" />
-      <el-table-column align="center" label="抄送发起人" prop="creatorNickname" />
-      <el-table-column align="center" label="抄送原因" prop="reason" />
+      <el-table-column align="center" label="流程名" prop="processInstanceName" min-width="180" />
+      <el-table-column align="center" label="流程发起人" prop="startUserName" min-width="100" />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="流程发起时间"
+        prop="processInstanceStartTime"
+        width="180"
+      />
+      <el-table-column align="center" label="抄送任务" prop="taskName" min-width="180" />
+      <el-table-column align="center" label="抄送人" prop="creatorName" min-width="100" />
       <el-table-column
         align="center"
         label="抄送时间"
@@ -59,9 +56,9 @@
         width="180"
         :formatter="dateFormatter"
       />
-      <el-table-column align="center" label="操作">
+      <el-table-column align="center" label="操作" fixed="right" width="80">
         <template #default="scope">
-          <el-button link type="primary" @click="handleAudit(scope.row)">跳转待办</el-button>
+          <el-button link type="primary" @click="handleAudit(scope.row)">详情</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -78,14 +75,14 @@
 import { dateFormatter } from '@/utils/formatTime'
 import * as ProcessInstanceApi from '@/api/bpm/processInstance'
 
-defineOptions({ name: 'BpmCCProcessInstance' })
+defineOptions({ name: 'BpmProcessInstanceCopy' })
 
 const { push } = useRouter() // 路由
 
 const loading = ref(false) // 列表的加载中
 const total = ref(0) // 列表的总页数
 const list = ref([]) // 列表的数据
-const queryParams = ref({
+const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   processInstanceId: '',
@@ -98,7 +95,7 @@ const queryFormRef = ref() // 搜索的表单
 const getList = async () => {
   loading.value = true
   try {
-    const data = await ProcessInstanceApi.getProcessInstanceCCPage(queryParams)
+    const data = await ProcessInstanceApi.getProcessInstanceCopyPage(queryParams)
     list.value = data.list
     total.value = data.total
   } finally {
@@ -118,7 +115,7 @@ const handleAudit = (row: any) => {
 
 /** 搜索按钮操作 */
 const handleQuery = () => {
-  queryParams.value.pageNo = 1
+  queryParams.pageNo = 1
   getList()
 }
 

+ 0 - 51
src/views/bpm/task/done/TaskDetail.vue

@@ -1,51 +0,0 @@
-<template>
-  <Dialog v-model="dialogVisible" :max-height="500" :scroll="true" title="详情">
-    <el-descriptions :column="1" border>
-      <el-descriptions-item label="任务编号" min-width="120">
-        {{ detailData.id }}
-      </el-descriptions-item>
-      <el-descriptions-item label="任务名称">
-        {{ detailData.name }}
-      </el-descriptions-item>
-      <el-descriptions-item label="所属流程">
-        {{ detailData.processInstance.name }}
-      </el-descriptions-item>
-      <el-descriptions-item label="流程发起人">
-        {{ detailData.processInstance.startUserNickname }}
-      </el-descriptions-item>
-      <el-descriptions-item label="状态">
-        <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="detailData.result" />
-      </el-descriptions-item>
-      <el-descriptions-item label="原因">
-        {{ detailData.reason }}
-      </el-descriptions-item>
-      <el-descriptions-item label="创建时间">
-        {{ formatDate(detailData.createTime) }}
-      </el-descriptions-item>
-    </el-descriptions>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-import { DICT_TYPE } from '@/utils/dict'
-import { formatDate } from '@/utils/formatTime'
-import * as TaskApi from '@/api/bpm/task'
-
-defineOptions({ name: 'BpmTaskDetail' })
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const detailLoading = ref(false) // 表单的加载中
-const detailData = ref() // 详情数据
-
-/** 打开弹窗 */
-const open = async (data: TaskApi.TaskVO) => {
-  dialogVisible.value = true
-  // 设置数据
-  detailLoading.value = true
-  try {
-    detailData.value = data
-  } finally {
-    detailLoading.value = false
-  }
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-</script>

+ 41 - 27
src/views/bpm/task/done/index.vue

@@ -46,27 +46,51 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
-      <el-table-column align="center" label="任务编号" prop="id" width="300px" />
-      <el-table-column align="center" label="任务名称" prop="name" />
-      <el-table-column align="center" label="所属流程" prop="processInstance.name" />
-      <el-table-column align="center" label="流程发起人" prop="processInstance.startUserNickname" />
-      <el-table-column align="center" label="状态" prop="result">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.result" />
-        </template>
-      </el-table-column>
-      <el-table-column align="center" label="原因" prop="reason" />
+      <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="创建时间"
+        label="发起时间"
         prop="createTime"
         width="180"
       />
-      <el-table-column align="center" label="操作">
+      <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">
-          <el-button link type="primary" @click="openDetail(scope.row)">详情</el-button>
-          <el-button link type="primary" @click="handleAudit(scope.row)">流程</el-button>
+          {{ formatPast2(scope.row.durationInMillis) }}
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="流程编号" prop="id" :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>
@@ -78,15 +102,11 @@
       @pagination="getList"
     />
   </ContentWrap>
-
-  <!-- 表单弹窗:详情 -->
-  <TaskDetail ref="detailRef" @success="getList" />
 </template>
 <script lang="ts" setup>
 import { DICT_TYPE } from '@/utils/dict'
-import { dateFormatter } from '@/utils/formatTime'
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
 import * as TaskApi from '@/api/bpm/task'
-import TaskDetail from './TaskDetail.vue'
 
 defineOptions({ name: 'BpmTodoTask' })
 
@@ -107,7 +127,7 @@ const queryFormRef = ref() // 搜索的表单
 const getList = async () => {
   loading.value = true
   try {
-    const data = await TaskApi.getDoneTaskPage(queryParams)
+    const data = await TaskApi.getTaskDonePage(queryParams)
     list.value = data.list
     total.value = data.total
   } finally {
@@ -127,14 +147,8 @@ const resetQuery = () => {
   handleQuery()
 }
 
-/** 详情操作 */
-const detailRef = ref()
-const openDetail = (row: TaskApi.TaskVO) => {
-  detailRef.value.open(row)
-}
-
 /** 处理审批按钮 */
-const handleAudit = (row) => {
+const handleAudit = (row: any) => {
   push({
     name: 'BpmProcessInstanceDetail',
     query: {

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

@@ -0,0 +1,166 @@
+<template>
+  <doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
+
+  <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="id" :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>

+ 23 - 25
src/views/bpm/task/todo/index.vue

@@ -46,27 +46,33 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
-      <el-table-column align="center" label="任务编号" prop="id" width="300px" />
-      <el-table-column align="center" label="任务名称" prop="name" />
-      <el-table-column align="center" label="所属流程" prop="processInstance.name" />
-      <el-table-column align="center" label="流程发起人" prop="processInstance.startUserNickname" />
+      <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="创建时间"
+        label="发起时间"
         prop="createTime"
         width="180"
       />
-      <el-table-column label="任务状态" prop="suspensionState">
-        <template #default="scope">
-          <el-tag v-if="scope.row.suspensionState === 1" type="success">激活</el-tag>
-          <el-tag v-if="scope.row.suspensionState === 2" type="warning">挂起</el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column align="center" label="操作">
+      <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="id" :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>
-          <el-button link type="primary" @click="handleCC(scope.row)">抄送</el-button>
+          <el-button link type="primary" @click="handleAudit(scope.row)">办理</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -77,16 +83,14 @@
       :total="total"
       @pagination="getList"
     />
-    <TaskCCDialogForm ref="taskCCDialogForm" />
   </ContentWrap>
 </template>
 
 <script lang="ts" setup>
 import { dateFormatter } from '@/utils/formatTime'
 import * as TaskApi from '@/api/bpm/task'
-import TaskCCDialogForm from '../../processInstance/detail/TaskCCDialogForm.vue'
 
-defineOptions({ name: 'BpmDoneTask' })
+defineOptions({ name: 'BpmTodoTask' })
 
 const { push } = useRouter() // 路由
 
@@ -105,7 +109,7 @@ const queryFormRef = ref() // 搜索的表单
 const getList = async () => {
   loading.value = true
   try {
-    const data = await TaskApi.getTodoTaskPage(queryParams)
+    const data = await TaskApi.getTaskTodoPage(queryParams)
     list.value = data.list
     total.value = data.total
   } finally {
@@ -126,7 +130,7 @@ const resetQuery = () => {
 }
 
 /** 处理审批按钮 */
-const handleAudit = (row) => {
+const handleAudit = (row: any) => {
   push({
     name: 'BpmProcessInstanceDetail',
     query: {
@@ -135,12 +139,6 @@ const handleAudit = (row) => {
   })
 }
 
-const taskCCDialogForm = ref()
-/** 处理抄送按钮 */
-const handleCC = (row) => {
-  taskCCDialogForm.value.open(row)
-}
-
 /** 初始化 **/
 onMounted(() => {
   getList()

+ 0 - 250
src/views/bpm/taskAssignRule/TaskAssignRuleForm.vue

@@ -1,250 +0,0 @@
-<template>
-  <Dialog v-model="dialogVisible" title="修改任务规则" width="600">
-    <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
-      <el-form-item label="任务名称" prop="taskDefinitionName">
-        <el-input v-model="formData.taskDefinitionName" disabled placeholder="请输入流标标识" />
-      </el-form-item>
-      <el-form-item label="任务标识" prop="taskDefinitionKey">
-        <el-input v-model="formData.taskDefinitionKey" disabled placeholder="请输入任务标识" />
-      </el-form-item>
-      <el-form-item label="规则类型" prop="type">
-        <el-select v-model="formData.type" clearable style="width: 100%">
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_RULE_TYPE)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item v-if="formData.type === 10" label="指定角色" prop="roleIds">
-        <el-select v-model="formData.roleIds" clearable multiple style="width: 100%">
-          <el-option
-            v-for="item in roleOptions"
-            :key="item.id"
-            :label="item.name"
-            :value="item.id"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item
-        v-if="formData.type === 20 || formData.type === 21"
-        label="指定部门"
-        prop="deptIds"
-        span="24"
-      >
-        <el-tree-select
-          ref="treeRef"
-          v-model="formData.deptIds"
-          :data="deptTreeOptions"
-          :props="defaultProps"
-          empty-text="加载中,请稍后"
-          multiple
-          node-key="id"
-          show-checkbox
-        />
-      </el-form-item>
-      <el-form-item v-if="formData.type === 22" label="指定岗位" prop="postIds" span="24">
-        <el-select v-model="formData.postIds" clearable multiple style="width: 100%">
-          <el-option
-            v-for="item in postOptions"
-            :key="parseInt(item.id)"
-            :label="item.name"
-            :value="parseInt(item.id)"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item
-        v-if="formData.type === 30 || formData.type === 31 || formData.type === 32"
-        label="指定用户"
-        prop="userIds"
-        span="24"
-      >
-        <el-select v-model="formData.userIds" clearable multiple style="width: 100%">
-          <el-option
-            v-for="item in userOptions"
-            :key="parseInt(item.id)"
-            :label="item.nickname"
-            :value="parseInt(item.id)"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item v-if="formData.type === 40" label="指定用户组" prop="userGroupIds">
-        <el-select v-model="formData.userGroupIds" clearable multiple style="width: 100%">
-          <el-option
-            v-for="item in userGroupOptions"
-            :key="parseInt(item.id)"
-            :label="item.name"
-            :value="parseInt(item.id)"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item v-if="formData.type === 50" label="指定脚本" prop="scripts">
-        <el-select v-model="formData.scripts" clearable multiple style="width: 100%">
-          <el-option
-            v-for="dict in taskAssignScriptDictDatas"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </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 { defaultProps, handleTree } from '@/utils/tree'
-import * as TaskAssignRuleApi from '@/api/bpm/taskAssignRule'
-import * as RoleApi from '@/api/system/role'
-import * as DeptApi from '@/api/system/dept'
-import * as PostApi from '@/api/system/post'
-import * as UserApi from '@/api/system/user'
-import * as UserGroupApi from '@/api/bpm/userGroup'
-
-defineOptions({ name: 'BpmTaskAssignRuleForm' })
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formData = ref({
-  type: Number(undefined),
-  modelId: '',
-  options: [],
-  roleIds: [],
-  deptIds: [],
-  postIds: [],
-  userIds: [],
-  userGroupIds: [],
-  scripts: []
-})
-const formRules = reactive({
-  type: [{ required: true, message: '规则类型不能为空', trigger: 'change' }],
-  roleIds: [{ required: true, message: '指定角色不能为空', trigger: 'change' }],
-  deptIds: [{ required: true, message: '指定部门不能为空', trigger: 'change' }],
-  postIds: [{ required: true, message: '指定岗位不能为空', trigger: 'change' }],
-  userIds: [{ required: true, message: '指定用户不能为空', trigger: 'change' }],
-  userGroupIds: [{ required: true, message: '指定用户组不能为空', trigger: 'change' }],
-  scripts: [{ required: true, message: '指定脚本不能为空', trigger: 'change' }]
-})
-const formRef = ref() // 表单 Ref
-const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
-const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表
-const deptTreeOptions = ref() // 部门树
-const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表
-const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
-const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
-const taskAssignScriptDictDatas = getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_SCRIPT)
-
-/** 打开弹窗 */
-const open = async (modelId: string, row: TaskAssignRuleApi.TaskAssignVO) => {
-  // 1. 先重置表单
-  resetForm()
-  // 2. 再设置表单
-  formData.value = {
-    ...row,
-    modelId: modelId,
-    options: [],
-    roleIds: [],
-    deptIds: [],
-    postIds: [],
-    userIds: [],
-    userGroupIds: [],
-    scripts: []
-  }
-  // 将 options 赋值到对应的 roleIds 等选项
-  if (row.type === 10) {
-    formData.value.roleIds.push(...row.options)
-  } else if (row.type === 20 || row.type === 21) {
-    formData.value.deptIds.push(...row.options)
-  } else if (row.type === 22) {
-    formData.value.postIds.push(...row.options)
-  } else if (row.type === 30 || row.type === 31 || row.type === 32) {
-    formData.value.userIds.push(...row.options)
-  } else if (row.type === 40) {
-    formData.value.userGroupIds.push(...row.options)
-  } else if (row.type === 50) {
-    formData.value.scripts.push(...row.options)
-  }
-  // 打开弹窗
-  dialogVisible.value = true
-
-  // 获得角色列表
-  roleOptions.value = await RoleApi.getSimpleRoleList()
-  // 获得部门列表
-  deptOptions.value = await DeptApi.getSimpleDeptList()
-  deptTreeOptions.value = handleTree(deptOptions.value, 'id')
-  // 获得岗位列表
-  postOptions.value = await PostApi.getSimplePostList()
-  // 获得用户列表
-  userOptions.value = await UserApi.getSimpleUserList()
-  // 获得用户组列表
-  userGroupOptions.value = await UserGroupApi.getSimpleUserGroupList()
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
-
-  // 构建表单
-  const form = {
-    ...formData.value,
-    taskDefinitionName: undefined
-  }
-  // 将 roleIds 等选项赋值到 options 中
-  if (form.type === 10) {
-    form.options = form.roleIds
-  } else if (form.type === 20 || form.type === 21) {
-    form.options = form.deptIds
-  } else if (form.type === 22) {
-    form.options = form.postIds
-  } else if (form.type === 30 || form.type === 31 || form.type === 32) {
-    form.options = form.userIds
-  } else if (form.type === 40) {
-    form.options = form.userGroupIds
-  } else if (form.type === 50) {
-    form.options = form.scripts
-  }
-  form.roleIds = undefined
-  form.deptIds = undefined
-  form.postIds = undefined
-  form.userIds = undefined
-  form.userGroupIds = undefined
-  form.scripts = undefined
-
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = form as unknown as TaskAssignRuleApi.TaskAssignVO
-    if (!data.id) {
-      await TaskAssignRuleApi.createTaskAssignRule(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await TaskAssignRuleApi.updateTaskAssignRule(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formRef.value?.resetFields()
-}
-</script>

+ 0 - 136
src/views/bpm/taskAssignRule/index.vue

@@ -1,136 +0,0 @@
-<template>
-  <ContentWrap>
-    <el-table v-loading="loading" :data="list">
-      <el-table-column label="任务名" align="center" prop="taskDefinitionName" />
-      <el-table-column label="任务标识" align="center" prop="taskDefinitionKey" />
-      <el-table-column label="规则类型" align="center" prop="type">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.BPM_TASK_ASSIGN_RULE_TYPE" :value="scope.row.type" />
-        </template>
-      </el-table-column>
-      <el-table-column label="规则范围" align="center" prop="options">
-        <template #default="scope">
-          <el-tag class="mr-5px" :key="option" v-for="option in scope.row.options">
-            {{ getAssignRuleOptionName(scope.row.type, option) }}
-          </el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column v-if="queryParams.modelId" label="操作" align="center">
-        <template #default="scope">
-          <el-button
-            link
-            type="primary"
-            @click="openForm(scope.row)"
-            v-hasPermi="['bpm:task-assign-rule:update']"
-          >
-            修改
-          </el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-  </ContentWrap>
-  <!-- 添加/修改弹窗 -->
-  <TaskAssignRuleForm ref="formRef" @success="getList" />
-</template>
-<script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import * as TaskAssignRuleApi from '@/api/bpm/taskAssignRule'
-import * as RoleApi from '@/api/system/role'
-import * as DeptApi from '@/api/system/dept'
-import * as PostApi from '@/api/system/post'
-import * as UserApi from '@/api/system/user'
-import * as UserGroupApi from '@/api/bpm/userGroup'
-import TaskAssignRuleForm from './TaskAssignRuleForm.vue'
-
-defineOptions({ name: 'BpmTaskAssignRule' })
-
-const { query } = useRoute() // 查询参数
-
-const loading = ref(true) // 列表的加载中
-const list = ref([]) // 列表的数据
-const queryParams = reactive({
-  modelId: query.modelId, // 流程模型的编号。如果 modelId 非空,则用于流程模型的查看与配置
-  processDefinitionId: query.processDefinitionId // 流程定义的编号。如果 processDefinitionId 非空,则用于流程定义的查看,不支持配置
-})
-const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
-const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表
-const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表
-const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
-const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
-const taskAssignScriptDictDatas = getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_SCRIPT)
-
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    list.value = await TaskAssignRuleApi.getTaskAssignRuleList(queryParams)
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 翻译规则范围 */
-// TODO 芋艿:各种 ts 报错
-const getAssignRuleOptionName = (type, option) => {
-  if (type === 10) {
-    for (const roleOption of roleOptions.value) {
-      if (roleOption.id === option) {
-        return roleOption.name
-      }
-    }
-  } else if (type === 20 || type === 21) {
-    for (const deptOption of deptOptions.value) {
-      if (deptOption.id === option) {
-        return deptOption.name
-      }
-    }
-  } else if (type === 22) {
-    for (const postOption of postOptions.value) {
-      if (postOption.id === option) {
-        return postOption.name
-      }
-    }
-  } else if (type === 30 || type === 31 || type === 32) {
-    for (const userOption of userOptions.value) {
-      if (userOption.id === option) {
-        return userOption.nickname
-      }
-    }
-  } else if (type === 40) {
-    for (const userGroupOption of userGroupOptions.value) {
-      if (userGroupOption.id === option) {
-        return userGroupOption.name
-      }
-    }
-  } else if (type === 50) {
-    option = option + '' // 转换成 string
-    for (const dictData of taskAssignScriptDictDatas) {
-      if (dictData.value === option) {
-        return dictData.label
-      }
-    }
-  }
-  return '未知(' + option + ')'
-}
-
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (row: TaskAssignRuleApi.TaskAssignVO) => {
-  formRef.value.open(queryParams.modelId, row)
-}
-
-/** 初始化 */
-onMounted(async () => {
-  await getList()
-  // 获得角色列表
-  roleOptions.value = await RoleApi.getSimpleRoleList()
-  // 获得部门列表
-  deptOptions.value = await DeptApi.getSimpleDeptList()
-  // 获得岗位列表
-  postOptions.value = await PostApi.getSimplePostList()
-  // 获得用户列表
-  userOptions.value = await UserApi.getSimpleUserList()
-  // 获得用户组列表
-  userGroupOptions.value = await UserGroupApi.getSimpleUserGroupList()
-})
-</script>

+ 11 - 11
src/views/crm/business/detail/index.vue

@@ -4,8 +4,8 @@
       编辑
     </el-button>
     <el-button
-      :disabled="business.endStatus"
       v-if="permissionListRef?.validateWrite"
+      :disabled="business.endStatus"
       type="success"
       @click="openStatusForm()"
     >
@@ -53,13 +53,12 @@
   </el-col>
 
   <!-- 表单弹窗:添加/修改 -->
-  <BusinessForm ref="formRef" @success="getBusiness(business.id)" />
-  <BusinessUpdateStatusForm ref="statusFormRef" @success="getBusiness(business.id)" />
-  <CrmTransferForm ref="transferFormRef" @success="close" />
+  <BusinessForm ref="formRef" @success="getBusiness" />
+  <BusinessUpdateStatusForm ref="statusFormRef" @success="getBusiness" />
+  <CrmTransferForm ref="transferFormRef" :biz-type="BizTypeEnum.CRM_BUSINESS" @success="close" />
 </template>
 <script lang="ts" setup>
 import { useTagsViewStore } from '@/store/modules/tagsView'
-import * as ContactApi from '@/api/crm/contact'
 import * as BusinessApi from '@/api/crm/business'
 import BusinessDetailsHeader from './BusinessDetailsHeader.vue'
 import BusinessDetailsInfo from './BusinessDetailsInfo.vue'
@@ -73,6 +72,7 @@ import FollowUpList from '@/views/crm/followup/index.vue'
 import ContactList from '@/views/crm/contact/components/ContactList.vue'
 import BusinessUpdateStatusForm from '@/views/crm/business/BusinessUpdateStatusForm.vue'
 import ContractList from '@/views/crm/contract/components/ContractList.vue'
+import BusinessProductList from '@/views/crm/business/detail/BusinessProductList.vue'
 
 defineOptions({ name: 'CrmBusinessDetail' })
 
@@ -80,15 +80,15 @@ const message = useMessage()
 
 const businessId = ref(0) // 线索编号
 const loading = ref(true) // 加载中
-const business = ref<ContactApi.ContactVO>({} as ContactApi.ContactVO) // 联系人详情
+const business = ref<BusinessApi.BusinessVO>({} as BusinessApi.BusinessVO) // 商机详情
 const permissionListRef = ref<InstanceType<typeof PermissionList>>() // 团队成员列表 Ref
 
 /** 获取详情 */
-const getBusiness = async (id: number) => {
+const getBusiness = async () => {
   loading.value = true
   try {
-    business.value = await BusinessApi.getBusiness(id)
-    await getOperateLog(id)
+    business.value = await BusinessApi.getBusiness(businessId.value)
+    await getOperateLog(businessId.value)
   } finally {
     loading.value = false
   }
@@ -109,7 +109,7 @@ const openStatusForm = () => {
 /** 联系人转移 */
 const transferFormRef = ref<InstanceType<typeof CrmTransferForm>>() // 联系人转移表单 ref
 const transfer = () => {
-  transferFormRef.value?.open('商机转移', business.value.id, BusinessApi.transferBusiness)
+  transferFormRef.value?.open(business.value.id)
 }
 
 /** 获取操作日志 */
@@ -141,6 +141,6 @@ onMounted(async () => {
     return
   }
   businessId.value = params.id as unknown as number
-  await getBusiness(businessId.value)
+  await getBusiness()
 })
 </script>

+ 3 - 3
src/views/crm/clue/detail/index.vue

@@ -18,7 +18,7 @@
     >
       转化为客户
     </el-button>
-    <el-button v-else type="success" disabled>已转化客户</el-button>
+    <el-button v-else disabled type="success">已转化客户</el-button>
   </ClueDetailsHeader>
   <el-col>
     <el-tabs>
@@ -45,7 +45,7 @@
 
   <!-- 表单弹窗:添加/修改 -->
   <ClueForm ref="formRef" @success="getClue" />
-  <CrmTransferForm ref="transferFormRef" @success="close" />
+  <CrmTransferForm ref="transferFormRef" :biz-type="BizTypeEnum.CRM_CLUE" @success="close" />
 </template>
 <script lang="ts" setup>
 import { useTagsViewStore } from '@/store/modules/tagsView'
@@ -91,7 +91,7 @@ const openForm = () => {
 /** 线索转移 */
 const transferFormRef = ref<InstanceType<typeof CrmTransferForm>>() // 线索转移表单 ref
 const transfer = () => {
-  transferFormRef.value?.open('线索转移', clueId.value, ClueApi.transferClue)
+  transferFormRef.value?.open(clueId.value)
 }
 
 /** 转化为客户 */

+ 24 - 21
src/views/crm/contact/components/ContactList.vue

@@ -6,18 +6,20 @@
       创建联系人
     </el-button>
     <el-button
-      @click="openBusinessModal"
-      v-hasPermi="['crm:contact:create-business']"
       v-if="queryParams.businessId"
+      v-hasPermi="['crm:contact:create-business']"
+      @click="openBusinessModal"
     >
-      <Icon class="mr-5px" icon="ep:circle-plus" />关联
+      <Icon class="mr-5px" icon="ep:circle-plus" />
+      关联
     </el-button>
     <el-button
-      @click="deleteContactBusinessList"
-      v-hasPermi="['crm:contact:delete-business']"
       v-if="queryParams.businessId"
+      v-hasPermi="['crm:contact:delete-business']"
+      @click="deleteContactBusinessList"
     >
-      <Icon class="mr-5px" icon="ep:remove" />解除关联
+      <Icon class="mr-5px" icon="ep:remove" />
+      解除关联
     </el-button>
   </el-row>
 
@@ -27,21 +29,21 @@
       ref="contactRef"
       v-loading="loading"
       :data="list"
-      :stripe="true"
       :show-overflow-tooltip="true"
+      :stripe="true"
     >
-      <el-table-column type="selection" width="55" v-if="queryParams.businessId" />
-      <el-table-column label="姓名" fixed="left" align="center" prop="name">
+      <el-table-column v-if="queryParams.businessId" type="selection" width="55" />
+      <el-table-column align="center" fixed="left" label="姓名" prop="name">
         <template #default="scope">
-          <el-link type="primary" :underline="false" @click="openDetail(scope.row.id)">
+          <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
             {{ scope.row.name }}
           </el-link>
         </template>
       </el-table-column>
-      <el-table-column label="手机号" align="center" prop="mobile" />
-      <el-table-column label="职位" align="center" prop="post" />
-      <el-table-column label="直属上级" align="center" prop="parentName" />
-      <el-table-column label="是否关键决策人" align="center" prop="master" min-width="100">
+      <el-table-column align="center" label="手机号" prop="mobile" />
+      <el-table-column align="center" label="职位" prop="post" />
+      <el-table-column align="center" label="直属上级" prop="parentName" />
+      <el-table-column align="center" label="是否关键决策人" min-width="100" prop="master">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.master" />
         </template>
@@ -49,9 +51,9 @@
     </el-table>
     <!-- 分页 -->
     <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
       v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
       @pagination="getList"
     />
   </ContentWrap>
@@ -60,12 +62,13 @@
   <ContactForm ref="formRef" @success="getList" />
   <!-- 关联商机选择弹框 -->
   <ContactListModal
+    v-if="customerId"
     ref="contactModalRef"
-    :customer-id="props.customerId"
+    :customer-id="customerId"
     @success="createContactBusinessList"
   />
 </template>
-<script setup lang="ts">
+<script lang="ts" setup>
 import * as ContactApi from '@/api/crm/contact'
 import ContactForm from './../ContactForm.vue'
 import { DICT_TYPE } from '@/utils/dict'
@@ -76,8 +79,8 @@ defineOptions({ name: 'CrmContactList' })
 const props = defineProps<{
   bizType: number // 业务类型
   bizId: number // 业务编号
-  customerId: number // 特殊:客户编号;在【商机】详情中,可以传递客户编号,默认新建的联系人关联到该客户
-  businessId: number // 特殊:商机编号;在【商机】详情中,可以传递商机编号,默认新建的联系人关联到该商机
+  customerId?: number // 特殊:客户编号;在【商机】详情中,可以传递客户编号,默认新建的联系人关联到该客户
+  businessId?: number // 特殊:商机编号;在【商机】详情中,可以传递商机编号,默认新建的联系人关联到该商机
 }>()
 
 const loading = ref(true) // 列表的加载中
@@ -147,7 +150,7 @@ const createContactBusinessList = async (contactIds: number[]) => {
     contactIds: contactIds
   } as ContactApi.ContactBusiness2ReqVO
   contactRef.value.getSelectionRows().forEach((row: ContactApi.ContactVO) => {
-    data.businessIds.push(row.id)
+    data.contactIds.push(row.id)
   })
   await ContactApi.createContactBusinessList2(data)
   // 刷新列表

+ 28 - 22
src/views/crm/contact/components/ContactListModal.vue

@@ -1,28 +1,35 @@
 <template>
-  <Dialog title="关联联系人" v-model="dialogVisible">
+  <Dialog v-model="dialogVisible" title="关联联系人">
     <!-- 搜索工作栏 -->
     <ContentWrap>
       <el-form
-        class="-mb-15px"
-        :model="queryParams"
         ref="queryFormRef"
         :inline="true"
+        :model="queryParams"
+        class="-mb-15px"
         label-width="90px"
       >
         <el-form-item label="联系人名称" prop="name">
           <el-input
             v-model="queryParams.name"
-            placeholder="请输入联系人名称"
+            class="!w-240px"
             clearable
+            placeholder="请输入联系人名称"
             @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-button type="primary" @click="openForm()" v-hasPermi="['crm:business:create']">
-            <Icon icon="ep:plus" class="mr-5px" /> 新增
+          <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="['crm:business:create']" type="primary" @click="openForm()">
+            <Icon class="mr-5px" icon="ep:plus" />
+            新增
           </el-button>
         </el-form-item>
       </el-form>
@@ -31,24 +38,24 @@
     <!-- 列表 -->
     <ContentWrap class="mt-10px">
       <el-table
-        v-loading="loading"
         ref="contactRef"
+        v-loading="loading"
         :data="list"
-        :stripe="true"
         :show-overflow-tooltip="true"
+        :stripe="true"
       >
         <el-table-column type="selection" width="55" />
-        <el-table-column label="姓名" fixed="left" align="center" prop="name">
+        <el-table-column align="center" fixed="left" label="姓名" prop="name">
           <template #default="scope">
-            <el-link type="primary" :underline="false" @click="openDetail(scope.row.id)">
+            <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
               {{ scope.row.name }}
             </el-link>
           </template>
         </el-table-column>
-        <el-table-column label="手机号" align="center" prop="mobile" />
-        <el-table-column label="职位" align="center" prop="post" />
-        <el-table-column label="直属上级" align="center" prop="parentName" />
-        <el-table-column label="是否关键决策人" align="center" prop="master" min-width="100">
+        <el-table-column align="center" label="手机号" prop="mobile" />
+        <el-table-column align="center" label="职位" prop="post" />
+        <el-table-column align="center" label="直属上级" prop="parentName" />
+        <el-table-column align="center" label="是否关键决策人" min-width="100" prop="master">
           <template #default="scope">
             <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.master" />
           </template>
@@ -56,14 +63,14 @@
       </el-table>
       <!-- 分页 -->
       <Pagination
-        :total="total"
-        v-model:page="queryParams.pageNo"
         v-model:limit="queryParams.pageSize"
+        v-model:page="queryParams.pageNo"
+        :total="total"
         @pagination="getList"
       />
     </ContentWrap>
     <template #footer>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
       <el-button @click="dialogVisible = false">取 消</el-button>
     </template>
 
@@ -71,10 +78,9 @@
     <ContactForm ref="formRef" @success="getList" />
   </Dialog>
 </template>
-<script setup lang="ts">
+<script lang="ts" setup>
 import * as ContactApi from '@/api/crm/contact'
 import ContactForm from '../ContactForm.vue'
-import { erpPriceTableColumnFormatter } from '@/utils'
 import { DICT_TYPE } from '@/utils/dict'
 
 const message = useMessage() // 消息弹窗

+ 8 - 8
src/views/crm/contact/detail/index.vue

@@ -31,15 +31,15 @@
         <BusinessList
           :biz-id="contact.id!"
           :biz-type="BizTypeEnum.CRM_CONTACT"
-          :customer-id="contact.customerId"
           :contact-id="contact.id"
+          :customer-id="contact.customerId"
         />
       </el-tab-pane>
     </el-tabs>
   </el-col>
   <!-- 表单弹窗:添加/修改 -->
-  <ContactForm ref="formRef" @success="getContact(contact.id)" />
-  <CrmTransferForm ref="transferFormRef" @success="close" />
+  <ContactForm ref="formRef" @success="getContact" />
+  <CrmTransferForm ref="transferFormRef" :biz-type="BizTypeEnum.CRM_CONTACT" @success="close" />
 </template>
 <script lang="ts" setup>
 import { useTagsViewStore } from '@/store/modules/tagsView'
@@ -65,11 +65,11 @@ const contact = ref<ContactApi.ContactVO>({} as ContactApi.ContactVO) // 联系
 const permissionListRef = ref<InstanceType<typeof PermissionList>>() // 团队成员列表 Ref
 
 /** 获取详情 */
-const getContact = async (id: number) => {
+const getContact = async () => {
   loading.value = true
   try {
-    contact.value = await ContactApi.getContact(id)
-    await getOperateLog(id)
+    contact.value = await ContactApi.getContact(contactId.value)
+    await getOperateLog(contactId.value)
   } finally {
     loading.value = false
   }
@@ -84,7 +84,7 @@ const openForm = (type: string, id?: number) => {
 /** 联系人转移 */
 const transferFormRef = ref<InstanceType<typeof CrmTransferForm>>() // 联系人转移表单 ref
 const transfer = () => {
-  transferFormRef.value?.open('联系人转移', contact.value.id, ContactApi.transferContact)
+  transferFormRef.value?.open(contact.value.id)
 }
 
 /** 获取操作日志 */
@@ -116,6 +116,6 @@ onMounted(async () => {
     return
   }
   contactId.value = params.id as unknown as number
-  await getContact(contactId.value)
+  await getContact()
 })
 </script>

+ 2 - 3
src/views/crm/contract/detail/index.vue

@@ -48,7 +48,7 @@
 
   <!-- 表单弹窗:添加/修改 -->
   <ContractForm ref="formRef" @success="getContractData" />
-  <CrmTransferForm ref="transferFormRef" @success="close" />
+  <CrmTransferForm ref="transferFormRef" :biz-type="BizTypeEnum.CRM_CONTRACT" @success="close" />
 </template>
 <script lang="ts" setup>
 import { useTagsViewStore } from '@/store/modules/tagsView'
@@ -113,10 +113,9 @@ const createReceivable = (planData: any) => {
 }
 
 /** 转移 */
-// TODO @puhui999:这个组件,要不传递业务类型,然后组件里判断 title 和 api 能调用哪个;整体治理掉;好呢
 const transferFormRef = ref<InstanceType<typeof CrmTransferForm>>() // 合同转移表单 ref
 const transferContract = () => {
-  transferFormRef.value?.open('合同转移', contract.value.id, ContractApi.transferContract)
+  transferFormRef.value?.open(contract.value.id)
 }
 
 /** 关闭 */

+ 2 - 2
src/views/crm/customer/detail/index.vue

@@ -76,7 +76,7 @@
   <!-- 表单弹窗:添加/修改 -->
   <CustomerForm ref="formRef" @success="getCustomer" />
   <CustomerDistributeForm ref="distributeForm" @success="getCustomer" />
-  <CrmTransferForm ref="transferFormRef" @success="getCustomer" />
+  <CrmTransferForm ref="transferFormRef" :biz-type="BizTypeEnum.CRM_CUSTOMER" @success="close" />
 </template>
 <script lang="ts" setup>
 import { useTagsViewStore } from '@/store/modules/tagsView'
@@ -142,7 +142,7 @@ const handleUpdateDealStatus = async () => {
 /** 客户转移 */
 const transferFormRef = ref<InstanceType<typeof CrmTransferForm>>() // 客户转移表单 ref
 const transfer = () => {
-  transferFormRef.value?.open('客户转移', customerId.value, CustomerApi.transferCustomer)
+  transferFormRef.value?.open(customerId.value)
 }
 
 /** 锁定客户 */

+ 10 - 7
src/views/crm/permission/components/PermissionForm.vue

@@ -29,12 +29,15 @@
           </template>
         </el-radio-group>
       </el-form-item>
-      <!-- TODO @puhui999:同时添加至,还没想好下次搞 -->
-      <el-form-item v-if="false && formType === 'create'" label="同时添加至" prop="toBizType">
-        <el-select v-model="formData.userId">
-          <el-option :value="1" label="联系人" />
-          <el-option :value="1" label="商机" />
-        </el-select>
+      <el-form-item
+        v-if="formType === 'create' && formData.bizType === BizTypeEnum.CRM_CUSTOMER"
+        label="同时添加至"
+      >
+        <el-checkbox-group v-model="formData.toBizTypes">
+          <el-checkbox :label="BizTypeEnum.CRM_CONTACT">联系人</el-checkbox>
+          <el-checkbox :label="BizTypeEnum.CRM_BUSINESS">商机</el-checkbox>
+          <el-checkbox :label="BizTypeEnum.CRM_CONTRACT">合同</el-checkbox>
+        </el-checkbox-group>
       </el-form-item>
     </el-form>
     <template #footer>
@@ -46,7 +49,7 @@
 <script lang="ts" setup>
 import * as UserApi from '@/api/system/user'
 import * as PermissionApi from '@/api/crm/permission'
-import { PermissionLevelEnum } from '@/api/crm/permission'
+import { BizTypeEnum, PermissionLevelEnum } from '@/api/crm/permission'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 
 defineOptions({ name: 'CrmPermissionForm' })

+ 57 - 23
src/views/crm/permission/components/TransferForm.vue

@@ -19,10 +19,7 @@
         </el-select>
       </el-form-item>
       <el-form-item label="老负责人">
-        <el-radio-group
-          v-model="oldOwnerHandler"
-          @change="formData.oldOwnerPermissionLevel = undefined"
-        >
+        <el-radio-group v-model="oldOwnerHandler" @change="formData.oldOwnerPermissionLevel">
           <el-radio :label="false" size="large">移除</el-radio>
           <el-radio :label="true" size="large">加入团队</el-radio>
         </el-radio-group>
@@ -39,8 +36,14 @@
           </template>
         </el-radio-group>
       </el-form-item>
+      <el-form-item v-if="bizType === BizTypeEnum.CRM_CUSTOMER" label="同时转移">
+        <el-checkbox-group v-model="formData.toBizTypes">
+          <el-checkbox :label="BizTypeEnum.CRM_CONTACT">联系人</el-checkbox>
+          <el-checkbox :label="BizTypeEnum.CRM_BUSINESS">商机</el-checkbox>
+          <el-checkbox :label="BizTypeEnum.CRM_CONTRACT">合同</el-checkbox>
+        </el-checkbox-group>
+      </el-form-item>
     </el-form>
-    <!-- TODO @puhui999 转移客户时,需要额外有【联系人】【商机】【合同】的 checkbox 选择 -->
     <template #footer>
       <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
       <el-button @click="dialogVisible = false">取 消</el-button>
@@ -49,23 +52,27 @@
 </template>
 <script lang="ts" setup>
 import * as UserApi from '@/api/system/user'
-import type { TransferReqVO } from '@/api/crm/customer'
+import * as BusinessApi from '@/api/crm/business'
+import * as ClueApi from '@/api/crm/clue'
+import * as ContactApi from '@/api/crm/contact'
+import * as CustomerApi from '@/api/crm/customer'
+import * as ContractApi from '@/api/crm/contract'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { PermissionLevelEnum } from '@/api/crm/permission'
+import { BizTypeEnum, PermissionLevelEnum, TransferReqVO } from '@/api/crm/permission'
 
 defineOptions({ name: 'CrmTransferForm' })
 
+const props = defineProps<{
+  bizType: number
+}>()
+
 const message = useMessage() // 消息弹窗
 const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
 const oldOwnerHandler = ref(false) // 老负责人的处理方式
-const formData = ref<TransferReqVO>({
-  id: undefined, // 客户编号
-  newOwnerUserId: undefined, // 新负责人的用户编号
-  oldOwnerPermissionLevel: undefined // 老负责人加入团队后的权限级别
-})
+const formData = ref<TransferReqVO>({} as TransferReqVO)
 const formRules = reactive({
   newOwnerUserId: [{ required: true, message: '新负责人不能为空', trigger: 'blur' }],
   oldOwnerPermissionLevel: [
@@ -73,15 +80,13 @@ const formRules = reactive({
   ]
 })
 const formRef = ref() // 表单 Ref
-const transferFuncRef = ref<Function>(() => {}) // 转移所需回调
 
 /** 打开弹窗 */
-const open = async (title: string, bizId: number, transferFunc: Function) => {
+const open = async (bizId: number) => {
   dialogVisible.value = true
-  dialogTitle.value = title
-  transferFuncRef.value = transferFunc
+  dialogTitle.value = getDialogTitle()
   resetForm()
-  formData.value.id = bizId
+  formData.value.bizId = bizId
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
@@ -96,7 +101,7 @@ const submitForm = async () => {
   formLoading.value = true
   try {
     const data = formData.value
-    await transferFuncRef.value(unref(data))
+    await transfer(unref(data))
     message.success(dialogTitle.value + '成功')
     dialogVisible.value = false
     // 发送操作成功的事件
@@ -105,15 +110,44 @@ const submitForm = async () => {
     formLoading.value = false
   }
 }
+const transfer = async (data: TransferReqVO) => {
+  switch (props.bizType) {
+    case BizTypeEnum.CRM_CLUE:
+      return await ClueApi.transferClue(data)
+    case BizTypeEnum.CRM_CUSTOMER:
+      return await CustomerApi.transferCustomer(data)
+    case BizTypeEnum.CRM_CONTACT:
+      return await ContactApi.transferContact(data)
+    case BizTypeEnum.CRM_BUSINESS:
+      return await BusinessApi.transferBusiness(data)
+    case BizTypeEnum.CRM_CONTRACT:
+      return await ContractApi.transferContract(data)
+    default:
+      message.error('【转移失败】没有转移接口')
+      throw new Error('【转移失败】没有转移接口')
+  }
+}
+const getDialogTitle = () => {
+  switch (props.bizType) {
+    case BizTypeEnum.CRM_CLUE:
+      return '线索转移'
+    case BizTypeEnum.CRM_CUSTOMER:
+      return '客户转移'
+    case BizTypeEnum.CRM_CONTACT:
+      return '联系人转移'
+    case BizTypeEnum.CRM_BUSINESS:
+      return '商机转移'
+    case BizTypeEnum.CRM_CONTRACT:
+      return '合同转移'
+    default:
+      return '转移'
+  }
+}
 
 /** 重置表单 */
 const resetForm = () => {
   formRef.value?.resetFields()
-  formData.value = {
-    id: undefined, // 客户编号
-    newOwnerUserId: undefined, // 新负责人的用户编号
-    oldOwnerPermissionLevel: undefined // 老负责人加入团队后的权限级别
-  }
+  formData.value = {} as TransferReqVO
 }
 onMounted(async () => {
   // 获得用户列表

+ 14 - 13
src/views/crm/receivable/ReceivableForm.vue

@@ -10,7 +10,7 @@
       <el-row>
         <el-col :span="12">
           <el-form-item label="回款编号" prop="no">
-            <el-input disabled v-model="formData.no" placeholder="保存时自动生成" />
+            <el-input v-model="formData.no" disabled placeholder="保存时自动生成" />
           </el-form-item>
         </el-col>
         <el-col :span="12">
@@ -38,8 +38,8 @@
               :disabled="formType !== 'create'"
               class="w-1/1"
               filterable
-              @change="handleCustomerChange"
               placeholder="请选择客户"
+              @change="handleCustomerChange"
             >
               <el-option
                 v-for="item in customerList"
@@ -57,15 +57,15 @@
               :disabled="formType !== 'create' || !formData.customerId"
               class="w-1/1"
               filterable
-              @change="handleContractChange"
               placeholder="请选择合同"
+              @change="handleContractChange"
             >
               <el-option
                 v-for="data in contractList"
                 :key="data.id"
+                :disabled="data.auditStatus !== 20"
                 :label="data.name"
                 :value="data.id!"
-                :disabled="data.auditStatus !== 20"
               />
             </el-select>
           </el-form-item>
@@ -78,15 +78,15 @@
               v-model="formData.planId"
               :disabled="formType !== 'create' || !formData.contractId"
               class="!w-1/1"
-              @change="handleReceivablePlanChange"
               placeholder="请选择回款期数"
+              @change="handleReceivablePlanChange"
             >
               <el-option
                 v-for="data in receivablePlanList"
                 :key="data.id"
+                :disabled="data.receivableId"
                 :label="'第 ' + data.period + ' 期'"
                 :value="data.id!"
-                :disabled="data.receivableId"
               />
             </el-select>
           </el-form-item>
@@ -109,11 +109,11 @@
           <el-form-item label="回款金额" prop="price">
             <el-input-number
               v-model="formData.price"
+              :min="0.01"
+              :precision="2"
               class="!w-100%"
               controls-position="right"
               placeholder="请输入回款金额"
-              :min="0.01"
-              :precision="2"
             />
           </el-form-item>
         </el-col>
@@ -145,12 +145,12 @@
 <script lang="ts" setup>
 import * as ReceivablePlanApi from '@/api/crm/receivable/plan'
 import * as ReceivableApi from '@/api/crm/receivable'
+import { ReceivableVO } from '@/api/crm/receivable'
 import * as UserApi from '@/api/system/user'
 import * as CustomerApi from '@/api/crm/customer'
 import * as ContractApi from '@/api/crm/contract'
 import { useUserStore } from '@/store/modules/user'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import form from '@/components/Form/src/Form.vue'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -185,9 +185,10 @@ const open = async (
   if (id) {
     formLoading.value = true
     try {
-      formData.value = await ReceivableApi.getReceivable(id)
-      await handleCustomerChange(formData.value.customerId)
-      formData.value.contractId = formData.value.contract.id
+      const data = (await ReceivableApi.getReceivable(id)) as ReceivableVO
+      formData.value = data
+      await handleCustomerChange(data.customerId!)
+      formData.value.contractId = data?.contract?.id
     } finally {
       formLoading.value = false
     }
@@ -266,7 +267,7 @@ const handleContractChange = async (contractId: number) => {
     // 获得回款计划列表
     receivablePlanList.value = []
     receivablePlanList.value = await ReceivablePlanApi.getReceivablePlanSimpleList(
-      formData.value.customerId,
+      formData.value.customerId!,
       contractId
     )
     // 设置金额

+ 9 - 6
src/views/crm/receivable/plan/ReceivablePlanForm.vue

@@ -10,7 +10,7 @@
       <el-row>
         <el-col :span="12">
           <el-form-item label="还款期数" prop="period">
-            <el-input disabled v-model="formData.period" placeholder="保存时自动生成" />
+            <el-input v-model="formData.period" disabled placeholder="保存时自动生成" />
           </el-form-item>
         </el-col>
         <el-col :span="12">
@@ -38,8 +38,8 @@
               :disabled="formType !== 'create'"
               class="w-1/1"
               filterable
-              @change="handleCustomerChange"
               placeholder="请选择客户"
+              @change="handleCustomerChange"
             >
               <el-option
                 v-for="item in customerList"
@@ -74,11 +74,11 @@
           <el-form-item label="计划回款金额" prop="price">
             <el-input-number
               v-model="formData.price"
+              :min="0.01"
+              :precision="2"
               class="!w-100%"
               controls-position="right"
               placeholder="请输入计划回款金额"
-              :min="0.01"
-              :precision="2"
             />
           </el-form-item>
         </el-col>
@@ -136,7 +136,7 @@ import * as CustomerApi from '@/api/crm/customer'
 import * as ContractApi from '@/api/crm/contract'
 import { useUserStore } from '@/store/modules/user'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { aw } from '../../../../../dist-prod/assets/index-9eac537b'
+import { cloneDeep } from 'lodash-es'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -167,7 +167,10 @@ const open = async (type: string, id?: number, customerId?: number, contractId?:
   if (id) {
     formLoading.value = true
     try {
-      formData.value = await ReceivablePlanApi.getReceivablePlan(id)
+      const data = await ReceivablePlanApi.getReceivablePlan(id)
+      formData.value = cloneDeep(data)
+      await handleCustomerChange(data.customerId!)
+      formData.value.contractId = data?.contractId
     } finally {
       formLoading.value = false
     }

+ 130 - 0
src/views/crm/statistics/customer/components/CustomerConversionStat.vue

@@ -0,0 +1,130 @@
+<!-- 客户转化率分析 -->
+<template>
+  <!-- Echarts图 -->
+  <el-card shadow="never">
+    <el-skeleton :loading="loading" animated>
+      <Echart :height="500" :options="echartsOption" />
+    </el-skeleton>
+  </el-card>
+
+  <!-- 统计列表 -->
+  <el-card shadow="never" class="mt-16px">
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="序号" align="center" type="index" width="80" />
+      <el-table-column label="客户名称" align="center" prop="customerName" min-width="200" />
+      <el-table-column label="合同名称" align="center" prop="contractName" min-width="200" />
+      <el-table-column label="合同总金额" align="center" prop="totalPrice" min-width="200" />
+      <el-table-column label="回款金额" align="center" prop="receivablePrice" min-width="200" />
+      <el-table-column label="负责人" align="center" prop="ownerUserName" min-width="200" />
+      <el-table-column label="创建人" align="center" prop="creatorUserName" min-width="200" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        min-width="200"
+      />
+      <el-table-column
+        label="下单日期"
+        align="center"
+        prop="orderDate"
+        :formatter="dateFormatter2"
+        min-width="200"
+      />
+    </el-table>
+  </el-card>
+</template>
+<script setup lang="ts">
+import {
+  StatisticsCustomerApi,
+  CrmStatisticsCustomerSummaryByDateRespVO
+} from '@/api/crm/statistics/customer'
+import { EChartsOption } from 'echarts'
+import { round } from 'lodash-es'
+import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
+
+defineOptions({ name: 'CustomerConversionStat' })
+const props = defineProps<{ queryParams: any }>() // 搜索参数
+
+const loading = ref(false) // 加载中
+const list = ref<CrmStatisticsCustomerSummaryByDateRespVO[]>([]) // 列表的数据
+
+/** 柱状图配置:纵向 */
+const echartsOption = reactive<EChartsOption>({
+  grid: {
+    left: 20,
+    right: 20,
+    bottom: 20,
+    containLabel: true
+  },
+  legend: {},
+  series: [
+    {
+      name: '客户转化率',
+      type: 'line',
+      data: []
+    }
+  ],
+  toolbox: {
+    feature: {
+      dataZoom: {
+        xAxisIndex: false // 数据区域缩放:Y 轴不缩放
+      },
+      brush: {
+        type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
+      },
+      saveAsImage: { show: true, name: '客户转化率分析' } // 保存为图片
+    }
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow'
+    }
+  },
+  yAxis: {
+    type: 'value',
+    name: '转化率(%)'
+  },
+  xAxis: {
+    type: 'category',
+    name: '日期',
+    data: []
+  }
+}) as EChartsOption
+
+/** 获取统计数据 */
+const loadData = async () => {
+  // 1. 加载统计数据
+  loading.value = true
+  const customerCount = await StatisticsCustomerApi.getCustomerSummaryByDate(props.queryParams)
+  const contractSummary = await StatisticsCustomerApi.getContractSummary(props.queryParams)
+  // 2.1 更新 Echarts 数据
+  if (echartsOption.xAxis && echartsOption.xAxis['data']) {
+    echartsOption.xAxis['data'] = customerCount.map(
+      (s: CrmStatisticsCustomerSummaryByDateRespVO) => s.time
+    )
+  }
+  if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+    echartsOption.series[0]['data'] = customerCount.map(
+      (item: CrmStatisticsCustomerSummaryByDateRespVO) => {
+        return {
+          name: item.time,
+          value: item.customerCreateCount
+            ? round((item.customerDealCount / item.customerCreateCount) * 100, 2)
+            : 0
+        }
+      }
+    )
+  }
+  // 2.2 更新列表数据
+  list.value = contractSummary
+  loading.value = false
+}
+defineExpose({ loadData })
+
+/** 初始化 */
+onMounted(() => {
+  loadData()
+})
+</script>

+ 127 - 0
src/views/crm/statistics/customer/components/CustomerDealCycle.vue

@@ -0,0 +1,127 @@
+<!-- 成交周期分析 -->
+<template>
+  <!-- Echarts图 -->
+  <el-card shadow="never">
+    <el-skeleton :loading="loading" animated>
+      <Echart :height="500" :options="echartsOption" />
+    </el-skeleton>
+  </el-card>
+
+  <!-- 统计列表 -->
+  <el-card shadow="never" class="mt-16px">
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="序号" align="center" type="index" width="80" />
+      <el-table-column label="日期" align="center" prop="ownerUserName" min-width="200" />
+      <el-table-column
+        label="成交周期(天)"
+        align="center"
+        prop="customerDealCycle"
+        min-width="200"
+      />
+      <el-table-column label="成交客户数" align="center" prop="customerDealCount" min-width="200" />
+    </el-table>
+  </el-card>
+</template>
+<script setup lang="ts">
+import {
+  StatisticsCustomerApi,
+  CrmStatisticsCustomerDealCycleByDateRespVO,
+  CrmStatisticsCustomerSummaryByDateRespVO,
+} from '@/api/crm/statistics/customer'
+import { EChartsOption } from 'echarts'
+
+defineOptions({ name: 'CustomerDealCycle' })
+const props = defineProps<{ queryParams: any }>() // 搜索参数
+
+const loading = ref(false) // 加载中
+const list = ref<CrmStatisticsCustomerDealCycleByDateRespVO[]>([]) // 列表的数据
+
+/** 柱状图配置:纵向 */
+const echartsOption = reactive<EChartsOption>({
+  grid: {
+    left: 20,
+    right: 20,
+    bottom: 20,
+    containLabel: true
+  },
+  legend: {},
+  series: [
+    {
+      name: '成交周期(天)',
+      type: 'bar',
+      data: []
+    },
+    {
+      name: '成交客户数',
+      type: 'bar',
+      data: []
+    }
+  ],
+  toolbox: {
+    feature: {
+      dataZoom: {
+        xAxisIndex: false // 数据区域缩放:Y 轴不缩放
+      },
+      brush: {
+        type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
+      },
+      saveAsImage: { show: true, name: '成交周期分析' } // 保存为图片
+    }
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow'
+    }
+  },
+  yAxis: {
+    type: 'value',
+    name: '数量(个)'
+  },
+  xAxis: {
+    type: 'category',
+    name: '日期',
+    data: []
+  }
+}) as EChartsOption
+
+/** 获取统计数据 */
+const loadData = async () => {
+  // 1. 加载统计数据
+  loading.value = true
+  const customerDealCycleByDate = await StatisticsCustomerApi.getCustomerDealCycleByDate(
+    props.queryParams
+  )
+    const customerSummaryByDate = await StatisticsCustomerApi.getCustomerSummaryByDate(
+    props.queryParams
+  )
+  const customerDealCycleByUser = await StatisticsCustomerApi.getCustomerDealCycleByUser(
+    props.queryParams
+  )
+  // 2.1 更新 Echarts 数据
+  if (echartsOption.xAxis && echartsOption.xAxis['data']) {
+    echartsOption.xAxis['data'] = customerDealCycleByDate.map(
+      (s: CrmStatisticsCustomerDealCycleByDateRespVO) => s.time
+    )
+  }
+  if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+    echartsOption.series[0]['data'] = customerDealCycleByDate.map(
+      (s: CrmStatisticsCustomerDealCycleByDateRespVO) => s.customerDealCycle
+    )
+  }
+  if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
+    echartsOption.series[1]['data'] = customerSummaryByDate.map(
+      (s: CrmStatisticsCustomerSummaryByDateRespVO) => s.customerDealCount
+    )
+  }
+  // 2.2 更新列表数据
+  list.value = customerDealCycleByUser
+  loading.value = false
+}
+defineExpose({ loadData })
+
+/** 初始化 */
+onMounted(() => {
+  loadData()
+})
+</script>

+ 124 - 0
src/views/crm/statistics/customer/components/CustomerFollowupSummary.vue

@@ -0,0 +1,124 @@
+<!-- 客户跟进次数分析 -->
+<template>
+  <!-- Echarts图 -->
+  <el-card shadow="never">
+    <el-skeleton :loading="loading" animated>
+      <Echart :height="500" :options="echartsOption" />
+    </el-skeleton>
+  </el-card>
+
+  <!-- 统计列表 -->
+  <el-card shadow="never" class="mt-16px">
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="序号" align="center" type="index" width="80" />
+      <el-table-column label="员工姓名" align="center" prop="ownerUserName" min-width="200" />
+      <el-table-column label="跟进次数" align="right" prop="followupRecordCount" min-width="200" />
+      <el-table-column
+        label="跟进客户数"
+        align="right"
+        prop="followupCustomerCount"
+        min-width="200"
+      />
+    </el-table>
+  </el-card>
+</template>
+<script setup lang="ts">
+import {
+  StatisticsCustomerApi,
+  CrmStatisticsFollowupSummaryByDateRespVO,
+  CrmStatisticsFollowupSummaryByUserRespVO
+} from '@/api/crm/statistics/customer'
+import { EChartsOption } from 'echarts'
+
+defineOptions({ name: 'CustomerFollowupSummary' })
+const props = defineProps<{ queryParams: any }>() // 搜索参数
+
+const loading = ref(false) // 加载中
+const list = ref<CrmStatisticsFollowupSummaryByUserRespVO[]>([]) // 列表的数据
+
+/** 柱状图配置:纵向 */
+const echartsOption = reactive<EChartsOption>({
+  grid: {
+    left: 20,
+    right: 20,
+    bottom: 20,
+    containLabel: true
+  },
+  legend: {},
+  series: [
+    {
+      name: '跟进客户数',
+      type: 'bar',
+      data: []
+    },
+    {
+      name: '跟进次数',
+      type: 'bar',
+      data: []
+    }
+  ],
+  toolbox: {
+    feature: {
+      dataZoom: {
+        xAxisIndex: false // 数据区域缩放:Y 轴不缩放
+      },
+      brush: {
+        type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
+      },
+      saveAsImage: { show: true, name: '客户跟进次数分析' } // 保存为图片
+    }
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow'
+    }
+  },
+  yAxis: {
+    type: 'value',
+    name: '数量(个)'
+  },
+  xAxis: {
+    type: 'category',
+    name: '日期',
+    data: []
+  }
+}) as EChartsOption
+
+/** 获取统计数据 */
+const loadData = async () => {
+  // 1. 加载统计数据
+  loading.value = true
+  const followupSummaryByDate = await StatisticsCustomerApi.getFollowupSummaryByDate(
+    props.queryParams
+  )
+  const followupSummaryByUser = await StatisticsCustomerApi.getFollowupSummaryByUser(
+    props.queryParams
+  )
+  // 2.1 更新 Echarts 数据
+  if (echartsOption.xAxis && echartsOption.xAxis['data']) {
+    echartsOption.xAxis['data'] = followupSummaryByDate.map(
+      (s: CrmStatisticsFollowupSummaryByDateRespVO) => s.time
+    )
+  }
+  if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+    echartsOption.series[0]['data'] = followupSummaryByDate.map(
+      (s: CrmStatisticsFollowupSummaryByDateRespVO) => s.followupCustomerCount
+    )
+  }
+  if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
+    echartsOption.series[1]['data'] = followupSummaryByDate.map(
+      (s: CrmStatisticsFollowupSummaryByDateRespVO) => s.followupRecordCount
+    )
+  }
+  // 2.2 更新列表数据
+  list.value = followupSummaryByUser
+  loading.value = false
+}
+defineExpose({ loadData })
+
+/** 初始化 */
+onMounted(() => {
+  loadData()
+})
+</script>

+ 105 - 0
src/views/crm/statistics/customer/components/CustomerFollowupType.vue

@@ -0,0 +1,105 @@
+<!-- 客户跟进方式分析 -->
+<template>
+  <!-- Echarts图 -->
+  <el-card shadow="never">
+    <el-skeleton :loading="loading" animated>
+      <Echart :height="500" :options="echartsOption" />
+    </el-skeleton>
+  </el-card>
+
+  <!-- 统计列表 -->
+  <el-card shadow="never" class="mt-16px">
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="序号" align="center" type="index" width="80" />
+      <el-table-column label="跟进方式" align="center" prop="followupType" min-width="200" />
+      <el-table-column label="个数" align="center" prop="followupRecordCount" min-width="200" />
+      <el-table-column label="占比(%)" align="center" prop="portion" min-width="200" />
+    </el-table>
+  </el-card>
+</template>
+<script setup lang="ts">
+import {
+  StatisticsCustomerApi,
+  CrmStatisticsFollowupSummaryByTypeRespVO
+} from '@/api/crm/statistics/customer'
+import { EChartsOption } from 'echarts'
+import { round, sumBy } from 'lodash-es'
+
+defineOptions({ name: 'CustomerFollowupType' })
+const props = defineProps<{ queryParams: any }>() // 搜索参数
+
+const loading = ref(false) // 加载中
+const list = ref<CrmStatisticsFollowupSummaryByTypeRespVO[]>([]) // 列表的数据
+
+/** 饼图配置 */
+const echartsOption = reactive<EChartsOption>({
+  title: {
+    text: '客户跟进方式分析',
+    left: 'center'
+  },
+  legend: {
+    orient: 'vertical',
+    left: 'left'
+  },
+  tooltip: {
+    trigger: 'item',
+    formatter: '{b} : {c}% '
+  },
+  toolbox: {
+    feature: {
+      saveAsImage: { show: true, name: '客户跟进方式分析' } // 保存为图片
+    }
+  },
+  series: [
+    {
+      name: '跟进方式',
+      type: 'pie',
+      radius: '50%',
+      data: [],
+      emphasis: {
+        itemStyle: {
+          shadowBlur: 10,
+          shadowOffsetX: 0,
+          shadowColor: 'rgba(0, 0, 0, 0.5)'
+        }
+      }
+    }
+  ]
+}) as EChartsOption
+
+/** 获取统计数据 */
+const loadData = async () => {
+  // 1. 加载统计数据
+  loading.value = true
+  const followupSummaryByType = await StatisticsCustomerApi.getFollowupSummaryByType(
+    props.queryParams
+  )
+  // 2.1 更新 Echarts 数据
+  if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+    echartsOption.series[0]['data'] = followupSummaryByType.map(
+      (r: CrmStatisticsFollowupSummaryByTypeRespVO) => {
+        return {
+          name: r.followupType,
+          value: r.followupRecordCount
+        }
+      }
+    )
+  }
+  // 2.2 更新列表数据
+  const totalCount = sumBy(followupSummaryByType, 'followupRecordCount')
+  list.value = followupSummaryByType.map((r: CrmStatisticsFollowupSummaryByTypeRespVO) => {
+    return {
+      followupType: r.followupType,
+      followupRecordCount: r.followupRecordCount,
+      portion: round((r.followupRecordCount / totalCount) * 100, 2)
+    }
+  })
+  loading.value = false
+}
+defineExpose({ loadData })
+
+/** 初始化 */
+onMounted(() => {
+  loadData()
+})
+</script>

+ 151 - 0
src/views/crm/statistics/customer/components/CustomerSummary.vue

@@ -0,0 +1,151 @@
+<!-- 客户总量统计 -->
+<template>
+  <!-- Echarts图 -->
+  <el-card shadow="never">
+    <el-skeleton :loading="loading" animated>
+      <Echart :height="500" :options="echartsOption" />
+    </el-skeleton>
+  </el-card>
+
+  <!-- 统计列表 -->
+  <el-card shadow="never" class="mt-16px">
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="序号" align="center" type="index" width="80" />
+      <el-table-column label="员工姓名" prop="ownerUserName" min-width="100" />
+      <el-table-column
+        label="新增客户数"
+        align="right"
+        prop="customerCreateCount"
+        min-width="200"
+      />
+      <el-table-column label="成交客户数" align="right" prop="customerDealCount" min-width="200" />
+      <el-table-column label="客户成交率(%)" align="right" min-width="200">
+        <template #default="scope">
+          {{
+            scope.row.customerCreateCount !== 0
+              ? round((scope.row.customerDealCount / scope.row.customerCreateCount) * 100, 2)
+              : 0
+          }}
+        </template>
+      </el-table-column>
+      <el-table-column label="合同总金额" align="right" prop="contractPrice" min-width="200" />
+      <el-table-column label="回款金额" align="right" prop="receivablePrice" min-width="200" />
+      <el-table-column label="未回款金额" align="right" min-width="200">
+        <!-- TODO @dhb52:参考 util/index.ts 的 // ========== ERP 专属方法 ========== 部分,搞个两个方法,一个格式化百分比,一个计算百分比  -->
+        <template #default="scope">
+          {{ round(scope.row.contractPrice - scope.row.receivablePrice, 2) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="回款完成率(%)" align="right" min-width="200">
+        <template #default="scope">
+          {{
+            scope.row.contractPrice !== 0
+              ? round((scope.row.receivablePrice / scope.row.contractPrice) * 100, 2)
+              : 0
+          }}
+        </template>
+      </el-table-column>
+    </el-table>
+  </el-card>
+</template>
+<script setup lang="ts">
+import {
+  StatisticsCustomerApi,
+  CrmStatisticsCustomerSummaryByDateRespVO,
+  CrmStatisticsCustomerSummaryByUserRespVO
+} from '@/api/crm/statistics/customer'
+import { EChartsOption } from 'echarts'
+import { round } from 'lodash-es'
+
+defineOptions({ name: 'CustomerSummary' })
+const props = defineProps<{ queryParams: any }>() // 搜索参数
+
+const loading = ref(false) // 加载中
+const list = ref<CrmStatisticsCustomerSummaryByUserRespVO[]>([]) // 列表的数据
+
+/** 柱状图配置:纵向 */
+const echartsOption = reactive<EChartsOption>({
+  grid: {
+    left: 20,
+    right: 20,
+    bottom: 20,
+    containLabel: true
+  },
+  legend: {},
+  series: [
+    {
+      name: '新增客户数',
+      type: 'bar',
+      data: []
+    },
+    {
+      name: '成交客户数',
+      type: 'bar',
+      data: []
+    }
+  ],
+  toolbox: {
+    feature: {
+      dataZoom: {
+        xAxisIndex: false // 数据区域缩放:Y 轴不缩放
+      },
+      brush: {
+        type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
+      },
+      saveAsImage: { show: true, name: '客户总量分析' } // 保存为图片
+    }
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow'
+    }
+  },
+  yAxis: {
+    type: 'value',
+    name: '数量(个)'
+  },
+  xAxis: {
+    type: 'category',
+    name: '日期',
+    data: []
+  }
+}) as EChartsOption
+
+/** 获取统计数据 */
+const loadData = async () => {
+  // 1. 加载统计数据
+  loading.value = true
+  const customerSummaryByDate = await StatisticsCustomerApi.getCustomerSummaryByDate(
+    props.queryParams
+  )
+  const customerSummaryByUser = await StatisticsCustomerApi.getCustomerSummaryByUser(
+    props.queryParams
+  )
+  // 2.1 更新 Echarts 数据
+  if (echartsOption.xAxis && echartsOption.xAxis['data']) {
+    echartsOption.xAxis['data'] = customerSummaryByDate.map(
+      (s: CrmStatisticsCustomerSummaryByDateRespVO) => s.time
+    )
+  }
+  if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+    echartsOption.series[0]['data'] = customerSummaryByDate.map(
+      (s: CrmStatisticsCustomerSummaryByDateRespVO) => s.customerCreateCount
+    )
+  }
+  if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
+    echartsOption.series[1]['data'] = customerSummaryByDate.map(
+      (s: CrmStatisticsCustomerSummaryByDateRespVO) => s.customerDealCount
+    )
+  }
+  // 2.2 更新列表数据
+  list.value = customerSummaryByUser
+  loading.value = false
+}
+defineExpose({ loadData })
+
+/** 初始化 */
+onMounted(() => {
+  loadData()
+})
+</script>

+ 166 - 0
src/views/crm/statistics/customer/index.vue

@@ -0,0 +1,166 @@
+<!-- 数据统计 - 员工客户分析 -->
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="时间范围" prop="orderDate">
+        <el-date-picker
+          v-model="queryParams.times"
+          :shortcuts="defaultShortcuts"
+          class="!w-240px"
+          end-placeholder="结束日期"
+          start-placeholder="开始日期"
+          type="daterange"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+        />
+      </el-form-item>
+      <el-form-item label="归属部门" prop="deptId">
+        <el-tree-select
+          v-model="queryParams.deptId"
+          class="!w-240px"
+          :data="deptList"
+          :props="defaultProps"
+          check-strictly
+          node-key="id"
+          placeholder="请选择归属部门"
+          @change="queryParams.userId = undefined"
+        />
+      </el-form-item>
+      <el-form-item label="员工" prop="userId">
+        <el-select v-model="queryParams.userId" class="!w-240px" placeholder="员工" clearable>
+          <el-option
+            v-for="(user, index) in userListByDeptId"
+            :label="user.nickname"
+            :value="user.id"
+            :key="index"
+          />
+        </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-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 客户统计 -->
+  <el-col>
+    <el-tabs v-model="activeTab">
+      <!-- 客户总量分析 -->
+      <el-tab-pane label="客户总量分析" name="customerSummary" lazy>
+        <CustomerSummary :query-params="queryParams" ref="customerSummaryRef" />
+      </el-tab-pane>
+      <!-- 客户跟进次数分析 -->
+      <el-tab-pane label="客户跟进次数分析" name="followupSummary" lazy>
+        <CustomerFollowupSummary :query-params="queryParams" ref="followupSummaryRef" />
+      </el-tab-pane>
+      <!-- 客户跟进方式分析 -->
+      <el-tab-pane label="客户跟进方式分析" name="followupType" lazy>
+        <CustomerFollowupType :query-params="queryParams" ref="followupTypeRef" />
+      </el-tab-pane>
+      <!-- 客户转化率分析 -->
+      <el-tab-pane label="客户转化率分析" name="conversionStat" lazy>
+        <CustomerConversionStat :query-params="queryParams" ref="conversionStatRef" />
+      </el-tab-pane>
+      <!-- 成交周期分析 -->
+      <el-tab-pane label="成交周期分析" name="dealCycle" lazy>
+        <CustomerDealCycle :query-params="queryParams" ref="dealCycleRef" />
+      </el-tab-pane>
+    </el-tabs>
+  </el-col>
+</template>
+
+<script lang="ts" setup>
+import * as DeptApi from '@/api/system/dept'
+import * as UserApi from '@/api/system/user'
+import { useUserStore } from '@/store/modules/user'
+import { beginOfDay, defaultShortcuts, endOfDay, formatDate } from '@/utils/formatTime'
+import { defaultProps, handleTree } from '@/utils/tree'
+import CustomerSummary from './components/CustomerSummary.vue'
+import CustomerFollowupSummary from './components/CustomerFollowupSummary.vue'
+import CustomerFollowupType from './components/CustomerFollowupType.vue'
+import CustomerConversionStat from './components/CustomerConversionStat.vue'
+import CustomerDealCycle from './components/CustomerDealCycle.vue'
+
+defineOptions({ name: 'CrmStatisticsCustomer' })
+
+const queryParams = reactive({
+  deptId: useUserStore().getUser.deptId,
+  userId: undefined,
+  times: [
+    // 默认显示最近一周的数据
+    formatDate(beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7))),
+    formatDate(endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24)))
+  ]
+})
+
+const queryFormRef = ref() // 搜索的表单
+const deptList = ref<Tree[]>([]) // 部门树形结构
+const userList = ref<UserApi.UserVO[]>([]) // 全量用户清单
+// 根据选择的部门筛选员工清单
+const userListByDeptId = computed(() =>
+  queryParams.deptId
+    ? userList.value.filter((u: UserApi.UserVO) => u.deptId === queryParams.deptId)
+    : []
+)
+
+// 活跃标签
+const activeTab = ref('customerSummary')
+// 1.客户总量分析
+const customerSummaryRef = ref()
+// 2.客户跟进次数分析
+const followupSummaryRef = ref()
+// 3.客户跟进方式分析
+const followupTypeRef = ref()
+// 4.客户转化率分析
+const conversionStatRef = ref()
+// 5.公海客户分析
+// 缺 crm_owner_record 表
+// 6.成交周期分析
+const dealCycleRef = ref()
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  switch (activeTab.value) {
+    case 'customerSummary':
+      customerSummaryRef.value?.loadData?.()
+      break
+    case 'followupSummary':
+      followupSummaryRef.value?.loadData?.()
+      break
+    case 'followupType':
+      followupTypeRef.value?.loadData?.()
+      break
+    case 'conversionStat':
+      conversionStatRef.value?.loadData?.()
+      break
+    case 'dealCycle':
+      dealCycleRef.value?.loadData?.()
+      break
+  }
+}
+
+// 当 activeTab 改变时,刷新当前活动的 tab
+watch(activeTab, () => {
+  handleQuery()
+})
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+// 加载部门树
+onMounted(async () => {
+  deptList.value = handleTree(await DeptApi.getSimpleDeptList())
+  userList.value = handleTree(await UserApi.getSimpleUserList())
+})
+</script>

Some files were not shown because too many files changed in this diff