huangxiaoqiang
2025-11-17 b07472f884708a6bfdf63d999004bbf0bb5f00a8
ÏîÄ¿´úÂë/WMS/WIDESEA_WMSClient/src/views/Home.vue
@@ -1,49 +1,242 @@
<template>
  <div class="dashboard-container">
    <!-- æ•°æ®æ€»è§ˆéƒ¨åˆ† -->
    <div class="overview-section">
      <div class="data-overview">
        <h3>数据总览</h3>
        <div class="metrics">
          <div class="metric-item" v-for="(item, index) in dataMetrics" :key="index">
            <div class="metric-name">{{ item.name }}</div>
            <div class="metric-value">{{ item.value }}</div>
          </div>
        </div>
      </div>
    </div>
    <div class="charts-section">
      <el-row :gutter="20">
        <el-col :lg="12">
          <div class="chart-container">
            <h3>出库量</h3>
            <div v-if="loading" class="loading">加载中...</div>
            <div v-else-if="error" class="error">数据加载失败</div>
            <div v-else-if="!chartData.outbound.values.length" class="no-data">暂无出库数据</div>
            <div v-else ref="outboundChart" class="chart"></div>
          </div>
        </el-col>
        <el-col :lg="12">
          <div class="chart-container">
            <h3>入库量</h3>
            <div v-if="loading" class="loading">加载中...</div>
            <div v-else-if="error" class="error">数据加载失败</div>
            <div v-else-if="!chartData.inbound.values.length" class="no-data">暂无入库数据</div>
            <div v-else ref="inboundChart" class="chart"></div>
        <el-col :lg="24">
          <div class="data-overview">
            <div class="overview-header">
              <div class="header-left">
                <h3 class="title">
                  <i class="el-icon-data-analysis header-icon"></i>
                  æ•°æ®æ€»è§ˆ
                </h3>
              </div>
              <div class="header-right">
                <div class="time-range">数据更新于: {{ currentTime }}</div>
                <div>
                  <el-button type="primary" size="small" :icon="Refresh" @click="handleRefresh" :loading="refreshing"
                    class="refresh-btn">
                    åˆ·æ–°æ•°æ®
                  </el-button>
                </div>
              </div>
            </div>
            <div class="metrics-grid">
              <div class="metric-card" v-for="(item, index) in dataMetrics" :key="`metric-${index}-${refreshKey}`"
                :class="getMetricCardClass(item.name)">
                <div class="metric-content">
                  <div class="metric-icon-wrapper">
                    <i :class="getMetricIcon(item.name)"></i>
                  </div>
                  <div class="metric-info">
                    <div class="metric-name">{{ item.name }}</div>
                    <div class="metric-value">{{ formatNumber(item.value) }}</div>
                    <div class="metric-compare" v-if="item.compare !== undefined">
                      <span v-if="item.name == '库存总量'">
                      </span>
                      <span :class="getCompareClass(item.compare)" v-else>
                        <i :class="getCompareIcon(item.compare)"></i>
                        {{ formatCompareValue(item.compare) }}
                      </span>
                      <!-- <span class="compare-label">较昨日</span> -->
                      <span class="compare-label">{{ text(item.name) }}</span>
                    </div>
                  </div>
                </div>
                <div class="metric-trend" v-if="item.compare !== undefined">
                  <div class="trend-chart">
                    <div class="trend-bar" :style="{ height: getTrendHeight(item) }"></div>
                  </div>
                </div>
                <div class="metric-decoration">
                  <div class="decoration-circle"></div>
                </div>
              </div>
            </div>
          </div>
        </el-col>
      </el-row>
    </div>
    <div style="margin-top: 20px;"></div>
    <!-- ç”Ÿäº§ä»»åŠ¡çœ‹æ¿ -->
    <div class="overview-section">
      <el-row :gutter="20">
        <el-col :lg="24">
          <div class="task-board-container">
            <div class="board-header">
              <div class="header-left">
                <i class="el-icon-s-management board-icon"></i>
                <span class="board-title">当前生产任务</span>
              </div>
              <div class="header-right">
                <span class="task-count">共 {{ tableData.length }} ä¸ªä»»åŠ¡</span>
              </div>
            </div>
            <div class="board-body">
              <div class="scroll-table-container" @mouseenter="pauseScroll" @mouseleave="resumeScroll">
                <!-- è¡¨å¤´ -->
                <div class="table-header" ref="tableHeader">
                  <table class="header-table">
                    <thead>
                      <tr>
                        <th>序号</th>
                        <th>托盘码</th>
                        <th>起始地址</th>
                        <th>目标地址</th>
                        <th>任务类型</th>
                        <th>任务状态</th>
                        <th>创建时间</th>
                      </tr>
                    </thead>
                  </table>
                </div>
                <!-- è¡¨æ ¼å†…容区域 -->
                <div class="table-body-container" ref="tableBody">
                  <div class="table-body-wrapper" :style="{ transform: `translateY(-${scrollPosition}px)` }">
                    <!-- åŽŸå§‹æ•°æ® -->
                    <table class="body-table">
                      <tbody>
                        <tr v-for="(row, index) in tableData" :key="`task-${index}-${refreshKey}`"
                          :class="index % 2 === 0 ? 'even-row' : 'odd-row'">
                          <td>{{ index + 1 }}</td>
                          <td>{{ row.palletCode || '无' }}</td>
                          <td>{{ row.sourceAddress || '无' }}</td>
                          <td>{{ row.targetAddress || '无' }}</td>
                          <td>{{ row.taskType || '无' }}</td>
                          <td>
                            <el-tag :type="getStatusType(row.taskState)" size="small" class="status-tag">
                              {{ row.taskState || '无' }}
                            </el-tag>
                          </td>
                          <td>{{ formatDateTime(row.createDate) || '无' }}</td>
                        </tr>
                      </tbody>
                    </table>
                    <!-- å¤åˆ¶ä¸€ä»½æ•°æ®ç”¨äºŽæ— ç¼æ»šåЍ -->
                    <table class="body-table" v-if="tableData.length > rowNum">
                      <tbody>
                        <tr v-for="(row, index) in tableData" :key="`task-copy-${index}-${refreshKey}`"
                          :class="index % 2 === 0 ? 'even-row' : 'odd-row'">
                          <td>{{ index + tableData.length + 1 }}</td>
                          <td>{{ row.palletCode || '无' }}</td>
                          <td>{{ row.sourceAddress || '无' }}</td>
                          <td>{{ row.targetAddress || '无' }}</td>
                          <td>{{ row.taskType || '无' }}</td>
                          <td>
                            <el-tag :type="getStatusType(row.taskState)" size="small" class="status-tag">
                              {{ row.taskState || '无' }}
                            </el-tag>
                          </td>
                          <td>{{ formatDateTime(row.createDate) || '无' }}</td>
                        </tr>
                      </tbody>
                    </table>
                  </div>
                </div>
              </div>
              <div v-if="tableData.length === 0" class="no-data-board">
                <el-empty description="暂无生产任务数据" :image-size="80" />
              </div>
            </div>
          </div>
        </el-col>
      </el-row>
    </div>
    <!-- å›¾è¡¨éƒ¨åˆ† -->
    <div class="charts-section">
      <el-row :gutter="20">
        <el-col :lg="12">
          <div class="chart-card">
            <div class="chart-header">
              <h3 class="chart-title">
                <i class="el-icon-trend-charts chart-icon"></i>
                å‡ºåº“量趋势
              </h3>
              <div class="chart-subtitle">近7日出库统计</div>
            </div>
            <div class="chart-content">
              <div v-if="loading" class="chart-loading">
                <i class="el-icon-loading"></i>
                <span>数据加载中...</span>
              </div>
              <div v-else-if="error" class="chart-error">
                <i class="el-icon-warning"></i>
                <span>数据加载失败</span>
                <el-button type="text" @click="fetchData" class="retry-btn">重试</el-button>
              </div>
              <div v-else-if="!chartData.outbound.values.length" class="chart-no-data">
                <i class="el-icon-data-line"></i>
                <span>暂无出库数据</span>
              </div>
              <div v-else ref="outboundChart" class="chart" :key="`outbound-${refreshKey}`"></div>
            </div>
          </div>
        </el-col>
        <el-col :lg="12">
          <div class="chart-card">
            <div class="chart-header">
              <h3 class="chart-title">
                <i class="el-icon-trend-charts chart-icon"></i>
                å…¥åº“量趋势
              </h3>
              <div class="chart-subtitle">近7日入库统计</div>
            </div>
            <div class="chart-content">
              <div v-if="loading" class="chart-loading">
                <i class="el-icon-loading"></i>
                <span>数据加载中...</span>
              </div>
              <div v-else-if="error" class="chart-error">
                <i class="el-icon-warning"></i>
                <span>数据加载失败</span>
                <el-button type="text" @click="fetchData" class="retry-btn">重试</el-button>
              </div>
              <div v-else-if="!chartData.inbound.values.length" class="chart-no-data">
                <i class="el-icon-data-line"></i>
                <span>暂无入库数据</span>
              </div>
              <div v-else ref="inboundChart" class="chart" :key="`inbound-${refreshKey}`"></div>
            </div>
          </div>
        </el-col>
      </el-row>
    </div>
    <!-- æœˆåº¦æ•°æ®å›¾è¡¨ -->
    <div class="charts-section">
      <el-row :gutter="20">
        <el-col :lg="24">
          <div class="chart-container">
            <h3>月出入库量</h3>
            <div v-if="loading" class="loading">加载中...</div>
            <div v-else-if="error" class="error">数据加载失败</div>
            <div v-else-if="!chartData.monthData.inValue.length || !chartData.monthData.outValue.length" class="no-data">
              æš‚无出库数据</div>
            <div v-else ref="monthDataChart" class="chart1"></div>
          <div class="chart-card">
            <div class="chart-header">
              <h3 class="chart-title">
                <i class="el-icon-data-line chart-icon"></i>
                æœˆåº¦å‡ºå…¥åº“对比
              </h3>
              <div class="chart-subtitle">本月每日出入库量统计</div>
            </div>
            <div class="chart-content">
              <div v-if="loading" class="chart-loading">
                <i class="el-icon-loading"></i>
                <span>数据加载中...</span>
              </div>
              <div v-else-if="error" class="chart-error">
                <i class="el-icon-warning"></i>
                <span>数据加载失败</span>
                <el-button type="text" @click="fetchData" class="retry-btn">重试</el-button>
              </div>
              <div v-else-if="!chartData.monthData.inValue.length || !chartData.monthData.outValue.length"
                class="chart-no-data">
                <i class="el-icon-data-line"></i>
                <span>暂无出入库数据</span>
              </div>
              <div v-else ref="monthDataChart" class="chart-large" :key="`month-${refreshKey}`"></div>
            </div>
          </div>
        </el-col>
      </el-row>
