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