工艺规程增强
This commit is contained in:
@@ -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 + BOTLIMIT(TOPLIMIT/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>
|
||||
|
||||
Reference in New Issue
Block a user