From f51c5018dc9088f393f34e3e2508d7caf80002c4 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期三, 19 八月 2026 15:37:16 +0800
Subject: [PATCH] feat(approval): 支持多审批人并行审批功能

---
 src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue |  100 ++++++++------
 src/views/safeProduction/safeWorkApproval/index.vue                        |   16 +
 src/views/collaborativeApproval/approvalProcess/index.vue                  |   16 +
 src/views/safeProduction/safeWorkApproval/components/approvalDia.vue       |  119 +++++++++-------
 src/views/collaborativeApproval/approvalManagement/index.vue               |  155 ++++++++++++++--------
 5 files changed, 244 insertions(+), 162 deletions(-)

diff --git a/src/views/collaborativeApproval/approvalManagement/index.vue b/src/views/collaborativeApproval/approvalManagement/index.vue
index e265cfe..41a8dda 100644
--- a/src/views/collaborativeApproval/approvalManagement/index.vue
+++ b/src/views/collaborativeApproval/approvalManagement/index.vue
@@ -36,7 +36,7 @@
             <div class="header-info">
               <span class="type-name">{{ currentApproveTypeName }}</span>
               <el-tag :type="approverList.length > 0 ? 'success' : 'warning'" size="small" effect="light">
-                {{ approverList.length > 0 ? `宸查厤缃� ${approverList.length} 涓鎵逛汉` : '鏈厤缃鎵逛汉' }}
+                {{ approverCountText }}
               </el-tag>
             </div>
           </div>
@@ -62,18 +62,18 @@
             class="flow-item"
           >
             <!-- 瀹℃壒鑺傜偣鍗$墖 -->
-            <div class="node-card" :class="{ 'empty': !item.approverId }">
+            <div class="node-card" :class="{ 'empty': !(item.approverIds && item.approverIds.length) }">
               <!-- 椤堕儴搴忓彿鍜岀骇鍒� -->
               <div class="node-badge">{{ index + 1 }}</div>
-              
+
               <!-- 澶村儚鍖哄煙 -->
               <div class="node-avatar-section">
-                <div 
-                  class="node-avatar" 
-                  :class="{ 'has-user': item.approverId }"
-                  :style="item.approverId ? { backgroundColor: getAvatarColor(item.approverName) } : {}"
+                <div
+                  class="node-avatar"
+                  :class="{ 'has-user': item.approverIds && item.approverIds.length }"
+                  :style="item.approverIds && item.approverIds.length ? { backgroundColor: getAvatarColor(item.approverNames?.[0] || '') } : {}"
                 >
-                  <span v-if="item.approverId">{{ item.approverName.charAt(0) }}</span>
+                  <span v-if="item.approverIds && item.approverIds.length">{{ (item.approverNames?.[0] || '?').charAt(0) }}</span>
                   <el-icon v-else :size="24"><User /></el-icon>
                 </div>
                 <div class="node-level">{{ getLevelText(index) }}</div>
@@ -82,8 +82,11 @@
               <!-- 閫夋嫨鍖哄煙 -->
               <div class="node-select-section">
                 <el-select
-                  v-model="item.approverId"
-                  placeholder="閫夋嫨瀹℃壒浜�"
+                  v-model="item.approverIds"
+                  multiple
+                  collapse-tags
+                  collapse-tags-tooltip
+                  placeholder="閫夋嫨瀹℃壒浜猴紙鍙閫夛級"
                   filterable
                   size="default"
                   @change="(val) => handleApproverChange(val, item)"
@@ -95,6 +98,13 @@
                     :value="user.userId"
                   />
                 </el-select>
+                <div class="node-mode">
+                  <span class="mode-label">瀹℃壒鏂瑰紡</span>
+                  <el-radio-group v-model="item.approveMode" size="small">
+                    <el-radio-button value="or">鎴栫</el-radio-button>
+                    <el-radio-button value="and">浼氱</el-radio-button>
+                  </el-radio-group>
+                </div>
               </div>
 
               <!-- 鎿嶄綔鎸夐挳 -->
@@ -150,7 +160,7 @@
               <div class="add-icon-wrapper">
                 <el-icon :size="28"><Plus /></el-icon>
               </div>
-              <span class="add-text">鏂板瀹℃壒浜�</span>
+              <span class="add-text">鏂板瀹℃壒鑺傜偣</span>
             </div>
           </div>
         </div>
@@ -166,7 +176,7 @@
           <div class="empty-subtext">鐐瑰嚮涓嬫柟鎸夐挳娣诲姞绗竴涓鎵逛汉</div>
           <el-button type="primary" size="large" @click="handleAdd" class="empty-add-btn">
             <el-icon><Plus /></el-icon>
