Files
klp-oa/klp-ui/src/views/mes/roll/grind/index.vue
砂糖 c5ce33f71a feat(mes/roll): 重构轧辊磨削页面布局并补充备注字段
1.  重构轧辊磨削页面的整体布局,调整为左侧产线Tab+右侧主体的结构
2.  在实体类和BO类中新增remark备注字段
3.  移除运单页面的冗余备注搜索框
4.  优化轧辊列表的检索和展示逻辑,新增厂家筛选选项
5.  补充磨削班组的下拉选择选项,新增字典配置
6.  优化页面样式和交互细节
2026-06-01 11:27:05 +08:00

732 lines
27 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-main">
<!-- 顶部检索栏 -->
<div class="search-bar">
<el-input
v-model="filterNo"
size="small"
placeholder="辊号检索"
prefix-icon="el-icon-search"
clearable
style="width:180px"
@input="filterRolls"
/>
<el-radio-group v-model="filterType" size="small" @change="filterRolls">
<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-button label="CR">CR</el-radio-button>
</el-radio-group>
<el-radio-group v-model="filterManufacturer" size="small" @change="filterRolls">
<el-radio-button label="">全部厂家</el-radio-button>
<el-radio-button v-for="mfr in manufacturerOptions" :key="mfr" :label="mfr">{{ mfr }}</el-radio-button>
</el-radio-group>
</div>
<!-- 下方左右布局 -->
<div class="content-area">
<!-- 左侧轧辊列表 -->
<div class="roll-list-panel">
<el-card shadow="never" class="panel-card">
<div slot="header" class="card-header">
<span class="card-title"><i class="el-icon-s-order" /> 轧辊列表</span>
<span class="card-count">{{ filteredRolls.length }}</span>
</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="r.manufacturer" class="ri-manufacturer">{{ r.manufacturer }}</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="detail-panel" v-loading="grindLoading">
<template v-if="!selectedRoll">
<div class="grind-empty"><i class="el-icon-arrow-left" /> 请从左侧选择一个轧辊</div>
</template>
<template v-else>
<!-- 轧辊基本信息 -->
<el-card shadow="never" class="detail-card" style="margin-bottom:10px">
<div slot="header" class="card-header">
<span class="card-title"><i class="el-icon-info" /> 轧辊明细</span>
</div>
<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">{{ { WR: '工作辊', BR: '支撑辊', CR: '中间辊' }[selectedRoll.rollType] || '—' }}</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="detail-card" body-style="padding:10px;overflow:auto">
<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
: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="100">
<template slot-scope="{row}">
<el-select
v-if="isEditing(row)"
v-model="editRow.team"
size="mini"
placeholder="班组"
clearable
filterable
allow-create
>
<el-option label="甲班" value="甲班" />
<el-option label="乙班" value="乙班" />
</el-select>
<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="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="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="140">
<template slot-scope="{row}">
<el-select
v-if="isEditing(row)"
v-model="editRow.operator"
size="mini"
placeholder="选填"
clearable
filterable
allow-create
>
<el-option
v-for="item in dict.type.mes_roll_operator"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<span v-else class="remark-text">{{ 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 v-if="grindList.length > 0" class="monthly-wrap">
<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>
</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],
dicts: ['mes_roll_operator'],
data() {
return {
productionLines: [],
filterLineId: null,
lineTabOrder: [],
rollLoading: false,
allRolls: [],
filteredRolls: [],
filterNo: '',
filterType: '',
filterManufacturer: '',
selectedRollId: null,
selectedRoll: null,
grindLoading: false,
grindList: [],
currentYear: new Date().getFullYear(),
monthlyList: [],
editRow: null,
grindSaving: false
}
},
computed: {
currentUserName() {
return this.$store.state.user.name || this.$store.getters.name || ''
},
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
})
},
manufacturerOptions() {
const set = new Set()
this.allRolls.forEach(r => {
if (r.manufacturer) set.add(r.manufacturer)
})
return [...set].sort()
},
effectiveCurrentDia() {
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
}
},
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 { /* ignore */ }
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
const matchMfr = !this.filterManufacturer || r.manufacturer === this.filterManufacturer
return matchNo && matchType && matchMfr
})
},
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 }
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-main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
/* 顶部检索栏 */
.search-bar {
display: flex;
align-items: center;
gap: 20px;
padding: 10px 14px;
background: #fff;
border: 1px solid #dcdee0;
border-radius: 4px;
flex-shrink: 0;
}
/* 下方内容区:左右分栏 */
.content-area {
flex: 1;
min-height: 0;
display: flex;
gap: 10px;
}
/* 左侧轧辊列表面板 */
.roll-list-panel {
width: 220px;
flex-shrink: 0;
height: calc(100vh - 140px);
overflow-y: auto;
}
/* 右侧明细面板 */
.detail-panel {
flex: 1;
min-width: 0;
overflow-y: auto;
}
/* 明细卡片 */
.detail-card {
border: 1px solid #dcdee0;
border-radius: 4px;
}
.detail-card ::v-deep .el-card__body {
overflow: auto;
}
/* 通用卡片 */
.panel-card {
border: 1px solid #dcdee0;
border-radius: 4px;
height: 100%;
display: flex;
flex-direction: column;
}
.panel-card ::v-deep .el-card__body {
flex: 1;
min-height: 0;
overflow-y: auto;
}
.card-header { display: flex; align-items: center; gap: 8px; }
.card-title { font-size: 13px; font-weight: 600; color: #3d4b5c; }
.card-count { font-size: 11px; color: #9aa0a6; background: #f0f2f5; padding: 1px 8px; border-radius: 10px; margin-left: auto; }
/* 轧辊列表 */
.roll-list { 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-manufacturer { font-size: 10px; color: #909399; margin-top: 2px; }
.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; }
.remark-text { font-size: 12px; color: #5f6368; }
.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>
<style>
.el-table .editing-row { background: #fffbf0 !important; }
.el-table .editing-row:hover > td { background: #fffbf0 !important; }
.el-table ::v-deep .cell {
padding: 0;
}
</style>