修复酸轧实绩提交问题,规程重新完成逻辑

This commit is contained in:
2026-05-21 13:41:21 +08:00
parent 2c9cc6241f
commit eb5601ade3
6 changed files with 677 additions and 557 deletions

View File

@@ -197,11 +197,94 @@
<el-button type="primary" size="mini" :loading="excoilLoading" @click="handleFindSearch">查找</el-button>
<el-button size="mini" @click="handleFindReset">重置</el-button>
</div>
<!-- 规程关联 -->
<div class="spec-bind-block">
<div class="panel-title">规程关联</div>
<template v-if="selectedRow">
<div class="bind-status">
<span class="bind-label">已关联版本</span>
<span v-if="coilBindingLoading" class="bind-val muted">加载中</span>
<el-tag v-else-if="coilBinding" type="success" size="mini" effect="plain">
{{ coilBinding.versionCode }}
</el-tag>
<span v-else class="bind-val muted">未关联</span>
</div>
<el-button
size="mini"
:type="coilBinding ? 'default' : 'primary'"
icon="el-icon-link"
style="width:100%;margin-top:8px"
@click="openSpecBindDialog"
>{{ coilBinding ? '重新关联' : '关联规程版本' }}</el-button>
</template>
<span v-else class="bind-val muted" style="font-size:12px">请先选择钢卷</span>
</div>
</div>
</div>
</div>
<quality-report-dialog ref="qualityReport" />
<!-- 规程版本选择弹窗 -->
<el-dialog
title="关联规程版本"
:visible.sync="specBindDialog"
width="580px"
append-to-body
@closed="specBindSelectedId = null"
>
<div class="spec-bind-toolbar">
<el-switch
v-model="specBindShowAll"
active-text="全部版本"
inactive-text="仅生效版本"
style="margin-right:8px"
/>
<span class="spec-bind-hint"> {{ specVersionsForDialog.length }} </span>
</div>
<el-table
v-loading="specVersionLoading"
:data="specVersionsForDialog"
size="mini"
border
highlight-current-row
max-height="340"
style="margin-top:10px"
@row-click="row => specBindSelectedId = row.versionId"
>
<el-table-column width="36" align="center">
<template slot-scope="{ row }">
<i
v-if="specBindSelectedId === row.versionId"
class="el-icon-check"
style="color:#409eff;font-weight:700"
/>
</template>
</el-table-column>
<el-table-column label="规程名称" prop="specName" min-width="140" show-overflow-tooltip />
<el-table-column label="版本号" prop="versionCode" width="90" />
<el-table-column label="状态" align="center" width="80">
<template slot-scope="{ row }">
<el-tag v-if="row.isActive === 1" type="success" size="mini" effect="dark">生效中</el-tag>
<el-tag v-else-if="row.status === 'PUBLISHED'" size="mini" effect="plain">已发布</el-tag>
<el-tag v-else type="info" size="mini" effect="plain">草稿</el-tag>
</template>
</el-table-column>
<el-table-column label="规程编码" prop="specCode" width="130" show-overflow-tooltip />
</el-table>
<div slot="footer">
<el-button size="small" @click="specBindDialog = false">取消</el-button>
<el-button
size="small"
type="primary"
:disabled="!specBindSelectedId"
:loading="specBindLoading"
@click="confirmSpecBind"
>确认关联</el-button>
</div>
</el-dialog>
</div>
</template>
@@ -216,10 +299,11 @@ import {
getTimingRealtimeData,
getPresetSetupByCoilId
} from '@/api/l2/timing'
import { listProcessSpecVersion } from '@/api/wms/processSpecVersion'
import { listProcessPlan, addProcessPlan } from '@/api/wms/processPlan'
import { listProcessPlanParam, addProcessPlanParam, updateProcessPlanParam } from '@/api/wms/processPlanParam'
import { upsertProcessCoilRecord } from '@/api/wms/processCoilRecord'
import { listProcessSpecVersion, getProcessSpecVersion } from '@/api/wms/processSpecVersion'
import { listProcessCoilRecord, upsertProcessCoilRecord } from '@/api/wms/processCoilRecord'
import { listProcessSpec } from '@/api/wms/processSpec'
import { listProcessPlan } from '@/api/wms/processPlan'
import { listProcessPlanParam } from '@/api/wms/processPlanParam'
import { batchAddProcessAnomaly } from '@/api/wms/processAnomaly'
// 趋势参数树结构,对应 PLTCM_PRO_SEG 列名
@@ -266,10 +350,11 @@ const TREND_GROUPS = [
]
// V_VBDA_GAUGE 厚度曲线4 个图,列名来自 DDL
// 厚度图纵轴显示实测/设定值百分比,速度图保持原单位
const GAUGE_COLS = [
{ col: 'THICK0', title: '入口测厚仪 [mm]' },
{ col: 'THICK1', title: '1架出口厚度 [mm]' },
{ col: 'THICK4', title: '末架出口厚度 [mm]' },
{ col: 'THICK0', title: '入口测厚仪 [%]' },
{ col: 'THICK1', title: '1架出口厚度 [%]' },
{ col: 'THICK4', title: '末架出口厚度 [%]' },
{ col: 'EXIT_SPEED', title: '轧制速度 [m/min]' }
]
@@ -317,8 +402,9 @@ function calcYRange(vals) {
/**
* 生成折线图 option。
* extras: [{ name, data, color, dash }] — 上下限或参考线
* yAxisSuffix: 纵轴标签后缀,如 '%'
*/
function makeLine(title, xData, yData, extras = []) {
function makeLine(title, xData, yData, extras = [], yAxisSuffix = '') {
const allVals = [yData, ...extras.map(e => e.data)].flat()
const range = calcYRange(allVals)
const hasExtras = extras.length > 0
@@ -338,7 +424,12 @@ function makeLine(title, xData, yData, extras = []) {
? { data: [title, ...extras.map(e => e.name)], top: 4, right: 4,
textStyle: { fontSize: 9 }, itemWidth: 14, itemHeight: 8 }
: { show: false },
tooltip: { trigger: 'axis' },
tooltip: {
trigger: 'axis',
formatter: yAxisSuffix
? params => params.map(p => `${p.marker}${p.seriesName}: ${p.value != null ? p.value + yAxisSuffix : '—'}`).join('<br/>')
: undefined
},
grid: { top: hasExtras ? 44 : 36, bottom: 28, left: 8, right: 16, containLabel: true },
xAxis: {
type: 'category', data: xData,
@@ -346,7 +437,11 @@ function makeLine(title, xData, yData, extras = []) {
},
yAxis: {
type: 'value', min: range.min, max: range.max,
nameTextStyle: { fontSize: 10 }, axisLabel: { fontSize: 10 }
nameTextStyle: { fontSize: 10 },
axisLabel: {
fontSize: 10,
formatter: yAxisSuffix ? val => val + yAxisSuffix : undefined
}
},
dataZoom: [
{ type: 'inside', xAxisIndex: 0, zoomOnMouseWheel: true, moveOnMouseMove: true },
@@ -396,7 +491,33 @@ export default {
pagination: { page: 1, pageSize: 50, total: 0 },
topTableHeight: 'calc(40vh - 80px)',
chartInstances: [],
resizeHandler: null
resizeHandler: null,
// ── 规程关联 ──
coilBinding: null, // 当前选中钢卷已有的关联记录 { versionId, versionCode, ... }
coilBindingLoading: false,
specBindDialog: false,
specVersionLoading: false,
specVersionRawList: [], // 所有 wms_process_spec_version
specList: [], // 所有 wms_process_spec用于显示规程名
specBindSelectedId: null, // 弹窗内选中的 versionId
specBindShowAll: false, // true=显示全部版本false=只显示生效版本
specBindLoading: false
}
},
computed: {
/** 弹窗中展示的版本列表(含规程名,支持只看生效版本) */
specVersionsForDialog() {
const specMap = {}
this.specList.forEach(s => { specMap[s.specId] = s })
let list = this.specVersionRawList.map(v => ({
...v,
specCode: specMap[v.specId] ? specMap[v.specId].specCode : '—',
specName: specMap[v.specId] ? specMap[v.specId].specName : '—',
}))
if (!this.specBindShowAll) {
list = list.filter(v => v.isActive === 1)
}
return list
}
},
created() {
@@ -431,18 +552,15 @@ export default {
// 快速点击防重:每次点击递增版本号,旧的 sync 任务检测到版本号变更后自动放弃
const clickRev = ++this._clickRev
// 记录点击前的钢卷号,用于判断是否同一行重复点击
const prevExcoilId = this.selectedRow
? (this.selectedRow.EXCOILID || this.selectedRow.excoilid || null)
: null
this.selectedRow = row
this.segData = null
this.gaugeRows = null
this.shapeRows = null
this.presetData = null
this.selectedTrendParam = null
this.coilBinding = null
this.disposeAllCharts()
this.loadCoilBinding()
const encoilId = row.ENCOILID || row.encoilid
const excoilId = row.EXCOILID || row.excoilid
@@ -456,13 +574,6 @@ export default {
// 如果期间又点击了其他行则放弃后续操作
if (this._clickRev !== clickRev) return
// 同一钢卷重复点击:跳过规程同步,避免重复写入
const isSameCoil = excoilId && excoilId === prevExcoilId
if (!isSameCoil) {
// 后台静默同步到规程(不阻塞 UI
this.autoSyncToActiveSpec(excoilId || encoilId, clickRev)
}
await this.$nextTick()
// 加载完成后自动选中第一个趋势参数
if (this.activeTab === 'trend' && this.segData) {
@@ -624,6 +735,7 @@ export default {
},
// ── 厚度曲线 (V_VBDA_GAUGE) ──────────────────
// 有参考值的厚度图纵轴显示百分比:实测值 / 设定值 × 100
renderGaugeCharts() {
const rows = this.gaugeRows
if (!rows || !rows.length) return
@@ -633,33 +745,48 @@ export default {
const chartRefs = ['chartGauge1', 'chartGauge2', 'chartGauge3', 'chartGauge4']
const charts = chartRefs.map((ref, i) => {
const { col, title } = GAUGE_COLS[i]
const yData = rows.map(r => {
const v = getRowVal(r, col)
return v == null ? null : parseFloat(v.toFixed(4))
})
const extras = []
const refCol = refColMap[col]
if (refCol) {
const refData = rows.map(r => {
// ── 厚度图:转为百分比(实测 / 设定 × 100 ──
const yData = rows.map(r => {
const v = getRowVal(r, col)
const rv = getRowVal(r, refCol)
return rv == null ? null : parseFloat(rv.toFixed(4))
if (v == null || rv == null || rv === 0) return null
return parseFloat(((v / rv) * 100).toFixed(3))
})
// 上限 = REF + TOPLIMIT下限 = REF + BOTLIMITTOPLIMIT/BOTLIMIT 单位与测厚仪一致)
const upData = rows.map((r, j) => {
const extras = []
// 目标值恒为 100%
const refLine = rows.map(r => {
const rv = getRowVal(r, refCol)
return (rv != null && rv !== 0) ? 100 : null
})
// 上限百分比 = (REF + TOPLIMIT) / REF × 100
const upData = rows.map(r => {
const rv = getRowVal(r, refCol)
const tl = getRowVal(r, 'TOPLIMIT')
return rv == null ? null : parseFloat((rv + (tl ?? 3)).toFixed(4))
if (rv == null || rv === 0) return null
return parseFloat(((rv + (tl ?? 3)) / rv * 100).toFixed(3))
})
const loData = rows.map((r, j) => {
// 下限百分比 = (REF + BOTLIMIT) / REF × 100
const loData = rows.map(r => {
const rv = getRowVal(r, refCol)
const bl = getRowVal(r, 'BOTLIMIT')
return rv == null ? null : parseFloat((rv + (bl ?? -3)).toFixed(4))
if (rv == null || rv === 0) return null
return parseFloat(((rv + (bl ?? -3)) / rv * 100).toFixed(3))
})
if (refData.some(v => v != null)) extras.push({ name: '目标值', data: refData, color: '#909399', dash: false })
if (refLine.some(v => v != null)) extras.push({ name: '目标值(100%)', data: refLine, color: '#909399', dash: false })
if (upData.some(v => v != null)) extras.push({ name: '上限', data: upData, color: '#F56C6C', dash: true })
if (loData.some(v => v != null)) extras.push({ name: '下限', data: loData, color: '#67C23A', dash: true })
return this.makeChart(ref, makeLine(title, xData, yData, extras, '%'))
} else {
// ── 速度等无参考值的图:保持原始单位 ──
const yData = rows.map(r => {
const v = getRowVal(r, col)
return v == null ? null : parseFloat(v.toFixed(4))
})
return this.makeChart(ref, makeLine(title, xData, yData))
}
return this.makeChart(ref, makeLine(title, xData, yData, extras))
})
this.chartInstances = charts.filter(Boolean)
this.setupResize()
@@ -852,199 +979,13 @@ export default {
this.gaugeRows = null
this.shapeRows = null
this.selectedTrendParam = null
this.coilBinding = null
this.disposeAllCharts()
this.pagination.page = 1
this.loadExcoilCount()
this.loadExcoilList()
},
// ── 自动同步到生效规程 ────────────────────────
async autoSyncToActiveSpec(coilId, rev) {
if (!this.presetData && !this.segData) return
const guard = () => rev !== undefined && this._clickRev !== rev
try {
// ① 查找生效版本
const verRes = await listProcessSpecVersion({ isActive: 1, pageNum: 1, pageSize: 10 })
if (guard()) return
const activeVer = (verRes.rows || []).find(v => v.isActive === 1)
if (!activeVer) return
const versionId = activeVer.versionId
// ② 构建本次写入条目
const items = this.buildSpecSyncItems()
if (!items.length) return
// ③ 加载已有 plan 点位
const plansRes = await listProcessPlan({ versionId, pageNum: 1, pageSize: 500 })
if (guard()) return
const planMap = {}
;(plansRes.rows || []).forEach(p => { planMap[p.pointCode] = p })
// ④ 逐条 upsert 点位 & 参数,收集异常
const anomalies = []
const detectedAt = new Date().toISOString()
// 提取本次钢卷的 enCoilId入口卷号
const enCoilId = this.selectedRow ? (this.selectedRow.ENCOILID || this.selectedRow.encoilid || null) : null
for (const item of items) {
if (guard()) return
// 确保 plan 点位存在(幂等:插入失败时降级重查,应对并发竞态)
let planId
const ep = planMap[item.pointCode]
if (ep) {
planId = ep.planId
} else {
try {
const r = await addProcessPlan({
versionId,
segmentType: 'PROCESS',
segmentName: item.groupLabel,
pointName: item.pointName,
pointCode: item.pointCode,
sortOrder: 0
})
if (guard()) return
planId = r.data
// 写入本地 map同一次 sync 内不再重复插入
planMap[item.pointCode] = { planId }
} catch (_dupErr) {
// 唯一键冲突:该点位已由并发请求写入,重新查询获取真实 planId
const refetch = await listProcessPlan({ versionId, pageNum: 1, pageSize: 500 })
if (guard()) return
const found = (refetch.rows || []).find(p => p.pointCode === item.pointCode)
if (!found) continue // 极端情况:查不到则跳过本条目
planId = found.planId
planMap[item.pointCode] = found
}
}
// 查已存储参数
const prRes = await listProcessPlanParam({ planId, pageNum: 1, pageSize: 100 })
if (guard()) return
const stored = (prRes.rows || []).find(p => p.paramCode === item.paramCode)
// 异常检测:与已有上下限比对
if (stored) {
const sUp = stored.upperLimit != null ? Number(stored.upperLimit) : null
const sLo = stored.lowerLimit != null ? Number(stored.lowerLimit) : null
const aUp = item.upperLimit
const aLo = item.lowerLimit
const overTypes = []
if (sUp != null && aUp != null && aUp > sUp) overTypes.push('OVER_MAX')
if (sLo != null && aLo != null && aLo < sLo) overTypes.push('UNDER_MIN')
if (overTypes.length) {
anomalies.push({
versionId,
planId,
paramId: stored.paramId || null,
coilId,
enCoilId,
paramCode: item.paramCode,
paramName: item.paramName,
unit: item.unit,
anomalyType: overTypes.length === 2 ? 'BOTH' : overTypes[0],
storedTarget: stored.targetValue != null ? Number(stored.targetValue) : null,
storedUpper: sUp,
storedLower: sLo,
actualTarget: item.targetValue,
actualMax: aUp,
actualMin: aLo,
deviationMax: sUp != null && aUp != null ? parseFloat((aUp - sUp).toFixed(4)) : null,
deviationMin: sLo != null && aLo != null ? parseFloat((aLo - sLo).toFixed(4)) : null,
detectedAt
})
}
}
// 写入/更新参数
// target_value 始终覆盖反映最新L1设定
// upper/lower 仅首次写入null 时写入作为基线)
// actualSrcId / presetSrcId 仅首次写入
if (stored) {
await updateProcessPlanParam({
...stored,
targetValue: item.targetValue ?? stored.targetValue,
upperLimit: stored.upperLimit ?? item.upperLimit,
lowerLimit: stored.lowerLimit ?? item.lowerLimit,
unit: item.unit || stored.unit,
actualSrcId: stored.actualSrcId || enCoilId,
presetSrcId: stored.presetSrcId || coilId
})
} else {
await addProcessPlanParam({
planId,
paramCode: item.paramCode,
paramName: item.paramName,
targetValue: item.targetValue,
upperLimit: item.upperLimit,
lowerLimit: item.lowerLimit,
unit: item.unit,
actualSrcId: enCoilId,
presetSrcId: coilId
})
}
}
if (guard()) return
// ⑤ 写入钢卷服役记录(幂等 upsert
await upsertProcessCoilRecord({
versionId,
coilId,
enCoilId,
hasAnomaly: anomalies.length > 0 ? 1 : 0,
anomalyCnt: anomalies.length,
processTime: detectedAt
})
// ⑥ 持久化异常到数据库
if (anomalies.length) {
await batchAddProcessAnomaly(anomalies)
console.log(`[规程同步] 检测到 ${anomalies.length} 个参数异常,已写入数据库`)
}
} catch (e) {
console.warn('[规程同步] 后台同步失败:', e)
}
},
/** 从当前 segData + presetData 构建写入条目 */
buildSpecSyncItems() {
const items = []
for (const group of TREND_GROUPS) {
for (const item of group.children) {
const col = item.col
const maxArr = this.segData ? this.seg(col + 'MAX').filter(v => v != null) : []
const minArr = this.segData ? this.seg(col + 'MIN').filter(v => v != null) : []
const presetCol = TREND_PRESET_MAP[col]
let targetValue = null
if (presetCol && this.presetData) {
const sv = this.presetData[presetCol] !== undefined
? this.presetData[presetCol]
: this.presetData[presetCol.toLowerCase()]
if (sv != null && Number(sv) !== 0) targetValue = parseFloat(Number(sv).toFixed(4))
}
const upperLimit = maxArr.length ? parseFloat(Math.max(...maxArr).toFixed(4)) : null
const lowerLimit = minArr.length ? parseFloat(Math.min(...minArr).toFixed(4)) : null
if (targetValue == null && upperLimit == null && lowerLimit == null) continue
items.push({
groupLabel: group.label,
pointCode: col,
pointName: item.label,
paramCode: col,
paramName: item.label,
targetValue,
upperLimit,
lowerLimit,
unit: TREND_UNIT_MAP[col] || ''
})
}
}
return items
},
openQualityReport(row) {
// If the clicked row is already the selected row, use in-memory data
const isSame = this.selectedRow &&
@@ -1056,6 +997,178 @@ export default {
this.$refs.qualityReport.open(row, segData, gaugeRows, shapeRows, presetData)
},
// ── 规程关联 ─────────────────────────────────────────
/** 加载当前选中钢卷的已有规程关联记录 */
async loadCoilBinding() {
if (!this.selectedRow) return
const coilId = this.selectedRow.EXCOILID || this.selectedRow.excoilid
if (!coilId) return
this.coilBindingLoading = true
try {
const res = await listProcessCoilRecord({ coilId, pageNum: 1, pageSize: 1 })
const rec = (res.rows || [])[0]
if (rec) {
// 补充版本号显示
try {
const verRes = await getProcessSpecVersion(rec.versionId)
this.coilBinding = { ...rec, versionCode: verRes.data?.versionCode || String(rec.versionId) }
} catch {
this.coilBinding = { ...rec, versionCode: String(rec.versionId) }
}
} else {
this.coilBinding = null
}
} catch {
this.coilBinding = null
} finally {
this.coilBindingLoading = false
}
},
/** 打开选择规程版本的弹窗 */
async openSpecBindDialog() {
this.specBindSelectedId = this.coilBinding ? this.coilBinding.versionId : null
this.specBindShowAll = !this.coilBinding // 若已关联默认显示全部;否则只看生效版本
this.specBindDialog = true
this.specVersionLoading = true
try {
const [verRes, specRes] = await Promise.all([
listProcessSpecVersion({ pageNum: 1, pageSize: 500 }),
listProcessSpec({ pageNum: 1, pageSize: 200 })
])
this.specVersionRawList = verRes.rows || []
this.specList = specRes.rows || []
} finally {
this.specVersionLoading = false
}
},
/** 确认关联,并自动检测 L1 实绩与规程参数限值的偏差,将异常落库 */
async confirmSpecBind() {
if (!this.specBindSelectedId) return
this.specBindLoading = true
try {
const coilId = this.selectedRow.EXCOILID || this.selectedRow.excoilid
const enCoilId = this.selectedRow.ENCOILID || this.selectedRow.encoilid || null
const versionId = this.specBindSelectedId
// 1. 先写入基础关联记录(异常数稍后更新)
await upsertProcessCoilRecord({
versionId,
coilId,
enCoilId,
hasAnomaly: 0,
anomalyCnt: 0,
processTime: new Date().toISOString()
})
// 2. 异常检测(仅当存在 SEG 数据时执行)
let anomalyCnt = 0
if (this.segData) {
try {
// 2.1 获取该版本下所有方案点位
const planRes = await listProcessPlan({ versionId })
const plans = planRes.rows || []
// 2.2 汇总所有点位的参数
const allParams = []
for (const plan of plans) {
const paramRes = await listProcessPlanParam({ planId: plan.planId })
;(paramRes.rows || []).forEach(p => allParams.push({ ...p, planId: plan.planId }))
}
// 2.3 逐参数比对,构建异常列表
const anomalies = []
for (const param of allParams) {
const col = (param.paramCode || '').toUpperCase()
if (!col) continue
// 从 SEG 系列数据取段内最大/最小值数组,再求全卷极值
const maxArr = this.seg(col + 'MAX').filter(v => v != null)
const minArr = this.seg(col + 'MIN').filter(v => v != null)
if (!maxArr.length && !minArr.length) continue
const actualMax = maxArr.length ? Math.max(...maxArr) : null
const actualMin = minArr.length ? Math.min(...minArr) : null
// 从预设数据取实际设定值(如有对应映射)
let actualTarget = null
const presetCol = TREND_PRESET_MAP[col]
if (presetCol && this.presetData) {
const raw = this.presetData[presetCol] !== undefined
? this.presetData[presetCol]
: this.presetData[presetCol.toLowerCase()]
if (raw != null) actualTarget = parseFloat(Number(raw).toFixed(4))
}
const upper = param.upperLimit != null ? Number(param.upperLimit) : null
const lower = param.lowerLimit != null ? Number(param.lowerLimit) : null
if (upper == null && lower == null) continue // 未配置限值,跳过
const overMax = upper != null && actualMax != null && actualMax > upper
const underMin = lower != null && actualMin != null && actualMin < lower
if (!overMax && !underMin) continue
const anomalyType = (overMax && underMin) ? 'BOTH' : (overMax ? 'OVER_MAX' : 'UNDER_MIN')
anomalies.push({
versionId,
planId: param.planId,
paramId: param.paramId,
coilId,
enCoilId,
paramCode: param.paramCode,
paramName: param.paramName,
unit: param.unit || TREND_UNIT_MAP[col] || null,
anomalyType,
storedTarget: param.targetValue != null ? Number(param.targetValue) : null,
storedUpper: upper,
storedLower: lower,
actualTarget,
actualMax,
actualMin,
deviationMax: overMax ? parseFloat((actualMax - upper).toFixed(4)) : null,
deviationMin: underMin ? parseFloat((actualMin - lower).toFixed(4)) : null,
detectedAt: new Date().toISOString()
})
}
anomalyCnt = anomalies.length
// 2.4 批量写入异常记录
if (anomalies.length > 0) {
await batchAddProcessAnomaly(anomalies)
}
// 2.5 若有异常则更新关联记录的计数字段
if (anomalyCnt > 0) {
await upsertProcessCoilRecord({
versionId,
coilId,
enCoilId,
hasAnomaly: 1,
anomalyCnt,
processTime: new Date().toISOString()
})
}
} catch (e) {
console.warn('[规程关联] 异常检测失败,跳过', e)
}
}
const msg = anomalyCnt > 0
? `关联成功,检测到 ${anomalyCnt} 个参数异常`
: '关联成功,无异常'
this.$message[anomalyCnt > 0 ? 'warning' : 'success'](msg)
this.specBindDialog = false
await this.loadCoilBinding()
} catch {
this.$message.error('关联失败,请重试')
} finally {
this.specBindLoading = false
}
},
calcLengthPerTon(row) {
const len = parseFloat(row.EXIT_LENGTH || row.exit_length)
const wt = parseFloat(row.MEAS_EXIT_WEIGHT || row.meas_exit_weight)
@@ -1276,4 +1389,45 @@ export default {
font-size: 13px;
}
/* ── 规程关联区块 ── */
.spec-bind-block {
border-top: 1px solid #ebeef5;
padding-top: 12px;
display: flex;
flex-direction: column;
gap: 6px;
}
.bind-status {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
}
.bind-label {
font-size: 12px;
color: #909399;
white-space: nowrap;
}
.bind-val {
font-size: 12px;
color: #303133;
font-weight: 500;
&.muted { color: #c0c4cc; font-weight: normal; }
}
/* ── 弹窗内工具栏 ── */
.spec-bind-toolbar {
display: flex;
align-items: center;
gap: 12px;
}
.spec-bind-hint {
font-size: 12px;
color: #909399;
}
</style>

View File

@@ -10,7 +10,10 @@
>
<div class="rpt-toolbar">
<span class="rpt-coil-hint">{{ coilLabel }}</span>
<el-button size="small" type="primary" icon="el-icon-download" :loading="exporting" @click="exportPdf">导出 PDF</el-button>
<span v-if="dataLoading" style="font-size:12px;color:#909399;margin-right:12px">
<i class="el-icon-loading" /> 加载图表数据
</span>
<el-button size="small" type="primary" icon="el-icon-download" :loading="exporting" :disabled="dataLoading" @click="exportPdf">导出 PDF</el-button>
</div>
<div ref="reportContent" class="report-body">
@@ -136,6 +139,11 @@
<script>
import * as echarts from 'echarts'
import {
getTimingSegByEncoilId,
getTimingRealtimeData,
getPresetSetupByCoilId
} from '@/api/l2/timing'
function getV(row, col) {
if (!row) return null
@@ -202,15 +210,16 @@ export default {
name: 'QualityReportDialog',
data() {
return {
visible: false,
exporting: false,
row: null,
segData: null,
gaugeRows: null,
shapeRows: null,
presetData: null,
nowStr: '',
chartInsts: []
visible: false,
exporting: false,
dataLoading: false,
row: null,
segData: null,
gaugeRows: null,
shapeRows: null,
presetData: null,
nowStr: '',
chartInsts: []
}
},
computed: {
@@ -282,18 +291,68 @@ export default {
fmtDate(v) { if (!v) return '—'; return String(v).replace('T', ' ').substring(0, 19) },
open(row, segData, gaugeRows, shapeRows, presetData) {
this.row = row
this.segData = segData || null
this.gaugeRows = gaugeRows || null
this.shapeRows = shapeRows || null
this.presetData = presetData || null
this.visible = true
this.row = row
this.segData = segData || null
this.gaugeRows = gaugeRows || null
this.shapeRows = shapeRows || null
this.presetData = presetData || null
this.dataLoading = false
this.visible = true
},
// @opened fires AFTER the dialog's CSS transition completes — DOM is fully painted
onOpened() {
async onOpened() {
this.nowStr = new Date().toLocaleString('zh-CN').replace(/\//g, '-')
this.$nextTick(() => this.renderAll())
// 若父组件没有传入数据(点击的行不是当前选中行),自行拉取
if (!this.segData && !this.gaugeRows && !this.shapeRows) {
await this.fetchMissingData()
}
// 等 Vue 把 v-if 新增的图表 DOM 全部渲染出来再初始化 ECharts
await this.$nextTick()
this.renderAll()
},
/** 自行从 L1 接口获取 SEG / 实时 / 预设数据 */
async fetchMissingData() {
const encoilId = getV(this.row, 'ENCOILID')
const excoilId = getV(this.row, 'EXCOILID')
if (!encoilId && !excoilId) return
this.dataLoading = true
try {
await Promise.all([
encoilId ? this._loadSeg(encoilId) : Promise.resolve(),
encoilId ? this._loadPreset(encoilId) : Promise.resolve(),
excoilId ? this._loadRealtime(excoilId): Promise.resolve()
])
} catch (e) {
console.warn('[质保书] 数据加载失败', e)
} finally {
this.dataLoading = false
}
},
async _loadSeg(encoilId) {
try {
const res = await getTimingSegByEncoilId(encoilId)
const rows = res?.data?.rows || []
this.segData = rows.length ? (res?.data?.series || null) : null
} catch (_) {}
},
async _loadRealtime(excoilId) {
try {
const res = await getTimingRealtimeData(excoilId)
const g = res?.data?.gauge?.result
const s = res?.data?.shape?.result
this.gaugeRows = Array.isArray(g) ? g : null
this.shapeRows = Array.isArray(s) ? s : null
} catch (_) {}
},
async _loadPreset(coilId) {
try {
const res = await getPresetSetupByCoilId(coilId)
this.presetData = res?.data?.data || null
} catch (_) {
this.presetData = null
}
},
onClose() {
this.disposeCharts()
@@ -481,7 +540,7 @@ export default {
const w = el.offsetWidth || 840
const c = echarts.init(el, null, { renderer: 'canvas', width: w, height: 200 })
this.chartInsts.push(c)
this.chartInsts.push(c) // 只 push 一次
c.setOption({
title: { text: '板形热力图', textStyle: { fontSize: 12, fontWeight: 'normal', color: '#1a365d' }, top: 4, left: 8 },
tooltip: {
@@ -521,7 +580,6 @@ export default {
emphasis: { itemStyle: { borderColor: '#333', borderWidth: 1 } }
}]
})
this.chartInsts.push(c)
},
async exportPdf() {