@@ -55,9 +248,11 @@
import http from '../api/http.js';
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue';
import * as echarts from 'echarts';
import { Refresh } from '@element-plus/icons-vue';
// å“åº”式数据
const dataMetrics = ref([]);
const tableData = ref([]);
const chartData = reactive({
  outbound: { dates: [], values: [] },
  inbound: { dates: [], values: [] },
@@ -65,6 +260,20 @@
});
const loading = ref(true);
const error = ref(false);
const refreshing = ref(false);
const refreshKey = ref(0); // ç”¨äºŽå¼ºåˆ¶é‡æ–°æ¸²æŸ“
// æ»šåŠ¨ç›¸å…³
const tableHeader = ref(null);
const tableBody = ref(null);
const scrollPosition = ref(0);
const isScrolling = ref(true);
let scrollInterval = null;
const rowNum = 7; // æ˜¾ç¤ºçš„行数
const rowHeight = 48; // æ¯è¡Œé«˜åº¦
// å½“前时间
const currentTime = ref('');
// å›¾è¡¨å¼•用和实例
const outboundChart = ref(null);
@@ -75,8 +284,166 @@
const monthDataInstance = ref(null);
const charts = ref([]);
// æ ¼å¼åŒ–æ•°å­—
const formatNumber = (num) => {
  if (num === undefined || num === null) return '0';
  return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};
