瀏覽代碼

feat: [bpm] 流程取消操作

jason 3 月之前
父節點
當前提交
a11ed15d3b

+ 53 - 10
src/pages-bpm/processInstance/detail/components/operation-button.vue

@@ -33,12 +33,24 @@
       </view>
     </view>
   </view>
-  <!-- TODO 无待审批的任务 需要显示什么 -->
+  <!--  无待审批的任务 仅显示取消按钮。TODO 看看还需要显示 -->
+  <view v-if="!runningTask && isShowProcessStartCancel()" class="yd-detail-footer">
+    <wd-button
+      plain
+      type="primary"
+      :round="false"
+      block
+      @click="handleOperation(BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL)"
+    >
+      取消
+    </wd-button>
+  </view>
 </template>
 
 <script lang="ts" setup>
 import type { Action } from 'wot-design-uni/components/wd-action-sheet/types'
 import type { ButtonType } from 'wot-design-uni/components/wd-button/types'
+import type { ProcessInstance } from '@/api/bpm/processInstance'
 import type { Task } from '@/api/bpm/task'
 import { useToast } from 'wot-design-uni'
 import { useUserStore } from '@/store'
@@ -74,23 +86,26 @@ const operationIconsMap: Record<number, string> = {
   [BpmTaskOperationButtonTypeEnum.RETURN]: 'arrow-left',
   [BpmTaskOperationButtonTypeEnum.COPY]: 'copy',
   [BpmTaskOperationButtonTypeEnum.DELETE_SIGN]: 'remove',
+  [BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL]: 'stop-circle',
 }
 
 const userStore = useUserStore()
 /** 左侧操作按钮 【最多两个】{转办, 委派, 退回, 加签, 抄送等} */
 const leftOperations = ref<LeftOperationType[]>([])
 
-/** 右侧操作按钮【最多两个】{通过,拒绝, 取消,减签} */
+/** 右侧操作按钮【最多两个】{通过,拒绝, 取消} */
 const rightOperationTypes = []
 const rightOperations = ref<RightOperationType[]>([])
 /** 更多操作 */
 const moreOperations = ref<MoreOperationType[]>([])
 const toast = useToast()
 const runningTask = ref<Task>()
+const processInstance = ref<ProcessInstance>()
 const reasonRequire = ref<boolean>(false)
 
-/** 加载待办任务 */
-function loadTodoTask(task: Task) {
+/** 初始化 */
+function init(theProcessInstance: ProcessInstance, task: Task) {
+  processInstance.value = theProcessInstance
   runningTask.value = task
   if (task) {
     reasonRequire.value = task.reasonRequire ?? false
@@ -149,12 +164,35 @@ function loadTodoTask(task: Task) {
       }
     }
   }
+
+  // 是否显示流程取消
+  if (isShowProcessStartCancel()) {
+    if (rightOperationTypes.length < 2) {
+      rightOperationTypes.push(BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL)
+      rightOperations.value.push({
+        operationType: BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL,
+        displayName: getButtonDisplayName(BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL),
+        btnType: 'primary',
+        plain: true,
+      })
+    } else {
+      if (leftOperations.value.length >= 2) {
+        moreOperations.value.push({
+          name: getButtonDisplayName(BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL),
+          operationType: BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL,
+        })
+      } else {
+        leftOperations.value.push({
+          operationType: BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL,
+          iconName: operationIconsMap[BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL],
+          displayName: getButtonDisplayName(BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL),
+        })
+      }
+    }
+  }
 }
 /** 跳转到相应的操作页面 */
 function handleOperation(operationType: number) {
-  if (!runningTask.value) {
-    return
-  }
   switch (operationType) {
     case BpmTaskOperationButtonTypeEnum.APPROVE:
       uni.navigateTo({ url: `/pages-bpm/processInstance/detail/audit/index?id=${runningTask.value.id}&pass=true` })
@@ -187,6 +225,11 @@ function handleOperation(operationType: number) {
         url: `/pages-bpm/processInstance/detail/delete-sign/index?processInstanceId=${runningTask.value.processInstanceId}&taskId=${runningTask.value.id}&children=${encodeURIComponent(JSON.stringify(runningTask.value.children || []))}`,
       })
       break
+    case BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL:
+      uni.navigateTo({
+        url: `/pages-bpm/processInstance/detail/process-cancel/index?processInstanceId=${processInstance.value.id}&taskId=${runningTask.value?.id}`,
+      })
+      break
   }
 }
 
