修复酸轧实绩提交问题,规程重新完成逻辑
This commit is contained in:
@@ -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 + BOTLIMIT(TOPLIMIT/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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user