Sfoglia il codice sorgente

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

jason 1 anno fa
parent
commit
cf28e42a26
31 ha cambiato i file con 1686 aggiunte e 68 eliminazioni
  1. 3 2
      src/api/bpm/definition/index.ts
  2. 42 0
      src/api/bpm/processExpression/index.ts
  3. 15 3
      src/api/bpm/processInstance/index.ts
  4. 40 0
      src/api/bpm/processListener/index.ts
  5. 6 2
      src/api/bpm/task/index.ts
  6. 8 0
      src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
  7. 46 1
      src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue
  8. 83 0
      src/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue
  9. 41 1
      src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue
  10. 27 0
      src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts
  11. 68 0
      src/components/bpmnProcessDesigner/package/penal/task/task-components/ProcessExpressionDialog.vue
  12. 19 8
      src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
  13. 1 0
      src/components/bpmnProcessDesigner/package/utils.ts
  14. 3 1
      src/utils/dict.ts
  15. 4 4
      src/views/bpm/definition/index.vue
  16. 7 1
      src/views/bpm/model/ModelForm.vue
  17. 5 0
      src/views/bpm/model/index.vue
  18. 79 3
      src/views/bpm/oa/leave/create.vue
  19. 1 1
      src/views/bpm/oa/leave/index.vue
  20. 114 0
      src/views/bpm/processExpression/ProcessExpressionForm.vue
  21. 180 0
      src/views/bpm/processExpression/index.vue
  22. 81 10
      src/views/bpm/processInstance/create/index.vue
  23. 10 7
      src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue
  24. 5 3
      src/views/bpm/processInstance/detail/index.vue
  25. 29 18
      src/views/bpm/processInstance/index.vue
  26. 255 0
      src/views/bpm/processInstance/manager/index.vue
  27. 162 0
      src/views/bpm/processListener/ProcessListenerForm.vue
  28. 183 0
      src/views/bpm/processListener/index.vue
  29. 2 2
      src/views/bpm/task/done/index.vue
  30. 166 0
      src/views/bpm/task/manager/index.vue
  31. 1 1
      src/views/bpm/task/todo/index.vue

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

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

+ 15 - 3
src/api/bpm/processInstance/index.ts

