君哥
1.恢复之前工艺路线模块,删除字段计件/计时字段,工资定额修改成计划工时,新增计划执行人员。工艺绑定,工艺路线恢复。仅需删除bom。
2.对于新增订单需流转协同办公进行审批,审批完成流转生产管控-生产订单
3.新增审批管理,规范管理所有节点审批人。
4.修改菜单栏样式bug
已添加2个文件
已修改17个文件
2569 ■■■■■ 文件已修改
src/api/collaborativeApproval/approvalManagement.js 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/element-ui.scss 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/sidebar.scss 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/SidebarItem.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/index.vue 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalManagement/index.vue 885 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index.vue 429 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementLedger/index.vue 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/Edit.vue 182 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/New.vue 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/processRouteItem/index.vue 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/New.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/Edit.vue 195 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/New.vue 137 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/index.vue 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesQuotation/index.vue 196 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/collaborativeApproval/approvalManagement.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
// å®¡æ‰¹ç®¡ç†é…ç½®
import request from "@/utils/request";
// æŸ¥è¯¢å®¡æ‰¹æµç¨‹é…ç½®èŠ‚ç‚¹åˆ—è¡¨
export function getApproveProcessConfigNodeList(type) {
    return request({
        url: '/approveProcessConfigNode/list',
        method: 'get',
        params: { type },
    })
}
// æ–°å¢žå®¡æ‰¹æµç¨‹é…ç½®èŠ‚ç‚¹
export function addApproveProcessConfigNode(data) {
    return request({
        url: '/approveProcessConfigNode/add',
        method: 'post',
        data: data,
    })
}
src/assets/styles/element-ui.scss
@@ -150,12 +150,34 @@
.el-menu--collapse
  > div
  > .el-submenu
  > .el-submenu__title
  .el-submenu__icon-arrow {
  > .el-sub-menu
  > .el-sub-menu__title
  .el-sub-menu__icon-arrow {
  display: none;
}
/* ç¡®ä¿èœå•箭头始终显示 - ä½¿ç”¨ flex å¸ƒå±€ */
.el-sub-menu__title {
  display: flex !important;
  align-items: center !important;
  line-height: normal !important;
}
.el-sub-menu__title .el-sub-menu__icon-arrow {
  position: static !important;
  display: inline-flex !important;
  visibility: visible !important;
  width: auto !important;
  height: auto !important;
  overflow: visible !important;
  margin-left: auto !important;
  align-self: center !important;
  margin-top: 0 !important;
  margin-bottom: 0 !important;
  transform: none !important;
  top: auto !important;
}
.el-dropdown .el-dropdown-link {
  color: var(--el-color-primary) !important;
}
src/assets/styles/sidebar.scss
@@ -16,7 +16,6 @@
    width: $base-sidebar-width !important;
    height: 100%;
    position: fixed;
    font-size: 0px;
    top: 0;
    bottom: 0;
    left: 0;
@@ -95,6 +94,22 @@
        border-radius: 14px;
      }
    }
    // æ‰€æœ‰å­èœå•标题,使用 flex å¸ƒå±€è®©ç®­å¤´å’Œæ–‡å­—在一排
    .el-sub-menu__title {
      padding-right: 10px !important;
      display: flex !important;
      align-items: center !important;
      line-height: normal !important;
    }
    // é¡¶çº§å­èœå•标题
    & > .el-menu > .el-sub-menu > .el-sub-menu__title {
      padding-right: 10px !important;
      display: flex !important;
      align-items: center !important;
      line-height: normal !important;
    }
    & .theme-light .is-active > .el-sub-menu__title {
      color: var(--current-color) !important;
    }
@@ -105,7 +120,7 @@
      margin: 0 12px 6px;
      width: calc(100% - 24px);
      padding-left: 8px !important;
      padding-right: 8px !important;
      padding-right: 24px !important;
      box-sizing: border-box;
      &:hover {
@@ -227,12 +242,8 @@
            visibility: hidden;
            display: inline-block;
          }
          & > i {
            height: 0;
            width: 0;
            overflow: hidden;
            visibility: hidden;
            display: inline-block;
          & > i.el-sub-menu__icon-arrow {
            display: none;
          }
        }
      }
src/layout/components/Sidebar/SidebarItem.vue
@@ -9,7 +9,7 @@
      </app-link>
    </template>
    <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported>
    <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)">
      <template v-if="item.meta" #title>
        <svg-icon :icon-class="item.meta && item.meta.icon" />
        <span class="menu-title" :title="hasTitle(item.meta.title)">{{ item.meta.title }}</span>
