Merge remote-tracking branch 'origin/0.8.X' into 0.8.X

This commit is contained in:
2026-05-21 14:29:28 +08:00
11 changed files with 745 additions and 631 deletions

View File

@@ -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: {

View File

@@ -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: {

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() {

View File

@@ -1,13 +1,10 @@
<template>
<div class="app-container" v-loading="loading">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="90px">
<el-form-item label="开始时间" prop="startTime">
<el-date-picker v-model="queryParams.startTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss"
placeholder="选择开始时间" />
</el-form-item>
<el-form-item label="结束时间" prop="endTime">
<el-date-picker v-model="queryParams.endTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss"
placeholder="选择结束时间" />
<el-form-item label="时间">
<time-range-picker v-model="timeRangeParams" start-key="startTime" end-key="endTime"
:default-start-time="queryParams.startTime" :default-end-time="queryParams.endTime"
@quick-select="handleQuery" />
</el-form-item>
<el-form-item label="目标炉" prop="targetFurnaceId">
<el-select v-model="queryParams.targetFurnaceId" placeholder="请选择" clearable filterable>
@@ -74,15 +71,35 @@ import ProductInfo from "@/components/KLPService/Renderer/ProductInfo";
import RawMaterialInfo from "@/components/KLPService/Renderer/RawMaterialInfo";
import CoilNo from "@/components/KLPService/Renderer/CoilNo.vue";
import WarehouseSelect from "@/components/KLPService/WarehouseSelect/index.vue";
import TimeRangePicker from "@/views/wms/report/components/timeRangePicker.vue";
export default {
name: "AnnealPerformance",
components: {
ProductInfo,
RawMaterialInfo,
CoilNo,
WarehouseSelect,
TimeRangePicker,
},
data() {
const startTime = (() => {
const d = new Date(); d.setDate(d.getDate() - 1);
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} 00:00:00`;
})()
const endTime = (() => {
const d = new Date();
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} 00:00:00`;
})()
return {
loading: false,
timeRangeParams: {
startTime,
endTime,
},
queryParams: {
startTime: undefined,
endTime: undefined,
startTime,
endTime,
targetFurnaceId: undefined,
planNo: undefined,
enterCoilNo: undefined,
@@ -92,11 +109,15 @@ export default {
furnaceOptions: [],
};
},
components: {
ProductInfo,
RawMaterialInfo,
CoilNo,
WarehouseSelect,
watch: {
timeRangeParams: {
handler(newVal) {
this.queryParams.startTime = newVal.startTime
this.queryParams.endTime = newVal.endTime
},
deep: true,
immediate: true
}
},
created() {
this.loadFurnaces();

View File

@@ -1,16 +1,5 @@
<template>
<div class="typing-coil-container">
<!-- 顶部操作栏 -->
<!-- <div class="header-bar">
<div class="header-title">
<i class="el-icon-edit"></i>
<span>钢卷信息更新</span>
</div>
<div class="header-actions">
</div>
</div> -->
<div>
<CoilInfoRender title="原料信息" :coilInfo="currentInfo" border>
<template slot="extra">
@@ -65,8 +54,6 @@
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<el-form-item label="材料类型" prop="materialType">
@@ -80,9 +67,11 @@
<el-col :span="8">
<el-form-item :label="getItemLabel" prop="itemId" :rules="rules.itemId">
<RawMaterialSelect v-if="updateForm.materialType === '原料'" v-model="updateForm.itemId"
placeholder="请选择原料" style="width: 100%" clearable :disabled="!updateForm.materialType" />
placeholder="请选择原料" style="width: 100%" clearable :disabled="!updateForm.materialType"
:default-query-params="itemSelectorQueryParams" />
<ProductSelect v-else-if="updateForm.materialType === '成品'" v-model="updateForm.itemId"
placeholder="请选择成品" style="width: 100%" clearable :disabled="!updateForm.materialType" />
placeholder="请选择成品" style="width: 100%" clearable :disabled="!updateForm.materialType"
:default-query-params="itemSelectorQueryParams" />
<div v-else>请先选择物料类型</div>
</el-form-item>
</el-col>
@@ -371,8 +360,8 @@ export default {
updateForm: {
currentCoilNo: '',
team: '',
materialType: null,
itemType: null,
materialType: '成品',
itemType: 'product',
itemId: null,
grossWeight: undefined,
netWeight: undefined,
@@ -460,6 +449,7 @@ export default {
cacheDialogVisible: false,
currentCache: null,
parsedCacheData: null,
itemSelectorQueryParams: {},
};
},
computed: {
@@ -567,6 +557,14 @@ export default {
}
if (data.exit_thick != null) this.$set(this.updateForm, 'actualThickness', parseFloat(data.exit_thick))
if (data.exit_width != null) this.$set(this.updateForm, 'actualWidth', parseFloat(data.exit_width))
const query = {
specification: data.exit_thick ? `${data.exit_thick}*${data.exit_width}` : '',
material: data.grade,
}
this.itemSelectorQueryParams = query
// 包装要求
if (data.park_type != null && data.park_type !== '') {
this.$set(this.updateForm, 'packagingRequirement', data.park_type)
@@ -633,7 +631,6 @@ export default {
}
},
// 复制当前信息到更新表单
copyFromCurrent() {
// 复制除了指定字段之外的其他字段

View File

@@ -160,6 +160,7 @@ import {
delProcessSpecVersion,
activateProcessSpecVersion
} from '@/api/wms/processSpecVersion'
import { listDrRecipe, listDrRecipeVersions } from '@/api/wms/drMill'
const STATUS_OPTIONS = [
{ value: 'DRAFT', label: '草稿' },
@@ -179,7 +180,7 @@ export default {
versionList: [],
versionLoading: false,
total: 0,
queryParams: { pageNum: 1, pageSize: 10, specType: '', lineId: '' },
queryParams: { pageNum: 1, pageSize: 20, specType: '', lineId: '' },
specOpen: false,
specTitle: '',
@@ -237,18 +238,43 @@ export default {
},
onSpecRowClick(row) { this.selectSpec(row) },
loadVersions() {
async loadVersions() {
if (!this.currentSpecId) return
this.versionLoading = true
listProcessSpecVersion({ specId: this.currentSpecId, pageNum: 1, pageSize: 200 }).then(res => {
try {
// DR 规程specCode 以 "DR-" 开头):先触发一次跨库同步,
// 确保 double-rack 库中的版本已写入 master wms_process_spec_version
const specCode = (this.currentSpec && this.currentSpec.specCode) || ''
if (specCode.startsWith('DR-')) {
const recipeNo = specCode.slice(3)
try {
const rRes = await listDrRecipe({ recipeNo })
const recipe = (rRes.data || [])[0]
if (recipe) {
await listDrRecipeVersions(recipe.recipeId) // 后端自动同步版本到 master
}
} catch (e) {
console.warn('[DR同步] 触发版本同步失败,将直接读取 master 库', e)
}
}
const res = await listProcessSpecVersion({ specId: this.currentSpecId, pageNum: 1, pageSize: 200 })
this.versionList = res.rows || []
}).catch(e => console.error(e)).finally(() => { this.versionLoading = false })
} catch (e) {
console.error(e)
} finally {
this.versionLoading = false
}
},
goPlanSpec(row) {
this.$router.push({
path: `/process/processSpec/planSpec`,
query: { specId: this.currentSpecId, versionId: String(row.versionId), versionCode: row.versionCode }
query: {
specId: this.currentSpecId,
versionId: String(row.versionId),
versionCode: row.versionCode,
specCode: this.currentSpec ? (this.currentSpec.specCode || '') : ''
}
})
},

View File

@@ -16,16 +16,6 @@
</div>
</div>
<!-- 全局异常提示条 -->
<el-alert
v-if="allAnomalies.length"
:title="`检测到 ${allAnomalies.length} 项实际生产偏差(来自最近一次实绩分析),请选择对应点位查看详情`"
type="warning"
show-icon
:closable="false"
style="margin-bottom:10px"
/>
<div class="main-layout">
<!-- 左侧段分组 -->
<div class="left-tree">
@@ -95,9 +85,19 @@
>模板导入</el-button>
</div>
<!-- DR 道次参数同步提示 -->
<el-alert
v-if="drSyncLoading"
title="正在从双机架版本同步道次参数,请稍候…"
type="info"
show-icon
:closable="false"
style="margin-bottom:10px"
/>
<!-- 参数平铺表 -->
<el-table
v-loading="planLoading || allParamLoading"
v-loading="planLoading || allParamLoading || drSyncLoading"
:data="filteredFlatRows"
size="small"
border
@@ -113,17 +113,6 @@
<el-table-column label="设定值" prop="targetValue" align="right" width="82" />
<el-table-column label="下限" prop="lowerLimit" align="right" width="72" />
<el-table-column label="上限" prop="upperLimit" align="right" width="72" />
<el-table-column label="实际状态" align="center" width="90">
<template slot-scope="{ row }">
<span v-if="paramAnomalyMap[row.paramCode]" class="anomaly-badge">
<i class="el-icon-warning-outline" /> 异常
</span>
<span v-else-if="row.upperLimit != null || row.lowerLimit != null" class="normal-badge">
<i class="el-icon-circle-check" /> 正常
</span>
<span v-else class="no-data-badge"></span>
</template>
</el-table-column>
<el-table-column label="操作" align="right" width="100" fixed="right">
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click="editParamFromFlat(row)">编辑</el-button>
@@ -134,98 +123,6 @@
</div>
</div>
<!-- 服役钢卷记录 -->
<div class="coil-record-section" v-loading="coilRecordLoading">
<div class="coil-record-hd">
<i class="el-icon-data-line" style="margin-right:5px;color:#5F7BA0" />
服役钢卷记录
<span class="coil-total-badge"> {{ coilRecordTotal }} </span>
<span v-if="coilRecords.filter(r => r.hasAnomaly).length" class="coil-anomaly-badge">
其中 {{ coilRecords.filter(r => r.hasAnomaly).length }} 根有异常
</span>
</div>
<el-table :data="coilRecords" size="small" border>
<el-table-column label="出口钢卷号" prop="coilId" min-width="140" show-overflow-tooltip />
<el-table-column label="入口钢卷号" prop="enCoilId" min-width="140" show-overflow-tooltip />
<el-table-column label="检测时间" min-width="140">
<template slot-scope="{ row }">{{ (row.processTime || '').substring(0, 16) || '—' }}</template>
</el-table-column>
<el-table-column label="参数异常情况" align="center" min-width="160">
<template slot-scope="{ row }">
<template v-if="row.hasAnomaly">
<el-tag type="danger" size="mini" effect="plain" style="margin-right:4px">
<i class="el-icon-warning-outline" /> {{ row.anomalyCnt }} 项参数超限
</el-tag>
<el-button type="text" size="mini" style="color:#F56C6C;padding:0"
@click="jumpToAnomaly(row.coilId)">查看</el-button>
</template>
<span v-else class="normal-badge"><i class="el-icon-circle-check" /> 全部正常</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" min-width="80">
<template slot-scope="{ row }">
<el-tag :type="row.hasAnomaly ? 'danger' : 'success'" size="mini" effect="dark">
{{ row.hasAnomaly ? '异常' : '正常' }}
</el-tag>
</template>
</el-table-column>
</el-table>
<pagination
v-show="coilRecordTotal > 0"
:total="coilRecordTotal"
:page.sync="coilRecordPage"
:limit.sync="coilRecordPageSize"
@pagination="loadCoilRecords"
/>
</div>
<!-- 钢卷异常明细 dialog -->
<el-dialog
:title="`钢卷 ${coilAnomalyCoilId} — 异常明细`"
:visible.sync="coilAnomalyDialog"
width="900px"
append-to-body
>
<el-table :data="coilAnomalyList" size="small" border>
<el-table-column label="参数名称" prop="paramName" min-width="110" show-overflow-tooltip />
<el-table-column label="异常类型" align="center" width="120">
<template slot-scope="{ row }">
<el-tag v-if="row.anomalyType === 'OVER_MAX' || row.anomalyType === 'BOTH'" size="mini" type="danger" style="margin:1px">超上限</el-tag>
<el-tag v-if="row.anomalyType === 'UNDER_MIN' || row.anomalyType === 'BOTH'" size="mini" type="warning" style="margin:1px">低于下限</el-tag>
</template>
</el-table-column>
<el-table-column label="规程设定值" prop="storedTarget" align="right" width="100" />
<el-table-column label="规程上限" prop="storedUpper" align="right" width="90" />
<el-table-column label="规程下限" prop="storedLower" align="right" width="90" />
<el-table-column label="实际最大值" align="right" width="100">
<template slot-scope="{ row }">
<span :class="(row.anomalyType === 'OVER_MAX' || row.anomalyType === 'BOTH') ? 'val-over' : ''">{{ row.actualMax }}</span>
</template>
</el-table-column>
<el-table-column label="实际最小值" align="right" width="100">
<template slot-scope="{ row }">
<span :class="(row.anomalyType === 'UNDER_MIN' || row.anomalyType === 'BOTH') ? 'val-under' : ''">{{ row.actualMin }}</span>
</template>
</el-table-column>
<el-table-column label="最大偏差" align="right" width="90">
<template slot-scope="{ row }">
<span v-if="row.deviationMax != null" class="val-over">+{{ row.deviationMax }}</span>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="最小偏差" align="right" width="90">
<template slot-scope="{ row }">
<span v-if="row.deviationMin != null" class="val-under">{{ row.deviationMin }}</span>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="单位" prop="unit" align="center" width="60" />
</el-table>
<div slot="footer">
<el-button size="small" @click="coilAnomalyDialog = false">关闭</el-button>
</div>
</el-dialog>
<!-- 方案点位 dialog -->
<el-dialog :title="planTitle" :visible.sync="planOpen" width="520px" append-to-body @close="planForm = {}">
<el-form ref="planFormRef" :model="planForm" :rules="planRules" label-width="90px" size="small">
@@ -392,11 +289,9 @@
<script>
import * as XLSX from 'xlsx'
import * as echarts from 'echarts'
import { listProcessPlan, addProcessPlan, updateProcessPlan, delProcessPlan } from '@/api/wms/processPlan'
import { listProcessPlanParam, addProcessPlanParam, updateProcessPlanParam, delProcessPlanParam } from '@/api/wms/processPlanParam'
import { listAllProcessAnomaly } from '@/api/wms/processAnomaly'
import { listProcessCoilRecord } from '@/api/wms/processCoilRecord'
import { listDrRecipe, listDrRecipeVersions, getDrRecipeVersionDetail } from '@/api/wms/drMill'
/** 表单内可选段类型(新建/编辑仍支持全部枚举) */
const SEGMENT_FORM_OPTIONS = [
@@ -428,6 +323,8 @@ export default {
versionId: undefined,
versionCode: '',
specId: undefined,
specCode: '', // 规程编码DR 规程以 "DR-" 开头
drSyncLoading: false, // DR 道次参数同步中
configMode: 'configurable',
/** 左侧:段类型;空=全部 */
activeSegmentType: '',
@@ -457,19 +354,6 @@ export default {
paramCode: [{ required: true, message: '参数编码不能为空', trigger: 'blur' }],
paramName: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }]
},
// 偏差分析
allAnomalies: [],
anomalyExpanded: true,
anomalyChartInst: null,
// 服役钢卷记录
coilRecords: [],
coilRecordTotal: 0,
coilRecordPage: 1,
coilRecordPageSize: 20,
coilRecordLoading: false,
// 钢卷异常明细弹窗
coilAnomalyDialog: false,
coilAnomalyCoilId: '',
// 导入相关数据
importOpen: false,
file: null,
@@ -487,6 +371,10 @@ export default {
}
},
computed: {
/** 当前规程是否为双机架规程specCode 以 DR- 开头) */
isDrSpec() {
return typeof this.specCode === 'string' && this.specCode.startsWith('DR-')
},
/**
* 左侧 Tab仅从点位数据汇总「段类型」展示用语义名称入口段/工艺段/出口段)
*/
@@ -555,16 +443,6 @@ export default {
extra.sort((a, b) => String(a.label).localeCompare(String(b.label), 'zh-CN'))
return [...SEGMENT_FORM_OPTIONS, ...extra]
},
/** paramCode → anomaly 的快速索引(用于状态列) */
paramAnomalyMap() {
const map = {}
this.allAnomalies.forEach(a => { map[a.paramCode] = a })
return map
},
coilAnomalyList() {
if (!this.coilAnomalyCoilId) return []
return this.allAnomalies.filter(a => a.coilId === this.coilAnomalyCoilId)
},
/** 所有 plan 的参数平铺成一行,带 plan 的段/点位信息 */
flatRows() {
const SEG_LABELS = { INLET: '入口段', PROCESS: '工艺段', OUTLET: '出口段' }
@@ -629,124 +507,16 @@ export default {
methods: {
syncFromRoute() {
const q = this.$route.query
this.versionId = q.versionId || undefined
this.versionId = q.versionId || undefined
this.versionCode = q.versionCode || ''
this.specId = q.specId || undefined
this.specId = q.specId || undefined
this.specCode = q.specCode || ''
this._drSyncAttempted = false // 路由切换时重置,避免重复同步
if (this.versionId) {
this.loadPlans()
this.loadAnomalies()
this.loadCoilRecords()
}
},
loadAnomalies() {
if (!this.versionId) return
listAllProcessAnomaly({ versionId: this.versionId }).then(res => {
this.allAnomalies = res.data || []
}).catch(() => { this.allAnomalies = [] })
},
loadCoilRecords() {
if (!this.versionId) return
this.coilRecordLoading = true
listProcessCoilRecord({ versionId: this.versionId, pageNum: this.coilRecordPage, pageSize: this.coilRecordPageSize })
.then(res => {
this.coilRecords = res.rows || []
this.coilRecordTotal = res.total || 0
}).catch(() => {}).finally(() => { this.coilRecordLoading = false })
},
renderAnomalyChart() {
const el = this.$refs.anomalyChart
if (!el || !this.planAnomalies.length) return
if (this.anomalyChartInst && !this.anomalyChartInst.isDisposed()) {
this.anomalyChartInst.dispose()
}
this.anomalyChartInst = echarts.init(el)
const items = this.planAnomalies
const names = items.map(a => a.paramName)
// 范围对比:规程范围 vs 实际范围,使用自定义 bar 叠加实现
const specRangeData = items.map(a => {
const lo = a.storedLower ?? a.storedTarget ?? 0
const hi = a.storedUpper ?? a.storedTarget ?? 0
return [lo, hi]
})
const actualRangeData = items.map(a => {
const lo = a.actualMin ?? 0
const hi = a.actualMax ?? 0
return [lo, hi]
})
// 把真实值编入 data让 ECharts 正确推断 y 轴范围
// data item: [categoryIndex, lo, hi]
const specSeriesData = specRangeData.map(([lo, hi], i) => [i, lo, hi])
const actualSeriesData = actualRangeData.map(([lo, hi], i) => [i, lo, hi])
const allVals = [...specRangeData, ...actualRangeData].flat().filter(v => v != null && isFinite(v))
const yMin = allVals.length ? Math.min(...allVals) : 0
const yMax = allVals.length ? Math.max(...allVals) : 1
const pad = (yMax - yMin) * 0.15 || 1
const option = {
title: { text: '规程范围 vs 实际范围对比', textStyle: { fontSize: 12, fontWeight: 'normal' }, top: 6, left: 8 },
tooltip: {
trigger: 'axis',
formatter(params) {
const idx = params[0].dataIndex
const name = names[idx]
const spec = specRangeData[idx]
const actual = actualRangeData[idx]
return `${name}<br/>规程范围:${spec[0]} ~ ${spec[1]}<br/>实际范围:${actual[0]} ~ ${actual[1]}`
}
},
legend: { data: ['规程范围', '实际范围'], top: 6, right: 8, textStyle: { fontSize: 11 } },
grid: { top: 44, bottom: 36, left: 12, right: 20, containLabel: true },
xAxis: { type: 'category', data: names, axisLabel: { fontSize: 11, rotate: names.length > 5 ? 30 : 0 } },
yAxis: { type: 'value', min: yMin - pad, max: yMax + pad, axisLabel: { fontSize: 11 } },
series: [
{
name: '规程范围',
type: 'custom',
renderItem(params, api) {
const idx = api.value(0)
const lo = api.value(1)
const hi = api.value(2)
const start = api.coord([idx, lo])
const end = api.coord([idx, hi])
const w = api.size([1, 0])[0] * 0.3
return {
type: 'rect',
shape: { x: start[0] - w / 2, y: end[1], width: w, height: Math.max(start[1] - end[1], 2) },
style: { fill: 'rgba(95,123,160,0.35)', stroke: '#5F7BA0', lineWidth: 1 }
}
},
data: specSeriesData,
encode: { x: 0, y: [1, 2] }
},
{
name: '实际范围',
type: 'custom',
renderItem(params, api) {
const idx = api.value(0)
const lo = api.value(1)
const hi = api.value(2)
const start = api.coord([idx, lo])
const end = api.coord([idx, hi])
const w = api.size([1, 0])[0] * 0.18
return {
type: 'rect',
shape: { x: start[0] - w / 2, y: end[1], width: w, height: Math.max(start[1] - end[1], 2) },
style: { fill: 'rgba(245,108,108,0.45)', stroke: '#F56C6C', lineWidth: 1.5 }
}
},
data: actualSeriesData,
encode: { x: 0, y: [1, 2] }
}
]
}
this.anomalyChartInst.setOption(option)
},
goBack() { this.$router.go(-1) },
jumpToAnomaly(coilId) {
this.coilAnomalyCoilId = coilId
this.coilAnomalyDialog = true
},
selectSegmentType(val) {
this.activeSegmentType = val === undefined || val === null ? '' : val
this.activeSegmentName = ''
@@ -768,9 +538,100 @@ export default {
this.planLoading = true
listProcessPlan({ versionId: this.versionId, pageNum: 1, pageSize: 500 }).then(res => {
this.planList = res.rows || []
this.loadAllParams()
// DR 规程:首次加载如果没有道次数据,自动从双机架版本同步
if (this.planList.length === 0 && this.isDrSpec && !this._drSyncAttempted) {
this._drSyncAttempted = true
this.drAutoSyncPasses()
} else {
this.loadAllParams()
}
}).catch(e => console.error(e)).finally(() => { this.planLoading = false })
},
/**
* DR 规程专用:自动从双机架版本道次数据同步到 wms_process_plan / wms_process_plan_param。
* 仅在首次加载且 planList 为空时触发(通过 _drSyncAttempted 防止重入)。
*/
async drAutoSyncPasses() {
this.drSyncLoading = true
try {
const recipeNo = this.specCode.slice(3) // "DR-R001" → "R001"
// 1. 找到对应双机架方案
const rRes = await listDrRecipe({ recipeNo })
const recipe = (rRes.data || [])[0]
if (!recipe) {
this.$message.warning(`未找到双机架方案 ${recipeNo},请手动新建参数`)
return
}
// 2. 在版本列表中找到与当前 versionCode 匹配的版本
const vRes = await listDrRecipeVersions(recipe.recipeId)
const drVersion = (vRes.data || []).find(v => v.versionCode === this.versionCode)
if (!drVersion) {
this.$message.warning(`未找到双机架版本 ${this.versionCode},请手动新建参数`)
return
}
// 3. 拉取完整版本详情(含 passList
const dRes = await getDrRecipeVersionDetail(drVersion.versionId)
const passList = dRes.data?.passList || []
if (!passList.length) {
this.$message.info('双机架版本暂无道次数据,可手动新建参数')
return
}
// 4. 逐道次写入 wms_process_plan + wms_process_plan_param
const PASS_PARAMS = [
{ code: 'IN_THICK', name: '入口厚度', unit: 'mm', key: 'inThick' },
{ code: 'OUT_THICK', name: '出口厚度', unit: 'mm', key: 'outThick' },
{ code: 'ROLL_FORCE', name: '轧制力', unit: 'kN', key: 'rollForce' },
{ code: 'IN_TENSION', name: '入口张力', unit: 'kN', key: 'inTension' },
{ code: 'OUT_TENSION', name: '出口张力', unit: 'kN', key: 'outTension' },
{ code: 'MAX_SPEED', name: '最高速度', unit: 'm/min', key: 'maxSpeed' },
{ code: 'IN_UNIT_TEN', name: '入口单位张力', unit: '', key: 'inUnitTension' },
{ code: 'OUT_UNIT_TEN', name: '出口单位张力', unit: '', key: 'outUnitTension' }
]
for (const pass of passList) {
const planRes = await addProcessPlan({
versionId: this.versionId,
segmentType: 'PROCESS',
segmentName: '轧制道次',
pointName: `${pass.passNo} 道次`,
pointCode: `PASS_${pass.passNo}`,
sortOrder: pass.passNo || 0,
remark: '由双机架版本道次自动导入'
})
const planId = planRes.data
if (!planId) continue
for (const p of PASS_PARAMS) {
const raw = pass[p.key]
if (raw === null || raw === undefined || raw === '') continue
const num = Number(raw)
if (!isFinite(num)) continue
await addProcessPlanParam({
planId,
paramCode: p.code,
paramName: p.name,
unit: p.unit || null,
targetValue: num
})
}
}
this.$message.success(`已从双机架版本同步 ${passList.length} 道次参数,可继续设置上下限`)
// 重新加载(此时 _drSyncAttempted=true不会再触发同步
this.loadPlans()
} catch (e) {
console.warn('[DR同步] 道次参数同步失败', e)
this.$message.warning('道次参数同步失败,可手动新建参数')
this.loadAllParams() // 同步失败也继续尝试加载已有数据
} finally {
this.drSyncLoading = false
}
},
async loadAllParams() {
if (!this.planList.length) { this.allParamList = []; return }
this.allParamLoading = true
@@ -1486,27 +1347,6 @@ export default {
.seg-process { background: #f0f9eb; color: #3a7a2a; border: 1px solid #b3e19d; }
.seg-outlet { background: #fdf6ec; color: #a86a00; border: 1px solid #f5dab1; }
/* ── 偏差分析 ── */
.anomaly-section-header {
display: flex;
align-items: center;
padding: 10px 0 8px;
margin-top: 14px;
border-top: 2px solid #fdf6ec;
font-size: 13px;
font-weight: 600;
color: #E6A23C;
}
.anomaly-chart {
width: 100%;
height: 260px;
margin-top: 10px;
}
.anomaly-badge { font-size: 12px; color: #F56C6C; font-weight: 600; }
.normal-badge { font-size: 12px; color: #67C23A; }
.no-data-badge { font-size: 12px; color: #c0c4cc; }
.val-over { color: #F56C6C; font-weight: 600; }
.val-under { color: #E6A23C; font-weight: 600; }
/* 导入对话框样式 */
.import-container {
@@ -1564,34 +1404,4 @@ export default {
margin-bottom: 20px;
}
/* ── 服役钢卷记录 ── */
.coil-record-section {
margin-top: 16px;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
}
.coil-record-hd {
display: flex;
align-items: center;
padding: 8px 14px;
font-size: 13px;
font-weight: 600;
color: #303133;
border-bottom: 1px solid #f0f2f5;
background: #fafafa;
}
.coil-total-badge {
font-size: 12px;
font-weight: normal;
color: #606266;
margin-left: 8px;
}
.coil-anomaly-badge {
font-size: 12px;
font-weight: normal;
color: #f56c6c;
margin-left: 6px;
}
</style>

View File

@@ -155,52 +155,14 @@ export default {
},
dicts: ['product_coil_status', 'coil_material', 'coil_itemname', 'coil_manufacturer', 'coil_quality_status'],
data() {
// 工具函数:个位数补零
const addZero = (num) => num.toString().padStart(2, '0')
// 获取当前日期(默认选中当天)
const now = new Date()
const currentDate = `${now.getFullYear()}-${addZero(now.getMonth() + 1)}`
const yesterday = new Date(now)
yesterday.setDate(yesterday.getDate() - 1)
/**
* 生成指定日期/月份的时间范围字符串
* @param {string} dateStr - 支持格式yyyy-MM月份 或 yyyy-MM-dd具体日期
* @returns {object} 包含start开始时间和end结束时间的对象
*/
const getDayTimeRange = (dateStr) => {
// 先校验输入格式是否合法
const monthPattern = /^\d{4}-\d{2}$/; // yyyy-MM 正则
const dayPattern = /^\d{4}-\d{2}-\d{2}$/; // yyyy-MM-dd 正则
if (!monthPattern.test(dateStr) && !dayPattern.test(dateStr)) {
throw new Error('输入格式错误,请传入 yyyy-MM 或 yyyy-MM-dd 格式的字符串');
}
let startDate, endDate;
if (monthPattern.test(dateStr)) {
// 处理 yyyy-MM 格式:获取本月第一天和最后一天
const [year, month] = dateStr.split('-').map(Number);
// 月份是0基的0=1月1=2月...所以要减1
// 第一天yyyy-MM-01
startDate = `${dateStr}-01`;
// 最后一天:通过 new Date(year, month, 0) 计算month是原始月份比如2代表2月传2则取3月0日=2月最后一天
const lastDayOfMonth = new Date(year, month, 0).getDate();
endDate = `${dateStr}-${lastDayOfMonth.toString().padStart(2, '0')}`;
} else {
// 处理 yyyy-MM-dd 格式:直接使用传入的日期
startDate = dateStr;
endDate = dateStr;
}
// 拼接时间部分
return {
start: `${startDate} 00:00:00`,
end: `${endDate} 23:59:59`
};
};
const { start, end } = getDayTimeRange(currentDate)
const start = `${yesterday.getFullYear()}-${addZero(yesterday.getMonth() + 1)}-${addZero(yesterday.getDate())} 00:00:00`
const end = `${now.getFullYear()}-${addZero(now.getMonth() + 1)}-${addZero(now.getDate())} 00:00:00`
return {
lossList: [],
outList: [],

View File

@@ -61,7 +61,13 @@ public class DrMillProcessRecipeController extends BaseController {
@GetMapping("/version/list/{recipeId}")
public R<List<DrMillProcessRecipeVersion>> versionList(@PathVariable Long recipeId) {
return R.ok(versionService.listByRecipeId(recipeId));
List<DrMillProcessRecipeVersion> list = versionService.listByRecipeId(recipeId);
// 同步版本到主库 wms_process_spec_version幂等
DrMillProcessRecipe recipe = recipeService.selectDetailById(recipeId);
if (recipe != null) {
syncService.syncVersionsToSpec(recipe.getRecipeNo(), list);
}
return R.ok(list);
}
@GetMapping("/version/{versionId}")
@@ -71,7 +77,14 @@ public class DrMillProcessRecipeController extends BaseController {
@PostMapping("/version")
public R<Long> addVersion(@RequestBody DrMillProcessRecipeVersion version) {
return R.ok(versionService.save(version));
Long newId = versionService.save(version);
// 新增后重新同步该方案的所有版本
List<DrMillProcessRecipeVersion> all = versionService.listByRecipeId(version.getRecipeId());
DrMillProcessRecipe recipe = recipeService.selectDetailById(version.getRecipeId());
if (recipe != null) {
syncService.syncVersionsToSpec(recipe.getRecipeNo(), all);
}
return R.ok(newId);
}
@PutMapping("/version")
@@ -83,6 +96,15 @@ public class DrMillProcessRecipeController extends BaseController {
@PutMapping("/version/activate/{versionId}")
public R<Void> activate(@PathVariable Long versionId) {
versionService.activate(versionId);
// 激活后重新同步 isActive 状态到主库
DrMillProcessRecipeVersion ver = versionService.getDetailById(versionId);
if (ver != null) {
List<DrMillProcessRecipeVersion> all = versionService.listByRecipeId(ver.getRecipeId());
DrMillProcessRecipe recipe = recipeService.selectDetailById(ver.getRecipeId());
if (recipe != null) {
syncService.syncVersionsToSpec(recipe.getRecipeNo(), all);
}
}
return R.ok();
}

View File

@@ -3,9 +3,12 @@ package com.klp.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.klp.domain.DrMillProcessRecipe;
import com.klp.domain.DrMillProcessRecipeVersion;
import com.klp.domain.WmsProcessSpec;
import com.klp.domain.WmsProcessSpecVersion;
import com.klp.domain.WmsProductionLine;
import com.klp.mapper.WmsProcessSpecMapper;
import com.klp.mapper.WmsProcessSpecVersionMapper;
import com.klp.mapper.WmsProductionLineMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -35,8 +38,9 @@ public class DrRecipeSyncService {
/** spec_code 前缀,区分同名方案号属于哪条产线 */
private static final String SPEC_CODE_PREFIX = "DR-";
private final WmsProcessSpecMapper specMapper;
private final WmsProductionLineMapper lineMapper;
private final WmsProcessSpecMapper specMapper;
private final WmsProcessSpecVersionMapper specVersionMapper;
private final WmsProductionLineMapper lineMapper;
/**
* 检查并补充 wms_process_spec
@@ -72,6 +76,52 @@ public class DrRecipeSyncService {
}
}
/**
* 将双机架工艺版本mill_process_recipe_version同步到主库规程版本wms_process_spec_version
* <p>已存在的版本更新 isActive / status不存在则新增。</p>
*
* @param recipeNo 对应方案号(用于定位 wms_process_spec
* @param drVersions 已从 double-rack 库查出的版本列表
*/
@Transactional(rollbackFor = Exception.class)
public void syncVersionsToSpec(String recipeNo, List<DrMillProcessRecipeVersion> drVersions) {
if (recipeNo == null || drVersions == null || drVersions.isEmpty()) return;
String specCode = SPEC_CODE_PREFIX + recipeNo;
LambdaQueryWrapper<WmsProcessSpec> sqw = Wrappers.lambdaQuery();
sqw.eq(WmsProcessSpec::getSpecCode, specCode);
WmsProcessSpec spec = specMapper.selectOne(sqw);
if (spec == null) return; // spec 尚未同步,跳过
Long specId = spec.getSpecId();
for (DrMillProcessRecipeVersion drVer : drVersions) {
LambdaQueryWrapper<WmsProcessSpecVersion> vqw = Wrappers.lambdaQuery();
vqw.eq(WmsProcessSpecVersion::getSpecId, specId)
.eq(WmsProcessSpecVersion::getVersionCode, drVer.getVersionCode());
WmsProcessSpecVersion existing = specVersionMapper.selectOne(vqw);
int isActive = drVer.getIsActive() != null ? drVer.getIsActive() : 0;
String status = "1".equals(drVer.getStatus()) ? "PUBLISHED" : "DRAFT";
if (existing == null) {
WmsProcessSpecVersion ver = new WmsProcessSpecVersion();
ver.setSpecId(specId);
ver.setVersionCode(drVer.getVersionCode());
ver.setIsActive(isActive);
ver.setStatus(status);
ver.setRemark(drVer.getRemark() != null ? drVer.getRemark() : "由双机架工艺版本自动同步");
specVersionMapper.insert(ver);
log.info("[DR同步] 新增 wms_process_spec_version: specCode={}, versionCode={}",
specCode, drVer.getVersionCode());
} else {
existing.setIsActive(isActive);
existing.setStatus(status);
specVersionMapper.updateById(existing);
}
}
}
/**
* 查找或创建双机架产线记录,返回 line_id。
*/