<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 />
|
</el-icon>
|
</div>
|
<div class="metric-content">
|
<div class="metric-value">{{ 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>
|
</div>
|
<div ref="efficiencyChartRef" class="chart-container"></div>
|
</div>
|
</div>
|
|
<!-- 第三行:数据表格 -->
|
<div class="table-row">
|
<div class="table-card">
|
<div class="card-title">
|
<span>实时作业任务</span>
|
<el-button type="primary" size="small" @click="refreshData">
|
<el-icon><Refresh /></el-icon>
|
刷新
|
</el-button>
|
</div>
|
<el-table :data="taskList" style="width: 100%" height="200">
|
<el-table-column prop="taskNo" label="任务编号" width="150" />
|
<el-table-column prop="type" label="任务类型" width="100">
|
<template #default="{ row }">
|
<el-tag :type="row.type === '入库' ? 'success' : 'warning'">{{ row.type }}</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column prop="material" label="物料名称" />
|
<el-table-column prop="quantity" label="数量" width="100" />
|
<el-table-column prop="location" label="库位" width="120" />
|
<el-table-column prop="status" label="状态" width="100">
|
<template #default="{ row }">
|
<el-tag :type="getStatusType(row.status)">{{ row.status }}</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column prop="operator" label="操作员" width="100" />
|
<el-table-column prop="time" label="时间" width="160" />
|
</el-table>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script>
|
import { ref, onMounted, onUnmounted } from 'vue'
|
import * as echarts from 'echarts'
|
import { http } from '@/utils/http'
|
import { formatDateTime } from '@/utils'
|
|
export default {
|
name: 'Dashboard',
|
setup() {
|
const currentTime = ref('')
|
const trendChartRef = ref(null)
|
const categoryChartRef = ref(null)
|
const efficiencyChartRef = ref(null)
|
|
const metrics = ref([
|
{ label: '总库存量', value: '12,580', icon: 'Box', color: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', trend: 8.5 },
|
{ label: '今日入库', value: '1,280', icon: 'Download', color: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', trend: 12.3 },
|
{ label: '今日出库', value: '965', icon: 'Upload', color: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', trend: -3.2 },
|
{ label: '待处理任务', value: '48', icon: 'List', color: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)', trend: 5.7 }
|
])
|
|
const taskList = ref([])
|
|
// 更新时间
|
let timer
|
const updateTime = () => {
|
currentTime.value = formatDateTime(new Date())
|
}
|
|
// 获取状态类型
|
const getStatusType = (status) => {
|
const map = {
|
'进行中': 'primary',
|
'已完成': 'success',
|
'待处理': 'info',
|
'异常': 'danger'
|
}
|
return map[status] || 'info'
|
}
|
|
// 初始化趋势图
|
const initTrendChart = () => {
|
const chart = echarts.init(trendChartRef.value)
|
const option = {
|
tooltip: { trigger: 'axis' },
|
legend: {
|
data: ['入库量', '出库量'],
|
textStyle: { color: '#fff' }
|
},
|
grid: {
|
left: '3%', right: '4%', bottom: '3%', top: '15%',
|
containLabel: true
|
},
|
xAxis: {
|
type: 'category',
|
boundaryGap: false,
|
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '24:00'],
|
axisLine: { lineStyle: { color: '#fff' } }
|
},
|
yAxis: {
|
type: 'value',
|
axisLine: { lineStyle: { color: '#fff' } },
|
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
|
},
|
series: [
|
{
|
name: '入库量',
|
type: 'line',
|
smooth: true,
|
data: [120, 200, 450, 680, 520, 780, 650],
|
itemStyle: { color: '#43e97b' },
|
areaStyle: {
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: 'rgba(67, 233, 123, 0.5)' },
|
{ offset: 1, color: 'rgba(67, 233, 123, 0)' }
|
])
|
}
|
},
|
{
|
name: '出库量',
|
type: 'line',
|
smooth: true,
|
data: [80, 150, 380, 520, 420, 650, 580],
|
itemStyle: { color: '#4facfe' },
|
areaStyle: {
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: 'rgba(79, 172, 254, 0.5)' },
|
{ offset: 1, color: 'rgba(79, 172, 254, 0)' }
|
])
|
}
|
}
|
]
|
}
|
chart.setOption(option)
|
return chart
|
}
|
|
// 初始化分类占比图
|
const initCategoryChart = () => {
|
const chart = echarts.init(categoryChartRef.value)
|
const option = {
|
tooltip: { trigger: 'item' },
|
legend: {
|
orient: 'vertical',
|
right: '10%',
|
top: 'center',
|
textStyle: { color: '#fff' }
|
},
|
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: 16, fontWeight: 'bold' }
|
},
|
data: [
|
{ value: 3580, name: '原材料', itemStyle: { color: '#5470c6' } },
|
{ value: 2840, name: '半成品', itemStyle: { color: '#91cc75' } },
|
{ value: 4120, name: '成品', itemStyle: { color: '#fac858' } },
|
{ value: 2040, name: '辅料', itemStyle: { color: '#ee6666' } }
|
]
|
}
|
]
|
}
|
chart.setOption(option)
|
return chart
|
}
|
|
// 初始化效率统计图
|
const initEfficiencyChart = () => {
|
const chart = echarts.init(efficiencyChartRef.value)
|
const option = {
|
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
grid: {
|
left: '3%', right: '4%', bottom: '3%', top: '10%',
|
containLabel: true
|
},
|
xAxis: {
|
type: 'category',
|
data: ['入库作业', '出库作业', '盘点作业', '调拨作业', '补货作业'],
|
axisLine: { lineStyle: { color: '#fff' } },
|
axisLabel: { color: '#fff' }
|
},
|
yAxis: {
|
type: 'value',
|
name: '效率(单位/小时)',
|
axisLine: { lineStyle: { color: '#fff' } },
|
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } },
|
axisLabel: { color: '#fff' }
|
},
|
series: [
|
{
|
data: [180, 156, 95, 78, 120],
|
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]
|
}
|
}
|
]
|
}
|
chart.setOption(option)
|
return chart
|
}
|
|
// 获取任务列表数据
|
const fetchTaskList = async () => {
|
try {
|
// 模拟数据,实际使用时调用接口
|
// const res = await http.get('/wms/task/list')
|
|
// 模拟数据
|
taskList.value = [
|
{ taskNo: 'RK20241224001', type: '入库', material: '钢板 A型', quantity: 500, location: 'A-01-01', status: '进行中', operator: '张三', time: '2024-12-24 14:25:30' },
|
{ taskNo: 'CK20241224002', type: '出库', material: '螺丝 M8', quantity: 2000, location: 'B-03-05', status: '已完成', operator: '李四', time: '2024-12-24 14:20:15' },
|
{ taskNo: 'RK20241224003', type: '入库', material: '铝板 B型', quantity: 300, location: 'A-02-03', status: '待处理', operator: '王五', time: '2024-12-24 14:15:00' },
|
{ taskNo: 'CK20241224004', type: '出库', material: '螺母 M8', quantity: 1500, location: 'B-02-01', status: '进行中', operator: '赵六', time: '2024-12-24 14:10:45' },
|
{ taskNo: 'PD20241224001', type: '盘点', material: '垫片', quantity: 5000, location: 'C-01-01', status: '进行中', operator: '孙七', time: '2024-12-24 14:05:20' }
|
]
|
} catch (error) {
|
console.error('获取任务列表失败:', error)
|
}
|
}
|
|
// 刷新数据
|
const refreshData = () => {
|
fetchTaskList()
|
}
|
|
// 窗口大小改变时重绘图表
|
const handleResize = () => {
|
try {
|
const refs = [trendChartRef.value, categoryChartRef.value, efficiencyChartRef.value]
|
refs.forEach(dom => {
|
if (dom) {
|
const chart = echarts.getInstanceByDom(dom)
|
if (chart) {
|
chart.resize()
|
}
|
}
|
})
|
} catch (error) {
|
console.warn('图表重绘时出错:', error)
|
}
|
}
|
|
onMounted(() => {
|
updateTime()
|
timer = setInterval(updateTime, 1000)
|
|
initTrendChart()
|
initCategoryChart()
|
initEfficiencyChart()
|
fetchTaskList()
|
|
window.addEventListener('resize', handleResize)
|
})
|
|
onUnmounted(() => {
|
clearInterval(timer)
|
window.removeEventListener('resize', handleResize)
|
|
// 销毁图表实例
|
try {
|
const refs = [trendChartRef.value, categoryChartRef.value, efficiencyChartRef.value]
|
refs.forEach(dom => {
|
if (dom) {
|
const chart = echarts.getInstanceByDom(dom)
|
if (chart) {
|
chart.dispose()
|
}
|
}
|
})
|
} catch (error) {
|
console.warn('图表销毁时出错:', error)
|
}
|
})
|
|
return {
|
currentTime,
|
metrics,
|
taskList,
|
trendChartRef,
|
categoryChartRef,
|
efficiencyChartRef,
|
getStatusType,
|
refreshData
|
}
|
}
|
}
|
</script>
|
|
<style scoped>
|
.dashboard-container {
|
width: 100%;
|
height: 100%;
|
padding: 20px;
|
display: flex;
|
flex-direction: column;
|
gap: 15px;
|
overflow-y: auto;
|
}
|
|
.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.1);
|
}
|
|
.title {
|
font-size: 24px;
|
font-weight: bold;
|
background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
|
-webkit-background-clip: text;
|
-webkit-text-fill-color: transparent;
|
}
|
|
.datetime {
|
font-size: 16px;
|
color: #4facfe;
|
}
|
|
.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.1);
|
border-radius: 8px;
|
color: #fff;
|
text-decoration: none;
|
transition: all 0.3s;
|
cursor: pointer;
|
}
|
|
.nav-item:hover {
|
background: rgba(79, 172, 254, 0.2);
|
border-color: #4facfe;
|
}
|
|
.nav-item.active {
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
border-color: transparent;
|
}
|
|
.main-content {
|
flex: 1;
|
display: flex;
|
flex-direction: column;
|
gap: 15px;
|
}
|
|
.metrics-row {
|
display: grid;
|
grid-template-columns: repeat(4, 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.1);
|
border-radius: 10px;
|
transition: all 0.3s;
|
}
|
|
.metric-card:hover {
|
transform: translateY(-5px);
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
}
|
|
.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: 28px;
|
font-weight: bold;
|
margin-bottom: 5px;
|
}
|
|
.metric-label {
|
font-size: 14px;
|
color: rgba(255, 255, 255, 0.6);
|
margin-bottom: 5px;
|
}
|
|
.metric-trend {
|
display: flex;
|
align-items: center;
|
gap: 5px;
|
font-size: 12px;
|
}
|
|
.metric-trend.up {
|
color: #67c23a;
|
}
|
|
.metric-trend.down {
|
color: #f56c6c;
|
}
|
|
.charts-row {
|
display: grid;
|
grid-template-columns: repeat(3, 1fr);
|
gap: 15px;
|
}
|
|
.chart-card {
|
padding: 20px;
|
background: rgba(255, 255, 255, 0.05);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
border-radius: 10px;
|
}
|
|
.card-title {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 15px;
|
font-size: 16px;
|
font-weight: bold;
|
color: #4facfe;
|
}
|
|
.chart-container {
|
width: 100%;
|
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.1);
|
border-radius: 10px;
|
}
|
|
:deep(.el-table) {
|
background: transparent !important;
|
}
|
|
:deep(.el-table th) {
|
background: rgba(255, 255, 255, 0.1) !important;
|
color: #fff !important;
|
border-color: rgba(255, 255, 255, 0.1) !important;
|
}
|
|
:deep(.el-table td) {
|
border-color: rgba(255, 255, 255, 0.1) !important;
|
}
|
|
:deep(.el-table tr) {
|
background: transparent !important;
|
}
|
|
:deep(.el-table__row:hover td) {
|
background: rgba(255, 255, 255, 0.05) !important;
|
}
|
|
:deep(.el-table--enable-row-hover .el-table__body tr:hover > td) {
|
background: rgba(255, 255, 255, 0.05) !important;
|
}
|
|
/* 响应式适配 */
|
@media screen and (max-width: 1920px) {
|
.metric-value {
|
font-size: 24px;
|
}
|
|
.chart-container {
|
height: 220px;
|
}
|
}
|
|
@media screen and (max-width: 1600px) {
|
.metrics-row {
|
grid-template-columns: repeat(2, 1fr);
|
}
|
|
.charts-row {
|
grid-template-columns: repeat(2, 1fr);
|
}
|
|
.title {
|
font-size: 20px;
|
}
|
|
.metric-value {
|
font-size: 20px;
|
}
|
|
.chart-container {
|
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;
|
}
|
|
.metrics-row {
|
grid-template-columns: repeat(2, 1fr);
|
gap: 10px;
|
}
|
|
.metric-card {
|
padding: 15px;
|
}
|
|
.metric-value {
|
font-size: 18px;
|
}
|
|
.charts-row {
|
grid-template-columns: 1fr;
|
gap: 10px;
|
}
|
|
.chart-card {
|
padding: 15px;
|
}
|
|
.chart-container {
|
height: 180px;
|
}
|
}
|
|
@media screen and (max-width: 1024px) {
|
.metrics-row {
|
grid-template-columns: 1fr 1fr;
|
}
|
|
.nav-menu {
|
flex-wrap: wrap;
|
}
|
|
.nav-item {
|
flex: 1;
|
min-width: 100px;
|
justify-content: center;
|
}
|
}
|
|
@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;
|
}
|
|
.nav-item {
|
padding: 6px 12px;
|
font-size: 12px;
|
flex-direction: column;
|
gap: 4px;
|
}
|
|
.metrics-row {
|
grid-template-columns: 1fr;
|
}
|
|
.metric-card {
|
padding: 12px;
|
flex-direction: column;
|
text-align: center;
|
}
|
|
.metric-icon {
|
width: 45px;
|
height: 45px;
|
}
|
|
.chart-container {
|
height: 150px;
|
}
|
}
|
</style>
|