| | |
| | | <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> |
| | |
| | | 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> |
| | |
| | | <!-- 选择区域 --> |
| | | <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)" |
| | |
| | | :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> |
| | | |
| | | <!-- 操作按钮 --> |
| | |
| | | <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> |
| | |
| | | <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> |
| | |
| | | <!-- 底部提示 --> |
| | | <div class="bottom-tips"> |
| | | <el-icon><InfoFilled /></el-icon> |
| | | <span>提示:每个流程至少配置一个审批人,审批按顺序流转,可通过箭头调整顺序</span> |
| | | <span>提示:每个节点可配置多名审批人并选择或签/会签;或签=任意一人通过即流转,会签=需全部人通过;审批按节点顺序流转</span> |
| | | </div> |
| | | </div> |
| | | </template> |
| | |
| | | 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'; |
| | |
| | | { 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' }, |
| | | ]; |
| | | |
| | |
| | | 5: '采购审批', |
| | | 6: '报价审批', |
| | | 7: '发货审批', |
| | | 8: '危险作业审批', |
| | | 9: '用印审批', |
| | | }; |
| | | |
| | |
| | | 5: 'ShoppingCart', |
| | | 6: 'DocumentChecked', |
| | | 7: 'Van', |
| | | 8: 'WarningFilled', |
| | | 9: 'Stamp', |
| | | }; |
| | | |
| | |
| | | 5: '#909399', |
| | | 6: '#9B59B6', |
| | | 7: '#1ABC9C', |
| | | 8: '#E74C3C', |
| | | 9: '#7D3C98', |
| | | }; |
| | | |
| | |
| | | // 当前审批类型 |
| | | 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} 审批人`; |
| | | }); |
| | | |
| | | // 获取类型图标 |
| | |
| | | loadData(); |
| | | }; |
| | | |
| | | // 加载审批配置数据(模拟) |
| | | // 加载审批配置数据:接口扁平行 → 按 nodeOrder 分组合并为节点卡片(同节点多审批人) |
| | | const loadData = async () => { |
| | | loading.value = true; |
| | | try { |
| | |
| | | : 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 = []; |
| | |
| | | } |
| | | }; |
| | | |
| | | // 审批人选择变化 |
| | | 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', |
| | |
| | | .then(() => { |
| | | approverList.value.splice(index, 1); |
| | | approverList.value.forEach((item, idx) => { |
| | | item.sortOrder = idx + 1; |
| | | item.nodeOrder = idx + 1; |
| | | }); |
| | | ElMessage.success('删除成功'); |
| | | }) |
| | |
| | | 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; }); |
| | | }; |
| | | |
| | | // 后移 |
| | |
| | | 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(); |
| | |
| | | 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; |