gaoluyang
2026-04-22 75b6b0a858f207518a303de28a252081be840181
升级pro
1.添加审批管理页面
2.协同审批页面样式优化
已添加2个文件
已修改1个文件
1412 ■■■■■ 文件已修改
src/api/collaborativeApproval/approvalManagement.js 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalManagement/index.vue 884 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index.vue 474 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/collaborativeApproval/approvalManagement.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,54 @@
// å®¡æ‰¹ç®¡ç†é…ç½®
import request from "@/utils/request";
// æŸ¥è¯¢å®¡æ‰¹é…ç½®åˆ—表
export function getApprovalConfigList(approveType) {
    return request({
        url: '/approvalConfig/list',
        method: 'get',
        params: { approveType },
    })
}
// æŸ¥è¯¢å®¡æ‰¹é…ç½®è¯¦æƒ…
export function getApprovalConfigDetail(id) {
    return request({
        url: '/approvalConfig/get/' + id,
        method: 'get',
    })
}
// æ–°å¢žå®¡æ‰¹é…ç½®
export function addApprovalConfig(data) {
    return request({
        url: '/approvalConfig/add',
        method: 'post',
        data: data,
    })
}
// ä¿®æ”¹å®¡æ‰¹é…ç½®
export function updateApprovalConfig(data) {
    return request({
        url: '/approvalConfig/update',
        method: 'post',
        data: data,
    })
}
// åˆ é™¤å®¡æ‰¹é…ç½®
export function deleteApprovalConfig(id) {
    return request({
        url: '/approvalConfig/delete/' + id,
        method: 'delete',
    })
}
// æ‰¹é‡ä¿å­˜å®¡æ‰¹é…ç½®
export function batchSaveApprovalConfig(data) {
    return request({
        url: '/approvalConfig/batchSave',
        method: 'post',
        data: data,
    })
}
src/views/collaborativeApproval/approvalManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,884 @@
<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
} from '@element-plus/icons-vue';
// å½“前选中的标签页
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' },
];
// å®¡æ‰¹ç±»åž‹åç§°æ˜ å°„
const approveTypeNameMap = {
  1: '公出管理',
  2: '请假管理',
  3: '出差管理',
  4: '报销管理',
  5: '采购审批',
  6: '报价审批',
  7: '发货审批',
};
// å®¡æ‰¹ç±»åž‹å›¾æ ‡æ˜ å°„
const typeIconMap = {
  1: 'Suitcase',
  2: 'Calendar',
  3: 'Location',
  4: 'Money',
  5: 'ShoppingCart',
  6: 'DocumentChecked',
  7: 'Van',
};
// å®¡æ‰¹ç±»åž‹é¢œè‰²æ˜ å°„
const typeColorMap = {
  1: '#409EFF',
  2: '#67C23A',
  3: '#E6A23C',
  4: '#F56C6C',
  5: '#909399',
  6: '#9B59B6',
  7: '#1ABC9C',
};
// å¤´åƒé¢œè‰²æ± 
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 getApproverCount = (typeValue) => {
  const type = Number(typeValue);
  const data = mockConfigData[type] || [];
  return data.length;
};
// æ¨¡æ‹Ÿç”¨æˆ·åˆ—表数据
const userList = ref([
  { userId: 1, nickName: '张三' },
  { userId: 2, nickName: '李四' },
  { userId: 3, nickName: '王五' },
  { userId: 4, nickName: '赵六' },
  { userId: 5, nickName: '孙七' },
  { userId: 6, nickName: '周八' },
  { userId: 7, nickName: '吴九' },
  { userId: 8, nickName: '郑十' },
]);
// æ¨¡æ‹Ÿå®¡æ‰¹é…ç½®æ•°æ®å­˜å‚¨ï¼ˆæŒ‰å®¡æ‰¹ç±»åž‹åˆ†ç±»ï¼‰
const mockConfigData = {
  1: [
    { id: 1, approveType: 1, approverId: 1, approverName: '张三', sortOrder: 1 },
    { id: 2, approveType: 1, approverId: 2, approverName: '李四', sortOrder: 2 },
  ],
  2: [
    { id: 3, approveType: 2, approverId: 3, approverName: '王五', sortOrder: 1 },
  ],
  3: [],
  4: [
    { id: 4, approveType: 4, approverId: 1, approverName: '张三', sortOrder: 1 },
    { id: 5, approveType: 4, approverId: 3, approverName: '王五', sortOrder: 2 },
    { id: 6, approveType: 4, approverId: 5, approverName: '孙七', sortOrder: 3 },
  ],
  5: [],
  6: [],
  7: [],
};
// å®¡æ‰¹äººåˆ—表
const approverList = ref([]);
// åŽŸå§‹æ•°æ®ï¼Œç”¨äºŽé‡ç½®
const originalList = ref([]);
// åŠ è½½çŠ¶æ€
const loading = ref(false);
const saveLoading = ref(false);
// æ ‡ç­¾é¡µåˆ‡æ¢å¤„理
const handleTabChange = () => {
  loadData();
};
// åŠ è½½å®¡æ‰¹é…ç½®æ•°æ®ï¼ˆæ¨¡æ‹Ÿï¼‰
const loadData = () => {
  loading.value = true;
  setTimeout(() => {
    const data = mockConfigData[currentApproveType.value] || [];
    approverList.value = data.sort((a, b) => a.sortOrder - b.sortOrder);
    originalList.value = JSON.parse(JSON.stringify(approverList.value));
    loading.value = false;
  }, 300);
};
// å®¡æ‰¹äººé€‰æ‹©å˜åŒ–
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 = () => {
  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;
  setTimeout(() => {
    mockConfigData[currentApproveType.value] = approverList.value.map((item, index) => ({
      ...item,
      id: item.id || Date.now() + index,
      sortOrder: index + 1,
    }));
    originalList.value = JSON.parse(JSON.stringify(mockConfigData[currentApproveType.value]));
    approverList.value = JSON.parse(JSON.stringify(originalList.value));
    ElMessage.success('保存成功');
    saveLoading.value = false;
  }, 500);
};
// é‡ç½®
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(() => {
  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/index.vue
@@ -1,67 +1,129 @@
<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="search_form">
      <div>
        <span class="search_title">流程编号:</span>
        <el-input
            v-model="searchForm.approveId"
            style="width: 240px"
            placeholder="请输入流程编号搜索"
            @change="handleQuery"
            clearable
            :prefix-icon="Search"
        />
        <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>
        <el-button
          type="primary"
          @click="openForm('add')"
          v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7"
        >新增</el-button>
        <el-button @click="handleOut">导出</el-button>
        <el-button
          type="danger"
          plain
          @click="handleDelete"
          v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7"
        >删除</el-button>
    <!-- å®¡æ‰¹ç±»åž‹åˆ‡æ¢ - ç´§å‡‘标签式 -->
    <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="table_list">
    <!-- æœç´¢å’Œæ“ä½œåŒºåŸŸ -->
    <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"
              placeholder="请输入流程编号"
              clearable
              :prefix-icon="Search"
              @keyup.enter="handleQuery"
              class="search-input"
            />
          </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"
            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"
            class="action-btn danger"
          >
            <el-icon><Delete /></el-icon>
            åˆ é™¤
          </el-button>
        </div>
      </div>
    </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"
          :tableData="tableData"
          :page="page"
          :isSelection="true"
          @selection-change="handleSelectionChange"
          :tableLoading="tableLoading"
          @pagination="pagination"
          :total="page.total"
        rowKey="id"
        :column="tableColumnCopy"
        :tableData="tableData"
        :page="page"
        :isSelection="true"
        @selection-change="handleSelectionChange"
        :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,37 @@
// å½“前选中的标签页,默认为公出管理
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' },
];
// å½“前审批类型信息
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 = '';
@@ -102,11 +188,18 @@
const data = reactive({
  searchForm: {
        approveId: "",
        approveStatus: "",
    approveId: "",
    approveStatus: "",
  },
});
const { searchForm } = toRefs(data);
// é‡ç½®æœç´¢
const resetQuery = () => {
  searchForm.value.approveId = '';
  searchForm.value.approveStatus = '';
  handleQuery();
};
// åŠ¨æ€è¡¨æ ¼åˆ—é…ç½®ï¼Œæ ¹æ®å®¡æ‰¹ç±»åž‹ç”Ÿæˆåˆ—
const tableColumnCopy = computed(() => {
@@ -238,7 +331,7 @@
    },
  ];
  // æŠ¥ä»·å®¡æ‰¹ï¼ˆç±»åž‹ 6)不展示“附件”操作
  // æŠ¥ä»·å®¡æ‰¹ï¼ˆç±»åž‹ 6)不展示"附件"操作
  if (!isQuotationType) {
    actionOperations.push({
      name: "附件",
@@ -294,6 +387,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;
  })
@@ -388,7 +483,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>