feat(停机统计): 优化停机统计页面并添加图表展示功能

重构停机统计页面,主要变更包括:
1. 添加核心指标卡片展示停机次数、总时长和作业率
2. 新增按天汇总的折线图和按类型汇总的饼图
3. 优化数据展示逻辑,增加无数据状态处理
4. 移除冗余CSS样式,简化DOM结构
5. 添加图表自适应功能,优化移动端显示
This commit is contained in:
砂糖
2026-01-21 13:02:22 +08:00
parent 9d35f39906
commit 8139143ecc
2 changed files with 419 additions and 66 deletions

View File

@@ -87,7 +87,7 @@
<div class="section-header">
<span class="section-title">停机详情</span>
</div>
<div class="detail-list">
<div>
<div v-if="tableData.length === 0" class="empty-state">
<span class="empty-text">暂无停机记录</span>
</div>
@@ -104,7 +104,7 @@
<div class="section-header">
<span class="section-title">停机趋势</span>
</div>
<div class="chart-wrapper trend-chart" ref="trendChart" id="trendChart"></div>
<div class="chart-wrapper trend-chart" v-show="activeTab !== 'day'" ref="trendChart" id="trendChart"></div>
</div>
</div>
</template>
@@ -342,53 +342,7 @@ export default {
loading.close()
console.log('停机统计响应:', response)
if (response.code === 200 && response.rows && response.rows.length > 0) {
this.tableData = response.rows.map(item => ({
time: this.formatDateTime(item.startDate) + ' - ' + this.formatDateTime(item.endDate),
duration: this.secondsToMinutes(item.duration) + 'min',
remark: item.remark || '-',
machine: item.unit || '-'
}))
const totalDurationSeconds = response.rows.reduce((sum, item) => sum + (Number(item.duration) || 0), 0)
const totalDurationMinutes = this.secondsToMinutes(totalDurationSeconds)
const totalCount = response.rows.length
const totalAvailableMinutes = this.getTotalAvailableMinutes()
const workRate = this.calculateWorkRate(totalDurationMinutes, totalAvailableMinutes)
this.summaryData = [
{ label: '停机时间', value: totalDurationMinutes, unit: 'min' },
{ label: '停机次数', value: totalCount, unit: '次' },
{ label: '作业率', value: workRate, unit: '%' }
]
const crewMap = {}
const typeMap = {}
response.rows.forEach(item => {
const crew = item.crew || '未知班组'
const type = item.stopType || '未知类型'
const durationMinutes = this.secondsToMinutes(item.duration)
crewMap[crew] = (crewMap[crew] || 0) + durationMinutes
typeMap[type] = (typeMap[type] || 0) + durationMinutes
})
this.crewPieData = Object.keys(crewMap).map(crew => ({ name: crew, value: crewMap[crew] }))
this.typePieData = Object.keys(typeMap).map(type => ({ name: type, value: typeMap[type] }))
// 渲染饼图
this.$nextTick(() => {
this.renderPieChart('crew', this.crewPieData)
this.renderPieChart('type', this.typePieData)
})
if (this.activeTab !== 'day') {
if (response.rows.length > 0) {
this.buildTrendChart(response.rows)
} else {
this.trendXData = []
}
}
} else {
if (response.code !== 200 || !response.rows || response.rows.length === 0) {
console.log('暂无停机数据')
this.tableData = []
this.summaryData = [
@@ -403,6 +357,53 @@ export default {
this.clearChart(this.trendChart)
this.renderPieChart('crew', [])
this.renderPieChart('type', [])
return
}
this.tableData = response.rows.map(item => ({
time: this.formatDateTime(item.startDate) + ' - ' + this.formatDateTime(item.endDate),
duration: this.secondsToMinutes(item.duration) + 'min',
remark: item.remark || '-',
machine: item.unit || '-'
}))
const totalDurationSeconds = response.rows.reduce((sum, item) => sum + (Number(item.duration) || 0), 0)
const totalDurationMinutes = this.secondsToMinutes(totalDurationSeconds)
const totalCount = response.rows.length
const totalAvailableMinutes = this.getTotalAvailableMinutes()
const workRate = this.calculateWorkRate(totalDurationMinutes, totalAvailableMinutes)
this.summaryData = [
{ label: '停机时间', value: totalDurationMinutes, unit: 'min' },
{ label: '停机次数', value: totalCount, unit: '次' },
{ label: '作业率', value: workRate, unit: '%' }
]
const crewMap = {}
const typeMap = {}
response.rows.forEach(item => {
const crew = item.crew || '未知班组'
const type = item.stopType || '未知类型'
const durationMinutes = this.secondsToMinutes(item.duration)
crewMap[crew] = (crewMap[crew] || 0) + durationMinutes
typeMap[type] = (typeMap[type] || 0) + durationMinutes
})
this.crewPieData = Object.keys(crewMap).map(crew => ({ name: crew, value: crewMap[crew] }))
this.typePieData = Object.keys(typeMap).map(type => ({ name: type, value: typeMap[type] }))
// 渲染饼图
this.$nextTick(() => {
this.renderPieChart('crew', this.crewPieData)
this.renderPieChart('type', this.typePieData)
})
if (this.activeTab !== 'day') {
if (response.rows.length > 0) {
this.buildTrendChart(response.rows)
} else {
this.trendXData = []
}
}
}).catch(error => {
loading.close()
@@ -807,6 +808,7 @@ export default {
.trend-chart {
height: 250px !important;
min-height: 250px;
max-height: 300px;
}
// ✅【修复】饼图容器增加固定高度,解决高度塌陷
@@ -845,13 +847,6 @@ export default {
color: #909399;
}
.detail-list {
background: #fff;
border-radius: 4px;
border: 1px solid #e4e7ed;
overflow: hidden;
}
.detail-item {
padding: 12px;
border-bottom: 1px solid #f5f7fa;

View File

@@ -13,7 +13,51 @@
<el-button type="primary" icon="el-icon-search" style="margin-left: 10px;" @click="handleQuery">查询</el-button>
</div>
<!-- 停机统计表格 全部修改适配真实JSON数据字段 -->
<!-- 核心指标卡片区域 -->
<div class="indicator-cards" style="display: flex; gap: 16px; margin: 16px 0; flex-wrap: wrap;">
<el-card class="card-item" style="flex: 1; min-width: 200px;">
<div class="card-content">
<p class="card-label">停机次数</p>
<p class="card-value">{{ stopCount }}</p>
</div>
</el-card>
<el-card class="card-item" style="flex: 1; min-width: 200px;">
<div class="card-content">
<p class="card-label">停机总时长</p>
<p class="card-value">{{ formatDuration(totalStopDuration) }}</p>
</div>
</el-card>
<el-card class="card-item" style="flex: 1; min-width: 200px;">
<div class="card-content">
<p class="card-label">作业率</p>
<p class="card-value">{{ operationRate }}%</p>
</div>
</el-card>
</div>
<!-- 图表区域 -->
<div class="charts-container" style="display: flex; gap: 16px; margin: 16px 0; flex-wrap: wrap;">
<!-- 按天汇总折线图 -->
<el-card style="flex: 1; min-width: 400px;">
<template #header>
<div class="chart-header">
<span>按天停机统计</span>
</div>
</template>
<div ref="lineChart" style="width: 100%; height: 400px;"></div>
</el-card>
<!-- 按类型汇总饼图 -->
<el-card style="flex: 1; min-width: 400px;">
<template #header>
<div class="chart-header">
<span>按停机类型统计</span>
</div>
</template>
<div ref="pieChart" style="width: 100%; height: 400px;"></div>
</el-card>
</div>
<!-- 停机统计表格 -->
<el-table
:data="stoppageList"
border
@@ -33,14 +77,8 @@
</template>
</el-table-column>
<el-table-column prop="insdate" label="数据录入时间" align="center" width="200" />
<!-- 生产相关附属字段 -->
<el-table-column prop="coilid" label="钢卷号" align="center" width="120" />
<el-table-column prop="shift" label="班次" align="center" width="100" />
<!-- <el-table-column prop="crew" label="班组人员" align="center" width="120" />
<el-table-column prop="area" label="区域" align="center" width="100" />
<el-table-column prop="unit" label="机组" align="center" width="100" />
<el-table-column prop="seton" label="开机人" align="center" width="100" /> -->
<!-- 备注字段 -->
<el-table-column prop="remark" label="备注" align="center" min-width="220" show-overflow-tooltip />
</el-table>
</div>
@@ -48,6 +86,14 @@
<script>
import { listStoppage } from '@/api/lines/zinc/stoppage'
// 引入ECharts核心及需要的组件
import * as echarts from 'echarts/core'
import { LineChart, PieChart } from 'echarts/charts'
import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
import { CanvasRenderer } from 'echarts/renderers'
// 注册ECharts组件
echarts.use([TitleComponent, TooltipComponent, LegendComponent, GridComponent, LineChart, PieChart, CanvasRenderer])
export default {
data() {
@@ -57,9 +103,16 @@ export default {
loading: false, // 加载状态
month: '', // 选中的月份 yyyy-MM格式
queryParams: {
startDate: '', // 开始时间 yyyy-MM-dd✅ 与后端字段名一致
endDate: '' // 结束时间 yyyy-MM-dd✅ 与后端字段名一致
}
startDate: '', // 开始时间 yyyy-MM-dd
endDate: '' // 结束时间 yyyy-MM-dd
},
// 指标卡数据
stopCount: 0, // 停机次数
totalStopDuration: 0, // 停机总时长(分钟)
operationRate: 0, // 作业率(百分比)
// 图表实例
lineChartInstance: null, // 折线图实例
pieChartInstance: null // 饼图实例
}
},
created() {
@@ -67,8 +120,22 @@ export default {
this.initDefaultMonth()
},
mounted() {
// 初始化图表实例
this.initCharts()
// 页面加载时,默认查询当月停机数据
this.listStoppage()
// 监听窗口大小变化,自适应图表
window.addEventListener('resize', this.resizeCharts)
},
beforeDestroy() {
// 销毁图表实例,防止内存泄漏
if (this.lineChartInstance) {
this.lineChartInstance.dispose()
}
if (this.pieChartInstance) {
this.pieChartInstance.dispose()
}
window.removeEventListener('resize', this.resizeCharts)
},
methods: {
// 格式化持续时间:将分钟数转换为"X天X小时X分钟"格式
@@ -94,6 +161,7 @@ export default {
return result || '0分钟'
},
/** 初始化默认月份和起止时间 */
initDefaultMonth() {
const now = new Date()
@@ -129,6 +197,254 @@ export default {
this.listStoppage()
},
/** 初始化图表实例 */
initCharts() {
// 初始化折线图
this.lineChartInstance = echarts.init(this.$refs.lineChart)
// 初始化饼图
this.pieChartInstance = echarts.init(this.$refs.pieChart)
},
/** 图表自适应大小 */
resizeCharts() {
if (this.lineChartInstance) {
this.lineChartInstance.resize()
}
if (this.pieChartInstance) {
this.pieChartInstance.resize()
}
},
/** 计算核心指标数据 */
calculateIndicators() {
// 1. 计算停机次数
this.stopCount = this.stoppageList.length
// 2. 计算停机总时长(分钟)
this.totalStopDuration = this.stoppageList.reduce((sum, item) => {
const duration = Number(item.duration) || 0
return sum + duration
}, 0)
// 3. 计算作业率:作业率 = (当月总分钟数 - 停机总分钟数) / 当月总分钟数 * 100%
if (this.month) {
const [year, month] = this.month.split('-').map(Number)
// 获取当月的总天数
const daysInMonth = new Date(year, month, 0).getDate()
// 当月总分钟数
const totalMinutesInMonth = daysInMonth * 24 * 60
// 作业时长(分钟)
const operationMinutes = totalMinutesInMonth - this.totalStopDuration
// 计算作业率保留2位小数
this.operationRate = totalMinutesInMonth > 0
? (operationMinutes / totalMinutesInMonth * 100).toFixed(2)
: 0
} else {
this.operationRate = 0
}
},
/** 工具函数:获取两个日期之间的所有日期(含起止日期) */
getAllDatesBetween(startDateStr, endDateStr) {
const dates = []
const start = new Date(startDateStr)
const end = new Date(endDateStr)
// 只保留日期部分,清除时分秒
start.setHours(0, 0, 0, 0)
end.setHours(0, 0, 0, 0)
let current = new Date(start)
while (current <= end) {
// 转成 MM月dd日 格式
const month = current.getMonth() + 1
const day = current.getDate()
dates.push(`${month}${day}`)
// 日期+1
current.setDate(current.getDate() + 1)
}
return dates
},
/** 处理折线图数据(按天汇总停机次数和时长,跨天记录每天+1 */
handleLineChartData() {
if (!this.stoppageList.length) {
// 无数据时的提示
this.lineChartInstance.setOption({
title: { text: '暂无停机数据', left: 'center' },
xAxis: { data: [] },
series: [{ data: [] }, { data: [] }]
})
return
}
// 1. 构建当月所有日期的数组用于x轴
const [year, month] = this.month.split('-').map(Number)
const daysInMonth = new Date(year, month, 0).getDate()
const dateLabels = Array.from({ length: daysInMonth }, (_, i) => `${month}${i+1}`)
// 2. 初始化所有日期的统计值为0
const dayMap = {}
dateLabels.forEach(date => {
dayMap[date] = { count: 0, duration: 0 }
})
// 3. 遍历停机数据,按【时间区间内所有日期】统计次数,按【开始日期】统计时长
this.stoppageList.forEach(item => {
if (!item.startDate || !item.endDate) return
// 获取停机区间内的所有日期
const relateDates = this.getAllDatesBetween(item.startDate, item.endDate)
// 遍历所有涉及日期,次数+1
relateDates.forEach(dateKey => {
if (dayMap[dateKey]) {
dayMap[dateKey].count += 1
}
})
// 时长:按开始日期统计(如需按天拆分时长,可在此处处理)
const startMonth = new Date(item.startDate).getMonth() + 1
const startDay = new Date(item.startDate).getDate()
const startDateKey = `${startMonth}${startDay}`
if (dayMap[startDateKey]) {
dayMap[startDateKey].duration += Number(item.duration) || 0
}
})
// 4. 提取x轴和series数据
const xAxisData = dateLabels
const countData = xAxisData.map(date => dayMap[date].count)
const durationData = xAxisData.map(date => dayMap[date].duration)
// 5. 设置折线图配置
const lineOption = {
tooltip: {
trigger: 'axis',
formatter: (params) => {
const date = params[0].name
const count = params[0].value
const duration = this.formatDuration(params[1].value)
return `${date}<br/>停机次数:${count}<br/>停机时长:${duration}`
}
},
legend: {
data: ['停机次数', '停机时长(分钟)'],
top: 0
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: xAxisData
},
yAxis: [
{
type: 'value',
name: '停机次数',
min: 0,
axisLabel: {
formatter: '{value}'
}
},
{
type: 'value',
name: '停机时长(分钟)',
min: 0,
axisLabel: {
formatter: '{value}'
},
position: 'right'
}
],
series: [
{
name: '停机次数',
type: 'line',
smooth: true,
data: countData
},
{
name: '停机时长(分钟)',
type: 'line',
smooth: true,
yAxisIndex: 1,
data: durationData
}
]
}
this.lineChartInstance.setOption(lineOption)
},
/** 处理饼图数据(按停机类型汇总) */
handlePieChartData() {
if (!this.stoppageList.length) {
// 无数据时的提示
this.pieChartInstance.setOption({
title: { text: '暂无停机数据', left: 'center' },
series: [{ data: [] }]
})
return
}
// 1. 按停机类型分组统计时长
const typeMap = {}
this.stoppageList.forEach(item => {
const type = item.stopType || '未知类型'
const duration = Number(item.duration) || 0
typeMap[type] = (typeMap[type] || 0) + duration
})
// 2. 转换为饼图数据格式
const pieData = Object.entries(typeMap).map(([name, value]) => ({
name,
value,
label: {
show: true,
formatter: '{b}: {c}分钟 ({d}%)'
}
}))
// 3. 设置饼图配置
const pieOption = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c}分钟 ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left',
top: 'center'
},
series: [
{
name: '停机时长',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
label: {
show: false
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: pieData
}
]
}
this.pieChartInstance.setOption(pieOption)
},
/** 核心:查询停机统计列表数据,带时间筛选参数 */
async listStoppage() {
this.loading = true
@@ -138,11 +454,23 @@ export default {
// 适配后端返回格式:数组/分页对象都兼容
this.stoppageList = res.data || res || []
this.total = res.total || this.stoppageList.length
// 计算核心指标
this.calculateIndicators()
// 更新图表数据
this.handleLineChartData()
this.handlePieChartData()
} catch (err) {
this.$message.error('查询停机统计数据失败!')
console.error('停机统计查询异常:', err)
this.stoppageList = []
this.total = 0
// 重置指标和图表
this.stopCount = 0
this.totalStopDuration = 0
this.operationRate = 0
this.handleLineChartData()
this.handlePieChartData()
} finally {
this.loading = false
}
@@ -162,4 +490,34 @@ export default {
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
/* 指标卡样式 */
.indicator-cards {
width: 100%;
}
.card-item {
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.card-content {
text-align: center;
padding: 8px 0;
}
.card-label {
font-size: 14px;
color: #666;
margin: 0 0 8px 0;
}
.card-value {
font-size: 24px;
font-weight: 600;
color: #1989fa;
margin: 0;
}
/* 图表样式 */
.charts-container {
width: 100%;
}
.chart-header {
font-weight: 600;
font-size: 14px;
}
</style>