heshaofeng
2026-01-13 64840e4e6d97d177b8a765ba8c53888abcf86d16
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/Home.vue
@@ -1,24 +1,1236 @@
<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">{{ formatNumber(bigscreendata.totalStockQuantity) }}</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="barcode" label="条码" align="center" /> -->
              <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="chart-row">
    </el-row>
    <!-- è¡¨æ ¼åŒºåŸŸ - ç»‘定后端作业数据 -->
    <el-row :gutter="20" class="table-row" width="100%">
      <el-col :span="24">
        <div class="table-card">
          <div class="table-title">实时作业监控</div>
          <el-table :data="showTaskList" border style="width: 100%;">
            <el-table-column prop="taskNum" label="任务号" />
            <el-table-column prop="taskStatus" label="任务状态" >
              <template #default="{ row }">
                <span class="task-status" :class="getStatusClass(row.taskStatus)">{{ getTaskStatusText(row.taskStatus) }}</span>
              </template>
            </el-table-column>
            <el-table-column prop="taskType" label="任务类型" >
              <template #default="{ row }">
                <span class="task-type" :class="getTypeClass(row.taskType)">{{ getTaskTypeText(row.taskType) }}</span>
              </template>
            </el-table-column>
            <el-table-column prop="palletCode" label="托盘编号" />
            <el-table-column prop="sourceAddress" label="起点位置"/>
            <el-table-column prop="targetAddress" label="终点位置"/>
            <el-table-column prop="createDate" label="创建时间"/>
          </el-table>
          <div class="table-pagination">
            <el-pagination layout="prev, pager, next, jumper" :current-page="1" :total="50" />
          </div>
        </div>
      </el-col>
    </el-row>
  </div>
