<template>
|
<div class="dashboard-container">
|
<!-- 数据总览部分 -->
|
<div class="overview-section">
|
<el-row :gutter="20">
|
<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 :class="getCompareClass(item.compare)">
|
<i :class="getCompareIcon(item.compare)"></i>
|
{{ formatCompareValue(item.compare) }}
|
</span>
|
<span class="compare-label">较昨日</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 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-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>
|
</div>
|
</div>
|
</template>
|
|
<script setup>
|
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: [] },
|
monthData: { dates: [], inValue: [], outValue: [] }
|
});
|
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);
|
const inboundChart = ref(null);
|
const monthDataChart = ref(null);
|
const outboundInstance = ref(null);
|
const inboundInstance = ref(null);
|
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 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;
|
}
|
|
outboundInstance.value = echarts.init(outboundChart.value);
|
inboundInstance.value = echarts.init(inboundChart.value);
|
monthDataInstance.value = echarts.init(monthDataChart.value);
|
|
charts.value = [outboundInstance.value, inboundInstance.value, monthDataInstance.value];
|
|
// 出库量图表配置(柱状图)
|
const outboundOption = {
|
tooltip: {
|
trigger: 'axis',
|
axisPointer: {
|
type: 'shadow'
|
},
|
formatter: function (params) {
|
const data = params[0];
|
return `
|
<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.95)',
|
borderColor: '#e4e7ed',
|
borderWidth: 1,
|
textStyle: {
|
color: '#303133'
|
},
|
padding: [8, 12],
|
borderRadius: 6,
|
shadowColor: 'rgba(0, 0, 0, 0.1)',
|
shadowBlur: 8
|
},
|
grid: {
|
left: '3%',
|
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: '数量',
|
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: 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,
|
label: {
|
show: true,
|
position: 'top',
|
formatter: '{c}',
|
color: '#ff6b6b',
|
fontWeight: 'bold'
|
}
|
}]
|
};
|
|
// 入库量图表配置(折线图)
|
const inboundOption = {
|
tooltip: {
|
trigger: 'axis',
|
axisPointer: {
|
type: 'line'
|
},
|
formatter: function (params) {
|
const data = params[0];
|
return `
|
<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.95)',
|
borderColor: '#e4e7ed',
|
borderWidth: 1,
|
textStyle: {
|
color: '#303133'
|
},
|
padding: [8, 12],
|
borderRadius: 6,
|
shadowColor: 'rgba(0, 0, 0, 0.1)',
|
shadowBlur: 8
|
},
|
grid: {
|
left: '3%',
|
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: '数量',
|
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: '#409eff',
|
borderColor: '#fff',
|
borderWidth: 2
|
},
|
lineStyle: {
|
width: 3,
|
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
{ offset: 0, color: '#409eff' },
|
{ offset: 1, color: '#67c23a' }
|
])
|
},
|
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: '#409eff',
|
fontWeight: 'bold'
|
}
|
}]
|
};
|
|
// 月出入库量图表配置(双折线图)
|
const monthDataOption = {
|
tooltip: {
|
trigger: 'axis',
|
axisPointer: {
|
type: 'cross',
|
crossStyle: {
|
color: '#999'
|
}
|
},
|
formatter: function (params) {
|
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: 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.95)',
|
borderColor: '#e4e7ed',
|
borderWidth: 1,
|
textStyle: {
|
color: '#303133'
|
},
|
padding: [12, 16],
|
borderRadius: 6,
|
shadowColor: 'rgba(0, 0, 0, 0.1)',
|
shadowBlur: 8
|
},
|
legend: {
|
data: ['入库量', '出库量'],
|
bottom: 10,
|
textStyle: {
|
color: '#606266'
|
},
|
itemWidth: 12,
|
itemHeight: 12
|
},
|
grid: {
|
left: '3%',
|
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: {
|
type: 'shadow'
|
}
|
},
|
yAxis: {
|
type: 'value',
|
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: '#409eff'
|
},
|
lineStyle: {
|
width: 3
|
},
|
animation: true,
|
label: {
|
show: true,
|
position: 'top',
|
formatter: '{c}',
|
color: '#409eff',
|
fontWeight: 'bold'
|
}
|
},
|
{
|
name: '出库量',
|
data: chartData.monthData.outValue,
|
type: 'line',
|
smooth: true,
|
symbol: 'circle',
|
symbolSize: 6,
|
itemStyle: {
|
color: '#ff6b6b'
|
},
|
lineStyle: {
|
width: 3
|
},
|
animation: true,
|
label: {
|
show: true,
|
position: 'top',
|
formatter: '{c}',
|
color: '#ff6b6b',
|
fontWeight: 'bold'
|
}
|
}
|
]
|
};
|
|
outboundInstance.value.setOption(outboundOption);
|
inboundInstance.value.setOption(inboundOption);
|
monthDataInstance.value.setOption(monthDataOption);
|
};
|
|
// 更新图表数据
|
const updateCharts = () => {
|
nextTick(() => {
|
// 先销毁旧的图表实例
|
if (outboundInstance.value) {
|
outboundInstance.value.dispose();
|
}
|
if (inboundInstance.value) {
|
inboundInstance.value.dispose();
|
}
|
if (monthDataInstance.value) {
|
monthDataInstance.value.dispose();
|
}
|
|
// 重新初始化图表
|
initCharts();
|
});
|
};
|
|
// 响应式窗口调整
|
const handleResize = () => {
|
charts.value.forEach(chart => chart && chart.resize());
|
};
|
|
// 数据处理
|
const handleDataUpdate = (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,
|
compare: item.compare != null ? item.compare : 0
|
}));
|
}
|
|
// 更新图表数据
|
if (data.outbound) {
|
chartData.outbound.dates = data.outbound.dates || [];
|
chartData.outbound.values = data.outbound.values || [];
|
}
|
|
if (data.inbound) {
|
chartData.inbound.dates = data.inbound.dates || [];
|
chartData.inbound.values = data.inbound.values || [];
|
}
|
|
if (data.monthData) {
|
chartData.monthData.dates = data.monthData.dates || [];
|
chartData.monthData.inValue = data.monthData.inValue || [];
|
chartData.monthData.outValue = data.monthData.outValue || [];
|
}
|
|
// 更新表格数据
|
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(() => {
|
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();
|
|
// 设置定时更新当前时间
|
timeIntervalId.value = setInterval(updateCurrentTime, 1000);
|
|
// 设置定时获取数据
|
intervalId.value = setInterval(fetchData, 5 * 60 * 1000); // 5分钟轮询
|
};
|
|
const stopPolling = () => {
|
if (intervalId.value) {
|
clearInterval(intervalId.value);
|
intervalId.value = null;
|
}
|
if (timeIntervalId.value) {
|
clearInterval(timeIntervalId.value);
|
timeIntervalId.value = null;
|
}
|
pauseScroll();
|
};
|
|
// 生命周期
|
onMounted(() => {
|
startPolling();
|
window.addEventListener('resize', handleResize);
|
});
|
|
onUnmounted(() => {
|
stopPolling();
|
charts.value.forEach(chart => chart && chart.dispose());
|
window.removeEventListener('resize', handleResize);
|
});
|
</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: 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 {
|
margin-bottom: 24px;
|
}
|
|
.data-overview {
|
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);
|
}
|
|
.overview-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: flex-start;
|
margin-bottom: 24px;
|
}
|
|
.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;
|
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: #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-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;
|
margin-right: 12px;
|
}
|
|
.board-title {
|
font-size: 18px;
|
font-weight: 600;
|
}
|
|
.task-count {
|
font-size: 14px;
|
opacity: 0.9;
|
background: rgba(255, 255, 255, 0.2);
|
padding: 4px 12px;
|
border-radius: 12px;
|
}
|
|
.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;
|
}
|
|
.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;
|
}
|
|
.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: 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;
|
}
|
|
.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>
|