工艺规程增强

This commit is contained in:
2026-05-12 17:15:29 +08:00
parent b44d9d9daf
commit 38138a828c
27 changed files with 1903 additions and 259 deletions

View File

@@ -137,7 +137,7 @@
</div>
</el-tab-pane>
<!-- 带钢板形 热力图 -->
<!-- 带钢板形 -->
<el-tab-pane label="带钢板形" name="flatness3d">
<div v-if="!selectedRow" class="no-data-hint">请在上方选择钢卷</div>
<div v-else-if="realtimeLoading" class="no-data-hint">加载中</div>
@@ -203,8 +203,14 @@ import {
getExcoilList,
getExcoilCount,
getTimingSegByEncoilId,
getTimingRealtimeData
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 { batchAddProcessAnomaly } from '@/api/wms/processAnomaly'
// 趋势参数树结构,对应 PLTCM_PRO_SEG 列名
const TREND_GROUPS = [
@@ -265,10 +271,26 @@ const SHAPE_SCALAR_COLS = [
{ col: 'IRBEND', title: '中间辊弯辊力 [kN]' }
]
/**
* 从数据数组计算合理的 Y 轴范围(带 15% 上下边距)。
* echarts 默认 scale 在数据变化幅度极小时会把轴拉到很大范围,导致曲线看起来是横线。
*/
// 参数单位映射
const TREND_UNIT_MAP = {
PORTENS: 'N/mm²', ENLTENS: 'N/mm²', TLTENS: 'N/mm²', PLTENS: 'N/mm²',
CXLTENS: 'N/mm²', TRIMTENS: 'N/mm²', TRTENS: 'N/mm²', TELTENS: 'N/mm²',
PORSPEED: 'm/min', PLSPEED: 'm/min', TRIMSPEED: 'm/min',
MILLENTRYSPEED: 'm/min', MILLEXITSPEED: 'm/min',
TLMESH1: 'mm', TLMESH2: 'mm', TLMESH3: 'mm',
TLELONG: '%',
TK1TEMP: '℃', TK2TEMP: '℃', TK3TEMP: '℃', RINSETEMP: '℃'
}
// PLTCM_PRO_SEG 列 → PLTCM_PRESET_SETUP 设定值列
const TREND_PRESET_MAP = {
PORTENS: 'POR_TEN', ENLTENS: 'CEL_TEN', TLTENS: 'TLV_TEN',
PLTENS: 'CPL_TEN', CXLTENS: 'CXL_TEN', TRIMTENS: 'TRIM_TEN',
TRTENS: 'TR_TEN', TELTENS: 'TEL_TEN',
TLMESH1: 'TLV_MESH_1', TLMESH2: 'TLV_MESH_2', TLMESH3: 'TLV_MESH_3',
TLELONG: 'TLV_ELONG', PLSPEED: 'CPL_MAX_SPEED'
}
function calcYRange(vals) {
const nums = vals.filter(v => v != null && isFinite(Number(v))).map(Number)
if (!nums.length) return {}
@@ -279,37 +301,48 @@ function calcYRange(vals) {
return { min: parseFloat((min - base * 0.2).toFixed(4)), max: parseFloat((max + base * 0.2).toFixed(4)) }
}
const pad = (max - min) * 0.15
return {
min: parseFloat((min - pad).toFixed(4)),
max: parseFloat((max + pad).toFixed(4))
}
return { min: parseFloat((min - pad).toFixed(4)), max: parseFloat((max + pad).toFixed(4)) }
}
function makeLine(title, xData, yData) {
const range = calcYRange(yData)
/**
* 生成折线图 option。
* extras: [{ name, data, color, dash }] — 上下限或参考线
*/
function makeLine(title, xData, yData, extras = []) {
const allVals = [yData, ...extras.map(e => e.data)].flat()
const range = calcYRange(allVals)
const hasExtras = extras.length > 0
const mainSeries = {
name: title, type: 'line', smooth: false, symbol: 'none',
lineStyle: { width: 1.5, color: '#409EFF' }, data: yData, z: 3
}
const extraSeries = extras.map(e => ({
name: e.name,
type: 'line', smooth: false, symbol: 'none',
lineStyle: { width: 1, color: e.color || '#E6A23C', type: e.dash !== false ? 'dashed' : 'solid' },
data: e.data, z: 2
}))
return {
title: { text: title, textStyle: { fontSize: 12, fontWeight: 'normal' }, top: 4, left: 8 },
legend: hasExtras
? { data: [title, ...extras.map(e => e.name)], top: 4, right: 4,
textStyle: { fontSize: 9 }, itemWidth: 14, itemHeight: 8 }
: { show: false },
tooltip: { trigger: 'axis' },
grid: { top: 36, bottom: 28, left: 8, right: 16, containLabel: true },
grid: { top: hasExtras ? 44 : 36, bottom: 28, left: 8, right: 16, containLabel: true },
xAxis: {
type: 'category', data: xData,
name: 'pos(m)', nameTextStyle: { fontSize: 10 }, axisLabel: { fontSize: 10 }
},
yAxis: {
type: 'value',
min: range.min,
max: range.max,
nameTextStyle: { fontSize: 10 },
axisLabel: { fontSize: 10 }
type: 'value', min: range.min, max: range.max,
nameTextStyle: { fontSize: 10 }, axisLabel: { fontSize: 10 }
},
dataZoom: [
{ type: 'inside', xAxisIndex: 0, zoomOnMouseWheel: true, moveOnMouseMove: true },
{ type: 'inside', yAxisIndex: 0, zoomOnMouseWheel: false, moveOnMouseWheel: true }
],
series: [{
name: title, type: 'line', smooth: false, symbol: 'none',
lineStyle: { width: 1 }, data: yData
}]
series: [mainSeries, ...extraSeries]
}
}
@@ -343,6 +376,7 @@ export default {
expandedGroups: { '张力': true, '速度': true, '拉矫机': true, '酸洗段': true },
selectedTrendParam: null,
trendChartInst: null,
presetData: null,
// 查找
searchType: 'coil',
searchCoilId: '',
@@ -355,6 +389,7 @@ export default {
}
},
created() {
this._clickRev = 0
this.loadExcoilCount()
this.loadExcoilList()
},
@@ -382,23 +417,31 @@ export default {
this.loadExcoilList()
},
async handleRowClick(row) {
// 快速点击防重:每次点击递增版本号,旧的 sync 任务检测到版本号变更后自动放弃
const clickRev = ++this._clickRev
this.selectedRow = row
this.segData = null
this.gaugeRows = null
this.shapeRows = null
this.presetData = null
this.selectedTrendParam = null
this.disposeAllCharts()
// PLTCM_PRO_SEG 用 ENCOILID 查(主键前缀)
const encoilId = row.ENCOILID || row.encoilid
// V_VBDA_GAUGE / V_VBDA_SHAPE 用 EXCOILID 作为 MATID
const excoilId = row.EXCOILID || row.excoilid
await Promise.all([
encoilId ? this.loadSeg(encoilId) : Promise.resolve(),
encoilId ? this.loadPreset(encoilId) : Promise.resolve(),
excoilId ? this.loadRealtime(excoilId) : Promise.resolve()
])
// 如果期间又点击了其他行则放弃后续操作
if (this._clickRev !== clickRev) return
// 后台静默同步到规程(不阻塞 UI
this.autoSyncToActiveSpec(excoilId || encoilId, clickRev)
await this.$nextTick()
// 加载完成后自动选中第一个趋势参数
if (this.activeTab === 'trend' && this.segData) {
@@ -434,6 +477,15 @@ export default {
}
},
async loadPreset(coilId) {
try {
const res = await getPresetSetupByCoilId(coilId)
this.presetData = res?.data?.data || null
} catch (_) {
this.presetData = null
}
},
// ── 趋势参数树 ──────────────────────────────
toggleGroup(label) {
this.$set(this.expandedGroups, label, !this.expandedGroups[label])
@@ -457,9 +509,31 @@ export default {
window.addEventListener('resize', resizeFn)
this._trendResizeFn = resizeFn
}
const col = this.selectedTrendParam.col
const x = this.segX()
const yData = this.seg(this.selectedTrendParam.col)
this.trendChartInst.setOption(makeLine(this.selectedTrendParam.label, x, yData), true)
const yData = this.seg(col)
// 实测最大值 / 最小值:来自 PLTCM_PRO_SEG 的段内统计 MAX / MIN
const maxData = this.seg(col + 'MAX')
const minData = this.seg(col + 'MIN')
const extras = []
if (maxData.some(v => v != null)) {
extras.push({ name: '最大值', data: maxData, color: '#F56C6C', dash: true })
}
if (minData.some(v => v != null)) {
extras.push({ name: '最小值', data: minData, color: '#67C23A', dash: true })
}
// 设定值:来自 PLTCM_PRESET_SETUP若有映射
const presetCol = TREND_PRESET_MAP[col]
if (presetCol && this.presetData) {
const setVal = this.presetData[presetCol] !== undefined
? this.presetData[presetCol]
: this.presetData[presetCol.toLowerCase()]
if (setVal != null && Number(setVal) !== 0) {
const sv = Number(Number(setVal).toFixed(3))
extras.push({ name: '设定值', data: new Array(x.length).fill(sv), color: '#E6A23C', dash: true })
}
}
this.trendChartInst.setOption(makeLine(this.selectedTrendParam.label, x, yData, extras), true)
},
// ── Tab 切换 ────────────────────────────────
@@ -533,14 +607,38 @@ export default {
const rows = this.gaugeRows
if (!rows || !rows.length) return
const xData = xLocData(rows)
const refs = ['chartGauge1', 'chartGauge2', 'chartGauge3', 'chartGauge4']
const charts = refs.map((ref, i) => {
// THICK0~THICK4 对应的参考列
const refColMap = { THICK0: 'THICK0REF', THICK1: 'THICK1REF', THICK4: 'THICK4REF', THICK5: 'THICK5REF' }
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))
})
return this.makeChart(ref, makeLine(title, xData, yData))
const extras = []
const refCol = refColMap[col]
if (refCol) {
const refData = rows.map(r => {
const rv = getRowVal(r, refCol)
return rv == null ? null : parseFloat(rv.toFixed(4))
})
// 上限 = REF + TOPLIMIT下限 = REF + BOTLIMITTOPLIMIT/BOTLIMIT 单位与测厚仪一致)
const upData = rows.map((r, j) => {
const rv = getRowVal(r, refCol)
const tl = getRowVal(r, 'TOPLIMIT')
return rv == null ? null : parseFloat((rv + (tl ?? 3)).toFixed(4))
})
const loData = rows.map((r, j) => {
const rv = getRowVal(r, refCol)
const bl = getRowVal(r, 'BOTLIMIT')
return rv == null ? null : parseFloat((rv + (bl ?? -3)).toFixed(4))
})
if (refData.some(v => v != null)) extras.push({ name: '目标值', data: refData, 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))
})
this.chartInstances = charts.filter(Boolean)
this.setupResize()
@@ -739,6 +837,181 @@ export default {
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 {
const r = await addProcessPlan({
versionId,
segmentType: 'PROCESS',
segmentName: item.groupLabel,
pointName: item.pointName,
pointCode: item.pointCode,
sortOrder: 0
})
if (guard()) return
planId = r.data
}
// 查已存储参数
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
},
calcLengthPerTon(row) {
const len = parseFloat(row.EXIT_LENGTH || row.exit_length)
const wt = parseFloat(row.MEAS_EXIT_WEIGHT || row.meas_exit_weight)
@@ -748,7 +1021,8 @@ export default {
formatDate(v) {
if (!v) return '—'
return String(v).replace('T', ' ').substring(0, 19)
}
},
}
}
</script>
@@ -957,4 +1231,5 @@ export default {
color: #c0c4cc;
font-size: 13px;
}
</style>