src/layout/components/Sidebar/index.vue
@@ -99,6 +99,10 @@
    .el-sub-menu__title {
      color: v-bind(getMenuTextColor);
      padding-right: 10px !important;
      display: flex !important;
      align-items: center !important;
      line-height: normal !important;
    }
    :deep(.el-sub-menu.is-active > .el-sub-menu__title) {
@@ -111,8 +115,10 @@
      padding-left: 10px !important;
      padding-right: 10px !important;
      box-sizing: border-box;
      overflow: hidden;
      background-clip: padding-box;
      display: flex !important;
      align-items: center !important;
      line-height: normal !important;
    }
    :deep(.el-menu-item.is-active) {
@@ -137,6 +143,43 @@
    :deep(.el-menu-item:hover) {
      border-radius: 14px;
    }
    /* ç¡®ä¿å­èœå•箭头显示 - ä½¿ç”¨ flex å¸ƒå±€è®©ç®­å¤´å’Œæ–‡å­—在一排 */
    :deep(.el-sub-menu .el-sub-menu__title) {
      display: flex !important;
      align-items: center !important;
      justify-content: flex-start !important;
      line-height: normal !important;
    }
    :deep(.el-sub-menu .el-sub-menu__title .el-sub-menu__icon-arrow) {
      position: static !important;
      display: inline-flex !important;
      visibility: visible !important;
      width: auto !important;
      height: auto !important;
      overflow: visible !important;
      margin-left: auto !important;
      margin-right: 0 !important;
      order: 999 !important;
      align-self: center !important;
      margin-top: 0 !important;
      margin-bottom: 0 !important;
      transform: none !important;
      top: auto !important;
    }
    /* ç¡®ä¿ä¸€çº§èœå•箭头显示 */
    :deep(> .el-menu > .el-sub-menu > .el-sub-menu__title .el-sub-menu__icon-arrow) {
      display: inline-flex !important;
      visibility: visible !important;
    }
    /* ç¡®ä¿äºŒçº§èœå•箭头显示 */
    :deep(.nest-menu .el-sub-menu > .el-sub-menu__title .el-sub-menu__icon-arrow) {
      display: inline-flex !important;
      visibility: visible !important;
    }
  }
}
</style>
src/views/collaborativeApproval/approvalManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,885 @@
<template>
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <div class="header-title">
        <el-icon class="title-icon"><Setting /></el-icon>
        <span>审批流程配置</span>
      </div>
      <div class="header-desc">为不同审批类型配置审批流程和审批人</div>
    </div>
    <!-- å®¡æ‰¹ç±»åž‹åˆ‡æ¢ - ç´§å‡‘标签式 -->
    <div class="type-tabs">
      <div
        v-for="type in approveTypes"
        :key="type.value"
        class="type-tab"
        :class="{ active: activeTab === type.value }"
        @click="activeTab = type.value; handleTabChange()"
      >
        <el-icon :size="14" :style="{ color: activeTab === type.value ? type.color : '#909399' }">
          <component :is="type.icon" />
        </el-icon>
        <span class="tab-name">{{ type.label }}</span>
      </div>
    </div>
    <!-- ä¸»è¦å†…容区域 -->
    <el-card class="config-card" shadow="hover" v-loading="loading">
      <template #header>
        <div class="card-header">
          <div class="header-left">
            <div class="type-icon" :style="{ backgroundColor: getTypeColor(currentApproveType) }">
              <el-icon :size="20" color="#fff"><component :is="getTypeIcon(currentApproveType)" /></el-icon>
            </div>
            <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} ä¸ªå®¡æ‰¹äºº` : '未配置审批人' }}
              </el-tag>
            </div>
          </div>
          <div class="header-actions" v-if="approverList.length > 0">
            <el-button @click="handleReset" size="default">
              <el-icon><RefreshLeft /></el-icon>
              é‡ç½®
            </el-button>
            <el-button type="primary" @click="handleSave" :loading="saveLoading" size="default">
              <el-icon><Check /></el-icon>
              ä¿å­˜é…ç½®
            </el-button>
          </div>
        </div>
      </template>
      <!-- å®¡æ‰¹æµç¨‹å±•示 -->
      <div class="flow-wrapper" v-if="approverList.length > 0">
        <div class="flow-container">
          <div
            v-for="(item, index) in approverList"
            :key="index"
            class="flow-item"
          >
            <!-- å®¡æ‰¹èŠ‚ç‚¹å¡ç‰‡ -->
            <div class="node-card" :class="{ 'empty': !item.approverId }">
              <!-- é¡¶éƒ¨åºå·å’Œçº§åˆ« -->
              <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) } : {}"
                >
                  <span v-if="item.approverId">{{ item.approverName.charAt(0) }}</span>
                  <el-icon v-else :size="24"><User /></el-icon>
                </div>
                <div class="node-level">{{ getLevelText(index) }}</div>
              </div>
              <!-- é€‰æ‹©åŒºåŸŸ -->
              <div class="node-select-section">
                <el-select
                  v-model="item.approverId"
                  placeholder="选择审批人"
                  filterable
                  size="default"
                  @change="(val) => handleApproverChange(val, item)"
                >
                  <el-option
                    v-for="user in userList"
                    :key="user.userId"
                    :label="user.nickName"
                    :value="user.userId"
                  />
                </el-select>
              </div>
              <!-- æ“ä½œæŒ‰é’® -->
              <div class="node-actions">
                <el-button
                  type="primary"
                  circle
                  :disabled="index === 0"
                  @click="moveLeft(index)"
                  size="small"
                  class="action-btn"
                  title="前移"
                >
                  <el-icon><ArrowLeft /></el-icon>
                </el-button>
                <el-button
                  type="primary"
                  circle
                  :disabled="index === approverList.length - 1"
                  @click="moveRight(index)"
                  size="small"
                  class="action-btn"
                  title="后移"
                >
                  <el-icon><ArrowRight /></el-icon>
                </el-button>
                <el-button
                  type="danger"
                  circle
                  @click="handleDelete(index)"
                  size="small"
                  class="action-btn"
                >
                  <el-icon><Delete /></el-icon>
                </el-button>
              </div>
            </div>
            <!-- è¿žæŽ¥ç®­å¤´ -->
            <div class="arrow-connector" v-if="index < approverList.length - 1">
              <div class="arrow-line"></div>
              <el-icon class="arrow-icon"><ArrowRight /></el-icon>
            </div>
          </div>
          <!-- æ–°å¢žèŠ‚ç‚¹æŒ‰é’® - æ”¾åœ¨æµç¨‹æœ€åŽ -->
          <div class="add-node-item">
            <div class="arrow-connector" v-if="approverList.length > 0">
              <div class="arrow-line"></div>
              <el-icon class="arrow-icon"><ArrowRight /></el-icon>
            </div>
            <div class="add-node-card" @click="handleAdd">
              <div class="add-icon-wrapper">
                <el-icon :size="28"><Plus /></el-icon>
              </div>
              <span class="add-text">新增审批人</span>
            </div>
          </div>
        </div>
      </div>
      <!-- ç©ºçŠ¶æ€ -->
      <div class="empty-state" v-else>
        <div class="empty-content">
          <div class="empty-icon-wrapper">
            <el-icon :size="48" color="#c0c4cc"><User /></el-icon>
          </div>
          <div class="empty-text">暂无审批人配置</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>
    </el-card>
    <!-- åº•部提示 -->
    <div class="bottom-tips">
      <el-icon><InfoFilled /></el-icon>
      <span>提示:每个流程至少配置一个审批人,审批按顺序流转,可通过箭头调整顺序</span>
    </div>
  </div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import {
  Plus, ArrowLeft, Delete, Check, RefreshLeft, Setting,
  Suitcase, Calendar, Location, Money, ShoppingCart, DocumentChecked,
  Van, ArrowRight, User, InfoFilled, Sell
} from '@element-plus/icons-vue';
import { getApproveProcessConfigNodeList, addApproveProcessConfigNode } from '@/api/collaborativeApproval/approvalManagement';
import { userListNoPage } from '@/api/system/user';
// å½“前选中的标签页
const activeTab = ref('1');
// å®¡æ‰¹ç±»åž‹é…ç½®æ•°ç»„
const approveTypes = [
  { value: '1', label: '公出管理', icon: 'Suitcase', color: '#409EFF' },
  { value: '2', label: '请假管理', icon: 'Calendar', color: '#67C23A' },
  { value: '3', label: '出差管理', icon: 'Location', color: '#E6A23C' },
  { value: '4', label: '报销管理', icon: 'Money', color: '#F56C6C' },
  { value: '5', label: '采购审批', icon: 'ShoppingCart', color: '#909399' },
  { value: '6', label: '报价审批', icon: 'DocumentChecked', color: '#9B59B6' },
  { value: '7', label: '发货审批', icon: 'Van', color: '#1ABC9C' },
  { value: '10', label: '销售审批', icon: 'Sell', color: '#FF6B6B' },
];
// å®¡æ‰¹ç±»åž‹åç§°æ˜ å°„
const approveTypeNameMap = {
  1: '公出管理',
  2: '请假管理',
  3: '出差管理',
  4: '报销管理',
  5: '采购审批',
  6: '报价审批',
  7: '发货审批',
  10: '销售审批',
};
// å®¡æ‰¹ç±»åž‹å›¾æ ‡æ˜ å°„
const typeIconMap = {
  1: 'Suitcase',
  2: 'Calendar',
  3: 'Location',
  4: 'Money',
  5: 'ShoppingCart',
  6: 'DocumentChecked',
  7: 'Van',
  10: 'Sell',
};
// å®¡æ‰¹ç±»åž‹é¢œè‰²æ˜ å°„
const typeColorMap = {
  1: '#409EFF',
  2: '#67C23A',
  3: '#E6A23C',
  4: '#F56C6C',
  5: '#909399',
  6: '#9B59B6',
  7: '#1ABC9C',
  10: '#FF6B6B',
};
// å¤´åƒé¢œè‰²æ± 
const avatarColors = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#9B59B6', '#1ABC9C', '#FF6B6B', '#4ECDC4'];
// å½“前审批类型名称
const currentApproveTypeName = computed(() => {
  return approveTypeNameMap[activeTab.value] || '未知类型';
});
// å½“前审批类型
const currentApproveType = computed(() => {
  return Number(activeTab.value);
});
// èŽ·å–ç±»åž‹å›¾æ ‡
const getTypeIcon = (type) => typeIconMap[type] || 'Setting';
// èŽ·å–ç±»åž‹é¢œè‰²
const getTypeColor = (type) => typeColorMap[type] || '#409EFF';
// èŽ·å–å¤´åƒé¢œè‰²
const getAvatarColor = (name) => {
  if (!name) return '#C0C4CC';
  let hash = 0;
  for (let i = 0; i < name.length; i++) {
    hash = name.charCodeAt(i) + ((hash << 5) - hash);
  }
  return avatarColors[Math.abs(hash) % avatarColors.length];
};
// èŽ·å–çº§åˆ«æ–‡æœ¬
const getLevelText = (index) => {
  const texts = ['第一级', '第二级', '第三级', '第四级', '第五级', '第六级', '第七级', '第八级'];
  return texts[index] || `第${index + 1}级`;
};
// å®¡æ‰¹äººåˆ—表(真实接口)
const userList = ref([]);
// å®¡æ‰¹äººåˆ—表
const approverList = ref([]);
// åŽŸå§‹æ•°æ®ï¼Œç”¨äºŽé‡ç½®
const originalList = ref([]);
// åŠ è½½çŠ¶æ€
const loading = ref(false);
const saveLoading = ref(false);
// æ ‡ç­¾é¡µåˆ‡æ¢å¤„理
const handleTabChange = () => {
  loadData();
};
// åŠ è½½å®¡æ‰¹é…ç½®æ•°æ®ï¼ˆæ¨¡æ‹Ÿï¼‰
const loadData = async () => {
  loading.value = true;
  try {
    const res = await getApproveProcessConfigNodeList(currentApproveType.value);
    const source = Array.isArray(res?.data)
      ? res.data
      : Array.isArray(res?.rows)
        ? res.rows
        : 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));
  } catch (error) {
    approverList.value = [];
    originalList.value = [];
    ElMessage.error('加载审批配置失败');
  } finally {
    loading.value = false;
  }
};
const loadUserList = async () => {
  try {
    const res = await userListNoPage();
    userList.value = Array.isArray(res?.data) ? res.data : [];
  } catch (error) {
    userList.value = [];
    ElMessage.error('加载人员列表失败');
  }
};
// å®¡æ‰¹äººé€‰æ‹©å˜åŒ–
const handleApproverChange = (userId, row) => {
  const user = userList.value.find((u) => u.userId === userId);
  if (user) {
    row.approverName = user.nickName;
  }
};
// æ–°å¢žå®¡æ‰¹äºº
const handleAdd = () => {
  const newOrder = approverList.value.length + 1;
  approverList.value.push({
    id: null,
    approveType: currentApproveType.value,
    approverId: null,
    approverName: '',
    sortOrder: newOrder,
  });
};
// åˆ é™¤å®¡æ‰¹äºº
const handleDelete = (index) => {
  ElMessageBox.confirm('确定删除该审批人吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning',
  })
    .then(() => {
      approverList.value.splice(index, 1);
      approverList.value.forEach((item, idx) => {
        item.sortOrder = idx + 1;
      });
      ElMessage.success('删除成功');
    })
    .catch(() => {});
};
// å‰ç§»
const moveLeft = (index) => {
  if (index === 0) return;
  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;
};
// åŽç§»
const moveRight = (index) => {
  if (index === approverList.value.length - 1) return;
  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;
};
// ä¿å­˜é…ç½®
const handleSave = async () => {
  if (approverList.value.length === 0) {
    ElMessage.warning('请至少配置一个审批人');
    return;
  }
  const hasEmptyApprover = approverList.value.some((item) => !item.approverId);
  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('审批人不能重复');
    return;
  }
  saveLoading.value = true;
  try {
    const payload = approverList.value.map((item, index) => ({
      approveType: currentApproveType.value,
      nodeOrder: index + 1,
      approverId: item.approverId,
      approverName: item.approverName,
    }));
    await addApproveProcessConfigNode(payload);
    ElMessage.success('保存成功');
    await loadData();
  } catch (error) {
    ElMessage.error('保存失败');
  } finally {
    saveLoading.value = false;
  }
};
// é‡ç½®
const handleReset = () => {
  if (originalList.value.length === 0) {
    approverList.value = [];
    return;
  }
  ElMessageBox.confirm('确定要重置当前配置吗?未保存的更改将丢失。', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning',
  })
    .then(() => {
      approverList.value = JSON.parse(JSON.stringify(originalList.value));
      ElMessage.success('已重置');
    })
    .catch(() => {});
};
onMounted(async () => {
  await loadUserList();
  await loadData();
});
</script>
<style scoped>
.page-header {
  margin-bottom: 20px;
}
.header-title {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 20px;
  font-weight: 600;
  color: var(--el-text-color-primary, #303133);
  margin-bottom: 6px;
}
.title-icon {
  font-size: 24px;
  color: var(--el-color-primary, #409EFF);
}
.header-desc {
  font-size: 13px;
  color: var(--el-text-color-secondary, #909399);
  margin-left: 34px;
}
/* å®¡æ‰¹ç±»åž‹åˆ‡æ¢ - ç´§å‡‘标签式 */
.type-tabs {
  display: flex;
  gap: 4px;
  margin-bottom: 16px;
  padding: 4px;
  background: var(--el-fill-color-light, #f5f7fa);
  border-radius: 8px;
  overflow-x: auto;
}
.type-tab {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  border-radius: 6px;
  cursor: pointer;
  transition: all 0.2s ease;
  white-space: nowrap;
  font-size: 13px;
  color: var(--el-text-color-regular, #606266);
}
.type-tab:hover {
  background: var(--el-color-primary-light-9, rgba(64, 158, 255, 0.1));
  color: var(--el-color-primary, #409EFF);
}
.type-tab.active {
  background: var(--el-bg-color, #fff);
  color: var(--el-color-primary, #409EFF);
  font-weight: 600;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.tab-name {
  font-size: 13px;
}
.tab-count {
  min-width: 16px;
  height: 16px;
  padding: 0 5px;
  background: var(--el-color-success, #67C23A);
  color: #fff;
  border-radius: 8px;
  font-size: 11px;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
}
.config-card {
  margin-bottom: 16px;
  border-radius: 12px;
}
:deep(.el-card__header) {
  padding: 16px 20px;
  border-bottom: 1px solid var(--el-border-color-light, #ebeef5);
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.header-left {
  display: flex;
  align-items: center;
  gap: 14px;
}
.type-icon {
  width: 44px;
  height: 44px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.header-info {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.type-name {
  font-size: 16px;
  font-weight: 600;
  color: var(--el-text-color-primary, #303133);
}
.header-actions {
  display: flex;
  gap: 10px;
}
.flow-wrapper {
  overflow-x: auto;
  padding: 8px 4px;
}
.flow-container {
  display: flex;
  align-items: center;
  gap: 0;
  min-width: min-content;
}
.flow-item {
  display: flex;
  align-items: center;
}
.node-card {
  width: 200px;
  background: var(--el-bg-color, #fff);
  border: 2px solid var(--el-border-color, #e4e7ed);
  border-radius: 12px;
  padding: 16px;
  position: relative;
  transition: all 0.3s ease;
  flex-shrink: 0;
}
.node-card:hover {
  border-color: var(--el-color-primary, #409EFF);
  box-shadow: 0 4px 16px rgba(64, 158, 255, 0.15);
  transform: translateY(-2px);
}
.node-card.empty {
  border-style: dashed;
  border-color: var(--el-border-color, #c0c4cc);
  background: var(--el-fill-color-light, #fafbfc);
}
.node-card.empty:hover {
  border-color: var(--el-color-primary, #409EFF);
  background: var(--el-fill-color-light, #f5f7fa);
}
.node-badge {
  position: absolute;
  top: -10px;
  left: 16px;
  width: 24px;
  height: 24px;
  background: var(--el-color-primary, #409EFF);
  color: #fff;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  font-weight: 700;
  box-shadow: 0 2px 8px rgba(64, 158, 255, 0.4);
}
.node-avatar-section {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-bottom: 12px;
  margin-top: 4px;
}
.node-avatar {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--el-fill-color, #f0f2f5);
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 8px;
  color: var(--el-text-color-placeholder, #c0c4cc);
  transition: all 0.3s ease;
}
.node-avatar.has-user {
  color: #fff;
  font-size: 22px;
  font-weight: 600;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.node-level {
  font-size: 12px;
  color: var(--el-text-color-secondary, #909399);
  font-weight: 500;
}
.node-select-section {
  margin-bottom: 12px;
}
.node-select-section :deep(.el-select) {
  width: 100%;
}
.node-actions {
  display: flex;
  justify-content: center;
  gap: 8px;
  padding-top: 12px;
  border-top: 1px solid var(--el-border-color-light, #ebeef5);
}
.action-btn {
  transition: all 0.2s;
}
.action-btn:hover:not(:disabled) {
  transform: scale(1.1);
}
.arrow-connector {
  display: flex;
  align-items: center;
  width: 50px;
  position: relative;
}
.arrow-line {
  flex: 1;
  height: 2px;
  background: var(--el-border-color, #c0c4cc);
}
.arrow-icon {
  color: var(--el-text-color-placeholder, #c0c4cc);
  font-size: 14px;
  margin-left: -2px;
}
/* æ–°å¢žèŠ‚ç‚¹æ ·å¼ */
.add-node-item {
  display: flex;
  align-items: center;
}
.add-node-card {
  width: 140px;
  height: 200px;
  border: 2px dashed var(--el-border-color, #c0c4cc);
  border-radius: 12px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  cursor: pointer;
  transition: all 0.3s ease;
  background: var(--el-fill-color-light, #fafbfc);
  flex-shrink: 0;
  margin-left: 0;
}
.add-node-card:hover {
  border-color: var(--el-color-primary, #409EFF);
  background: var(--el-color-primary-light-9, #f0f7ff);
  transform: translateY(-2px);
  box-shadow: 0 4px 16px rgba(64, 158, 255, 0.15);
}
.add-icon-wrapper {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: var(--el-color-primary, #409EFF);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
  transition: all 0.3s ease;
}
.add-node-card:hover .add-icon-wrapper {
  transform: scale(1.1);
  box-shadow: 0 6px 16px rgba(64, 158, 255, 0.4);
}
.add-text {
  font-size: 14px;
  color: var(--el-text-color-regular, #606266);
  font-weight: 500;
}
.add-node-card:hover .add-text {
  color: var(--el-color-primary, #409EFF);
}
/* ç©ºçŠ¶æ€ */
.empty-state {
  padding: 50px 20px;
}
.empty-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}
.empty-icon-wrapper {
  width: 80px;
  height: 80px;
  border-radius: 50%;
  background: var(--el-fill-color-light, #f5f7fa);
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 8px;
}
.empty-text {
  font-size: 16px;
  font-weight: 600;
  color: var(--el-text-color-regular, #606266);
}
.empty-subtext {
  font-size: 13px;
  color: var(--el-text-color-secondary, #909399);
}
.empty-add-btn {
  margin-top: 8px;
  padding: 12px 28px;
}
/* åº•部提示 */
.bottom-tips {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 12px 20px;
  background: var(--el-fill-color-light, #f5f7fa);
  border-radius: 8px;
  color: var(--el-text-color-regular, #606266);
  font-size: 13px;
}
.bottom-tips .el-icon {
  color: var(--el-color-primary, #409EFF);
  font-size: 16px;
}
@media (max-width: 768px) {
  .type-tabs {
    padding: 3px;
  }
  .type-tab {
    padding: 6px 10px;
    font-size: 12px;
  }
  .tab-name {
    font-size: 12px;
  }
  .flow-container {
    flex-wrap: wrap;
    justify-content: center;
  }
  .arrow-connector {
    width: 100%;
    height: 30px;
    flex-direction: row;
    justify-content: center;
  }
  .arrow-line {
    width: 2px;
    height: 30px;
  }
  .arrow-icon {
    right: auto;
    top: auto;
    bottom: -5px;
    transform: rotate(90deg);
  }
  .add-node-item {
    width: 100%;
    justify-content: center;
    margin-top: 10px;
  }
  .add-node-item .arrow-connector {
    display: none;
  }
}
</style>
src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue
@@ -39,39 +39,6 @@
                        </el-form-item>
                    </el-col>
                </el-row>
                <!-- å®¡æ‰¹äººé€‰æ‹©ï¼ˆåŠ¨æ€èŠ‚ç‚¹ï¼‰ -->
                <el-row :gutter="30">
                    <el-col :span="12">
                        <el-form-item label="申请人:" prop="approveUser">
                            <el-select
                                v-model="form.approveUser"
                                placeholder="选择人员"
                                disabled
                            >
                                <el-option
                                    v-for="user in userList"
                                    :key="user.userId"
                                    :label="user.nickName"
                                    :value="user.userId"
                                />
                            </el-select>
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="申请日期:" prop="approveTime">
                            <el-date-picker
                                v-model="form.approveTime"
                                type="date"
                                placeholder="请选择日期"
                                value-format="YYYY-MM-DD"
                                format="YYYY-MM-DD"
                                clearable
                                style="width: 100%"
                                disabled
                            />
                        </el-form-item>
                    </el-col>
                </el-row>
            </el-form>
      <!-- æŠ¥ä»·å®¡æ‰¹ï¼šå±•示报价详情(复用销售报价"查看详情对话框"内容结构) -->
@@ -228,7 +195,6 @@
    updateApproveNode
} from "@/api/collaborativeApproval/approvalProcess.js";
import useUserStore from "@/store/modules/user.js";
import {userListNoPageByTenantId} from "@/api/system/user.js";
import { WarningFilled, Edit, Check, MoreFilled } from '@element-plus/icons-vue'
import { getQuotationList } from "@/api/salesManagement/salesQuotation.js";
import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger.js";
@@ -248,7 +214,6 @@
const formRef = ref(null);
const userStore = useUserStore()
const productOptions = ref([]);
const userList = ref([])
const quotationLoading = ref(false)
const currentQuotation = ref({})
const purchaseLoading = ref(false)
@@ -258,9 +223,7 @@
const data = reactive({
    form: {
        approveTime: "",
        approveId: "",
        approveUser: "",
        approveDeptId: "",
        approveReason: "",
        checkResult: "",
@@ -295,9 +258,6 @@
  dialogFormVisible.value = true;
  currentQuotation.value = {}
  currentPurchase.value = {}
    userListNoPageByTenantId().then((res) => {
        userList.value = res.data;
    });
    form.value = {...row}
    // ç«‹å³æ¸…除表单验证状态(因为字段是disabled的,不需要验证)
    nextTick(() => {
src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue
@@ -97,82 +97,7 @@
            </el-form-item>
          </el-col>
        </el-row>
        <!-- å®¡æ‰¹äººé€‰æ‹©ï¼ˆåŠ¨æ€èŠ‚ç‚¹ï¼‰ -->
        <el-row>
          <el-col :span="24">
            <el-form-item>
              <template #label>
                <span>审批人选择:</span>
                <el-button type="primary" @click="addApproverNode" style="margin-left: 8px;">新增节点</el-button>
              </template>
              <div style="display: flex; align-items: flex-end; flex-wrap: wrap;">
                <div
                  v-for="(node, index) in approverNodes"
                  :key="node.id"
                  style="margin-right: 30px; text-align: center; margin-bottom: 10px;"
                >
                  <div>
                    <span>审批人</span>
                    â†’
                  </div>
                  <el-select
                    v-model="node.userId"
                    placeholder="选择人员"
                    style="width: 120px; margin-bottom: 8px;"
                  >
                    <el-option
                      v-for="user in userList"
                      :key="user.userId"
                      :label="user.nickName"
                      :value="user.userId"
                    />
                  </el-select>
                  <div>
                    <el-button
                      type="danger"
                      size="small"
                      @click="removeApproverNode(index)"
                      v-if="approverNodes.length > 1"
                    >删除</el-button>
                  </div>
                </div>
              </div>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="申请人:" prop="approveUser">
                            <el-select
                                v-model="form.approveUser"
                                placeholder="选择人员"
                filterable
                default-first-option
                :reserve-keyword="false"
                            >
                                <el-option
                                    v-for="user in userList"
                                    :key="user.userId"
                                    :label="user.nickName"
                                    :value="user.userId"
                                />
                            </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="申请日期:" prop="approveTime">
              <el-date-picker
                  v-model="form.approveTime"
                  type="date"
                  placeholder="请选择日期"
                  value-format="YYYY-MM-DD"
                  format="YYYY-MM-DD"
                  clearable
                  style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="附件材料:" prop="remark">
@@ -211,13 +136,10 @@
import {
  delLedgerFile,
} from "@/api/salesManagement/salesLedger.js";
import {userListNoPageByTenantId} from "@/api/system/user.js";
import { getToken } from "@/utils/auth";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
import useUserStore from "@/store/modules/user";
import { getCurrentDate } from "@/utils/index.js";
import log from "@/views/monitor/job/log.vue";
const userStore = useUserStore();
const dialogFormVisible = ref(false);
@@ -231,24 +153,19 @@
});
const data = reactive({
  form: {
    approveTime: "",
    approveId: "",
    approveUser: "",
        approveDeptId: "",
    approveDeptName: "",
    approveReason: "",
    checkResult: "",
    tempFileIds: [],
    approverList: [], // æ–°å¢žå­—段,存储所有节点的审批人id
    startDate: "", // è¯·å‡å¼€å§‹æ—¶é—´
    endDate: "", // è¯·å‡ç»“束时间
    price: null, // æŠ¥é”€é‡‘额
    location: "" // å‡ºå·®åœ°ç‚¹
  },
  rules: {
    approveTime: [{ required: false, message: "请输入", trigger: "change" },],
    approveId: [{ required: false, message: "请输入", trigger: "blur" }],
    approveUser: [{ required: false, message: "请输入", trigger: "blur" }],
    approveDeptName: [{ required: true, message: "请输入", trigger: "blur" }],
    approveReason: [{ required: true, message: "请输入", trigger: "blur" }],
    checkResult: [{ required: false, message: "请输入", trigger: "blur" }],
@@ -268,18 +185,7 @@
  }
})
// å®¡æ‰¹äººèŠ‚ç‚¹ç›¸å…³
const approverNodes = ref([
  { id: 1, userId: null }
])
let nextApproverId = 2
const userList = ref([])
function addApproverNode() {
  approverNodes.value.push({ id: nextApproverId++, userId: null })
}
function removeApproverNode(index) {
  approverNodes.value.splice(index, 1)
}
// å¤„理部门选择变化
const handleDeptChange = (deptId) => {
  if (deptId) {
@@ -295,15 +201,7 @@
const openDialog = (type, row) => {
  operationType.value = type;
  dialogFormVisible.value = true;
    userListNoPageByTenantId().then((res) => {
    userList.value = res.data;
  });
    form.value = {}
    approverNodes.value = [
        { id: 1, userId: null }
    ]
  form.value.approveUser = userStore.id;
  form.value.approveTime = getCurrentDate();
  
  // èŽ·å–å½“å‰ç”¨æˆ·ä¿¡æ¯å¹¶è®¾ç½®éƒ¨é—¨ID
  form.value.approveDeptId = userStore.currentDeptId
@@ -316,18 +214,6 @@
        currentApproveStatus.value = row.approveStatus
    approveProcessGetInfo({id: row.approveId,approveReason: '1'}).then(res => {
            form.value = {...res.data}
      // åæ˜¾å®¡æ‰¹äºº
      if (res.data && res.data.approveUserIds) {
        const userIds = res.data.approveUserIds.split(',')
        approverNodes.value = userIds.map((userId, idx) => ({
          id: idx + 1,
          userId: parseInt(userId.trim())
        }))
        nextApproverId = userIds.length + 1
      } else {
        approverNodes.value = [{ id: 1, userId: null }]
        nextApproverId = 2
      }
    })
  }
}
@@ -362,15 +248,7 @@
}
// æäº¤äº§å“è¡¨å•
const submitForm = () => {
  // æ”¶é›†æ‰€æœ‰èŠ‚ç‚¹çš„å®¡æ‰¹äººid
  form.value.approveUserIds = approverNodes.value.map(node => node.userId).join(',')
  form.value.approveType = props.approveType
  // å®¡æ‰¹äººå¿…填校验
  const hasEmptyApprover = approverNodes.value.some(node => !node.userId)
  if (hasEmptyApprover) {
    proxy.$modal.msgError("请为所有审批节点选择审批人!")
    return
  }
  // å½“ approveType ä¸º 2 æ—¶ï¼Œæ ¡éªŒè¯·å‡æ—¶é—´
  if (props.approveType == 2) {
    if (!form.value.startDate) {
src/views/collaborativeApproval/approvalProcess/index.vue
@@ -1,55 +1,114 @@
<template>
  <div class="app-container">
    <!-- æ ‡ç­¾é¡µåˆ‡æ¢ä¸åŒçš„审批类型 -->
    <el-tabs v-model="activeTab" @tab-change="handleTabChange" class="approval-tabs">
      <el-tab-pane label="公出管理" name="1"></el-tab-pane>
      <el-tab-pane label="请假管理" name="2"></el-tab-pane>
      <el-tab-pane label="出差管理" name="3"></el-tab-pane>
      <el-tab-pane label="报销管理" name="4"></el-tab-pane>
      <el-tab-pane label="采购审批" name="5"></el-tab-pane>
      <el-tab-pane label="报价审批" name="6"></el-tab-pane>
      <el-tab-pane label="发货审批" name="7"></el-tab-pane>
    </el-tabs>
    <!-- å®¡æ‰¹ç±»åž‹åˆ‡æ¢ - ç´§å‡‘标签式 -->
    <div class="type-tabs">
      <div
        v-for="type in approveTypes"
        :key="type.value"
        class="type-tab"
        :class="{ active: activeTab === type.value }"
        @click="activeTab = type.value; handleTabChange()"
      >
        <el-icon :size="14" :style="{ color: activeTab === type.value ? type.color : '#909399' }">
          <component :is="type.icon" />
        </el-icon>
        <span class="tab-name">{{ type.label }}</span>
      </div>
    </div>
    
    <div class="search_form">
      <div>
        <span class="search_title">流程编号:</span>
    <!-- æœç´¢å’Œæ“ä½œåŒºåŸŸ -->
    <el-card class="search-card" shadow="never">
      <div class="search-content">
        <div class="search-filters">
          <div class="filter-item">
            <span class="filter-label">流程编号</span>
        <el-input
            v-model="searchForm.approveId"
            style="width: 240px"
            placeholder="请输入流程编号搜索"
            @change="handleQuery"
              placeholder="请输入流程编号"
            clearable
            :prefix-icon="Search"
              @keyup.enter="handleQuery"
              class="search-input"
        />
        <span class="search_title ml10">审批状态:</span>
                <el-select v-model="searchForm.approveStatus" clearable @change="handleQuery" style="width: 240px">
                    <el-option label="待审核" :value="0" />
                    <el-option label="审核中" :value="1" />
                    <el-option label="审核完成" :value="2" />
                    <el-option label="审核未通过" :value="3" />
                    <el-option label="已重新提交" :value="4" />
                </el-select>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
        >搜索</el-button
        >
      </div>
      <div>
          <div class="filter-item">
            <span class="filter-label">审批状态</span>
            <el-select
              v-model="searchForm.approveStatus"
              clearable
              @change="handleQuery"
              placeholder="请选择状态"
              class="search-select"
            >
              <el-option label="待审核" :value="0">
                <el-tag size="small" type="warning">待审核</el-tag>
              </el-option>
              <el-option label="审核中" :value="1">
                <el-tag size="small" type="primary">审核中</el-tag>
              </el-option>
              <el-option label="审核完成" :value="2">
                <el-tag size="small" type="success">审核完成</el-tag>
              </el-option>
              <el-option label="审核未通过" :value="3">
                <el-tag size="small" type="danger">审核未通过</el-tag>
              </el-option>
              <el-option label="已重新提交" :value="4">
                <el-tag size="small" type="info">已重新提交</el-tag>
              </el-option>
            </el-select>
          </div>
          <el-button type="primary" @click="handleQuery" class="search-btn">
            <el-icon><Search /></el-icon>
            æœç´¢
          </el-button>
          <el-button @click="resetQuery" class="reset-btn">
            <el-icon><RefreshRight /></el-icon>
            é‡ç½®
          </el-button>
        </div>
        <div class="search-actions">
        <el-button
          type="primary"
          @click="openForm('add')"
          v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7"
        >新增</el-button>
        <el-button @click="handleOut">导出</el-button>
            v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7 && currentApproveType !== 10"
            class="action-btn primary"
          >
            <el-icon><Plus /></el-icon>
            æ–°å¢ž
          </el-button>
          <el-button @click="handleOut" class="action-btn">
            <el-icon><Download /></el-icon>
            å¯¼å‡º
          </el-button>
        <el-button
          type="danger"
          plain
          @click="handleDelete"
          v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7"
        >删除</el-button>
            v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7 && currentApproveType !== 10"
            class="action-btn danger"
          >
            <el-icon><Delete /></el-icon>
            åˆ é™¤
          </el-button>
      </div>
    </div>
    <div class="table_list">
    </el-card>
    <!-- æ•°æ®è¡¨æ ¼ -->
    <el-card class="table-card" shadow="never" v-loading="tableLoading">
      <template #header>
        <div class="table-header">
          <div class="table-title">
            <div class="type-tag" :style="{ backgroundColor: currentTypeInfo.color }">
              <el-icon color="#fff" :size="16"><component :is="currentTypeInfo.icon" /></el-icon>
            </div>
            <span>{{ currentTypeInfo.label }}列表</span>
            <el-tag type="info" size="small" effect="plain" class="count-tag">
              å…± {{ page.total }} æ¡
            </el-tag>
          </div>
        </div>
      </template>
      <PIMTable
          rowKey="id"
          :column="tableColumnCopy"
@@ -60,8 +119,11 @@
          :tableLoading="tableLoading"
          @pagination="pagination"
          :total="page.total"
        class="custom-table"
      ></PIMTable>
    </div>
    </el-card>
    <!-- å¼¹çª—组件 -->
    <info-form-dia ref="infoFormDia" @close="handleQuery" :approveType="currentApproveType"></info-form-dia>
    <approval-dia ref="approvalDia" @close="handleQuery" :approveType="currentApproveType"></approval-dia>
    <FileList ref="fileListRef" />
@@ -70,7 +132,7 @@
<script setup>
import FileList from "./fileList.vue";
import { Search } from "@element-plus/icons-vue";
import { Search, Plus, Delete, Download, RefreshRight, DocumentChecked } from "@element-plus/icons-vue";
import {onMounted, ref, computed, reactive, toRefs, nextTick, getCurrentInstance} from "vue";
import {ElMessageBox} from "element-plus";
import { useRoute } from 'vue-router';
@@ -85,13 +147,38 @@
// å½“前选中的标签页,默认为公出管理
const activeTab = ref('1');
// å„类型数量统计
const typeCounts = ref({});
// å®¡æ‰¹ç±»åž‹é…ç½®
const approveTypes = [
  { value: '1', label: '公出管理', icon: 'Suitcase', color: '#409EFF' },
  { value: '2', label: '请假管理', icon: 'Calendar', color: '#67C23A' },
  { value: '3', label: '出差管理', icon: 'Location', color: '#E6A23C' },
  { value: '4', label: '报销管理', icon: 'Money', color: '#F56C6C' },
  { value: '5', label: '采购审批', icon: 'ShoppingCart', color: '#909399' },
  { value: '6', label: '报价审批', icon: 'DocumentChecked', color: '#9B59B6' },
  { value: '7', label: '发货审批', icon: 'Van', color: '#1ABC9C' },
  { value: '10', label: '销售审批', icon: 'Sell', color: '#FF6B6B' },
];
// å½“前审批类型信息
const currentTypeInfo = computed(() => {
  return approveTypes.find(t => t.value === activeTab.value) || approveTypes[0];
});
// èŽ·å–ç±»åž‹æ•°é‡
const getTypeCount = (value) => {
  return typeCounts.value[value] || 0;
};
// å½“前审批类型,根据选中的标签页计算
const currentApproveType = computed(() => {
  return Number(activeTab.value);
});
// æ ‡ç­¾é¡µåˆ‡æ¢å¤„理
const handleTabChange = (tabName) => {
const handleTabChange = () => {
  // åˆ‡æ¢æ ‡ç­¾é¡µæ—¶é‡ç½®æœç´¢æ¡ä»¶å’Œåˆ†é¡µï¼Œå¹¶é‡æ–°åŠ è½½æ•°æ®
  searchForm.value.approveId = '';
  searchForm.value.approveStatus = '';
@@ -108,12 +195,20 @@
});
const { searchForm } = toRefs(data);
// é‡ç½®æœç´¢
const resetQuery = () => {
  searchForm.value.approveId = '';
  searchForm.value.approveStatus = '';
  handleQuery();
};
// åŠ¨æ€è¡¨æ ¼åˆ—é…ç½®ï¼Œæ ¹æ®å®¡æ‰¹ç±»åž‹ç”Ÿæˆåˆ—
const tableColumnCopy = computed(() => {
  const isLeaveType = currentApproveType.value === 2; // è¯·å‡ç®¡ç†
  const isReimburseType = currentApproveType.value === 4; // æŠ¥é”€ç®¡ç†
  const isQuotationType = currentApproveType.value === 6; // æŠ¥ä»·å®¡æ‰¹
  const isPurchaseType = currentApproveType.value === 5; // é‡‡è´­å®¡æ‰¹
  const isSalesType = currentApproveType.value === 10; // é”€å”®å®¡æ‰¹
  
  // åŸºç¡€åˆ—配置
  const baseColumns = [
@@ -212,6 +307,7 @@
        currentApproveType.value === 5 ||
        currentApproveType.value === 6 ||
        currentApproveType.value === 7 ||
        currentApproveType.value === 10 ||
        row.approveStatus == 2 ||
        row.approveStatus == 1 ||
        row.approveStatus == 4
@@ -238,7 +334,7 @@
    },
  ];
  // æŠ¥ä»·å®¡æ‰¹ï¼ˆç±»åž‹ 6)不展示“附件”操作
  // æŠ¥ä»·å®¡æ‰¹ï¼ˆç±»åž‹ 6)不展示"附件"操作
  if (!isQuotationType) {
    actionOperations.push({
      name: "附件",
@@ -294,6 +390,8 @@
    tableLoading.value = false;
    tableData.value = res.data.records
    page.total = res.data.total;
    // æ›´æ–°å½“前类型数量
    typeCounts.value[activeTab.value] = res.data.total;
  }).catch(err => {
    tableLoading.value = false;
  })
@@ -310,6 +408,7 @@
    5: "/approveProcess/exportFive",
    6: "/approveProcess/exportSix",
    7: "/approveProcess/exportSeven",
    10: "/approveProcess/exportTen",
  }
  const url = urlMap[type] || urlMap[0]
  const nameMap = {
@@ -321,6 +420,7 @@
    5: "采购申请审批表",
    6: "报价审批表",
    7: "发货审批表",
    10: "销售审批表",
  }
  const fileName = nameMap[type] || nameMap[0]
  proxy.download(url, {}, `${fileName}.xlsx`)
@@ -388,7 +488,256 @@
</script>
<style scoped>
.approval-tabs {
  margin-bottom: 10px;
.page-header {
  margin-bottom: 20px;
}
.header-title {
  display: flex;
  align-items: center;
  gap: 12px;
}
.title-icon {
  font-size: 28px;
  color: var(--el-color-primary, #409EFF);
}
.header-text {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.main-title {
  font-size: 20px;
  font-weight: 600;
  color: var(--el-text-color-primary, #303133);
}
.sub-title {
  font-size: 13px;
  color: var(--el-text-color-secondary, #909399);
}
/* å®¡æ‰¹ç±»åž‹åˆ‡æ¢ - ç´§å‡‘标签式 */
.type-tabs {
  display: flex;
  gap: 4px;
  margin-bottom: 16px;
  padding: 4px;
  background: var(--el-fill-color-light, #f5f7fa);
  border-radius: 8px;
  overflow-x: auto;
}
.type-tab {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  border-radius: 6px;
  cursor: pointer;
  transition: all 0.2s ease;
  white-space: nowrap;
  font-size: 13px;
  color: var(--el-text-color-regular, #606266);
}
.type-tab:hover {
  background: var(--el-color-primary-light-9, rgba(64, 158, 255, 0.1));
  color: var(--el-color-primary, #409EFF);
}
.type-tab.active {
  background: var(--el-bg-color, #fff);
  color: var(--el-color-primary, #409EFF);
  font-weight: 600;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.tab-name {
  font-size: 13px;
}
.tab-count {
  min-width: 16px;
  height: 16px;
  padding: 0 5px;
  background: var(--el-color-success, #67C23A);
  color: #fff;
  border-radius: 8px;
  font-size: 11px;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* æœç´¢å¡ç‰‡ */
.search-card {
  margin-bottom: 16px;
  border-radius: 12px;
}
:deep(.el-card__body) {
  padding: 20px;
}
.search-content {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 16px;
}
.search-filters {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
}
.filter-item {
  display: flex;
  align-items: center;
  gap: 8px;
}
.filter-label {
  font-size: 14px;
  color: var(--el-text-color-regular, #606266);
  font-weight: 500;
  white-space: nowrap;
}
.search-input,
.search-select {
  width: 200px;
}
.search-btn {
  display: flex;
  align-items: center;
  gap: 4px;
}
.reset-btn {
  display: flex;
  align-items: center;
  gap: 4px;
}
.search-actions {
  display: flex;
  gap: 10px;
}
.action-btn {
  display: flex;
  align-items: center;
  gap: 4px;
}
.action-btn.primary {
  background: var(--el-color-primary, #409EFF);
  border: none;
}
.action-btn.danger {
  transition: all 0.3s;
}
.action-btn.danger:hover {
  background: #f56c6c;
  color: #fff;
}
/* è¡¨æ ¼å¡ç‰‡ */
.table-card {
  border-radius: 12px;
}
:deep(.el-card__header) {
  padding: 16px 20px;
  border-bottom: 1px solid var(--el-border-color-light, #ebeef5);
}
.table-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.table-title {
  display: flex;
  align-items: center;
  gap: 12px;
  font-size: 16px;
  font-weight: 600;
  color: var(--el-text-color-primary, #303133);
}
.type-tag {
  width: 32px;
  height: 32px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.count-tag {
  margin-left: 8px;
}
.custom-table {
  margin-top: 8px;
}
/* å“åº”式 */
@media (max-width: 1200px) {
  .search-content {
    flex-direction: column;
    align-items: stretch;
  }
  .search-filters {
    justify-content: flex-start;
  }
  .search-actions {
    justify-content: flex-end;
  }
}
@media (max-width: 768px) {
  .type-tabs {
    padding: 3px;
  }
  .type-tab {
    padding: 6px 10px;
    font-size: 12px;
  }
  .tab-name {
    font-size: 12px;
  }
  .search-filters {
    flex-direction: column;
    align-items: stretch;
  }
  .filter-item {
    width: 100%;
  }
  .search-input,
  .search-select {
    width: 100%;
  }
}
</style>
src/views/procurementManagement/procurementLedger/index.vue
@@ -298,50 +298,6 @@
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item>
              <template #label>
                <div style="display: flex; align-items: center; justify-content: space-between; width: 100%;">
                  <span>审批人选择:</span>
                  <el-button type="primary" size="small" @click="addApproverNode" icon="Plus">新增节点</el-button>
                </div>
              </template>
              <div class="approver-nodes-container">
                <div
                  v-for="(node, index) in approverNodes"
                  :key="node.id"
                  class="approver-node-item"
                >
                  <div class="approver-node-header">
                    <span class="approver-node-label">审批节点 {{ index + 1 }}</span>
                    <el-button
                      v-if="approverNodes.length > 1"
                      type="danger"
                      size="small"
                      text
                      @click="removeApproverNode(index)"
                      icon="Delete"
                    >删除</el-button>
                  </div>
                  <el-select
                    v-model="node.userId"
                    placeholder="请选择审批人"
                    filterable
                    style="width: 100%;"
                  >
                    <el-option
                      v-for="user in userList"
                      :key="user.userId"
                      :label="user.nickName"
                      :value="user.userId"
                    />
                  </el-select>
                </div>
              </div>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-form-item label="产品信息:"
                        prop="entryDate">
@@ -760,16 +716,6 @@
  const userStore = useUserStore();
  // å®¡æ‰¹äººèŠ‚ç‚¹ï¼ˆä»¿é”€å”®å°è´¦å‘è´§å®¡æ‰¹äººï¼‰
  const approverNodes = ref([{ id: 1, userId: null }]);
  let nextApproverId = 2;
  const addApproverNode = () => {
    approverNodes.value.push({ id: nextApproverId++, userId: null });
  };
  const removeApproverNode = (index) => {
    approverNodes.value.splice(index, 1);
  };
  // è®¢å•审批状态显示文本
  const approvalStatusText = {
    1: "待审核",
@@ -1121,19 +1067,12 @@
    }
    try {
      // èŽ·å–å®¡æ‰¹äººID字符串
      const approveUserIds = approverNodes.value
        .filter(node => node.userId)
        .map(node => node.userId)
        .join(",");
      let params = {
        productData: proxy.HaveJson(productData.value),
        supplierId: form.value.supplierId,
        paymentMethod: form.value.paymentMethod,
        recorderId: form.value.recorderId,
        projectName: form.value.projectName,
        approveUserIds: approveUserIds,
        templateName: templateName.value.trim(),
      };
      console.log("template params ===>", params, "currentTemplateId:", currentTemplateId.value);
@@ -1284,9 +1223,6 @@
    templateName.value = "";
    filterInputValue.value = "";
    isTemplateNameDuplicate.value = false;
    // é‡ç½®å®¡æ‰¹äººèŠ‚ç‚¹ï¼ˆé»˜è®¤ä¸€ä¸ªç©ºèŠ‚ç‚¹ï¼‰
    approverNodes.value = [{ id: 1, userId: null }];
    nextApproverId = 2;
    try {
      // å¹¶è¡ŒåŠ è½½åŸºç¡€æ•°æ®
      const [userRes, salesRes, supplierRes] = await Promise.all([
@@ -1325,15 +1261,6 @@
          form.value = { ...purchaseRes };
          productData.value = purchaseRes.productData || [];
          fileList.value = purchaseRes.salesLedgerFiles || [];
          // å¦‚果编辑时有审批人,解析审批人ID字符串并设置到节点中
          if (purchaseRes.approveUserIds) {
            const approverIds = purchaseRes.approveUserIds.split(",");
            approverNodes.value = approverIds.map((id, index) => ({
              id: index + 1,
              userId: Number(id)
            }));
            nextApproverId = approverIds.length + 1;
          }
        } catch (error) {
          console.error("加载采购台账数据失败:", error);
          proxy.$modal.msgError("加载数据失败");
@@ -1402,14 +1329,6 @@
  const submitForm = () => {
    proxy.$refs["formRef"].validate(valid => {
      if (valid) {
        // å®¡æ‰¹äººå¿…填校验(所有节点都要选人)
        const hasEmptyApprover = approverNodes.value.some(node => !node.userId);
        if (hasEmptyApprover) {
          proxy.$modal.msgError("请为所有审批节点选择审批人!");
          return;
        }
        const approveUserIds = approverNodes.value.map(node => node.userId).join(",");
        if (productData.value.length > 0) {
          // æ–°å¢žæ—¶ï¼Œéœ€è¦ä»Žæ¯ä¸ªäº§å“å¯¹è±¡ä¸­åˆ é™¤ id å­—段
          let processedProductData = productData.value;
@@ -1430,7 +1349,6 @@
        }
        form.value.tempFileIds = tempFileIds;
        form.value.type = 2;
        form.value.approveUserIds = approveUserIds;
        // å¦‚æžœsalesLedgerId为空,则不传递salesContractNo
        if (!form.value.salesLedgerId) {
@@ -1454,9 +1372,6 @@
  // å…³é—­å¼¹æ¡†
  const closeDia = () => {
    proxy.resetForm("formRef");
    // é‡ç½®å®¡æ‰¹äººèŠ‚ç‚¹ï¼ˆé»˜è®¤ä¸€ä¸ªç©ºèŠ‚ç‚¹ï¼‰
    approverNodes.value = [{ id: 1, userId: null }];
    nextApproverId = 2;
    dialogFormVisible.value = false;
  };
  // æ‰“开产品弹框
src/views/productionManagement/processRoute/Edit.vue
@@ -3,7 +3,7 @@
    <el-dialog
        v-model="isShow"
        title="编辑工艺路线"
        width="400"
        width="800"
        @close="closeModal"
    >
      <el-form label-width="140px" :model="formState" label-position="top" ref="formRef">
@@ -25,35 +25,54 @@
          </el-button>
        </el-form-item>
        <el-form-item
            label="BOM"
            prop="bomId"
            :rules="[
                {
                required: true,
                message: '请选择BOM',
                trigger: 'change',
              }
            ]"
        >
        <el-form-item label="备注" prop="description">
          <el-input v-model="formState.description" type="textarea" />
        </el-form-item>
        <!-- å·¥åºé…ç½® -->
        <el-form-item label="工序配置" required>
          <el-table :data="formState.processRouteItemList" border size="small" style="width: 100%">
            <el-table-column label="部件" min-width="200">
              <template #default="{ row }">
          <el-select
              v-model="formState.bomId"
              placeholder="请选择BOM"
                    v-model="row.processId"
                    placeholder="请选择部件"
              clearable
              :disabled="!formState.productModelId || bomOptions.length === 0"
                    filterable
              style="width: 100%"
          >
            <el-option
                v-for="item in bomOptions"
                :key="item.id"
                :label="item.bomNo || `BOM-${item.id}`"
                :value="item.id"
                      v-for="process in processOptions"
                      :key="process.id"
                      :label="formatProcessOptionLabel(process)"
                      :value="process.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="备注" prop="description">
          <el-input v-model="formState.description" type="textarea" />
              </template>
            </el-table-column>
            <el-table-column label="质检" width="80" align="center">
              <template #default="{ row }">
                <el-switch v-model="row.isQuality" :active-value="true" inactive-value="false"/>
              </template>
            </el-table-column>
            <el-table-column label="备注" min-width="150">
              <template #default="{ row }">
                <el-input v-model="row.remark" placeholder="请输入备注" size="small"/>
              </template>
            </el-table-column>
            <el-table-column label="操作" width="60" align="center">
              <template #default="{ $index }">
                <el-button type="danger" link size="small" @click="removeProcessItem($index)">
                  <el-icon><Delete /></el-icon>
                </el-button>
              </template>
            </el-table-column>
          </el-table>
          <div style="margin-top: 8px;">
            <el-button type="primary" link size="small" @click="addProcessItem">
              <el-icon><Plus /></el-icon> æ·»åŠ å·¥åº
            </el-button>
          </div>
        </el-form-item>
      </el-form>
      
@@ -76,7 +95,7 @@
<script setup>
import {ref, computed, getCurrentInstance, onMounted, nextTick, watch} from "vue";
import {update} from "@/api/productionManagement/processRoute.js";
import {getByModel} from "@/api/productionManagement/productBom.js";
import {processList} from "@/api/productionManagement/productionProcess.js";
import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
const props = defineProps({
@@ -99,8 +118,8 @@
  productModelId: undefined,
  productName: "",
  productModelName: "",
  bomId: undefined,
  description: '',
  processRouteItemList: [],
});
const isShow = computed({
@@ -113,9 +132,49 @@
});
const showProductSelectDialog = ref(false);
const bomOptions = ref([]);
const processOptions = ref([]);
let { proxy } = getCurrentInstance()
// èŽ·å–å·¥åºåˆ—è¡¨
const getProcessOptions = async () => {
  try {
    const res = await processList();
    processOptions.value = res.data || [];
  } catch (error) {
    console.error("获取工序列表失败", error);
    processOptions.value = [];
  }
};
// æ ¼å¼åŒ–工序选项标签
const formatProcessOptionLabel = (process) => {
  if (!process) return '';
  const typeMap = {
    1: '加工',
    2: '刮板冷芯制作',
    3: '管路组对',
    4: '罐体连接及调试',
    5: '测试打压',
    6: '其他',
  };
  const typeText = typeMap[process.type] || '';
  return `${process.name} ${process.no ? '(' + process.no + ')' : ''} ${typeText ? '[' + typeText + ']' : ''}`;
};
// æ·»åŠ å·¥åº
const addProcessItem = () => {
  formState.value.processRouteItemList.push({
    processId: undefined,
    isQuality: false,
    remark: '',
  });
};
// ç§»é™¤å·¥åº
const removeProcessItem = (index) => {
  formState.value.processRouteItemList.splice(index, 1);
};
const closeModal = () => {
  isShow.value = false;
@@ -131,90 +190,42 @@
      productName: props.record.productName || "",
      // æ³¨æ„ï¼šrecord中的字段是model,需要映射到productModelName
      productModelName: props.record.model || props.record.productModelName || "",
      bomId: props.record.bomId,
      description: props.record.description || '',
      processRouteItemList: props.record.processRouteItemList || [],
    };
    // å¦‚果有产品型号ID,加载BOM列表
    if (props.record.productModelId) {
      loadBomList(props.record.productModelId);
    }
  }
}
// åŠ è½½BOM列表
const loadBomList = async (productModelId) => {
  if (!productModelId) {
    bomOptions.value = [];
    return;
  }
  try {
    const res = await getByModel(productModelId);
    // å¤„理返回的BOM数据:可能是数组、对象或包含data字段
    let bomList = [];
    if (Array.isArray(res)) {
      bomList = res;
    } else if (res && res.data) {
      bomList = Array.isArray(res.data) ? res.data : [res.data];
    } else if (res && typeof res === 'object') {
      bomList = [res];
    }
    bomOptions.value = bomList;
  } catch (error) {
    console.error("加载BOM列表失败:", error);
    bomOptions.value = [];
  }
};
// äº§å“é€‰æ‹©å¤„理
const handleProductSelect = async (products) => {
  if (products && products.length > 0) {
    const product = products[0];
    // å…ˆæŸ¥è¯¢BOM列表(必选)
    try {
      const res = await getByModel(product.id);
      // å¤„理返回的BOM数据:可能是数组、对象或包含data字段
      let bomList = [];
      if (Array.isArray(res)) {
        bomList = res;
      } else if (res && res.data) {
        bomList = Array.isArray(res.data) ? res.data : [res.data];
      } else if (res && typeof res === 'object') {
        bomList = [res];
      }
      if (bomList.length > 0) {
        formState.value.productModelId = product.id;
        formState.value.productName = product.productName;
        formState.value.productModelName = product.model;
        // å¦‚果当前选择的BOM不在新列表中,则重置BOM选择
        const currentBomExists = bomList.some(bom => bom.id === formState.value.bomId);
        if (!currentBomExists) {
          formState.value.bomId = undefined;
        }
        bomOptions.value = bomList;
        showProductSelectDialog.value = false;
        // è§¦å‘表单验证更新
        proxy.$refs["formRef"]?.validateField('productModelId');
      } else {
        proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
      }
    } catch (error) {
      // å¦‚果接口返回404或其他错误,说明没有BOM
      proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
    }
  }
};
const handleSubmit = () => {
  proxy.$refs["formRef"].validate(valid => {
    if (valid) {
      // éªŒè¯æ˜¯å¦é€‰æ‹©äº†äº§å“å’ŒBOM
      // éªŒè¯æ˜¯å¦é€‰æ‹©äº†äº§å“
      if (!formState.value.productModelId) {
        proxy.$modal.msgError("请选择产品");
        return;
      }
      if (!formState.value.bomId) {
        proxy.$modal.msgError("请选择BOM");
      // éªŒè¯æ˜¯å¦é…ç½®äº†å·¥åº
      if (!formState.value.processRouteItemList || formState.value.processRouteItemList.length === 0) {
        proxy.$modal.msgError("请至少配置一个工序");
        return;
      }
      // éªŒè¯æ‰€æœ‰å·¥åºæ˜¯å¦é€‰æ‹©äº†éƒ¨ä»¶
      const invalidItem = formState.value.processRouteItemList.find(item => !item.processId);
      if (invalidItem) {
        proxy.$modal.msgError("请选择所有工序的部件");
        return;
      }
      update(formState.value).then(res => {
@@ -245,6 +256,7 @@
}, { immediate: true });
onMounted(() => {
  getProcessOptions();
  if (props.visible && props.record) {
    setFormData();
  }
src/views/productionManagement/processRoute/New.vue
@@ -25,33 +25,6 @@
          </el-button>
        </el-form-item>
        <el-form-item
            label="BOM"
            prop="bomId"
            :rules="[
                {
                required: true,
                message: '请选择BOM',
                trigger: 'change',
              }
            ]"
        >
          <el-select
              v-model="formState.bomId"
              placeholder="请选择BOM"
              clearable
              :disabled="!formState.productModelId || bomOptions.length === 0"
              style="width: 100%"
          >
            <el-option
                v-for="item in bomOptions"
                :key="item.id"
                :label="item.bomNo || `BOM-${item.id}`"
                :value="item.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="备注" prop="description">
          <el-input v-model="formState.description" type="textarea" />
        </el-form-item>
@@ -76,7 +49,6 @@
<script setup>
import {ref, computed, getCurrentInstance} from "vue";
import {add} from "@/api/productionManagement/processRoute.js";
import {getByModel} from "@/api/productionManagement/productBom.js";
import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
const props = defineProps({
@@ -94,7 +66,6 @@
  productModelId: undefined,
  productName: "",
  productModelName: "",
  bomId: undefined,
  description: '',
});
@@ -108,7 +79,6 @@
});
const showProductSelectDialog = ref(false);
const bomOptions = ref([]);
let { proxy } = getCurrentInstance()
@@ -119,10 +89,8 @@
    productModelId: undefined,
    productName: "",
    productModelName: "",
    bomId: undefined,
    description: '',
  };
  bomOptions.value = [];
  isShow.value = false;
};
@@ -130,48 +98,21 @@
const handleProductSelect = async (products) => {
  if (products && products.length > 0) {
    const product = products[0];
    // å…ˆæŸ¥è¯¢BOM列表(必选)
    try {
      const res = await getByModel(product.id);
      // å¤„理返回的BOM数据:可能是数组、对象或包含data字段
      let bomList = [];
      if (Array.isArray(res)) {
        bomList = res;
      } else if (res && res.data) {
        bomList = Array.isArray(res.data) ? res.data : [res.data];
      } else if (res && typeof res === 'object') {
        bomList = [res];
      }
      if (bomList.length > 0) {
        formState.value.productModelId = product.id;
        formState.value.productName = product.productName;
        formState.value.productModelName = product.model;
        formState.value.bomId = undefined; // é‡ç½®BOM选择
        bomOptions.value = bomList;
        showProductSelectDialog.value = false;
        // è§¦å‘表单验证更新
        proxy.$refs["formRef"]?.validateField('productModelId');
      } else {
        proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
      }
    } catch (error) {
      // å¦‚果接口返回404或其他错误,说明没有BOM
      proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
    }
  }
};
const handleSubmit = () => {
  proxy.$refs["formRef"].validate(valid => {
    if (valid) {
      // éªŒè¯æ˜¯å¦é€‰æ‹©äº†äº§å“å’ŒBOM
      // éªŒè¯æ˜¯å¦é€‰æ‹©äº†äº§å“
      if (!formState.value.productModelId) {
        proxy.$modal.msgError("请选择产品");
        return;
      }
      if (!formState.value.bomId) {
        proxy.$modal.msgError("请选择BOM");
        return;
      }
      add(formState.value).then(res => {
@@ -185,10 +126,11 @@
  })
};
defineExpose({
  closeModal,
  handleSubmit,
  isShow,
});
</script>
src/views/productionManagement/processRoute/processRouteItem/index.vue
@@ -50,7 +50,7 @@
    
    <!-- è¡¨æ ¼è§†å›¾ -->
    <div v-if="viewMode === 'table'" class="section-header">
      <div class="section-title">产品部件列表</div>
      <div class="section-title">工序列表</div>
      <div class="section-actions">
        <el-button 
            icon="Grid" 
@@ -74,14 +74,9 @@
        class="lims-table"
    >
      <el-table-column align="center" label="序号" width="60" type="index" />
      <el-table-column label="产品名称" prop="name" min-width="140" show-overflow-tooltip>
      <el-table-column label="部件名称" prop="name" min-width="140" show-overflow-tooltip>
        <template #default="scope">
          {{ getProcessField(scope.row, 'name') }}
        </template>
      </el-table-column>
      <el-table-column label="产品规格" prop="productModel" min-width="120" show-overflow-tooltip>
        <template #default="scope">
          {{ getProcessField(scope.row, 'productModel') }}
        </template>
      </el-table-column>
      <el-table-column label="部件编号" prop="no" width="120" show-overflow-tooltip>
@@ -92,6 +87,16 @@
      <el-table-column label="部件类型" prop="typeText" width="120" show-overflow-tooltip>
        <template #default="scope">
          {{ getProcessTypeText(getProcessRaw(scope.row)?.type) || '-' }}
        </template>
      </el-table-column>
      <el-table-column label="产品名称" prop="productName" min-width="140" show-overflow-tooltip>
        <template #default="scope">
          {{ scope.row.productName || getProcessField(scope.row, 'productName') }}
        </template>
      </el-table-column>
      <el-table-column label="产品规格" prop="productModel" min-width="120" show-overflow-tooltip>
        <template #default="scope">
          {{ scope.row.model || getProcessField(scope.row, 'productModel') }}
        </template>
      </el-table-column>
      <el-table-column label="计划工时(小时)" prop="salaryQuota" width="130" align="center">
@@ -130,7 +135,7 @@
    <!-- å¡ç‰‡è§†å›¾ -->
    <template v-else>
      <div class="section-header">
        <div class="section-title">工艺路线项目列表</div>
        <div class="section-title">工序列表</div>
        <div class="section-actions">
          <el-button 
              icon="Menu" 
@@ -162,7 +167,8 @@
          <!-- ä¸Žå·¥åºä¸»è¡¨ä¸€è‡´çš„简要信息 -->
          <div class="card-content">
            <div class="product-info">
              <div class="product-name">{{ getProcessField(item, 'productModel') }}</div>
              <div class="product-name">{{ item.productName || getProcessField(item, 'productName') }}</div>
              <div class="product-model">{{ item.model || getProcessField(item, 'productModel') }}</div>
              <div v-if="getProcessRaw(item)?.no" class="product-model">编号 {{ getProcessRaw(item)?.no }}</div>
              <div v-if="getProcessTypeText(getProcessRaw(item)?.type)" class="product-model">
                {{ getProcessTypeText(getProcessRaw(item)?.type) }}
@@ -184,7 +190,7 @@
    <!-- æ–°å¢ž/编辑弹窗(布局、字段与工序/部件页 New、Edit ä¸€è‡´ï¼›æäº¤å‚数仍为原接口字段) -->
    <el-dialog
        v-model="dialogVisible"
        :title="operationType === 'add' ? '新增工艺路线项目' : '编辑工艺路线项目'"
        :title="operationType === 'add' ? '新增工序' : '编辑工序'"
        width="760"
        @close="closeDialog"
    >
@@ -322,7 +328,7 @@
const submitLoading = ref(false);
const cardsContainer = ref(null);
const tableRef = ref(null);
const viewMode = ref('table'); // table | card
const viewMode = ref('card'); // table | card
const routeInfo = ref({
  processRouteCode: '',
  productName: '',
src/views/productionManagement/productionOrder/New.vue
@@ -37,7 +37,7 @@
          <el-input v-model="formState.unit"  disabled />
        </el-form-item>
        <!-- <el-form-item label="工艺路线">
        <el-form-item label="工艺路线">
          <el-select v-model="formState.routeId"
                     placeholder="请选择工艺路线"
                     style="width: 100%;"
@@ -47,7 +47,7 @@
                       :label="`${item.processRouteCode || ''}`"
                       :value="item.id" />
          </el-select>
        </el-form-item> -->
        </el-form-item>
        <el-form-item
            label="需求数量"
src/views/productionManagement/productionProcess/Edit.vue
@@ -10,53 +10,21 @@
        <el-row :gutter="16">
          <el-col :span="12">
            <el-form-item
            label="产品名称:"
            prop="productId"
            label="部件名称"
            prop="name"
            :rules="[
                {
                required: true,
                message: '请选择产品名称',
              },
            ]">
              <el-tree-select
            v-model="formState.productId"
            placeholder="请选择产品名称"
            clearable
            filterable
            check-strictly
            :data="productCategoryOptions"
            :render-after-expand="false"
            style="width: 100%"
            @change="handleProductChange"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item
            label="产品规格:"
            prop="productModelId"
            :rules="[
                {
                required: true,
                message: '请选择产品规格',
              },
            ]">
              <el-select v-model="formState.productModelId"
                     placeholder="请选择产品规格"
                     clearable
                     filterable
                     :disabled="!formState.productId"
                     style="width: 100%">
                <el-option v-for="item in modelOptions"
                       :key="item.id"
                       :label="item.model"
                       :value="item.id" />
              </el-select>
                message: '请输入部件名称',
              }
            ]"
            >
              <el-input v-model="formState.name" placeholder="请输入部件名称" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="部件编号" prop="no">
              <el-input v-model="formState.no"  />
              <el-input v-model="formState.no" placeholder="请输入部件编号" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
@@ -111,6 +79,25 @@
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="计划执行人员" prop="executorId">
              <el-select
            v-model="formState.executorId"
            placeholder="请选择计划执行人员"
            clearable
            filterable
            style="width: 100%"
            @change="handleExecutorChange"
              >
                <el-option
              v-for="item in plannerOptions"
              :key="item.userId"
              :label="item.nickName"
              :value="item.userId"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="是否质检" prop="isQuality">
              <el-switch v-model="formState.isQuality" :active-value="true" inactive-value="false"/>
            </el-form-item>
@@ -135,7 +122,6 @@
<script setup>
import { ref, computed, getCurrentInstance, watch, onMounted } from "vue";
import {update} from "@/api/productionManagement/productionProcess.js";
import { modelListPage, productTreeList } from "@/api/basicData/product";
import { userListNoPageByTenantId } from "@/api/system/user.js";
const props = defineProps({
@@ -156,18 +142,16 @@
const formState = ref({
  id: props.record.id,
  name: props.record.name,
  productId: props.record.productId,
  productModelId: props.record.productModelId,
  type: props.record.type,
  no: props.record.no,
  remark: props.record.remark,
  salaryQuota: props.record.salaryQuota,
  plannerId: props.record.plannerId,
  plannerName: props.record.plannerName,
  executorId: props.record.executorId,
  executorName: props.record.executorName,
  isQuality: props.record.isQuality,
});
const productCategoryOptions = ref([]);
const modelOptions = ref([]);
const plannerOptions = ref([]);
const isShow = computed({
@@ -185,14 +169,14 @@
    formState.value = {
      id: newRecord.id,
      name: newRecord.name || '',
      productId: newRecord.productId,
      productModelId: getRecordProductModelId(newRecord),
      no: newRecord.no || '',
      type: newRecord.type,
      no: newRecord.no || '',
      remark: newRecord.remark || '',
      salaryQuota: newRecord.salaryQuota || '',
      plannerId: newRecord.plannerId,
      plannerName: newRecord.plannerName || '',
      executorId: newRecord.executorId,
      executorName: newRecord.executorName || '',
      isQuality: props.record.isQuality,
    };
  }
@@ -204,17 +188,16 @@
    formState.value = {
      id: props.record.id,
      name: props.record.name || '',
      productId: props.record.productId,
      productModelId: getRecordProductModelId(props.record),
      no: props.record.no || '',
      type: props.record.type,
      no: props.record.no || '',
      remark: props.record.remark || '',
      salaryQuota: props.record.salaryQuota || '',
      plannerId: props.record.plannerId,
      plannerName: props.record.plannerName || '',
      executorId: props.record.executorId,
      executorName: props.record.executorName || '',
      isQuality: props.record.isQuality,
    };
    getModelOptions(formState.value.productId);
  }
});
@@ -231,105 +214,6 @@
    return;
  }
  callback();
};
const convertProductTree = list => {
  return (list || []).map(item => {
    const children = convertProductTree(item.children || item.childList || []);
    return {
      ...item,
      value: item.id,
      label: item.name || item.label,
      children,
      disabled: children.length > 0,
    };
  });
};
const findNodeById = (nodes, targetId) => {
  for (const node of nodes || []) {
    if (String(node.value) === String(targetId)) {
      return node;
    }
    if (node.children && node.children.length > 0) {
      const found = findNodeById(node.children, targetId);
      if (found) return found;
    }
  }
  return null;
};
const findNodeIdByLabel = (nodes, targetLabel) => {
  for (const node of nodes || []) {
    if (node.label === targetLabel) {
      return node.value;
    }
    if (node.children && node.children.length > 0) {
      const found = findNodeIdByLabel(node.children, targetLabel);
      if (found !== null && found !== undefined) return found;
    }
  }
  return undefined;
};
function findModelIdByName(models, modelName) {
  if (!modelName) {
    return undefined;
  }
  const matched = (models || []).find(item => String(item.model) === String(modelName));
  return matched?.id;
}
function getRecordProductModelId(record) {
  if (!record) {
    return undefined;
  }
  return record.productModelId ?? record.modelId ?? record.specificationModelId ?? undefined;
}
function syncProductModelIdFromModelName() {
  if (formState.value.productModelId || modelOptions.value.length === 0) {
    return;
  }
  const modelName = props.record?.productModel || props.record?.model || props.record?.specificationModel || "";
  const matchedId = findModelIdByName(modelOptions.value, modelName);
  if (matchedId !== undefined) {
    formState.value.productModelId = matchedId;
  }
}
const getProductCategoryOptions = async () => {
  try {
    const res = await productTreeList();
    const list = Array.isArray(res) ? res : res?.data || [];
    productCategoryOptions.value = convertProductTree(list);
    if (!formState.value.productId && formState.value.name) {
      formState.value.productId = findNodeIdByLabel(productCategoryOptions.value, formState.value.name);
    }
    await getModelOptions(formState.value.productId);
    syncProductModelIdFromModelName();
  } catch (e) {
    productCategoryOptions.value = [];
  }
};
const getModelOptions = async productId => {
  if (!productId) {
    modelOptions.value = [];
    return;
  }
  try {
    const res = await modelListPage({
      id: productId,
      current: 1,
      size: 999,
    });
    const records = res?.records || res?.data?.records || [];
    modelOptions.value = records;
    syncProductModelIdFromModelName();
  } catch (e) {
    modelOptions.value = [];
  }
};
const getPlannerOptions = async () => {
@@ -350,11 +234,9 @@
  formState.value.plannerName = selectedUser?.nickName || '';
};
const handleProductChange = async value => {
  const selectedNode = findNodeById(productCategoryOptions.value, value);
  formState.value.name = selectedNode?.label || '';
  formState.value.productModelId = undefined;
  await getModelOptions(value);
const handleExecutorChange = value => {
  const selectedUser = plannerOptions.value.find(item => String(item.userId) === String(value));
  formState.value.executorName = selectedUser?.nickName || '';
};
const closeModal = () => {
@@ -376,7 +258,6 @@
};
onMounted(() => {
  getProductCategoryOptions();
  getPlannerOptions();
});
src/views/productionManagement/productionProcess/New.vue
@@ -10,53 +10,21 @@
        <el-row :gutter="16">
          <el-col :span="12">
            <el-form-item
            label="产品名称:"
            prop="productId"
            label="部件名称"
            prop="name"
            :rules="[
                {
                required: true,
                message: '请选择产品名称',
              },
            ]">
              <el-tree-select
            v-model="formState.productId"
            placeholder="请选择产品名称"
            clearable
            filterable
            check-strictly
            :data="productCategoryOptions"
            :render-after-expand="false"
            style="width: 100%"
            @change="handleProductChange"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item
            label="产品规格:"
            prop="productModelId"
            :rules="[
                {
                required: true,
                message: '请选择产品规格',
              },
            ]">
              <el-select v-model="formState.productModelId"
                     placeholder="请选择产品规格"
                     clearable
                     filterable
                     :disabled="!formState.productId"
                     style="width: 100%">
                <el-option v-for="item in modelOptions"
                       :key="item.id"
                       :label="item.model"
                       :value="item.id" />
              </el-select>
                message: '请输入部件名称',
              }
            ]"
            >
              <el-input v-model="formState.name" placeholder="请输入部件名称" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="部件编号" prop="no">
              <el-input v-model="formState.no"  />
              <el-input v-model="formState.no" placeholder="请输入部件编号" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
@@ -113,6 +81,25 @@
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="计划执行人员" prop="executorId">
              <el-select
            v-model="formState.executorId"
            placeholder="请选择计划执行人员"
            clearable
            filterable
            style="width: 100%"
            @change="handleExecutorChange"
              >
                <el-option
              v-for="item in plannerOptions"
              :key="item.userId"
              :label="item.nickName"
              :value="item.userId"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="是否质检" prop="isQuality">
              <el-switch v-model="formState.isQuality" :active-value="true" inactive-value="false"/>
            </el-form-item>
@@ -137,7 +124,6 @@
<script setup>
import { ref, computed, getCurrentInstance, onMounted } from "vue";
import {add} from "@/api/productionManagement/productionProcess.js";
import { modelListPage, productTreeList } from "@/api/basicData/product";
import { userListNoPageByTenantId } from "@/api/system/user.js";
const props = defineProps({
@@ -152,17 +138,15 @@
// å“åº”式数据(替代选项式的 data)
const formState = ref({
  name: '',
  productId: undefined,
  productModelId: undefined,
  type: undefined,
  remark: '',
  salaryQuota:  '',
  plannerId: undefined,
  plannerName: '',
  executorId: undefined,
  executorName: '',
  isQuality: false,
});
const productCategoryOptions = ref([]);
const modelOptions = ref([]);
const plannerOptions = ref([]);
const isShow = computed({
@@ -189,60 +173,6 @@
  callback();
};
const convertProductTree = list => {
  return (list || []).map(item => {
    const children = convertProductTree(item.children || item.childList || []);
    return {
      ...item,
      value: item.id,
      label: item.name || item.label,
      children,
      disabled: children.length > 0,
    };
  });
};
const findNodeById = (nodes, targetId) => {
  for (const node of nodes || []) {
    if (String(node.value) === String(targetId)) {
      return node;
    }
    if (node.children && node.children.length > 0) {
      const found = findNodeById(node.children, targetId);
      if (found) return found;
    }
  }
  return null;
};
const getProductCategoryOptions = async () => {
  try {
    const res = await productTreeList();
    const list = Array.isArray(res) ? res : res?.data || [];
    productCategoryOptions.value = convertProductTree(list);
  } catch (e) {
    productCategoryOptions.value = [];
  }
};
const getModelOptions = async productId => {
  if (!productId) {
    modelOptions.value = [];
    return;
  }
  try {
    const res = await modelListPage({
      id: productId,
      current: 1,
      size: 999,
    });
    const records = res?.records || res?.data?.records || [];
    modelOptions.value = records;
  } catch (e) {
    modelOptions.value = [];
  }
};
const getPlannerOptions = async () => {
  try {
    const res = await userListNoPageByTenantId();
@@ -257,11 +187,9 @@
  formState.value.plannerName = selectedUser?.nickName || '';
};
const handleProductChange = async value => {
  const selectedNode = findNodeById(productCategoryOptions.value, value);
  formState.value.name = selectedNode?.label || '';
  formState.value.productModelId = undefined;
  await getModelOptions(value);
const handleExecutorChange = value => {
  const selectedUser = plannerOptions.value.find(item => String(item.userId) === String(value));
  formState.value.executorName = selectedUser?.nickName || '';
};
const closeModal = () => {
@@ -283,7 +211,6 @@
};
onMounted(() => {
  getProductCategoryOptions();
  getPlannerOptions();
});
src/views/productionManagement/productionProcess/index.vue
@@ -115,14 +115,9 @@
  });
  const { searchForm } = toRefs(data);
  const tableColumn = ref([
    {
      label: "产品名称",
      label: "部件名称",
      prop: "name",
    },
    {
      label: "产品规格",
      prop: "productModel",
    },
    {
      label: "部件编号",
@@ -141,6 +136,10 @@
      prop: "plannerName",
    },
    {
      label: "计划执行人员",
      prop: "executorName",
    },
    {
      label: "是否质检",
      prop: "isQuality",
      formatData: (params) => {
src/views/salesManagement/salesLedger/index.vue
@@ -595,50 +595,6 @@
                        </el-form-item>
                    </el-col>
                </el-row>
        <!-- å®¡æ‰¹äººé€‰æ‹©ï¼ˆä»¿ååŒå®¡æ‰¹é‡Œçš„审批人节点选择) -->
        <el-row>
          <el-col :span="24">
            <el-form-item>
              <template #label>
                <span>审批人选择:</span>
                <el-button type="primary" @click="addApproverNode" style="margin-left: 8px;">新增节点</el-button>
              </template>
              <div style="display: flex; align-items: flex-end; flex-wrap: wrap;">
                <div
                  v-for="(node, index) in approverNodes"
                  :key="node.id"
                  style="margin-right: 20px; text-align: center; margin-bottom: 10px;"
                >
                  <div>
                    <span>审批人</span>
                    â†’
                  </div>
                  <el-select
                    v-model="node.userId"
                    placeholder="选择人员"
                    filterable
                    style="width: 140px; margin-bottom: 8px;"
                  >
                    <el-option
                      v-for="user in userList"
                      :key="user.userId"
                      :label="user.nickName"
                      :value="user.userId"
                    />
                  </el-select>
                  <div>
                    <el-button
                      type="danger"
                      @click="removeApproverNode(index)"
                      v-if="approverNodes.length > 1"
                    >删除</el-button>
                  </div>
                </div>
              </div>
            </el-form-item>
          </el-col>
        </el-row>
            </el-form>
            <template #footer>
                <div class="dialog-footer">
@@ -809,16 +765,6 @@
  },
});
const { deliveryForm, deliveryRules } = toRefs(deliveryFormData);
// å‘货审批人节点(仿协同审批 infoFormDia.vue)
const approverNodes = ref([{ id: 1, userId: null }]);
let nextApproverId = 2;
const addApproverNode = () => {
  approverNodes.value.push({ id: nextApproverId++, userId: null });
};
const removeApproverNode = (index) => {
  approverNodes.value.splice(index, 1);
};
// å¯¼å…¥ç›¸å…³
const importUploadRef = ref(null);
@@ -2115,9 +2061,6 @@
  deliveryForm.value = {
    type: "货车",
  };
  // é‡ç½®å®¡æ‰¹äººèŠ‚ç‚¹ï¼ˆé»˜è®¤ä¸€ä¸ªç©ºèŠ‚ç‚¹ï¼‰
  approverNodes.value = [{ id: 1, userId: null }];
  nextApproverId = 2;
    deliveryFormVisible.value = true;
};
@@ -2125,13 +2068,6 @@
const submitDelivery = () => {
  proxy.$refs["deliveryFormRef"].validate((valid) => {
    if (valid) {
      // å®¡æ‰¹äººå¿…填校验(所有节点都要选人)
      const hasEmptyApprover = approverNodes.value.some(node => !node.userId);
      if (hasEmptyApprover) {
        proxy.$modal.msgError("请为所有审批节点选择审批人!");
        return;
      }
      const approveUserIds = approverNodes.value.map(node => node.userId).join(",");
      // ä¿å­˜å½“前展开的行ID,以便发货后重新加载子表格数据
      const currentExpandedKeys = [...expandedRowKeys.value];
      const salesLedgerId = currentDeliveryRow.value.salesLedgerId;
@@ -2139,7 +2075,6 @@
        salesLedgerId: salesLedgerId,
        salesLedgerProductId: currentDeliveryRow.value.id,
        type: deliveryForm.value.type,
                approveUserIds,
      })
        .then(() => {
          proxy.$modal.msgSuccess("发货成功");
src/views/salesManagement/salesQuotation/index.vue
@@ -173,69 +173,6 @@
            </div>
          </el-card>
          <!-- å®¡æ‰¹äººä¿¡æ¯ -->
          <el-card class="form-card" shadow="hover">
            <template #header>
              <div class="card-header-wrapper">
                <el-icon class="card-icon">
                  <UserFilled/>
                </el-icon>
                <span class="card-title">审批人选择</span>
                <el-button type="primary" size="small" @click="addApproverNode" class="header-btn">
                  <el-icon>
                    <Plus/>
                  </el-icon>
                  æ–°å¢žèŠ‚ç‚¹
                </el-button>
              </div>
            </template>
            <div class="form-content">
              <el-row>
                <el-col :span="24">
                  <el-form-item>
                    <div class="approver-nodes-container">
                      <div
                          v-for="(node, index) in approverNodes"
                          :key="node.id"
                          class="approver-node-item"
                      >
                        <div class="approver-node-label">
                          <span class="node-step">{{ index + 1 }}</span>
                          <span class="node-text">审批人</span>
                          <el-icon class="arrow-icon">
                            <ArrowRight/>
                          </el-icon>
                        </div>
                        <el-select
                            v-model="node.userId"
                            placeholder="选择人员"
                            class="approver-select"
                            clearable
                        >
                          <el-option
                              v-for="user in userList"
                              :key="user.userId"
                              :label="user.nickName"
                              :value="user.userId"
                          />
                        </el-select>
                        <el-button
                            type="danger"
                            size="small"
                            :icon="Delete"
                            @click="removeApproverNode(index)"
                            v-if="approverNodes.length > 1"
                            class="remove-btn"
                        >删除
                        </el-button>
                      </div>
                    </div>
                  </el-form-item>
                </el-col>
              </el-row>
            </div>
          </el-card>
          <!-- äº§å“ä¿¡æ¯ -->
          <el-card class="form-card" shadow="hover">
            <template #header>
@@ -345,68 +282,6 @@
    <FormDialog v-model="importDialogVisible" title="导入报价单" width="85%" :close-on-click-modal="false"
                @close="importDialogVisible = false" @confirm="handleImportSubmit"
                @cancel="importDialogVisible = false">
      <!-- å®¡æ‰¹äººä¿¡æ¯ -->
      <el-card class="form-card" shadow="hover">
        <template #header>
          <div class="card-header-wrapper">
            <el-icon class="card-icon">
              <UserFilled/>
            </el-icon>
            <span class="card-title">审批人选择</span>
            <el-button type="primary" size="small" @click="addImportApproverNode" class="header-btn">
              <el-icon>
                <Plus/>
              </el-icon>
              æ–°å¢žèŠ‚ç‚¹
            </el-button>
          </div>
        </template>
        <div class="form-content">
          <el-row>
            <el-col :span="24">
              <el-form-item>
                <div class="approver-nodes-container">
                  <div
                      v-for="(node, index) in importApproverNodes"
                      :key="node.id"
                      class="approver-node-item"
                  >
                    <div class="approver-node-label">
                      <span class="node-step">{{ index + 1 }}</span>
                      <span class="node-text">审批人</span>
                      <el-icon class="arrow-icon">
                        <ArrowRight/>
                      </el-icon>
                    </div>
                    <el-select
                        v-model="node.userId"
                        placeholder="选择人员"
                        class="approver-select"
                        clearable
                    >
                      <el-option
                          v-for="user in userList"
                          :key="user.userId"
                          :label="user.nickName"
                          :value="user.userId"
                      />
                    </el-select>
                    <el-button
                        type="danger"
                        size="small"
                        :icon="Delete"
                        @click="removeImportApproverNode(index)"
                        v-if="importApproverNodes.length > 1"
                        class="remove-btn"
                    >删除
                    </el-button>
                  </div>
                </div>
              </el-form-item>
            </el-col>
          </el-row>
        </div>
      </el-card>
      <el-card class="form-card" shadow="hover">
        <template #header>
          <div class="card-header-wrapper">
@@ -546,10 +421,6 @@
const importDialogVisible = ref(false)
const viewDialogVisible = ref(false)
const importFileList = ref([])
const importApproverNodes = ref([
  {id: 1, userId: null}
])
let nextImportApproverId = 2
const fileUpload = ref(null)
@@ -599,36 +470,11 @@
const userList = ref([]);
const customerOption = ref([]);
// å®¡æ‰¹äººèŠ‚ç‚¹ç›¸å…³
const approverNodes = ref([
  {id: 1, userId: null}
])
let nextApproverId = 2
const isEdit = ref(false)
const showDetail = ref(false)
const editId = ref(null)
const currentQuotation = ref({})
const formRef = ref()
// æ·»åŠ å®¡æ‰¹äººèŠ‚ç‚¹
function addApproverNode() {
  approverNodes.value.push({id: nextApproverId++, userId: null})
}
// åˆ é™¤å®¡æ‰¹äººèŠ‚ç‚¹
function removeApproverNode(index) {
  approverNodes.value.splice(index, 1)
}
// å¯¼å…¥å¼¹çª—审批人节点相关
function addImportApproverNode() {
  importApproverNodes.value.push({id: nextImportApproverId++, userId: null})
}
function removeImportApproverNode(index) {
  importApproverNodes.value.splice(index, 1)
}
function triggerRemoveImportFile(file) {
  const index = importFileList.value.indexOf(file)
@@ -643,12 +489,6 @@
    return
  }
  const hasEmptyApprover = importApproverNodes.value.some(node => !node.userId)
  if (hasEmptyApprover) {
    ElMessage.error('请为所有审批节点选择审批人!')
    return
  }
  const selectedFile = importFileList.value[0]
  const rawFile = selectedFile?.raw || selectedFile
  if (!validateImportFile(rawFile)) {
@@ -657,10 +497,6 @@
  const formData = new FormData()
  formData.append('file', rawFile)
  // å®¡æ ¸äºº IDs,以逗号分割
  const approveUserIds = importApproverNodes.value.map(node => node.userId).join(',')
  formData.append('approveUserIdsJson', approveUserIds)
  loading.value = true
  try {
@@ -767,10 +603,6 @@
const handleImport = async () => {
  importFileList.value = []
  // âœ… æ¸…空“导入用”的审批人
  importApproverNodes.value = [{id: 1, userId: null}]
  nextImportApproverId = 2
  let userLists = await userListNoPage();
  importDialogVisible.value = true
@@ -785,9 +617,6 @@
  dialogTitle.value = '新增报价'
  isEdit.value = false
  resetForm()
  // é‡ç½®å®¡æ‰¹äººèŠ‚ç‚¹
  approverNodes.value = [{id: 1, userId: null}]
  nextApproverId = 2
  dialogVisible.value = true
  let userLists = await userListNoPage();
  // åªå¤åˆ¶éœ€è¦çš„字段,避免将组件引用放入响应式对象
@@ -992,19 +821,6 @@
  form.discountAmount = row.discountAmount || 0
  form.totalAmount = row.totalAmount || 0
  // åæ˜¾å®¡æ‰¹äºº
  if (row.approveUserIds) {
    const userIds = row.approveUserIds.split(',')
    approverNodes.value = userIds.map((userId, idx) => ({
      id: idx + 1,
      userId: parseInt(userId.trim())
    }))
    nextApproverId = userIds.length + 1
  } else {
    approverNodes.value = [{id: 1, userId: null}]
    nextApproverId = 2
  }
  // åŠ è½½ç”¨æˆ·åˆ—è¡¨
  let userLists = await userListNoPage();
  userList.value = (userLists.data || []).map(item => ({
@@ -1103,16 +919,6 @@
        return
      }
      // å®¡æ‰¹äººå¿…填校验
      const hasEmptyApprover = approverNodes.value.some(node => !node.userId)
      if (hasEmptyApprover) {
        ElMessage.error('请为所有审批节点选择审批人!')
        return
      }
      // æ”¶é›†æ‰€æœ‰èŠ‚ç‚¹çš„å®¡æ‰¹äººid
      form.approveUserIds = approverNodes.value.map(node => node.userId).join(',')
      // è®¡ç®—所有产品的单价总和
      form.totalAmount = form.products.reduce((sum, product) => {
        const price = Number(product.unitPrice) || 0
@@ -1177,8 +983,6 @@
        validDate: item.validDate || '',
        paymentMethod: item.paymentMethod || '',
        status: item.status || '草稿',
        // å®¡æ‰¹äººï¼ˆç”¨äºŽç¼–辑时反显)
        approveUserIds: item.approveUserIds || '',
        remark: item.remark || '',
        products: item.products ? item.products.map(product => ({
          productId: product.productId || '',