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/approvalManagement/index.vue |  155 ++++++++++++++++++++++++++++++++-------------------
 1 files changed, 98 insertions(+), 57 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;

--
Gitblit v1.9.3