feat(roll report): 添加磨削报表产线维度分析与多维度趋势展示

1. 新增lineId字段到磨削VO及查询SQL,支持产线数据统计
2. 增加按天/周/月切换的磨削趋势图表,自动适配时间区间
3. 新增月度产线汇总表格,支持按产线统计磨削次数与总量
4. 新增汇总数据导出Excel功能
This commit is contained in:
砂糖
2026-07-06 13:44:05 +08:00
parent f164fa5b12
commit 223c0361ab
3 changed files with 186 additions and 5 deletions

View File

@@ -13,6 +13,7 @@ import java.util.Date;
public class MesRollGrindVo {
private Long grindId;
private Long lineId;
private Long rollId;
private String rollNo;

View File

@@ -3,7 +3,7 @@
<mapper namespace="com.klp.mes.roll.mapper.MesRollGrindMapper">
<select id="selectByRollId" resultType="com.klp.mes.roll.domain.vo.MesRollGrindVo">
SELECT grind_id, roll_id, roll_no, grind_time, team,
SELECT grind_id, line_id, roll_id, roll_no, grind_time, team,
dia_before, dia_after, grind_amount, roll_shape,
flaw_result,
CASE WHEN hardness = '未倒角' THEN 0 ELSE CAST(hardness AS DECIMAL(10,1)) END AS hardness,
@@ -14,7 +14,7 @@
</select>
<select id="selectList" resultType="com.klp.mes.roll.domain.vo.MesRollGrindVo">
SELECT g.grind_id, g.roll_id, g.roll_no, g.grind_time, g.team,
SELECT g.grind_id, g.line_id, g.roll_id, g.roll_no, g.grind_time, g.team,
g.dia_before, g.dia_after, g.grind_amount, g.roll_shape,
g.flaw_result,
CASE WHEN g.hardness = '未倒角' THEN 0 ELSE CAST(g.hardness AS DECIMAL(10,1)) END AS hardness,

View File

@@ -56,7 +56,12 @@
<el-col :span="14">
<div class="chart-card">
<div class="chart-header">
<span class="chart-title"><i class="el-icon-data-line" /> 每日磨削趋势</span>
<span class="chart-title"><i class="el-icon-data-line" /> {{ trendChartTitle }}</span>
<el-radio-group v-model="trendDimension" size="mini" @change="onTrendDimensionChange" style="margin-left:12px">
<el-radio-button label="day">按天</el-radio-button>
<el-radio-button label="week">按周</el-radio-button>
<el-radio-button label="month">按月</el-radio-button>
</el-radio-group>
</div>
<div ref="trendChartRef" class="chart-container" style="height:340px"></div>
<div v-if="!dailyTrend.length" class="chart-empty">暂无数据</div>
@@ -131,6 +136,38 @@
<el-empty v-else description="暂无磨削记录" />
</div>
<!-- 月度产线汇总二维表 -->
<div class="table-card mt16">
<div class="card-header">
<span class="card-title"><i class="el-icon-s-data" /> 月度产线汇总</span>
<span class="card-subtitle" v-if="monthlyPivot.length">{{ monthlyPivotMonths.length }} 个月 · {{ monthlyPivot.length }} 条产线</span>
<el-button size="mini" type="primary" plain icon="el-icon-download" style="margin-left:8px" @click="exportMonthlyPivot" :disabled="!monthlyPivot.length">导出</el-button>
</div>
<div style="overflow-x:auto" v-if="monthlyPivot.length">
<el-table :data="monthlyPivot" size="small" border stripe style="min-width:100%"
:header-cell-style="{ textAlign: 'center', fontWeight: 600 }">
<el-table-column label="产线" prop="lineName" fixed="left" width="140" align="center" />
<el-table-column v-for="m in monthlyPivotMonths" :key="m" :label="m" align="center">
<el-table-column label="磨削次数" align="center" min-width="80">
<template slot-scope="{row}">{{ row[m + '_count'] || 0 }}</template>
</el-table-column>
<el-table-column label="磨削量(mm)" align="right" min-width="110">
<template slot-scope="{row}">{{ (row[m + '_amount'] || 0).toFixed(2) }}</template>
</el-table-column>
</el-table-column>
<el-table-column label="合计" align="center">
<el-table-column label="磨削次数" align="center" min-width="80">
<template slot-scope="{row}">{{ row.totalCount }}</template>
</el-table-column>
<el-table-column label="磨削量(mm)" align="right" min-width="110">
<template slot-scope="{row}">{{ row.totalAmount.toFixed(2) }}</template>
</el-table-column>
</el-table-column>
</el-table>
</div>
<el-empty v-else description="暂无磨削记录" />
</div>
</div>
</template>
@@ -139,12 +176,19 @@ import * as echarts from 'echarts'
import * as XLSX from 'xlsx'
import { getRollStats, listRollInfo } from '@/api/mes/roll/rollInfo'
import { listRollGrindAll } from '@/api/mes/roll/rollGrind'
import { mapGetters } from 'vuex'
import rollLineMixin from '../rollLineMixin'
export default {
name: 'RollReport',
mixins: [rollLineMixin],
dicts: ['mes_roll_operator'],
computed: {
...mapGetters(['productionLines']),
trendChartTitle() {
return { day: '每日磨削趋势', week: '每周磨削趋势', month: '每月磨削趋势' }[this.trendDimension] || '磨削趋势'
}
},
data() {
const now = new Date()
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
@@ -161,7 +205,10 @@ export default {
trendChart: null,
pieChart: null,
_beginTime: fmt(sevenDaysAgo),
_endTime: fmt(now)
_endTime: fmt(now),
monthlyPivot: [],
monthlyPivotMonths: [],
trendDimension: 'day'
}
},
watch: {
@@ -211,10 +258,20 @@ export default {
const allRecords = grindRes.data || []
// 4. 汇总计算
this.autoDetectTrendDimension()
this.computeSummary(allRecords, rollMap)
this.computeDailyTrend(allRecords)
this.computeTrendData(allRecords)
this.computeRollTypeDist(allRecords, rollMap)
this.computeOperatorStats(allRecords, rollMap)
// 5. 月度产线汇总(跨全部产线,不受当前 lineId 限制)
await this.$store.dispatch('productionLine/getProductionLines')
const allParams = {}
if (this._beginTime) allParams.beginTime = this._beginTime
if (this._endTime) allParams.endTime = this._endTime + ' 23:59:59'
const allGrindRes = await listRollGrindAll(allParams)
this.computeMonthlyPivot(allGrindRes.data || [])
this.updateCharts()
} catch (e) {
console.error('加载报表数据失败', e)
@@ -249,6 +306,56 @@ export default {
})
this.dailyTrend = Object.values(dateMap).sort((a, b) => a.grindDate.localeCompare(b.grindDate))
},
computeWeeklyTrend(records) {
const weekMap = {}
const pad = n => String(n).padStart(2, '0')
records.forEach(r => {
if (!r.grindTime) return
const d = new Date(r.grindTime)
const dayOfWeek = d.getDay() || 7
const monday = new Date(d)
monday.setDate(d.getDate() - dayOfWeek + 1)
const sunday = new Date(monday)
sunday.setDate(monday.getDate() + 6)
const key = `${monday.getFullYear()}-${pad(monday.getMonth() + 1)}-${pad(monday.getDate())}`
const label = `${pad(monday.getMonth() + 1)}-${pad(monday.getDate())}~${pad(sunday.getMonth() + 1)}-${pad(sunday.getDate())}`
if (!weekMap[key]) weekMap[key] = { grindDate: label, grindCount: 0, totalGrindAmount: 0, _sort: key }
weekMap[key].grindCount++
weekMap[key].totalGrindAmount += Number(r.grindAmount || 0)
})
this.dailyTrend = Object.values(weekMap).sort((a, b) => a._sort.localeCompare(b._sort))
},
computeMonthlyTrend(records) {
const monthMap = {}
records.forEach(r => {
if (!r.grindTime) return
const m = r.grindTime.substring(0, 7)
if (!monthMap[m]) monthMap[m] = { grindDate: m, grindCount: 0, totalGrindAmount: 0 }
monthMap[m].grindCount++
monthMap[m].totalGrindAmount += Number(r.grindAmount || 0)
})
this.dailyTrend = Object.values(monthMap).sort((a, b) => a.grindDate.localeCompare(b.grindDate))
},
computeTrendData(records) {
this._cachedRecords = records
if (this.trendDimension === 'week') this.computeWeeklyTrend(records)
else if (this.trendDimension === 'month') this.computeMonthlyTrend(records)
else this.computeDailyTrend(records)
},
autoDetectTrendDimension() {
const [b, e] = this.dateRange
if (!b || !e) return
const days = (new Date(e) - new Date(b)) / (24 * 60 * 60 * 1000)
if (days <= 31) this.trendDimension = 'day'
else if (days <= 90) this.trendDimension = 'week'
else this.trendDimension = 'month'
},
onTrendDimensionChange() {
if (this._cachedRecords) {
this.computeTrendData(this._cachedRecords)
this.updateTrendChart()
}
},
computeRollTypeDist(records, rollMap) {
const typeMap = {}
records.forEach(r => {
@@ -420,6 +527,78 @@ export default {
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
},
computeMonthlyPivot(records) {
const lineMonthMap = {}
const monthSet = new Set()
records.forEach(r => {
if (!r.grindTime) return
const month = r.grindTime.substring(0, 7)
const lid = Number(r.lineId) || 0
if (!lineMonthMap[lid]) {
const line = this.productionLines.find(l => Number(l.lineId) === lid)
lineMonthMap[lid] = { lineId: lid, lineName: line ? line.lineName : (r.lineName || '未知产线'), months: {} }
}
if (!lineMonthMap[lid].months[month]) {
lineMonthMap[lid].months[month] = { grindCount: 0, totalGrindAmount: 0 }
}
lineMonthMap[lid].months[month].grindCount++
lineMonthMap[lid].months[month].totalGrindAmount += Number(r.grindAmount || 0)
monthSet.add(month)
})
const months = [...monthSet].sort()
const rows = this.productionLines.map(line => {
const ld = lineMonthMap[line.lineId]
const row = { lineId: line.lineId, lineName: line.lineName, totalCount: 0, totalAmount: 0 }
months.forEach(m => {
if (ld && ld.months[m]) {
row[m + '_count'] = ld.months[m].grindCount
row[m + '_amount'] = Number(ld.months[m].totalGrindAmount.toFixed(2))
row.totalCount += ld.months[m].grindCount
row.totalAmount += ld.months[m].totalGrindAmount
} else {
row[m + '_count'] = 0
row[m + '_amount'] = 0
}
})
row.totalAmount = Number(row.totalAmount.toFixed(2))
return row
})
this.monthlyPivotMonths = months
this.monthlyPivot = rows
},
exportMonthlyPivot() {
const months = this.monthlyPivotMonths
const header1 = ['产线']
const header2 = ['']
months.forEach(m => { header1.push(m, ''); header2.push('磨削次数', '磨削量(mm)') })
header1.push('合计', '')
header2.push('磨削次数', '磨削量(mm)')
const rows = [header1, header2]
this.monthlyPivot.forEach(row => {
const r = [row.lineName]
months.forEach(m => { r.push(row[m + '_count'] || 0, (row[m + '_amount'] || 0).toFixed(2)) })
r.push(row.totalCount, row.totalAmount.toFixed(2))
rows.push(r)
})
const ws = XLSX.utils.aoa_to_sheet(rows)
// merge header row 1 for month groups
ws['!merges'] = []
let col = 1
months.forEach(() => { ws['!merges'].push({ s: { r: 0, c: col }, e: { r: 0, c: col + 1 } }); col += 2 })
ws['!merges'].push({ s: { r: 0, c: col }, e: { r: 0, c: col + 1 } })
const wb = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(wb, ws, '月度产线汇总')
const buf = XLSX.write(wb, { bookType: 'xlsx', type: 'array' })
const blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `月度产线汇总_${this._beginTime || ''}_${this._endTime ? this._endTime.substring(0, 10) : ''}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
}
}
@@ -431,6 +610,7 @@ export default {
min-height: 100%;
}
.mb16 { margin-bottom: 16px; }
.mt16 { margin-top: 16px; }
/* 筛选栏 */
.filter-panel {