@@ -31,20 +31,32 @@ export type ProcessInstanceCopyVO = {
   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-by-start-user', data: data })
+}
+
+export const cancelProcessInstanceByAdmin = async (id: number, reason: string) => {
   const data = {
     id: id,
     reason: reason
   }
-  return await request.delete({ url: '/bpm/process-instance/cancel', data: data })
+  return await request.delete({ url: '/bpm/process-instance/cancel-by-admin', data: data })
 }
 
 export const getProcessInstance = async (id: string) => {

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

+ 6 - 2
src/api/bpm/task/index.ts

@@ -4,14 +4,18 @@ export type TaskVO = {
   id: number
 }
 
-export const getTodoTaskPage = async (params: any) => {
+export const getTaskTodoPage = async (params: any) => {
   return await request.get({ url: '/bpm/task/todo-page', params })
 }
 
-export const getDoneTaskPage = async (params: any) => {
+export const getTaskDonePage = async (params: any) => {
   return await request.get({ url: '/bpm/task/done-page', params })
 }
 
+export const getTaskManagerPage = async (params: any) => {
+  return await request.get({ url: '/bpm/task/manager-page', params })
+}
+
 export const approveTask = async (data: any) => {
   return await request.put({ url: '/bpm/task/approve', data })
 }

+ 8 - 0
src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue

@@ -139,6 +139,14 @@ const updateBaseInfo = (key) => {
   }
 }
 
+onMounted(() => {
+  // 针对上传的 bpmn 流程图时,需要延迟 1 毫秒的时间,保证 key 和 name 的更新
+  setTimeout(() => {
+    handleKeyUpdate(props.model.key)
+    handleNameUpdate(props.model.name)
+  }, 110)
+})
+
 watch(
   () => props.businessObject,
   (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: '表达式',

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

+ 19 - 8
src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue

@@ -5,7 +5,7 @@
         v-model="userTaskForm.candidateStrategy"
         clearable
         style="width: 100%"
-        @change="changecandidateStrategy"
+        @change="changeCandidateStrategy"
       >
         <el-option
           v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_CANDIDATE_STRATEGY)"
@@ -65,11 +65,7 @@
       </el-select>
     </el-form-item>
     <el-form-item
-      v-if="
-        userTaskForm.candidateStrategy == 30 ||
-        userTaskForm.candidateStrategy == 31 ||
-        userTaskForm.candidateStrategy == 32
-      "
+      v-if="userTaskForm.candidateStrategy == 30"
       label="指定用户"
       prop="candidateParam"
       span="24"
@@ -118,9 +114,14 @@
         type="textarea"
         v-model="userTaskForm.candidateParam[0]"
         clearable
-        style="width: 100%"
+        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>
@@ -133,6 +134,7 @@ 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({
@@ -177,7 +179,7 @@ const resetTaskForm = () => {
 }
 
 /** 更新 candidateStrategy 字段时,需要清空 candidateParam,并触发 bpmn 图更新 */
-const changecandidateStrategy = () => {
+const changeCandidateStrategy = () => {
   userTaskForm.value.candidateParam = []
   updateElementTask()
 }
@@ -190,6 +192,15 @@ const updateElementTask = () => {
   })
 }
 
+// 打开监听器弹窗
+const processExpressionDialogRef = ref()
+const openProcessExpressionDialog = async () => {
+  processExpressionDialogRef.value.open()
+}
+const selectProcessExpression = (expression) => {
+  userTaskForm.value.candidateParam = [expression.expression]
+}
+
 watch(
   () => props.id,
   () => {

+ 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 字段

+ 3 - 1
src/utils/dict.ts

@@ -141,6 +141,8 @@ export enum DICT_TYPE {
   BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status',
   BPM_TASK_STATUS = 'bpm_task_status',
   BPM_OA_LEAVE_TYPE = 'bpm_oa_leave_type',
+  BPM_PROCESS_LISTENER_TYPE = 'bpm_process_listener_type',
+  BPM_PROCESS_LISTENER_VALUE_TYPE = 'bpm_process_listener_value_type',
 
   // ========== PAY 模块 ==========
   PAY_CHANNEL_CODE = 'pay_channel_code', // 支付渠道编码类型
@@ -155,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', // 会员经验业务类型
 

+ 4 - 4
src/views/bpm/definition/index.vue

@@ -72,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"
     />
@@ -133,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
 }
 

+ 7 - 1
src/views/bpm/model/ModelForm.vue

@@ -50,6 +50,9 @@
           />
         </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>
@@ -141,15 +144,17 @@ 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' }]
 })
@@ -223,6 +228,7 @@ const resetForm = () => {
     formType: 10,
     name: '',
     category: undefined,
+    icon: '',
     description: '',
     formId: '',
     formCustomCreatePath: '',

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

@@ -72,6 +72,11 @@
           </el-button>
         </template>
       </el-table-column>
+      <el-table-column label="流程图标" align="center" prop="icon" width="100">
+        <template #default="scope">
+          <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">

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

+ 1 - 1
src/views/bpm/oa/leave/index.vue

@@ -226,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>

+ 81 - 10
src/views/bpm/processInstance/create/index.vue

@@ -23,11 +23,7 @@
             >
               <template #default>
                 <div class="flex">
-                  <!-- TODO 芋艿:流程图,增加 icon -->
-                  <el-image
-                    src="http://test.yudao.iocoder.cn/96c787a2ce88bf6d0ce3cd8b6cf5314e80e7703cd41bf4af8cd2e2909dbd6b6d.png"
-                    class="w-32px h-32px"
-                  />
+                  <el-image :src="definition.icon" class="w-32px h-32px" />
                   <el-text class="!ml-10px" size="large">{{ definition.name }}</el-text>
                 </div>
               </template>
@@ -54,7 +50,40 @@
           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>
     <!-- 流程图预览 -->
@@ -69,6 +98,7 @@ 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' })
 
@@ -124,7 +154,6 @@ const categoryProcessDefinitionList = computed(() => {
 })
 
 // ========== 表单相关 ==========
-const bpmnXML = ref(null) // BPMN 数据
 const fApi = ref<ApiAttrs>()
 const detailForm = ref({
   rule: [],
@@ -133,17 +162,53 @@ const detailForm = ref({
 }) // 流程表单详情
 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, formVariables) => {
   // 设置选择的流程
   selectProcessDefinition.value = row
 
+  // 重置指定审批人
+  startUserSelectTasks.value = []
+  startUserSelectAssignees.value = {}
+  startUserSelectAssigneesFormRules.value = {}
+
   // 情况一:流程表单
   if (row.formType == 10) {
     // 设置表单
     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 push({
@@ -158,19 +223,25 @@ const submitForm = async (formData) => {
   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: selectProcessDefinition.value.id,
-      variables: formData
+      variables: formData,
+      startUserSelectAssignees: startUserSelectAssignees.value
     })
     // 提示
     message.success('发起流程成功')
     // 跳转回去
     delView(unref(currentRoute))
     await push({
-      name: 'BpmProcessInstance'
+      name: 'BpmProcessInstanceMy'
     })
   } finally {
     fApi.value.btn.loading(false)

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

@@ -34,14 +34,17 @@ const bpmnControlForm = ref({
 })
 const activityList = ref([]) // 任务列表
 
-/** 初始化 */
-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 {

+ 5 - 3
src/views/bpm/processInstance/detail/index.vue

@@ -115,7 +115,7 @@
     <!-- 高亮流程图 -->
     <ProcessInstanceBpmnViewer
       :id="`${id}`"
-      :bpmn-xml="bpmnXML"
+      :bpmn-xml="bpmnXml"
       :loading="processInstanceLoading"
       :process-instance="processInstance"
       :tasks="tasks"
@@ -158,7 +158,7 @@ const userId = useUserStore().getUser.id // 当前登录的编号
 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[]>([]) // 任务列表
 // ========== 审批信息 ==========
@@ -290,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
   }

+ 29 - 18
src/views/bpm/processInstance/index.vue

@@ -58,7 +58,7 @@
           />
         </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"
@@ -87,23 +87,21 @@
   <!-- 列表 -->
   <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="categoryName" />
-      <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="提交时间"
+        label="发起时间"
         align="center"
         prop="startTime"
         width="180"
@@ -116,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
@@ -152,12 +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() // 消息弹窗
@@ -182,7 +193,7 @@ 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 {
@@ -230,7 +241,7 @@ const handleCancel = async (row) => {
     inputErrorMessage: '取消原因不能为空'
   })
   // 发起取消
-  await ProcessInstanceApi.cancelProcessInstance(row.id, value)
+  await ProcessInstanceApi.cancelProcessInstanceByStartUser(row.id, value)
   message.success('取消成功')
   // 刷新列表
   await getList()

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

+ 2 - 2
src/views/bpm/task/done/index.vue

@@ -81,7 +81,7 @@
         </template>
       </el-table-column>
       <el-table-column align="center" label="审批建议" prop="reason" min-width="180" />
-      <el-table-column align="center" label="耗时" prop="durationInMillis" width="120">
+      <el-table-column align="center" label="耗时" prop="durationInMillis" width="160">
         <template #default="scope">
           {{ formatPast2(scope.row.durationInMillis) }}
         </template>
@@ -127,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 {

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

+ 1 - 1
src/views/bpm/task/todo/index.vue

@@ -109,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 {