</template>
<script>
import { ref, reactive } from 'vue'
<script setup>
import { ref, onMounted, onUnmounted, nextTick, computed } from 'vue';
import * as echarts from 'echarts';
import http from "@/api/http.js";
// è¡¥å……缺失的图标导入
import { Box, Document, Download, Upload } from '@element-plus/icons-vue';
// è¡¥å……ElMessage导入
import { ElMessage } from 'element-plus';
export default {
  setup() {
    return {
// å“åº”式数据
const month = ref('month');
const orderType = ref('return');
// åŽç«¯è¿”回数据(响应式)
const bigscreendata = ref({
  totalStockQuantity: 0,
  unOutBoundOrderCount: 0,
  dailyCompletionRate: 0,
  unhandledExceptionCount: 0,
  locationUtilizationRate: 0,
  inStockPallet: 0,
  freeStockPallet: 0,
  dailyInOutBoundList: [],
  taskList: [],
  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 || 'PCS'
    };
  });
});
// ç©ºæ•°æ®æç¤ºæ–‡æœ¬ï¼ˆä¿®æ­£åˆ¤æ–­é€»è¾‘)
const emptyText = computed(() => {
  const expirationList = bigscreendata.value.nearExpirationList || [];
  // æ•°ç»„长度为0时显示空提示,否则不显示(原逻辑写反了)
  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 taskStatusMap = {
  100: "新建",
  105: "已发送",
  200: "堆垛机待执行",
  210: "堆垛机执行中",
  220: "堆垛机完成",
  400: "输送线待执行",
  410: "输送线执行中",
  420: "输送线完成",
  300: "AGV待执行",
  310: "AGV执行中",
  315: "AGV取货中",
  320: "AGV待继续执行",
  325: "AGV放货中",
  330: "AGV完成",
  900: "任务完成",
  970: "任务挂起",
  980: "任务取消",
  990: "任务异常",
  110: "提升机执行中"
};
// èŽ·å–ä»»åŠ¡çŠ¶æ€æ–‡æœ¬
const getTaskStatusText = (statusNum) => {
  if (statusNum === undefined || statusNum === null || isNaN(statusNum)) {
    return "未知状态";
  }
  return taskStatusMap[statusNum] || "未知状态";
};
// è¡¨æ ¼æ˜¾ç¤ºçš„任务列表(轮播用)
const showTaskList = ref([]);
const currentTaskIndex = ref(5);
let taskCarouselTimer = null;
// æ•°å­—格式化(千分位分隔)
const formatNumber = (num) => {
  if (!num) return '0';
  return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};
// ä»»åŠ¡åˆ—è¡¨è½®æ’­é€»è¾‘
const startTaskCarousel = () => {
  if (taskCarouselTimer) clearInterval(taskCarouselTimer);
  const totalTask = bigscreendata.value.taskList.length;
  if (totalTask <= 5) {
    showTaskList.value = [...bigscreendata.value.taskList];
    return;
  }
  taskCarouselTimer = setInterval(() => {
    showTaskList.value.push(bigscreendata.value.taskList[currentTaskIndex.value]);
    showTaskList.value.shift();
    currentTaskIndex.value++;
    if (currentTaskIndex.value >= totalTask) {
      currentTaskIndex.value = 0;
    }
  }, 5000);
};
// èŽ·å–ä»»åŠ¡ç±»åž‹æ–‡æœ¬
const getTaskTypeText = (taskTypeNum) => {
  if (!taskTypeNum || isNaN(taskTypeNum)) return "未知类型";
  if (taskTypeNum >= 500 && taskTypeNum < 900) return "入库";
  if (taskTypeNum >= 100 && taskTypeNum < 500) return "出库";
  if (taskTypeNum >= 900 && taskTypeNum < 1000) return "移库";
  return "其他作业";
};
// èŽ·å–ä»»åŠ¡çŠ¶æ€æ ·å¼ç±»
const getStatusClass = (statusNum) => {
  if (statusNum === undefined || statusNum === null || isNaN(statusNum)) {
    return "status-unknown";
  }
  if (statusNum >= 900) return "status-completed";
  if (statusNum >= 400) return "status-processing";
  if (statusNum >= 300) return "status-processing";
  if (statusNum >= 200) return "status-processing";
  if (statusNum >= 100) return "status-pending";
  if (statusNum === 970) return "status-suspended";
  if (statusNum === 980) return "status-canceled";
  if (statusNum === 990) return "status-error";
  return "status-unknown";
};
// èŽ·å–ä»»åŠ¡ç±»åž‹æ ·å¼ç±»
const getTypeClass = (taskTypeNum) => {
  if (!taskTypeNum || isNaN(taskTypeNum)) return "type-unknown";
  if (taskTypeNum >= 500 && taskTypeNum < 900) return "type-inbound";
  if (taskTypeNum >= 100 && taskTypeNum < 500) return "type-outbound";
  if (taskTypeNum >= 900 && taskTypeNum < 1000) return "type-transfer";
  return "type-other";
};
// èŽ·å–åŽç«¯å¤§å±æ•°æ®
const fetchBigGreenData = async () => {
  try {
    const res = await http.get('/api/BigScreen/GetBigGreenData');
    console.log('大屏数据', res);
    bigscreendata.value = res.data || res;
    // æ•°æ®æ›´æ–°åŽåˆ·æ–°å›¾è¡¨å’Œè¡¨æ ¼
    nextTick(() => {
      const total = bigscreendata.value.taskList.length;
      showTaskList.value = total >=5
        ? [...bigscreendata.value.taskList.slice(0,5)]
        : [...bigscreendata.value.taskList];
      startTaskCarousel();
      refreshCharts();
    });
  } catch (error) {
    ElMessage.error('数据获取失败,请检查后端接口是否正常');
    console.error('数据获取失败:', error);
  }
};
// å¤‡ç”¨æ¨¡æ‹Ÿæ•°æ®
const operationList = ref([
  { opNo: 'JW251224001', opType: '入库', operator: '张三', startTime: '15:30:22', status: '处理中' },
  { opNo: 'CK251224002', opType: '出库', operator: '李四', startTime: '15:25:10', status: '已完成' },
  { opNo: 'PD251224003', opType: '盘点', operator: '王五', startTime: '15:20:05', status: '待确认' },
  { opNo: 'SC251224005', opType: '上架', operator: '孙七', startTime: '15:10:18', status: '异常' }
]);
// å›¾è¡¨å®¹å™¨Ref
const inventoryPieRef = ref(null);
const stockTrendRef = ref(null);
const locationRateRef = ref(null);
const exceptionTrendRef = ref(null);
// å›¾è¡¨å®žä¾‹ï¼ˆå…¨å±€ç®¡ç†ï¼‰
let inventoryPieChart = null;
let stockTrendChart = null;
let locationRateChart = null;
let exceptionTrendChart = null;
// åˆå§‹åŒ–库存库位分布饼图
const initInventoryPie= () => {
  if (!inventoryPieRef.value) return;
  if (inventoryPieChart) {
    inventoryPieChart.dispose();
  }
  inventoryPieChart = echarts.init(inventoryPieRef.value);
  const locationData = bigscreendata.value.inventoryLocationDist.length
    ? bigscreendata.value.inventoryLocationDist
    : [
        { value: 48.7, name: '常温区A区', itemStyle: { color: '#5470c6' } },
        { value: 29.2, name: '冷藏区B区', itemStyle: { color: '#91cc75' } },
        { value: 21.9, name: '保税区C区', itemStyle: { color: '#fac858' } },
        { value: 2.2, name: '残次品区D区', itemStyle: { color: '#ee6666' } }
      ];
  const option = {
    tooltip: {
      trigger: 'item',
      formatter: '{a} <br/>{b}: {c}%'
    },
    legend: {
      bottom: 0,
      left: 'center',
      data: locationData.map(item => item.name)
    },
    series: [{
      name: '库存库位分布',
      type: 'pie',
      radius: ['40%', '70%'],
      center: ['50%', '40%'],
      avoidLabelOverlap: false,
      itemStyle: {
        borderRadius: 10,
        borderColor: '#fff',
        borderWidth: 2
      },
      label: {
        show: false,
        position: 'center'
      },
      emphasis: {
        label: {
          show: true,
          fontSize: 20,
          fontWeight: 'bold'
        }
      },
      labelLine: {
        show: false
      },
      data: locationData
    }]
  };
  inventoryPieChart.setOption(option);
  return inventoryPieChart;
};
// åˆå§‹åŒ–è¿‘7日出入库趋势图
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 initExceptionTrend = () => {
  if (!exceptionTrendRef.value) return;
  if (exceptionTrendChart) {
    exceptionTrendChart.dispose();
  }
  exceptionTrendChart = echarts.init(exceptionTrendRef.value);
  const exceptionData = bigscreendata.value.exceptionTypeTrend.length
    ? bigscreendata.value.exceptionTypeTrend
    : {
        dates: ['12-18', '12-19', '12-20', '12-21', '12-22', '12-23', '12-24'],
        stockShort: [10, 11, 9, 12, 10, 13, 12],
        orderTimeout: [8, 9, 7, 8, 7, 9, 8],
        locationException: [4, 5, 2, 4, 3, 5, 4],
        checkDiff: [2, 3, 1, 2, 1, 3, 2]
      };
  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: false,
      data: exceptionData.dates
    },
    yAxis: {
      type: 'value',
      name: '异常数量'
    },
    series: [
      {
        name: '库存不足',
        type: 'line',
        smooth: true,
        symbol: 'circle',
        symbolSize: 8,
        data: exceptionData.stockShort,
        itemStyle: { color: '#ff4d4f' },
        lineStyle: { width: 3 }
      },
      {
        name: '订单超时',
        type: 'line',
        smooth: true,
        symbol: 'circle',
        symbolSize: 8,
        data: exceptionData.orderTimeout,
        itemStyle: { color: '#faad14' },
        lineStyle: { width: 3 }
      },
      {
        name: '库位异常',
        type: 'line',
        smooth: true,
        symbol: 'circle',
        symbolSize: 8,
        data: exceptionData.locationException,
        itemStyle: { color: '#722ed1' },
        lineStyle: { width: 3 }
      },
      {
        name: '盘点差异',
        type: 'line',
        smooth: true,
        symbol: 'circle',
        symbolSize: 8,
        data: exceptionData.checkDiff,
        itemStyle: { color: '#13c2c2' },
        lineStyle: { width: 3 }
      }
    ]
  };
  exceptionTrendChart.setOption(option);
  return exceptionTrendChart;
};
// åˆ·æ–°æ‰€æœ‰å›¾è¡¨
const refreshCharts = () => {
  const charts = [
    initStockTrend,
    initLocationRate
  ];
  charts.forEach(initFunc => {
    const chart = initFunc();
    if (chart) {
      chart.resize();
    }
  });
};
// çª—口大小变化监听
const handleResize = () => {
  const charts = [
    inventoryPieChart,
    stockTrendChart,
    locationRateChart,
    exceptionTrendChart
  ];
  charts.forEach(chart => {
    if (chart) {
      chart.resize();
    }
  });
};
// ç»„件挂载时
onMounted(() => {
  // å…ˆèŽ·å–åŽç«¯æ•°æ®
  fetchBigGreenData();
  // åˆå§‹åŒ–图表(确保DOM已渲染)
  nextTick(() => {
    initInventoryPie();
    initStockTrend();
    initLocationRate();
    initExceptionTrend();
    window.addEventListener('resize', handleResize);
  });
});
// ç»„件卸载时
onUnmounted(() => {
  const charts = [
    inventoryPieChart,
    stockTrendChart,
    locationRateChart,
    exceptionTrendChart
  ];
  charts.forEach(chart => {
    if (chart) {
      chart.dispose();
    }
  });
  clearInterval(taskCarouselTimer);
  window.removeEventListener('resize', handleResize);
});
</script>
<style scoped>
.title {
  line-height: 70vh;
.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;
}
.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
  background: white;
  padding: 15px 20px;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.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;
}
.card-change {
  margin-top: 3px;
}
.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;
}
.view-btn {
  font-size: 12px;
}
.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) {
  display: none;
}
:deep(.el-table--group::after) {
  display: none;
}
:deep(.el-table::before) {
  display: none;
}
:deep(.el-table__fixed-right::before) {
  display: none;
}
:deep(.el-table__fixed::before) {
  display: none;
}
/* åˆ†é¡µå™¨æ ·å¼ä¼˜åŒ– */
:deep(.el-pagination) {
  margin-top: 10px;
}
: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 {
  display: inline-block;
  padding: 6px 12px;
  border-radius: 20px;
  font-size: 13px;
  font-weight: 500;
  text-align: center;
  font-size: 28px;
  color: orange;
  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);
}
/* ä»»åŠ¡ç±»åž‹æ ·å¼ */
.task-type {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 5px 12px;
  border-radius: 16px;
  font-size: 13px;
  font-weight: 600;
  text-align: center;
  min-width: 70px;
  position: relative;
  overflow: hidden;
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  letter-spacing: 0.5px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
@keyframes pulse {
  0% {
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  }
  50% {
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
  }
  100% {
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  }
}
.task-type::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
  transform: translateX(-100%);
  transition: transform 0.8s cubic-bezier(0.19, 1, 0.22, 1);
}
.task-type:hover::before {
  transform: translateX(0);
}
.task-type:hover {
  transform: translateY(-3px);
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
}
.task-type::after {
  content: "";
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 3px;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.7), transparent);
  transform: scaleX(0);
  transform-origin: center;
  transition: transform 0.5s cubic-bezier(0.19, 1, 0.22, 1);
}
.task-type:hover::after {
  transform: scaleX(1);
}
.type-inbound:hover {
  box-shadow: 0 5px 15px rgba(19, 194, 194, 0.4);
}
.type-outbound:hover {
  box-shadow: 0 5px 15px rgba(47, 84, 235, 0.4);
}
.type-transfer:hover {
  box-shadow: 0 5px 15px rgba(250, 140, 22, 0.4);
}
.type-other:hover {
  box-shadow: 0 5px 15px rgba(114, 46, 209, 0.4);
}
.type-unknown:hover {
  box-shadow: 0 5px 15px rgba(89, 89, 89, 0.4);
}
.type-inbound {
  background: linear-gradient(135deg, #13c2c2 0%, #36cfc9 100%);
  color: white;
  box-shadow: 0 3px 5px rgba(19, 194, 194, 0.3);
  animation: pulse 2s infinite;
}
.type-outbound {
  background: linear-gradient(135deg, #2f54eb 0%, #597ef7 100%);
  color: white;
  box-shadow: 0 3px 5px rgba(47, 84, 235, 0.3);
  animation: pulse 2.5s infinite;
}
.type-transfer {
  background: linear-gradient(135deg, #fa8c16 0%, #ffc53d 100%);
  color: white;
  box-shadow: 0 3px 5px rgba(250, 140, 22, 0.3);
  animation: pulse 3s infinite;
}
.type-other {
  background: linear-gradient(135deg, #722ed1 0%, #b37feb 100%);
  color: white;
  box-shadow: 0 3px 5px rgba(114, 46, 209, 0.3);
  animation: pulse 2.2s infinite;
}
.type-unknown {
  background: linear-gradient(135deg, #595959 0%, #8c8c8c 100%);
  color: white;
  box-shadow: 0 3px 5px rgba(89, 89, 89, 0.3);
  animation: pulse 2.8s infinite;
}
.btn-group {
  margin-left: 10px;
}
</style>