<template>
|
<div class="wms-dashboard">
|
<!-- 统计卡片区域 - 绑定响应式数据 -->
|
<el-row :gutter="20" class="stats-card-row">
|
<el-col :span="4">
|
<div class="stats-card">
|
<div class="metric-icon">
|
<el-icon :size="32">
|
<Box />
|
</el-icon>
|
</div>
|
<div class="card-title">总库存</div>
|
<div class="card-value">{{ formatNumber(bigscreendata.totalStockQuantity) }}</div>
|
</div>
|
</el-col>
|
<el-col :span="4">
|
<div class="stats-card">
|
<div class="metric-icon">
|
<el-icon :size="32">
|
<Document />
|
</el-icon>
|
</div>
|
<div class="card-title">待出库订单</div>
|
<div class="card-value">{{ bigscreendata.unOutBoundOrderCount }}</div>
|
</div>
|
</el-col>
|
<el-col :span="4">
|
<div class="stats-card">
|
<div class="metric-icon">
|
<el-icon :size="32">
|
<Download />
|
</el-icon>
|
</div>
|
<div class="card-title">今日入库完成数</div>
|
<div class="card-value">{{ bigscreendata.inboundCount }}</div>
|
</div>
|
</el-col>
|
<el-col :span="4">
|
<div class="stats-card">
|
<div class="metric-icon">
|
<el-icon :size="32">
|
<Upload />
|
</el-icon>
|
</div>
|
<div class="card-title">今日出库完成数</div>
|
<div class="card-value">{{ bigscreendata.outboundCount }}</div>
|
</div>
|
</el-col>
|
<el-col :span="4">
|
<div class="stats-card">
|
<div class="metric-icon">
|
<el-icon :size="32">
|
<Box />
|
</el-icon>
|
</div>
|
<div class="card-title">有货料箱</div>
|
<div class="card-value">{{ formatNumber(bigscreendata.inStockPallet) }}</div>
|
</div>
|
</el-col>
|
<el-col :span="4">
|
<div class="stats-card">
|
<div class="metric-icon">
|
<el-icon :size="32">
|
<Box />
|
</el-icon>
|
</div>
|
<div class="card-title">空箱数量</div>
|
<div class="card-value">{{ formatNumber(bigscreendata.freeStockPallet) }}</div>
|
</div>
|
</el-col>
|
</el-row>
|
|
<!-- 图表区域(第一行) -->
|
<el-row :gutter="20" class="chart-row">
|
<el-col :span="8">
|
<div class="chart-card">
|
<div class="chart-title">库位利用率</div>
|
<div ref="locationRateRef" class="chart-container"></div>
|
</div>
|
</el-col>
|
<!-- 临期信息改为表格 -->
|
<el-col :span="8">
|
<div class="chart-card">
|
<div class="chart-title">物料临期信息</div>
|
<!-- 临期物料表格 -->
|
<div class="expiration-table-container">
|
<el-table
|
:data="expirationTableData"
|
border
|
stripe
|
style="width: 100%;"
|
:empty-text="emptyText"
|
>
|
<el-table-column prop="materielCode" label="物料编码" align="center" />
|
<el-table-column prop="materielName" label="物料名称" align="center" show-overflow-tooltip />
|
<el-table-column prop="batchNo" label="批次号" align="center" />
|
<el-table-column prop="validDate" label="有效期" align="center" />
|
<el-table-column label="临期等级" align="center">
|
<template #default="{ row }">
|
<span :class="getExpireLevelClass(row.expireLevel)">{{ row.expireLevel }}</span>
|
</template>
|
</el-table-column>
|
<el-table-column prop="daysToExpiration" label="临期天数" align="center">
|
<template #default="{ row }">
|
<span :class="row.daysToExpiration < 0 ? 'text-red' : ''">
|
{{ row.daysToExpiration < 0 ? '已过期' : `${row.daysToExpiration}天` }}
|
</span>
|
</template>
|
</el-table-column>
|
<!-- <el-table-column prop="barcode" label="条码" align="center" /> -->
|
<el-table-column prop="stockQuantity" label="库存数量" align="center" />
|
<el-table-column prop="locationCode" label="库位" align="center" />
|
<el-table-column prop="palletCode" label="托盘编号" align="center" />
|
</el-table>
|
</div>
|
</div>
|
</el-col>
|
<el-col :span="8">
|
<div class="chart-card">
|
<div class="chart-title">近7日出入库趋势(图像化走势)</div>
|
<div ref="stockTrendRef" class="chart-container"></div>
|
</div>
|
</el-col>
|
</el-row>
|
|
<!-- 图表区域(第二行) -->
|
<el-row :gutter="20" class="chart-row">
|
</el-row>
|
|
<!-- 表格区域 - 绑定后端作业数据 -->
|
<el-row :gutter="20" class="table-row" width="100%">
|
<el-col :span="24">
|
<div class="table-card">
|
<div class="table-title">实时作业监控</div>
|
<el-table :data="showTaskList" border style="width: 100%;">
|
<el-table-column prop="taskNum" label="任务号" />
|
<el-table-column prop="taskStatus" label="任务状态" >
|
<template #default="{ row }">
|
<span class="task-status" :class="getStatusClass(row.taskStatus)">{{ getTaskStatusText(row.taskStatus) }}</span>
|
</template>
|
</el-table-column>
|
<el-table-column prop="taskType" label="任务类型" >
|
<template #default="{ row }">
|
<span class="task-type" :class="getTypeClass(row.taskType)">{{ getTaskTypeText(row.taskType) }}</span>
|
</template>
|
</el-table-column>
|
<el-table-column prop="palletCode" label="托盘编号" />
|
<el-table-column prop="sourceAddress" label="起点位置"/>
|
<el-table-column prop="targetAddress" label="终点位置"/>
|
<el-table-column prop="createDate" label="创建时间"/>
|
</el-table>
|
<div class="table-pagination">
|
<el-pagination layout="prev, pager, next, jumper" :current-page="1" :total="50" />
|
</div>
|
</div>
|
</el-col>
|
</el-row>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, onMounted, onUnmounted, nextTick, computed } from 'vue';
|
import * as echarts from 'echarts';
|
import http from "@/api/http.js";
|
// 补充缺失的图标导入
|
import { Box, Document, Download, Upload } from '@element-plus/icons-vue';
|
// 补充ElMessage导入
|
import { ElMessage } from 'element-plus';
|
|
// 响应式数据
|
const month = ref('month');
|
const orderType = ref('return');
|
|
// 后端返回数据(响应式)
|
const bigscreendata = ref({
|
totalStockQuantity: 0,
|
unOutBoundOrderCount: 0,
|
dailyCompletionRate: 0,
|
unhandledExceptionCount: 0,
|
locationUtilizationRate: 0,
|
inStockPallet: 0,
|
freeStockPallet: 0,
|
dailyInOutBoundList: [],
|
taskList: [],
|
inboundCount: 0,
|
outboundCount: 0,
|
inventoryLocationDist: [],
|
exceptionTypeTrend: [],
|
nearExpirationList: [] // 改为数组类型,匹配新数据结构
|
});
|
|
// 临期表格数据(计算属性:适配新的数组数据结构)
|
const expirationTableData = computed(() => {
|
// 新数据结构是数组,直接取数组并处理空值
|
const expirationList = bigscreendata.value.nearExpirationList || [];
|
|
// 第一步:去重 - 按 物料编码+批次号+托盘编号+有效期 唯一标识去重
|
const uniqueMap = new Map();
|
expirationList.forEach(item => {
|
// 拼接唯一标识(处理空值,避免因空值导致重复判断错误)
|
const uniqueKey = [
|
item.materielCode || '未知编码',
|
item.batchNo || '未知批次',
|
item.palletCode || '未知托盘',
|
].join('|'); // 用特殊字符分隔,避免字段值拼接后混淆
|
|
// 只保留第一次出现的记录
|
if (!uniqueMap.has(uniqueKey)) {
|
uniqueMap.set(uniqueKey, item);
|
}
|
});
|
|
// 提取去重后的数组
|
const uniqueExpirationList = Array.from(uniqueMap.values());
|
|
// 第二步:遍历去重后的数据,补充临期等级和默认值
|
return uniqueExpirationList.map(item => {
|
// 提取当前物料的临期天数(核心字段)
|
const daysToExpiration = item.daysToExpiration || 0;
|
// 计算临期等级
|
let expireLevel = '';
|
if (daysToExpiration < 0) {
|
expireLevel = '已过期';
|
} else if (daysToExpiration <= 7) {
|
expireLevel = '7天内临期';
|
} else if (daysToExpiration <= 15) {
|
expireLevel = '15天内临期';
|
} else if (daysToExpiration <= 30) {
|
expireLevel = '30天内临期';
|
} else {
|
expireLevel = '30天以上';
|
}
|
|
return {
|
materielCode: item.materielCode || '未知编码',
|
materielName: item.materielName || '未知名称',
|
batchNo: item.batchNo || '未知批次',
|
validDate: item.validDate || '未知有效期',
|
daysToExpiration: daysToExpiration,
|
expireLevel: expireLevel,
|
stockQuantity: item.stockQuantity || 0,
|
locationCode: item.locationCode || '未知库位',
|
palletCode: item.palletCode || '未知托盘',
|
unit: item.unit || 'PCS'
|
};
|
});
|
});
|
|
// 空数据提示文本(修正判断逻辑)
|
const emptyText = computed(() => {
|
const expirationList = bigscreendata.value.nearExpirationList || [];
|
// 数组长度为0时显示空提示,否则不显示(原逻辑写反了)
|
return expirationList.length === 0 ? '暂无临期物料数据' : '';
|
});
|
|
// 获取临期等级样式类
|
const getExpireLevelClass = (level) => {
|
switch(level) {
|
case '已过期': return 'expire-level expired';
|
case '7天内临期': return 'expire-level urgent';
|
case '15天内临期': return 'expire-level warning';
|
case '30天内临期': return 'expire-level normal';
|
case '30天以上': return 'expire-level low';
|
default: return 'expire-level default';
|
}
|
};
|
|
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 getTaskStatusText = (statusNum) => {
|
if (statusNum === undefined || statusNum === null || isNaN(statusNum)) {
|
return "未知状态";
|
}
|
return taskStatusMap[statusNum] || "未知状态";
|
};
|
|
// 表格显示的任务列表(轮播用)
|
const showTaskList = ref([]);
|
const currentTaskIndex = ref(5);
|
let taskCarouselTimer = null;
|
|
// 数字格式化(千分位分隔)
|
const formatNumber = (num) => {
|
if (!num) return '0';
|
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
};
|
|
// 任务列表轮播逻辑
|
const startTaskCarousel = () => {
|
if (taskCarouselTimer) clearInterval(taskCarouselTimer);
|
|
const totalTask = bigscreendata.value.taskList.length;
|
if (totalTask <= 5) {
|
showTaskList.value = [...bigscreendata.value.taskList];
|
return;
|
}
|
|
taskCarouselTimer = setInterval(() => {
|
showTaskList.value.push(bigscreendata.value.taskList[currentTaskIndex.value]);
|
showTaskList.value.shift();
|
currentTaskIndex.value++;
|
if (currentTaskIndex.value >= totalTask) {
|
currentTaskIndex.value = 0;
|
}
|
}, 5000);
|
};
|
|
// 获取任务类型文本
|
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 >= 400) return "status-processing";
|
if (statusNum >= 300) return "status-processing";
|
if (statusNum >= 200) return "status-processing";
|
if (statusNum >= 100) return "status-pending";
|
if (statusNum === 970) return "status-suspended";
|
if (statusNum === 980) return "status-canceled";
|
if (statusNum === 990) return "status-error";
|
|
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 res = await http.get('/api/BigScreen/GetBigGreenData');
|
console.log('大屏数据', res);
|
|
bigscreendata.value = res.data || res;
|
// 数据更新后刷新图表和表格
|
nextTick(() => {
|
const total = bigscreendata.value.taskList.length;
|
showTaskList.value = total >=5
|
? [...bigscreendata.value.taskList.slice(0,5)]
|
: [...bigscreendata.value.taskList];
|
startTaskCarousel();
|
refreshCharts();
|
});
|
} catch (error) {
|
ElMessage.error('数据获取失败,请检查后端接口是否正常');
|
console.error('数据获取失败:', error);
|
}
|
};
|
|
// 备用模拟数据
|
const operationList = ref([
|
{ opNo: 'JW251224001', opType: '入库', operator: '张三', startTime: '15:30:22', status: '处理中' },
|
{ opNo: 'CK251224002', opType: '出库', operator: '李四', startTime: '15:25:10', status: '已完成' },
|
{ opNo: 'PD251224003', opType: '盘点', operator: '王五', startTime: '15:20:05', status: '待确认' },
|
{ opNo: 'SC251224005', opType: '上架', operator: '孙七', startTime: '15:10:18', status: '异常' }
|
]);
|
|
// 图表容器Ref
|
const inventoryPieRef = ref(null);
|
const stockTrendRef = ref(null);
|
const locationRateRef = ref(null);
|
const exceptionTrendRef = ref(null);
|
|
// 图表实例(全局管理)
|
let inventoryPieChart = null;
|
let stockTrendChart = null;
|
let locationRateChart = null;
|
let exceptionTrendChart = null;
|
|
// 初始化库存库位分布饼图
|
const initInventoryPie= () => {
|
if (!inventoryPieRef.value) return;
|
|
if (inventoryPieChart) {
|
inventoryPieChart.dispose();
|
}
|
|
inventoryPieChart = echarts.init(inventoryPieRef.value);
|
const locationData = bigscreendata.value.inventoryLocationDist.length
|
? bigscreendata.value.inventoryLocationDist
|
: [
|
{ value: 48.7, name: '常温区A区', itemStyle: { color: '#5470c6' } },
|
{ value: 29.2, name: '冷藏区B区', itemStyle: { color: '#91cc75' } },
|
{ value: 21.9, name: '保税区C区', itemStyle: { color: '#fac858' } },
|
{ value: 2.2, name: '残次品区D区', itemStyle: { color: '#ee6666' } }
|
];
|
|
const option = {
|
tooltip: {
|
trigger: 'item',
|
formatter: '{a} <br/>{b}: {c}%'
|
},
|
legend: {
|
bottom: 0,
|
left: 'center',
|
data: locationData.map(item => item.name)
|
},
|
series: [{
|
name: '库存库位分布',
|
type: 'pie',
|
radius: ['40%', '70%'],
|
center: ['50%', '40%'],
|
avoidLabelOverlap: false,
|
itemStyle: {
|
borderRadius: 10,
|
borderColor: '#fff',
|
borderWidth: 2
|
},
|
label: {
|
show: false,
|
position: 'center'
|
},
|
emphasis: {
|
label: {
|
show: true,
|
fontSize: 20,
|
fontWeight: 'bold'
|
}
|
},
|
labelLine: {
|
show: false
|
},
|
data: locationData
|
}]
|
};
|
|
inventoryPieChart.setOption(option);
|
return inventoryPieChart;
|
};
|
|
// 初始化近7日出入库趋势图
|
const initStockTrend = () => {
|
if (!stockTrendRef.value) return;
|
|
if (stockTrendChart) {
|
stockTrendChart.dispose();
|
}
|
|
stockTrendChart = echarts.init(stockTrendRef.value);
|
const trendData = bigscreendata.value.dailyInOutBoundList;
|
|
const maxInbound = trendData.length ? Math.max(...trendData.map(item => item.dailyInboundQuantity || 0)) : 0;
|
const maxOutbound = trendData.length ? Math.max(...trendData.map(item => item.dailyOutboundQuantity || 0)) : 0;
|
const maxValue = Math.max(maxInbound, maxOutbound);
|
|
const option = {
|
tooltip: {
|
trigger: 'axis',
|
axisPointer: {
|
type: 'cross'
|
}
|
},
|
legend: {
|
data: ['入库量', '出库量'],
|
top: 10
|
},
|
grid: {
|
left: '3%',
|
right: '4%',
|
bottom: '3%',
|
top: '15%',
|
containLabel: true
|
},
|
xAxis: {
|
type: 'category',
|
boundaryGap: true,
|
data: trendData.map(item => item.date)
|
},
|
yAxis: {
|
type: 'value',
|
name: '数量',
|
min: 0,
|
max: maxValue > 0 ? Math.ceil(maxValue * 1.2) : 10
|
},
|
series: [
|
{
|
name: '入库量',
|
type: 'bar',
|
barWidth: '30%',
|
data: trendData.map(item => item.dailyInboundQuantity),
|
itemStyle: {
|
color: '#52c41a',
|
borderRadius: [4, 4, 0, 0]
|
}
|
},
|
{
|
name: '出库量',
|
type: 'bar',
|
barWidth: '30%',
|
data: trendData.map(item => item.dailyOutboundQuantity),
|
itemStyle: {
|
color: '#1890ff',
|
borderRadius: [4, 4, 0, 0]
|
}
|
}
|
]
|
};
|
|
stockTrendChart.setOption(option);
|
return stockTrendChart;
|
};
|
|
// 初始化库位利用率环形图
|
const initLocationRate = () => {
|
if (!locationRateRef.value) return;
|
|
if (locationRateChart) {
|
locationRateChart.dispose();
|
}
|
|
locationRateChart = echarts.init(locationRateRef.value);
|
const utilizationRate = bigscreendata.value.locationUtilizationRate || 0;
|
const freeRate = 100 - utilizationRate;
|
|
const option = {
|
tooltip: {
|
trigger: 'item',
|
formatter: '{b}: {c}%'
|
},
|
legend: {
|
bottom: 0,
|
left: 'center',
|
data: ['已占用库位', '空闲库位'],
|
textStyle: { fontSize: 12, color: '#666' }
|
},
|
graphic: [
|
{
|
type: 'text',
|
left: 'right',
|
top: '10%',
|
style: {
|
text: `${utilizationRate}%`,
|
fontSize: 24,
|
fontWeight: 'bold',
|
fill: '#333'
|
}
|
},
|
{
|
type: 'text',
|
left: 'right',
|
top: '25%',
|
style: {
|
text: '库位利用率',
|
fontSize: 14,
|
fill: '#666'
|
}
|
}
|
],
|
series: [
|
{
|
type: 'pie',
|
radius: ['50%', '70%'],
|
center: ['40%', '50%'],
|
avoidLabelOverlap: false,
|
label: { show: false },
|
labelLine: { show: false },
|
data: [
|
{ value: utilizationRate, name: '已占用库位', itemStyle: { color: '#1890ff' } },
|
{ value: freeRate, name: '空闲库位', itemStyle: { color: '#e5e9f2' } }
|
]
|
}
|
]
|
};
|
|
locationRateChart.setOption(option);
|
return locationRateChart;
|
};
|
|
// 初始化异常类型统计趋势图
|
const initExceptionTrend = () => {
|
if (!exceptionTrendRef.value) return;
|
|
if (exceptionTrendChart) {
|
exceptionTrendChart.dispose();
|
}
|
|
exceptionTrendChart = echarts.init(exceptionTrendRef.value);
|
const exceptionData = bigscreendata.value.exceptionTypeTrend.length
|
? bigscreendata.value.exceptionTypeTrend
|
: {
|
dates: ['12-18', '12-19', '12-20', '12-21', '12-22', '12-23', '12-24'],
|
stockShort: [10, 11, 9, 12, 10, 13, 12],
|
orderTimeout: [8, 9, 7, 8, 7, 9, 8],
|
locationException: [4, 5, 2, 4, 3, 5, 4],
|
checkDiff: [2, 3, 1, 2, 1, 3, 2]
|
};
|
|
const option = {
|
tooltip: {
|
trigger: 'axis',
|
axisPointer: {
|
type: 'cross'
|
}
|
},
|
legend: {
|
data: ['库存不足', '订单超时', '库位异常', '盘点差异'],
|
top: 10
|
},
|
grid: {
|
left: '3%',
|
right: '4%',
|
bottom: '3%',
|
top: '15%',
|
containLabel: true
|
},
|
xAxis: {
|
type: 'category',
|
boundaryGap: false,
|
data: exceptionData.dates
|
},
|
yAxis: {
|
type: 'value',
|
name: '异常数量'
|
},
|
series: [
|
{
|
name: '库存不足',
|
type: 'line',
|
smooth: true,
|
symbol: 'circle',
|
symbolSize: 8,
|
data: exceptionData.stockShort,
|
itemStyle: { color: '#ff4d4f' },
|
lineStyle: { width: 3 }
|
},
|
{
|
name: '订单超时',
|
type: 'line',
|
smooth: true,
|
symbol: 'circle',
|
symbolSize: 8,
|
data: exceptionData.orderTimeout,
|
itemStyle: { color: '#faad14' },
|
lineStyle: { width: 3 }
|
},
|
{
|
name: '库位异常',
|
type: 'line',
|
smooth: true,
|
symbol: 'circle',
|
symbolSize: 8,
|
data: exceptionData.locationException,
|
itemStyle: { color: '#722ed1' },
|
lineStyle: { width: 3 }
|
},
|
{
|
name: '盘点差异',
|
type: 'line',
|
smooth: true,
|
symbol: 'circle',
|
symbolSize: 8,
|
data: exceptionData.checkDiff,
|
itemStyle: { color: '#13c2c2' },
|
lineStyle: { width: 3 }
|
}
|
]
|
};
|
|
exceptionTrendChart.setOption(option);
|
return exceptionTrendChart;
|
};
|
|
// 刷新所有图表
|
const refreshCharts = () => {
|
const charts = [
|
initStockTrend,
|
initLocationRate
|
];
|
|
charts.forEach(initFunc => {
|
const chart = initFunc();
|
if (chart) {
|
chart.resize();
|
}
|
});
|
};
|
|
// 窗口大小变化监听
|
const handleResize = () => {
|
const charts = [
|
inventoryPieChart,
|
stockTrendChart,
|
locationRateChart,
|
exceptionTrendChart
|
];
|
|
charts.forEach(chart => {
|
if (chart) {
|
chart.resize();
|
}
|
});
|
};
|
|
// 组件挂载时
|
onMounted(() => {
|
// 先获取后端数据
|
fetchBigGreenData();
|
// 初始化图表(确保DOM已渲染)
|
nextTick(() => {
|
initInventoryPie();
|
initStockTrend();
|
initLocationRate();
|
initExceptionTrend();
|
window.addEventListener('resize', handleResize);
|
});
|
});
|
|
// 组件卸载时
|
onUnmounted(() => {
|
const charts = [
|
inventoryPieChart,
|
stockTrendChart,
|
locationRateChart,
|
exceptionTrendChart
|
];
|
|
charts.forEach(chart => {
|
if (chart) {
|
chart.dispose();
|
}
|
});
|
clearInterval(taskCarouselTimer);
|
window.removeEventListener('resize', handleResize);
|
});
|
</script>
|
|
<style scoped>
|
.wms-dashboard {
|
padding: 24px;
|
background: linear-gradient(135deg, #f0f2f5 0%, #e6e9f0 100%);
|
min-height: 100vh;
|
box-sizing: border-box;
|
font-family: "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
}
|
|
.header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 20px;
|
background: white;
|
padding: 15px 20px;
|
border-radius: 8px;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
}
|
|
.stats-card-row,
|
.chart-row,
|
.table-row {
|
margin-bottom: 20px;
|
}
|
|
/* 核心样式:卡片基础样式 */
|
.stats-card, .chart-card, .table-card {
|
background: #fff;
|
border-radius: 8px;
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
|
border: 1px solid #ebeef5;
|
}
|
|
.stats-card {
|
height: 140px;
|
display: flex;
|
flex-direction: column;
|
justify-content: center;
|
align-items: center;
|
padding: 20px 15px;
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
position: relative;
|
overflow: hidden;
|
background: linear-gradient(145deg, #ffffff 0%, #f9fafc 100%);
|
}
|
|
.stats-card:hover {
|
transform: translateY(-6px) scale(1.02);
|
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
|
border-color: #409eff;
|
background: linear-gradient(145deg, #ffffff 0%, #f0f2f5 100%);
|
}
|
|
.metric-icon {
|
width: 56px;
|
height: 56px;
|
border-radius: 16px;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
color: white;
|
margin-bottom: 12px;
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
transition: all 0.3s ease;
|
}
|
|
.card-title {
|
font-size: 15px;
|
color: #606266;
|
margin-bottom: 10px;
|
font-weight: 500;
|
letter-spacing: 0.5px;
|
}
|
|
.card-value {
|
font-size: 32px;
|
font-weight: 700;
|
margin: 8px 0 4px;
|
color: #2c3e50;
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
line-height: 1.2;
|
background: linear-gradient(to right, #409eff, #36cfc9);
|
-webkit-background-clip: text;
|
-webkit-text-fill-color: transparent;
|
background-clip: text;
|
}
|
|
.card-change {
|
margin-top: 3px;
|
}
|
|
.chart-card {
|
height: 400px;
|
padding: 24px;
|
display: flex;
|
flex-direction: column;
|
background: linear-gradient(180deg, #ffffff 0%, #f8f9fa 100%);
|
border: none;
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.06);
|
border-radius: 12px;
|
transition: all 0.3s ease;
|
overflow: hidden;
|
position: relative;
|
}
|
|
.chart-card:hover {
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
transform: translateY(-2px);
|
}
|
|
.chart-container {
|
width: 100%;
|
height: 100%;
|
min-height: 300px;
|
flex: 1;
|
position: relative;
|
border-radius: 8px;
|
background: rgba(255, 255, 255, 0.7);
|
backdrop-filter: blur(5px);
|
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.03);
|
}
|
|
/* 临期物料表格容器样式 */
|
.expiration-table-container {
|
width: 100%;
|
height: 100%;
|
min-height: 300px;
|
flex: 1;
|
overflow-y: auto;
|
}
|
|
/* 临期等级标签样式 */
|
.expire-level {
|
padding: 2px 8px;
|
border-radius: 4px;
|
font-size: 12px;
|
font-weight: 500;
|
}
|
.expire-level.expired {
|
background-color: #fff2f0;
|
color: #ff4d4f;
|
border: 1px solid #ffccc7;
|
}
|
.expire-level.urgent {
|
background-color: #fff7e6;
|
color: #fa8c16;
|
border: 1px solid #ffd591;
|
}
|
.expire-level.warning {
|
background-color: #f6ffed;
|
color: #52c41a;
|
border: 1px solid #b7eb8f;
|
}
|
.expire-level.normal {
|
background-color: #e6f7ff;
|
color: #1890ff;
|
border: 1px solid #91d5ff;
|
}
|
.expire-level.low {
|
background-color: #f0f2f5;
|
color: #666666;
|
border: 1px solid #d9d9d9;
|
}
|
.expire-level.default {
|
background-color: #fafafa;
|
color: #8c8c8c;
|
border: 1px solid #e8e8e8;
|
}
|
|
/* 已过期文字红色 */
|
.text-red {
|
color: #ff4d4f;
|
font-weight: 500;
|
}
|
|
.chart-title,
|
.table-title {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 24px;
|
font-size: 18px;
|
font-weight: 600;
|
color: #2c3e50;
|
padding-left: 12px;
|
border-left: 4px solid #409eff;
|
position: relative;
|
letter-spacing: 0.5px;
|
}
|
|
.view-btn {
|
font-size: 12px;
|
}
|
|
.table-card {
|
padding: 24px;
|
background: #fff;
|
border-radius: 12px;
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.06);
|
border: 1px solid rgba(0, 0, 0, 0.04);
|
overflow: hidden;
|
transition: all 0.3s ease;
|
}
|
|
.table-card:hover {
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
transform: translateY(-2px);
|
}
|
|
.table-pagination {
|
display: flex;
|
justify-content: flex-end;
|
align-items: center;
|
margin-top: 20px;
|
padding-top: 15px;
|
border-top: 1px solid #ebeef5;
|
}
|
|
/* 表格样式优化 */
|
:deep(.el-table) {
|
border-radius: 6px;
|
overflow: hidden;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
}
|
|
:deep(.el-table th) {
|
background-color: #f5f7fa;
|
color: #606266;
|
font-weight: 600;
|
padding: 12px 0;
|
}
|
|
:deep(.el-table td) {
|
padding: 12px 0;
|
}
|
|
:deep(.el-table--border) {
|
border-radius: 6px;
|
}
|
|
:deep(.el-table--border::after) {
|
display: none;
|
}
|
|
:deep(.el-table--group::after) {
|
display: none;
|
}
|
|
:deep(.el-table::before) {
|
display: none;
|
}
|
|
:deep(.el-table__fixed-right::before) {
|
display: none;
|
}
|
|
:deep(.el-table__fixed::before) {
|
display: none;
|
}
|
|
/* 分页器样式优化 */
|
:deep(.el-pagination) {
|
margin-top: 10px;
|
}
|
|
:deep(.el-pagination .btn-prev),
|
:deep(.el-pagination .btn-next),
|
:deep(.el-pagination .el-pager li) {
|
border-radius: 4px;
|
margin: 0 2px;
|
transition: all 0.3s;
|
}
|
|
:deep(.el-pagination .btn-prev:hover),
|
:deep(.el-pagination .btn-next:hover),
|
:deep(.el-pagination .el-pager li:hover) {
|
transform: translateY(-2px);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
}
|
|
:deep(.el-pagination .el-pager li.active) {
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
color: #fff;
|
}
|
|
/* 任务状态样式 */
|
.task-status {
|
display: inline-block;
|
padding: 6px 12px;
|
border-radius: 20px;
|
font-size: 13px;
|
font-weight: 500;
|
text-align: center;
|
min-width: 80px;
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
transition: all 0.2s ease;
|
letter-spacing: 0.5px;
|
}
|
|
.status-pending {
|
background-color: rgba(64, 158, 255, 0.1);
|
color: #409eff;
|
border: 1px solid rgba(64, 158, 255, 0.2);
|
}
|
|
.status-processing {
|
background-color: rgba(103, 194, 58, 0.1);
|
color: #67c23a;
|
border: 1px solid rgba(103, 194, 58, 0.2);
|
}
|
|
.status-completed {
|
background-color: rgba(103, 194, 58, 0.1);
|
color: #67c23a;
|
border: 1px solid rgba(103, 194, 58, 0.2);
|
}
|
|
.status-suspended {
|
background-color: rgba(230, 162, 60, 0.1);
|
color: #e6a23c;
|
border: 1px solid rgba(230, 162, 60, 0.2);
|
}
|
|
.status-canceled {
|
background-color: rgba(144, 147, 153, 0.1);
|
color: #909399;
|
border: 1px solid rgba(144, 147, 153, 0.2);
|
}
|
|
.status-error {
|
background-color: rgba(245, 108, 108, 0.1);
|
color: #f56c6c;
|
border: 1px solid rgba(245, 108, 108, 0.2);
|
}
|
|
.status-unknown {
|
background-color: rgba(144, 147, 153, 0.1);
|
color: #909399;
|
border: 1px solid rgba(144, 147, 153, 0.2);
|
}
|
|
/* 任务类型样式 */
|
.task-type {
|
display: inline-flex;
|
align-items: center;
|
justify-content: center;
|
padding: 5px 12px;
|
border-radius: 16px;
|
font-size: 13px;
|
font-weight: 600;
|
text-align: center;
|
min-width: 70px;
|
position: relative;
|
overflow: hidden;
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
letter-spacing: 0.5px;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
}
|
|
@keyframes pulse {
|
0% {
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
}
|
50% {
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
|
}
|
100% {
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
}
|
}
|
|
.task-type::before {
|
content: "";
|
position: absolute;
|
top: 0;
|
left: 0;
|
width: 100%;
|
height: 100%;
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
transform: translateX(-100%);
|
transition: transform 0.8s cubic-bezier(0.19, 1, 0.22, 1);
|
}
|
|
.task-type:hover::before {
|
transform: translateX(0);
|
}
|
|
.task-type:hover {
|
transform: translateY(-3px);
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
|
}
|
|
.task-type::after {
|
content: "";
|
position: absolute;
|
bottom: 0;
|
left: 0;
|
width: 100%;
|
height: 3px;
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.7), transparent);
|
transform: scaleX(0);
|
transform-origin: center;
|
transition: transform 0.5s cubic-bezier(0.19, 1, 0.22, 1);
|
}
|
|
.task-type:hover::after {
|
transform: scaleX(1);
|
}
|
|
.type-inbound:hover {
|
box-shadow: 0 5px 15px rgba(19, 194, 194, 0.4);
|
}
|
|
.type-outbound:hover {
|
box-shadow: 0 5px 15px rgba(47, 84, 235, 0.4);
|
}
|
|
.type-transfer:hover {
|
box-shadow: 0 5px 15px rgba(250, 140, 22, 0.4);
|
}
|
|
.type-other:hover {
|
box-shadow: 0 5px 15px rgba(114, 46, 209, 0.4);
|
}
|
|
.type-unknown:hover {
|
box-shadow: 0 5px 15px rgba(89, 89, 89, 0.4);
|
}
|
|
.type-inbound {
|
background: linear-gradient(135deg, #13c2c2 0%, #36cfc9 100%);
|
color: white;
|
box-shadow: 0 3px 5px rgba(19, 194, 194, 0.3);
|
animation: pulse 2s infinite;
|
}
|
|
.type-outbound {
|
background: linear-gradient(135deg, #2f54eb 0%, #597ef7 100%);
|
color: white;
|
box-shadow: 0 3px 5px rgba(47, 84, 235, 0.3);
|
animation: pulse 2.5s infinite;
|
}
|
|
.type-transfer {
|
background: linear-gradient(135deg, #fa8c16 0%, #ffc53d 100%);
|
color: white;
|
box-shadow: 0 3px 5px rgba(250, 140, 22, 0.3);
|
animation: pulse 3s infinite;
|
}
|
|
.type-other {
|
background: linear-gradient(135deg, #722ed1 0%, #b37feb 100%);
|
color: white;
|
box-shadow: 0 3px 5px rgba(114, 46, 209, 0.3);
|
animation: pulse 2.2s infinite;
|
}
|
|
.type-unknown {
|
background: linear-gradient(135deg, #595959 0%, #8c8c8c 100%);
|
color: white;
|
box-shadow: 0 3px 5px rgba(89, 89, 89, 0.3);
|
animation: pulse 2.8s infinite;
|
}
|
|
.btn-group {
|
margin-left: 10px;
|
}
|
</style>
|