Merge remote-tracking branch 'origin/0.8.X' into 0.8.X
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -197,11 +197,94 @@
|
||||
<el-button type="primary" size="mini" :loading="excoilLoading" @click="handleFindSearch">查找</el-button>
|
||||
<el-button size="mini" @click="handleFindReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 规程关联 -->
|
||||
<div class="spec-bind-block">
|
||||
<div class="panel-title">规程关联</div>
|
||||
<template v-if="selectedRow">
|
||||
<div class="bind-status">
|
||||
<span class="bind-label">已关联版本</span>
|
||||
<span v-if="coilBindingLoading" class="bind-val muted">加载中…</span>
|
||||
<el-tag v-else-if="coilBinding" type="success" size="mini" effect="plain">
|
||||
{{ coilBinding.versionCode }}
|
||||
</el-tag>
|
||||
<span v-else class="bind-val muted">未关联</span>
|
||||
</div>
|
||||
<el-button
|
||||
size="mini"
|
||||
:type="coilBinding ? 'default' : 'primary'"
|
||||
icon="el-icon-link"
|
||||
style="width:100%;margin-top:8px"
|
||||
@click="openSpecBindDialog"
|
||||
>{{ coilBinding ? '重新关联' : '关联规程版本' }}</el-button>
|
||||
</template>
|
||||
<span v-else class="bind-val muted" style="font-size:12px">请先选择钢卷</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<quality-report-dialog ref="qualityReport" />
|
||||
|
||||
<!-- 规程版本选择弹窗 -->
|
||||
<el-dialog
|
||||
title="关联规程版本"
|
||||
:visible.sync="specBindDialog"
|
||||
width="580px"
|
||||
append-to-body
|
||||
@closed="specBindSelectedId = null"
|
||||
>
|
||||
<div class="spec-bind-toolbar">
|
||||
<el-switch
|
||||
v-model="specBindShowAll"
|
||||
active-text="全部版本"
|
||||
inactive-text="仅生效版本"
|
||||
style="margin-right:8px"
|
||||
/>
|
||||
<span class="spec-bind-hint">共 {{ specVersionsForDialog.length }} 条</span>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="specVersionLoading"
|
||||
:data="specVersionsForDialog"
|
||||
size="mini"
|
||||
border
|
||||
highlight-current-row
|
||||
max-height="340"
|
||||
style="margin-top:10px"
|
||||
@row-click="row => specBindSelectedId = row.versionId"
|
||||
>
|
||||
<el-table-column width="36" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
<i
|
||||
v-if="specBindSelectedId === row.versionId"
|
||||
class="el-icon-check"
|
||||
style="color:#409eff;font-weight:700"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="规程名称" prop="specName" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="版本号" prop="versionCode" width="90" />
|
||||
<el-table-column label="状态" align="center" width="80">
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag v-if="row.isActive === 1" type="success" size="mini" effect="dark">生效中</el-tag>
|
||||
<el-tag v-else-if="row.status === 'PUBLISHED'" size="mini" effect="plain">已发布</el-tag>
|
||||
<el-tag v-else type="info" size="mini" effect="plain">草稿</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="规程编码" prop="specCode" width="130" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<div slot="footer">
|
||||
<el-button size="small" @click="specBindDialog = false">取消</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
:disabled="!specBindSelectedId"
|
||||
:loading="specBindLoading"
|
||||
@click="confirmSpecBind"
|
||||
>确认关联</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -216,10 +299,11 @@ import {
|
||||
getTimingRealtimeData,
|
||||
getPresetSetupByCoilId
|
||||
} from '@/api/l2/timing'
|
||||
import { listProcessSpecVersion } from '@/api/wms/processSpecVersion'
|
||||
import { listProcessPlan, addProcessPlan } from '@/api/wms/processPlan'
|
||||
import { listProcessPlanParam, addProcessPlanParam, updateProcessPlanParam } from '@/api/wms/processPlanParam'
|
||||
import { upsertProcessCoilRecord } from '@/api/wms/processCoilRecord'
|
||||
import { listProcessSpecVersion, getProcessSpecVersion } from '@/api/wms/processSpecVersion'
|
||||
import { listProcessCoilRecord, upsertProcessCoilRecord } from '@/api/wms/processCoilRecord'
|
||||
import { listProcessSpec } from '@/api/wms/processSpec'
|
||||
import { listProcessPlan } from '@/api/wms/processPlan'
|
||||
import { listProcessPlanParam } from '@/api/wms/processPlanParam'
|
||||
import { batchAddProcessAnomaly } from '@/api/wms/processAnomaly'
|
||||
|
||||
// 趋势参数树结构,对应 PLTCM_PRO_SEG 列名
|
||||
@@ -266,10 +350,11 @@ const TREND_GROUPS = [
|
||||
]
|
||||
|
||||
// V_VBDA_GAUGE 厚度曲线:4 个图,列名来自 DDL
|
||||
// 厚度图纵轴显示实测/设定值百分比,速度图保持原单位
|
||||
const GAUGE_COLS = [
|
||||
{ col: 'THICK0', title: '入口测厚仪 [mm]' },
|
||||
{ col: 'THICK1', title: '1架出口厚度 [mm]' },
|
||||
{ col: 'THICK4', title: '末架出口厚度 [mm]' },
|
||||
{ col: 'THICK0', title: '入口测厚仪 [%]' },
|
||||
{ col: 'THICK1', title: '1架出口厚度 [%]' },
|
||||
{ col: 'THICK4', title: '末架出口厚度 [%]' },
|
||||
{ col: 'EXIT_SPEED', title: '轧制速度 [m/min]' }
|
||||
]
|
||||
|
||||
@@ -317,8 +402,9 @@ function calcYRange(vals) {
|
||||
/**
|
||||
* 生成折线图 option。
|
||||
* extras: [{ name, data, color, dash }] — 上下限或参考线
|
||||
* yAxisSuffix: 纵轴标签后缀,如 '%'
|
||||
*/
|
||||
function makeLine(title, xData, yData, extras = []) {
|
||||
function makeLine(title, xData, yData, extras = [], yAxisSuffix = '') {
|
||||
const allVals = [yData, ...extras.map(e => e.data)].flat()
|
||||
const range = calcYRange(allVals)
|
||||
const hasExtras = extras.length > 0
|
||||
@@ -338,7 +424,12 @@ function makeLine(title, xData, yData, extras = []) {
|
||||
? { data: [title, ...extras.map(e => e.name)], top: 4, right: 4,
|
||||
textStyle: { fontSize: 9 }, itemWidth: 14, itemHeight: 8 }
|
||||
: { show: false },
|
||||
tooltip: { trigger: 'axis' },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: yAxisSuffix
|
||||
? params => params.map(p => `${p.marker}${p.seriesName}: ${p.value != null ? p.value + yAxisSuffix : '—'}`).join('<br/>')
|
||||
: undefined
|
||||
},
|
||||
grid: { top: hasExtras ? 44 : 36, bottom: 28, left: 8, right: 16, containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category', data: xData,
|
||||
@@ -346,7 +437,11 @@ function makeLine(title, xData, yData, extras = []) {
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value', min: range.min, max: range.max,
|
||||
nameTextStyle: { fontSize: 10 }, axisLabel: { fontSize: 10 }
|
||||
nameTextStyle: { fontSize: 10 },
|
||||
axisLabel: {
|
||||
fontSize: 10,
|
||||
formatter: yAxisSuffix ? val => val + yAxisSuffix : undefined
|
||||
}
|
||||
},
|
||||
dataZoom: [
|
||||
{ type: 'inside', xAxisIndex: 0, zoomOnMouseWheel: true, moveOnMouseMove: true },
|
||||
@@ -396,7 +491,33 @@ export default {
|
||||
pagination: { page: 1, pageSize: 50, total: 0 },
|
||||
topTableHeight: 'calc(40vh - 80px)',
|
||||
chartInstances: [],
|
||||
resizeHandler: null
|
||||
resizeHandler: null,
|
||||
// ── 规程关联 ──
|
||||
coilBinding: null, // 当前选中钢卷已有的关联记录 { versionId, versionCode, ... }
|
||||
coilBindingLoading: false,
|
||||
specBindDialog: false,
|
||||
specVersionLoading: false,
|
||||
specVersionRawList: [], // 所有 wms_process_spec_version
|
||||
specList: [], // 所有 wms_process_spec(用于显示规程名)
|
||||
specBindSelectedId: null, // 弹窗内选中的 versionId
|
||||
specBindShowAll: false, // true=显示全部版本,false=只显示生效版本
|
||||
specBindLoading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/** 弹窗中展示的版本列表(含规程名,支持只看生效版本) */
|
||||
specVersionsForDialog() {
|
||||
const specMap = {}
|
||||
this.specList.forEach(s => { specMap[s.specId] = s })
|
||||
let list = this.specVersionRawList.map(v => ({
|
||||
...v,
|
||||
specCode: specMap[v.specId] ? specMap[v.specId].specCode : '—',
|
||||
specName: specMap[v.specId] ? specMap[v.specId].specName : '—',
|
||||
}))
|
||||
if (!this.specBindShowAll) {
|
||||
list = list.filter(v => v.isActive === 1)
|
||||
}
|
||||
return list
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@@ -431,18 +552,15 @@ export default {
|
||||
// 快速点击防重:每次点击递增版本号,旧的 sync 任务检测到版本号变更后自动放弃
|
||||
const clickRev = ++this._clickRev
|
||||
|
||||
// 记录点击前的钢卷号,用于判断是否同一行重复点击
|
||||
const prevExcoilId = this.selectedRow
|
||||
? (this.selectedRow.EXCOILID || this.selectedRow.excoilid || null)
|
||||
: null
|
||||
|
||||
this.selectedRow = row
|
||||
this.segData = null
|
||||
this.gaugeRows = null
|
||||
this.shapeRows = null
|
||||
this.presetData = null
|
||||
this.selectedTrendParam = null
|
||||
this.coilBinding = null
|
||||
this.disposeAllCharts()
|
||||
this.loadCoilBinding()
|
||||
|
||||
const encoilId = row.ENCOILID || row.encoilid
|
||||
const excoilId = row.EXCOILID || row.excoilid
|
||||
@@ -456,13 +574,6 @@ export default {
|
||||
// 如果期间又点击了其他行则放弃后续操作
|
||||
if (this._clickRev !== clickRev) return
|
||||
|
||||
// 同一钢卷重复点击:跳过规程同步,避免重复写入
|
||||
const isSameCoil = excoilId && excoilId === prevExcoilId
|
||||
if (!isSameCoil) {
|
||||
// 后台静默同步到规程(不阻塞 UI)
|
||||
this.autoSyncToActiveSpec(excoilId || encoilId, clickRev)
|
||||
}
|
||||
|
||||
await this.$nextTick()
|
||||
// 加载完成后自动选中第一个趋势参数
|
||||
if (this.activeTab === 'trend' && this.segData) {
|
||||
@@ -624,6 +735,7 @@ export default {
|
||||
},
|
||||
|
||||
// ── 厚度曲线 (V_VBDA_GAUGE) ──────────────────
|
||||
// 有参考值的厚度图纵轴显示百分比:实测值 / 设定值 × 100
|
||||
renderGaugeCharts() {
|
||||
const rows = this.gaugeRows
|
||||
if (!rows || !rows.length) return
|
||||
@@ -633,33 +745,48 @@ export default {
|
||||
const chartRefs = ['chartGauge1', 'chartGauge2', 'chartGauge3', 'chartGauge4']
|
||||
const charts = chartRefs.map((ref, i) => {
|
||||
const { col, title } = GAUGE_COLS[i]
|
||||
const yData = rows.map(r => {
|
||||
const v = getRowVal(r, col)
|
||||
return v == null ? null : parseFloat(v.toFixed(4))
|
||||
})
|
||||
const extras = []
|
||||
const refCol = refColMap[col]
|
||||
|
||||
if (refCol) {
|
||||
const refData = rows.map(r => {
|
||||
// ── 厚度图:转为百分比(实测 / 设定 × 100) ──
|
||||
const yData = rows.map(r => {
|
||||
const v = getRowVal(r, col)
|
||||
const rv = getRowVal(r, refCol)
|
||||
return rv == null ? null : parseFloat(rv.toFixed(4))
|
||||
if (v == null || rv == null || rv === 0) return null
|
||||
return parseFloat(((v / rv) * 100).toFixed(3))
|
||||
})
|
||||
// 上限 = REF + TOPLIMIT;下限 = REF + BOTLIMIT(TOPLIMIT/BOTLIMIT 单位与测厚仪一致)
|
||||
const upData = rows.map((r, j) => {
|
||||
const extras = []
|
||||
// 目标值恒为 100%
|
||||
const refLine = rows.map(r => {
|
||||
const rv = getRowVal(r, refCol)
|
||||
return (rv != null && rv !== 0) ? 100 : null
|
||||
})
|
||||
// 上限百分比 = (REF + TOPLIMIT) / REF × 100
|
||||
const upData = rows.map(r => {
|
||||
const rv = getRowVal(r, refCol)
|
||||
const tl = getRowVal(r, 'TOPLIMIT')
|
||||
return rv == null ? null : parseFloat((rv + (tl ?? 3)).toFixed(4))
|
||||
if (rv == null || rv === 0) return null
|
||||
return parseFloat(((rv + (tl ?? 3)) / rv * 100).toFixed(3))
|
||||
})
|
||||
const loData = rows.map((r, j) => {
|
||||
// 下限百分比 = (REF + BOTLIMIT) / REF × 100
|
||||
const loData = rows.map(r => {
|
||||
const rv = getRowVal(r, refCol)
|
||||
const bl = getRowVal(r, 'BOTLIMIT')
|
||||
return rv == null ? null : parseFloat((rv + (bl ?? -3)).toFixed(4))
|
||||
if (rv == null || rv === 0) return null
|
||||
return parseFloat(((rv + (bl ?? -3)) / rv * 100).toFixed(3))
|
||||
})
|
||||
if (refData.some(v => v != null)) extras.push({ name: '目标值', data: refData, color: '#909399', dash: false })
|
||||
if (refLine.some(v => v != null)) extras.push({ name: '目标值(100%)', data: refLine, color: '#909399', dash: false })
|
||||
if (upData.some(v => v != null)) extras.push({ name: '上限', data: upData, color: '#F56C6C', dash: true })
|
||||
if (loData.some(v => v != null)) extras.push({ name: '下限', data: loData, color: '#67C23A', dash: true })
|
||||
return this.makeChart(ref, makeLine(title, xData, yData, extras, '%'))
|
||||
} else {
|
||||
// ── 速度等无参考值的图:保持原始单位 ──
|
||||
const yData = rows.map(r => {
|
||||
const v = getRowVal(r, col)
|
||||
return v == null ? null : parseFloat(v.toFixed(4))
|
||||
})
|
||||
return this.makeChart(ref, makeLine(title, xData, yData))
|
||||
}
|
||||
return this.makeChart(ref, makeLine(title, xData, yData, extras))
|
||||
})
|
||||
this.chartInstances = charts.filter(Boolean)
|
||||
this.setupResize()
|
||||
@@ -852,199 +979,13 @@ export default {
|
||||
this.gaugeRows = null
|
||||
this.shapeRows = null
|
||||
this.selectedTrendParam = null
|
||||
this.coilBinding = null
|
||||
this.disposeAllCharts()
|
||||
this.pagination.page = 1
|
||||
this.loadExcoilCount()
|
||||
this.loadExcoilList()
|
||||
},
|
||||
|
||||
// ── 自动同步到生效规程 ────────────────────────
|
||||
async autoSyncToActiveSpec(coilId, rev) {
|
||||
if (!this.presetData && !this.segData) return
|
||||
const guard = () => rev !== undefined && this._clickRev !== rev
|
||||
try {
|
||||
// ① 查找生效版本
|
||||
const verRes = await listProcessSpecVersion({ isActive: 1, pageNum: 1, pageSize: 10 })
|
||||
if (guard()) return
|
||||
const activeVer = (verRes.rows || []).find(v => v.isActive === 1)
|
||||
if (!activeVer) return
|
||||
const versionId = activeVer.versionId
|
||||
|
||||
// ② 构建本次写入条目
|
||||
const items = this.buildSpecSyncItems()
|
||||
if (!items.length) return
|
||||
|
||||
// ③ 加载已有 plan 点位
|
||||
const plansRes = await listProcessPlan({ versionId, pageNum: 1, pageSize: 500 })
|
||||
if (guard()) return
|
||||
const planMap = {}
|
||||
;(plansRes.rows || []).forEach(p => { planMap[p.pointCode] = p })
|
||||
|
||||
// ④ 逐条 upsert 点位 & 参数,收集异常
|
||||
const anomalies = []
|
||||
const detectedAt = new Date().toISOString()
|
||||
// 提取本次钢卷的 enCoilId(入口卷号)
|
||||
const enCoilId = this.selectedRow ? (this.selectedRow.ENCOILID || this.selectedRow.encoilid || null) : null
|
||||
|
||||
for (const item of items) {
|
||||
if (guard()) return
|
||||
|
||||
// 确保 plan 点位存在(幂等:插入失败时降级重查,应对并发竞态)
|
||||
let planId
|
||||
const ep = planMap[item.pointCode]
|
||||
if (ep) {
|
||||
planId = ep.planId
|
||||
} else {
|
||||
try {
|
||||
const r = await addProcessPlan({
|
||||
versionId,
|
||||
segmentType: 'PROCESS',
|
||||
segmentName: item.groupLabel,
|
||||
pointName: item.pointName,
|
||||
pointCode: item.pointCode,
|
||||
sortOrder: 0
|
||||
})
|
||||
if (guard()) return
|
||||
planId = r.data
|
||||
// 写入本地 map,同一次 sync 内不再重复插入
|
||||
planMap[item.pointCode] = { planId }
|
||||
} catch (_dupErr) {
|
||||
// 唯一键冲突:该点位已由并发请求写入,重新查询获取真实 planId
|
||||
const refetch = await listProcessPlan({ versionId, pageNum: 1, pageSize: 500 })
|
||||
if (guard()) return
|
||||
const found = (refetch.rows || []).find(p => p.pointCode === item.pointCode)
|
||||
if (!found) continue // 极端情况:查不到则跳过本条目
|
||||
planId = found.planId
|
||||
planMap[item.pointCode] = found
|
||||
}
|
||||
}
|
||||
|
||||
// 查已存储参数
|
||||
const prRes = await listProcessPlanParam({ planId, pageNum: 1, pageSize: 100 })
|
||||
if (guard()) return
|
||||
const stored = (prRes.rows || []).find(p => p.paramCode === item.paramCode)
|
||||
|
||||
// 异常检测:与已有上下限比对
|
||||
if (stored) {
|
||||
const sUp = stored.upperLimit != null ? Number(stored.upperLimit) : null
|
||||
const sLo = stored.lowerLimit != null ? Number(stored.lowerLimit) : null
|
||||
const aUp = item.upperLimit
|
||||
const aLo = item.lowerLimit
|
||||
const overTypes = []
|
||||
if (sUp != null && aUp != null && aUp > sUp) overTypes.push('OVER_MAX')
|
||||
if (sLo != null && aLo != null && aLo < sLo) overTypes.push('UNDER_MIN')
|
||||
if (overTypes.length) {
|
||||
anomalies.push({
|
||||
versionId,
|
||||
planId,
|
||||
paramId: stored.paramId || null,
|
||||
coilId,
|
||||
enCoilId,
|
||||
paramCode: item.paramCode,
|
||||
paramName: item.paramName,
|
||||
unit: item.unit,
|
||||
anomalyType: overTypes.length === 2 ? 'BOTH' : overTypes[0],
|
||||
storedTarget: stored.targetValue != null ? Number(stored.targetValue) : null,
|
||||
storedUpper: sUp,
|
||||
storedLower: sLo,
|
||||
actualTarget: item.targetValue,
|
||||
actualMax: aUp,
|
||||
actualMin: aLo,
|
||||
deviationMax: sUp != null && aUp != null ? parseFloat((aUp - sUp).toFixed(4)) : null,
|
||||
deviationMin: sLo != null && aLo != null ? parseFloat((aLo - sLo).toFixed(4)) : null,
|
||||
detectedAt
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 写入/更新参数
|
||||
// target_value 始终覆盖(反映最新L1设定)
|
||||
// upper/lower 仅首次写入(null 时写入作为基线)
|
||||
// actualSrcId / presetSrcId 仅首次写入
|
||||
if (stored) {
|
||||
await updateProcessPlanParam({
|
||||
...stored,
|
||||
targetValue: item.targetValue ?? stored.targetValue,
|
||||
upperLimit: stored.upperLimit ?? item.upperLimit,
|
||||
lowerLimit: stored.lowerLimit ?? item.lowerLimit,
|
||||
unit: item.unit || stored.unit,
|
||||
actualSrcId: stored.actualSrcId || enCoilId,
|
||||
presetSrcId: stored.presetSrcId || coilId
|
||||
})
|
||||
} else {
|
||||
await addProcessPlanParam({
|
||||
planId,
|
||||
paramCode: item.paramCode,
|
||||
paramName: item.paramName,
|
||||
targetValue: item.targetValue,
|
||||
upperLimit: item.upperLimit,
|
||||
lowerLimit: item.lowerLimit,
|
||||
unit: item.unit,
|
||||
actualSrcId: enCoilId,
|
||||
presetSrcId: coilId
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (guard()) return
|
||||
|
||||
// ⑤ 写入钢卷服役记录(幂等 upsert)
|
||||
await upsertProcessCoilRecord({
|
||||
versionId,
|
||||
coilId,
|
||||
enCoilId,
|
||||
hasAnomaly: anomalies.length > 0 ? 1 : 0,
|
||||
anomalyCnt: anomalies.length,
|
||||
processTime: detectedAt
|
||||
})
|
||||
|
||||
// ⑥ 持久化异常到数据库
|
||||
if (anomalies.length) {
|
||||
await batchAddProcessAnomaly(anomalies)
|
||||
console.log(`[规程同步] 检测到 ${anomalies.length} 个参数异常,已写入数据库`)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[规程同步] 后台同步失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
/** 从当前 segData + presetData 构建写入条目 */
|
||||
buildSpecSyncItems() {
|
||||
const items = []
|
||||
for (const group of TREND_GROUPS) {
|
||||
for (const item of group.children) {
|
||||
const col = item.col
|
||||
const maxArr = this.segData ? this.seg(col + 'MAX').filter(v => v != null) : []
|
||||
const minArr = this.segData ? this.seg(col + 'MIN').filter(v => v != null) : []
|
||||
const presetCol = TREND_PRESET_MAP[col]
|
||||
|
||||
let targetValue = null
|
||||
if (presetCol && this.presetData) {
|
||||
const sv = this.presetData[presetCol] !== undefined
|
||||
? this.presetData[presetCol]
|
||||
: this.presetData[presetCol.toLowerCase()]
|
||||
if (sv != null && Number(sv) !== 0) targetValue = parseFloat(Number(sv).toFixed(4))
|
||||
}
|
||||
const upperLimit = maxArr.length ? parseFloat(Math.max(...maxArr).toFixed(4)) : null
|
||||
const lowerLimit = minArr.length ? parseFloat(Math.min(...minArr).toFixed(4)) : null
|
||||
|
||||
if (targetValue == null && upperLimit == null && lowerLimit == null) continue
|
||||
items.push({
|
||||
groupLabel: group.label,
|
||||
pointCode: col,
|
||||
pointName: item.label,
|
||||
paramCode: col,
|
||||
paramName: item.label,
|
||||
targetValue,
|
||||
upperLimit,
|
||||
lowerLimit,
|
||||
unit: TREND_UNIT_MAP[col] || ''
|
||||
})
|
||||
}
|
||||
}
|
||||
return items
|
||||
},
|
||||
|
||||
openQualityReport(row) {
|
||||
// If the clicked row is already the selected row, use in-memory data
|
||||
const isSame = this.selectedRow &&
|
||||
@@ -1056,6 +997,178 @@ export default {
|
||||
this.$refs.qualityReport.open(row, segData, gaugeRows, shapeRows, presetData)
|
||||
},
|
||||
|
||||
// ── 规程关联 ─────────────────────────────────────────
|
||||
/** 加载当前选中钢卷的已有规程关联记录 */
|
||||
async loadCoilBinding() {
|
||||
if (!this.selectedRow) return
|
||||
const coilId = this.selectedRow.EXCOILID || this.selectedRow.excoilid
|
||||
if (!coilId) return
|
||||
this.coilBindingLoading = true
|
||||
try {
|
||||
const res = await listProcessCoilRecord({ coilId, pageNum: 1, pageSize: 1 })
|
||||
const rec = (res.rows || [])[0]
|
||||
if (rec) {
|
||||
// 补充版本号显示
|
||||
try {
|
||||
const verRes = await getProcessSpecVersion(rec.versionId)
|
||||
this.coilBinding = { ...rec, versionCode: verRes.data?.versionCode || String(rec.versionId) }
|
||||
} catch {
|
||||
this.coilBinding = { ...rec, versionCode: String(rec.versionId) }
|
||||
}
|
||||
} else {
|
||||
this.coilBinding = null
|
||||
}
|
||||
} catch {
|
||||
this.coilBinding = null
|
||||
} finally {
|
||||
this.coilBindingLoading = false
|
||||
}
|
||||
},
|
||||
|
||||
/** 打开选择规程版本的弹窗 */
|
||||
async openSpecBindDialog() {
|
||||
this.specBindSelectedId = this.coilBinding ? this.coilBinding.versionId : null
|
||||
this.specBindShowAll = !this.coilBinding // 若已关联默认显示全部;否则只看生效版本
|
||||
this.specBindDialog = true
|
||||
this.specVersionLoading = true
|
||||
try {
|
||||
const [verRes, specRes] = await Promise.all([
|
||||
listProcessSpecVersion({ pageNum: 1, pageSize: 500 }),
|
||||
listProcessSpec({ pageNum: 1, pageSize: 200 })
|
||||
])
|
||||
this.specVersionRawList = verRes.rows || []
|
||||
this.specList = specRes.rows || []
|
||||
} finally {
|
||||
this.specVersionLoading = false
|
||||
}
|
||||
},
|
||||
|
||||
/** 确认关联,并自动检测 L1 实绩与规程参数限值的偏差,将异常落库 */
|
||||
async confirmSpecBind() {
|
||||
if (!this.specBindSelectedId) return
|
||||
this.specBindLoading = true
|
||||
try {
|
||||
const coilId = this.selectedRow.EXCOILID || this.selectedRow.excoilid
|
||||
const enCoilId = this.selectedRow.ENCOILID || this.selectedRow.encoilid || null
|
||||
const versionId = this.specBindSelectedId
|
||||
|
||||
// 1. 先写入基础关联记录(异常数稍后更新)
|
||||
await upsertProcessCoilRecord({
|
||||
versionId,
|
||||
coilId,
|
||||
enCoilId,
|
||||
hasAnomaly: 0,
|
||||
anomalyCnt: 0,
|
||||
processTime: new Date().toISOString()
|
||||
})
|
||||
|
||||
// 2. 异常检测(仅当存在 SEG 数据时执行)
|
||||
let anomalyCnt = 0
|
||||
if (this.segData) {
|
||||
try {
|
||||
// 2.1 获取该版本下所有方案点位
|
||||
const planRes = await listProcessPlan({ versionId })
|
||||
const plans = planRes.rows || []
|
||||
|
||||
// 2.2 汇总所有点位的参数
|
||||
const allParams = []
|
||||
for (const plan of plans) {
|
||||
const paramRes = await listProcessPlanParam({ planId: plan.planId })
|
||||
;(paramRes.rows || []).forEach(p => allParams.push({ ...p, planId: plan.planId }))
|
||||
}
|
||||
|
||||
// 2.3 逐参数比对,构建异常列表
|
||||
const anomalies = []
|
||||
for (const param of allParams) {
|
||||
const col = (param.paramCode || '').toUpperCase()
|
||||
if (!col) continue
|
||||
|
||||
// 从 SEG 系列数据取段内最大/最小值数组,再求全卷极值
|
||||
const maxArr = this.seg(col + 'MAX').filter(v => v != null)
|
||||
const minArr = this.seg(col + 'MIN').filter(v => v != null)
|
||||
if (!maxArr.length && !minArr.length) continue
|
||||
|
||||
const actualMax = maxArr.length ? Math.max(...maxArr) : null
|
||||
const actualMin = minArr.length ? Math.min(...minArr) : null
|
||||
|
||||
// 从预设数据取实际设定值(如有对应映射)
|
||||
let actualTarget = null
|
||||
const presetCol = TREND_PRESET_MAP[col]
|
||||
if (presetCol && this.presetData) {
|
||||
const raw = this.presetData[presetCol] !== undefined
|
||||
? this.presetData[presetCol]
|
||||
: this.presetData[presetCol.toLowerCase()]
|
||||
if (raw != null) actualTarget = parseFloat(Number(raw).toFixed(4))
|
||||
}
|
||||
|
||||
const upper = param.upperLimit != null ? Number(param.upperLimit) : null
|
||||
const lower = param.lowerLimit != null ? Number(param.lowerLimit) : null
|
||||
if (upper == null && lower == null) continue // 未配置限值,跳过
|
||||
|
||||
const overMax = upper != null && actualMax != null && actualMax > upper
|
||||
const underMin = lower != null && actualMin != null && actualMin < lower
|
||||
if (!overMax && !underMin) continue
|
||||
|
||||
const anomalyType = (overMax && underMin) ? 'BOTH' : (overMax ? 'OVER_MAX' : 'UNDER_MIN')
|
||||
|
||||
anomalies.push({
|
||||
versionId,
|
||||
planId: param.planId,
|
||||
paramId: param.paramId,
|
||||
coilId,
|
||||
enCoilId,
|
||||
paramCode: param.paramCode,
|
||||
paramName: param.paramName,
|
||||
unit: param.unit || TREND_UNIT_MAP[col] || null,
|
||||
anomalyType,
|
||||
storedTarget: param.targetValue != null ? Number(param.targetValue) : null,
|
||||
storedUpper: upper,
|
||||
storedLower: lower,
|
||||
actualTarget,
|
||||
actualMax,
|
||||
actualMin,
|
||||
deviationMax: overMax ? parseFloat((actualMax - upper).toFixed(4)) : null,
|
||||
deviationMin: underMin ? parseFloat((actualMin - lower).toFixed(4)) : null,
|
||||
detectedAt: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
anomalyCnt = anomalies.length
|
||||
|
||||
// 2.4 批量写入异常记录
|
||||
if (anomalies.length > 0) {
|
||||
await batchAddProcessAnomaly(anomalies)
|
||||
}
|
||||
|
||||
// 2.5 若有异常则更新关联记录的计数字段
|
||||
if (anomalyCnt > 0) {
|
||||
await upsertProcessCoilRecord({
|
||||
versionId,
|
||||
coilId,
|
||||
enCoilId,
|
||||
hasAnomaly: 1,
|
||||
anomalyCnt,
|
||||
processTime: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[规程关联] 异常检测失败,跳过', e)
|
||||
}
|
||||
}
|
||||
|
||||
const msg = anomalyCnt > 0
|
||||
? `关联成功,检测到 ${anomalyCnt} 个参数异常`
|
||||
: '关联成功,无异常'
|
||||
this.$message[anomalyCnt > 0 ? 'warning' : 'success'](msg)
|
||||
this.specBindDialog = false
|
||||
await this.loadCoilBinding()
|
||||
} catch {
|
||||
this.$message.error('关联失败,请重试')
|
||||
} finally {
|
||||
this.specBindLoading = false
|
||||
}
|
||||
},
|
||||
|
||||
calcLengthPerTon(row) {
|
||||
const len = parseFloat(row.EXIT_LENGTH || row.exit_length)
|
||||
const wt = parseFloat(row.MEAS_EXIT_WEIGHT || row.meas_exit_weight)
|
||||
@@ -1276,4 +1389,45 @@ export default {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ── 规程关联区块 ── */
|
||||
.spec-bind-block {
|
||||
border-top: 1px solid #ebeef5;
|
||||
padding-top: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.bind-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.bind-label {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bind-val {
|
||||
font-size: 12px;
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
&.muted { color: #c0c4cc; font-weight: normal; }
|
||||
}
|
||||
|
||||
/* ── 弹窗内工具栏 ── */
|
||||
.spec-bind-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.spec-bind-hint {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
>
|
||||
<div class="rpt-toolbar">
|
||||
<span class="rpt-coil-hint">{{ coilLabel }}</span>
|
||||
<el-button size="small" type="primary" icon="el-icon-download" :loading="exporting" @click="exportPdf">导出 PDF</el-button>
|
||||
<span v-if="dataLoading" style="font-size:12px;color:#909399;margin-right:12px">
|
||||
<i class="el-icon-loading" /> 加载图表数据…
|
||||
</span>
|
||||
<el-button size="small" type="primary" icon="el-icon-download" :loading="exporting" :disabled="dataLoading" @click="exportPdf">导出 PDF</el-button>
|
||||
</div>
|
||||
|
||||
<div ref="reportContent" class="report-body">
|
||||
@@ -136,6 +139,11 @@
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
import {
|
||||
getTimingSegByEncoilId,
|
||||
getTimingRealtimeData,
|
||||
getPresetSetupByCoilId
|
||||
} from '@/api/l2/timing'
|
||||
|
||||
function getV(row, col) {
|
||||
if (!row) return null
|
||||
@@ -202,15 +210,16 @@ export default {
|
||||
name: 'QualityReportDialog',
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
exporting: false,
|
||||
row: null,
|
||||
segData: null,
|
||||
gaugeRows: null,
|
||||
shapeRows: null,
|
||||
presetData: null,
|
||||
nowStr: '',
|
||||
chartInsts: []
|
||||
visible: false,
|
||||
exporting: false,
|
||||
dataLoading: false,
|
||||
row: null,
|
||||
segData: null,
|
||||
gaugeRows: null,
|
||||
shapeRows: null,
|
||||
presetData: null,
|
||||
nowStr: '',
|
||||
chartInsts: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -282,18 +291,68 @@ export default {
|
||||
fmtDate(v) { if (!v) return '—'; return String(v).replace('T', ' ').substring(0, 19) },
|
||||
|
||||
open(row, segData, gaugeRows, shapeRows, presetData) {
|
||||
this.row = row
|
||||
this.segData = segData || null
|
||||
this.gaugeRows = gaugeRows || null
|
||||
this.shapeRows = shapeRows || null
|
||||
this.presetData = presetData || null
|
||||
this.visible = true
|
||||
this.row = row
|
||||
this.segData = segData || null
|
||||
this.gaugeRows = gaugeRows || null
|
||||
this.shapeRows = shapeRows || null
|
||||
this.presetData = presetData || null
|
||||
this.dataLoading = false
|
||||
this.visible = true
|
||||
},
|
||||
|
||||
// @opened fires AFTER the dialog's CSS transition completes — DOM is fully painted
|
||||
onOpened() {
|
||||
async onOpened() {
|
||||
this.nowStr = new Date().toLocaleString('zh-CN').replace(/\//g, '-')
|
||||
this.$nextTick(() => this.renderAll())
|
||||
// 若父组件没有传入数据(点击的行不是当前选中行),自行拉取
|
||||
if (!this.segData && !this.gaugeRows && !this.shapeRows) {
|
||||
await this.fetchMissingData()
|
||||
}
|
||||
// 等 Vue 把 v-if 新增的图表 DOM 全部渲染出来再初始化 ECharts
|
||||
await this.$nextTick()
|
||||
this.renderAll()
|
||||
},
|
||||
|
||||
/** 自行从 L1 接口获取 SEG / 实时 / 预设数据 */
|
||||
async fetchMissingData() {
|
||||
const encoilId = getV(this.row, 'ENCOILID')
|
||||
const excoilId = getV(this.row, 'EXCOILID')
|
||||
if (!encoilId && !excoilId) return
|
||||
this.dataLoading = true
|
||||
try {
|
||||
await Promise.all([
|
||||
encoilId ? this._loadSeg(encoilId) : Promise.resolve(),
|
||||
encoilId ? this._loadPreset(encoilId) : Promise.resolve(),
|
||||
excoilId ? this._loadRealtime(excoilId): Promise.resolve()
|
||||
])
|
||||
} catch (e) {
|
||||
console.warn('[质保书] 数据加载失败', e)
|
||||
} finally {
|
||||
this.dataLoading = false
|
||||
}
|
||||
},
|
||||
async _loadSeg(encoilId) {
|
||||
try {
|
||||
const res = await getTimingSegByEncoilId(encoilId)
|
||||
const rows = res?.data?.rows || []
|
||||
this.segData = rows.length ? (res?.data?.series || null) : null
|
||||
} catch (_) {}
|
||||
},
|
||||
async _loadRealtime(excoilId) {
|
||||
try {
|
||||
const res = await getTimingRealtimeData(excoilId)
|
||||
const g = res?.data?.gauge?.result
|
||||
const s = res?.data?.shape?.result
|
||||
this.gaugeRows = Array.isArray(g) ? g : null
|
||||
this.shapeRows = Array.isArray(s) ? s : null
|
||||
} catch (_) {}
|
||||
},
|
||||
async _loadPreset(coilId) {
|
||||
try {
|
||||
const res = await getPresetSetupByCoilId(coilId)
|
||||
this.presetData = res?.data?.data || null
|
||||
} catch (_) {
|
||||
this.presetData = null
|
||||
}
|
||||
},
|
||||
onClose() {
|
||||
this.disposeCharts()
|
||||
@@ -481,7 +540,7 @@ export default {
|
||||
|
||||
const w = el.offsetWidth || 840
|
||||
const c = echarts.init(el, null, { renderer: 'canvas', width: w, height: 200 })
|
||||
this.chartInsts.push(c)
|
||||
this.chartInsts.push(c) // 只 push 一次
|
||||
c.setOption({
|
||||
title: { text: '板形热力图', textStyle: { fontSize: 12, fontWeight: 'normal', color: '#1a365d' }, top: 4, left: 8 },
|
||||
tooltip: {
|
||||
@@ -521,7 +580,6 @@ export default {
|
||||
emphasis: { itemStyle: { borderColor: '#333', borderWidth: 1 } }
|
||||
}]
|
||||
})
|
||||
this.chartInsts.push(c)
|
||||
},
|
||||
|
||||
async exportPdf() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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() {
|
||||
// 复制除了指定字段之外的其他字段
|
||||
|
||||
@@ -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 || '') : ''
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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。
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user