4 天以前 f51c5018dc9088f393f34e3e2508d7caf80002c4
feat(approval): 支持多审批人并行审批功能

- 审批流程界面实现节点分组展示,支持同节点多个审批人
- 审批管理界面新增多选审批人功能,支持或签/会签模式配置
- 审批过程界面优化审批人显示逻辑,支持多审批人状态展示
- 危险作业审批类型新增,完善审批流程类型配置
- 审批权限控制优化,支持多审批人权限验证逻辑
- 表单校验机制调整,适配多审批人场景下的表单验证需求
已修改5个文件
406 ■■■■■ 文件已修改
src/views/collaborativeApproval/approvalManagement/index.vue 155 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/safeWorkApproval/components/approvalDia.vue 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/safeWorkApproval/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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();
};
// 加载审批配置数据(模拟)
// 加载审批配置数据:接口扁平行 → 按 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;
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;
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;
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;
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 => {