|
|
<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>
|