647556386
2026-01-27 1378fc4cd7abc24ed3a982e09437c2c8a74e9f2f
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/Home.vue
@@ -1,24 +1,818 @@
<template>
  <div class="title"></div>
  <div class="wms-dashboard">
    <!-- ç»Ÿè®¡å¡ç‰‡åŒºåŸŸ -->
    <el-row :gutter="20" class="stats-card-row">
      <el-col :span="4">
        <div class="stats-card">
          <div class="metric-icon">
            <el-icon :size="32"><Box /></el-icon>
          </div>
          <div class="card-title">待入库订单</div>
          <div class="card-value">{{ bigscreendata.unInBoundOrderCount }}</div>
        </div>
      </el-col>
      <el-col :span="4">
        <div class="stats-card">
          <div class="metric-icon">
            <el-icon :size="32"><Document /></el-icon>
          </div>
          <div class="card-title">待出库订单</div>
          <div class="card-value">{{ bigscreendata.unOutBoundOrderCount }}</div>
        </div>
      </el-col>
      <el-col :span="4">
        <div class="stats-card">
          <div class="metric-icon">
            <el-icon :size="32"><Download /></el-icon>
          </div>
          <div class="card-title">今日入库完成箱数</div>
          <div class="card-value">{{ bigscreendata.inboundCount }}</div>
        </div>
      </el-col>
      <el-col :span="4">
        <div class="stats-card">
          <div class="metric-icon">
            <el-icon :size="32"><Upload /></el-icon>
          </div>
          <div class="card-title">今日出库完成箱数</div>
          <div class="card-value">{{ bigscreendata.outboundCount }}</div>
        </div>
      </el-col>
      <el-col :span="4">
        <div class="stats-card">
          <div class="metric-icon">
            <el-icon :size="32"><Box /></el-icon>
          </div>
          <div class="card-title">有货料箱</div>
          <div class="card-value">{{ formatNumber(bigscreendata.inStockPallet) }}</div>
        </div>
      </el-col>
      <el-col :span="4">
        <div class="stats-card">
          <div class="metric-icon">
            <el-icon :size="32"><Box /></el-icon>
          </div>
          <div class="card-title">空箱数量</div>
          <div class="card-value">{{ formatNumber(bigscreendata.freeStockPallet) }}</div>
        </div>
      </el-col>
    </el-row>
    <!-- å›¾è¡¨åŒºåŸŸï¼ˆç¬¬ä¸€è¡Œï¼‰ -->
    <el-row :gutter="20" class="chart-row">
      <el-col :span="8">
        <div class="chart-card">
          <div class="chart-title">库位利用率</div>
          <div ref="locationRateRef" class="chart-container"></div>
        </div>
      </el-col>
      <el-col :span="8">
        <div class="chart-card">
          <div class="chart-title">物料临期信息</div>
          <div class="expiration-table-container">
            <el-table
              :data="expirationTableData"
              border
              stripe
              style="width: 100%;"
              :empty-text="emptyText"
            >
              <el-table-column prop="materielCode" label="物料编码" align="center" />
              <el-table-column prop="materielName" label="物料名称" align="center" show-overflow-tooltip />
              <el-table-column prop="batchNo" label="批次号" align="center" />
              <el-table-column prop="validDate" label="有效期" align="center" />
              <el-table-column label="临期等级" align="center">
                <template #default="{ row }">
                  <span :class="getExpireLevelClass(row.expireLevel)">{{ row.expireLevel }}</span>
                </template>
              </el-table-column>
              <el-table-column prop="daysToExpiration" label="临期天数" align="center">
                <template #default="{ row }">
                  <span :class="row.daysToExpiration < 0 ? 'text-red' : ''">
                    {{ row.daysToExpiration < 0 ? '已过期' : `${row.daysToExpiration}天` }}
                  </span>
                </template>
              </el-table-column>
              <el-table-column prop="stockQuantity" label="库存数量" align="center" />
              <el-table-column prop="locationCode" label="库位" align="center" />
              <el-table-column prop="palletCode" label="托盘编号" align="center" />
            </el-table>
          </div>
        </div>
      </el-col>
      <el-col :span="8">
        <div class="chart-card">
          <div class="chart-title">近7日出入库单据趋势</div>
          <div ref="stockTrendRef" class="chart-container"></div>
        </div>
      </el-col>
    </el-row>
    <!-- è¡¨æ ¼åŒºåŸŸ - å®žæ—¶ä½œä¸šç›‘控(区分正常/失败作业) -->
    <el-row :gutter="20" class="table-row" width="100%">
      <!-- å·¦ä¾§ï¼šå½“天正常作业单据 -->
      <el-col :span="12">
        <div class="table-card">
          <div class="table-title">实时作业监控(正常单据)</div>
          <el-table
            :data="normalShowTaskList"
            border
            style="width: 100%;"
            :empty-text="normalShowTaskList.length === 0 ? '暂无正常作业数据' : ''"
          >
            <el-table-column prop="upperOrderNo" label="单据编号" />
            <el-table-column label="单据状态" >
              <template #default="{ row }">
                <span class="task-status" :class="getStatusClass(row.taskStatus)">
                  {{ getTaskStatusText(row.taskStatus, row.taskType) }}
                </span>
              </template>
            </el-table-column>
            <el-table-column label="单据类型" >
              <template #default="{ row }">
                <span class="task-type" :class="getTypeClass(row.taskType)">
                  {{ getTaskTypeText(row) }}
                </span>
              </template>
            </el-table-column>
            <el-table-column label="回传MES状态" >
              <template #default="{ row }">
                <span class="task-status" :class="getMESStatusClass(row.returnToMESStatus)">
                  {{ getMESStatusText(row.returnToMESStatus) }}
                </span>
              </template>
            </el-table-column>
            <el-table-column prop="factoryArea" label="厂区" />
            <el-table-column prop="modifier" label="修改人" />
            <el-table-column prop="createDate" label="创建时间"/>
            <el-table-column prop="modifyDate" label="修改时间"/>
          </el-table>
          <div class="table-pagination">
            <el-pagination
              layout="prev, pager, next, jumper"
              v-model:current-page="normalCurrentPage"
              :page-size="5"
              :total="normalTaskList.length"
              @current-change="handleNormalPageChange"
            />
          </div>
        </div>
      </el-col>
      <!-- å³ä¾§ï¼šè¿‘3天回传失败/部分失败单据 -->
      <el-col :span="12">
        <div class="table-card">
          <div class="table-title">实时作业监控(回传失败)</div>
          <el-table
            :data="failShowTaskList"
            border
            style="width: 100%;"
            :empty-text="failShowTaskList.length === 0 ? '暂无回传失败数据' : ''"
          >
            <el-table-column prop="upperOrderNo" label="单据编号" />
            <el-table-column label="单据状态" >
              <template #default="{ row }">
                <span class="task-status" :class="getStatusClass(row.taskStatus)">
                  {{ getTaskStatusText(row.taskStatus, row.taskType) }}
                </span>
              </template>
            </el-table-column>
            <el-table-column label="单据类型" >
              <template #default="{ row }">
                <span class="task-type" :class="getTypeClass(row.taskType)">
                  {{ getTaskTypeText(row) }}
                </span>
              </template>
            </el-table-column>
            <el-table-column label="回传MES状态" >
              <template #default="{ row }">
                <span class="task-status" :class="getMESStatusClass(row.returnToMESStatus)">
                  {{ getMESStatusText(row.returnToMESStatus) }}
                </span>
              </template>
            </el-table-column>
            <el-table-column prop="remark" label="失败原因" show-overflow-tooltip />
            <el-table-column prop="factoryArea" label="厂区" />
            <el-table-column prop="modifier" label="修改人" />
            <el-table-column prop="createDate" label="创建时间"/>
            <el-table-column prop="modifyDate" label="修改时间"/>
          </el-table>
          <div class="table-pagination">
            <el-pagination
              layout="prev, pager, next, jumper"
              v-model:current-page="failCurrentPage"
              :page-size="5"
              :total="failTaskList.length"
              @current-change="handleFailPageChange"
            />
          </div>
        </div>
      </el-col>
    </el-row>
  </div>
