Files
klp-oa/klp-ui/src/views/mes/roll/grind/index.vue
砂糖 6f7a85025d feat(mes/roll): 新增轧辊磨削记录通用查询和报表页面
1. 新增通用查询接口,支持按轧辊ID、产线ID、时间范围筛选磨削记录
2. 重构后端列表接口,支持不传轧辊ID查询全部记录
3. 修复硬度字段类型转换问题,将未倒角转为0数值
4. 新增磨辊报表页面,支持统计分析和图表展示
2026-05-25 17:31:46 +08:00

590 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-container grind-page">
<div class="grind-layout">
<!-- 产线 Tab -->
<div class="line-tabs">
<div
:class="['lt-item', filterLineId === null ? 'lt-item--active' : '']"
@click="handleLineTab(null)"
>全部</div>
<div
v-for="l in sortedProductionLines"
:key="l.lineId"
:class="['lt-item', filterLineId === l.lineId ? 'lt-item--active' : '']"
@click="handleLineTab(l.lineId)"
>{{ l.lineName }}</div>
</div>
<!-- 轧辊列表 -->
<div class="grind-left">
<el-card shadow="never" class="grind-card h-full">
<div slot="header" class="card-header">
<span class="card-title"><i class="el-icon-s-order" /> 轧辊列表</span>
</div>
<!-- 搜索过滤 -->
<div class="roll-filter">
<el-input v-model="filterNo" size="small" placeholder="编号搜索" prefix-icon="el-icon-search"
clearable @input="filterRolls" style="margin-bottom:8px" />
<el-radio-group v-model="filterType" size="small" @change="filterRolls" style="margin-bottom:8px">
<el-radio-button label="">全部</el-radio-button>
<el-radio-button label="WR">WR</el-radio-button>
<el-radio-button label="BR">BR</el-radio-button>
</el-radio-group>
</div>
<div v-loading="rollLoading" class="roll-list">
<div
v-for="r in filteredRolls"
:key="r.rollId"
:class="['roll-item', selectedRollId === r.rollId ? 'roll-item--active' : '']"
@click="selectRoll(r)"
>
<div class="ri-no">{{ r.rollNo }}</div>
<div class="ri-meta">
<el-tag size="mini" :type="r.rollType === 'WR' ? 'primary' : 'warning'">{{ r.rollType }}</el-tag>
<span :class="['ri-status', 'st-' + r.status]">{{ statusLabel(r.status) }}</span>
<span class="ri-dia">φ{{ r.currentDia != null ? r.currentDia : r.initialDia }}</span>
</div>
<div v-if="filterLineId === null && r.lineName" class="ri-line">{{ r.lineName }}</div>
</div>
<div v-if="!rollLoading && filteredRolls.length === 0" class="roll-empty">暂无数据</div>
</div>
</el-card>
</div>
<!-- 右侧台账 -->
<div class="grind-right">
<template v-if="!selectedRoll">
<div class="grind-empty"><i class="el-icon-arrow-left" /> 请从左侧选择一个轧辊</div>
</template>
<template v-else>
<!-- 轧辊基本信息头 -->
<el-card shadow="never" class="grind-card" style="margin-bottom:12px">
<div class="roll-header-grid">
<div class="rh-item"><span class="rh-k">轧辊编号</span><span class="rh-v bold">{{ selectedRoll.rollNo }}</span></div>
<div class="rh-item"><span class="rh-k">辊型</span><span class="rh-v">{{ selectedRoll.rollType === 'WR' ? '工作辊' : '支撑辊' }}</span></div>
<div class="rh-item"><span class="rh-k">材质</span><span class="rh-v">{{ selectedRoll.material || '—' }}</span></div>
<div class="rh-item"><span class="rh-k">初始辊径</span><span class="rh-v">{{ selectedRoll.initialDia != null ? selectedRoll.initialDia + ' mm' : '—' }}</span></div>
<div class="rh-item"><span class="rh-k">当前辊径</span><span class="rh-v bold accent">{{ effectiveCurrentDia != null ? effectiveCurrentDia + ' mm' : '—' }}</span></div>
<div class="rh-item"><span class="rh-k">最小辊径</span><span class="rh-v">{{ selectedRoll.minDia != null ? selectedRoll.minDia + ' mm' : '—' }}</span></div>
<div class="rh-item"><span class="rh-k">磨削次数</span><span class="rh-v">{{ tableData.length ? tableData.length + ' 次' : '0 次' }}</span></div>
<div class="rh-item"><span class="rh-k">粗糙度</span><span class="rh-v">{{ selectedRoll.roughness != null ? selectedRoll.roughness + ' μm' : '—' }}</span></div>
<div class="rh-item"><span class="rh-k">凸度</span><span class="rh-v">{{ selectedRoll.crown != null ? selectedRoll.crown + ' mm' : '—' }}</span></div>
<div class="rh-item"><span class="rh-k">状态</span>
<span :class="['rh-v', 'st-' + selectedRoll.status]">{{ statusLabel(selectedRoll.status) }}</span>
</div>
<div class="rh-item"><span class="rh-k">制造日期</span><span class="rh-v">{{ selectedRoll.manufactureDate || '—' }}</span></div>
<div class="rh-item"><span class="rh-k">备注</span><span class="rh-v">{{ selectedRoll.remark || '—' }}</span></div>
</div>
</el-card>
<!-- 磨削台账行内编辑 -->
<el-card shadow="never" class="grind-card">
<div slot="header" class="card-header">
<span class="card-title"><i class="el-icon-document" /> 磨削台账</span>
<el-button type="primary" size="mini" icon="el-icon-plus"
style="margin-left:auto" :disabled="!!editRow" @click="startAdd">新增磨削记录</el-button>
</div>
<el-table v-loading="grindLoading" :data="tableData" size="small" border
style="width:100%" :row-class-name="rowClassName">
<el-table-column label="序号" type="index" width="46" align="center" />
<!-- 磨削时间 -->
<el-table-column label="磨削时间" align="center" width="200">
<template slot-scope="{row}">
<el-date-picker v-if="isEditing(row)" v-model="editRow.grindTime"
type="datetime" size="mini" value-format="yyyy-MM-dd HH:mm:ss"
style="width:182px" placeholder="请选择" />
<span v-else>{{ row.grindTime }}</span>
</template>
</el-table-column>
<!-- 班组 -->
<el-table-column label="班组" align="center" width="80">
<template slot-scope="{row}">
<el-input v-if="isEditing(row)" v-model="editRow.team"
size="mini" placeholder="甲班" style="width:64px" />
<span v-else>{{ row.team || '' }}</span>
</template>
</el-table-column>
<!-- 磨前直径 -->
<el-table-column label="磨前径(mm)" align="center" width="100">
<template slot-scope="{row}">
<el-input v-if="isEditing(row)" v-model.number="editRow.diaBefore"
type="number" size="mini" style="width:80px" />
<span v-else>{{ row.diaBefore }}</span>
</template>
</el-table-column>
<!-- 磨后直径 -->
<el-table-column label="磨后径(mm)" align="center" width="100">
<template slot-scope="{row}">
<el-input v-if="isEditing(row)" v-model.number="editRow.diaAfter"
type="number" size="mini" style="width:80px" />
<span v-else>{{ row.diaAfter }}</span>
</template>
</el-table-column>
<!-- 磨削量只读计算 -->
<el-table-column label="磨削量(mm)" align="center" width="88">
<template slot-scope="{row}">
<span v-if="isEditing(row)" class="computed-val">
{{ grindAmountOf(editRow) }}
</span>
<span v-else>{{ row.grindAmount }}</span>
</template>
</el-table-column>
<!-- 辊型 -->
<el-table-column label="辊型" align="center" width="100">
<template slot-scope="{row}">
<el-select v-if="isEditing(row)" v-model="editRow.rollShape"
size="mini" style="width:84px">
<el-option label="平" value="平" />
<el-option label="凸" value="凸" />
<el-option label="凹" value="凹" />
</el-select>
<span v-else>{{ row.rollShape || '' }}</span>
</template>
</el-table-column>
<!-- 探伤结果 -->
<el-table-column label="探伤结果" align="center" width="110">
<template slot-scope="{row}">
<el-select v-if="isEditing(row)" v-model="editRow.flawResult"
size="mini" style="width:94px">
<el-option label="合格" value="合格" />
<el-option label="不合格" value="不合格" />
</el-select>
<el-tag v-else size="mini" :type="row.flawResult === '合格' ? 'success' : 'danger'">
{{ row.flawResult || '—' }}
</el-tag>
</template>
</el-table-column>
<!-- 硬度 -->
<el-table-column label="硬度" align="center" width="80">
<template slot-scope="{row}">
<el-input v-if="isEditing(row)" v-model.number="editRow.hardness"
type="number" size="mini" style="width:64px" />
<span v-else>{{ row.hardness || '' }}</span>
</template>
</el-table-column>
<!-- 操作者自动填入仅展示 -->
<el-table-column label="操作者" align="center" width="100">
<template slot-scope="{row}">
<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> -->
</template>
</el-table-column>
<!-- 备注 -->
<el-table-column label="备注" align="left" width="120">
<template slot-scope="{row}">
<el-input v-if="isEditing(row)" v-model="editRow.remark"
size="mini" placeholder="选填" />
<span v-else class="remark-text">{{ row.remark || '' }}</span>
</template>
</el-table-column>
<!-- 操作列 -->
<el-table-column label="操作" align="center" width="100" fixed="right">
<template slot-scope="{row}">
<template v-if="isEditing(row)">
<el-button size="mini" type="text" icon="el-icon-check"
:loading="grindSaving" @click="saveRow">保存</el-button>
<el-button size="mini" type="text" icon="el-icon-close"
style="color:#909399" @click="cancelEdit">取消</el-button>
</template>
<template v-else>
<el-button size="mini" type="text" icon="el-icon-edit"
:disabled="!!editRow" @click="startEdit(row)">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete"
style="color:#c5221f" :disabled="!!editRow" @click="handleDelete(row)">删除</el-button>
</template>
</template>
</el-table-column>
</el-table>
<!-- 月度汇总 -->
<div class="monthly-wrap" v-if="grindList.length > 0">
<div class="monthly-title">
<span>{{ currentYear }} 年月度汇总</span>
<el-button-group size="mini" style="margin-left:8px">
<el-button icon="el-icon-arrow-left" @click="changeYear(-1)" />
<el-button icon="el-icon-arrow-right" @click="changeYear(1)" />
</el-button-group>
</div>
<el-table :data="monthlyList" size="mini" border style="width:100%;margin-top:8px">
<el-table-column label="月份" prop="month" align="center" width="90" />
<el-table-column label="磨削次数" prop="grindCount" align="center" width="90" />
<el-table-column label="累计磨削量(mm)" prop="totalGrindAmount" align="center" min-width="120" />
</el-table>
</div>
</el-card>
</template>
</div>
</div>
</div>
</template>
<script>
import { listRollInfo, getRollInfo } from '@/api/mes/roll/rollInfo'
import { listRollGrind, addRollGrind, updateRollGrind, delRollGrind, getMonthlyStats } from '@/api/mes/roll/rollGrind'
import { listProductionLine } from '@/api/wms/productionLine'
import rollLineMixin from '../rollLineMixin'
export default {
name: 'GrindRoom',
mixins: [rollLineMixin],
data() {
return {
// 产线列表
productionLines: [],
filterLineId: null,
lineTabOrder: [], // 本地记录的点击顺序(最近点过的在前)
// 左侧辊列表
rollLoading: false,
allRolls: [],
filteredRolls: [],
filterNo: '',
filterType: '',
// 右侧选中辊
selectedRollId: null,
selectedRoll: null,
// 磨削记录
grindLoading: false,
grindList: [],
// 月度汇总
currentYear: new Date().getFullYear(),
monthlyList: [],
// 行内编辑editRow 不为 null 时表示当前正在编辑/新增的行数据
// __isNew: true → 新增行;存在 grindId → 修改行
editRow: null,
grindSaving: false
}
},
computed: {
// 当前登录用户名RuoYi Plus 的 store 路径)
currentUserName() {
return this.$store.state.user.name || this.$store.getters.name || ''
},
// 产线 tab 按本地点击顺序排序
sortedProductionLines() {
const order = this.lineTabOrder
return [...this.productionLines].sort((a, b) => {
const ai = order.indexOf(a.lineId)
const bi = order.indexOf(b.lineId)
if (ai === -1 && bi === -1) return 0
if (ai === -1) return 1
if (bi === -1) return -1
return ai - bi
})
},
// 有效当前辊径:优先取 currentDia无则取最新磨削记录的磨后径
effectiveCurrentDia() {
if (this.selectedRoll && this.selectedRoll.currentDia != null) {
return parseFloat(this.selectedRoll.currentDia)
}
if (this.grindList.length > 0) {
const latest = [...this.grindList].sort((a, b) => {
const ta = a.grindTime ? new Date(a.grindTime).getTime() : 0
const tb = b.grindTime ? new Date(b.grindTime).getTime() : 0
return tb - ta
})[0]
if (latest && latest.diaAfter != null) return parseFloat(latest.diaAfter)
}
return null
},
// 表格数据:新增时在顶部插入一个编辑行占位
tableData() {
if (this.editRow && this.editRow.__isNew) {
return [this.editRow, ...this.grindList]
}
return this.grindList
}
},
created() {},
methods: {
onLineResolved() {
this.filterLineId = this.lineId
const uid = this.$store.state.user?.userId || 0
try { this.lineTabOrder = JSON.parse(localStorage.getItem(`grind_line_order_${uid}`) || '[]') } catch {}
listProductionLine({ pageNum: 1, pageSize: 100 }).then(res => {
this.productionLines = res.rows || []
})
this.loadRolls()
},
// ── 轧辊列表 ──────────────────────────────────────
loadRolls() {
this.rollLoading = true
listRollInfo({ pageNum: 1, pageSize: 500, lineId: this.filterLineId }).then(res => {
this.allRolls = res.rows || []
this.filterRolls()
}).finally(() => { this.rollLoading = false })
},
handleLineTab(lineId) {
if (this.filterLineId === lineId) return
this.filterLineId = lineId
if (lineId !== null) {
this.lineTabOrder = [lineId, ...this.lineTabOrder.filter(id => id !== lineId)]
const uid = this.$store.state.user?.userId || 0
localStorage.setItem(`grind_line_order_${uid}`, JSON.stringify(this.lineTabOrder))
}
if (this.editRow) this.cancelEdit()
this.selectedRollId = null
this.selectedRoll = null
this.grindList = []
this.loadRolls()
},
filterRolls() {
this.filteredRolls = this.allRolls.filter(r => {
const matchNo = !this.filterNo || r.rollNo.includes(this.filterNo)
const matchType = !this.filterType || r.rollType === this.filterType
return matchNo && matchType
})
},
selectRoll(r) {
if (this.editRow) this.cancelEdit()
this.selectedRollId = r.rollId
getRollInfo(r.rollId).then(res => {
this.selectedRoll = res.data || r
const idx = this.allRolls.findIndex(x => x.rollId === r.rollId)
if (idx !== -1) this.$set(this.allRolls, idx, { ...this.allRolls[idx], ...res.data })
}).catch(() => { this.selectedRoll = r })
this.loadGrindList(r.rollId)
},
// ── 磨削记录 ──────────────────────────────────────
loadGrindList(rollId) {
this.grindLoading = true
listRollGrind(rollId).then(res => {
this.grindList = res.data || []
}).finally(() => {
this.grindLoading = false
this.loadMonthlyStats()
})
},
loadMonthlyStats() {
if (!this.selectedRollId) return
getMonthlyStats(this.selectedRollId, this.currentYear).then(res => {
this.monthlyList = (res.data || []).map(r => ({
month: r.month, grindCount: r.grindCount, totalGrindAmount: r.totalGrindAmount
}))
})
},
changeYear(delta) {
this.currentYear += delta
this.loadMonthlyStats()
},
// ── 行内编辑 ──────────────────────────────────────
isEditing(row) {
if (!this.editRow) return false
if (row.__isNew && this.editRow.__isNew) return true
return !!row.grindId && row.grindId === this.editRow.grindId
},
rowClassName({ row }) {
return this.isEditing(row) ? 'editing-row' : ''
},
startAdd() {
const now = new Date()
const pad = n => String(n).padStart(2, '0')
const grindTime = `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())} ` +
`${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
this.editRow = {
__isNew: true,
rollId: this.selectedRollId,
grindTime,
team: undefined,
diaBefore: this.effectiveCurrentDia != null ? this.effectiveCurrentDia : undefined,
diaAfter: undefined,
rollShape: '平',
flawResult: '合格',
hardness: undefined,
operator: this.currentUserName,
remark: undefined
}
},
startEdit(row) {
this.editRow = { ...row, operator: row.operator || this.currentUserName }
},
cancelEdit() {
this.editRow = null
},
saveRow() {
const r = this.editRow
if (!r.grindTime) { this.$modal.msgWarning('请填写磨削时间'); return }
if (r.diaBefore == null) { this.$modal.msgWarning('请填写磨前直径'); return }
if (r.diaAfter == null) { this.$modal.msgWarning('请填写磨后直径'); return }
// 自动带入操作人
r.operator = this.currentUserName
this.grindSaving = true
const isNew = !!r.__isNew
const payload = { ...r }
delete payload.__isNew
const api = isNew ? addRollGrind : updateRollGrind
api(payload).then(() => {
this.$modal.msgSuccess(isNew ? '新增成功' : '修改成功')
this.editRow = null
this.loadGrindList(this.selectedRollId)
getRollInfo(this.selectedRollId).then(res => {
this.selectedRoll = res.data
const idx = this.allRolls.findIndex(x => x.rollId === this.selectedRollId)
if (idx !== -1) this.$set(this.allRolls, idx, { ...this.allRolls[idx], ...res.data })
})
}).finally(() => { this.grindSaving = false })
},
handleDelete(row) {
this.$modal.confirm('确认删除该磨削记录?').then(() => {
return delRollGrind(row.grindId)
}).then(() => {
this.$modal.msgSuccess('已删除')
this.loadGrindList(this.selectedRollId)
})
},
// ── 计算磨削量 ──────────────────────────────────────
grindAmountOf(row) {
const b = parseFloat(row.diaBefore)
const a = parseFloat(row.diaAfter)
if (!isNaN(b) && !isNaN(a)) return (b - a).toFixed(2)
return '—'
},
// 辅助
statusLabel(s) {
return { Online: '在线', Standby: '备用', Offline: '离线', Scrapped: '报废' }[s] || s
}
}
}
</script>
<style scoped>
.grind-page { background: #f4f5f7; height: 100%; }
.grind-layout { display: flex; gap: 12px; height: 100%; }
/* 产线 Tab 列 */
.line-tabs {
width: 64px;
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 2px;
padding-top: 2px;
}
.lt-item {
padding: 10px 4px;
text-align: center;
font-size: 12px;
color: #5f6368;
background: #fff;
border: 1px solid #dcdee0;
border-radius: 4px;
cursor: pointer;
word-break: break-all;
line-height: 1.4;
transition: all .15s;
}
.lt-item:hover { background: #f0f6ff; color: #409eff; border-color: #c6d9f5; }
.lt-item--active {
background: #409eff;
color: #fff;
border-color: #409eff;
font-weight: 600;
}
/* 轧辊列表 */
.grind-left { width: 220px; flex-shrink: 0; }
.grind-right { flex: 1; min-width: 0; }
.grind-card { border: 1px solid #dcdee0; border-radius: 4px; }
.h-full { height: calc(100vh - 120px); display: flex; flex-direction: column; }
.card-header { display: flex; align-items: center; gap: 8px; }
.card-title { font-size: 13px; font-weight: 600; color: #3d4b5c; }
/* 辊列表 */
.roll-filter { padding: 0 0 4px; }
::v-deep .el-card .el-card__body {
overflow-y: scroll;
overflow-x: hidden;
}
.roll-list { overflow-y: auto; flex: 1; min-height: 0; padding: 0; }
.roll-item { padding: 8px 12px; cursor: pointer; border-bottom: 1px solid #f0f2f5; }
.roll-item:hover { background: #f5f7fa; }
.roll-item--active { background: #e8f4ff !important; border-left: 3px solid #409eff; }
.ri-no { font-family: 'Consolas', monospace; font-size: 13px; font-weight: 600; color: #1f2329; }
.ri-meta { display: flex; align-items: center; gap: 6px; margin-top: 3px; }
.ri-dia { font-size: 11px; color: #9aa0a6; }
.ri-line { font-size: 10px; color: #b0b3bb; margin-top: 2px; }
.ri-status { font-size: 11px; }
.roll-empty { text-align: center; color: #c0c4cc; padding: 20px 0; font-size: 12px; }
/* 状态色 */
.st-Online { color: #0a7c42; }
.st-Standby { color: #d4860a; }
.st-Offline { color: #9aa0a6; }
.st-Scrapped { color: #c5221f; }
/* 空状态 */
.grind-empty { display: flex; align-items: center; justify-content: center;
height: 300px; color: #c0c4cc; font-size: 14px; gap: 6px; }
/* 辊头信息格 */
.roll-header-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px 16px; }
.rh-item { display: flex; flex-direction: column; gap: 2px; }
.rh-k { font-size: 11px; color: #9aa0a6; }
.rh-v { font-size: 13px; color: #3d4b5c; }
.rh-v.bold { font-weight: 600; }
.rh-v.accent { color: #0a7c42; }
/* 月度汇总 */
.monthly-wrap { margin-top: 16px; border-top: 1px solid #f0f2f5; padding-top: 12px; }
.monthly-title { font-size: 12px; color: #5f6368; display: flex; align-items: center; }
/* 行内编辑 */
.computed-val { color: #409eff; font-weight: 600; font-size: 13px; }
.auto-operator { color: #0a7c42; font-size: 12px; }
.remark-text { font-size: 12px; color: #5f6368; }
/* 去掉 number input 的默认上下箭头,保持表格整洁 */
.el-table :deep(input[type=number]::-webkit-inner-spin-button),
.el-table :deep(input[type=number]::-webkit-outer-spin-button) { -webkit-appearance: none; }
.el-table :deep(input[type=number]) { -moz-appearance: textfield; }
</style>
<!-- 编辑行高亮 scoped作用于 el-table row-class-name -->
<style>
.el-table .editing-row { background: #fffbf0 !important; }
.el-table .editing-row:hover > td { background: #fffbf0 !important; }
</style>