1
heshaofeng
2026-01-20 5dfd83bd540c2e43af2e0449c246c79a22cb1296
ÏîÄ¿´úÂë/Dashboard/src/views/Dashboard.vue
@@ -36,11 +36,13 @@
              <Box v-if="index === 0" />
              <Download v-else-if="index === 1" />
              <Upload v-else-if="index === 2" />
              <List v-else />
              <List v-else-if="index === 3" />
              <Box v-else-if="index === 4" />
              <Box v-else />
            </el-icon>
          </div>
          <div class="metric-content">
            <div class="metric-value">{{ item.value }}</div>
            <div class="metric-value">{{ formatNumber(item.value) }}</div>
            <div class="metric-label">{{ item.label }}</div>
            <div class="metric-trend" :class="item.trend > 0 ? 'up' : 'down'">
              <el-icon>
@@ -71,12 +73,32 @@
          <div ref="categoryChartRef" class="chart-container"></div>
        </div>
        <!-- ä½œä¸šæ•ˆçއ -->
        <!-- ç‰©æ–™ä¸´æœŸæ•°æ® -->
        <div class="chart-card">
          <div class="card-title">
            <span>作业效率统计</span>
            <span>物料临期数据</span>
            <span class="task-count">共 {{ nearExpirationList.length }} æ¡è®°å½•</span>
          </div>
          <div ref="efficiencyChartRef" class="chart-container"></div>
          <el-table
            :data="showNearExpirationList"
            style="width: 100%"
            :height="250"
            :empty-text="nearExpirationList.length === 0 ? '暂无临期数据' : ''"
          >
            <el-table-column prop="materielCode" label="物料编码" min-width="120" />
            <el-table-column prop="batchNo" label="批次号" min-width="100" />
            <el-table-column prop="palletCode" label="托盘编号" min-width="100" />
            <el-table-column prop="locationCode" label="库位" min-width="100" />
            <el-table-column prop="stockQuantity" label="库存数量" width="100" />
            <el-table-column prop="daysToExpiration" label="临期天数" width="100">
              <template #default="{ row }">
                <div :class="getExpirationClass(row.daysToExpiration)">
                  {{ row.daysToExpiration }}天
                </div>
              </template>
            </el-table-column>
            <el-table-column prop="validDate" label="有效期至" width="160" />
          </el-table>
        </div>
      </div>
@@ -85,28 +107,40 @@
        <div class="table-card">
          <div class="card-title">
            <span>实时作业任务</span>
            <el-button type="primary" size="small" @click="refreshData">
              <el-icon><Refresh /></el-icon>
              åˆ·æ–°
            </el-button>
            <span class="task-count">共 {{ taskList.length }} æ¡ä»»åŠ¡</span>
          </div>
          <el-table :data="taskList" style="width: 100%" height="200">
            <el-table-column prop="taskNo" label="任务编号" width="150" />
            <el-table-column prop="type" label="任务类型" width="100">
          <el-table
            :data="showTaskList"
            style="width: 100%"
            :height="tableHeight"
            :empty-text="taskList.length === 0 ? '暂无作业任务数据' : ''"
          >
            <el-table-column prop="taskNum" label="任务号" min-width="120" />
            <el-table-column prop="taskStatus" label="任务状态" width="140">
              <template #default="{ row }">
                <el-tag :type="row.type === '入库' ? 'success' : 'warning'">{{ row.type }}</el-tag>
                <div class="status-container" :class="getStatusClass(row.taskStatus)">
                  <div class="status-dot"></div>
                  <span class="status-text">{{ getTaskStatusText(row.taskStatus) }}</span>
                </div>
              </template>
            </el-table-column>
            <el-table-column prop="material" label="物料名称" />
            <el-table-column prop="quantity" label="数量" width="100" />
            <el-table-column prop="location" label="库位" width="120" />
            <el-table-column prop="status" label="状态" width="100">
            <el-table-column prop="taskType" label="任务类型" width="120">
              <template #default="{ row }">
                <el-tag :type="getStatusType(row.status)">{{ row.status }}</el-tag>
                <div class="type-container" :class="getTypeClass(row.taskType)">
                  <el-icon class="type-icon">
                    <Box v-if="getTypeClass(row.taskType) === 'type-inbound'" />
                    <Upload v-else-if="getTypeClass(row.taskType) === 'type-outbound'" />
                    <Refresh v-else-if="getTypeClass(row.taskType) === 'type-transfer'" />
                    <Operation v-else />
                  </el-icon>
                  <span class="type-text">{{ getTaskTypeText(row.taskType) }}</span>
                </div>
              </template>
            </el-table-column>
            <el-table-column prop="operator" label="操作员" width="100" />
            <el-table-column prop="time" label="时间" width="160" />
            <el-table-column prop="palletCode" label="托盘编号" min-width="100" />
            <el-table-column prop="sourceAddress" label="起点位置" min-width="100"/>
            <el-table-column prop="targetAddress" label="终点位置" min-width="100"/>
            <el-table-column prop="createDate" label="创建时间" width="180"/>
          </el-table>
        </div>
      </div>
