diff --git a/klp-ui/src/components/KLPService/ProductSelect/index.vue b/klp-ui/src/components/KLPService/ProductSelect/index.vue
index 69890d26..389537a4 100644
--- a/klp-ui/src/components/KLPService/ProductSelect/index.vue
+++ b/klp-ui/src/components/KLPService/ProductSelect/index.vue
@@ -163,6 +163,10 @@ export default {
filters: {
type: Object,
default: () => ({})
+ },
+ defaultQueryParams: {
+ type: Object,
+ default: () => ({})
}
},
data() {
@@ -235,6 +239,9 @@ export default {
this.getList();
this.listRecentlySelected();
}
+ },
+ defaultQueryParams(val) {
+ this.queryParams = { ...this.queryParams, ...val };
}
},
methods: {
diff --git a/klp-ui/src/components/KLPService/RawMaterialSelect/index.vue b/klp-ui/src/components/KLPService/RawMaterialSelect/index.vue
index 89d9a7aa..4e4a9de2 100644
--- a/klp-ui/src/components/KLPService/RawMaterialSelect/index.vue
+++ b/klp-ui/src/components/KLPService/RawMaterialSelect/index.vue
@@ -151,6 +151,10 @@ export default {
filters: {
type: Object,
default: () => ({})
+ },
+ defaultQueryParams: {
+ type: Object,
+ default: () => ({})
}
},
data() {
@@ -234,6 +238,9 @@ export default {
this.listRecentlySelected();
this.getList();
}
+ },
+ defaultQueryParams(val) {
+ this.queryParams = { ...this.queryParams, ...val };
}
},
methods: {
diff --git a/klp-ui/src/views/micro/pages/acid/components/ActualPerformance.vue b/klp-ui/src/views/micro/pages/acid/components/ActualPerformance.vue
index 728ffdce..3416d481 100644
--- a/klp-ui/src/views/micro/pages/acid/components/ActualPerformance.vue
+++ b/klp-ui/src/views/micro/pages/acid/components/ActualPerformance.vue
@@ -197,11 +197,94 @@
查找
重置
+
+
+
+
规程关联
+
+
+ 已关联版本
+ 加载中…
+
+ {{ coilBinding.versionCode }}
+
+ 未关联
+
+ {{ coilBinding ? '重新关联' : '关联规程版本' }}
+
+
请先选择钢卷
+
+
+
+
+
+
+ 共 {{ specVersionsForDialog.length }} 条
+
+ specBindSelectedId = row.versionId"
+ >
+
+
+
+
+
+
+
+
+
+ 生效中
+ 已发布
+ 草稿
+
+
+
+
+
+ 取消
+ 确认关联
+
+
+
@@ -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('
')
+ : 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;
+}
+
diff --git a/klp-ui/src/views/micro/pages/acid/components/QualityReportDialog.vue b/klp-ui/src/views/micro/pages/acid/components/QualityReportDialog.vue
index 1c4b6767..08de5cb9 100644
--- a/klp-ui/src/views/micro/pages/acid/components/QualityReportDialog.vue
+++ b/klp-ui/src/views/micro/pages/acid/components/QualityReportDialog.vue
@@ -10,7 +10,10 @@
>
{{ coilLabel }}
- 导出 PDF
+
+ 加载图表数据…
+
+ 导出 PDF
@@ -136,6 +139,11 @@