</template>
<script>
import { ref, reactive } from 'vue'
<script setup>
import { ref, onMounted, onUnmounted, nextTick, computed, watch } from 'vue';
import * as echarts from 'echarts';
import http from "@/api/http.js";
import { Box, Document, Download, Upload } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
export default {
  setup() {
// å“åº”式数据
const bigscreendata = ref({
  totalStockQuantity: 0,
  unOutBoundOrderCount: 0,
  unInBoundOrderCount:0,
  dailyCompletionRate: 0,
  unhandledExceptionCount: 0,
  locationUtilizationRate: 0,
  inStockPallet: 0,
  freeStockPallet: 0,
  dailyInOutBoundList: [],
  completeTask: [], // åŽç«¯è¿”回的任务数据(当天正常+近3天失败)
  inboundCount: 0,
  outboundCount: 0,
  inventoryLocationDist: [],
  exceptionTypeTrend: [],
  nearExpirationList: []
});
// ä¸´æœŸè¡¨æ ¼æ•°æ®ï¼ˆä¿æŒä¸å˜ï¼‰
const expirationTableData = computed(() => {
  const expirationList = bigscreendata.value.nearExpirationList || [];
  const uniqueMap = new Map();
  expirationList.forEach(item => {
    const uniqueKey = [item.materielCode, item.batchNo, item.palletCode].join('|');
    if (!uniqueMap.has(uniqueKey)) uniqueMap.set(uniqueKey, item);
  });
  const uniqueExpirationList = Array.from(uniqueMap.values());
  return uniqueExpirationList.map(item => {
    const daysToExpiration = item.daysToExpiration || 0;
    let expireLevel = '';
    if (daysToExpiration < 0) expireLevel = '已过期';
    else if (daysToExpiration <= 7) expireLevel = '7天内临期';
    else if (daysToExpiration <= 15) expireLevel = '15天内临期';
    else if (daysToExpiration <= 30) expireLevel = '30天内临期';
    else expireLevel = '30天以上';
    return {
      materielCode: item.materielCode,
      materielName: item.materielName,
      batchNo: item.batchNo,
      validDate: item.validDate,
      daysToExpiration: daysToExpiration,
      expireLevel: expireLevel,
      stockQuantity: item.stockQuantity || 0,
      locationCode: item.locationCode,
      palletCode: item.palletCode,
      unit: item.unit
    };
  });
});
    }
const emptyText = computed(() => {
  const expirationList = bigscreendata.value.nearExpirationList || [];
  return expirationList.length === 0 ? '暂无临期物料数据' : '';
});
const getExpireLevelClass = (level) => {
  switch(level) {
    case '已过期': return 'expire-level expired';
    case '7天内临期': return 'expire-level urgent';
    case '15天内临期': return 'expire-level warning';
    case '30天内临期': return 'expire-level normal';
    case '30天以上': return 'expire-level low';
    default: return 'expire-level default';
  }
}
};
// ä¸šåŠ¡ç±»åž‹æ˜ å°„
const inboundBusinessTypeMap = {
  11: "采购入库", 12: "杂收单", 13: "生产退料单",
  14: "外协退料单", 15: "销售退库单", 3: "调拨入库单"
};
const outboundBusinessTypeMap = {
  21: "工单领料出库单", 22: "杂发单", 23: "退货单",
  24: "销售出库单", 25: "外协领料申请单", 2: "调拨出库单"
};
// æ‹†åˆ†ï¼šå½“天正常作业单据
const normalTaskList = computed(() => {
  const taskData = (bigscreendata.value.completeTask[0] || {});
  const inboundOrders = taskData.inboundOrders || [];
  const outboundOrders = taskData.outboundOrders || [];
  // æ ¼å¼åŒ–入库订单
  const formattedInbound = inboundOrders.map(item => ({
    upperOrderNo: item.upperOrderNo || item.inboundOrderNo,
    taskStatus: item.orderStatus || 0,
    taskType: 'inbound',
    businessType: item.businessType || '',
    returnToMESStatus: item.returnToMESStatus || 0,
    factoryArea: item.factoryArea || '',
    modifier: item.modifier || '',
    createDate: item.createDate || '',
    modifyDate: item.modifyDate || '',
    remark: item.remark || ''
  }));
  // æ ¼å¼åŒ–出库订单
  const formattedOutbound = outboundOrders.map(item => ({
    upperOrderNo: item.upperOrderNo || item.orderNo,
    taskStatus: item.orderStatus || 0,
    taskType: 'outbound',
    businessType: item.businessType || '',
    returnToMESStatus: item.returnToMESStatus || 0,
    factoryArea: item.factoryArea || '',
    modifier: item.modifier || '',
    createDate: item.createDate || '',
    modifyDate: item.modifyDate || '',
    remark: item.remark || ''
  }));
  return [...formattedInbound, ...formattedOutbound];
});
// æ‹†åˆ†ï¼šè¿‘3天回传失败/部分失败单据
const failTaskList = computed(() => {
  const taskData = (bigscreendata.value.completeTask[0] || {});
  const inboundFailOrders = taskData.inboundReturnFailOrders || [];
  const outboundFailOrders = taskData.outboundReturnFailOrders || [];
  const formattedInboundFail = inboundFailOrders.map(item => ({
    upperOrderNo: item.upperOrderNo || item.inboundOrderNo,
    taskStatus: item.orderStatus || 0,
    taskType: 'inbound',
    businessType: item.businessType || '',
    returnToMESStatus: item.returnToMESStatus || 0,
    factoryArea: item.factoryArea || '',
    modifier: item.modifier || '',
    createDate: item.createDate || '',
    modifyDate: item.modifyDate || '',
    remark: item.remark || ''
  }));
  const formattedOutboundFail = outboundFailOrders.map(item => ({
    upperOrderNo: item.upperOrderNo || item.orderNo,
    taskStatus: item.orderStatus || 0,
    taskType: 'outbound',
    businessType: item.businessType || '',
    returnToMESStatus: item.returnToMESStatus || 0,
    factoryArea: item.factoryArea || '',
    modifier: item.modifier || '',
    createDate: item.createDate || '',
    modifyDate: item.modifyDate || '',
    remark: item.remark || ''
  }));
  return [...formattedInboundFail, ...formattedOutboundFail];
});
// åˆ†é¡µ&轮播相关响应式变量
const normalCurrentPage = ref(1);
const failCurrentPage = ref(1);
const normalShowTaskList = ref([]);
const failShowTaskList = ref([]);
let normalCarouselTimer = null;
let failCarouselTimer = null;
// ä¿®å¤ï¼šå•据状态文本(新增taskType参数)
const getTaskStatusText = (statusNum, taskType) => {
  const statusMap = {
    0: "未开始",
    1: taskType === 'inbound' ? "入库中" : (taskType === 'outbound' ? "出库中" : "处理中"),
    2: "处理中",
    3: "已完成",
    4: "已取消",
    5: "异常"
  };
  return statusMap[statusNum] ;
};
// MES回传状态映射
const mesStatusMap = {
  0: "未回传", 1: "回传成功", 2: "回传失败",
  3: "部分回传成功", 4: "部分回传失败"
};
const getMESStatusText = (statusNum) => {
  if (statusNum === undefined || statusNum === null || isNaN(statusNum)) return "未知状态";
  return mesStatusMap[statusNum] || `未知状态(${statusNum})`;
};
const getMESStatusClass = (statusNum) => {
  if (statusNum === undefined || statusNum === null || isNaN(statusNum)) return "status-unknown";
  const classMap = { 0: "status-pending", 1: "status-completed", 2: "status-error", 3: "status-processing", 4: "status-error" };
  return classMap[statusNum] || "status-unknown";
};
// å•据类型文本/样式
const getTaskTypeText = (row) => {
  const businessType = Number(row.businessType) || 0;
  if (row.taskType === 'inbound') {
    return inboundBusinessTypeMap[businessType] || `未知类型(${businessType})`;
  } else if (row.taskType === 'outbound') {
    return outboundBusinessTypeMap[businessType] || `未知类型(${businessType})`;
  }
  return "其他作业";
};
const getStatusClass = (statusNum) => {
  if (statusNum === undefined || statusNum === null || isNaN(statusNum)) return "status-unknown";
  const classMap = { 0: "status-pending", 1: "status-processing", 2: "status-processing", 3: "status-completed", 4: "status-canceled", 5: "status-error" };
  return classMap[statusNum] || "status-unknown";
};
const getTypeClass = (taskType) => {
  const classMap = { 'inbound': "type-inbound", 'outbound': "type-outbound" };
  return classMap[taskType] || "type-other";
};
// æ•°å­—格式化
const formatNumber = (num) => {
  if (!num) return '0';
  return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};