@@ -115,27 +149,334 @@
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue'
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
import * as echarts from 'echarts'
import { http } from '@/utils/http'
import { formatDateTime } from '@/utils'
import { ElMessage } from 'element-plus'
// å¯¼å…¥Element Plus图标
import {
  DataBoard, Box, Operation, Warning, Download, Upload, List,
  Top, Bottom, Refresh, ArrowRight, Clock
} from '@element-plus/icons-vue'
export default {
  name: 'Dashboard',
  components: {
    DataBoard, Box, Operation, Warning, Download, Upload, List,
    Top, Bottom, Refresh, ArrowRight, Clock
  },
  setup() {
    const currentTime = ref('')
    const trendChartRef = ref(null)
    const categoryChartRef = ref(null)
    const efficiencyChartRef = ref(null)
    const tableHeight = ref(200)
    // å›¾è¡¨å®žä¾‹å¼•用
    const trendChart = ref(null)
    const categoryChart = ref(null)
    const efficiencyChart = ref(null)
    // åŽç«¯è¿”回数据(响应式)
    const bigscreendata = ref({
      totalStockQuantity: 0,
      unOutBoundOrderCount: 0,
      dailyCompletionRate: 0,
      unhandledExceptionCount: 0,
      locationUtilizationRate: 0,
      inStockPallet: 0,
      freeStockPallet: 0,
      dailyInOutBoundList: [],
      taskList: [],
      inboundCount: 0,
      outboundCount: 0,
      inventoryLocationDist: [],
      completeTask: []
    })
    // ä»»åŠ¡çŠ¶æ€æ˜ å°„
    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 metrics = ref([
      { label: '总库存量', value: '12,580', icon: 'Box', color: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', trend: 8.5 },
      { label: '今日入库', value: '1,280', icon: 'Download', color: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', trend: 12.3 },
      { label: '今日出库', value: '965', icon: 'Upload', color: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', trend: -3.2 },
      { label: '待处理任务', value: '48', icon: 'List', color: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)', trend: 5.7 }
      {
        label: '待入库订单',
        value: 0,
        icon: 'Box',
        color: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
        trend: 8.5
      },
      {
        label: '待出库订单',
        value: 0,
        icon: 'Download',
        color: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
        trend: 12.3
      },
      {
        label: '今日入库完成数',
        value: 0,
        icon: 'Upload',
        color: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
        trend: -3.2
      },
      {
        label: '今日出库完成数',
        value: 0,
        icon: 'List',
        color: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
        trend: 5.7
      },
      {
        label: '有货料箱',
        value: 0,
        icon: 'List',
        color: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)',
        trend: 2.1
      },
      {
        label: '空箱数量',
        value: 0,
        icon: 'List',
        color: 'linear-gradient(135deg, #8176af 0%, #c0b7e8 100%)',
        trend: -1.8
      }
    ])
    // ä»»åŠ¡åˆ—è¡¨ç›¸å…³
    const taskList = ref([])
    const showTaskList = ref([])
    const currentTaskIndex = ref(7) // åˆå§‹ä»Žç¬¬8条开始(前7条默认显示)
    let taskCarouselTimer = null
    // ä¸´æœŸç‰©æ–™åˆ—表相关
    const nearExpirationList = ref([])
    const showNearExpirationList = ref([])
    const currentExpirationIndex = ref(7) // åˆå§‹ä»Žç¬¬8条开始(前7条默认显示)
    let expirationCarouselTimer = null
    // è‡ªåŠ¨åˆ·æ–°ç›¸å…³é…ç½®
    const lastInboundToday = ref(0)  // ä¸Šä¸€æ¬¡å½“天入库量
    const lastOutboundToday = ref(0) // ä¸Šä¸€æ¬¡å½“天出库量
    const refreshInterval = ref(5 * 60 * 1000) // å®šæ—¶åˆ·æ–°é—´éš”(5分钟)
    const minRefreshGap = ref(30 * 1000) // æœ€å°åˆ·æ–°é—´éš”(防抖,30秒)
    let lastRefreshTime = ref(0) // ä¸Šä¸€æ¬¡åˆ·æ–°æ—¶é—´
    let autoRefreshTimer = null // è‡ªåŠ¨åˆ·æ–°å®šæ—¶å™¨
    // æ•°å­—格式化
    const formatNumber = (num) => {
      if (num === undefined || num === null || isNaN(num)) return '0';
      return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
    }
    // èŽ·å–ä»»åŠ¡çŠ¶æ€æ–‡æœ¬
    const getTaskStatusText = (statusNum) => {
      if (statusNum === undefined || statusNum === null || isNaN(statusNum)) {
        return "未知状态";
      }
      return taskStatusMap[statusNum] || `未知状态(${statusNum})`;
    }
    // å¯åŠ¨ä»»åŠ¡è½®æ’­
    const startTaskCarousel = () => {
      // æ¸…除旧定时器
      if (taskCarouselTimer) clearInterval(taskCarouselTimer);
      const totalTask = taskList.value.length;
      // ä»»åŠ¡æ•°<=7时不轮播
      if (totalTask <= 7) {
        showTaskList.value = [...taskList.value];
        return;
      }
      // é‡ç½®ç´¢å¼•
      currentTaskIndex.value = 7;
      // åˆå§‹æ˜¾ç¤ºå‰7条
      showTaskList.value = taskList.value.slice(0, 7);
      // å¯åŠ¨è½®æ’­
      taskCarouselTimer = setInterval(() => {
        const tableElement = document.querySelector('.el-table');
        if (tableElement) {
          tableElement.classList.add('flash-effect');
          setTimeout(() => {
            tableElement.classList.remove('flash-effect');
          }, 600);
        }
        // æ–°å¢žä¸‹1条,删除最前1条(保持7条显示)
        showTaskList.value.push(taskList.value[currentTaskIndex.value]);
        showTaskList.value.shift();
        // å¾ªçŽ¯ç´¢å¼•
        currentTaskIndex.value++;
        if (currentTaskIndex.value >= totalTask) {
          currentTaskIndex.value = 0;
        }
      }, 5000); // 5秒轮播一次
    }
    // åœæ­¢ä»»åŠ¡è½®æ’­
    const stopTaskCarousel = () => {
      if (taskCarouselTimer) {
        clearInterval(taskCarouselTimer);
        taskCarouselTimer = null;
      }
    }
    // å¯åŠ¨ä¸´æœŸç‰©æ–™è½®æ’­
    const startExpirationCarousel = () => {
      // æ¸…除旧定时器
      if (expirationCarouselTimer) clearInterval(expirationCarouselTimer);
      const totalExpiration = nearExpirationList.value.length;
      // è®°å½•æ•°<=7时不轮播
      if (totalExpiration <= 7) {
        showNearExpirationList.value = [...nearExpirationList.value];
        return;
      }
      // é‡ç½®ç´¢å¼•
      currentExpirationIndex.value = 7;
      // åˆå§‹æ˜¾ç¤ºå‰7条
      showNearExpirationList.value = nearExpirationList.value.slice(0, 7);
      // å¯åŠ¨è½®æ’­
      expirationCarouselTimer = setInterval(() => {
        // æ–°å¢žä¸‹1条,删除最前1条(保持7条显示)
        showNearExpirationList.value.push(nearExpirationList.value[currentExpirationIndex.value]);
        showNearExpirationList.value.shift();
        // å¾ªçŽ¯ç´¢å¼•
        currentExpirationIndex.value++;
        if (currentExpirationIndex.value >= totalExpiration) {
          currentExpirationIndex.value = 0;
        }
      }, 5000); // 5秒轮播一次
    }
    // åœæ­¢ä¸´æœŸç‰©æ–™è½®æ’­
    const stopExpirationCarousel = () => {
      if (expirationCarouselTimer) {
        clearInterval(expirationCarouselTimer);
        expirationCarouselTimer = null;
      }
    }
    // èŽ·å–ä¸´æœŸå¤©æ•°æ ·å¼ç±»
    const getExpirationClass = (days) => {
      if (days === undefined || days === null || isNaN(days)) return "exp-unknown";
      if (days <= 0) return "exp-expired"; // å·²è¿‡æœŸ
      if (days <= 7) return "exp-critical"; // 7天内临期
      if (days <= 30) return "exp-warning"; // 30天内临期
      return "exp-normal"; // æ­£å¸¸
    }
    // èŽ·å–ä»»åŠ¡ç±»åž‹æ–‡æœ¬
    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 === 970) return "status-suspended"; // æŒ‚èµ·
      if (statusNum === 980) return "status-canceled"; // å–消
      if (statusNum === 990) return "status-error"; // å¼‚常
      if (statusNum >= 200 && statusNum < 900) return "status-processing"; // æ‰§è¡Œä¸­
      if (statusNum >= 100 && statusNum < 200) return "status-pending"; // æ–°å»ºã€å·²å‘送
      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 now = Date.now();
        if (now - lastRefreshTime.value < minRefreshGap.value) {
          return;
        }
        lastRefreshTime.value = now;
        const res = await http.get('/api/BigScreen/GetBigGreenData');
        console.log('大屏数据', res);
        bigscreendata.value = res.data || res;
        updateMetrics();
        // æ›´æ–°ä»»åŠ¡åˆ—è¡¨
        taskList.value = Array.isArray(bigscreendata.value.taskList) ? bigscreendata.value.taskList : [];
        // é‡å¯è½®æ’­
        stopTaskCarousel();
        startTaskCarousel();
        // æ›´æ–°ä¸´æœŸç‰©æ–™åˆ—表
        nearExpirationList.value = Array.isArray(bigscreendata.value.nearExpirationList) ? bigscreendata.value.nearExpirationList : [];
        // é‡å¯ä¸´æœŸç‰©æ–™è½®æ’­
        stopExpirationCarousel();
        startExpirationCarousel();
        // æ•°æ®åŠ è½½å®ŒæˆåŽåˆ·æ–°å›¾è¡¨
        nextTick(() => {
          refreshCharts(true);
        });
      } catch (error) {
        console.error('获取大屏数据失败:', error);
        ElMessage.error('获取数据失败,请稍后重试');
      }
    };
    // æ›´æ–°å…³é”®æŒ‡æ ‡ - ä¿®æ­£æ•°æ®æ˜ å°„
    const updateMetrics = () => {
      metrics.value[0].value = bigscreendata.value.unInBoundOrderCount || 0; // å¾…入库订单
      metrics.value[1].value = bigscreendata.value.unOutBoundOrderCount || 0; // å¾…出库订单
      metrics.value[2].value = bigscreendata.value.inboundCount || 0; // ä»Šæ—¥å…¥åº“完成数
      metrics.value[3].value = bigscreendata.value.outboundCount || 0; // ä»Šæ—¥å‡ºåº“完成数
      metrics.value[4].value = bigscreendata.value.inStockPallet || 0; // æœ‰è´§æ–™ç®±
      metrics.value[5].value = bigscreendata.value.freeStockPallet || 0; // ç©ºç®±æ•°é‡
    }
    // æ›´æ–°æ—¶é—´
    let timer
@@ -143,84 +484,139 @@
      currentTime.value = formatDateTime(new Date())
    }
    // èŽ·å–çŠ¶æ€ç±»åž‹
    const getStatusType = (status) => {
      const map = {
        '进行中': 'primary',
        '已完成': 'success',
        '待处理': 'info',
        '异常': 'danger'
      }
      return map[status] || 'info'
    }
    // åˆå§‹åŒ–趋势图
    const initTrendChart = () => {
      const chart = echarts.init(trendChartRef.value)
      if (!trendChartRef.value) return;
      // é”€æ¯æ—§å®žä¾‹
      if (trendChart.value) {
        trendChart.value.dispose();
      }
      trendChart.value = echarts.init(trendChartRef.value);
      // ç›´æŽ¥ä»ŽåŽç«¯èŽ·å–æ—¥æœŸå’Œæ•°æ®
      const dates = [];
      const inboundData = [];
      const outboundData = [];
      if (Array.isArray(bigscreendata.value.dailyInOutBoundList) && bigscreendata.value.dailyInOutBoundList.length > 0) {
        bigscreendata.value.dailyInOutBoundList.forEach(item => {
          dates.push(item.date || ''); // ç›´æŽ¥ä½¿ç”¨åŽç«¯è¿”回的日期
          inboundData.push(item.dailyInboundQuantity || 0);
          outboundData.push(item.dailyOutboundQuantity || 0);
        });
      }
      const hasData = dates.length > 0;
      const option = {
        tooltip: { trigger: 'axis' },
        backgroundColor: 'transparent',
        tooltip: {
          trigger: 'axis',
          textStyle: { color: '#fff', fontSize: 12 },
          backgroundColor: 'rgba(0,0,0,0.7)'
        },
        legend: {
          data: ['入库量', '出库量'],
          textStyle: { color: '#fff' }
          textStyle: { color: '#e0e0e0', fontSize: 12 }
        },
        grid: {
          left: '3%', right: '4%', bottom: '3%', top: '15%',
          left: '3%',
          right: '4%',
          bottom: '3%',
          top: '15%',
          containLabel: true
        },
        xAxis: {
          type: 'category',
          boundaryGap: false,
          data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '24:00'],
          axisLine: { lineStyle: { color: '#fff' } }
          data: hasData ? dates : ['暂无数据'],
          axisLine: { lineStyle: { color: '#666' } },
          axisLabel: { color: '#e0e0e0', fontSize: 11 }
        },
        yAxis: {
          type: 'value',
          axisLine: { lineStyle: { color: '#fff' } },
          splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
          axisLine: { lineStyle: { color: '#666' } },
          splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } },
          axisLabel: { color: '#e0e0e0', fontSize: 11 },
          min: 0
        },
        series: [
          {
            name: '入库量',
            type: 'line',
            smooth: true,
            data: [120, 200, 450, 680, 520, 780, 650],
            itemStyle: { color: '#43e97b' },
            data: hasData ? inboundData : [0],
            itemStyle: { color: '#5470c6' },
            lineStyle: { color: '#5470c6' },
            areaStyle: {
              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                { offset: 0, color: 'rgba(67, 233, 123, 0.5)' },
                { offset: 1, color: 'rgba(67, 233, 123, 0)' }
                { offset: 0, color: 'rgba(84, 112, 198, 0.5)' },
                { offset: 1, color: 'rgba(84, 112, 198, 0)' }
              ])
            }
            },
            showSymbol: hasData,
            symbol: 'circle',
            symbolSize: 6
          },
          {
            name: '出库量',
            type: 'line',
            smooth: true,
            data: [80, 150, 380, 520, 420, 650, 580],
            itemStyle: { color: '#4facfe' },
            data: hasData ? outboundData : [0],
            itemStyle: { color: '#91cc75' },
            lineStyle: { color: '#91cc75' },
            areaStyle: {
              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                { offset: 0, color: 'rgba(79, 172, 254, 0.5)' },
                { offset: 1, color: 'rgba(79, 172, 254, 0)' }
                { offset: 0, color: 'rgba(145, 204, 117, 0.5)' },
                { offset: 1, color: 'rgba(145, 204, 117, 0)' }
              ])
            }
            },
            showSymbol: hasData,
            symbol: 'circle',
            symbolSize: 6
          }
        ]
      }
      chart.setOption(option)
      return chart
      };
      trendChart.value.setOption(option);
      return trendChart.value;
    }
    // åˆå§‹åŒ–分类占比图
    const initCategoryChart = () => {
      const chart = echarts.init(categoryChartRef.value)
      if (!categoryChartRef.value) return;
      // é”€æ¯æ—§å®žä¾‹
      if (categoryChart.value) {
        categoryChart.value.dispose();
      }
      categoryChart.value = echarts.init(categoryChartRef.value);
      const chartData = Array.isArray(bigscreendata.value.inventoryLocationDist) && bigscreendata.value.inventoryLocationDist.length > 0
        ? bigscreendata.value.inventoryLocationDist
        : [
            { value: 3580, name: '原材料', itemStyle: { color: '#5470c6' } },
            { value: 2840, name: '半成品', itemStyle: { color: '#91cc75' } },
            { value: 4120, name: '成品', itemStyle: { color: '#fac858' } },
            { value: 2040, name: '辅料', itemStyle: { color: '#ee6666' } }
          ];
      const option = {
        tooltip: { trigger: 'item' },
        backgroundColor: 'transparent',
        tooltip: {
          trigger: 'item',
          textStyle: { color: '#fff', fontSize: 12 },
          backgroundColor: 'rgba(0,0,0,0.7)',
          formatter: '{b}: {c} ({d}%)'
        },
        legend: {
          orient: 'vertical',
          right: '10%',
          top: 'center',
          textStyle: { color: '#fff' }
          textStyle: { color: '#e0e0e0', fontSize: 12 }
        },
        series: [
          {
@@ -236,46 +632,89 @@
            },
            label: { show: false },
            emphasis: {
              label: { show: true, fontSize: 16, fontWeight: 'bold' }
              label: { show: true, fontSize: 14, fontWeight: 'bold', color: '#fff' }
            },
            data: [
              { value: 3580, name: '原材料', itemStyle: { color: '#5470c6' } },
              { value: 2840, name: '半成品', itemStyle: { color: '#91cc75' } },
              { value: 4120, name: '成品', itemStyle: { color: '#fac858' } },
              { value: 2040, name: '辅料', itemStyle: { color: '#ee6666' } }
            ]
            data: chartData
          }
        ]
      }
      chart.setOption(option)
      return chart
      };
      categoryChart.value.setOption(option);
      return categoryChart.value;
    }
    // åˆå§‹åŒ–效率统计图
    const initEfficiencyChart = () => {
      const chart = echarts.init(efficiencyChartRef.value)
      if (!efficiencyChartRef.value) {
        console.warn('效率图表容器不存在!');
        return;
      }
      // ç¡®ä¿å®¹å™¨æœ‰é«˜åº¦
      efficiencyChartRef.value.style.height = '250px';
      efficiencyChartRef.value.style.width = '100%';
      // é”€æ¯æ—§å®žä¾‹
      if (efficiencyChart.value) {
        efficiencyChart.value.dispose();
      }
      // åˆå§‹åŒ– ECharts
      efficiencyChart.value = echarts.init(efficiencyChartRef.value);
      // æ•°æ®å¤„理
      const taskData = {
        '入库': 0,
        '出库': 0
      };
      if (Array.isArray(bigscreendata.value.completeTask)) {
        bigscreendata.value.completeTask.forEach(item => {
          if (item.taskType && typeof item.count === 'number') {
            taskData[item.taskType] = item.count;
          }
        });
      }
      const hasData = taskData.入库 > 0 || taskData.出库 > 0;
      const option = {
        tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
        backgroundColor: 'transparent',
        tooltip: {
          trigger: 'axis',
          axisPointer: { type: 'shadow' },
          textStyle: { color: '#fff', fontSize: 12 },
          backgroundColor: 'rgba(0,0,0,0.7)',
          formatter: params => {
            return params.map(p => `${p.seriesName}: ${p.value || 0} å•`).join('<br/>');
          }
        },
        grid: {
          left: '3%', right: '4%', bottom: '3%', top: '10%',
          left: '5%',
          right: '5%',
          bottom: '10%',
          top: '15%',
          containLabel: true
        },
        xAxis: {
          type: 'category',
          data: ['入库作业', '出库作业', '盘点作业', '调拨作业', '补货作业'],
          axisLine: { lineStyle: { color: '#fff' } },
          axisLabel: { color: '#fff' }
          data: hasData ? ['入库作业', '出库作业'] : ['暂无数据'],
          axisLine: { lineStyle: { color: '#666' } },
          axisLabel: { color: '#e0e0e0', fontSize: 11 }
        },
        yAxis: {
          type: 'value',
          name: '效率(单位/小时)',
          axisLine: { lineStyle: { color: '#fff' } },
          name: '完成数量(单)',
          nameTextStyle: { color: '#e0e0e0', fontSize: 12 },
          axisLine: { lineStyle: { color: '#666' } },
          splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } },
          axisLabel: { color: '#fff' }
          axisLabel: { color: '#e0e0e0', fontSize: 11 },
          min: 0
        },
        series: [
          {
            data: [180, 156, 95, 78, 120],
            name: '作业数量',
            data: hasData ? [taskData['入库'], taskData['出库']] : [0],
            type: 'bar',
            barWidth: '40%',
            itemStyle: {
@@ -284,110 +723,162 @@
                { offset: 1, color: '#188df0' }
              ]),
              borderRadius: [5, 5, 0, 0]
            },
            label: {
              show: hasData,
              position: 'top',
              color: '#ffffff',
              fontSize: 12,
              fontWeight: 600
            }
          }
        ]
      };
      efficiencyChart.value.setOption(option, true);
      console.log('效率图表渲染完成,数据:', taskData);
    };
    // åˆ·æ–°å›¾è¡¨æ•°æ®
    const refreshCharts = (isDataChange = false) => {
      // æ•°æ®å˜åŒ–时添加闪烁效果
      if (isDataChange) {
        const chartElements = [trendChartRef.value, categoryChartRef.value, efficiencyChartRef.value];
        chartElements.forEach(el => {
          if (el) {
            el.classList.add('flash-effect');
            setTimeout(() => el.classList.remove('flash-effect'), 600);
          }
        });
      }
      chart.setOption(option)
      return chart
      // é‡æ–°åˆå§‹åŒ–图表
      initTrendChart();
      initCategoryChart();
      initEfficiencyChart();
    }
    // èŽ·å–ä»»åŠ¡åˆ—è¡¨æ•°æ®
    const fetchTaskList = async () => {
      try {
        // æ¨¡æ‹Ÿæ•°æ®ï¼Œå®žé™…使用时调用接口
        // const res = await http.get('/wms/task/list')
        // æ¨¡æ‹Ÿæ•°æ®
        taskList.value = [
          { taskNo: 'RK20241224001', type: '入库', material: '钢板 A型', quantity: 500, location: 'A-01-01', status: '进行中', operator: '张三', time: '2024-12-24 14:25:30' },
          { taskNo: 'CK20241224002', type: '出库', material: '螺丝 M8', quantity: 2000, location: 'B-03-05', status: '已完成', operator: '李四', time: '2024-12-24 14:20:15' },
          { taskNo: 'RK20241224003', type: '入库', material: '铝板 B型', quantity: 300, location: 'A-02-03', status: '待处理', operator: '王五', time: '2024-12-24 14:15:00' },
          { taskNo: 'CK20241224004', type: '出库', material: '螺母 M8', quantity: 1500, location: 'B-02-01', status: '进行中', operator: '赵六', time: '2024-12-24 14:10:45' },
          { taskNo: 'PD20241224001', type: '盘点', material: '垫片', quantity: 5000, location: 'C-01-01', status: '进行中', operator: '孙七', time: '2024-12-24 14:05:20' }
        ]
      } catch (error) {
        console.error('获取任务列表失败:', error)
      }
    }
    // åˆ·æ–°æ•°æ®
    // æ‰‹åŠ¨åˆ·æ–°æ•°æ®
    const refreshData = () => {
      fetchTaskList()
      fetchBigGreenData();
    }
    // çª—口大小改变时重绘图表
    // å¯åŠ¨è‡ªåŠ¨åˆ·æ–°
    const startAutoRefresh = () => {
      if (autoRefreshTimer) clearInterval(autoRefreshTimer);
      autoRefreshTimer = setInterval(() => {
        console.log('定时刷新数据:', new Date().toLocaleString());
        fetchBigGreenData();
      }, refreshInterval.value);
    }
    // åœæ­¢è‡ªåŠ¨åˆ·æ–°
    const stopAutoRefresh = () => {
      if (autoRefreshTimer) {
        clearInterval(autoRefreshTimer);
        autoRefreshTimer = null;
      }
    }
    // çª—口大小改变时重绘图表和调整表格高度
    const handleResize = () => {
      try {
        const refs = [trendChartRef.value, categoryChartRef.value, efficiencyChartRef.value]
        refs.forEach(dom => {
          if (dom) {
            const chart = echarts.getInstanceByDom(dom)
            if (chart) {
              chart.resize()
            }
          }
        })
        const windowHeight = window.innerHeight;
        const headerHeight = 60;
        const navHeight = 50;
        const metricsHeight = 120;
        const chartsHeight = 300;
        const padding = 80;
        const availableHeight = windowHeight - headerHeight - navHeight - metricsHeight - chartsHeight - padding;
        tableHeight.value = Math.max(200, Math.min(availableHeight, 400));
        // é‡ç»˜å›¾è¡¨
        const charts = [trendChart.value, categoryChart.value, efficiencyChart.value];
        charts.forEach(chart => {
          if (chart) chart.resize();
        });
      } catch (error) {
        console.warn('图表重绘时出错:', error)
        console.warn('图表重绘时出错:', error);
      }
    }
    onMounted(() => {
      updateTime()
      timer = setInterval(updateTime, 1000)
      // åˆå§‹åŒ–æ—¶é—´
      updateTime();
      timer = setInterval(updateTime, 1000);
      initTrendChart()
      initCategoryChart()
      initEfficiencyChart()
      fetchTaskList()
      window.addEventListener('resize', handleResize)
    })
      // åˆå§‹åŒ–数据
      fetchBigGreenData();
      nextTick(() => {
        handleResize();
        startAutoRefresh(); // å¯åŠ¨è‡ªåŠ¨åˆ·æ–°
      });
      // ç›‘听窗口大小变化
      window.addEventListener('resize', handleResize);
    });
    onUnmounted(() => {
      clearInterval(timer)
      window.removeEventListener('resize', handleResize)
      // æ¸…除所有定时器
      clearInterval(timer);
      stopTaskCarousel();
      stopExpirationCarousel();
      stopAutoRefresh();
      // ç§»é™¤çª—口监听
      window.removeEventListener('resize', handleResize);
      // é”€æ¯å›¾è¡¨å®žä¾‹
      try {
        const refs = [trendChartRef.value, categoryChartRef.value, efficiencyChartRef.value]
        refs.forEach(dom => {
          if (dom) {
            const chart = echarts.getInstanceByDom(dom)
            if (chart) {
              chart.dispose()
            }
          }
        })
        if (trendChart.value) trendChart.value.dispose();
        if (categoryChart.value) categoryChart.value.dispose();
        if (efficiencyChart.value) efficiencyChart.value.dispose();
      } catch (error) {
        console.warn('图表销毁时出错:', error)
        console.warn('图表销毁时出错:', error);
      }
    })
    });
    // ç›‘听任务列表变化,自动重启轮播
    watch(taskList, () => {
      stopTaskCarousel();
      startTaskCarousel();
    }, { deep: true });
    return {
      currentTime,
      metrics,
      taskList,
      showTaskList,
      nearExpirationList,
      showNearExpirationList,
      trendChartRef,
      categoryChartRef,
      efficiencyChartRef,
      getStatusType,
      tableHeight,
      formatNumber,
      getTaskTypeText,
      getTaskStatusText,
      getStatusClass,
      getTypeClass,
      getExpirationClass,
      refreshData
    }
    };
  }
}
};
</script>
<style scoped>
.dashboard-container {
  width: 100%;
  height: 100%;
  height: 100vh;
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 15px;
  overflow-y: auto;
  background-color: #0a0e27;
}
.header {
@@ -398,7 +889,7 @@
  height: 60px;
  background: linear-gradient(90deg, rgba(30, 58, 138, 0.5) 0%, rgba(30, 58, 138, 0.1) 100%);
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.15);
}
.title {
@@ -407,11 +898,14 @@
  background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  margin: 0;
}
.datetime {
  font-size: 16px;
  color: #4facfe;
  font-weight: 600;
  font-family: 'Consolas', monospace;
}
.nav-menu {
@@ -426,9 +920,9 @@
  gap: 8px;
  padding: 10px 25px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 8px;
  color: #fff;
  color: #e0e0e0;
  text-decoration: none;
  transition: all 0.3s;
  cursor: pointer;
@@ -437,11 +931,14 @@
.nav-item:hover {
  background: rgba(79, 172, 254, 0.2);
  border-color: #4facfe;
  transform: translateY(-1px);
}
.nav-item.active {
  background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
  border-color: transparent;
  color: #fff;
  box-shadow: 0 4px 12px rgba(79, 172, 254, 0.3);
}
.main-content {
@@ -451,9 +948,10 @@
  gap: 15px;
}
/* å…³é”®æŒ‡æ ‡å¡ç‰‡ - ä¼˜åŒ–网格布局和样式对比度 */
.metrics-row {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 15px;
}
@@ -463,7 +961,7 @@
  gap: 20px;
  padding: 20px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 10px;
  transition: all 0.3s;
}
@@ -471,6 +969,7 @@
.metric-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
  border-color: rgba(79, 172, 254, 0.5);
}
.metric-icon {
@@ -488,22 +987,26 @@
}
.metric-value {
  font-size: 28px;
  font-weight: bold;
  font-size: 32px;
  font-weight: 700;
  color: #ffffff;
  margin-bottom: 5px;
  text-shadow: 0 0 8px rgba(255,255,255,0.2);
}
.metric-label {
  font-size: 14px;
  color: rgba(255, 255, 255, 0.6);
  color: #cccccc;
  margin-bottom: 5px;
  font-weight: 500;
}
.metric-trend {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 12px;
  font-size: 13px;
  font-weight: 600;
}
.metric-trend.up {
@@ -514,17 +1017,20 @@
  color: #f56c6c;
}
/* å›¾è¡¨åŒºåŸŸ - ä¼˜åŒ–布局和样式 */
.charts-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
  gap: 15px;
}
.chart-card {
  padding: 20px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 10px;
  display: flex;
  flex-direction: column;
}
.card-title {
@@ -533,15 +1039,23 @@
  align-items: center;
  margin-bottom: 15px;
  font-size: 16px;
  font-weight: bold;
  font-weight: 600;
  color: #4facfe;
}
.chart-container {
  width: 100%;
  height: 250px;
.task-count {
  font-size: 12px;
  color: #cccccc;
  font-weight: normal;
}
.chart-container {
  flex: 1;
  width: 100%;
  min-height: 250px;
}
/* è¡¨æ ¼åŒºåŸŸ */
.table-row {
  flex: 1;
}
@@ -550,22 +1064,31 @@
  height: 100%;
  padding: 20px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 10px;
  display: flex;
  flex-direction: column;
}
/* Element Plus Table æ ·å¼æ·±åº¦ä¼˜åŒ– */
:deep(.el-table) {
  background: transparent !important;
  color: #e0e0e0;
  font-size: 13px;
}
:deep(.el-table th) {
  background: rgba(255, 255, 255, 0.1) !important;
  color: #fff !important;
  border-color: rgba(255, 255, 255, 0.1) !important;
  background: rgba(255, 255, 255, 0.15) !important;
  color: #ffffff !important;
  border-color: rgba(255, 255, 255, 0.2) !important;
  font-weight: 600;
  padding: 10px 0;
}
:deep(.el-table td) {
  border-color: rgba(255, 255, 255, 0.1) !important;
  color: #e0e0e0 !important;
  padding: 12px 0;
}
:deep(.el-table tr) {
@@ -573,43 +1096,278 @@
}
:deep(.el-table__row:hover td) {
  background: rgba(255, 255, 255, 0.05) !important;
  background: rgba(255, 255, 255, 0.08) !important;
}
:deep(.el-table--enable-row-hover .el-table__body tr:hover > td) {
  background: rgba(255, 255, 255, 0.05) !important;
:deep(.el-table--border) {
  border: 1px solid rgba(255,255,255,0.15) !important;
}
/* å“åº”式适配 */
:deep(.el-table__empty-text) {
  color: #cccccc !important;
}
/* ä»»åŠ¡çŠ¶æ€æ ·å¼ - é«˜å¯¹æ¯”度优化 */
.status-container {
  display: flex;
  align-items: center;
  padding: 4px 10px;
  border-radius: 20px;
  position: relative;
  overflow: hidden;
  font-size: 12px;
  font-weight: 600;
}
.status-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  margin-right: 6px;
  position: relative;
}
.status-dot::after {
  content: '';
  position: absolute;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  animation: pulse 2s infinite;
}
.status-text {
  white-space: nowrap;
}
/* ä¸åŒçŠ¶æ€çš„é¢œè‰² - æ·±è‰²ä¸»é¢˜é«˜å¯¹æ¯”度 */
.status-completed {
  background: rgba(103, 194, 58, 0.15);
  color: #67c23a;
}
.status-completed .status-dot {
  background: #67c23a;
}
.status-completed .status-dot::after {
  background: rgba(103, 194, 58, 0.5);
}
.status-processing {
  background: rgba(64, 158, 255, 0.15);
  color: #409eff;
}
.status-processing .status-dot {
  background: #409eff;
}
.status-processing .status-dot::after {
  background: rgba(64, 158, 255, 0.5);
}
.status-pending {
  background: rgba(144, 147, 153, 0.15);
  color: #909399;
}
.status-pending .status-dot {
  background: #909399;
}
.status-pending .status-dot::after {
  background: rgba(144, 147, 153, 0.5);
}
.status-suspended {
  background: rgba(230, 162, 60, 0.15);
  color: #e6a23c;
}
.status-suspended .status-dot {
  background: #e6a23c;
}
.status-suspended .status-dot::after {
  background: rgba(230, 162, 60, 0.5);
}
.status-canceled {
  background: rgba(144, 147, 153, 0.15);
  color: #909399;
}
.status-canceled .status-dot {
  background: #909399;
}
.status-canceled .status-dot::after {
  background: rgba(144, 147, 153, 0.5);
}
.status-error {
  background: rgba(245, 108, 108, 0.15);
  color: #f56c6c;
}
.status-error .status-dot {
  background: #f56c6c;
}
.status-error .status-dot::after {
  background: rgba(245, 108, 108, 0.5);
}
.status-unknown {
  background: rgba(75, 85, 99, 0.15);
  color: #9ca3af;
}
.status-unknown .status-dot {
  background: #9ca3af;
}
.status-unknown .status-dot::after {
  background: rgba(156, 163, 175, 0.5);
}
/* ä»»åŠ¡ç±»åž‹æ ·å¼ - é«˜å¯¹æ¯”度优化 */
.type-container {
  display: flex;
  align-items: center;
  padding: 4px 10px;
  border-radius: 20px;
  position: relative;
  font-size: 12px;
  font-weight: 600;
}
.type-icon {
  margin-right: 6px;
  font-size: 14px;
}
.type-text {
  white-space: nowrap;
}
/* ä¸åŒç±»åž‹çš„颜色 */
.type-inbound {
  background: rgba(103, 194, 58, 0.15);
  color: #67c23a;
}
.type-outbound {
  background: rgba(230, 162, 60, 0.15);
  color: #e6a23c;
}
.type-transfer {
  background: rgba(64, 158, 255, 0.15);
  color: #409eff;
}
.type-other {
  background: rgba(144, 147, 153, 0.15);
  color: #909399;
}
.type-unknown {
  background: rgba(75, 85, 99, 0.15);
  color: #9ca3af;
}
/* ä¸´æœŸå¤©æ•°æ ·å¼ */
.exp-expired {
  color: #f56c6c;
  font-weight: 600;
}
.exp-critical {
  color: #e6a23c;
  font-weight: 600;
}
.exp-warning {
  color: #f0ad4e;
  font-weight: 500;
}
.exp-normal {
  color: #67c23a;
  font-weight: 500;
}
.exp-unknown {
  color: #909399;
  font-weight: 400;
}
/* åŠ¨ç”»æ•ˆæžœ */
@keyframes pulse {
  0% {
    transform: scale(1);
    opacity: 1;
  }
  50% {
    transform: scale(1.5);
    opacity: 0.3;
  }
  100% {
    transform: scale(1);
    opacity: 1;
  }
}
/* é—ªçƒæ•ˆæžœï¼ˆè¡¨æ ¼å’Œå›¾è¡¨åˆ·æ–°æ—¶ï¼‰ */
.flash-effect {
  animation: flash 0.6s ease-in-out;
}
@keyframes flash {
  0% {
    box-shadow: 0 0 0 rgba(79, 172, 254, 0);
  }
  20% {
    box-shadow: 0 0 15px rgba(79, 172, 254, 0.7);
  }
  40% {
    box-shadow: 0 0 0 rgba(79, 172, 254, 0);
  }
  60% {
    box-shadow: 0 0 15px rgba(79, 172, 254, 0.7);
  }
  80% {
    box-shadow: 0 0 0 rgba(79, 172, 254, 0);
  }
  100% {
    box-shadow: 0 0 0 rgba(79, 172, 254, 0);
  }
}
/* å“åº”式适配增强 */
@media screen and (max-width: 1920px) {
  .metric-value {
    font-size: 24px;
    font-size: 28px;
  }
  .chart-container {
    height: 220px;
    min-height: 220px;
  }
}
@media screen and (max-width: 1600px) {
  .metrics-row {
    grid-template-columns: repeat(2, 1fr);
  }
  .charts-row {
    grid-template-columns: repeat(2, 1fr);
  }
  .title {
    font-size: 20px;
  }
  .metric-value {
    font-size: 20px;
    font-size: 24px;
  }
  .chart-container {
    height: 200px;
    min-height: 200px;
  }
}
@@ -632,22 +1390,12 @@
    font-size: 14px;
  }
  .metrics-row {
    grid-template-columns: repeat(2, 1fr);
    gap: 10px;
  }
  .metric-card {
    padding: 15px;
  }
  .metric-value {
    font-size: 18px;
  }
  .charts-row {
    grid-template-columns: 1fr;
    gap: 10px;
    font-size: 22px;
  }
  .chart-card {
@@ -655,23 +1403,7 @@
  }
  .chart-container {
    height: 180px;
  }
}
@media screen and (max-width: 1024px) {
  .metrics-row {
    grid-template-columns: 1fr 1fr;
  }
  .nav-menu {
    flex-wrap: wrap;
  }
  .nav-item {
    flex: 1;
    min-width: 100px;
    justify-content: center;
    min-height: 180px;
  }
}
@@ -698,17 +1430,17 @@
  .nav-menu {
    gap: 8px;
    padding: 0;
    flex-wrap: wrap;
  }
  .nav-item {
    padding: 6px 12px;
    font-size: 12px;
    flex: 1;
    min-width: 80px;
    justify-content: center;
    flex-direction: column;
    gap: 4px;
  }
  .metrics-row {
    grid-template-columns: 1fr;
  }
  .metric-card {
@@ -722,8 +1454,8 @@
    height: 45px;
  }
  .chart-container {
    height: 150px;
  .metric-trend {
    justify-content: center;
  }
}
</style>
</style>