You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

671 lines
21 KiB

<template>
<div class="overview-page">
<div class="overview-header">
<div class="header-left"><img class="site-navbar__brand-logo" src="~@/assets/img/lc.png" alt="龙闯电梯"></div>
<div class="header-center">
<div class="header-title">龙闯电梯 MES - 工厂综合运营大屏</div>
</div>
<div class="right-tools">
<div class="time-text">{{ currentTime }}</div>
</div>
</div>
<div class="kpi-grid">
<div class="kpi-card">
<div class="kpi-label">近一月整梯排产</div>
<div class="kpi-value">{{ kpi.monthWhole }}</div>
</div>
<div class="kpi-card">
<div class="kpi-label">近一月VL2.5升级排产</div>
<div class="kpi-value">{{ kpi.monthRenovation }}</div>
</div>
<div class="kpi-card">
<div class="kpi-label">近一月线缆/COP任务</div>
<div class="kpi-value">{{ kpi.monthTask }}</div>
</div>
<div class="kpi-card">
<div class="kpi-label">近一月机加工任务</div>
<div class="kpi-value">{{ kpi.monthMachining }}</div>
</div>
<div class="kpi-card">
<div class="kpi-label">近一月完工达成率</div>
<div class="kpi-value highlight">{{ kpi.finishRate }}%</div>
</div>
<div class="kpi-card">
<div class="kpi-label">当前在制总量</div>
<div class="kpi-value warning">{{ kpi.wipTotal }}</div>
</div>
<div class="kpi-card">
<div class="kpi-label">订单准交率</div>
<div class="kpi-value success">{{ kpi.onTimeRate }}%</div>
</div>
</div>
<div class="chart-grid">
<div class="panel">
<div class="panel-title">近一月排产/完工对比</div>
<div id="overviewBarChart" class="chart-box"></div>
</div>
<div class="panel">
<div class="panel-title">生产结构分布</div>
<div id="overviewPieChart" class="chart-box"></div>
</div>
<div class="panel wide">
<div class="panel-title">最近 7 天完工趋势</div>
<div id="overviewLineChart" class="chart-box line-box"></div>
</div>
<div class="panel">
<div class="panel-title">关键环节达成率</div>
<div class="node-rate-grid">
<div v-for="item in nodeRates" :key="item.name" class="node-rate-card">
<div class="node-rate-name">{{ item.name }}</div>
<div class="node-rate-meta">
<span class="node-rate-count">{{ item.done }}/{{ item.total }}</span>
<span class="node-rate-badge" :class="getNodeRateClass(item.rate)">{{ item.rate }}%</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import echarts from 'echarts'
import { getFactoryOverviewBoardData } from '@/api/longchuang/productionPlan'
const STATUS_ALLOW_LIST = ['已排产', '进行中', '已完成']
export default {
name: 'ScreenFactoryOverview',
data() {
return {
currentTime: '',
timerId: null,
boardTimerId: null,
loading: false,
chartBar: null,
chartPie: null,
chartLine: null,
wholeList: [],
taskList: [],
renovationList: [],
machiningList: [],
kpi: {
monthWhole: 0,
monthRenovation: 0,
monthTask: 0,
monthMachining: 0,
finishRate: 0,
wipTotal: 0,
onTimeRate: 0
},
nodeRates: []
}
},
mounted() {
this.loadBoardData()
this.updateTime()
this.timerId = setInterval(this.updateTime, 1000)
this.boardTimerId = setInterval(() => {
this.loadBoardData()
}, 60000)
window.addEventListener('resize', this.resizeCharts)
},
beforeDestroy() {
if (this.timerId) clearInterval(this.timerId)
if (this.boardTimerId) clearInterval(this.boardTimerId)
window.removeEventListener('resize', this.resizeCharts)
this.disposeCharts()
},
methods: {
loadBoardData() {
if (this.loading) return
this.loading = true
getFactoryOverviewBoardData({ page: 1, limit: 500, statusList: STATUS_ALLOW_LIST }).then(({ data }) => {
this.loading = false
const boardData = data && data.code === 0 && data.boardData ? data.boardData : {}
this.wholeList = this.normalizeBoardList(boardData.homeLiftList)
this.taskList = this.normalizeBoardList(boardData.cableCopTaskList)
this.renovationList = this.normalizeBoardList(boardData.renovationOrderList)
this.machiningList = this.normalizeBoardList(boardData.machiningTaskList)
this.buildDashboard()
this.$nextTick(() => {
setTimeout(() => {
this.resizeCharts()
}, 120)
})
}).catch(() => {
this.loading = false
this.wholeList = []
this.taskList = []
this.renovationList = []
this.machiningList = []
this.buildDashboard()
})
},
normalizeBoardList(sourceList) {
return (sourceList || [])
.filter(item => STATUS_ALLOW_LIST.includes(item.status))
.map(item => ({
...item,
nodeList: item.nodeList || []
}))
},
buildDashboard() {
const periodStart = this.dayjs().subtract(1, 'month').startOf('day')
const allOrderList = this.wholeList
.concat(this.renovationList)
.concat(this.taskList)
.concat(this.machiningList)
const recentOrderList = this.filterByDateRange(allOrderList, periodStart)
const finishedAll = recentOrderList.filter(item => item.status === '已完成').length
const inScheduleAll = recentOrderList.filter(item => STATUS_ALLOW_LIST.includes(item.status)).length
const wipTotal = this.wholeList.filter(item => item.status === '进行中').length +
this.taskList.filter(item => item.status === '进行中').length +
this.renovationList.filter(item => item.status === '进行中').length +
this.machiningList.filter(item => item.status === '进行中').length
const monthWhole = this.countByMonth(this.wholeList, periodStart)
const monthRenovation = this.countByMonth(this.renovationList, periodStart)
const monthTask = this.countByMonth(this.taskList, periodStart)
const monthMachining = this.countByMonth(this.machiningList, periodStart)
const onTimeTotal = allOrderList.filter(item => item.status === '已完成').length
const onTimeCount = allOrderList.filter(item => item.status === '已完成' && this.isOnTime(item)).length
this.kpi.monthWhole = monthWhole
this.kpi.monthRenovation = monthRenovation
this.kpi.monthTask = monthTask
this.kpi.monthMachining = monthMachining
this.kpi.finishRate = inScheduleAll ? Math.round((finishedAll / inScheduleAll) * 100) : 0
this.kpi.wipTotal = wipTotal
this.kpi.onTimeRate = onTimeTotal ? Math.round((onTimeCount / onTimeTotal) * 100) : 0
this.nodeRates = this.buildNodeRates()
this.$nextTick(() => {
this.renderBarChart()
this.renderPieChart()
this.renderLineChart()
})
},
countByMonth(list, monthStart) {
const startValue = monthStart.valueOf()
return list.filter(item => {
const dateVal = item.planDeliveryDate || item.planFinishDate || item.createTime
const dateObj = this.toDayjs(dateVal)
return dateObj && dateObj.valueOf() >= startValue
}).length
},
filterByDateRange(list, startDate) {
return (list || []).filter(item => {
const dateVal = item.planDeliveryDate || item.planFinishDate || item.createTime
const dateObj = this.toDayjs(dateVal)
return dateObj && dateObj.valueOf() >= startDate.valueOf()
})
},
countFinishedInRange(list, startDate) {
return (list || []).filter(item => {
if (item.status !== '已完成') return false
const dateVal = item.finishDate || item.planDeliveryDate || item.planFinishDate || item.createTime
const dateObj = this.toDayjs(dateVal)
return dateObj && dateObj.valueOf() >= startDate.valueOf()
}).length
},
isOnTime(item) {
const finishDate = this.toDayjs(item.finishDate)
const planDate = this.toDayjs(item.planDeliveryDate || item.planFinishDate)
if (!finishDate || !planDate) return false
return finishDate.valueOf() <= planDate.valueOf()
},
toDayjs(dateVal) {
if (!dateVal) return null
const d = this.dayjs(dateVal)
return d && d.isValid() ? d : null
},
buildNodeRates() {
const nodeMap = {}
const all = this.wholeList.concat(this.taskList).concat(this.renovationList).concat(this.machiningList)
all.forEach(item => {
const list = item.nodeList || []
list.forEach(node => {
if (!nodeMap[node.nodeName]) nodeMap[node.nodeName] = { total: 0, done: 0 }
nodeMap[node.nodeName].total += 1
if (node.status === '已完成') nodeMap[node.nodeName].done += 1
})
})
return Object.keys(nodeMap).map((name) => {
const total = nodeMap[name].total
const done = nodeMap[name].done
return {
name,
total,
done,
rate: total ? Math.round((done / total) * 100) : 0,
}
}).sort((a, b) => b.rate - a.rate)
},
getNodeRateClass(rate) {
if (rate >= 85) return 'is-high'
if (rate >= 60) return 'is-mid'
return 'is-low'
},
renderBarChart() {
const el = document.getElementById('overviewBarChart')
if (!el) return
if (!this.chartBar) this.chartBar = echarts.init(el)
const periodStart = this.dayjs().subtract(1, 'month').startOf('day')
const scheduledWhole = this.countByMonth(this.wholeList.filter(item => STATUS_ALLOW_LIST.includes(item.status)), periodStart)
const scheduledRenovation = this.countByMonth(this.renovationList.filter(item => STATUS_ALLOW_LIST.includes(item.status)), periodStart)
const scheduledTask = this.countByMonth(this.taskList.filter(item => STATUS_ALLOW_LIST.includes(item.status)), periodStart)
const scheduledMachining = this.countByMonth(this.machiningList.filter(item => STATUS_ALLOW_LIST.includes(item.status)), periodStart)
const finishedWhole = this.countFinishedInRange(this.wholeList, periodStart)
const finishedRenovation = this.countFinishedInRange(this.renovationList, periodStart)
const finishedTask = this.countFinishedInRange(this.taskList, periodStart)
const finishedMachining = this.countFinishedInRange(this.machiningList, periodStart)
this.chartBar.setOption({
color: ['#4fa8ff', '#6ed3a6'],
grid: { left: 30, right: 20, top: 40, bottom: 20, containLabel: true },
tooltip: { trigger: 'axis' },
legend: { textStyle: { color: '#cbe7ff' }, data: ['排产量', '完工量'] },
xAxis: { type: 'category', axisLabel: { color: '#cbe7ff' }, data: ['整梯', 'VL2.5升级', '线缆/COP', '机加工'] },
yAxis: { type: 'value', axisLabel: { color: '#cbe7ff' }, splitLine: { lineStyle: { color: 'rgba(160,200,240,0.15)' } } },
series: [
{
name: '排产量',
type: 'bar',
barWidth: 24,
data: [scheduledWhole, scheduledRenovation, scheduledTask, scheduledMachining],
itemStyle: { color: '#4fa8ff' },
label: {
normal: {
show: true,
position: 'top',
color: '#d4ebff',
fontSize: 12,
formatter: '{c}'
}
}
},
{
name: '完工量',
type: 'bar',
barWidth: 24,
data: [finishedWhole, finishedRenovation, finishedTask, finishedMachining],
itemStyle: { color: '#6ed3a6' },
label: {
normal: {
show: true,
position: 'top',
color: '#d4ebff',
fontSize: 12,
formatter: '{c}'
}
}
}
]
}, true)
},
renderPieChart() {
const el = document.getElementById('overviewPieChart')
if (!el) return
if (!this.chartPie) this.chartPie = echarts.init(el)
const wholeCount = this.wholeList.length
const renovationCount = this.renovationList.length
const taskCount = this.taskList.length
const machiningCount = this.machiningList.length
const pieData = [
{ value: wholeCount, name: '整梯订单' },
{ value: renovationCount, name: 'VL2.5升级订单' },
{ value: taskCount, name: '线缆/COP任务' },
{ value: machiningCount, name: '机加工任务' }
]
this.chartPie.setOption({
color: ['#57b8ff', '#5dd4b0', '#7fa6d9', '#f5c15e'],
tooltip: {
trigger: 'item',
formatter: (params) => `${params.name}<br/>数量:${params.value}<br/>占比:${params.percent || 0}%`
},
legend: { bottom: 2, textStyle: { color: '#cbe7ff' } },
series: [{
type: 'pie',
radius: ['42%', '72%'],
center: ['50%', '45%'],
avoidLabelOverlap: false,
minShowLabelAngle: 0,
label: {
normal: {
show: true,
position: 'outside',
color: '#d4ebff',
fontSize: 14,
formatter: '{b} {d}%'
},
emphasis: {
show: true,
formatter: '{b} {d}%'
}
},
labelLine: {
normal: {
show: true,
length: 14,
length2: 10,
lineStyle: {
color: '#7fb4de'
}
}
},
data: pieData
}]
}, true)
},
renderLineChart() {
const el = document.getElementById('overviewLineChart')
if (!el) return
if (!this.chartLine) this.chartLine = echarts.init(el)
const xAxis = []
const wholeSeries = []
const renovationSeries = []
const taskSeries = []
const machiningSeries = []
for (let i = 6; i >= 0; i--) {
const day = this.dayjs().subtract(i, 'day')
xAxis.push(day.format('MM-DD'))
wholeSeries.push(this.countFinishByDay(this.wholeList, day))
renovationSeries.push(this.countFinishByDay(this.renovationList, day))
taskSeries.push(this.countFinishByDay(this.taskList, day))
machiningSeries.push(this.countFinishByDay(this.machiningList, day))
}
this.chartLine.setOption({
color: ['#6cc7ff', '#74e0b3', '#7fa6d9', '#f5c15e'],
grid: { left: 35, right: 20, top: 34, bottom: 20, containLabel: true },
tooltip: { trigger: 'axis' },
legend: { textStyle: { color: '#cbe7ff' }, data: ['整梯完工', 'VL2.5升级完工', '线缆/COP完工', '机加工完工'] },
xAxis: { type: 'category', axisLabel: { color: '#cbe7ff' }, data: xAxis },
yAxis: { type: 'value', axisLabel: { color: '#cbe7ff' }, splitLine: { lineStyle: { color: 'rgba(160,200,240,0.15)' } } },
series: [
{ name: '整梯完工', type: 'line', smooth: true, symbol: 'circle', symbolSize: 7, data: wholeSeries, itemStyle: { color: '#6cc7ff' }, lineStyle: { color: '#6cc7ff' }, areaStyle: { color: 'rgba(108,199,255,0.18)' } },
{ name: 'VL2.5升级完工', type: 'line', smooth: true, symbol: 'circle', symbolSize: 7, data: renovationSeries, itemStyle: { color: '#74e0b3' }, lineStyle: { color: '#74e0b3' }, areaStyle: { color: 'rgba(116,224,179,0.18)' } },
{ name: '线缆/COP完工', type: 'line', smooth: true, symbol: 'circle', symbolSize: 7, data: taskSeries, itemStyle: { color: '#7fa6d9' }, lineStyle: { color: '#7fa6d9' }, areaStyle: { color: 'rgba(127,166,217,0.16)' } },
{ name: '机加工完工', type: 'line', smooth: true, symbol: 'circle', symbolSize: 7, data: machiningSeries, itemStyle: { color: '#f5c15e' }, lineStyle: { color: '#f5c15e' }, areaStyle: { color: 'rgba(245,193,94,0.15)' } }
]
}, true)
},
countFinishByDay(list, day) {
return list.filter(item => {
const finishDate = this.toDayjs(item.finishDate)
return finishDate && finishDate.format('YYYY-MM-DD') === day.format('YYYY-MM-DD')
}).length
},
updateTime() {
const now = new Date()
const weekList = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
const d = this.dayjs(now)
this.currentTime = `${d.format('YYYY/MM/DD')} ${weekList[now.getDay()]} ${d.format('HH:mm:ss')}`
},
resizeCharts() {
if (this.chartBar) this.chartBar.resize()
if (this.chartPie) this.chartPie.resize()
if (this.chartLine) this.chartLine.resize()
},
disposeCharts() {
if (this.chartBar) { this.chartBar.dispose(); this.chartBar = null }
if (this.chartPie) { this.chartPie.dispose(); this.chartPie = null }
if (this.chartLine) { this.chartLine.dispose(); this.chartLine = null }
}
}
}
</script>
<style scoped>
.overview-page {
height: 100vh;
padding: 16px;
background: radial-gradient(circle at 20% 20%, #1b3352 0%, #102338 45%, #0a1523 100%);
color: #e7f3ff;
overflow: hidden;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.overview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
padding: 12px 16px;
border-radius: 10px;
border: 1px solid rgba(100, 187, 255, 0.28);
background: rgba(12, 36, 58, 0.78);
position: relative;
}
.left-logo {
display: flex;
align-items: center;
min-width: 110px;
z-index: 2;
}
.logo-box {
width: 86px;
height: 30px;
border-radius: 6px;
border: 1px solid rgba(128, 198, 255, 0.45);
color: #d8edff;
font-size: 14px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
background: rgba(36, 82, 122, 0.35);
}
.overview-header .header-center {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
text-align: center;
pointer-events: none;
max-width: 70%;
width: auto;
padding: 6px 24px 8px;
border-radius: 8px;
border: 1px solid rgba(96, 170, 232, 0.34);
background: linear-gradient(180deg, rgba(33, 73, 116, 0.52), rgba(16, 44, 77, 0.42));
}
.overview-header .header-center::before {
content: '';
position: absolute;
left: 50%;
transform: translateX(-50%);
top: -8px;
width: 70%;
height: 1px;
background: linear-gradient(90deg, rgba(87,164,230,0), rgba(87,164,230,.9), rgba(87,164,230,0));
}
.header-title {
font-size: 30px;
font-weight: 800;
letter-spacing: 3px;
line-height: 1.05;
color: #8fe7ff;
text-shadow: 0 0 14px rgba(79, 179, 255, 0.36);
}
.header-subtitle {
margin-top: 4px;
font-size: 12px;
color: #c7e8ff;
letter-spacing: 1px;
}
.right-tools {
display: flex;
align-items: center;
gap: 12px;
margin-left: auto;
z-index: 2;
}
.time-text {
font-size: 18px;
font-weight: 600;
color: #7bd9ff;
}
.refresh-btn {
background: #ecf5ff;
border-color: #b3d8ff;
color: #409eff;
}
.refresh-btn:hover {
background: #409eff;
border-color: #409eff;
color: #fff;
}
.kpi-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 10px;
margin-bottom: 14px;
}
.kpi-card {
border-radius: 10px;
padding: 12px 14px;
background: linear-gradient(135deg, rgba(29, 66, 106, 0.95), rgba(17, 43, 69, 0.92));
border: 1px solid rgba(95, 180, 255, 0.24);
}
.kpi-label {
font-size: 12px;
color: #b9d8f8;
}
.kpi-value {
margin-top: 6px;
font-size: 28px;
font-weight: 700;
color: #dff0ff;
}
.kpi-value.highlight {
color: #67d2ff;
}
.kpi-value.warning {
color: #f5c15e;
}
.kpi-value.success {
color: #4ade80;
}
.chart-grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
gap: 12px;
flex: 1;
min-height: 0;
}
.panel {
border-radius: 10px;
background: rgba(13, 34, 54, 0.86);
border: 1px solid rgba(95, 180, 255, 0.24);
padding: 10px 12px;
display: flex;
flex-direction: column;
min-height: 0;
}
.panel.wide {
grid-column: span 1;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: #d9ebff;
margin-bottom: 8px;
}
.chart-box {
flex: 1;
min-height: 160px;
height: auto;
}
.line-box {
min-height: 160px;
}
.node-rate-grid {
flex: 1;
min-height: 0;
overflow: hidden;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.node-rate-card {
padding: 8px 10px;
border-radius: 8px;
border: 1px solid rgba(103, 168, 227, 0.25);
background: rgba(29, 58, 86, 0.5);
}
.node-rate-name {
color: #d1e7ff;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.node-rate-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 6px;
}
.node-rate-count {
color: #9cc5ea;
font-size: 12px;
}
.node-rate-badge {
min-width: 50px;
text-align: center;
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
font-weight: 700;
}
.node-rate-badge.is-high {
color: #81f3bd;
background: rgba(23, 108, 80, 0.35);
}
.node-rate-badge.is-mid {
color: #ffd978;
background: rgba(122, 92, 25, 0.35);
}
.node-rate-badge.is-low {
color: #ff9fa3;
background: rgba(126, 49, 53, 0.35);
}
</style>