| | |
| | | <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> |
| | |
| | | </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> |
| | |
| | | <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> |
| | |
| | | |
| | | <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: [] }, |
| | |
| | | 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); |
| | |
| | | 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 = () => { |
| | |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'shadow' // é´å½±æç¤ºå¨ |
| | | type: 'shadow' |
| | | }, |
| | | formatter: function (params) { |
| | | const data = params[0]; |
| | |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'line' // 线åæç¤ºå¨ |
| | | type: 'line' |
| | | }, |
| | | formatter: function (params) { |
| | | const data = params[0]; |
| | |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'cross', // åååææç¤ºå¨ |
| | | type: 'cross', |
| | | crossStyle: { |
| | | color: '#999' |
| | | } |
| | |
| | | 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)) { |
| | |
| | | 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(() => { |
| | |
| | | }); |
| | | }; |
| | | |
| | | // 轮询æ§å¶ |
| | | // æ°æ®è·å |
| | | 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 = () => { |
| | |
| | | clearInterval(intervalId.value); |
| | | intervalId.value = null; |
| | | } |
| | | pauseScroll(); |
| | | }; |
| | | |
| | | // çå½å¨æ |
| | |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | <style scoped lang="less"> |
| | | .dashboard-container { |
| | | padding: 20px; |
| | | background-color: #f5f6fa; |
| | |
| | | } |
| | | |
| | | .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 { |
| | |
| | | } |
| | | |
| | | .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 { |
| | |
| | | } |
| | | |
| | | .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> |