// åˆå§‹åŒ–表格数据(分页+轮播)
const initTableData = () => {
  // åœæ­¢æ—§å®šæ—¶å™¨
  clearInterval(normalCarouselTimer);
  clearInterval(failCarouselTimer);
  // æ­£å¸¸å•据分页/轮播
  const loadNormalData = (page) => {
    const start = (page - 1) * 5;
    const end = start + 5;
    normalShowTaskList.value = normalTaskList.value.slice(start, end);
  };
  loadNormalData(normalCurrentPage.value);
  // å¤±è´¥å•据分页/轮播
  const loadFailData = (page) => {
    const start = (page - 1) * 5;
    const end = start + 5;
    failShowTaskList.value = failTaskList.value.slice(start, end);
  };
  loadFailData(failCurrentPage.value);
  // è½®æ’­é€»è¾‘(仅数据超过5条时开启)
  if (normalTaskList.value.length > 5) {
    normalCarouselTimer = setInterval(() => {
      normalCurrentPage.value = normalCurrentPage.value >= Math.ceil(normalTaskList.value.length / 5) ? 1 : normalCurrentPage.value + 1;
      loadNormalData(normalCurrentPage.value);
    }, 5000);
  }
  if (failTaskList.value.length > 5) {
    failCarouselTimer = setInterval(() => {
      failCurrentPage.value = failCurrentPage.value >= Math.ceil(failTaskList.value.length / 5) ? 1 : failCurrentPage.value + 1;
      loadFailData(failCurrentPage.value);
    }, 5000);
  }
};
// åˆ†é¡µåˆ‡æ¢äº‹ä»¶
const handleNormalPageChange = (page) => {
  clearInterval(normalCarouselTimer); // æ‰‹åŠ¨åˆ‡æ¢åˆ†é¡µæ—¶åœæ­¢è½®æ’­
  normalCurrentPage.value = page;
  const start = (page - 1) * 5;
  const end = start + 5;
  normalShowTaskList.value = normalTaskList.value.slice(start, end);
};
const handleFailPageChange = (page) => {
  clearInterval(failCarouselTimer);
  failCurrentPage.value = page;
  const start = (page - 1) * 5;
  const end = start + 5;
  failShowTaskList.value = failTaskList.value.slice(start, end);
};
// ç›‘听数据变化,重新初始化表格
watch([normalTaskList, failTaskList], () => {
  initTableData();
}, { deep: true });
// èŽ·å–åŽç«¯æ•°æ®
const fetchBigGreenData = async () => {
  try {
    const res = await http.get('/api/BigScreen/GetBigGreenData');
    bigscreendata.value = res.data || res;
    nextTick(() => {
      initTableData();
      refreshCharts();
    });
  } catch (error) {
    ElMessage.error('数据获取失败,请检查后端接口是否正常');
    console.error('数据获取失败:', error);
  }
};
// å›¾è¡¨ç›¸å…³ï¼ˆç²¾ç®€å†—余逻辑)
const stockTrendRef = ref(null);
const locationRateRef = ref(null);
let stockTrendChart = null;
let locationRateChart = null;
const initStockTrend = () => {
  if (!stockTrendRef.value) return;
  if (stockTrendChart) stockTrendChart.dispose();
  stockTrendChart = echarts.init(stockTrendRef.value);
  const trendData = bigscreendata.value.dailyInOutBoundList;
  const maxInbound = trendData.length ? Math.max(...trendData.map(item => item.dailyInboundQuantity || 0)) : 0;
  const maxOutbound = trendData.length ? Math.max(...trendData.map(item => item.dailyOutboundQuantity || 0)) : 0;
  const maxValue = Math.max(maxInbound, maxOutbound);
  const option = {
    tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } },
    legend: { data: ['入库量', '出库量'], top: 10 },
    grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true },
    xAxis: { type: 'category', boundaryGap: true, data: trendData.map(item => item.date) },
    yAxis: { type: 'value', name: '数量', min: 0, max: maxValue > 0 ? Math.ceil(maxValue * 1.2) : 10 },
    series: [
      { name: '入库量', type: 'bar', barWidth: '30%', data: trendData.map(item => item.dailyInboundQuantity), itemStyle: { color: '#52c41a', borderRadius: [4, 4, 0, 0] } },
      { name: '出库量', type: 'bar', barWidth: '30%', data: trendData.map(item => item.dailyOutboundQuantity), itemStyle: { color: '#1890ff', borderRadius: [4, 4, 0, 0] } }
    ]
  };
  stockTrendChart.setOption(option);
  return stockTrendChart;
};
const initLocationRate = () => {
  if (!locationRateRef.value) return;
  if (locationRateChart) locationRateChart.dispose();
  locationRateChart = echarts.init(locationRateRef.value);
  const utilizationRate = bigscreendata.value.locationUtilizationRate || 0;
  const freeRate = 100 - utilizationRate;
  const option = {
    tooltip: { trigger: 'item', formatter: '{b}: {c}%' },
    legend: { bottom: 0, left: 'center', data: ['已占用库位', '空闲库位'], textStyle: { fontSize: 12, color: '#666' } },
    graphic: [
      { type: 'text', left: 'right', top: '10%', style: { text: `${utilizationRate}%`, fontSize: 24, fontWeight: 'bold', fill: '#333' } },
      { type: 'text', left: 'right', top: '25%', style: { text: '库位利用率', fontSize: 14, fill: '#666' } }
    ],
    series: [{
      type: 'pie', radius: ['50%', '70%'], center: ['40%', '50%'],
      avoidLabelOverlap: false, label: { show: false }, labelLine: { show: false },
      data: [
        { value: utilizationRate, name: '已占用库位', itemStyle: { color: '#1890ff' } },
        { value: freeRate, name: '空闲库位', itemStyle: { color: '#e5e9f2' } }
      ]
    }]
  };
  locationRateChart.setOption(option);
  return locationRateChart;
};
const refreshCharts = () => {
  [initStockTrend, initLocationRate].forEach(initFunc => {
    const chart = initFunc();
    if (chart) chart.resize();
  });
};
// é˜²æŠ–处理resize事件
let resizeTimer = null;
const handleResize = () => {
  clearTimeout(resizeTimer);
  resizeTimer = setTimeout(() => {
    [stockTrendChart, locationRateChart].forEach(chart => {
      if (chart) chart.resize();
    });
  }, 200);
};
// ç”Ÿå‘½å‘¨æœŸ
onMounted(() => {
  fetchBigGreenData();
  nextTick(() => {
    initStockTrend();
    initLocationRate();
    window.addEventListener('resize', handleResize);
  });
});
onUnmounted(() => {
  // é”€æ¯å›¾è¡¨å’Œå®šæ—¶å™¨
  [stockTrendChart, locationRateChart].forEach(chart => {
    if (chart) chart.dispose();
  });
  [normalCarouselTimer, failCarouselTimer, resizeTimer].forEach(timer => {
    clearInterval(timer);
    clearTimeout(timer);
  });
  window.removeEventListener('resize', handleResize);
});
</script>
<style scoped>
.title {
  line-height: 70vh;
  text-align: center;
  font-size: 28px;
  color: orange;
.wms-dashboard {
  padding: 24px;
  background: linear-gradient(135deg, #f0f2f5 0%, #e6e9f0 100%);
  min-height: 100vh;
  box-sizing: border-box;
  font-family: "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.stats-card-row, .chart-row, .table-row {
  margin-bottom: 20px;
}
.stats-card, .chart-card, .table-card {
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
  border: 1px solid #ebeef5;
}
.stats-card {
  height: 140px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding: 20px 15px;
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  position: relative;
  overflow: hidden;
  background: linear-gradient(145deg, #ffffff 0%, #f9fafc 100%);
}
.stats-card:hover {
  transform: translateY(-6px) scale(1.02);
  box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
  border-color: #409eff;
  background: linear-gradient(145deg, #ffffff 0%, #f0f2f5 100%);
}
.metric-icon {
  width: 56px;
  height: 56px;
  border-radius: 16px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  margin-bottom: 12px;
  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
  transition: all 0.3s ease;
}
.card-title {
  font-size: 15px;
  color: #606266;
  margin-bottom: 10px;
  font-weight: 500;
  letter-spacing: 0.5px;
}
.card-value {
  font-size: 32px;
  font-weight: 700;
  margin: 8px 0 4px;
  color: #2c3e50;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
  line-height: 1.2;
  background: linear-gradient(to right, #409eff, #36cfc9);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}
.chart-card {
  height: 400px;
  padding: 24px;
  display: flex;
  flex-direction: column;
  background: linear-gradient(180deg, #ffffff 0%, #f8f9fa 100%);
  border: none;
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.06);
  border-radius: 12px;
  transition: all 0.3s ease;
  overflow: hidden;
  position: relative;
}
.chart-card:hover {
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
  transform: translateY(-2px);
}
.chart-container {
  width: 100%;
  height: 100%;
  min-height: 300px;
  flex: 1;
  position: relative;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.7);
  backdrop-filter: blur(5px);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.03);
}
.expiration-table-container {
  width: 100%;
  height: 100%;
  min-height: 300px;
  flex: 1;
  overflow-y: auto;
}
.expire-level {
  padding: 2px 8px;
  border-radius: 4px;
  font-size: 12px;
  font-weight: 500;
}
.expire-level.expired { background-color: #fff2f0; color: #ff4d4f; border: 1px solid #ffccc7; }
.expire-level.urgent { background-color: #fff7e6; color: #fa8c16; border: 1px solid #ffd591; }
.expire-level.warning { background-color: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; }
.expire-level.normal { background-color: #e6f7ff; color: #1890ff; border: 1px solid #91d5ff; }
.expire-level.low { background-color: #f0f2f5; color: #666666; border: 1px solid #d9d9d9; }
.expire-level.default { background-color: #fafafa; color: #8c8c8c; border: 1px solid #e8e8e8; }
.text-red { color: #ff4d4f; font-weight: 500; }
.chart-title, .table-title {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 24px;
  font-size: 18px;
  font-weight: 600;
  color: #2c3e50;
  padding-left: 12px;
  border-left: 4px solid #409eff;
  position: relative;
  letter-spacing: 0.5px;
}
.table-card {
  padding: 24px;
  background: #fff;
  border-radius: 12px;
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.06);
  border: 1px solid rgba(0, 0, 0, 0.04);
  overflow: hidden;
  transition: all 0.3s ease;
}
.table-card:hover {
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
  transform: translateY(-2px);
}
.table-pagination {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  margin-top: 20px;
  padding-top: 15px;
  border-top: 1px solid #ebeef5;
}
:deep(.el-table) {
  border-radius: 6px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
:deep(.el-table th) { background-color: #f5f7fa; color: #606266; font-weight: 600; padding: 12px 0; }
:deep(.el-table td) { padding: 12px 0; }
:deep(.el-table--border) { border-radius: 6px; }
:deep(.el-table--border::after), :deep(.el-table--group::after), :deep(.el-table::before), :deep(.el-table__fixed-right::before), :deep(.el-table__fixed::before) { display: none; }
:deep(.el-pagination .btn-prev), :deep(.el-pagination .btn-next), :deep(.el-pagination .el-pager li) {
  border-radius: 4px;
  margin: 0 2px;
  transition: all 0.3s;
}
:deep(.el-pagination .btn-prev:hover), :deep(.el-pagination .btn-next:hover), :deep(.el-pagination .el-pager li:hover) {
  transform: translateY(-2px);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
:deep(.el-pagination .el-pager li.active) {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: #fff;
}
.task-status, .task-type {
  display: inline-block;
  padding: 6px 12px;
  border-radius: 20px;
  font-size: 13px;
  font-weight: 500;
  text-align: center;
  min-width: 80px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  transition: all 0.2s ease;
  letter-spacing: 0.5px;
}
.status-pending { background-color: rgba(64, 158, 255, 0.1); color: #409eff; border: 1px solid rgba(64, 158, 255, 0.2); }
.status-processing { background-color: rgba(103, 194, 58, 0.1); color: #67c23a; border: 1px solid rgba(103, 194, 58, 0.2); }
.status-completed { background-color: rgba(103, 194, 58, 0.1); color: #67c23a; border: 1px solid rgba(103, 194, 58, 0.2); }
.status-suspended { background-color: rgba(230, 162, 60, 0.1); color: #e6a23c; border: 1px solid rgba(230, 162, 60, 0.2); }
.status-canceled { background-color: rgba(144, 147, 153, 0.1); color: #909399; border: 1px solid rgba(144, 147, 153, 0.2); }
.status-error { background-color: rgba(245, 108, 108, 0.1); color: #f56c6c; border: 1px solid rgba(245, 108, 108, 0.2); }
.status-unknown { background-color: rgba(144, 147, 153, 0.1); color: #909399; border: 1px solid rgba(144, 147, 153, 0.2); }
.type-inbound { background-color: rgba(64, 158, 255, 0.1); color: #409eff; border: 1px solid rgba(64, 158, 255, 0.2); }
.type-outbound { background-color: rgba(103, 194, 58, 0.1); color: #67c23a; border: 1px solid rgba(103, 194, 58, 0.2); }
.type-transfer { background-color: rgba(230, 162, 60, 0.1); color: #e6a23c; border: 1px solid rgba(230, 162, 60, 0.2); }
.type-other { background-color: rgba(144, 147, 153, 0.1); color: #909399; border: 1px solid rgba(144, 147, 153, 0.2); }
.type-unknown { background-color: rgba(144, 147, 153, 0.1); color: #909399; border: 1px solid rgba(144, 147, 153, 0.2); }
</style>