feat(mes/roll): 新增轧辊磨削记录通用查询和报表页面

1. 新增通用查询接口,支持按轧辊ID、产线ID、时间范围筛选磨削记录
2. 重构后端列表接口,支持不传轧辊ID查询全部记录
3. 修复硬度字段类型转换问题,将未倒角转为0数值
4. 新增磨辊报表页面,支持统计分析和图表展示
This commit is contained in:
2026-05-25 17:31:46 +08:00
parent 95c23462c9
commit 6f7a85025d
8 changed files with 505 additions and 8 deletions

View File

@@ -26,10 +26,17 @@ public class MesRollGrindController extends BaseController {
private final IMesRollGrindService iMesRollGrindService;
/** 查询某轧辊的磨削记录列表 */
/** 查询磨削记录列表(不传 rollId 时返回全部,支持时间范围和产线筛选) */
@GetMapping("/list")
public R<List<MesRollGrindVo>> list(@NotNull(message = "轧辊ID不能为空") @RequestParam Long rollId) {
return R.ok(iMesRollGrindService.listByRoll(rollId));
public R<List<MesRollGrindVo>> list(
@RequestParam(required = false) Long rollId,
@RequestParam(required = false) Long lineId,
@RequestParam(required = false) String beginTime,
@RequestParam(required = false) String endTime) {
if (rollId != null) {
return R.ok(iMesRollGrindService.listByRoll(rollId));
}
return R.ok(iMesRollGrindService.listByQuery(null, lineId, beginTime, endTime));
}
/** 按年份查询月度汇总 */

View File

@@ -16,6 +16,12 @@ public interface MesRollGrindMapper extends BaseMapperPlus<MesRollGrindMapper, M
/** 查询某轧辊全部磨削记录(按时间升序) */
List<MesRollGrindVo> selectByRollId(@Param("rollId") Long rollId);
/** 通用查询(所有参数可选) */
List<MesRollGrindVo> selectList(@Param("rollId") Long rollId,
@Param("lineId") Long lineId,
@Param("beginTime") String beginTime,
@Param("endTime") String endTime);
/** 按年份统计每月磨削量 { month, grindCount, totalGrindAmount } */
List<Map<String, Object>> selectMonthlyStats(@Param("rollId") Long rollId, @Param("year") int year);
}

View File

@@ -14,6 +14,9 @@ public interface IMesRollGrindService {
/** 查询某轧辊全部磨削记录 */
List<MesRollGrindVo> listByRoll(Long rollId);
/** 通用查询(所有参数可选) */
List<MesRollGrindVo> listByQuery(Long rollId, Long lineId, String beginTime, String endTime);
/** 新增磨削记录,同步更新轧辊当前直径和磨削次数 */
Long addGrind(MesRollGrindBo bo);

View File

@@ -34,6 +34,11 @@ public class MesRollGrindServiceImpl implements IMesRollGrindService {
return baseMapper.selectByRollId(rollId);
}
@Override
public List<MesRollGrindVo> listByQuery(Long rollId, Long lineId, String beginTime, String endTime) {
return baseMapper.selectList(rollId, lineId, beginTime, endTime);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long addGrind(MesRollGrindBo bo) {

View File

@@ -5,12 +5,29 @@
<select id="selectByRollId" resultType="com.klp.mes.roll.domain.vo.MesRollGrindVo">
SELECT grind_id, roll_id, roll_no, grind_time, team,
dia_before, dia_after, grind_amount, roll_shape,
flaw_result, hardness, operator, remark, create_time
flaw_result,
CASE WHEN hardness = '未倒角' THEN 0 ELSE CAST(hardness AS DECIMAL(10,1)) END AS hardness,
operator, remark, create_time
FROM mes_roll_grind
WHERE del_flag = 0 AND roll_id = #{rollId}
ORDER BY grind_time ASC, grind_id ASC
</select>
<select id="selectList" resultType="com.klp.mes.roll.domain.vo.MesRollGrindVo">
SELECT grind_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,
operator, remark, create_time
FROM mes_roll_grind
WHERE del_flag = 0
<if test="rollId != null">AND roll_id = #{rollId}</if>
<if test="lineId != null">AND line_id = #{lineId}</if>
<if test="beginTime != null and beginTime != ''">AND grind_time &gt;= #{beginTime}</if>
<if test="endTime != null and endTime != ''">AND grind_time &lt;= #{endTime}</if>
ORDER BY grind_time ASC, grind_id ASC
</select>
<select id="selectMonthlyStats" resultType="map">
SELECT
DATE_FORMAT(grind_time, '%Y-%m') AS month,

View File

@@ -1,10 +1,15 @@
import request from '@/utils/request'
// 查询轧辊磨削记录列表
// 查询轧辊磨削记录列表(传 rollId 查单个轧辊,不传则查全部,支持时间范围)
export function listRollGrind(rollId) {
return request({ url: '/mes/rollGrind/list', method: 'get', params: { rollId } })
}
// 通用磨削记录查询(所有参数可选)
export function listRollGrindAll(params) {
return request({ url: '/mes/rollGrind/list', method: 'get', params })
}
// 查询月度汇总
export function getMonthlyStats(rollId, year) {
return request({ url: '/mes/rollGrind/monthlyStats', method: 'get', params: { rollId, year } })

View File

@@ -177,11 +177,14 @@
</el-table-column>
<!-- 操作者自动填入仅展示 -->
<el-table-column label="操作者" align="center" width="72">
<el-table-column label="操作者" align="center" width="100">
<template slot-scope="{row}">
<span :class="isEditing(row) ? 'auto-operator' : ''">
<el-input v-if="isEditing(row)" v-model="editRow.operator"
size="mini" placeholder="选填" />
<span v-else class="remark-text">{{ row.operator || '' }}</span>
<!-- <span :class="isEditing(row) ? 'auto-operator' : ''">
{{ isEditing(row) ? currentUserName : (row.operator || '—') }}
</span>
</span> -->
</template>
</el-table-column>

View File

@@ -0,0 +1,451 @@
<template>
<div class="app-container roll-report" v-loading="loading">
<!-- 筛选栏 -->
<div class="filter-panel mb16">
<el-form :inline="true" size="small">
<el-form-item label="磨削时间">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
:default-time="['00:00:00', '23:59:59']"
style="width:260px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
<el-button icon="el-icon-refresh" @click="resetFilter">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 统计汇总 -->
<el-row :gutter="16" class="mb16">
<el-col :span="6">
<div class="summary-box">
<div class="summary-label">磨削总次数</div>
<div class="summary-value">{{ summary.grindCount }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="summary-box">
<div class="summary-label">磨削总量(mm)</div>
<div class="summary-value accent">{{ summary.totalGrindAmount }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="summary-box">
<div class="summary-label">涉及轧辊数</div>
<div class="summary-value">{{ summary.rollCount }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="summary-box">
<div class="summary-label">磨辊人数</div>
<div class="summary-value">{{ summary.operatorCount }}</div>
</div>
</el-col>
</el-row>
<!-- 图表区域 -->
<el-row :gutter="16" class="mb16">
<el-col :span="14">
<div class="chart-card">
<div class="chart-header">
<span class="chart-title"><i class="el-icon-data-line" /> 每日磨削趋势</span>
</div>
<div ref="trendChartRef" class="chart-container" style="height:340px"></div>
<div v-if="!dailyTrend.length" class="chart-empty">暂无数据</div>
</div>
</el-col>
<el-col :span="10">
<div class="chart-card">
<div class="chart-header">
<span class="chart-title"><i class="el-icon-pie-chart" /> 辊型磨削分布</span>
</div>
<div ref="pieChartRef" class="chart-container" style="height:340px"></div>
<div v-if="!rollTypeDist.length" class="chart-empty">暂无数据</div>
</div>
</el-col>
</el-row>
<!-- 磨辊人统计表 -->
<div class="table-card">
<div class="card-header">
<span class="card-title"><i class="el-icon-user" /> 磨辊人统计</span>
<span class="card-subtitle" v-if="operatorStats.length"> {{ operatorStats.length }} </span>
</div>
<el-table :data="operatorStats" size="small" border stripe style="width:100%"
v-if="operatorStats.length" :default-sort="{ prop: 'totalGrindAmount', order: 'descending' }">
<el-table-column label="序号" type="index" width="50" align="center" />
<el-table-column label="磨辊人" prop="operator" min-width="120" sortable />
<el-table-column label="磨削次数" prop="grindCount" width="110" align="center" sortable />
<el-table-column label="磨削总量(mm)" prop="totalGrindAmount" width="130" align="right" sortable>
<template slot-scope="{row}">{{ row.totalGrindAmount.toFixed(2) }}</template>
</el-table-column>
<el-table-column label="平均磨削量(mm)" prop="avgGrindAmount" width="140" align="right" sortable>
<template slot-scope="{row}">{{ row.avgGrindAmount.toFixed(2) }}</template>
</el-table-column>
</el-table>
<el-empty v-else description="暂无磨削记录" />
</div>
</div>
</template>
<script>
import * as echarts from 'echarts'
import { getRollStats, listRollInfo } from '@/api/mes/roll/rollInfo'
import { listRollGrindAll } from '@/api/mes/roll/rollGrind'
import rollLineMixin from '../rollLineMixin'
export default {
name: 'RollReport',
mixins: [rollLineMixin],
data() {
const now = new Date()
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
const pad = n => String(n).padStart(2, '0')
const fmt = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
return {
loading: false,
dateRange: [fmt(sevenDaysAgo), fmt(now)],
stats: {},
summary: { grindCount: 0, totalGrindAmount: 0, rollCount: 0, operatorCount: 0 },
dailyTrend: [],
rollTypeDist: [],
operatorStats: [],
trendChart: null,
pieChart: null,
_beginTime: fmt(sevenDaysAgo),
_endTime: fmt(now)
}
},
watch: {
lineId() {
this.loadData()
}
},
mounted() {
this.$nextTick(() => {
this.initCharts()
})
window.addEventListener('resize', this.resizeCharts)
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeCharts)
this.trendChart && this.trendChart.dispose()
this.pieChart && this.pieChart.dispose()
},
methods: {
onLineResolved() {
this.loadData()
},
async loadData() {
this.loading = true
try {
const [b, e] = this.dateRange
this._beginTime = b || ''
this._endTime = e || ''
// 1. 状态统计
if (this.lineId) {
const statsRes = await getRollStats(this.lineId)
this.stats = statsRes.data || {}
}
// 2. 获取全部轧辊(用于辊型映射)
const rollRes = await listRollInfo({ pageNum: 1, pageSize: 99999, lineId: this.lineId || undefined })
const rolls = rollRes.rows || []
const rollMap = {}
rolls.forEach(r => { rollMap[r.rollId] = r })
// 3. 一次性获取全部磨削记录(后端支持可选参数)
const params = { lineId: this.lineId || undefined }
if (this._beginTime) params.beginTime = this._beginTime
if (this._endTime) params.endTime = this._endTime + ' 23:59:59'
const grindRes = await listRollGrindAll(params)
const allRecords = grindRes.data || []
// 4. 汇总计算
this.computeSummary(allRecords, rollMap)
this.computeDailyTrend(allRecords)
this.computeRollTypeDist(allRecords, rollMap)
this.computeOperatorStats(allRecords)
this.updateCharts()
} catch (e) {
console.error('加载报表数据失败', e)
} finally {
this.loading = false
}
},
computeSummary(records, rollMap) {
const rollSet = new Set()
const operatorSet = new Set()
let totalAmount = 0
records.forEach(r => {
if (r.rollId) rollSet.add(r.rollId)
if (r.operator) operatorSet.add(r.operator)
totalAmount += Number(r.grindAmount || 0)
})
this.summary = {
grindCount: records.length,
totalGrindAmount: totalAmount.toFixed(2),
rollCount: rollSet.size,
operatorCount: operatorSet.size
}
},
computeDailyTrend(records) {
const dateMap = {}
records.forEach(r => {
if (!r.grindTime) return
const d = r.grindTime.substring(0, 10)
if (!dateMap[d]) dateMap[d] = { grindDate: d, grindCount: 0, totalGrindAmount: 0 }
dateMap[d].grindCount++
dateMap[d].totalGrindAmount += Number(r.grindAmount || 0)
})
this.dailyTrend = Object.values(dateMap).sort((a, b) => a.grindDate.localeCompare(b.grindDate))
},
computeRollTypeDist(records, rollMap) {
const typeMap = {}
records.forEach(r => {
const roll = rollMap[r.rollId]
const type = roll ? (roll.rollType === 'WR' ? '工作辊' : roll.rollType === 'BR' ? '支撑辊' : roll.rollType || '未知') : '未知'
if (!typeMap[type]) typeMap[type] = { name: type, value: 0, grindCount: 0 }
typeMap[type].value += Number(r.grindAmount || 0)
typeMap[type].grindCount++
})
this.rollTypeDist = Object.values(typeMap)
},
computeOperatorStats(records) {
const opMap = {}
records.forEach(r => {
const name = r.operator || '未知'
if (!opMap[name]) opMap[name] = { operator: name, grindCount: 0, totalGrindAmount: 0 }
opMap[name].grindCount++
opMap[name].totalGrindAmount += Number(r.grindAmount || 0)
})
this.operatorStats = Object.values(opMap).map(item => ({
...item,
totalGrindAmount: Number(item.totalGrindAmount.toFixed(2)),
avgGrindAmount: item.grindCount > 0 ? Number((item.totalGrindAmount / item.grindCount).toFixed(2)) : 0
})).sort((a, b) => b.totalGrindAmount - a.totalGrindAmount)
},
initCharts() {
this.initTrendChart()
this.initPieChart()
},
initTrendChart() {
const dom = this.$refs.trendChartRef
if (!dom) return
if (this.trendChart) this.trendChart.dispose()
this.trendChart = echarts.init(dom)
this.trendChart.setOption({
tooltip: { trigger: 'axis' },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', data: [], axisLabel: { rotate: 45, fontSize: 11 } },
yAxis: [
{ type: 'value', name: '磨削次数', minInterval: 1 },
{ type: 'value', name: '磨削量(mm)', position: 'right' }
],
series: [
{ name: '磨削次数', type: 'bar', data: [], barMaxWidth: 24 },
{ name: '磨削量(mm)', type: 'line', yAxisIndex: 1, smooth: true, data: [] }
],
color: ['#409eff', '#0a7c42']
})
},
initPieChart() {
const dom = this.$refs.pieChartRef
if (!dom) return
if (this.pieChart) this.pieChart.dispose()
this.pieChart = echarts.init(dom)
this.pieChart.setOption({
tooltip: { trigger: 'item', formatter: '{b}: {c}mm ({d}%)' },
series: [{
type: 'pie', radius: ['30%', '60%'], center: ['50%', '50%'],
label: { formatter: '{b}\n{d}%' },
data: [],
emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0,0,0,0.5)' } }
}],
color: ['#409eff', '#e6a23c', '#909399']
})
},
updateCharts() {
this.updateTrendChart()
this.updatePieChart()
},
updateTrendChart() {
if (!this.trendChart) this.initTrendChart()
const dates = this.dailyTrend.map(d => d.grindDate)
const counts = this.dailyTrend.map(d => d.grindCount)
const amounts = this.dailyTrend.map(d => Number(d.totalGrindAmount.toFixed(2)))
this.trendChart.setOption({
xAxis: { data: dates },
series: [
{ data: counts },
{ data: amounts }
]
})
this.trendChart.resize()
},
updatePieChart() {
if (!this.pieChart) this.initPieChart()
const data = this.rollTypeDist.map(d => ({ name: d.name, value: Number(d.value.toFixed(2)) }))
this.pieChart.setOption({ series: [{ data }] })
this.pieChart.resize()
},
resizeCharts() {
this.trendChart && this.trendChart.resize()
this.pieChart && this.pieChart.resize()
},
handleQuery() {
this.loadData()
},
resetFilter() {
const now = new Date()
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
const pad = n => String(n).padStart(2, '0')
const fmt = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
this.dateRange = [fmt(sevenDaysAgo), fmt(now)]
this.loadData()
}
}
}
</script>
<style scoped>
.roll-report {
background: #f4f5f7;
min-height: 100%;
}
.mb16 { margin-bottom: 16px; }
/* 筛选栏 */
.filter-panel {
background: #fff;
border: 1px solid #dcdee0;
border-radius: 4px;
padding: 12px 20px;
}
/* 统计看板 */
.stat-panel {
display: flex;
background: #fff;
border: 1px solid #dcdee0;
border-radius: 4px;
overflow: hidden;
}
.stat-item {
flex: 1;
display: flex;
align-items: center;
padding: 18px 20px;
gap: 14px;
position: relative;
}
.stat-divider {
position: absolute;
right: 0; top: 16px; bottom: 16px;
width: 1px;
background: #e4e6ea;
}
.stat-icon-wrap {
width: 42px; height: 42px;
border-radius: 4px;
display: flex; align-items: center; justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.total-icon { background: #edf0f3; color: #3d4b5c; }
.online-icon { background: #e8f5ef; color: #0a7c42; }
.standby-icon { background: #fdf3e3; color: #8b5c00; }
.offline-icon { background: #f0f1f2; color: #5f6368; }
.scrapped-icon { background: #fdecea; color: #a61c00; }
.stat-body { display: flex; flex-direction: column; }
.stat-num {
font-size: 26px;
font-weight: 700;
line-height: 1;
color: #1f2329;
font-variant-numeric: tabular-nums;
letter-spacing: -0.5px;
}
.online-num { color: #0a7c42; }
.standby-num { color: #8b5c00; }
.offline-num { color: #5f6368; }
.scrapped-num { color: #a61c00; }
.stat-label {
font-size: 12px;
color: #8f9099;
margin-top: 4px;
letter-spacing: 1px;
}
/* 汇总框 */
.summary-box {
background: #fff;
border: 1px solid #dcdee0;
border-radius: 4px;
padding: 18px 24px;
text-align: center;
}
.summary-label {
font-size: 12px;
color: #8f9099;
margin-bottom: 6px;
}
.summary-value {
font-size: 28px;
font-weight: 700;
color: #1f2329;
font-variant-numeric: tabular-nums;
}
.summary-value.accent { color: #409eff; }
/* 图表卡片 */
.chart-card {
background: #fff;
border: 1px solid #dcdee0;
border-radius: 4px;
padding: 16px 20px;
position: relative;
}
.chart-header, .card-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.chart-title, .card-title {
font-size: 14px;
font-weight: 600;
color: #3d4b5c;
}
.card-subtitle {
font-size: 12px;
color: #9aa0a6;
margin-left: auto;
}
.chart-container { width: 100%; }
.chart-empty {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
color: #c0c4cc;
font-size: 13px;
}
/* 表格卡片 */
.table-card {
background: #fff;
border: 1px solid #dcdee0;
border-radius: 4px;
padding: 16px 20px;
}
</style>