瀏覽代碼

feat: 去掉转办入口并完善回退到上一节点

- 工作台、我的待办、流程详情移除转办按钮/选项
- 工作台和我的待办增加回退快捷入口
- 流程详情回退改为自动回退到上一个审批节点,无需手动选择
- 流程详情支持当前节点处理人补充/编辑表单材料
- FlowFormFields 保留已有数据,适配编辑场景
- 前端构建通过
ye-zhaojia 1 周之前
父節點
當前提交
f5a33ed1cb

+ 6 - 0
src/api/flow/instance.ts

@@ -51,3 +51,9 @@ export function batchDeleteInstance(ids: number[]): Promise<void> {
 export function getInstanceAttachments(id: number): Promise<Attachment[]> {
   return request.get(`/flow/instance/${id}/attachments`)
 }
+
+export function updateInstanceFormData(id: number, formData: Record<string, unknown>): Promise<void> {
+  return request.post(`/flow/instance/${id}/form-data`, {
+    formData: JSON.stringify(formData)
+  })
+}

+ 4 - 1
src/components/FlowFormFields/index.vue

@@ -113,7 +113,10 @@ const fields = computed<FormField[]>(() => {
 })
 
 // 切换流程定义时重置数据;同时为多选下拉设置数组默认值
-watch(() => props.definition?.id, () => {
+watch(() => props.definition?.id, (newId, oldId) => {
+  if (newId === oldId) return
+  // 保留已有数据(如回退后补充材料场景)
+  if (Object.keys(formData.value || {}).length > 0) return
   formData.value = {}
   for (const field of fields.value) {
     if (field.type === 'select' && field.multiple) {

+ 107 - 51
src/views/flow/execute/InstanceDetail.vue

@@ -30,8 +30,23 @@
 
       <!-- 表单数据 -->
       <div v-if="formDataDisplay" class="form-data-section">
-        <h4>表单数据</h4>
+        <div class="section-title">
+          <h4>表单数据</h4>
+          <el-button v-if="canEditForm && !formEditing" type="primary" size="small" link @click="startEditForm">补充/编辑材料</el-button>
+        </div>
+        <template v-if="formEditing">
+          <FlowFormFields
+            ref="formFieldsRef"
+            :definition="progress?.definition"
+            v-model="editFormData"
+          />
+          <div class="form-edit-actions">
+            <el-button size="small" @click="cancelEditForm">取消</el-button>
+            <el-button type="primary" size="small" :loading="savingForm" @click="saveFormData">保存</el-button>
+          </div>
+        </template>
         <FormDataDisplay
+          v-else
           :form-schema="progress?.definition?.formSchema"
           :form-data="progress?.instance?.formData"
         />
@@ -110,23 +125,17 @@
                   <el-radio label="pass">通过</el-radio>
                   <el-radio label="reject">拒绝</el-radio>
                   <el-radio label="rollback">回退</el-radio>
-                  <el-radio label="transfer">转办</el-radio>
                 </el-radio-group>
               </el-form-item>
-              <el-form-item v-if="form.action === 'rollback'" label="回退到">
-                <el-select v-model="form.targetNodeId" filterable placeholder="请选择回退节点" style="width: 100%">
-                  <el-option
-                    v-for="node in rollbackNodeOptions"
-                    :key="node.nodeId"
-                    :label="node.nodeName"
-                    :value="node.nodeId"
-                  />
-                </el-select>
-              </el-form-item>
-              <el-form-item v-if="form.action === 'transfer'" label="转给人">
-                <el-select v-model="form.transferTo" filterable placeholder="请选择转办人" style="width: 100%">
-                  <el-option v-for="u in userList" :key="u.id" :label="u.realName || u.username" :value="u.id" />
-                </el-select>
+              <el-form-item v-if="form.action === 'rollback'" label="回退说明">
+                <el-alert type="warning" :closable="false" show-icon>
+                  <template #title>
+                    提交后将自动回退到上一个审批节点
+                    <div v-if="lastRollbackRecord" class="rollback-reason">
+                      上次回退原因:{{ lastRollbackRecord.comment }}
+                    </div>
+                  </template>
+                </el-alert>
               </el-form-item>
               <el-form-item label="附件">
                 <el-upload
@@ -212,14 +221,15 @@
 <script setup lang="ts">
 import { ref, reactive, computed, watch } from 'vue'
 import { ElMessage } from 'element-plus'
-import { getProgress, getInstanceAttachments } from '@/api/flow/instance'
-import { approveTask, rejectTask, returnTask, transferTask, addSignTask, getTransferableUsers } from '@/api/flow/task'
+import { getProgress, getInstanceAttachments, updateInstanceFormData } from '@/api/flow/instance'
+import { approveTask, rejectTask, returnTask, addSignTask, getTransferableUsers } from '@/api/flow/task'
 import { uploadFile } from '@/api/file'
 import { beforeFileUpload, getFileUrl, getFileName, getUploadUrl, parseAttachments, collectAttachmentUrls } from '@/utils/file'
 import { instanceStatusText, instanceStatusTagType, taskStatusText, taskStatusType, recordActionText, recordActionType } from '@/utils/flow'
 import FilePreview from '@/components/FilePreview/index.vue'
 import FormDataDisplay from '@/components/FormDataDisplay/index.vue'
-import type { ProcessProgress, FlowTask, NodeProgress, ApprovalAction, Attachment } from '@/types/flow'
+import FlowFormFields from '@/components/FlowFormFields/index.vue'
+import type { ProcessProgress, FlowTask, ApprovalAction, Attachment } from '@/types/flow'
 import type { User } from '@/types/system'
 import { CircleCheck, Clock, Document } from '@element-plus/icons-vue'
 
@@ -243,30 +253,20 @@ const previewVisible = ref(false)
 const previewUrl = ref('')
 const progress = ref<ProcessProgress | null>(null)
 
-type ApprovalActionType = 'pass' | 'reject' | 'rollback' | 'transfer'
+type ApprovalActionType = 'pass' | 'reject' | 'rollback'
 
 const form = reactive<{
   action: ApprovalActionType
   comment: string
-  transferTo: number | undefined
-  targetNodeId: string | undefined
 }>({
   action: 'pass',
-  comment: '',
-  transferTo: undefined,
-  targetNodeId: undefined
+  comment: ''
 })
 
 const attachmentList = ref<any[]>([])
 const submitting = ref(false)
 const addSignDialogVisible = ref(false)
 const addSignUserId = ref<number | undefined>(undefined)
-
-const rollbackNodeOptions = computed<NodeProgress[]>(() => {
-  if (!progress.value) return []
-  return progress.value.nodes.filter(n => n.status === 'completed')
-})
-
 const userList = ref<User[]>([])
 
 async function loadTransferableUsers() {
@@ -276,7 +276,6 @@ async function loadTransferableUsers() {
     return
   }
   try {
-    // 按当前节点审批角色过滤可选用户,已自动排除当前登录人
     userList.value = await getTransferableUsers(task.id)
   } catch {
     userList.value = []
@@ -379,26 +378,12 @@ async function loadProgress() {
 async function submitApprove() {
   const task = myPendingTask.value
   if (!task) return
-  if (form.action === 'transfer' && !form.transferTo) {
-    ElMessage.warning('请选择转办人')
-    return
-  }
-  if (form.action === 'rollback' && !form.targetNodeId) {
-    ElMessage.warning('请选择回退节点')
-    return
-  }
   submitting.value = true
   try {
     const data: ApprovalAction = {
       action: form.action,
       comment: form.comment
     }
-    if (form.action === 'transfer' && form.transferTo) {
-      data.transferTo = Number(form.transferTo)
-    }
-    if (form.action === 'rollback' && form.targetNodeId) {
-      data.targetNodeId = form.targetNodeId
-    }
     if (attachmentList.value.length > 0) {
       data.attachmentUrls = collectAttachmentUrls(attachmentList.value)
     }
@@ -412,15 +397,10 @@ async function submitApprove() {
       case 'rollback':
         await returnTask(task.id, data)
         break
-      case 'transfer':
-        await transferTask(task.id, data)
-        break
     }
     ElMessage.success('审批提交成功')
     form.action = 'pass'
     form.comment = ''
-    form.transferTo = undefined
-    form.targetNodeId = undefined
     attachmentList.value = []
     // 刷新进度展示最新状态,不关闭抽屉
     await loadProgress()
@@ -434,8 +414,63 @@ function handleClose() {
   progress.value = null
   attachmentList.value = []
   attachments.value = []
+  cancelEditForm()
+}
+
+// 表单补充/编辑
+const formEditing = ref(false)
+const savingForm = ref(false)
+const editFormData = ref<Record<string, unknown>>({})
+const formFieldsRef = ref<{ validate: () => boolean } | null>(null)
+
+const canEditForm = computed(() => !!myPendingTask.value)
+
+function parseInstanceFormData(): Record<string, unknown> {
+  const fd = progress.value?.instance?.formData
+  if (!fd) return {}
+  if (fd !== null && typeof fd === 'object') return { ...fd as Record<string, unknown> }
+  try {
+    return JSON.parse(fd as string)
+  } catch {
+    return {}
+  }
+}
+
+function startEditForm() {
+  editFormData.value = parseInstanceFormData()
+  formEditing.value = true
+}
+
+function cancelEditForm() {
+  formEditing.value = false
+  editFormData.value = {}
 }
 
+async function saveFormData() {
+  const valid = formFieldsRef.value?.validate?.()
+  if (valid === false) return
+  savingForm.value = true
+  try {
+    await updateInstanceFormData(props.instanceId, editFormData.value)
+    ElMessage.success('材料已更新')
+    cancelEditForm()
+    await loadProgress()
+    emit('approved')
+  } finally {
+    savingForm.value = false
+  }
+}
+
+// 当前节点最近一次被回退的记录
+const lastRollbackRecord = computed(() => {
+  if (!progress.value?.records) return null
+  const currentNodeId = progress.value.instance?.currentNode
+  const list = progress.value.records.filter(r =>
+    r.nodeId === currentNodeId && (r.actionType === 'RETURN' || r.actionResult === 'RETURN')
+  )
+  return list.length > 0 ? list[list.length - 1] : null
+})
+
 function handleAddSign() {
   const task = myPendingTask.value
   if (!task) return
@@ -609,4 +644,25 @@ watch(() => props.modelValue, async (val) => {
   flex-wrap: wrap;
   gap: 8px;
 }
+.section-title {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
+}
+.section-title h4 {
+  margin: 0;
+}
+.form-edit-actions {
+  display: flex;
+  justify-content: flex-end;
+  gap: 8px;
+  margin-top: 12px;
+}
+.rollback-reason {
+  margin-top: 4px;
+  font-size: 13px;
+  color: #e6a23c;
+  line-height: 1.5;
+}
 </style>

+ 14 - 29
src/views/flow/task/todo.vue

@@ -142,8 +142,8 @@
                   </el-button>
                   <template #dropdown>
                     <el-dropdown-menu>
-                      <el-dropdown-item command="transfer">
-                        <el-icon><Switch /></el-icon>转办
+                      <el-dropdown-item command="rollback">
+                        <el-icon><Back /></el-icon>回退
                       </el-dropdown-item>
                       <el-dropdown-item command="addSign">
                         <el-icon><Plus /></el-icon>加签
@@ -204,11 +204,6 @@
               <el-option v-for="n in nextNodes" :key="n.nodeId" :label="n.nodeName" :value="n.nodeId" />
             </el-select>
           </el-form-item>
-          <el-form-item v-if="!batchMode && form.action === 'transfer'" label="转办人">
-            <el-select v-model="form.transferTo" filterable placeholder="请选择转办人" style="width: 100%">
-              <el-option v-for="u in userList" :key="u.id" :label="u.realName || u.username" :value="u.id" />
-            </el-select>
-          </el-form-item>
           <el-form-item label="附件">
             <div v-if="!batchMode && previousAttachments.length > 0" class="previous-attachments">
               <div class="previous-title">历史附件</div>
@@ -272,8 +267,8 @@
 <script setup lang="ts">
 import { ref, reactive, onMounted, computed } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import { Check, Close, Switch, Plus, View, ArrowDown } from '@element-plus/icons-vue'
-import { listTodo, approveTask, rejectTask, transferTask, addSignTask, batchApproveTask, batchRejectTask, listNextNodes, getTransferableUsers } from '@/api/flow/task'
+import { Check, Close, Back, Plus, View, ArrowDown } from '@element-plus/icons-vue'
+import { listTodo, approveTask, rejectTask, returnTask, addSignTask, batchApproveTask, batchRejectTask, listNextNodes, getTransferableUsers } from '@/api/flow/task'
 import { listEnabled } from '@/api/flow/definition'
 import { uploadFile } from '@/api/file'
 import { getInstanceAttachments } from '@/api/flow/instance'
@@ -417,10 +412,9 @@ const batchMode = ref(false)
 const currentTask = ref<FlowTask | null>(null)
 const currentTasks = ref<FlowTask[]>([])
 const submitting = ref(false)
-const form = reactive<ApprovalAction & { transferTo?: number }>({
+const form = reactive<ApprovalAction>({
   action: 'pass',
-  comment: '',
-  transferTo: undefined
+  comment: ''
 })
 const attachmentList = ref<any[]>([])
 const previousAttachments = ref<Attachment[]>([])
@@ -428,7 +422,7 @@ const previewVisible = ref(false)
 const previewUrl = ref('')
 
 const drawerActionText = computed(() => {
-  const map: Record<string, string> = { pass: '通过', reject: '拒绝', transfer: '转办' }
+  const map: Record<string, string> = { pass: '通过', reject: '拒绝', rollback: '回退' }
   return map[form.action] || '审批'
 })
 
@@ -440,7 +434,6 @@ const drawerTitle = computed(() => {
 function resetApproveForm() {
   form.action = 'pass'
   form.comment = ''
-  form.transferTo = undefined
   attachmentList.value = []
   previousAttachments.value = []
   currentTask.value = null
@@ -453,9 +446,6 @@ function buildActionData(): ApprovalAction {
     action: form.action,
     comment: form.comment
   }
-  if (form.action === 'transfer' && form.transferTo) {
-    data.transferTo = Number(form.transferTo)
-  }
   if (form.action === 'pass' && form.targetNodeId) {
     data.targetNodeId = form.targetNodeId
   }
@@ -491,17 +481,12 @@ function openBatchDrawer(action: 'pass' | 'reject') {
   approveDrawerVisible.value = true
 }
 
-async function openTransfer(row: FlowTask) {
+async function openRollback(row: FlowTask) {
   resetApproveForm()
   currentTask.value = row
-  form.action = 'transfer'
+  form.action = 'rollback'
   batchMode.value = false
-  // 转办选人:按当前节点审批角色过滤可选用户,已自动排除当前登录人
-  try {
-    userList.value = await getTransferableUsers(row.id)
-  } catch {
-    userList.value = []
-  }
+  nextNodes.value = []
   await loadPreviousAttachments(row.instanceId)
   approveDrawerVisible.value = true
 }
@@ -539,8 +524,8 @@ async function submitApprove() {
       case 'reject':
         await rejectTask(taskId, data)
         break
-      case 'transfer':
-        await transferTask(taskId, data)
+      case 'rollback':
+        await returnTask(taskId, data)
         break
     }
     ElMessage.success('审批成功')
@@ -633,8 +618,8 @@ async function submitAddSign() {
 
 function handleMoreAction(cmd: string, row: FlowTask) {
   switch (cmd) {
-    case 'transfer':
-      openTransfer(row)
+    case 'rollback':
+      openRollback(row)
       break
     case 'addSign':
       handleAddSign(row)

+ 3 - 3
src/views/workbench/components/TaskCard.vue

@@ -24,7 +24,7 @@
     <div v-if="showActions" class="task-card-actions">
       <el-button circle type="success" size="small" :icon="Check" @click.stop="$emit('approve', task)" />
       <el-button circle type="danger" size="small" :icon="Close" @click.stop="$emit('reject', task)" />
-      <el-button circle type="warning" size="small" :icon="Switch" @click.stop="$emit('transfer', task)" />
+      <el-button circle type="warning" size="small" :icon="Back" @click.stop="$emit('rollback', task)" />
       <el-button circle type="info" size="small" :icon="Plus" @click.stop="$emit('add-sign', task)" />
       <el-button circle type="primary" size="small" :icon="View" @click.stop="$emit('detail', task)" />
     </div>
@@ -36,7 +36,7 @@
 
 <script setup lang="ts">
 import { computed } from 'vue'
-import { Check, Close, Switch, Plus, View } from '@element-plus/icons-vue'
+import { Check, Close, Back, Plus, View } from '@element-plus/icons-vue'
 import type { FlowTask } from '@/types/flow'
 
 const props = defineProps<{
@@ -47,7 +47,7 @@ const props = defineProps<{
 defineEmits<{
   (e: 'approve', task: FlowTask): void
   (e: 'reject', task: FlowTask): void
-  (e: 'transfer', task: FlowTask): void
+  (e: 'rollback', task: FlowTask): void
   (e: 'add-sign', task: FlowTask): void
   (e: 'detail', task: FlowTask): void
 }>()

+ 12 - 27
src/views/workbench/index.vue

@@ -63,7 +63,7 @@
               <div class="task-card-actions">
                 <el-button circle type="success" size="small" :icon="Check" @click.stop="openQuickApprove(row, 'pass')" />
                 <el-button circle type="danger" size="small" :icon="Close" @click.stop="openQuickApprove(row, 'reject')" />
-                <el-button circle type="warning" size="small" :icon="Switch" @click.stop="openTransfer(row)" />
+                <el-button circle type="warning" size="small" :icon="Back" @click.stop="openRollback(row)" />
                 <el-button circle type="info" size="small" :icon="Plus" @click.stop="handleAddSign(row)" />
                 <el-button circle type="primary" size="small" :icon="View" @click.stop="handleDetail(row)" />
               </div>
@@ -80,7 +80,7 @@
               :task="row"
               @approve="openQuickApprove($event, 'pass')"
               @reject="openQuickApprove($event, 'reject')"
-              @transfer="openTransfer"
+              @rollback="openRollback"
               @add-sign="handleAddSign"
               @detail="handleDetail"
             />
@@ -96,7 +96,7 @@
               :task="row"
               @approve="openQuickApprove($event, 'pass')"
               @reject="openQuickApprove($event, 'reject')"
-              @transfer="openTransfer"
+              @rollback="openRollback"
               @add-sign="handleAddSign"
               @detail="handleDetail"
             />
@@ -192,11 +192,6 @@
               <el-option v-for="n in nextNodes" :key="n.nodeId" :label="n.nodeName" :value="n.nodeId" />
             </el-select>
           </el-form-item>
-          <el-form-item v-if="form.action === 'transfer'" label="转办人">
-            <el-select v-model="form.transferTo" filterable placeholder="请选择转办人" style="width: 100%">
-              <el-option v-for="u in userList" :key="u.id" :label="u.realName || u.username" :value="u.id" />
-            </el-select>
-          </el-form-item>
           <el-form-item label="附件">
             <div v-if="previousAttachments.length > 0" class="previous-attachments">
               <div class="previous-title">历史附件</div>
@@ -257,8 +252,8 @@
 import { ref, reactive, onMounted, computed } from 'vue'
 import { useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
-import { Check, Close, Switch, Plus, View } from '@element-plus/icons-vue'
-import { listTodo, approveTask, rejectTask, transferTask, addSignTask, listNextNodes, getTransferableUsers } from '@/api/flow/task'
+import { Check, Close, Back, Plus, View } from '@element-plus/icons-vue'
+import { listTodo, approveTask, rejectTask, returnTask, addSignTask, listNextNodes, getTransferableUsers } from '@/api/flow/task'
 import { listHandled, listCc } from '@/api/flow/task'
 import { listMyInstance, getInstanceAttachments } from '@/api/flow/instance'
 import { uploadFile } from '@/api/file'
@@ -357,10 +352,9 @@ function goToExecute() {
 const approveDrawerVisible = ref(false)
 const currentTask = ref<FlowTask | null>(null)
 const submitting = ref(false)
-const form = reactive<ApprovalAction & { transferTo?: number }>({
+const form = reactive<ApprovalAction>({
   action: 'pass',
-  comment: '',
-  transferTo: undefined
+  comment: ''
 })
 const attachmentList = ref<any[]>([])
 const previousAttachments = ref<Attachment[]>([])
@@ -368,7 +362,7 @@ const previewVisible = ref(false)
 const previewUrl = ref('')
 
 const drawerActionText = computed(() => {
-  const map: Record<string, string> = { pass: '通过', reject: '拒绝', transfer: '转办' }
+  const map: Record<string, string> = { pass: '通过', reject: '拒绝', rollback: '回退' }
   return map[form.action] || '审批'
 })
 
@@ -377,7 +371,6 @@ const drawerTitle = computed(() => `${drawerActionText.value}审批`)
 function resetApproveForm() {
   form.action = 'pass'
   form.comment = ''
-  form.transferTo = undefined
   attachmentList.value = []
   previousAttachments.value = []
   currentTask.value = null
@@ -385,9 +378,6 @@ function resetApproveForm() {
 
 function buildActionData(): ApprovalAction {
   const data: ApprovalAction = { action: form.action, comment: form.comment }
-  if (form.action === 'transfer' && form.transferTo) {
-    data.transferTo = Number(form.transferTo)
-  }
   if (form.action === 'pass' && form.targetNodeId) {
     data.targetNodeId = form.targetNodeId
   }
@@ -410,16 +400,11 @@ async function openQuickApprove(row: FlowTask, action: 'pass' | 'reject') {
   approveDrawerVisible.value = true
 }
 
-async function openTransfer(row: FlowTask) {
+async function openRollback(row: FlowTask) {
   resetApproveForm()
   currentTask.value = row
-  form.action = 'transfer'
-  // 转办选人:按当前节点审批角色过滤可选用户,已自动排除当前登录人
-  try {
-    userList.value = await getTransferableUsers(row.id)
-  } catch {
-    userList.value = []
-  }
+  form.action = 'rollback'
+  nextNodes.value = []
   await loadPreviousAttachments(row.instanceId)
   approveDrawerVisible.value = true
 }
@@ -432,7 +417,7 @@ async function submitApprove() {
     const taskId = currentTask.value.id
     if (form.action === 'pass') await approveTask(taskId, data)
     else if (form.action === 'reject') await rejectTask(taskId, data)
-    else if (form.action === 'transfer') await transferTask(taskId, data)
+    else if (form.action === 'rollback') await returnTask(taskId, data)
     ElMessage.success('审批成功')
     approveDrawerVisible.value = false
     loadData()