-            鏂板瀹℃壒浜�
+            鏂板瀹℃壒鑺傜偣
           </el-button>
         </div>
       </div>
@@ -175,7 +185,7 @@
     <!-- 搴曢儴鎻愮ず -->
     <div class="bottom-tips">
       <el-icon><InfoFilled /></el-icon>
-      <span>鎻愮ず锛氭瘡涓祦绋嬭嚦灏戦厤缃竴涓鎵逛汉锛屽鎵规寜椤哄簭娴佽浆锛屽彲閫氳繃绠ご璋冩暣椤哄簭</span>
+      <span>鎻愮ず锛氭瘡涓妭鐐瑰彲閰嶇疆澶氬悕瀹℃壒浜哄苟閫夋嫨鎴栫/浼氱锛涙垨绛�=浠绘剰涓�浜洪�氳繃鍗虫祦杞紝浼氱=闇�鍏ㄩ儴浜洪�氳繃锛涘鎵规寜鑺傜偣椤哄簭娴佽浆</span>
     </div>
   </div>
 </template>
@@ -186,7 +196,7 @@
 import {
   Plus, ArrowLeft, Delete, Check, RefreshLeft, Setting,
   Suitcase, Calendar, Location, Money, ShoppingCart, DocumentChecked,
-  Van, ArrowRight, User, InfoFilled
+  Van, ArrowRight, User, InfoFilled, Stamp, WarningFilled
 } from '@element-plus/icons-vue';
 import { getApproveProcessConfigNodeList, addApproveProcessConfigNode } from '@/api/collaborativeApproval/approvalManagement';
 import { userListNoPage } from '@/api/system/user';
@@ -203,6 +213,7 @@
   { value: '5', label: '閲囪喘瀹℃壒', icon: 'ShoppingCart', color: '#909399' },
   { value: '6', label: '鎶ヤ环瀹℃壒', icon: 'DocumentChecked', color: '#9B59B6' },
   { value: '7', label: '鍙戣揣瀹℃壒', icon: 'Van', color: '#1ABC9C' },
+  { value: '8', label: '鍗遍櫓浣滀笟瀹℃壒', icon: 'WarningFilled', color: '#E74C3C' },
   { value: '9', label: '鐢ㄥ嵃瀹℃壒', icon: 'Stamp', color: '#7D3C98' },
 ];
 
@@ -215,6 +226,7 @@
   5: '閲囪喘瀹℃壒',
   6: '鎶ヤ环瀹℃壒',
   7: '鍙戣揣瀹℃壒',
+  8: '鍗遍櫓浣滀笟瀹℃壒',
   9: '鐢ㄥ嵃瀹℃壒',
 };
 
@@ -227,6 +239,7 @@
   5: 'ShoppingCart',
   6: 'DocumentChecked',
   7: 'Van',
+  8: 'WarningFilled',
   9: 'Stamp',
 };
 
@@ -239,6 +252,7 @@
   5: '#909399',
   6: '#9B59B6',
   7: '#1ABC9C',
+  8: '#E74C3C',
   9: '#7D3C98',
 };
 
