<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">{{ bigscreendata.unInBoundOrderCount }}</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="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="table-row" width="100%">
|
<!-- 左侧:当天正常作业单据 -->
|
<el-col :span="12">
|
<div class="table-card">
|
<div class="table-title">实时作业监控(正常单据)</div>
|
<el-table
|
:data="normalShowTaskList"
|
border
|
style="width: 100%;"
|
:empty-text="normalShowTaskList.length === 0 ? '暂无正常作业数据' : ''"
|
>
|
<el-table-column prop="upperOrderNo" label="单据编号" />
|
<el-table-column label="单据状态" >
|
<template #default="{ row }">
|
<span class="task-status" :class="getStatusClass(row.taskStatus)">
|
{{ getTaskStatusText(row.taskStatus, row.taskType) }}
|
</span>
|
</template>
|
</el-table-column>
|
<el-table-column label="单据类型" >
|
<template #default="{ row }">
|
<span class="task-type" :class="getTypeClass(row.taskType)">
|
{{ getTaskTypeText(row) }}
|
</span>
|
</template>
|
</el-table-column>
|
<el-table-column label="回传MES状态" >
|
<template #default="{ row }">
|
<span class="task-status" :class="getMESStatusClass(row.returnToMESStatus)">
|
{{ getMESStatusText(row.returnToMESStatus) }}
|
</span>
|
</template>
|
</el-table-column>
|
<el-table-column prop="factoryArea" label="厂区" />
|
<el-table-column prop="modifier" label="修改人" />
|
<el-table-column prop="createDate" label="创建时间"/>
|
<el-table-column prop="modifyDate" label="修改时间"/>
|
</el-table>
|
<div class="table-pagination">
|
<el-pagination
|
layout="prev, pager, next, jumper"
|
v-model:current-page="normalCurrentPage"
|
:page-size="5"
|
:total="normalTaskList.length"
|
@current-change="handleNormalPageChange"
|
/>
|
</div>
|
</div>
|
</el-col>
|
<!-- 右侧:近3天回传失败/部分失败单据 -->
|
<el-col :span="12">
|
<div class="table-card">
|
<div class="table-title">实时作业监控(回传失败)</div>
|
<el-table
|
:data="failShowTaskList"
|
border
|
style="width: 100%;"
|
:empty-text="failShowTaskList.length === 0 ? '暂无回传失败数据' : ''"
|
>
|
<el-table-column prop="upperOrderNo" label="单据编号" />
|
<el-table-column label="单据状态" >
|
<template #default="{ row }">
|
<span class="task-status" :class="getStatusClass(row.taskStatus)">
|
{{ getTaskStatusText(row.taskStatus, row.taskType) }}
|
</span>
|
</template>
|
</el-table-column>
|
<el-table-column label="单据类型" >
|
<template #default="{ row }">
|
<span class="task-type" :class="getTypeClass(row.taskType)">
|
{{ getTaskTypeText(row) }}
|
</span>
|
</template>
|
</el-table-column>
|
<el-table-column label="回传MES状态" >
|
<template #default="{ row }">
|
<span class="task-status" :class="getMESStatusClass(row.returnToMESStatus)">
|
{{ getMESStatusText(row.returnToMESStatus) }}
|
</span>
|
</template>
|
</el-table-column>
|
<el-table-column prop="remark" label="失败原因" show-overflow-tooltip />
|
<el-table-column prop="factoryArea" label="厂区" />
|
<el-table-column prop="modifier" label="修改人" />
|
<el-table-column prop="createDate" label="创建时间"/>
|
<el-table-column prop="modifyDate" label="修改时间"/>
|
</el-table>
|
<div class="table-pagination">
|
<el-pagination
|
layout="prev, pager, next, jumper"
|
v-model:current-page="failCurrentPage"
|
:page-size="5"
|
:total="failTaskList.length"
|
@current-change="handleFailPageChange"
|
/>
|
</div>
|
</div>
|
</el-col>
|
</el-row>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, onMounted, onUnmounted, nextTick, computed, watch } from 'vue';
|
import * as echarts from 'echarts';
|
import http from "@/api/http.js";
|
import { Box, Document, Download, Upload } from '@element-plus/icons-vue';
|
import { ElMessage } from 'element-plus';
|
|
// 响应式数据
|
const bigscreendata = ref({
|
totalStockQuantity: 0,
|
unOutBoundOrderCount: 0,
|
unInBoundOrderCount:0,
|
dailyCompletionRate: 0,
|
unhandledExceptionCount: 0,
|
locationUtilizationRate: 0,
|
inStockPallet: 0,
|
freeStockPallet: 0,
|
dailyInOutBoundList: [],
|
completeTask: [], // 后端返回的任务数据(当天正常+近3天失败)
|
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
|
};
|
});
|
});
|
|
const emptyText = computed(() => {
|
const expirationList = bigscreendata.value.nearExpirationList || [];
|
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 inboundBusinessTypeMap = {
|
11: "采购入库", 12: "杂收单", 13: "生产退料单",
|
14: "外协退料单", 15: "销售退库单", 3: "调拨入库单"
|
};
|
const outboundBusinessTypeMap = {
|
21: "工单领料出库单", 22: "杂发单", 23: "退货单",
|
24: "销售出库单", 25: "外协领料申请单", 2: "调拨出库单"
|
};
|
|
// 拆分:当天正常作业单据
|
const normalTaskList = computed(() => {
|
const taskData = (bigscreendata.value.completeTask[0] || {});
|
const inboundOrders = taskData.inboundOrders || [];
|
const outboundOrders = taskData.outboundOrders || [];
|
|
// 格式化入库订单
|
const formattedInbound = inboundOrders.map(item => ({
|
upperOrderNo: item.upperOrderNo || item.inboundOrderNo,
|
taskStatus: item.orderStatus || 0,
|
taskType: 'inbound',
|
businessType: item.businessType || '',
|
returnToMESStatus: item.returnToMESStatus || 0,
|
factoryArea: item.factoryArea || '',
|
modifier: item.modifier || '',
|
createDate: item.createDate || '',
|
modifyDate: item.modifyDate || '',
|
remark: item.remark || ''
|
}));
|
|
// 格式化出库订单
|
const formattedOutbound = outboundOrders.map(item => ({
|
upperOrderNo: item.upperOrderNo || item.orderNo,
|
taskStatus: item.orderStatus || 0,
|
taskType: 'outbound',
|
businessType: item.businessType || '',
|
returnToMESStatus: item.returnToMESStatus || 0,
|
factoryArea: item.factoryArea || '',
|
modifier: item.modifier || '',
|
createDate: item.createDate || '',
|
modifyDate: item.modifyDate || '',
|
remark: item.remark || ''
|
}));
|
|
return [...formattedInbound, ...formattedOutbound];
|
});
|
|
// 拆分:近3天回传失败/部分失败单据
|
const failTaskList = computed(() => {
|
const taskData = (bigscreendata.value.completeTask[0] || {});
|
const inboundFailOrders = taskData.inboundReturnFailOrders || [];
|
const outboundFailOrders = taskData.outboundReturnFailOrders || [];
|
|
const formattedInboundFail = inboundFailOrders.map(item => ({
|
upperOrderNo: item.upperOrderNo || item.inboundOrderNo,
|
taskStatus: item.orderStatus || 0,
|
taskType: 'inbound',
|
businessType: item.businessType || '',
|
returnToMESStatus: item.returnToMESStatus || 0,
|
factoryArea: item.factoryArea || '',
|
modifier: item.modifier || '',
|
createDate: item.createDate || '',
|
modifyDate: item.modifyDate || '',
|
remark: item.remark || ''
|
}));
|
|
const formattedOutboundFail = outboundFailOrders.map(item => ({
|
upperOrderNo: item.upperOrderNo || item.orderNo,
|
taskStatus: item.orderStatus || 0,
|
taskType: 'outbound',
|
businessType: item.businessType || '',
|
returnToMESStatus: item.returnToMESStatus || 0,
|
factoryArea: item.factoryArea || '',
|
modifier: item.modifier || '',
|
createDate: item.createDate || '',
|
modifyDate: item.modifyDate || '',
|
remark: item.remark || ''
|
}));
|
|
return [...formattedInboundFail, ...formattedOutboundFail];
|
});
|
|
// 分页&轮播相关响应式变量
|
const normalCurrentPage = ref(1);
|
const failCurrentPage = ref(1);
|
const normalShowTaskList = ref([]);
|
const failShowTaskList = ref([]);
|
let normalCarouselTimer = null;
|
let failCarouselTimer = null;
|
|
// 修复:单据状态文本(新增taskType参数)
|
const getTaskStatusText = (statusNum, taskType) => {
|
const statusMap = {
|
0: "未开始",
|
1: taskType === 'inbound' ? "入库中" : (taskType === 'outbound' ? "出库中" : "处理中"),
|
2: "处理中",
|
3: "已完成",
|
4: "已取消",
|
5: "异常"
|
};
|
return statusMap[statusNum] ;
|
};
|
|
// MES回传状态映射
|
const mesStatusMap = {
|
0: "未回传", 1: "回传成功", 2: "回传失败",
|
3: "部分回传成功", 4: "部分回传失败"
|
};
|
const getMESStatusText = (statusNum) => {
|
if (statusNum === undefined || statusNum === null || isNaN(statusNum)) return "未知状态";
|
return mesStatusMap[statusNum] || `未知状态(${statusNum})`;
|
};
|
const getMESStatusClass = (statusNum) => {
|
if (statusNum === undefined || statusNum === null || isNaN(statusNum)) return "status-unknown";
|
const classMap = { 0: "status-pending", 1: "status-completed", 2: "status-error", 3: "status-processing", 4: "status-error" };
|
return classMap[statusNum] || "status-unknown";
|
};
|
|
// 单据类型文本/样式
|
const getTaskTypeText = (row) => {
|
const businessType = Number(row.businessType) || 0;
|
if (row.taskType === 'inbound') {
|
return inboundBusinessTypeMap[businessType] || `未知类型(${businessType})`;
|
} else if (row.taskType === 'outbound') {
|
return outboundBusinessTypeMap[businessType] || `未知类型(${businessType})`;
|
}
|
return "其他作业";
|
};
|
const getStatusClass = (statusNum) => {
|
if (statusNum === undefined || statusNum === null || isNaN(statusNum)) return "status-unknown";
|
const classMap = { 0: "status-pending", 1: "status-processing", 2: "status-processing", 3: "status-completed", 4: "status-canceled", 5: "status-error" };
|
return classMap[statusNum] || "status-unknown";
|
};
|
const getTypeClass = (taskType) => {
|
const classMap = { 'inbound': "type-inbound", 'outbound': "type-outbound" };
|
return classMap[taskType] || "type-other";
|
};
|
|
// 数字格式化
|
const formatNumber = (num) => {
|
if (!num) return '0';
|
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
};
|
|
// 初始化表格数据(分页+轮播)
|
const initTableData = () => {
|
// 停止旧定时器
|
clearInterval(normalCarouselTimer);
|
clearInterval(failCarouselTimer);
|
|
// 正常单据分页/轮播
|
const loadNormalData = (page) => {
|
const start = (page - 1) * 5;
|
const end = start + 5;
|
normalShowTaskList.value = normalTaskList.value.slice(start, end);
|
};
|
loadNormalData(normalCurrentPage.value);
|
|
// 失败单据分页/轮播
|
const loadFailData = (page) => {
|
const start = (page - 1) * 5;
|
const end = start + 5;
|
failShowTaskList.value = failTaskList.value.slice(start, end);
|
};
|
loadFailData(failCurrentPage.value);
|
|
// 轮播逻辑(仅数据超过5条时开启)
|
if (normalTaskList.value.length > 5) {
|
normalCarouselTimer = setInterval(() => {
|
normalCurrentPage.value = normalCurrentPage.value >= Math.ceil(normalTaskList.value.length / 5) ? 1 : normalCurrentPage.value + 1;
|
loadNormalData(normalCurrentPage.value);
|
}, 5000);
|
}
|
if (failTaskList.value.length > 5) {
|
failCarouselTimer = setInterval(() => {
|
failCurrentPage.value = failCurrentPage.value >= Math.ceil(failTaskList.value.length / 5) ? 1 : failCurrentPage.value + 1;
|
loadFailData(failCurrentPage.value);
|
}, 5000);
|
}
|
};
|
|
// 分页切换事件
|
const handleNormalPageChange = (page) => {
|
clearInterval(normalCarouselTimer); // 手动切换分页时停止轮播
|
normalCurrentPage.value = page;
|
const start = (page - 1) * 5;
|
const end = start + 5;
|
normalShowTaskList.value = normalTaskList.value.slice(start, end);
|
};
|
const handleFailPageChange = (page) => {
|
clearInterval(failCarouselTimer);
|
failCurrentPage.value = page;
|
const start = (page - 1) * 5;
|
const end = start + 5;
|
failShowTaskList.value = failTaskList.value.slice(start, end);
|
};
|
|
// 监听数据变化,重新初始化表格
|
watch([normalTaskList, failTaskList], () => {
|
initTableData();
|
}, { deep: true });
|
|
// 获取后端数据
|
const fetchBigGreenData = async () => {
|
try {
|
const res = await http.get('/api/BigScreen/GetBigGreenData');
|
bigscreendata.value = res.data || res;
|
nextTick(() => {
|
initTableData();
|
refreshCharts();
|
});
|
} catch (error) {
|
ElMessage.error('数据获取失败,请检查后端接口是否正常');
|
console.error('数据获取失败:', error);
|
}
|
};
|
|
// 图表相关(精简冗余逻辑)
|
const stockTrendRef = ref(null);
|
const locationRateRef = ref(null);
|
let stockTrendChart = null;
|
let locationRateChart = null;
|
|
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 refreshCharts = () => {
|
[initStockTrend, initLocationRate].forEach(initFunc => {
|
const chart = initFunc();
|
if (chart) chart.resize();
|
});
|
};
|
|
// 防抖处理resize事件
|
let resizeTimer = null;
|
const handleResize = () => {
|
clearTimeout(resizeTimer);
|
resizeTimer = setTimeout(() => {
|
[stockTrendChart, locationRateChart].forEach(chart => {
|
if (chart) chart.resize();
|
});
|
}, 200);
|
};
|
|
// 生命周期
|
onMounted(() => {
|
fetchBigGreenData();
|
nextTick(() => {
|
initStockTrend();
|
initLocationRate();
|
window.addEventListener('resize', handleResize);
|
});
|
});
|
|
onUnmounted(() => {
|
// 销毁图表和定时器
|
[stockTrendChart, locationRateChart].forEach(chart => {
|
if (chart) chart.dispose();
|
});
|
[normalCarouselTimer, failCarouselTimer, resizeTimer].forEach(timer => {
|
clearInterval(timer);
|
clearTimeout(timer);
|
});
|
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;
|
}
|
|
.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;
|
}
|
|
.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;
|
}
|
|
.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), :deep(.el-table--group::after), :deep(.el-table::before), :deep(.el-table__fixed-right::before), :deep(.el-table__fixed::before) { display: none; }
|
|
: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, .task-type {
|
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); }
|
|
.type-inbound { background-color: rgba(64, 158, 255, 0.1); color: #409eff; border: 1px solid rgba(64, 158, 255, 0.2); }
|
.type-outbound { background-color: rgba(103, 194, 58, 0.1); color: #67c23a; border: 1px solid rgba(103, 194, 58, 0.2); }
|
.type-transfer { background-color: rgba(230, 162, 60, 0.1); color: #e6a23c; border: 1px solid rgba(230, 162, 60, 0.2); }
|
.type-other { background-color: rgba(144, 147, 153, 0.1); color: #909399; border: 1px solid rgba(144, 147, 153, 0.2); }
|
.type-unknown { background-color: rgba(144, 147, 153, 0.1); color: #909399; border: 1px solid rgba(144, 147, 153, 0.2); }
|
</style>
|