ÏîÄ¿´úÂë/WMS/WIDESEA_WMSClient/src/views/Home.vue
@@ -1,21 +1,113 @@
<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>
      <el-row :gutter="20">
        <el-col :lg="24">
          <div class="data-overview">
            <h3 class="title">数据总览</h3>
            <div class="metrics">
              <div class="metric-item" v-for="(item, index) in dataMetrics" :key="index">
                <div style="display: flex;align-items: center; margin-left: 80px;">
                  <div class="metric-icon">
                    <i :class="getMetricIcon(item.name)"></i>
                  </div>
                  <div class="metric-name">{{ item.name }}</div>
                </div>
                <div class="metric-value">{{ item.value }}</div>
              </div>
            </div>
          </div>
        </div>
      </div>
        </el-col>
      </el-row>
    </div>
    <div class="overview-section">
      <el-row :gutter="20">
        <el-col :lg="24">
          <div class="bg-color-black task-board-container">
            <div class="d-flex pt-2 pl-2">
              <div class="d-flex">
                <span class="fs-xl text mx-2 title">当前生产任务</span>
              </div>
            </div>
            <div class="body-box">
              <div class="scroll-table-container" @mouseenter="pauseScroll" @mouseleave="resumeScroll">
                <!-- è¡¨å¤´ -->
                <div class="table-header" ref="tableHeader">
                  <table class="header-table">
                    <thead>
                      <tr>
                        <th style="width: 60px;font-size: 18px;">序号</th>
                        <th style="width: 100px;font-size: 18px;">托盘码</th>
                        <th style="width: 120px;font-size: 18px;">起始地址</th>
                        <th style="width: 120px;font-size: 18px;">目标地址</th>
                        <th style="width: 120px;font-size: 18px;">任务类型</th>
                        <th style="width: 120px;font-size: 18px;">任务状态</th>
                        <th style="width: 160px;font-size: 18px;">创建时间</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="index"
                          :class="index % 2 === 0 ? 'even-row' : 'odd-row'">
                          <td style="width: 60px; text-align: center;font-size: 16px;">{{ index + 1 }}</td>
                          <td style="width: 100px; text-align: center;font-size: 16px;">{{ row.palletCode || '无' }}</td>
                          <td style="width: 120px; text-align: center;font-size: 16px;">{{ row.sourceAddress || '无' }}
                          </td>
                          <td style="width: 120px; text-align: center;font-size: 16px;">{{ row.targetAddress || '无' }}
                          </td>
                          <td style="width: 120px; text-align: center;font-size: 16px;">{{ row.taskType || '无' }}</td>
                          <td style="width: 120px; text-align: center;font-size: 16px;">
                            <el-tag :type="getStatusType(row.taskState)" size="small">
                              {{ row.taskState || '无' }}
                            </el-tag>
                          </td>
                          <td style="width: 160px; text-align: center;font-size: 18px;">{{ row.createDate || '无' }}</td>
                        </tr>
                      </tbody>
                    </table>
                    <!-- å¤åˆ¶ä¸€ä»½æ•°æ®ç”¨äºŽæ— ç¼æ»šåЍ -->
                    <table class="body-table" v-if="tableData.length > rowNum">
                      <tbody>
                        <tr v-for="(row, index) in tableData" :key="`copy-${index}`"
                          :class="index % 2 === 0 ? 'even-row' : 'odd-row'">
                          <td style="width: 60px; text-align: center;">{{ index + tableData.length + 1 }}</td>
                          <td style="width: 100px; text-align: center;">{{ row.palletCode || '无' }}</td>
                          <td style="width: 120px; text-align: center;">{{ row.sourceAddress || '无' }}</td>
                          <td style="width: 120px; text-align: center;">{{ row.targetAddress || '无' }}</td>
                          <td style="width: 120px; text-align: center;">{{ row.taskType || '无' }}</td>
                          <td style="width: 120px; text-align: center;">
                            <el-tag :type="getStatusType(row.taskState)" size="small">
                              {{ row.taskState || '无' }}
                            </el-tag>
                          </td>
                          <td style="width: 160px; text-align: center;">{{ 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-container">
            <h3>出库量</h3>
            <h3 class="title">出库量</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>
@@ -24,7 +116,7 @@
        </el-col>
        <el-col :lg="12">
          <div class="chart-container">
            <h3>入库量</h3>
            <h3 class="title">入库量</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>
@@ -38,11 +130,11 @@
      <el-row :gutter="20">
        <el-col :lg="24">
          <div class="chart-container">
            <h3>月出入库量</h3>
            <h3 class="title">月出入库量</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>
            <div v-else ref="monthDataChart" class="chart1"></div>
          </div>
        </el-col>
@@ -53,11 +145,13 @@
<script setup>
import http from '../api/http.js';
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue';
import { ref, reactive, onMounted, onUnmounted, nextTick, computed } from 'vue';
import * as echarts from 'echarts';
// import { TrendCharts } from '@element-plus/icons-vue';
// å“åº”式数据
const dataMetrics = ref([]);
const tableData = ref([]);
const chartData = reactive({
  outbound: { dates: [], values: [] },
  inbound: { dates: [], values: [] },
@@ -66,6 +160,26 @@
const loading = ref(true);
const error = ref(false);
// æ»šåŠ¨ç›¸å…³
const tableHeader = ref(null);
const tableBody = ref(null);
const scrollPosition = ref(0);
const isScrolling = ref(true);
let scrollInterval = null;
const rowNum = 7; // æ˜¾ç¤ºçš„行数
const rowHeight = 40; // æ¯è¡Œé«˜åº¦
const getMetricIcon = (type) => {
  const iconMap = {
    '今日进库量': 'iconfont icon-cangpeitubiao_huanhuorukutuihuoruku',
    '今日出库量': 'el-icon-takeaway-box',
    '本月进库量': 'iconfont icon-cangpeitubiao_huanhuorukutuihuoruku',
    '本月出库量': 'el-icon-takeaway-box',
    '库存总量': 'el-icon-goods',
  };
  return iconMap[type] || 'el-icon-data-board';
};
// å›¾è¡¨å¼•用和实例
const outboundChart = ref(null);
const inboundChart = ref(null);
@@ -74,6 +188,49 @@
const inboundInstance = ref(null);
const monthDataInstance = ref(null);
const charts = ref([]);
// å¯åŠ¨æ»šåŠ¨
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;
    }
  }, 20); // è°ƒæ•´æ»šåŠ¨é€Ÿåº¦
};
// æš‚停滚动
const pauseScroll = () => {
  if (scrollInterval) {
    clearInterval(scrollInterval);
    scrollInterval = null;
  }
  isScrolling.value = false;
};
// æ¢å¤æ»šåЍ
const resumeScroll = () => {
  if (tableData.value.length > rowNum) {
    startScrolling();
  }
};
// çŠ¶æ€æ ‡ç­¾ç±»åž‹
const getStatusType = (status) => {
  const statusMap = {
  };
  return statusMap[status] || 'primary';
};
// åˆå§‹åŒ–图表
const initCharts = () => {
@@ -93,7 +250,7 @@
    tooltip: {
      trigger: 'axis',
      axisPointer: {
        type: 'shadow' // é˜´å½±æŒ‡ç¤ºå™¨
        type: 'shadow'
      },
      formatter: function (params) {
        const data = params[0];
@@ -153,7 +310,7 @@
    tooltip: {
      trigger: 'axis',
      axisPointer: {
        type: 'line' // çº¿åž‹æŒ‡ç¤ºå™¨
        type: 'line'
      },
      formatter: function (params) {
        const data = params[0];
@@ -216,7 +373,7 @@
    tooltip: {
      trigger: 'axis',
      axisPointer: {
        type: 'cross', // åå­—准星指示器
        type: 'cross',
        crossStyle: {
          color: '#999'
        }
@@ -346,32 +503,9 @@
  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);
  // æ›´æ–°æ•°æ®æŒ‡æ ‡
  if (data.metrics && Array.isArray(data.metrics)) {
@@ -398,8 +532,28 @@
    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();
  }
  // å»¶è¿Ÿåˆå§‹åŒ–图表,确保数据已更新
  nextTick(() => {
@@ -411,11 +565,32 @@
  });
};
// è½®è¯¢æŽ§åˆ¶
// æ•°æ®èŽ·å–
const fetchData = async () => {
  try {
    loading.value = true;
    error.value = false;
    const response = await http.post("api/StockInfo/GetStockData", {});
    console.log('API响应:', response);
    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 intervalId = ref(null);
const startPolling = () => {
  fetchData();
  intervalId.value = setInterval(fetchData, 5 * 60 * 1000);
  intervalId.value = setInterval(fetchData, 5 * 60 * 1000); // 5分钟轮询
};
const stopPolling = () => {
@@ -423,6 +598,7 @@
    clearInterval(intervalId.value);
    intervalId.value = null;
  }
  pauseScroll();
};
// ç”Ÿå‘½å‘¨æœŸ
@@ -438,7 +614,7 @@
});
</script>
<style scoped>
<style scoped lang="less">
.dashboard-container {
  padding: 20px;
  background-color: #f5f6fa;
@@ -446,23 +622,179 @@
}
.overview-section {
  display: flex;
  gap: 20px;
  margin-bottom: 20px;
}
.data-overview {
  flex: 1;
  background: white;
  padding: 16px;
  border-radius: 8px;
  height: 200px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  .title {
    display: flex;
    align-items: center;
    &::before {
      display: block;
      content: "";
      width: 6px;
      height: 20px;
      background-color: #409eff;
      margin-right: 10px;
    }
  }
}
.task-board-container {
  height: 300px;
  border-radius: 8px;
  background-color: white;
  display: flex;
  flex-direction: column;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.d-flex {
  display: flex;
  .title {
    display: flex;
    align-items: center;
    &::before {
      display: block;
      content: "";
      width: 6px;
      height: 20px;
      background-color: #409eff;
      margin-right: 10px;
    }
  }
}
.pt-2 {
  padding-top: 0.5rem;
}
.pl-2 {
  padding-left: 0.5rem;
}
.text-icon {
  color: #409eff;
  font-size: 18px;
}
.fs-xl {
  font-size: 16px;
  font-weight: bold;
}
.text {
  color: #333;
}
.mx-2 {
  margin-left: 0.5rem;
  margin-right: 0.5rem;
}
.body-box {
  flex: 1;
  padding: 10px;
  border-radius: 8px;
  overflow: hidden;
}
.scroll-table-container {
  height: 250px;
  position: relative;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
  overflow: hidden;
}
.table-header {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 10;
  background: #0066cc;
}
.header-table {
  width: 100%;
  border-collapse: collapse;
}
.header-table th {
  background: #0066cc;
  color: white;
  font-weight: bold;
  padding: 8px 4px;
  text-align: center;
  border-right: 1px solid #0055aa;
  font-size: 12px;
}
.header-table th:last-child {
  border-right: none;
}
.table-body-container {
  position: absolute;
  top: 40px;
  /* è¡¨å¤´é«˜åº¦ */
  left: 0;
  right: 0;
  bottom: 0;
  overflow: hidden;
}
.table-body-wrapper {
  transition: transform 0.1s linear;
}
.body-table {
  width: 100%;
  border-collapse: collapse;
}
.body-table td {
  padding: 8px 4px;
  border-bottom: 1px solid #f0f0f0;
  font-size: 12px;
  height: 40px;
  box-sizing: border-box;
}
.even-row {
  background-color: #f8f9fa;
}
.odd-row {
  background-color: #ffffff;
}
.body-table tr:hover {
  background-color: #e6f7ff !important;
}
.no-data-board {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  width: 100%;
}
.metrics {
  display: flex;
  justify-content: space-between;
  margin-top: 16px;
  margin-top: 15px;
}
.metric-item {
@@ -472,48 +804,60 @@
}
.metric-name {
  font-size: 14px;
  font-size: 24px;
  color: #666;
  margin-bottom: 8px;
}
.metric-value {
  font-size: 20px;
  font-size: 22px;
  font-weight: bold;
  margin: 8px 0;
  margin: 20px 0;
  color: #333;
}
.charts-section {
  gap: 20px;
  margin-bottom: 20px;
}
.chart-container {
  flex: 1;
  background: white;
  padding: 16px;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  height: 400px;
  .title {
    display: flex;
    align-items: center;
    &::before {
      display: block;
      content: "";
      width: 6px;
      height: 20px;
      background-color: #409eff;
      margin-right: 10px;
    }
  }
}
.chart {
  height: 550px;
  height: 350px;
  width: 100%;
}
.chart1 {
  height: 550px;
  height: 350px;
  width: 100%;
}
.loading,
.error,
.no-data {
  display: flex;
  justify-content: center;
  align-items: center;
  margin-top: 50px;
  height: 500px;
  font-size: 18px;
  font-size: 20px;
}
.loading {
@@ -521,10 +865,49 @@
}
.error {
  color: #2d8cf0;
  color: #f56c6c;
}
.metric-icon {
  width: 28px;
  height: 28px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 12px;
  margin-bottom: 6px;
  font-size: 24px;
  background: rgba(255, 255, 255, 0.8);
}
.no-data {
  color: #909399;
}
/* Element Plus æ ‡ç­¾æ ·å¼è°ƒæ•´ */
:deep(.el-tag) {
  border: none;
  font-size: 12px;
}
:deep(.el-tag--success) {
  background-color: #f0f9ff;
  color: #67c23a;
}
:deep(.el-tag--primary) {
  background-color: #f0f9ff;
  color: #409eff;
}
:deep(.el-tag--info) {
  background-color: #f4f4f5;
  color: #909399;
}
:deep(.el-tag--danger) {
  background-color: #fef0f0;
  color: #f56c6c;
}
</style>