@@ -253,6 +267,14 @@
 // 褰撳墠瀹℃壒绫诲瀷
 const currentApproveType = computed(() => {
   return Number(activeTab.value);
+});
+
+// 澶撮儴缁熻鏂囨锛氳妭鐐规暟 / 瀹℃壒浜烘暟
+const approverCountText = computed(() => {
+  const nodes = approverList.value.length;
+  const users = approverList.value.reduce((sum, item) => sum + (item.approverIds ? item.approverIds.length : 0), 0);
+  if (nodes === 0) return '鏈厤缃鎵逛汉';
+  return `宸查厤缃� ${nodes} 鑺傜偣 / ${users} 瀹℃壒浜篳;
 });
 
 // 鑾峰彇绫诲瀷鍥炬爣
@@ -295,7 +317,7 @@
   loadData();
 };
 
-// 鍔犺浇瀹℃壒閰嶇疆鏁版嵁锛堟ā鎷燂級
+// 鍔犺浇瀹℃壒閰嶇疆鏁版嵁锛氭帴鍙f墎骞宠 鈫� 鎸� nodeOrder 鍒嗙粍鍚堝苟涓鸿妭鐐瑰崱鐗囷紙鍚岃妭鐐瑰瀹℃壒浜猴級
 const loadData = async () => {
   loading.value = true;
   try {
@@ -307,12 +329,19 @@
         : Array.isArray(res?.data?.records)
           ? res.data.records
           : [];
-    const data = source.map((item, index) => ({
-      ...item,
-      sortOrder: item.nodeOrder ?? item.sortOrder ?? index + 1,
-    }));
-    approverList.value = data.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
-    originalList.value = JSON.parse(JSON.stringify(approverList.value));
+    const map = new Map();
+    source.forEach(item => {
+      const order = item.nodeOrder ?? 1;
+      if (!map.has(order)) {
+        map.set(order, { nodeOrder: order, approverIds: [], approverNames: [], approveMode: item.approveMode || 'or' });
+      }
+      const card = map.get(order);
+      card.approverIds.push(item.approverId);
+      if (item.approverName) card.approverNames.push(item.approverName);
+    });
+    const cards = [...map.values()].sort((a, b) => a.nodeOrder - b.nodeOrder);
+    approverList.value = cards;
+    originalList.value = JSON.parse(JSON.stringify(cards));
   } catch (error) {
     approverList.value = [];
     originalList.value = [];
@@ -332,29 +361,28 @@
   }
 };
 
-// 瀹℃壒浜洪�夋嫨鍙樺寲
-const handleApproverChange = (userId, row) => {
-  const user = userList.value.find((u) => u.userId === userId);
-  if (user) {
-    row.approverName = user.nickName;
-  }
+// 瀹℃壒浜洪�夋嫨鍙樺寲锛堝閫夛級
+const handleApproverChange = (ids, row) => {
+  row.approverNames = (ids || []).map(id => {
+    const user = userList.value.find((u) => u.userId === id);
+    return user ? user.nickName : '';
+  }).filter(Boolean);
 };
 
-// 鏂板瀹℃壒浜�
+// 鏂板瀹℃壒鑺傜偣
 const handleAdd = () => {
   const newOrder = approverList.value.length + 1;
   approverList.value.push({
-    id: null,
-    approveType: currentApproveType.value,
-    approverId: null,
-    approverName: '',
-    sortOrder: newOrder,
+    nodeOrder: newOrder,
+    approverIds: [],
+    approverNames: [],
+    approveMode: 'or',
   });
 };
 
-// 鍒犻櫎瀹℃壒浜�
+// 鍒犻櫎瀹℃壒鑺傜偣
 const handleDelete = (index) => {
-  ElMessageBox.confirm('纭畾鍒犻櫎璇ュ鎵逛汉鍚楋紵', '鎻愮ず', {
+  ElMessageBox.confirm('纭畾鍒犻櫎璇ュ鎵硅妭鐐瑰悧锛�', '鎻愮ず', {
     confirmButtonText: '纭畾',
     cancelButtonText: '鍙栨秷',
     type: 'warning',
@@ -362,7 +390,7 @@
     .then(() => {
       approverList.value.splice(index, 1);
       approverList.value.forEach((item, idx) => {
-        item.sortOrder = idx + 1;
+        item.nodeOrder = idx + 1;
       });
       ElMessage.success('鍒犻櫎鎴愬姛');
     })
@@ -375,8 +403,7 @@
   const temp = approverList.value[index];
   approverList.value[index] = approverList.value[index - 1];
   approverList.value[index - 1] = temp;
-  approverList.value[index].sortOrder = index + 1;
-  approverList.value[index - 1].sortOrder = index;
+  approverList.value.forEach((item, idx) => { item.nodeOrder = idx + 1; });
 };
 
 // 鍚庣Щ
@@ -385,38 +412,38 @@
   const temp = approverList.value[index];
   approverList.value[index] = approverList.value[index + 1];
   approverList.value[index + 1] = temp;
-  approverList.value[index].sortOrder = index + 1;
-  approverList.value[index + 1].sortOrder = index + 2;
+  approverList.value.forEach((item, idx) => { item.nodeOrder = idx + 1; });
 };
 
-// 淇濆瓨閰嶇疆
+// 淇濆瓨閰嶇疆锛氳妭鐐瑰崱鐗囨寜瀹℃壒浜哄睍寮�涓哄琛� payload
 const handleSave = async () => {
   if (approverList.value.length === 0) {
-    ElMessage.warning('璇疯嚦灏戦厤缃竴涓鎵逛汉');
+    ElMessage.warning('璇疯嚦灏戦厤缃竴涓鎵硅妭鐐�');
     return;
   }
 
-  const hasEmptyApprover = approverList.value.some((item) => !item.approverId);
+  const hasEmptyApprover = approverList.value.some((item) => !item.approverIds || item.approverIds.length === 0);
   if (hasEmptyApprover) {
-    ElMessage.warning('璇烽�夋嫨鎵�鏈夊鎵逛汉');
-    return;
-  }
-
-  const approverIds = approverList.value.map((item) => item.approverId);
-  const uniqueIds = [...new Set(approverIds)];
-  if (uniqueIds.length !== approverIds.length) {
-    ElMessage.warning('瀹℃壒浜轰笉鑳介噸澶�');
+    ElMessage.warning('璇蜂负姣忎釜鑺傜偣閫夋嫨瀹℃壒浜�');
     return;
   }
 
   saveLoading.value = true;
   try {
-    const payload = approverList.value.map((item, index) => ({
-      approveType: currentApproveType.value,
-      nodeOrder: index + 1,
-      approverId: item.approverId,
-      approverName: item.approverName,
-    }));
+    const payload = [];
+    approverList.value.forEach((card) => {
+      const uniqueIds = [...new Set(card.approverIds || [])];
+      uniqueIds.forEach(id => {
+        const user = userList.value.find(u => u.userId === id);
+        payload.push({
+          approveType: currentApproveType.value,
+          nodeOrder: card.nodeOrder,
+          approverId: id,
+          approverName: user ? user.nickName : '',
+          approveMode: card.approveMode || 'or',
+        });
+      });
+    });
     await addApproveProcessConfigNode(payload);
     ElMessage.success('淇濆瓨鎴愬姛');
     await loadData();
@@ -684,6 +711,20 @@
   width: 100%;
 }
 
+.node-mode {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-top: 10px;
+  padding-top: 10px;
+  border-top: 1px dashed var(--el-border-color-light, #ebeef5);
+}
+
+.mode-label {
+  font-size: 12px;
+  color: var(--el-text-color-secondary, #909399);
+}
+
 .node-actions {
   display: flex;
   justify-content: center;
diff --git a/src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue b/src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue
index 90802a5..425c3b6 100644
--- a/src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue
+++ b/src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue
@@ -173,41 +173,43 @@
       </div>
 
       <el-form :model="{ activities }" ref="formRef" label-position="top">
-        <el-steps :active="getActiveStep()" finish-status="success" process-status="process" align-center direction="vertical">
+                <el-steps :active="getActiveStep()" finish-status="success" process-status="process" align-center direction="vertical">
           <el-step
-            v-for="(activity, index) in activities"
-            :key="index"
-						finish-status="success"
-            :title="getNodeTitle(index, activities.length)"
-            :description="activity.approveNodeUser"
-            :icon="getNodeIcon(activity, index)"
+            v-for="(group, gIndex) in stepGroups"
+            :key="gIndex"
+            finish-status="success"
+            :title="getNodeTitle(gIndex, stepGroups.length)"
+            :icon="getNodeIcon(group)"
           >
-						<template #icon>
-							<el-icon v-if="activity.approveNodeStatus === 2" color="red" :size="22"><WarningFilled /></el-icon>
-							<el-icon v-else-if="activity.isShen" color="#1890ff" :size="22"><Edit /></el-icon>
-							<el-icon v-else-if="activity.approveNodeStatus === 1" color="#67C23A" :size="26"><Check /></el-icon>
-							<el-icon v-else color="#C0C4CC" :size="22"><MoreFilled /></el-icon>
-						</template>
+            <template #icon>
+              <el-icon v-if="group.some(a => a.approveNodeStatus === 2)" color="red" :size="22"><WarningFilled /></el-icon>
+              <el-icon v-else-if="group.some(a => a.isShen)" color="#1890ff" :size="22"><Edit /></el-icon>
+              <el-icon v-else-if="group.every(a => a.approveNodeStatus === 1)" color="#67C23A" :size="26"><Check /></el-icon>
+              <el-icon v-else color="#C0C4CC" :size="22"><MoreFilled /></el-icon>
+            </template>
             <template #title>
-              <span style="color: #000000">{{ getNodeTitle(index, activities.length) }}</span>
+              <span style="color: #000000">{{ getNodeTitle(gIndex, stepGroups.length) }}</span>
+              <el-tag :type="group[0]?.approveMode === 'and' ? 'warning' : 'info'" size="small" style="margin-left: 6px">{{ group[0]?.approveMode === 'and' ? '浼氱' : '鎴栫' }}</el-tag>
             </template>
             <template #description>
-              <div class="node-user">
-                <div class="avatar-wrapper">
-                  <img :src="userStore.avatar" class="user-avatar" alt=""/>
+              <div v-for="(activity, aIndex) in group" :key="activity.id || aIndex" class="node-user-item">
+                <div class="node-user">
+                  <div class="avatar-wrapper">
+                    <img :src="userStore.avatar" class="user-avatar" alt=""/>
+                  </div>
+                  <span style="color: #000000">{{ activity.approveNodeUser }}锛坽{ activity.isApproval }}锛�</span>
                 </div>
-                <span style="color: #000000">{{ activity.approveNodeUser }}-{{activity.isApproval}}</span>
-              </div>
-              <div v-if="!activity.isShen" class="node-reason">
-                <span>瀹℃壒鎰忚锛�</span>{{ activity.approveNodeReason }}
-              </div>
-              <div v-else-if="activity.isShen">
-                <el-form-item
-                  :prop="'activities.' + index + '.approveNodeReason'"
-                  :rules="[{ required: true, message: '瀹℃壒鎰忚涓嶈兘涓虹┖', trigger: 'blur' }]"
-                >
-                  <el-input v-model="activity.approveNodeReason" clearable type="textarea" :disabled="operationType === 'view'"></el-input>
-                </el-form-item>
+                <div v-if="!activity.isShen" class="node-reason">
+                  <span>瀹℃壒鎰忚锛�</span>{{ activity.approveNodeReason }}
+                </div>
+                <div v-else-if="activity.isShen">
+                  <el-form-item
+                    :prop="'activities.' + getActivityIndex(activity) + '.approveNodeReason'"
+                    :rules="[{ required: true, message: '瀹℃壒鎰忚涓嶈兘涓虹┖', trigger: 'blur' }]"
+                  >
+                    <el-input v-model="activity.approveNodeReason" clearable type="textarea" :disabled="operationType === 'view'"></el-input>
+                  </el-form-item>
+                </div>
               </div>
             </template>
           </el-step>
@@ -281,19 +283,31 @@
   return '瀹℃壒';
 };
 
-// 鑾峰彇褰撳墠婵�娲绘楠�
+// 鑾峰彇褰撳墠婵�娲绘楠わ紙鍒嗙粍鍚庯細鍚緟瀹′汉鐨勭粍涓嬫爣锛屾棤鍒欒繑鍥炵粍鎬绘暟=鍏ㄩ儴瀹屾垚锛�
 const getActiveStep = () => {
-  // 濡傛灉鎵�鏈� isShen 閮戒负 false锛岃繑鍥炴渶鍚庝竴涓楠わ紙鍏ㄩ儴瀹屾垚锛�
-  const hasActive = activities.value.some(a => a.isShen === true);
-  if (!hasActive) return activities.value.length;
-  // 褰撳墠鑺傜偣绱㈠紩
-  return activities.value.findIndex(a => a.isShen  == true);
+  const idx = stepGroups.value.findIndex(group => group.some(a => a.isShen === true));
+  if (idx === -1) return stepGroups.value.length;
+  return idx;
 };
-// 姝ラicon
-const getNodeIcon = (activity, index) => {
-  if (activity.approveNodeStatus === 2) return 'el-icon-warning'; // 涓嶉�氳繃
-  if (activity.isShen) return 'Edit';
+// 姝ラicon锛堢粍绾э細浠讳竴浜洪┏鍥�/浠讳竴浜哄緟瀹★級
+const getNodeIcon = (group) => {
+  if (group.some(a => a.approveNodeStatus === 2)) return 'el-icon-warning'; // 涓嶉�氳繃
+  if (group.some(a => a.isShen)) return 'Edit';
   return '';
+};
+// 瀹℃壒鑺傜偣鍒嗙粍锛堝悓涓� approveNodeOrder = 鍚屼竴鑺傜偣鐨勫鍚嶅鎵逛汉锛屾垨绛�/浼氱锛�
+const stepGroups = computed(() => {
+  const map = new Map();
+  (activities.value || []).forEach(a => {
+    const order = a.approveNodeOrder;
+    if (!map.has(order)) map.set(order, []);
+    map.get(order).push(a);
+  });
+  return Array.from(map.values());
+});
+// 鑾峰彇鑺傜偣鎴愬憳鍦� activities 涓殑绱㈠紩锛堢敤浜庤〃鍗曟牎楠� prop锛�
+const getActivityIndex = (activity) => {
+  return activities.value.findIndex(a => a.id === activity.id);
 };
 
 // 鎵撳紑寮规
@@ -445,9 +459,8 @@
     return;
   }
   currentActivity.approveNodeStatus = status;
-  // 鍒ゆ柇鏄惁涓烘渶鍚庝竴姝�
-  const isLast = activities.value.findIndex(a => a.isShen) === activities.value.length-1;
-  updateApproveNode({ ...currentActivity, isLast }).then(() => {
+  // 娴佽浆鍒ゆ柇鐢卞悗绔畬鎴愶紝鍓嶇涓嶅啀浼� isLast
+  updateApproveNode({ ...currentActivity }).then(() => {
     proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
     closeDia();
   });
@@ -479,6 +492,9 @@
   align-items: center;
   gap: 8px;
 }
+.node-user-item {
+  margin: 8px 0;
+}
 .node-status {
   color: #1890ff;
   margin-left: 8px;
diff --git a/src/views/collaborativeApproval/approvalProcess/index.vue b/src/views/collaborativeApproval/approvalProcess/index.vue
index 6ea576b..a15b121 100644
--- a/src/views/collaborativeApproval/approvalProcess/index.vue
+++ b/src/views/collaborativeApproval/approvalProcess/index.vue
@@ -287,11 +287,11 @@
     }
   );
   
-  // 褰撳墠瀹℃壒浜哄垪
+  // 褰撳墠瀹℃壒浜哄垪锛堟敮鎸佸悓鑺傜偣澶氬鎵逛汉锛�
   baseColumns.push({
     label: "褰撳墠瀹℃壒浜�",
-    prop: "approveUserCurrentName",
-    width: 120
+    prop: "currentApproverNames",
+    width: 180
   });
   
   // 鎿嶄綔鍒�
@@ -318,11 +318,10 @@
         openApprovalDia("approval", row);
       },
       disabled: (row) =>
-        row.approveUserCurrentId == null ||
         row.approveStatus == 2 ||
         row.approveStatus == 3 ||
         row.approveStatus == 4 ||
-        row.approveUserCurrentId !== userStore.id
+        !row._canApprove
     },
     {
       name: "璇︽儏",
@@ -387,7 +386,12 @@
   tableLoading.value = true;
   approveProcessListPage({...page, ...searchForm.value, approveType: currentApproveType.value}).then(res => {
     tableLoading.value = false;
-    tableData.value = res.data.records
+    // 瀵屽寲琛屾暟鎹細褰撳墠瀹℃壒浜烘樉绀猴紙澶氬�煎厹搴曟棫鍗曞�硷級+ 褰撳墠鐢ㄦ埛鏄惁鍙鏍�
+    tableData.value = (res.data.records || []).map(r => ({
+      ...r,
+      currentApproverNames: r.approveUserCurrentNames || r.approveUserCurrentName || '-',
+      _canApprove: String(r.approveUserCurrentIds ?? '').split(',').filter(Boolean).includes(String(userStore.id)) || r.approveUserCurrentId === userStore.id
+    }));
     page.total = res.data.total;
     // 鏇存柊褰撳墠绫诲瀷鏁伴噺
     typeCounts.value[activeTab.value] = res.data.total;
diff --git a/src/views/safeProduction/safeWorkApproval/components/approvalDia.vue b/src/views/safeProduction/safeWorkApproval/components/approvalDia.vue
index 5a3a5de..b7167be 100644
--- a/src/views/safeProduction/safeWorkApproval/components/approvalDia.vue
+++ b/src/views/safeProduction/safeWorkApproval/components/approvalDia.vue
@@ -198,29 +198,28 @@
       <el-form :model="{ activities }"
                ref="formRef"
                label-position="top">
-        <el-steps :active="getActiveStep()"
+                <el-steps :active="getActiveStep()"
                   finish-status="success"
                   process-status="process"
                   align-center
                   direction="vertical">
-          <el-step v-for="(activity, index) in activities"
-                   :key="index"
+          <el-step v-for="(group, gIndex) in stepGroups"
+                   :key="gIndex"
                    finish-status="success"
-                   :title="getNodeTitle(index, activities.length)"
-                   :description="activity.approveNodeUser"
-                   :icon="getNodeIcon(activity, index)">
+                   :title="getNodeTitle(gIndex, stepGroups.length)"
+                   :icon="getNodeIcon(group)">
             <template #icon>
-              <el-icon v-if="activity.approveNodeStatus === 2"
+              <el-icon v-if="group.some(a => a.approveNodeStatus === 2)"
                        color="red"
                        :size="22">
                 <WarningFilled />
               </el-icon>
-              <el-icon v-else-if="activity.isShen"
+              <el-icon v-else-if="group.some(a => a.isShen)"
                        color="#1890ff"
                        :size="22">
                 <Edit />
               </el-icon>
-              <el-icon v-else-if="activity.approveNodeStatus === 1"
+              <el-icon v-else-if="group.every(a => a.approveNodeStatus === 1)"
                        color="#67C23A"
                        :size="26">
                 <Check />
@@ -232,37 +231,42 @@
               </el-icon>
             </template>
             <template #title>
-              <span style="color: #000000">{{ getNodeTitle(index, activities.length) }}</span>
+              <span style="color: #000000">{{ getNodeTitle(gIndex, stepGroups.length) }}</span>
+              <el-tag :type="group[0]?.approveMode === 'and' ? 'warning' : 'info'" size="small" style="margin-left: 6px">{{ group[0]?.approveMode === 'and' ? '浼氱' : '鎴栫' }}</el-tag>
             </template>
             <template #description>
-              <div class="node-user">
-                <div class="avatar-wrapper">
-                  <img :src="userStore.avatar"
-                       class="user-avatar"
-                       alt="" />
+              <div v-for="(activity, aIndex) in group"
+                   :key="activity.id || aIndex"
+                   class="node-user-item">
+                <div class="node-user">
+                  <div class="avatar-wrapper">
+                    <img :src="userStore.avatar"
+                         class="user-avatar"
+                         alt="" />
+                  </div>
+                  <span style="color: #000000">{{ activity.approveNodeUser }}锛坽{ activity.isApproval }}锛�</span>
                 </div>
-                <span style="color: #000000">{{ activity.approveNodeUser }}-{{activity.isApproval}}</span>
-              </div>
-              <div v-if="!activity.isShen"
-                   class="node-reason">
-                <span>瀹℃壒鎰忚锛�</span>{{ activity.approveNodeReason }}
-              </div>
-              <div v-if="!activity.isShen"
-                   class="node-reason">
-                <span>绛惧悕锛�</span>
-                <img :src="activity.urlTem"
-                     class="signImg"
-                     alt=""
-                     v-if="activity.urlTem" />
-              </div>
-              <div v-else-if="activity.isShen">
-                <el-form-item :prop="'activities.' + index + '.approveNodeReason'"
-                              :rules="[{ required: true, message: '瀹℃壒鎰忚涓嶈兘涓虹┖', trigger: 'blur' }]">
-                  <el-input v-model="activity.approveNodeReason"
-                            clearable
-                            type="textarea"
-                            :disabled="operationType === 'view'"></el-input>
-                </el-form-item>
+                <div v-if="!activity.isShen"
+                     class="node-reason">
+                  <span>瀹℃壒鎰忚锛�</span>{{ activity.approveNodeReason }}
+                </div>
+                <div v-if="!activity.isShen"
+                     class="node-reason">
+                  <span>绛惧悕锛�</span>
+                  <img :src="activity.urlTem"
+                       class="signImg"
+                       alt=""
+                       v-if="activity.urlTem" />
+                </div>
+                <div v-else-if="activity.isShen">
+                  <el-form-item :prop="'activities.' + getActivityIndex(activity) + '.approveNodeReason'"
+                                :rules="[{ required: true, message: '瀹℃壒鎰忚涓嶈兘涓虹┖', trigger: 'blur' }]">
+                    <el-input v-model="activity.approveNodeReason"
+                              clearable
+                              type="textarea"
+                              :disabled="operationType === 'view'"></el-input>
+                  </el-form-item>
+                </div>
               </div>
             </template>
           </el-step>
@@ -348,19 +352,31 @@
     return "瀹℃壒";
   };
 
-  // 鑾峰彇褰撳墠婵�娲绘楠�
+  // 鑾峰彇褰撳墠婵�娲绘楠わ紙鍒嗙粍鍚庯細鍚緟瀹′汉鐨勭粍涓嬫爣锛屾棤鍒欒繑鍥炵粍鎬绘暟=鍏ㄩ儴瀹屾垚锛�
   const getActiveStep = () => {
-    // 濡傛灉鎵�鏈� isShen 閮戒负 false锛岃繑鍥炴渶鍚庝竴涓楠わ紙鍏ㄩ儴瀹屾垚锛�
-    const hasActive = activities.value.some(a => a.isShen === true);
-    if (!hasActive) return activities.value.length;
-    // 褰撳墠鑺傜偣绱㈠紩
-    return activities.value.findIndex(a => a.isShen == true);
+    const idx = stepGroups.value.findIndex(group => group.some(a => a.isShen === true));
+    if (idx === -1) return stepGroups.value.length;
+    return idx;
   };
-  // 姝ラicon
-  const getNodeIcon = (activity, index) => {
-    if (activity.approveNodeStatus === 2) return "el-icon-warning"; // 涓嶉�氳繃
-    if (activity.isShen) return "Edit";
+  // 姝ラicon锛堢粍绾э細浠讳竴浜洪┏鍥�/浠讳竴浜哄緟瀹★級
+  const getNodeIcon = group => {
+    if (group.some(a => a.approveNodeStatus === 2)) return "el-icon-warning"; // 涓嶉�氳繃
+    if (group.some(a => a.isShen)) return "Edit";
     return "";
+  };
+  // 瀹℃壒鑺傜偣鍒嗙粍锛堝悓涓� approveNodeOrder = 鍚屼竴鑺傜偣鐨勫鍚嶅鎵逛汉锛屾垨绛�/浼氱锛�
+  const stepGroups = computed(() => {
+    const map = new Map();
+    (activities.value || []).forEach(a => {
+      const order = a.approveNodeOrder;
+      if (!map.has(order)) map.set(order, []);
+      map.get(order).push(a);
+    });
+    return Array.from(map.values());
+  });
+  // 鑾峰彇鑺傜偣鎴愬憳鍦� activities 涓殑绱㈠紩锛堢敤浜庤〃鍗曟牎楠� prop锛�
+  const getActivityIndex = activity => {
+    return activities.value.findIndex(a => a.id === activity.id);
   };
 
   // 鎵撳紑寮规
@@ -474,10 +490,8 @@
       return;
     }
     currentActivity.approveNodeStatus = status;
-    // 鍒ゆ柇鏄惁涓烘渶鍚庝竴姝�
-    const isLast =
-      activities.value.findIndex(a => a.isShen) === activities.value.length - 1;
-    updateApproveNode({ ...currentActivity, isLast }).then(() => {
+    // 娴佽浆鍒ゆ柇鐢卞悗绔畬鎴愶紝鍓嶇涓嶅啀浼� isLast
+    updateApproveNode({ ...currentActivity }).then(() => {
       proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
       closeDia();
     });
@@ -506,6 +520,9 @@
     align-items: center;
     gap: 8px;
   }
+  .node-user-item {
+    margin: 8px 0;
+  }
   .node-status {
     color: #1890ff;
     margin-left: 8px;
diff --git a/src/views/safeProduction/safeWorkApproval/index.vue b/src/views/safeProduction/safeWorkApproval/index.vue
index 2d8362e..1d40ab3 100644
--- a/src/views/safeProduction/safeWorkApproval/index.vue
+++ b/src/views/safeProduction/safeWorkApproval/index.vue
@@ -179,11 +179,11 @@
       }
     );
 
-    // 褰撳墠瀹℃壒浜哄垪
+    // 褰撳墠瀹℃壒浜哄垪锛堟敮鎸佸悓鑺傜偣澶氬鎵逛汉锛�
     baseColumns.push({
       label: "褰撳墠瀹℃壒浜�",
-      prop: "approveUserCurrentName",
-      width: 120,
+      prop: "currentApproverNames",
+      width: 180,
     });
 
     // 鎿嶄綔鍒�
@@ -212,11 +212,10 @@
             openApprovalDia("approval", row);
           },
           disabled: row =>
-            row.approveUserCurrentId == null ||
             row.approveStatus == 2 ||
             row.approveStatus == 3 ||
             row.approveStatus == 4 ||
-            row.approveUserCurrentId !== userStore.id,
+            !row._canApprove,
         },
         {
           name: "璇︽儏",
@@ -273,7 +272,12 @@
     })
       .then(res => {
         tableLoading.value = false;
-        tableData.value = res.data.records;
+        // 瀵屽寲琛屾暟鎹細褰撳墠瀹℃壒浜烘樉绀猴紙澶氬�煎厹搴曟棫鍗曞�硷級+ 褰撳墠鐢ㄦ埛鏄惁鍙鏍�
+        tableData.value = (res.data.records || []).map(r => ({
+          ...r,
+          currentApproverNames: r.approveUserCurrentNames || r.approveUserCurrentName || '-',
+          _canApprove: String(r.approveUserCurrentIds ?? '').split(',').filter(Boolean).includes(String(userStore.id)) || r.approveUserCurrentId === userStore.id
+        }));
         page.total = res.data.total;
       })
       .catch(err => {

--
Gitblit v1.9.3