const text = (str) => {
  if (str == "今日进库量" || str == "今日出库量") {
    return "较昨日"
  } else if (str == "本月进库量" || str == "本月出库量") {
    return "较上月"
  } else {
    return ""
  }
}
// æ ¼å¼åŒ–比较值
const formatCompareValue = (value) => {
  if (value === 0) return '0';
  return value > 0 ? `+${formatNumber(value)}` : formatNumber(value);
};
// æ ¼å¼åŒ–日期时间
const formatDateTime = (dateString) => {
  if (!dateString) return '';
  try {
    const date = new Date(dateString);
    return date.toLocaleString('zh-CN', {
      month: '2-digit',
      day: '2-digit',
      hour: '2-digit',
      minute: '2-digit'
    });
  } catch {
    return dateString;
  }
};
// èŽ·å–æ¯”è¾ƒå€¼æ ·å¼ç±»
const getCompareClass = (compare) => {
  if (compare > 0) return 'compare-positive';
  if (compare < 0) return 'compare-negative';
  return 'compare-zero';
};
// èŽ·å–æ¯”è¾ƒå€¼å›¾æ ‡
const getCompareIcon = (compare) => {
  if (compare > 0) return 'el-icon-top';
  if (compare < 0) return 'el-icon-bottom';
  return 'el-icon-minus';
};
// èŽ·å–è¶‹åŠ¿é«˜åº¦
const getTrendHeight = (item) => {
  if (item.compare === undefined || item.value === 0) return '0%';
  const maxValue = Math.max(Math.abs(item.value), Math.abs(item.compare));
  if (maxValue === 0) return '0%';
  const percentage = (Math.abs(item.compare) / maxValue) * 100;
  return `${Math.min(percentage, 100)}%`;
};
// èŽ·å–æŒ‡æ ‡å¡ç‰‡æ ·å¼ç±»
const getMetricCardClass = (name) => {
  const classMap = {
    '今日进库量': 'metric-inbound-today',
    '今日出库量': 'metric-outbound-today',
    '本月进库量': 'metric-inbound-month',
    '本月出库量': 'metric-outbound-month',
    '库存总量': 'metric-total'
  };
  return classMap[name] || '';
};
const getMetricIcon = (type) => {
  const iconMap = {
    '今日进库量': 'el-icon-download',
    '今日出库量': 'el-icon-upload2',
    '本月进库量': 'el-icon-download',
    '本月出库量': 'el-icon-upload2',
    '库存总量': 'el-icon-box',
  };
  return iconMap[type] || 'el-icon-data-board';
};
// çŠ¶æ€æ ‡ç­¾ç±»åž‹
const getStatusType = (status) => {
  const statusMap = {
    '进行中': 'primary',
    '已完成': 'success',
    '已取消': 'info',
    '异常': 'danger',
    '待执行': 'warning'
  };
  return statusMap[status] || 'primary';
};
// æ›´æ–°å½“前时间
const updateCurrentTime = () => {
  const now = new Date();
  currentTime.value = now.toLocaleString('zh-CN', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit'
  });
};
// å¯åŠ¨æ»šåŠ¨
const startScrolling = () => {
  if (tableData.value.length <= rowNum) {
    isScrolling.value = false;
    return; // æ•°æ®å°‘时不滚动
  }
  isScrolling.value = true;
  const totalHeight = tableData.value.length * rowHeight;
  scrollInterval = setInterval(() => {
    scrollPosition.value += 1;
    // å½“滚动到第一份数据的末尾时,重置位置实现无缝滚动
    if (scrollPosition.value >= totalHeight) {
      scrollPosition.value = 0;
    }
  }, 30); // è°ƒæ•´æ»šåŠ¨é€Ÿåº¦
};
// æš‚停滚动
const pauseScroll = () => {
  if (scrollInterval) {
    clearInterval(scrollInterval);
    scrollInterval = null;
  }
  isScrolling.value = false;
};
// æ¢å¤æ»šåЍ
const resumeScroll = () => {
  if (tableData.value.length > rowNum) {
    startScrolling();
  }
};
// åˆå§‹åŒ–图表
const initCharts = () => {
  // æ¸…理旧的图表实例
  if (outboundInstance.value) {
    outboundInstance.value.dispose();
  }
  if (inboundInstance.value) {
    inboundInstance.value.dispose();
  }
  if (monthDataInstance.value) {
    monthDataInstance.value.dispose();
  }
  if (!outboundChart.value || !inboundChart.value || !monthDataChart.value) {
    console.log('图表容器未找到,延迟初始化');
    return;
@@ -93,49 +460,81 @@
    tooltip: {
      trigger: 'axis',
      axisPointer: {
        type: 'shadow' // é˜´å½±æŒ‡ç¤ºå™¨
        type: 'shadow'
      },
      formatter: function (params) {
        const data = params[0];
        return `
          <div style="font-weight: bold; margin-bottom: 5px;">${data.name}</div>
          <div style="display: flex; align-items: center;">
            <span style="display: inline-block; width: 10px; height: 10px; background: ${data.color}; border-radius: 50%; margin-right: 5px;"></span>
            <span>${data.seriesName}: </span>
            <span style="font-weight: bold; margin-left: 5px;">${data.value}</span>
          <div style="font-weight: 600; margin-bottom: 8px; color: #303133;">${data.name}</div>
          <div style="display: flex; align-items: center; font-size: 14px;">
            <span style="display: inline-block; width: 8px; height: 8px; background: ${data.color}; border-radius: 50%; margin-right: 8px;"></span>
            <span style="color: #606266;">${data.seriesName}: </span>
            <span style="font-weight: 600; margin-left: 8px; color: #303133;">${data.value}</span>
          </div>
        `;
      },
      backgroundColor: 'rgba(255, 255, 255, 0.9)',
      borderColor: '#ddd',
      backgroundColor: 'rgba(255, 255, 255, 0.95)',
      borderColor: '#e4e7ed',
      borderWidth: 1,
      textStyle: {
        color: '#333'
      }
        color: '#303133'
      },
      padding: [8, 12],
      borderRadius: 6,
      shadowColor: 'rgba(0, 0, 0, 0.1)',
      shadowBlur: 8
    },
    grid: {
      left: '3%',
      right: '4%',
      right: '3%',
      bottom: '3%',
      top: '15%',
      containLabel: true
    },
    xAxis: {
      type: 'category',
      data: chartData.outbound.dates,
      axisLine: {
        lineStyle: {
          color: '#e4e7ed'
        }
      },
      axisLabel: {
        color: '#606266',
        rotate: 45
      }
    },
    yAxis: {
      type: 'value',
      name: '数量'
      name: '数量',
      nameTextStyle: {
        color: '#909399'
      },
      axisLine: {
        lineStyle: {
          color: '#e4e7ed'
        }
      },
      axisLabel: {
        color: '#606266'
      },
      splitLine: {
        lineStyle: {
          color: '#f0f2f5',
          type: 'dashed'
        }
      }
    },
    series: [{
      name: '出库量',
      data: chartData.outbound.values,
      type: 'bar',
      itemStyle: {
        color: '#e06e6e'
        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
          { offset: 0, color: '#ff9f7f' },
          { offset: 1, color: '#ff6b6b' }
        ]),
        borderRadius: [4, 4, 0, 0]
      },
      barWidth: '60%',
      animation: true,
@@ -143,7 +542,8 @@
        show: true,
        position: 'top',
        formatter: '{c}',
        color: '#e06e6e'
        color: '#ff6b6b',
        fontWeight: 'bold'
      }
    }]
  };
