<template>
|
<div class="dashboard-container">
|
<!-- 顶部标题 -->
|
<div class="header">
|
<h1 class="title">WMS仓库管理系统监控看板</h1>
|
<div class="datetime">{{ currentTime }}</div>
|
</div>
|
|
<!-- 导航菜单 -->
|
<div class="nav-menu">
|
<router-link to="/dashboard" class="nav-item" active-class="active">
|
<el-icon><DataBoard /></el-icon>
|
<span>综合看板</span>
|
</router-link>
|
<router-link to="/warehouse" class="nav-item" active-class="active">
|
<el-icon><Box /></el-icon>
|
<span>仓库监控</span>
|
</router-link>
|
<router-link to="/production" class="nav-item" active-class="active">
|
<el-icon><Operation /></el-icon>
|
<span>生产监控</span>
|
</router-link>
|
<router-link to="/inventory" class="nav-item" active-class="active">
|
<el-icon><Warning /></el-icon>
|
<span>库存预警</span>
|
</router-link>
|
</div>
|
|
<!-- 主要内容区域 -->
|
<div class="main-content">
|
<!-- 第一行:关键指标卡片 -->
|
<div class="metrics-row">
|
<div class="metric-card" v-for="(item, index) in metrics" :key="index">
|
<div class="metric-icon" :style="{ background: item.color }">
|
<el-icon :size="32">
|
<Box v-if="index === 0" />
|
<Download v-else-if="index === 1" />
|
<Upload v-else-if="index === 2" />
|
<List v-else-if="index === 3" />
|
<Box v-else-if="index === 4" />
|
<Box v-else />
|
</el-icon>
|
</div>
|
<div class="metric-content">
|
<div class="metric-value">{{ formatNumber(item.value) }}</div>
|
<div class="metric-label">{{ item.label }}</div>
|
<div class="metric-trend" :class="item.trend > 0 ? 'up' : 'down'">
|
<el-icon>
|
<Top v-if="item.trend > 0" />
|
<Bottom v-else />
|
</el-icon>
|
<span>{{ Math.abs(item.trend) }}%</span>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<!-- 第二行:图表区域 -->
|
<div class="charts-row">
|
<!-- 入库出库趋势 -->
|
<div class="chart-card">
|
<div class="card-title">
|
<span>出入库趋势</span>
|
</div>
|
<div ref="trendChartRef" class="chart-container"></div>
|
</div>
|
|
<!-- 库存分类占比 -->
|
<div class="chart-card">
|
<div class="card-title">
|
<span>库存分类占比</span>
|
</div>
|
<div ref="categoryChartRef" class="chart-container"></div>
|
</div>
|
|
<!-- 物料临期数据 -->
|
<div class="chart-card">
|
<div class="card-title">
|
<span>物料临期数据</span>
|
<span class="task-count">共 {{ nearExpirationList.length }} 条记录</span>
|
</div>
|
<el-table
|
:data="showNearExpirationList"
|
style="width: 100%"
|
:height="250"
|
:empty-text="nearExpirationList.length === 0 ? '暂无临期数据' : ''"
|
>
|
<el-table-column prop="materielCode" label="物料编码" min-width="120" />
|
<el-table-column prop="batchNo" label="批次号" min-width="100" />
|
<el-table-column prop="palletCode" label="托盘编号" min-width="100" />
|
<el-table-column prop="locationCode" label="库位" min-width="100" />
|
<el-table-column prop="stockQuantity" label="库存数量" width="100" />
|
<el-table-column prop="daysToExpiration" label="临期天数" width="100">
|
<template #default="{ row }">
|
<div :class="getExpirationClass(row.daysToExpiration)">
|
{{ row.daysToExpiration }}天
|
</div>
|
</template>
|
</el-table-column>
|
<el-table-column prop="validDate" label="有效期至" width="160" />
|
</el-table>
|
</div>
|
</div>
|
|
<!-- 第三行:数据表格 -->
|
<div class="table-row">
|
<div class="table-card">
|
<div class="card-title">
|
<span>实时作业任务</span>
|
<span class="task-count">共 {{ taskList.length }} 条任务</span>
|
</div>
|
<el-table
|
:data="showTaskList"
|
style="width: 100%"
|
:height="tableHeight"
|
:empty-text="taskList.length === 0 ? '暂无作业任务数据' : ''"
|
>
|
<el-table-column prop="taskNum" label="任务号" min-width="120" />
|
<el-table-column prop="taskStatus" label="任务状态" width="140">
|
<template #default="{ row }">
|
<div class="status-container" :class="getStatusClass(row.taskStatus)">
|
<div class="status-dot"></div>
|
<span class="status-text">{{ getTaskStatusText(row.taskStatus) }}</span>
|
</div>
|
</template>
|
</el-table-column>
|
<el-table-column prop="taskType" label="任务类型" width="120">
|
<template #default="{ row }">
|
<div class="type-container" :class="getTypeClass(row.taskType)">
|
<el-icon class="type-icon">
|
<Box v-if="getTypeClass(row.taskType) === 'type-inbound'" />
|
<Upload v-else-if="getTypeClass(row.taskType) === 'type-outbound'" />
|
<Refresh v-else-if="getTypeClass(row.taskType) === 'type-transfer'" />
|
<Operation v-else />
|
</el-icon>
|
<span class="type-text">{{ getTaskTypeText(row.taskType) }}</span>
|
</div>
|
</template>
|
</el-table-column>
|
<el-table-column prop="palletCode" label="托盘编号" min-width="100" />
|
<el-table-column prop="sourceAddress" label="起点位置" min-width="100"/>
|
<el-table-column prop="targetAddress" label="终点位置" min-width="100"/>
|
<el-table-column prop="createDate" label="创建时间" width="180"/>
|
</el-table>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script>
|
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
|
import * as echarts from 'echarts'
|
import { http } from '@/utils/http'
|
import { formatDateTime } from '@/utils'
|
import { ElMessage } from 'element-plus'
|
// 导入Element Plus图标
|
import {
|
DataBoard, Box, Operation, Warning, Download, Upload, List,
|
Top, Bottom, Refresh, ArrowRight, Clock
|
} from '@element-plus/icons-vue'
|
|
export default {
|
name: 'Dashboard',
|
components: {
|
DataBoard, Box, Operation, Warning, Download, Upload, List,
|
Top, Bottom, Refresh, ArrowRight, Clock
|
},
|
setup() {
|
const currentTime = ref('')
|
const trendChartRef = ref(null)
|
const categoryChartRef = ref(null)
|
const efficiencyChartRef = ref(null)
|
const tableHeight = ref(200)
|
|
// 图表实例引用
|
const trendChart = ref(null)
|
const categoryChart = ref(null)
|
const efficiencyChart = ref(null)
|
|
// 后端返回数据(响应式)
|
const bigscreendata = ref({
|
totalStockQuantity: 0,
|
unOutBoundOrderCount: 0,
|
dailyCompletionRate: 0,
|
unhandledExceptionCount: 0,
|
locationUtilizationRate: 0,
|
inStockPallet: 0,
|
freeStockPallet: 0,
|
dailyInOutBoundList: [],
|
taskList: [],
|
inboundCount: 0,
|
outboundCount: 0,
|
inventoryLocationDist: [],
|
completeTask: []
|
})
|
|
// 任务状态映射
|
const taskStatusMap = {
|
100: "新建",
|
105: "已发送",
|
200: "堆垛机待执行",
|
210: "堆垛机执行中",
|
220: "堆垛机完成",
|
400: "输送线待执行",
|
410: "输送线执行中",
|
420: "输送线完成",
|
300: "AGV待执行",
|
310: "AGV执行中",
|
315: "AGV取货中",
|
320: "AGV待继续执行",
|
325: "AGV放货中",
|
330: "AGV完成",
|
900: "任务完成",
|
970: "任务挂起",
|
980: "任务取消",
|
990: "任务异常",
|
110: "提升机执行中"
|
}
|
|
// 关键指标(响应式)- 修正映射关系
|
const metrics = ref([
|
{
|
label: '待入库订单',
|
value: 0,
|
icon: 'Box',
|
color: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
trend: 8.5
|
},
|
{
|
label: '待出库订单',
|
value: 0,
|
icon: 'Download',
|
color: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
|
trend: 12.3
|
},
|
{
|
label: '今日入库完成数',
|
value: 0,
|
icon: 'Upload',
|
color: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
|
trend: -3.2
|
},
|
{
|
label: '今日出库完成数',
|
value: 0,
|
icon: 'List',
|
color: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
|
trend: 5.7
|
},
|
{
|
label: '有货料箱',
|
value: 0,
|
icon: 'List',
|
color: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)',
|
trend: 2.1
|
},
|
{
|
label: '空箱数量',
|
value: 0,
|
icon: 'List',
|
color: 'linear-gradient(135deg, #8176af 0%, #c0b7e8 100%)',
|
trend: -1.8
|
}
|
])
|
|
// 任务列表相关
|
const taskList = ref([])
|
const showTaskList = ref([])
|
const currentTaskIndex = ref(7) // 初始从第8条开始(前7条默认显示)
|
let taskCarouselTimer = null
|
|
// 临期物料列表相关
|
const nearExpirationList = ref([])
|
const showNearExpirationList = ref([])
|
const currentExpirationIndex = ref(7) // 初始从第8条开始(前7条默认显示)
|
let expirationCarouselTimer = null
|
|
// 自动刷新相关配置
|
const lastInboundToday = ref(0) // 上一次当天入库量
|
const lastOutboundToday = ref(0) // 上一次当天出库量
|
const refreshInterval = ref(5 * 60 * 1000) // 定时刷新间隔(5分钟)
|
const minRefreshGap = ref(30 * 1000) // 最小刷新间隔(防抖,30秒)
|
let lastRefreshTime = ref(0) // 上一次刷新时间
|
let autoRefreshTimer = null // 自动刷新定时器
|
|
// 数字格式化
|
const formatNumber = (num) => {
|
if (num === undefined || num === null || isNaN(num)) return '0';
|
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
}
|
|
// 获取任务状态文本
|
const getTaskStatusText = (statusNum) => {
|
if (statusNum === undefined || statusNum === null || isNaN(statusNum)) {
|
return "未知状态";
|
}
|
return taskStatusMap[statusNum] || `未知状态(${statusNum})`;
|
}
|
|
// 启动任务轮播
|
const startTaskCarousel = () => {
|
// 清除旧定时器
|
if (taskCarouselTimer) clearInterval(taskCarouselTimer);
|
|
const totalTask = taskList.value.length;
|
// 任务数<=7时不轮播
|
if (totalTask <= 7) {
|
showTaskList.value = [...taskList.value];
|
return;
|
}
|
|
// 重置索引
|
currentTaskIndex.value = 7;
|
// 初始显示前7条
|
showTaskList.value = taskList.value.slice(0, 7);
|
|
// 启动轮播
|
taskCarouselTimer = setInterval(() => {
|
const tableElement = document.querySelector('.el-table');
|
if (tableElement) {
|
tableElement.classList.add('flash-effect');
|
setTimeout(() => {
|
tableElement.classList.remove('flash-effect');
|
}, 600);
|
}
|
|
// 新增下1条,删除最前1条(保持7条显示)
|
showTaskList.value.push(taskList.value[currentTaskIndex.value]);
|
showTaskList.value.shift();
|
|
// 循环索引
|
currentTaskIndex.value++;
|
if (currentTaskIndex.value >= totalTask) {
|
currentTaskIndex.value = 0;
|
}
|
}, 5000); // 5秒轮播一次
|
}
|
|
// 停止任务轮播
|
const stopTaskCarousel = () => {
|
if (taskCarouselTimer) {
|
clearInterval(taskCarouselTimer);
|
taskCarouselTimer = null;
|
}
|
}
|
|
// 启动临期物料轮播
|
const startExpirationCarousel = () => {
|
// 清除旧定时器
|
if (expirationCarouselTimer) clearInterval(expirationCarouselTimer);
|
|
const totalExpiration = nearExpirationList.value.length;
|
// 记录数<=7时不轮播
|
if (totalExpiration <= 7) {
|
showNearExpirationList.value = [...nearExpirationList.value];
|
return;
|
}
|
|
// 重置索引
|
currentExpirationIndex.value = 7;
|
// 初始显示前7条
|
showNearExpirationList.value = nearExpirationList.value.slice(0, 7);
|
|
// 启动轮播
|
expirationCarouselTimer = setInterval(() => {
|
// 新增下1条,删除最前1条(保持7条显示)
|
showNearExpirationList.value.push(nearExpirationList.value[currentExpirationIndex.value]);
|
showNearExpirationList.value.shift();
|
|
// 循环索引
|
currentExpirationIndex.value++;
|
if (currentExpirationIndex.value >= totalExpiration) {
|
currentExpirationIndex.value = 0;
|
}
|
}, 5000); // 5秒轮播一次
|
}
|
|
// 停止临期物料轮播
|
const stopExpirationCarousel = () => {
|
if (expirationCarouselTimer) {
|
clearInterval(expirationCarouselTimer);
|
expirationCarouselTimer = null;
|
}
|
}
|
|
// 获取临期天数样式类
|
const getExpirationClass = (days) => {
|
if (days === undefined || days === null || isNaN(days)) return "exp-unknown";
|
if (days <= 0) return "exp-expired"; // 已过期
|
if (days <= 7) return "exp-critical"; // 7天内临期
|
if (days <= 30) return "exp-warning"; // 30天内临期
|
return "exp-normal"; // 正常
|
}
|
|
// 获取任务类型文本
|
const getTaskTypeText = (taskTypeNum) => {
|
if (!taskTypeNum || isNaN(taskTypeNum)) return "未知类型";
|
|
if (taskTypeNum >= 500 && taskTypeNum < 900) return "入库";
|
if (taskTypeNum >= 100 && taskTypeNum < 500) return "出库";
|
if (taskTypeNum >= 900 && taskTypeNum < 1000) return "移库";
|
return "其他作业";
|
}
|
|
// 获取任务状态样式类
|
const getStatusClass = (statusNum) => {
|
if (statusNum === undefined || statusNum === null || isNaN(statusNum)) {
|
return "status-unknown";
|
}
|
|
if (statusNum === 900) return "status-completed"; // 完成
|
if (statusNum === 970) return "status-suspended"; // 挂起
|
if (statusNum === 980) return "status-canceled"; // 取消
|
if (statusNum === 990) return "status-error"; // 异常
|
if (statusNum >= 200 && statusNum < 900) return "status-processing"; // 执行中
|
if (statusNum >= 100 && statusNum < 200) return "status-pending"; // 新建、已发送
|
|
return "status-unknown";
|
}
|
|
// 获取任务类型样式类
|
const getTypeClass = (taskTypeNum) => {
|
if (!taskTypeNum || isNaN(taskTypeNum)) return "type-unknown";
|
|
if (taskTypeNum >= 500 && taskTypeNum < 900) return "type-inbound"; // 入库
|
if (taskTypeNum >= 100 && taskTypeNum < 500) return "type-outbound"; // 出库
|
if (taskTypeNum >= 900 && taskTypeNum < 1000) return "type-transfer"; // 移库
|
|
return "type-other"; // 其他作业
|
}
|
|
// 从后端获取数据
|
const fetchBigGreenData = async () => {
|
try {
|
// 防抖处理:避免短时间内多次请求
|
const now = Date.now();
|
if (now - lastRefreshTime.value < minRefreshGap.value) {
|
return;
|
}
|
lastRefreshTime.value = now;
|
|
const res = await http.get('/api/BigScreen/GetBigGreenData');
|
console.log('大屏数据', res);
|
bigscreendata.value = res.data || res;
|
|
updateMetrics();
|
|
// 更新任务列表
|
taskList.value = Array.isArray(bigscreendata.value.taskList) ? bigscreendata.value.taskList : [];
|
// 重启轮播
|
stopTaskCarousel();
|
startTaskCarousel();
|
|
// 更新临期物料列表
|
nearExpirationList.value = Array.isArray(bigscreendata.value.nearExpirationList) ? bigscreendata.value.nearExpirationList : [];
|
// 重启临期物料轮播
|
stopExpirationCarousel();
|
startExpirationCarousel();
|
|
// 数据加载完成后刷新图表
|
nextTick(() => {
|
refreshCharts(true);
|
});
|
} catch (error) {
|
console.error('获取大屏数据失败:', error);
|
ElMessage.error('获取数据失败,请稍后重试');
|
}
|
};
|
|
// 更新关键指标 - 修正数据映射
|
const updateMetrics = () => {
|
metrics.value[0].value = bigscreendata.value.unInBoundOrderCount || 0; // 待入库订单
|
metrics.value[1].value = bigscreendata.value.unOutBoundOrderCount || 0; // 待出库订单
|
metrics.value[2].value = bigscreendata.value.inboundCount || 0; // 今日入库完成数
|
metrics.value[3].value = bigscreendata.value.outboundCount || 0; // 今日出库完成数
|
metrics.value[4].value = bigscreendata.value.inStockPallet || 0; // 有货料箱
|
metrics.value[5].value = bigscreendata.value.freeStockPallet || 0; // 空箱数量
|
}
|
|
// 更新时间
|
let timer
|
const updateTime = () => {
|
currentTime.value = formatDateTime(new Date())
|
}
|
|
// 初始化趋势图
|
const initTrendChart = () => {
|
if (!trendChartRef.value) return;
|
|
// 销毁旧实例
|
if (trendChart.value) {
|
trendChart.value.dispose();
|
}
|
|
trendChart.value = echarts.init(trendChartRef.value);
|
|
// 直接从后端获取日期和数据
|
const dates = [];
|
const inboundData = [];
|
const outboundData = [];
|
|
if (Array.isArray(bigscreendata.value.dailyInOutBoundList) && bigscreendata.value.dailyInOutBoundList.length > 0) {
|
bigscreendata.value.dailyInOutBoundList.forEach(item => {
|
dates.push(item.date || ''); // 直接使用后端返回的日期
|
inboundData.push(item.dailyInboundQuantity || 0);
|
outboundData.push(item.dailyOutboundQuantity || 0);
|
});
|
}
|
|
const hasData = dates.length > 0;
|
|
const option = {
|
backgroundColor: 'transparent',
|
tooltip: {
|
trigger: 'axis',
|
textStyle: { color: '#fff', fontSize: 12 },
|
backgroundColor: 'rgba(0,0,0,0.7)'
|
},
|
legend: {
|
data: ['入库量', '出库量'],
|
textStyle: { color: '#e0e0e0', fontSize: 12 }
|
},
|
grid: {
|
left: '3%',
|
right: '4%',
|
bottom: '3%',
|
top: '15%',
|
containLabel: true
|
},
|
xAxis: {
|
type: 'category',
|
boundaryGap: false,
|
data: hasData ? dates : ['暂无数据'],
|
axisLine: { lineStyle: { color: '#666' } },
|
axisLabel: { color: '#e0e0e0', fontSize: 11 }
|
},
|
yAxis: {
|
type: 'value',
|
axisLine: { lineStyle: { color: '#666' } },
|
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } },
|
axisLabel: { color: '#e0e0e0', fontSize: 11 },
|
min: 0
|
},
|
series: [
|
{
|
name: '入库量',
|
type: 'line',
|
smooth: true,
|
data: hasData ? inboundData : [0],
|
itemStyle: { color: '#5470c6' },
|
lineStyle: { color: '#5470c6' },
|
areaStyle: {
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: 'rgba(84, 112, 198, 0.5)' },
|
{ offset: 1, color: 'rgba(84, 112, 198, 0)' }
|
])
|
},
|
showSymbol: hasData,
|
symbol: 'circle',
|
symbolSize: 6
|
},
|
{
|
name: '出库量',
|
type: 'line',
|
smooth: true,
|
data: hasData ? outboundData : [0],
|
itemStyle: { color: '#91cc75' },
|
lineStyle: { color: '#91cc75' },
|
areaStyle: {
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: 'rgba(145, 204, 117, 0.5)' },
|
{ offset: 1, color: 'rgba(145, 204, 117, 0)' }
|
])
|
},
|
showSymbol: hasData,
|
symbol: 'circle',
|
symbolSize: 6
|
}
|
]
|
};
|
|
trendChart.value.setOption(option);
|
return trendChart.value;
|
}
|
|
// 初始化分类占比图
|
const initCategoryChart = () => {
|
if (!categoryChartRef.value) return;
|
|
// 销毁旧实例
|
if (categoryChart.value) {
|
categoryChart.value.dispose();
|
}
|
|
categoryChart.value = echarts.init(categoryChartRef.value);
|
|
const chartData = Array.isArray(bigscreendata.value.inventoryLocationDist) && bigscreendata.value.inventoryLocationDist.length > 0
|
? bigscreendata.value.inventoryLocationDist
|
: [
|
{ value: 3580, name: '原材料', itemStyle: { color: '#5470c6' } },
|
{ value: 2840, name: '半成品', itemStyle: { color: '#91cc75' } },
|
{ value: 4120, name: '成品', itemStyle: { color: '#fac858' } },
|
{ value: 2040, name: '辅料', itemStyle: { color: '#ee6666' } }
|
];
|
|
const option = {
|
backgroundColor: 'transparent',
|
tooltip: {
|
trigger: 'item',
|
textStyle: { color: '#fff', fontSize: 12 },
|
backgroundColor: 'rgba(0,0,0,0.7)',
|
formatter: '{b}: {c} ({d}%)'
|
},
|
legend: {
|
orient: 'vertical',
|
right: '10%',
|
top: 'center',
|
textStyle: { color: '#e0e0e0', fontSize: 12 }
|
},
|
series: [
|
{
|
name: '库存分类',
|
type: 'pie',
|
radius: ['40%', '70%'],
|
center: ['35%', '50%'],
|
avoidLabelOverlap: false,
|
itemStyle: {
|
borderRadius: 10,
|
borderColor: '#0a0e27',
|
borderWidth: 2
|
},
|
label: { show: false },
|
emphasis: {
|
label: { show: true, fontSize: 14, fontWeight: 'bold', color: '#fff' }
|
},
|
data: chartData
|
}
|
]
|
};
|
|
categoryChart.value.setOption(option);
|
return categoryChart.value;
|
}
|
|
// 初始化效率统计图
|
const initEfficiencyChart = () => {
|
if (!efficiencyChartRef.value) {
|
console.warn('效率图表容器不存在!');
|
return;
|
}
|
|
// 确保容器有高度
|
efficiencyChartRef.value.style.height = '250px';
|
efficiencyChartRef.value.style.width = '100%';
|
|
// 销毁旧实例
|
if (efficiencyChart.value) {
|
efficiencyChart.value.dispose();
|
}
|
|
// 初始化 ECharts
|
efficiencyChart.value = echarts.init(efficiencyChartRef.value);
|
|
// 数据处理
|
const taskData = {
|
'入库': 0,
|
'出库': 0
|
};
|
|
if (Array.isArray(bigscreendata.value.completeTask)) {
|
bigscreendata.value.completeTask.forEach(item => {
|
if (item.taskType && typeof item.count === 'number') {
|
taskData[item.taskType] = item.count;
|
}
|
});
|
}
|
|
const hasData = taskData.入库 > 0 || taskData.出库 > 0;
|
|
const option = {
|
backgroundColor: 'transparent',
|
tooltip: {
|
trigger: 'axis',
|
axisPointer: { type: 'shadow' },
|
textStyle: { color: '#fff', fontSize: 12 },
|
backgroundColor: 'rgba(0,0,0,0.7)',
|
formatter: params => {
|
return params.map(p => `${p.seriesName}: ${p.value || 0} 单`).join('<br/>');
|
}
|
},
|
grid: {
|
left: '5%',
|
right: '5%',
|
bottom: '10%',
|
top: '15%',
|
containLabel: true
|
},
|
xAxis: {
|
type: 'category',
|
data: hasData ? ['入库作业', '出库作业'] : ['暂无数据'],
|
axisLine: { lineStyle: { color: '#666' } },
|
axisLabel: { color: '#e0e0e0', fontSize: 11 }
|
},
|
yAxis: {
|
type: 'value',
|
name: '完成数量(单)',
|
nameTextStyle: { color: '#e0e0e0', fontSize: 12 },
|
axisLine: { lineStyle: { color: '#666' } },
|
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } },
|
axisLabel: { color: '#e0e0e0', fontSize: 11 },
|
min: 0
|
},
|
series: [
|
{
|
name: '作业数量',
|
data: hasData ? [taskData['入库'], taskData['出库']] : [0],
|
type: 'bar',
|
barWidth: '40%',
|
itemStyle: {
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: '#83bff6' },
|
{ offset: 1, color: '#188df0' }
|
]),
|
borderRadius: [5, 5, 0, 0]
|
},
|
label: {
|
show: hasData,
|
position: 'top',
|
color: '#ffffff',
|
fontSize: 12,
|
fontWeight: 600
|
}
|
}
|
]
|
};
|
|
efficiencyChart.value.setOption(option, true);
|
console.log('效率图表渲染完成,数据:', taskData);
|
};
|
|
// 刷新图表数据
|
const refreshCharts = (isDataChange = false) => {
|
// 数据变化时添加闪烁效果
|
if (isDataChange) {
|
const chartElements = [trendChartRef.value, categoryChartRef.value, efficiencyChartRef.value];
|
chartElements.forEach(el => {
|
if (el) {
|
el.classList.add('flash-effect');
|
setTimeout(() => el.classList.remove('flash-effect'), 600);
|
}
|
});
|
}
|
|
// 重新初始化图表
|
initTrendChart();
|
initCategoryChart();
|
initEfficiencyChart();
|
}
|
|
// 手动刷新数据
|
const refreshData = () => {
|
fetchBigGreenData();
|
}
|
|
// 启动自动刷新
|
const startAutoRefresh = () => {
|
if (autoRefreshTimer) clearInterval(autoRefreshTimer);
|
autoRefreshTimer = setInterval(() => {
|
console.log('定时刷新数据:', new Date().toLocaleString());
|
fetchBigGreenData();
|
}, refreshInterval.value);
|
}
|
|
// 停止自动刷新
|
const stopAutoRefresh = () => {
|
if (autoRefreshTimer) {
|
clearInterval(autoRefreshTimer);
|
autoRefreshTimer = null;
|
}
|
}
|
|
// 窗口大小改变时重绘图表和调整表格高度
|
const handleResize = () => {
|
try {
|
const windowHeight = window.innerHeight;
|
const headerHeight = 60;
|
const navHeight = 50;
|
const metricsHeight = 120;
|
const chartsHeight = 300;
|
const padding = 80;
|
|
const availableHeight = windowHeight - headerHeight - navHeight - metricsHeight - chartsHeight - padding;
|
tableHeight.value = Math.max(200, Math.min(availableHeight, 400));
|
|
// 重绘图表
|
const charts = [trendChart.value, categoryChart.value, efficiencyChart.value];
|
charts.forEach(chart => {
|
if (chart) chart.resize();
|
});
|
} catch (error) {
|
console.warn('图表重绘时出错:', error);
|
}
|
}
|
|
onMounted(() => {
|
// 初始化时间
|
updateTime();
|
timer = setInterval(updateTime, 1000);
|
|
// 初始化数据
|
fetchBigGreenData();
|
|
nextTick(() => {
|
handleResize();
|
startAutoRefresh(); // 启动自动刷新
|
});
|
|
// 监听窗口大小变化
|
window.addEventListener('resize', handleResize);
|
});
|
|
onUnmounted(() => {
|
// 清除所有定时器
|
clearInterval(timer);
|
stopTaskCarousel();
|
stopExpirationCarousel();
|
stopAutoRefresh();
|
|
// 移除窗口监听
|
window.removeEventListener('resize', handleResize);
|
|
// 销毁图表实例
|
try {
|
if (trendChart.value) trendChart.value.dispose();
|
if (categoryChart.value) categoryChart.value.dispose();
|
if (efficiencyChart.value) efficiencyChart.value.dispose();
|
} catch (error) {
|
console.warn('图表销毁时出错:', error);
|
}
|
});
|
|
// 监听任务列表变化,自动重启轮播
|
watch(taskList, () => {
|
stopTaskCarousel();
|
startTaskCarousel();
|
}, { deep: true });
|
|
return {
|
currentTime,
|
metrics,
|
taskList,
|
showTaskList,
|
nearExpirationList,
|
showNearExpirationList,
|
trendChartRef,
|
categoryChartRef,
|
efficiencyChartRef,
|
tableHeight,
|
formatNumber,
|
getTaskTypeText,
|
getTaskStatusText,
|
getStatusClass,
|
getTypeClass,
|
getExpirationClass,
|
refreshData
|
};
|
}
|
};
|
</script>
|
|
<style scoped>
|
.dashboard-container {
|
width: 100%;
|
height: 100vh;
|
padding: 20px;
|
display: flex;
|
flex-direction: column;
|
gap: 15px;
|
overflow-y: auto;
|
background-color: #0a0e27;
|
}
|
|
.header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
padding: 0 20px;
|
height: 60px;
|
background: linear-gradient(90deg, rgba(30, 58, 138, 0.5) 0%, rgba(30, 58, 138, 0.1) 100%);
|
border-radius: 10px;
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
}
|
|
.title {
|
font-size: 24px;
|
font-weight: bold;
|
background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
|
-webkit-background-clip: text;
|
-webkit-text-fill-color: transparent;
|
margin: 0;
|
}
|
|
.datetime {
|
font-size: 16px;
|
color: #4facfe;
|
font-weight: 600;
|
font-family: 'Consolas', monospace;
|
}
|
|
.nav-menu {
|
display: flex;
|
gap: 15px;
|
padding: 0 20px;
|
}
|
|
.nav-item {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
padding: 10px 25px;
|
background: rgba(255, 255, 255, 0.05);
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
border-radius: 8px;
|
color: #e0e0e0;
|
text-decoration: none;
|
transition: all 0.3s;
|
cursor: pointer;
|
}
|
|
.nav-item:hover {
|
background: rgba(79, 172, 254, 0.2);
|
border-color: #4facfe;
|
transform: translateY(-1px);
|
}
|
|
.nav-item.active {
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
border-color: transparent;
|
color: #fff;
|
box-shadow: 0 4px 12px rgba(79, 172, 254, 0.3);
|
}
|
|
.main-content {
|
flex: 1;
|
display: flex;
|
flex-direction: column;
|
gap: 15px;
|
}
|
|
/* 关键指标卡片 - 优化网格布局和样式对比度 */
|
.metrics-row {
|
display: grid;
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
gap: 15px;
|
}
|
|
.metric-card {
|
display: flex;
|
align-items: center;
|
gap: 20px;
|
padding: 20px;
|
background: rgba(255, 255, 255, 0.05);
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
border-radius: 10px;
|
transition: all 0.3s;
|
}
|
|
.metric-card:hover {
|
transform: translateY(-5px);
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
border-color: rgba(79, 172, 254, 0.5);
|
}
|
|
.metric-icon {
|
width: 60px;
|
height: 60px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
border-radius: 12px;
|
color: #fff;
|
}
|
|
.metric-content {
|
flex: 1;
|
}
|
|
.metric-value {
|
font-size: 32px;
|
font-weight: 700;
|
color: #ffffff;
|
margin-bottom: 5px;
|
text-shadow: 0 0 8px rgba(255,255,255,0.2);
|
}
|
|
.metric-label {
|
font-size: 14px;
|
color: #cccccc;
|
margin-bottom: 5px;
|
font-weight: 500;
|
}
|
|
.metric-trend {
|
display: flex;
|
align-items: center;
|
gap: 5px;
|
font-size: 13px;
|
font-weight: 600;
|
}
|
|
.metric-trend.up {
|
color: #67c23a;
|
}
|
|
.metric-trend.down {
|
color: #f56c6c;
|
}
|
|
/* 图表区域 - 优化布局和样式 */
|
.charts-row {
|
display: grid;
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
gap: 15px;
|
}
|
|
.chart-card {
|
padding: 20px;
|
background: rgba(255, 255, 255, 0.05);
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
border-radius: 10px;
|
display: flex;
|
flex-direction: column;
|
}
|
|
.card-title {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 15px;
|
font-size: 16px;
|
font-weight: 600;
|
color: #4facfe;
|
}
|
|
.task-count {
|
font-size: 12px;
|
color: #cccccc;
|
font-weight: normal;
|
}
|
|
.chart-container {
|
flex: 1;
|
width: 100%;
|
min-height: 250px;
|
}
|
|
/* 表格区域 */
|
.table-row {
|
flex: 1;
|
}
|
|
.table-card {
|
height: 100%;
|
padding: 20px;
|
background: rgba(255, 255, 255, 0.05);
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
border-radius: 10px;
|
display: flex;
|
flex-direction: column;
|
}
|
|
/* Element Plus Table 样式深度优化 */
|
:deep(.el-table) {
|
background: transparent !important;
|
color: #e0e0e0;
|
font-size: 13px;
|
}
|
|
:deep(.el-table th) {
|
background: rgba(255, 255, 255, 0.15) !important;
|
color: #ffffff !important;
|
border-color: rgba(255, 255, 255, 0.2) !important;
|
font-weight: 600;
|
padding: 10px 0;
|
}
|
|
:deep(.el-table td) {
|
border-color: rgba(255, 255, 255, 0.1) !important;
|
color: #e0e0e0 !important;
|
padding: 12px 0;
|
}
|
|
:deep(.el-table tr) {
|
background: transparent !important;
|
}
|
|
:deep(.el-table__row:hover td) {
|
background: rgba(255, 255, 255, 0.08) !important;
|
}
|
|
:deep(.el-table--border) {
|
border: 1px solid rgba(255,255,255,0.15) !important;
|
}
|
|
:deep(.el-table__empty-text) {
|
color: #cccccc !important;
|
}
|
|
/* 任务状态样式 - 高对比度优化 */
|
.status-container {
|
display: flex;
|
align-items: center;
|
padding: 4px 10px;
|
border-radius: 20px;
|
position: relative;
|
overflow: hidden;
|
font-size: 12px;
|
font-weight: 600;
|
}
|
|
.status-dot {
|
width: 8px;
|
height: 8px;
|
border-radius: 50%;
|
margin-right: 6px;
|
position: relative;
|
}
|
|
.status-dot::after {
|
content: '';
|
position: absolute;
|
width: 100%;
|
height: 100%;
|
border-radius: 50%;
|
animation: pulse 2s infinite;
|
}
|
|
.status-text {
|
white-space: nowrap;
|
}
|
|
/* 不同状态的颜色 - 深色主题高对比度 */
|
.status-completed {
|
background: rgba(103, 194, 58, 0.15);
|
color: #67c23a;
|
}
|
|
.status-completed .status-dot {
|
background: #67c23a;
|
}
|
|
.status-completed .status-dot::after {
|
background: rgba(103, 194, 58, 0.5);
|
}
|
|
.status-processing {
|
background: rgba(64, 158, 255, 0.15);
|
color: #409eff;
|
}
|
|
.status-processing .status-dot {
|
background: #409eff;
|
}
|
|
.status-processing .status-dot::after {
|
background: rgba(64, 158, 255, 0.5);
|
}
|
|
.status-pending {
|
background: rgba(144, 147, 153, 0.15);
|
color: #909399;
|
}
|
|
.status-pending .status-dot {
|
background: #909399;
|
}
|
|
.status-pending .status-dot::after {
|
background: rgba(144, 147, 153, 0.5);
|
}
|
|
.status-suspended {
|
background: rgba(230, 162, 60, 0.15);
|
color: #e6a23c;
|
}
|
|
.status-suspended .status-dot {
|
background: #e6a23c;
|
}
|
|
.status-suspended .status-dot::after {
|
background: rgba(230, 162, 60, 0.5);
|
}
|
|
.status-canceled {
|
background: rgba(144, 147, 153, 0.15);
|
color: #909399;
|
}
|
|
.status-canceled .status-dot {
|
background: #909399;
|
}
|
|
.status-canceled .status-dot::after {
|
background: rgba(144, 147, 153, 0.5);
|
}
|
|
.status-error {
|
background: rgba(245, 108, 108, 0.15);
|
color: #f56c6c;
|
}
|
|
.status-error .status-dot {
|
background: #f56c6c;
|
}
|
|
.status-error .status-dot::after {
|
background: rgba(245, 108, 108, 0.5);
|
}
|
|
.status-unknown {
|
background: rgba(75, 85, 99, 0.15);
|
color: #9ca3af;
|
}
|
|
.status-unknown .status-dot {
|
background: #9ca3af;
|
}
|
|
.status-unknown .status-dot::after {
|
background: rgba(156, 163, 175, 0.5);
|
}
|
|
/* 任务类型样式 - 高对比度优化 */
|
.type-container {
|
display: flex;
|
align-items: center;
|
padding: 4px 10px;
|
border-radius: 20px;
|
position: relative;
|
font-size: 12px;
|
font-weight: 600;
|
}
|
|
.type-icon {
|
margin-right: 6px;
|
font-size: 14px;
|
}
|
|
.type-text {
|
white-space: nowrap;
|
}
|
|
/* 不同类型的颜色 */
|
.type-inbound {
|
background: rgba(103, 194, 58, 0.15);
|
color: #67c23a;
|
}
|
|
.type-outbound {
|
background: rgba(230, 162, 60, 0.15);
|
color: #e6a23c;
|
}
|
|
.type-transfer {
|
background: rgba(64, 158, 255, 0.15);
|
color: #409eff;
|
}
|
|
.type-other {
|
background: rgba(144, 147, 153, 0.15);
|
color: #909399;
|
}
|
|
.type-unknown {
|
background: rgba(75, 85, 99, 0.15);
|
color: #9ca3af;
|
}
|
|
/* 临期天数样式 */
|
.exp-expired {
|
color: #f56c6c;
|
font-weight: 600;
|
}
|
|
.exp-critical {
|
color: #e6a23c;
|
font-weight: 600;
|
}
|
|
.exp-warning {
|
color: #f0ad4e;
|
font-weight: 500;
|
}
|
|
.exp-normal {
|
color: #67c23a;
|
font-weight: 500;
|
}
|
|
.exp-unknown {
|
color: #909399;
|
font-weight: 400;
|
}
|
|
/* 动画效果 */
|
@keyframes pulse {
|
0% {
|
transform: scale(1);
|
opacity: 1;
|
}
|
50% {
|
transform: scale(1.5);
|
opacity: 0.3;
|
}
|
100% {
|
transform: scale(1);
|
opacity: 1;
|
}
|
}
|
|
/* 闪烁效果(表格和图表刷新时) */
|
.flash-effect {
|
animation: flash 0.6s ease-in-out;
|
}
|
|
@keyframes flash {
|
0% {
|
box-shadow: 0 0 0 rgba(79, 172, 254, 0);
|
}
|
20% {
|
box-shadow: 0 0 15px rgba(79, 172, 254, 0.7);
|
}
|
40% {
|
box-shadow: 0 0 0 rgba(79, 172, 254, 0);
|
}
|
60% {
|
box-shadow: 0 0 15px rgba(79, 172, 254, 0.7);
|
}
|
80% {
|
box-shadow: 0 0 0 rgba(79, 172, 254, 0);
|
}
|
100% {
|
box-shadow: 0 0 0 rgba(79, 172, 254, 0);
|
}
|
}
|
|
/* 响应式适配增强 */
|
@media screen and (max-width: 1920px) {
|
.metric-value {
|
font-size: 28px;
|
}
|
|
.chart-container {
|
min-height: 220px;
|
}
|
}
|
|
@media screen and (max-width: 1600px) {
|
.title {
|
font-size: 20px;
|
}
|
|
.metric-value {
|
font-size: 24px;
|
}
|
|
.chart-container {
|
min-height: 200px;
|
}
|
}
|
|
@media screen and (max-width: 1366px) {
|
.dashboard-container {
|
padding: 15px;
|
}
|
|
.header {
|
height: 50px;
|
padding: 0 15px;
|
}
|
|
.title {
|
font-size: 18px;
|
}
|
|
.nav-item {
|
padding: 8px 20px;
|
font-size: 14px;
|
}
|
|
.metric-card {
|
padding: 15px;
|
}
|
|
.metric-value {
|
font-size: 22px;
|
}
|
|
.chart-card {
|
padding: 15px;
|
}
|
|
.chart-container {
|
min-height: 180px;
|
}
|
}
|
|
@media screen and (max-width: 768px) {
|
.dashboard-container {
|
padding: 10px;
|
}
|
|
.header {
|
flex-direction: column;
|
height: auto;
|
padding: 10px;
|
gap: 10px;
|
}
|
|
.title {
|
font-size: 16px;
|
}
|
|
.datetime {
|
font-size: 14px;
|
}
|
|
.nav-menu {
|
gap: 8px;
|
padding: 0;
|
flex-wrap: wrap;
|
}
|
|
.nav-item {
|
padding: 6px 12px;
|
font-size: 12px;
|
flex: 1;
|
min-width: 80px;
|
justify-content: center;
|
flex-direction: column;
|
gap: 4px;
|
}
|
|
.metric-card {
|
padding: 12px;
|
flex-direction: column;
|
text-align: center;
|
}
|
|
.metric-icon {
|
width: 45px;
|
height: 45px;
|
}
|
|
.metric-trend {
|
justify-content: center;
|
}
|
}
|
</style>
|