feat(roll report): 添加磨削报表产线维度分析与多维度趋势展示
1. 新增lineId字段到磨削VO及查询SQL,支持产线数据统计 2. 增加按天/周/月切换的磨削趋势图表,自动适配时间区间 3. 新增月度产线汇总表格,支持按产线统计磨削次数与总量 4. 新增汇总数据导出Excel功能
This commit is contained in:
@@ -13,6 +13,7 @@ import java.util.Date;
|
||||
public class MesRollGrindVo {
|
||||
|
||||
private Long grindId;
|
||||
private Long lineId;
|
||||
private Long rollId;
|
||||
private String rollNo;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user