@@ -153,60 +553,103 @@
    tooltip: {
      trigger: 'axis',
      axisPointer: {
        type: 'line' // çº¿åž‹æŒ‡ç¤ºå™¨
        type: 'line'
      },
      formatter: function (params) {
        const data = params[0];
        return `
          <div style="font-weight: bold; margin-bottom: 5px;">${data.name}</div>
          <div style="display: flex; align-items: center;">
            <span style="display: inline-block; width: 10px; height: 10px; background: ${data.color}; border-radius: 50%; margin-right: 5px;"></span>
            <span>${data.seriesName}: </span>
            <span style="font-weight: bold; margin-left: 5px;">${data.value}</span>
          <div style="font-weight: 600; margin-bottom: 8px; color: #303133;">${data.name}</div>
          <div style="display: flex; align-items: center; font-size: 14px;">
            <span style="display: inline-block; width: 8px; height: 8px; background: ${data.color}; border-radius: 50%; margin-right: 8px;"></span>
            <span style="color: #606266;">${data.seriesName}: </span>
            <span style="font-weight: 600; margin-left: 8px; color: #303133;">${data.value}</span>
          </div>
        `;
      },
      backgroundColor: 'rgba(255, 255, 255, 0.9)',
      borderColor: '#ddd',
      backgroundColor: 'rgba(255, 255, 255, 0.95)',
      borderColor: '#e4e7ed',
      borderWidth: 1,
      textStyle: {
        color: '#333'
      }
        color: '#303133'
      },
      padding: [8, 12],
      borderRadius: 6,
      shadowColor: 'rgba(0, 0, 0, 0.1)',
      shadowBlur: 8
    },
    grid: {
      left: '3%',
      right: '4%',
      right: '3%',
      bottom: '3%',
      top: '15%',
      containLabel: true
    },
    xAxis: {
      type: 'category',
      data: chartData.inbound.dates,
      axisLine: {
        lineStyle: {
          color: '#e4e7ed'
        }
      },
      axisLabel: {
        color: '#606266',
        rotate: 45
      }
    },
    yAxis: {
      type: 'value',
      name: '数量'
      name: '数量',
      nameTextStyle: {
        color: '#909399'
      },
      axisLine: {
        lineStyle: {
          color: '#e4e7ed'
        }
      },
      axisLabel: {
        color: '#606266'
      },
      splitLine: {
        lineStyle: {
          color: '#f0f2f5',
          type: 'dashed'
        }
      }
    },
    series: [{
      name: '入库量',
      data: chartData.inbound.values,
      type: 'line',
      smooth: true,
      symbol: 'circle',
      symbolSize: 8,
      itemStyle: {
        color: '#4a7bff'
        color: '#409eff',
        borderColor: '#fff',
        borderWidth: 2
      },
      lineStyle: {
        width: 3
        width: 3,
        color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
          { offset: 0, color: '#409eff' },
          { offset: 1, color: '#67c23a' }
        ])
      },
      smooth: true,
      areaStyle: {
        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
          { offset: 0, color: 'rgba(64, 158, 255, 0.3)' },
          { offset: 1, color: 'rgba(64, 158, 255, 0.1)' }
        ])
      },
      animation: true,
      label: {
        show: true,
        position: 'top',
        formatter: '{c}',
        color: '#4a7bff'
        color: '#409eff',
        fontWeight: 'bold'
      }
    }]
  };