@@ -250,7 +293,7 @@ function isEndProcessStatus(status: number) {
 /** 流程发起人是否为当前用户 */
 function isProcessStartUser() {
   let isStartUser = false
-  if (userStore.userInfo?.userId === runningTask.value?.processInstance?.startUser?.id) {
+  if (userStore.userInfo?.userId === processInstance.value?.startUser?.id) {
     isStartUser = true
   }
   return isStartUser
@@ -263,9 +306,9 @@ function isShowDeleteSign() {
 
 /** 是否显示流程发起人取消 */
 function isShowProcessStartCancel() {
-  return isProcessStartUser
+  return isProcessStartUser() && !isEndProcessStatus(processInstance.value?.status)
 }
 
 /** 暴露方法 */
-defineExpose({ loadTodoTask })
+defineExpose({ init })
 </script>

+ 13 - 14
src/pages-bpm/processInstance/detail/index.vue

@@ -12,32 +12,31 @@
       <view class="p-24rpx">
         <!-- 标题和状态 -->
         <view class="mb-16rpx flex items-center justify-between">
-          <text class="text-32rpx text-[#333] font-bold">{{ processInstance.name }}</text>
-          <wd-tag :type="getStatusType(processInstance.status)">
-            {{ getStatusText(processInstance.status) }}
+          <text class="text-32rpx text-[#333] font-bold">{{ processInstance?.name }}</text>
+          <wd-tag :type="getStatusType(processInstance?.status)">
+            {{ getStatusText(processInstance?.status) }}
           </wd-tag>
         </view>
         <!-- 发起人信息 -->
         <view class="flex items-center">
           <view class="mr-12rpx h-64rpx w-64rpx flex items-center justify-center rounded-full bg-[#1890ff] text-white">
-            {{ processInstance.startUser?.nickname?.[0] || '?' }}
+            {{ processInstance?.startUser?.nickname?.[0] || '?' }}
           </view>
           <view>
-            <text class="text-28rpx text-[#333]">{{ processInstance.startUser?.nickname }}</text>
-            <text v-if="processInstance.startUser?.deptName" class="ml-8rpx text-24rpx text-[#999]">
-              {{ processInstance.startUser?.deptName }}
+            <text class="text-28rpx text-[#333]">{{ processInstance?.startUser?.nickname }}</text>
+            <text v-if="processInstance?.startUser?.deptName" class="ml-8rpx text-24rpx text-[#999]">
+              {{ processInstance?.startUser?.deptName }}
             </text>
           </view>
         </view>
         <!-- 提交时间 -->
         <view class="mt-16rpx text-24rpx text-[#999]">
-          提交于 {{ formatDateTime(processInstance.startTime) }}
+          提交于 {{ formatDateTime(processInstance?.startTime) }}
         </view>
       </view>
     </view>
 
     <!-- 区域:审批详情(表单) -->
-    <!-- TODO @jason:看看 idea 告警,怎么优化下 -->
     <FormDetail :process-definition="processDefinition" :process-instance="processInstance" />
 
     <!-- 区域:审批记录 TODO @jason:抽成类似 /Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/views/bpm/processInstance/detail/modules/task-list.vue -->
@@ -89,7 +88,7 @@
     <!-- TODO 待开发:区域:流程评论 -->
 
     <!-- 区域:底部操作栏 -->
-    <ProcessInstanceOperationButton ref="operationButtonRef" />
+    <ProcessInstanceOperationButton ref="operationButtonRef" :process-instance="processInstance" />
   </view>
 </template>
 
@@ -120,8 +119,8 @@ definePage({
 
 const userStore = useUserStore()
 const toast = useToast()
-const processInstance = ref<Partial<ProcessInstance>>({})
-const processDefinition = ref<Partial<ProcessDefinition>>({})
+const processInstance = ref<ProcessInstance>()
+const processDefinition = ref<ProcessDefinition>()
 const tasks = ref<Task[]>([])
 const orderAsc = ref(true)
 
@@ -238,8 +237,8 @@ async function loadProcessInstance() {
     return
   }
   processInstance.value = data.processInstance
-  processDefinition.value = data.processDefinition || {}
-  operationButtonRef.value?.loadTodoTask(data.todoTask)
+  processDefinition.value = data.processDefinition
+  operationButtonRef.value?.init(data.processInstance, data.todoTask)
 }
 
 /** 加载任务列表 */

+ 133 - 0
src/pages-bpm/processInstance/detail/process-cancel/index.vue

@@ -0,0 +1,133 @@
+<template>
+  <view class="yd-page-container">
+    <!-- 顶部导航栏 -->
+    <wd-navbar
+      title="取消流程"
+      left-arrow placeholder safe-area-inset-top fixed
+      @click-left="handleBack"
+    />
+
+    <!-- 操作表单 -->
+    <view class="p-24rpx">
+      <wd-form ref="formRef" :model="formData" :rules="formRules">
+        <wd-cell-group border>
+          <!-- 友情提醒 -->
+          <view class="mb-24rpx border border-[#ffd591] rounded-16rpx bg-[#fff7e6] p-24rpx">
+            <view class="mb-12rpx flex items-center">
+              <wd-icon name="warning" color="#faad14" size="32rpx" />
+              <text class="ml-12rpx text-28rpx text-[#faad14] font-bold">友情提醒</text>
+            </view>
+            <text class="text-26rpx text-[#666]">取消后,该审批流程将自动结束。</text>
+          </view>
+
+          <!-- 取消理由 -->
+          <wd-textarea
+            v-model="formData.cancelReason"
+            prop="cancelReason"
+            label="取消理由:"
+            label-width="180rpx"
+            placeholder="请输入取消理由"
+            :maxlength="500"
+            show-word-limit
+            clearable
+          />
+        </wd-cell-group>
+        <!-- 提交按钮 -->
+        <view class="mt-48rpx">
+          <wd-button
+            type="primary"
+            block
+            :loading="submitting"
+            :disabled="submitting"
+            @click="handleSubmit"
+          >
+            确认取消
+          </wd-button>
+        </view>
+      </wd-form>
+    </view>
+  </view>
+</template>
+
+<script lang="ts" setup>
+import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
+import { computed, reactive, ref } from 'vue'
+import { useToast } from 'wot-design-uni'
+import { cancelProcessInstanceByStartUser } from '@/api/bpm/processInstance'
+import { navigateBackPlus } from '@/utils'
+
+const props = defineProps<{
+  processInstanceId: string
+  taskId?: string
+}>()
+
+definePage({
+  style: {
+    navigationBarTitleText: '',
+    navigationStyle: 'custom',
+  },
+})
+
+const processInstanceId = computed(() => props.processInstanceId)
+const taskId = computed(() => props.taskId)
+const toast = useToast()
+const submitting = ref(false)
+const formRef = ref<FormInstance>()
+
+const formData = reactive({
+  cancelReason: '',
+})
+
+const formRules = {
+  cancelReason: [
+    { required: true, message: '取消理由不能为空' },
+  ],
+}
+
+/** 返回上一页 */
+function handleBack() {
+  const backUrl = taskId.value
+    ? `/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`
+    : `/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}`
+  navigateBackPlus(backUrl)
+}
+
+/** 初始化校验 */
+if (!props.processInstanceId) {
+  toast.show('参数错误')
+}
+
+/** 提交操作 */
+async function handleSubmit() {
+  if (submitting.value)
+    return
+
+  // 使用 wd-form 的校验方法
+  const { valid } = await formRef.value!.validate()
+  if (!valid) {
+    return
+  }
+
+  submitting.value = true
+  try {
+    const result = await cancelProcessInstanceByStartUser(
+      processInstanceId.value,
+      formData.cancelReason,
+    )
+
+    if (result) {
+      toast.success('流程取消成功')
+      setTimeout(() => {
+        uni.redirectTo({
+          url: `/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}`,
+        })
+      }, 1500)
+    }
+  } catch (error) {
+    console.error('[process-cancel] 取消流程失败:', error)
+    toast.error('取消流程失败')
+  } finally {
+    submitting.value = false
+  }
+}
+</script>

+ 2 - 0
src/store/user.ts

@@ -63,6 +63,8 @@ export const useUserStore = defineStore(
     /** 获取用户信息 */
     const fetchUserInfo = async () => {
       const res = await getAuthPermissionInfo()
+      // 后端返回的用户 Id 字段为 id.
+      res.user.userId = res.user.id
       setUserInfo(res)
       return res
     }

+ 5 - 1
src/utils/constants/biz-bpm-enum.ts

@@ -151,6 +151,10 @@ export enum BpmTaskOperationButtonTypeEnum {
    * 减签
    */
   DELETE_SIGN = 8,
+  /**
+   * 流程发起人取消
+   */
+  PROCESS_START_CANCEL = 9,
   /**
    * 拒绝
    */
@@ -163,7 +167,6 @@ export enum BpmTaskOperationButtonTypeEnum {
    * 转办
    */
   TRANSFER = 3,
-
 }
 
 /**
@@ -292,6 +295,7 @@ OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.ADD_SIGN, '加签')
 OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.RETURN, '退回')
 OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.COPY, '抄送')
 OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.DELETE_SIGN, '减签')
+OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL, '取消')
 
 /**
  * 流程实例的变量枚举