@@ -216,45 +659,61 @@
    tooltip: {
      trigger: 'axis',
      axisPointer: {
        type: 'cross', // åå­—准星指示器
        type: 'cross',
        crossStyle: {
          color: '#999'
        }
      },
      formatter: function (params) {
        let html = `<div style="font-weight: bold; margin-bottom: 5px;">${params[0].name}</div>`;
        let html = `<div style="font-weight: 600; margin-bottom: 8px; color: #303133;">${params[0].name}</div>`;
        params.forEach(param => {
          html += `
            <div style="display: flex; align-items: center; margin: 2px 0;">
              <span style="display: inline-block; width: 10px; height: 10px; background: ${param.color}; border-radius: 50%; margin-right: 5px;"></span>
              <span>${param.seriesName}: </span>
              <span style="font-weight: bold; margin-left: 5px;">${param.value}</span>
            <div style="display: flex; align-items: center; margin: 4px 0; font-size: 14px;">
              <span style="display: inline-block; width: 8px; height: 8px; background: ${param.color}; border-radius: 50%; margin-right: 8px;"></span>
              <span style="color: #606266;">${param.seriesName}: </span>
              <span style="font-weight: 600; margin-left: 8px; color: #303133;">${param.value}</span>
            </div>
          `;
        });
        return html;
      },
      backgroundColor: 'rgba(255, 255, 255, 0.9)',
      borderColor: '#ddd',
      backgroundColor: 'rgba(255, 255, 255, 0.95)',
      borderColor: '#e4e7ed',
      borderWidth: 1,
      textStyle: {
        color: '#333'
      }
        color: '#303133'
      },
      padding: [12, 16],
      borderRadius: 6,
      shadowColor: 'rgba(0, 0, 0, 0.1)',
      shadowBlur: 8
    },
    legend: {
      data: ['入库量', '出库量'],
      bottom: 0
      bottom: 10,
      textStyle: {
        color: '#606266'
      },
      itemWidth: 12,
      itemHeight: 12
    },
    grid: {
      left: '3%',
      right: '4%',
      bottom: '10%',
      right: '3%',
      bottom: '12%',
      top: '15%',
      containLabel: true
    },
    xAxis: {
      type: 'category',
      data: chartData.monthData.dates,
      axisLine: {
        lineStyle: {
          color: '#e4e7ed'
        }
      },
      axisLabel: {
        color: '#606266',
        rotate: 45
      },
      axisPointer: {
@@ -263,45 +722,68 @@
    },
    yAxis: {
      type: 'value',
      name: '数量'
      name: '数量',
      nameTextStyle: {
        color: '#909399'
      },
      axisLine: {
        lineStyle: {
          color: '#e4e7ed'
        }
      },
      axisLabel: {
        color: '#606266'
      },
      splitLine: {
        lineStyle: {
          color: '#f0f2f5',
          type: 'dashed'
        }
      }
    },
    series: [
      {
        name: '入库量',
        data: chartData.monthData.inValue,
        type: 'line',
        smooth: true,
        symbol: 'circle',
        symbolSize: 6,
        itemStyle: {
          color: '#4a7bff'
          color: '#409eff'
        },
        lineStyle: {
          width: 3
        },
        smooth: true,
        animation: true,
        label: {
          show: true,
          position: 'top',
          formatter: '{c}',
          color: '#4a7bff'
          color: '#409eff',
          fontWeight: 'bold'
        }
      },
      {
        name: '出库量',
        data: chartData.monthData.outValue,
        type: 'line',
        smooth: true,
        symbol: 'circle',
        symbolSize: 6,
        itemStyle: {
          color: '#e06e6e'
          color: '#ff6b6b'
        },
        lineStyle: {
          width: 3
        },
        smooth: true,
        animation: true,
        label: {
          show: true,
          position: 'top',
          formatter: '{c}',
          color: '#e06e6e'
          color: '#ff6b6b',
          fontWeight: 'bold'
        }
      }
    ]
@@ -315,29 +797,19 @@
// æ›´æ–°å›¾è¡¨æ•°æ®
const updateCharts = () => {
  nextTick(() => {
    if (outboundInstance.value && chartData.outbound.values.length > 0) {
      outboundInstance.value.setOption({
        xAxis: { data: chartData.outbound.dates },
        series: [{ data: chartData.outbound.values }]
      });
    // å…ˆé”€æ¯æ—§çš„图表实例
    if (outboundInstance.value) {
      outboundInstance.value.dispose();
    }
    if (inboundInstance.value) {
      inboundInstance.value.dispose();
    }
    if (monthDataInstance.value) {
      monthDataInstance.value.dispose();
    }
    if (inboundInstance.value && chartData.inbound.values.length > 0) {
      inboundInstance.value.setOption({
        xAxis: { data: chartData.inbound.dates },
        series: [{ data: chartData.inbound.values }]
      });
    }
    if (monthDataInstance.value && chartData.monthData.dates.length > 0) {
      monthDataInstance.value.setOption({
        xAxis: { data: chartData.monthData.dates },
        series: [
          { data: chartData.monthData.inValue },
          { data: chartData.monthData.outValue }
        ]
      });
    }
    // é‡æ–°åˆå§‹åŒ–图表
    initCharts();
  });
};
@@ -346,38 +818,16 @@
  charts.value.forEach(chart => chart && chart.resize());
};
// æ•°æ®èŽ·å–
const fetchData = async () => {
  try {
    loading.value = true;
    error.value = false;
    const response = await http.post("api/StockInfo/GetStockData", {});
    console.log('API响应数据:', response.data);
    if (response.data && response.data.success !== false) {
      handleDataUpdate(response.data);
    } else {
      throw new Error('API返回数据格式错误');
    }
    loading.value = false;
  } catch (err) {
    console.error('API请求失败:', err);
    loading.value = false;
    error.value = true;
  }
};
// æ•°æ®å¤„理
const handleDataUpdate = (data) => {
  console.log('处理数据:', data);
  console.log('API响应数据:', data);
  // æ›´æ–°æ•°æ®æŒ‡æ ‡
  // ä½¿ç”¨ Object.assign ç¡®ä¿å“åº”式更新
  if (data.metrics && Array.isArray(data.metrics)) {
    dataMetrics.value = data.metrics.map(item => ({
      name: item.name || item.Name || '未知指标',
      value: item.value != null ? item.value : item.Value || 0
      value: item.value != null ? item.value : item.Value || 0,
      compare: item.compare != null ? item.compare : 0
    }));
  }
@@ -398,24 +848,80 @@
    chartData.monthData.outValue = data.monthData.outValue || [];
  }
  console.log('更新后的数据指标:', dataMetrics.value);
  console.log('更新后的图表数据:', chartData);
  // æ›´æ–°è¡¨æ ¼æ•°æ®
  if (data && data.newTask && Array.isArray(data.newTask)) {
    tableData.value = data.newTask.map(task => ({
      palletCode: task.palletCode || '无',
      roadway: task.roadway || '无',
      sourceAddress: task.sourceAddress || '无',
      targetAddress: task.targetAddress || '无',
      taskType: task.taskType || '无',
      taskState: task.taskState || '无',
      errorMessage: task.errorMessage || '无',
      createDate: task.createDate || '无',
    }));
    // æ•°æ®æ›´æ–°åŽé‡æ–°å¯åŠ¨æ»šåŠ¨
    nextTick(() => {
      pauseScroll();
      startScrolling();
    });
  } else {
    tableData.value = [];
    pauseScroll();
  }
  // å¼ºåˆ¶æ›´æ–° refreshKey è§¦å‘重新渲染
  refreshKey.value++;
  // å»¶è¿Ÿåˆå§‹åŒ–图表,确保数据已更新
  nextTick(() => {
    if (!outboundInstance.value || !inboundInstance.value || !monthDataInstance.value) {
      initCharts();
    } else {
      updateCharts();
    }
    updateCharts();
  });
};
// è½®è¯¢æŽ§åˆ¶
// æ•°æ®èŽ·å–
const fetchData = async () => {
  try {
    loading.value = true;
    error.value = false;
    refreshing.value = true;
    const response = await http.post("api/StockInfo/GetStockData", {});
    console.log('API响应:', response);
    if (response.data && response.data.success !== false) {
      handleDataUpdate(response.data);
      error.value = false;
    } else {
      throw new Error('API返回数据格式错误');
    }
  } catch (err) {
    console.error('API请求失败:', err);
    error.value = true;
  } finally {
    loading.value = false;
    refreshing.value = false;
    updateCurrentTime();
  }
};
// åˆ·æ–°å¤„理
const handleRefresh = () => {
  fetchData();
};
const intervalId = ref(null);
const timeIntervalId = ref(null);
const startPolling = () => {
  fetchData();
  intervalId.value = setInterval(fetchData, 5 * 60 * 1000);
  // è®¾ç½®å®šæ—¶æ›´æ–°å½“前时间
  timeIntervalId.value = setInterval(updateCurrentTime, 1000);
  // è®¾ç½®å®šæ—¶èŽ·å–æ•°æ®
  intervalId.value = setInterval(fetchData, 5 * 60 * 1000); // 5分钟轮询
};
const stopPolling = () => {
@@ -423,6 +929,11 @@
    clearInterval(intervalId.value);
    intervalId.value = null;
  }
  if (timeIntervalId.value) {
    clearInterval(timeIntervalId.value);
    timeIntervalId.value = null;
  }
  pauseScroll();
};
// ç”Ÿå‘½å‘¨æœŸ
@@ -439,92 +950,684 @@
</script>
<style scoped>
.retry-btn {
  margin-left: 8px;
  color: #409eff;
}
.chart-error {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
}
.chart-error .el-button {
  margin-top: 8px;
}
.dashboard-container {
  padding: 20px;
  background-color: #f5f6fa;
  background: linear-gradient(135deg, #f5f7fa 0%, #e4e7ed 100%);
  min-height: 100vh;
  font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
}
.overview-section {
  display: flex;
  gap: 20px;
  margin-bottom: 20px;
  margin-bottom: 24px;
}
.data-overview {
  flex: 1;
  background: white;
  padding: 16px;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
  padding: 24px;
  border-radius: 16px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.8);
  backdrop-filter: blur(10px);
}
.metrics {
.overview-header {
  display: flex;
  justify-content: space-between;
  margin-top: 16px;
  align-items: flex-start;
  margin-bottom: 24px;
}
.metric-item {
  text-align: center;
.header-left .title {
  display: flex;
  align-items: center;
  margin: 0 0 8px 0;
  font-size: 20px;
  font-weight: 700;
  color: #303133;
  line-height: 1.2;
}
.header-icon {
  margin-right: 12px;
  font-size: 24px;
  color: #409eff;
  background: linear-gradient(135deg, #409eff, #79bbff);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}
.subtitle {
  font-size: 14px;
  color: #909399;
  margin: 0;
}
.header-right {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 12px;
}
.time-range {
  font-size: 13px;
  color: #909399;
  background: rgba(64, 158, 255, 0.1);
  padding: 6px 12px;
  border-radius: 20px;
  border: 1px solid rgba(64, 158, 255, 0.2);
}
.refresh-btn {
  border-radius: 20px;
  padding: 8px 16px;
  font-weight: 500;
  box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
  transition: all 0.3s ease;
}
.refresh-btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
}
.metrics-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 20px;
}
.metric-card {
  background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
  border-radius: 12px;
  padding: 24px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.8);
  transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
  display: flex;
  justify-content: space-between;
  align-items: center;
  position: relative;
  overflow: hidden;
}
.metric-card:hover {
  transform: translateY(-6px);
  box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
}
.metric-card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: linear-gradient(90deg, transparent 0%, currentColor 50%, transparent 100%);
  opacity: 0;
  transition: opacity 0.3s ease;
}
.metric-card:hover::before {
  opacity: 1;
}
.metric-inbound-today {
  border-left: 4px solid #67c23a;
  color: #67c23a;
}
.metric-outbound-today {
  border-left: 4px solid #e6a23c;
  color: #e6a23c;
}
.metric-inbound-month {
  border-left: 4px solid #409eff;
  color: #409eff;
}
.metric-outbound-month {
  border-left: 4px solid #f56c6c;
  color: #f56c6c;
}
.metric-total {
  border-left: 4px solid #909399;
  color: #909399;
}
.metric-content {
  display: flex;
  align-items: flex-start;
  flex: 1;
  padding: 10px;
  z-index: 2;
}
.metric-icon-wrapper {
  width: 56px;
  height: 56px;
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 16px;
  font-size: 28px;
  transition: all 0.3s ease;
}
.metric-card:hover .metric-icon-wrapper {
  transform: scale(1.1);
}
.metric-inbound-today .metric-icon-wrapper {
  background: linear-gradient(135deg, rgba(103, 194, 58, 0.15), rgba(103, 194, 58, 0.05));
  color: #67c23a;
}
.metric-outbound-today .metric-icon-wrapper {
  background: linear-gradient(135deg, rgba(230, 162, 60, 0.15), rgba(230, 162, 60, 0.05));
  color: #e6a23c;
}
.metric-inbound-month .metric-icon-wrapper {
  background: linear-gradient(135deg, rgba(64, 158, 255, 0.15), rgba(64, 158, 255, 0.05));
  color: #409eff;
}
.metric-outbound-month .metric-icon-wrapper {
  background: linear-gradient(135deg, rgba(245, 108, 108, 0.15), rgba(245, 108, 108, 0.05));
  color: #f56c6c;
}
.metric-total .metric-icon-wrapper {
  background: linear-gradient(135deg, rgba(144, 147, 153, 0.15), rgba(144, 147, 153, 0.05));
  color: #909399;
}
.metric-info {
  flex: 1;
}
.metric-name {
  font-size: 14px;
  color: #666;
  color: #606266;
  margin-bottom: 8px;
  font-weight: 500;
}
.metric-value {
  font-size: 28px;
  font-weight: 800;
  color: #303133;
  margin-bottom: 8px;
  line-height: 1;
  background: linear-gradient(135deg, currentColor, #303133);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}
.metric-compare {
  display: flex;
  align-items: center;
  font-size: 12px;
}
.compare-positive {
  color: #f56c6c;
  font-weight: 600;
  display: flex;
  align-items: center;
  margin-right: 8px;
}
.compare-positive i {
  font-size: 12px;
  margin-right: 4px;
}
.compare-negative {
  color: #67c23a;
  font-weight: 600;
  display: flex;
  align-items: center;
  margin-right: 8px;
}
.compare-negative i {
  font-size: 12px;
  margin-right: 4px;
}
.compare-zero {
  color: #909399;
  font-weight: 600;
  display: flex;
  align-items: center;
  margin-right: 8px;
}
.compare-hidden {
  display: hidden;
}
.compare-zero i {
  font-size: 12px;
  margin-right: 4px;
}
.compare-label {
  color: #909399;
  font-size: 11px;
}
.metric-trend {
  width: 44px;
  height: 44px;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  z-index: 2;
}
.trend-chart {
  width: 6px;
  height: 100%;
  background: rgba(0, 0, 0, 0.06);
  border-radius: 3px;
  position: relative;
  overflow: hidden;
}
.trend-bar {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  border-radius: 3px;
  transition: height 0.8s cubic-bezier(0.4, 0, 0.2, 1);
}
.metric-inbound-today .trend-bar {
  background: linear-gradient(to top, #67c23a, #95d475);
}
.metric-outbound-today .trend-bar {
  background: linear-gradient(to top, #e6a23c, #eebe77);
}
.metric-inbound-month .trend-bar {
  background: linear-gradient(to top, #409eff, #79bbff);
}
.metric-outbound-month .trend-bar {
  background: linear-gradient(to top, #f56c6c, #f89898);
}
.metric-total .trend-bar {
  background: linear-gradient(to top, #909399, #b1b3b8);
}
.metric-decoration {
  position: absolute;
  top: -20px;
  right: -20px;
  width: 80px;
  height: 80px;
  opacity: 0.1;
  z-index: 1;
}
.decoration-circle {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background: currentColor;
}
/* ä»»åŠ¡çœ‹æ¿æ ·å¼ */
.task-board-container {
  background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
  border-radius: 16px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.8);
  overflow: hidden;
}
.board-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px 24px;
  background: linear-gradient(135deg, #409eff 0%, #79bbff 100%);
  color: white;
}
.header-left {
  display: flex;
  align-items: center;
}
.board-icon {
  font-size: 20px;
  font-weight: bold;
  margin: 8px 0;
  color: #333;
  margin-right: 12px;
}
.charts-section {
  gap: 20px;
.board-title {
  font-size: 18px;
  font-weight: 600;
}
.chart-container {
  flex: 1;
  background: white;
  padding: 16px;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.task-count {
  font-size: 14px;
  opacity: 0.9;
  background: rgba(255, 255, 255, 0.2);
  padding: 4px 12px;
  border-radius: 12px;
}
.chart {
  height: 550px;
.board-body {
  height: 320px;
  padding: 0;
}
.scroll-table-container {
  height: 100%;
  position: relative;
  overflow: hidden;
}
.table-header {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 20;
  background: #f8f9fa;
  border-bottom: 2px solid #e4e7ed;
}
.header-table {
  width: 100%;
  border-collapse: collapse;
}
.chart1 {
  height: 550px;
.header-table th {
  background: #f8f9fa;
  color: #606266;
  font-weight: 600;
  padding: 16px 12px;
  text-align: center;
  border-right: 1px solid #e4e7ed;
  font-size: 14px;
  position: relative;
}
.header-table th:last-child {
  border-right: none;
}
.header-table th::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 2px;
  background: linear-gradient(90deg, #409eff, #79bbff);
}
.table-body-container {
  position: absolute;
  top: 56px;
  left: 0;
  right: 0;
  bottom: 0;
  overflow: hidden;
}
.table-body-wrapper {
  transition: transform 0.3s ease;
}
.body-table {
  width: 100%;
  border-collapse: collapse;
}
.loading,
.error,
.no-data {
.body-table td {
  padding: 14px 12px;
  border-bottom: 1px solid #f0f2f5;
  font-size: 13px;
  height: 48px;
  box-sizing: border-box;
  text-align: center;
  color: #606266;
}
.even-row {
  background-color: #fafbfc;
}
.odd-row {
  background-color: #ffffff;
}
.body-table tr:hover {
  background-color: #f0f7ff !important;
  transform: scale(1.01);
  transition: all 0.2s ease;
}
.status-tag {
  border-radius: 12px;
  padding: 4px 12px;
  font-weight: 500;
  border: none;
}
.no-data-board {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 500px;
  height: 100%;
  width: 100%;
  background: #f8f9fa;
}
/* å›¾è¡¨å¡ç‰‡æ ·å¼ */
.charts-section {
  margin-bottom: 24px;
}
.chart-card {
  background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
  border-radius: 16px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.8);
  overflow: hidden;
  height: 400px;
  display: flex;
  flex-direction: column;
}
.chart-header {
  padding: 20px 24px 0;
  background: transparent;
}
.chart-title {
  display: flex;
  align-items: center;
  margin: 0 0 8px 0;
  font-size: 16px;
  font-weight: 600;
  color: #303133;
}
.chart-icon {
  margin-right: 8px;
  color: #409eff;
  font-size: 18px;
}
.loading {
  color: #999;
}
.error {
  color: #2d8cf0;
}
.no-data {
.chart-subtitle {
  font-size: 13px;
  color: #909399;
  margin: 0;
}
.chart-content {
  flex: 1;
  padding: 0 12px 20px;
  position: relative;
}
.chart {
  height: 100%;
  width: 100%;
}
.chart-large {
  height: 100%;
  width: 100%;
}
.chart-loading,
.chart-error,
.chart-no-data {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  height: 100%;
  color: #909399;
  font-size: 14px;
}
.chart-loading i,
.chart-error i,
.chart-no-data i {
  font-size: 48px;
  margin-bottom: 16px;
  opacity: 0.6;
}
.chart-loading {
  color: #409eff;
}
.chart-error {
  color: #f56c6c;
}
/* Element Plus æ ‡ç­¾æ ·å¼è°ƒæ•´ */
:deep(.el-tag) {
  border: none;
  font-size: 12px;
  font-weight: 500;
}
:deep(.el-tag--success) {
  background: linear-gradient(135deg, #f0f9ff, #e1f3ff);
  color: #67c23a;
}
:deep(.el-tag--primary) {
  background: linear-gradient(135deg, #f0f9ff, #e1f3ff);
  color: #409eff;
}
:deep(.el-tag--info) {
  background: linear-gradient(135deg, #f4f4f5, #e9e9eb);
  color: #909399;
}
:deep(.el-tag--warning) {
  background: linear-gradient(135deg, #fdf6ec, #faecd8);
  color: #e6a23c;
}
:deep(.el-tag--danger) {
  background: linear-gradient(135deg, #fef0f0, #fde2e2);
  color: #f56c6c;
}
/* å“åº”式设计 */
@media (max-width: 1200px) {
  .metrics-grid {
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 16px;
  }
  .metric-card {
    padding: 20px;
  }
  .metric-value {
    font-size: 24px;
  }
}
@media (max-width: 768px) {
  .dashboard-container {
    padding: 16px;
  }
  .overview-header {
    flex-direction: column;
    gap: 16px;
  }
  .header-right {
    align-items: flex-start;
  }
  .metrics-grid {
    grid-template-columns: 1fr;
    gap: 12px;
  }
  .chart-card {
    height: 350px;
  }
}
/* æ»šåŠ¨æ¡æ ·å¼ä¼˜åŒ– */
.scroll-table-container::-webkit-scrollbar {
  width: 6px;
}
.scroll-table-container::-webkit-scrollbar-track {
  background: #f1f1f1;
  border-radius: 3px;
}
.scroll-table-container::-webkit-scrollbar-thumb {
  background: #c1c1c1;
  border-radius: 3px;
}
.scroll-table-container::-webkit-scrollbar-thumb:hover {
  background: #a8a8a8;
}
</style>