Merge remote-tracking branch 'origin/0.8.X' into 0.8.X
This commit is contained in:
@@ -163,6 +163,10 @@ export default {
|
|||||||
filters: {
|
filters: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
|
},
|
||||||
|
defaultQueryParams: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -235,6 +239,9 @@ export default {
|
|||||||
this.getList();
|
this.getList();
|
||||||
this.listRecentlySelected();
|
this.listRecentlySelected();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
defaultQueryParams(val) {
|
||||||
|
this.queryParams = { ...this.queryParams, ...val };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -151,6 +151,10 @@ export default {
|
|||||||
filters: {
|
filters: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
|
},
|
||||||
|
defaultQueryParams: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -234,6 +238,9 @@ export default {
|
|||||||
this.listRecentlySelected();
|
this.listRecentlySelected();
|
||||||
this.getList();
|
this.getList();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
defaultQueryParams(val) {
|
||||||
|
this.queryParams = { ...this.queryParams, ...val };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -197,11 +197,94 @@
|
|||||||
<el-button type="primary" size="mini" :loading="excoilLoading" @click="handleFindSearch">查找</el-button>
|
<el-button type="primary" size="mini" :loading="excoilLoading" @click="handleFindSearch">查找</el-button>
|
||||||
<el-button size="mini" @click="handleFindReset">重置</el-button>
|
<el-button size="mini" @click="handleFindReset">重置</el-button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<quality-report-dialog ref="qualityReport" />
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -216,10 +299,11 @@ import {
|
|||||||
getTimingRealtimeData,
|
getTimingRealtimeData,
|
||||||
getPresetSetupByCoilId
|
getPresetSetupByCoilId
|
||||||
} from '@/api/l2/timing'
|
} from '@/api/l2/timing'
|
||||||
import { listProcessSpecVersion } from '@/api/wms/processSpecVersion'
|
import { listProcessSpecVersion, getProcessSpecVersion } from '@/api/wms/processSpecVersion'
|
||||||
import { listProcessPlan, addProcessPlan } from '@/api/wms/processPlan'
|
import { listProcessCoilRecord, upsertProcessCoilRecord } from '@/api/wms/processCoilRecord'
|
||||||
import { listProcessPlanParam, addProcessPlanParam, updateProcessPlanParam } from '@/api/wms/processPlanParam'
|
import { listProcessSpec } from '@/api/wms/processSpec'
|
||||||
import { upsertProcessCoilRecord } from '@/api/wms/processCoilRecord'
|
import { listProcessPlan } from '@/api/wms/processPlan'
|
||||||
|
import { listProcessPlanParam } from '@/api/wms/processPlanParam'
|
||||||
import { batchAddProcessAnomaly } from '@/api/wms/processAnomaly'
|
import { batchAddProcessAnomaly } from '@/api/wms/processAnomaly'
|
||||||
|
|
||||||
// 趋势参数树结构,对应 PLTCM_PRO_SEG 列名
|
// 趋势参数树结构,对应 PLTCM_PRO_SEG 列名
|
||||||
@@ -266,10 +350,11 @@ const TREND_GROUPS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
// V_VBDA_GAUGE 厚度曲线:4 个图,列名来自 DDL
|
// V_VBDA_GAUGE 厚度曲线:4 个图,列名来自 DDL
|
||||||
|
// 厚度图纵轴显示实测/设定值百分比,速度图保持原单位
|
||||||
const GAUGE_COLS = [
|
const GAUGE_COLS = [
|
||||||
{ col: 'THICK0', title: '入口测厚仪 [mm]' },
|
{ col: 'THICK0', title: '入口测厚仪 [%]' },
|
||||||
{ col: 'THICK1', title: '1架出口厚度 [mm]' },
|
{ col: 'THICK1', title: '1架出口厚度 [%]' },
|
||||||
{ col: 'THICK4', title: '末架出口厚度 [mm]' },
|
{ col: 'THICK4', title: '末架出口厚度 [%]' },
|
||||||
{ col: 'EXIT_SPEED', title: '轧制速度 [m/min]' }
|
{ col: 'EXIT_SPEED', title: '轧制速度 [m/min]' }
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -317,8 +402,9 @@ function calcYRange(vals) {
|
|||||||
/**
|
/**
|
||||||
* 生成折线图 option。
|
* 生成折线图 option。
|
||||||
* extras: [{ name, data, color, dash }] — 上下限或参考线
|
* 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 allVals = [yData, ...extras.map(e => e.data)].flat()
|
||||||
const range = calcYRange(allVals)
|
const range = calcYRange(allVals)
|
||||||
const hasExtras = extras.length > 0
|
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,
|
? { data: [title, ...extras.map(e => e.name)], top: 4, right: 4,
|
||||||
textStyle: { fontSize: 9 }, itemWidth: 14, itemHeight: 8 }
|
textStyle: { fontSize: 9 }, itemWidth: 14, itemHeight: 8 }
|
||||||
: { show: false },
|
: { 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 },
|
grid: { top: hasExtras ? 44 : 36, bottom: 28, left: 8, right: 16, containLabel: true },
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'category', data: xData,
|
type: 'category', data: xData,
|
||||||
@@ -346,7 +437,11 @@ function makeLine(title, xData, yData, extras = []) {
|
|||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: 'value', min: range.min, max: range.max,
|
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: [
|
dataZoom: [
|
||||||
{ type: 'inside', xAxisIndex: 0, zoomOnMouseWheel: true, moveOnMouseMove: true },
|
{ type: 'inside', xAxisIndex: 0, zoomOnMouseWheel: true, moveOnMouseMove: true },
|
||||||
@@ -396,7 +491,33 @@ export default {
|
|||||||
pagination: { page: 1, pageSize: 50, total: 0 },
|
pagination: { page: 1, pageSize: 50, total: 0 },
|
||||||
topTableHeight: 'calc(40vh - 80px)',
|
topTableHeight: 'calc(40vh - 80px)',
|
||||||
chartInstances: [],
|
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() {
|
created() {
|
||||||
@@ -431,18 +552,15 @@ export default {
|
|||||||
// 快速点击防重:每次点击递增版本号,旧的 sync 任务检测到版本号变更后自动放弃
|
// 快速点击防重:每次点击递增版本号,旧的 sync 任务检测到版本号变更后自动放弃
|
||||||
const clickRev = ++this._clickRev
|
const clickRev = ++this._clickRev
|
||||||
|
|
||||||
// 记录点击前的钢卷号,用于判断是否同一行重复点击
|
|
||||||
const prevExcoilId = this.selectedRow
|
|
||||||
? (this.selectedRow.EXCOILID || this.selectedRow.excoilid || null)
|
|
||||||
: null
|
|
||||||
|
|
||||||
this.selectedRow = row
|
this.selectedRow = row
|
||||||
this.segData = null
|
this.segData = null
|
||||||
this.gaugeRows = null
|
this.gaugeRows = null
|
||||||
this.shapeRows = null
|
this.shapeRows = null
|
||||||
this.presetData = null
|
this.presetData = null
|
||||||
this.selectedTrendParam = null
|
this.selectedTrendParam = null
|
||||||
|
this.coilBinding = null
|
||||||
this.disposeAllCharts()
|
this.disposeAllCharts()
|
||||||
|
this.loadCoilBinding()
|
||||||
|
|
||||||
const encoilId = row.ENCOILID || row.encoilid
|
const encoilId = row.ENCOILID || row.encoilid
|
||||||
const excoilId = row.EXCOILID || row.excoilid
|
const excoilId = row.EXCOILID || row.excoilid
|
||||||
@@ -456,13 +574,6 @@ export default {
|
|||||||
// 如果期间又点击了其他行则放弃后续操作
|
// 如果期间又点击了其他行则放弃后续操作
|
||||||
if (this._clickRev !== clickRev) return
|
if (this._clickRev !== clickRev) return
|
||||||
|
|
||||||
// 同一钢卷重复点击:跳过规程同步,避免重复写入
|
|
||||||
const isSameCoil = excoilId && excoilId === prevExcoilId
|
|
||||||
if (!isSameCoil) {
|
|
||||||
// 后台静默同步到规程(不阻塞 UI)
|
|
||||||
this.autoSyncToActiveSpec(excoilId || encoilId, clickRev)
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.$nextTick()
|
await this.$nextTick()
|
||||||
// 加载完成后自动选中第一个趋势参数
|
// 加载完成后自动选中第一个趋势参数
|
||||||
if (this.activeTab === 'trend' && this.segData) {
|
if (this.activeTab === 'trend' && this.segData) {
|
||||||
@@ -624,6 +735,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// ── 厚度曲线 (V_VBDA_GAUGE) ──────────────────
|
// ── 厚度曲线 (V_VBDA_GAUGE) ──────────────────
|
||||||
|
// 有参考值的厚度图纵轴显示百分比:实测值 / 设定值 × 100
|
||||||
renderGaugeCharts() {
|
renderGaugeCharts() {
|
||||||
const rows = this.gaugeRows
|
const rows = this.gaugeRows
|
||||||
if (!rows || !rows.length) return
|
if (!rows || !rows.length) return
|
||||||
@@ -633,33 +745,48 @@ export default {
|
|||||||
const chartRefs = ['chartGauge1', 'chartGauge2', 'chartGauge3', 'chartGauge4']
|
const chartRefs = ['chartGauge1', 'chartGauge2', 'chartGauge3', 'chartGauge4']
|
||||||
const charts = chartRefs.map((ref, i) => {
|
const charts = chartRefs.map((ref, i) => {
|
||||||
const { col, title } = GAUGE_COLS[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]
|
const refCol = refColMap[col]
|
||||||
|
|
||||||
if (refCol) {
|
if (refCol) {
|
||||||
const refData = rows.map(r => {
|
// ── 厚度图:转为百分比(实测 / 设定 × 100) ──
|
||||||
|
const yData = rows.map(r => {
|
||||||
|
const v = getRowVal(r, col)
|
||||||
const rv = getRowVal(r, refCol)
|
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 extras = []
|
||||||
const upData = rows.map((r, j) => {
|
// 目标值恒为 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 rv = getRowVal(r, refCol)
|
||||||
const tl = getRowVal(r, 'TOPLIMIT')
|
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 rv = getRowVal(r, refCol)
|
||||||
const bl = getRowVal(r, 'BOTLIMIT')
|
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 (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 })
|
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.chartInstances = charts.filter(Boolean)
|
||||||
this.setupResize()
|
this.setupResize()
|
||||||
@@ -852,199 +979,13 @@ export default {
|
|||||||
this.gaugeRows = null
|
this.gaugeRows = null
|
||||||
this.shapeRows = null
|
this.shapeRows = null
|
||||||
this.selectedTrendParam = null
|
this.selectedTrendParam = null
|
||||||
|
this.coilBinding = null
|
||||||
this.disposeAllCharts()
|
this.disposeAllCharts()
|
||||||
this.pagination.page = 1
|
this.pagination.page = 1
|
||||||
this.loadExcoilCount()
|
this.loadExcoilCount()
|
||||||
this.loadExcoilList()
|
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) {
|
openQualityReport(row) {
|
||||||
// If the clicked row is already the selected row, use in-memory data
|
// If the clicked row is already the selected row, use in-memory data
|
||||||
const isSame = this.selectedRow &&
|
const isSame = this.selectedRow &&
|
||||||
@@ -1056,6 +997,178 @@ export default {
|
|||||||
this.$refs.qualityReport.open(row, segData, gaugeRows, shapeRows, presetData)
|
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) {
|
calcLengthPerTon(row) {
|
||||||
const len = parseFloat(row.EXIT_LENGTH || row.exit_length)
|
const len = parseFloat(row.EXIT_LENGTH || row.exit_length)
|
||||||
const wt = parseFloat(row.MEAS_EXIT_WEIGHT || row.meas_exit_weight)
|
const wt = parseFloat(row.MEAS_EXIT_WEIGHT || row.meas_exit_weight)
|
||||||
@@ -1276,4 +1389,45 @@ export default {
|
|||||||
font-size: 13px;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -10,7 +10,10 @@
|
|||||||
>
|
>
|
||||||
<div class="rpt-toolbar">
|
<div class="rpt-toolbar">
|
||||||
<span class="rpt-coil-hint">{{ coilLabel }}</span>
|
<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>
|
||||||
|
|
||||||
<div ref="reportContent" class="report-body">
|
<div ref="reportContent" class="report-body">
|
||||||
@@ -136,6 +139,11 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
|
import {
|
||||||
|
getTimingSegByEncoilId,
|
||||||
|
getTimingRealtimeData,
|
||||||
|
getPresetSetupByCoilId
|
||||||
|
} from '@/api/l2/timing'
|
||||||
|
|
||||||
function getV(row, col) {
|
function getV(row, col) {
|
||||||
if (!row) return null
|
if (!row) return null
|
||||||
@@ -202,15 +210,16 @@ export default {
|
|||||||
name: 'QualityReportDialog',
|
name: 'QualityReportDialog',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
visible: false,
|
visible: false,
|
||||||
exporting: false,
|
exporting: false,
|
||||||
row: null,
|
dataLoading: false,
|
||||||
segData: null,
|
row: null,
|
||||||
gaugeRows: null,
|
segData: null,
|
||||||
shapeRows: null,
|
gaugeRows: null,
|
||||||
presetData: null,
|
shapeRows: null,
|
||||||
nowStr: '',
|
presetData: null,
|
||||||
chartInsts: []
|
nowStr: '',
|
||||||
|
chartInsts: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -282,18 +291,68 @@ export default {
|
|||||||
fmtDate(v) { if (!v) return '—'; return String(v).replace('T', ' ').substring(0, 19) },
|
fmtDate(v) { if (!v) return '—'; return String(v).replace('T', ' ').substring(0, 19) },
|
||||||
|
|
||||||
open(row, segData, gaugeRows, shapeRows, presetData) {
|
open(row, segData, gaugeRows, shapeRows, presetData) {
|
||||||
this.row = row
|
this.row = row
|
||||||
this.segData = segData || null
|
this.segData = segData || null
|
||||||
this.gaugeRows = gaugeRows || null
|
this.gaugeRows = gaugeRows || null
|
||||||
this.shapeRows = shapeRows || null
|
this.shapeRows = shapeRows || null
|
||||||
this.presetData = presetData || null
|
this.presetData = presetData || null
|
||||||
this.visible = true
|
this.dataLoading = false
|
||||||
|
this.visible = true
|
||||||
},
|
},
|
||||||
|
|
||||||
// @opened fires AFTER the dialog's CSS transition completes — DOM is fully painted
|
// @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.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() {
|
onClose() {
|
||||||
this.disposeCharts()
|
this.disposeCharts()
|
||||||
@@ -481,7 +540,7 @@ export default {
|
|||||||
|
|
||||||
const w = el.offsetWidth || 840
|
const w = el.offsetWidth || 840
|
||||||
const c = echarts.init(el, null, { renderer: 'canvas', width: w, height: 200 })
|
const c = echarts.init(el, null, { renderer: 'canvas', width: w, height: 200 })
|
||||||
this.chartInsts.push(c)
|
this.chartInsts.push(c) // 只 push 一次
|
||||||
c.setOption({
|
c.setOption({
|
||||||
title: { text: '板形热力图', textStyle: { fontSize: 12, fontWeight: 'normal', color: '#1a365d' }, top: 4, left: 8 },
|
title: { text: '板形热力图', textStyle: { fontSize: 12, fontWeight: 'normal', color: '#1a365d' }, top: 4, left: 8 },
|
||||||
tooltip: {
|
tooltip: {
|
||||||
@@ -521,7 +580,6 @@ export default {
|
|||||||
emphasis: { itemStyle: { borderColor: '#333', borderWidth: 1 } }
|
emphasis: { itemStyle: { borderColor: '#333', borderWidth: 1 } }
|
||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
this.chartInsts.push(c)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async exportPdf() {
|
async exportPdf() {
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container" v-loading="loading">
|
<div class="app-container" v-loading="loading">
|
||||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="90px">
|
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="90px">
|
||||||
<el-form-item label="开始时间" prop="startTime">
|
<el-form-item label="时间">
|
||||||
<el-date-picker v-model="queryParams.startTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss"
|
<time-range-picker v-model="timeRangeParams" start-key="startTime" end-key="endTime"
|
||||||
placeholder="选择开始时间" />
|
:default-start-time="queryParams.startTime" :default-end-time="queryParams.endTime"
|
||||||
</el-form-item>
|
@quick-select="handleQuery" />
|
||||||
<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>
|
</el-form-item>
|
||||||
<el-form-item label="目标炉" prop="targetFurnaceId">
|
<el-form-item label="目标炉" prop="targetFurnaceId">
|
||||||
<el-select v-model="queryParams.targetFurnaceId" placeholder="请选择" clearable filterable>
|
<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 RawMaterialInfo from "@/components/KLPService/Renderer/RawMaterialInfo";
|
||||||
import CoilNo from "@/components/KLPService/Renderer/CoilNo.vue";
|
import CoilNo from "@/components/KLPService/Renderer/CoilNo.vue";
|
||||||
import WarehouseSelect from "@/components/KLPService/WarehouseSelect/index.vue";
|
import WarehouseSelect from "@/components/KLPService/WarehouseSelect/index.vue";
|
||||||
|
import TimeRangePicker from "@/views/wms/report/components/timeRangePicker.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "AnnealPerformance",
|
name: "AnnealPerformance",
|
||||||
|
components: {
|
||||||
|
ProductInfo,
|
||||||
|
RawMaterialInfo,
|
||||||
|
CoilNo,
|
||||||
|
WarehouseSelect,
|
||||||
|
TimeRangePicker,
|
||||||
|
},
|
||||||
data() {
|
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 {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
|
timeRangeParams: {
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
},
|
||||||
queryParams: {
|
queryParams: {
|
||||||
startTime: undefined,
|
startTime,
|
||||||
endTime: undefined,
|
endTime,
|
||||||
targetFurnaceId: undefined,
|
targetFurnaceId: undefined,
|
||||||
planNo: undefined,
|
planNo: undefined,
|
||||||
enterCoilNo: undefined,
|
enterCoilNo: undefined,
|
||||||
@@ -92,11 +109,15 @@ export default {
|
|||||||
furnaceOptions: [],
|
furnaceOptions: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
components: {
|
watch: {
|
||||||
ProductInfo,
|
timeRangeParams: {
|
||||||
RawMaterialInfo,
|
handler(newVal) {
|
||||||
CoilNo,
|
this.queryParams.startTime = newVal.startTime
|
||||||
WarehouseSelect,
|
this.queryParams.endTime = newVal.endTime
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.loadFurnaces();
|
this.loadFurnaces();
|
||||||
|
|||||||
@@ -1,16 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="typing-coil-container">
|
<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>
|
<div>
|
||||||
<CoilInfoRender title="原料信息" :coilInfo="currentInfo" border>
|
<CoilInfoRender title="原料信息" :coilInfo="currentInfo" border>
|
||||||
<template slot="extra">
|
<template slot="extra">
|
||||||
@@ -65,8 +54,6 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<el-form-item label="材料类型" prop="materialType">
|
<el-form-item label="材料类型" prop="materialType">
|
||||||
@@ -80,9 +67,11 @@
|
|||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<el-form-item :label="getItemLabel" prop="itemId" :rules="rules.itemId">
|
<el-form-item :label="getItemLabel" prop="itemId" :rules="rules.itemId">
|
||||||
<RawMaterialSelect v-if="updateForm.materialType === '原料'" v-model="updateForm.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"
|
<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>
|
<div v-else>请先选择物料类型</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -371,8 +360,8 @@ export default {
|
|||||||
updateForm: {
|
updateForm: {
|
||||||
currentCoilNo: '',
|
currentCoilNo: '',
|
||||||
team: '',
|
team: '',
|
||||||
materialType: null,
|
materialType: '成品',
|
||||||
itemType: null,
|
itemType: 'product',
|
||||||
itemId: null,
|
itemId: null,
|
||||||
grossWeight: undefined,
|
grossWeight: undefined,
|
||||||
netWeight: undefined,
|
netWeight: undefined,
|
||||||
@@ -460,6 +449,7 @@ export default {
|
|||||||
cacheDialogVisible: false,
|
cacheDialogVisible: false,
|
||||||
currentCache: null,
|
currentCache: null,
|
||||||
parsedCacheData: null,
|
parsedCacheData: null,
|
||||||
|
itemSelectorQueryParams: {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -567,6 +557,14 @@ export default {
|
|||||||
}
|
}
|
||||||
if (data.exit_thick != null) this.$set(this.updateForm, 'actualThickness', parseFloat(data.exit_thick))
|
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))
|
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 !== '') {
|
if (data.park_type != null && data.park_type !== '') {
|
||||||
this.$set(this.updateForm, 'packagingRequirement', data.park_type)
|
this.$set(this.updateForm, 'packagingRequirement', data.park_type)
|
||||||
@@ -633,7 +631,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
// 复制当前信息到更新表单
|
// 复制当前信息到更新表单
|
||||||
copyFromCurrent() {
|
copyFromCurrent() {
|
||||||
// 复制除了指定字段之外的其他字段
|
// 复制除了指定字段之外的其他字段
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ import {
|
|||||||
delProcessSpecVersion,
|
delProcessSpecVersion,
|
||||||
activateProcessSpecVersion
|
activateProcessSpecVersion
|
||||||
} from '@/api/wms/processSpecVersion'
|
} from '@/api/wms/processSpecVersion'
|
||||||
|
import { listDrRecipe, listDrRecipeVersions } from '@/api/wms/drMill'
|
||||||
|
|
||||||
const STATUS_OPTIONS = [
|
const STATUS_OPTIONS = [
|
||||||
{ value: 'DRAFT', label: '草稿' },
|
{ value: 'DRAFT', label: '草稿' },
|
||||||
@@ -179,7 +180,7 @@ export default {
|
|||||||
versionList: [],
|
versionList: [],
|
||||||
versionLoading: false,
|
versionLoading: false,
|
||||||
total: 0,
|
total: 0,
|
||||||
queryParams: { pageNum: 1, pageSize: 10, specType: '', lineId: '' },
|
queryParams: { pageNum: 1, pageSize: 20, specType: '', lineId: '' },
|
||||||
|
|
||||||
specOpen: false,
|
specOpen: false,
|
||||||
specTitle: '',
|
specTitle: '',
|
||||||
@@ -237,18 +238,43 @@ export default {
|
|||||||
},
|
},
|
||||||
onSpecRowClick(row) { this.selectSpec(row) },
|
onSpecRowClick(row) { this.selectSpec(row) },
|
||||||
|
|
||||||
loadVersions() {
|
async loadVersions() {
|
||||||
if (!this.currentSpecId) return
|
if (!this.currentSpecId) return
|
||||||
this.versionLoading = true
|
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 || []
|
this.versionList = res.rows || []
|
||||||
}).catch(e => console.error(e)).finally(() => { this.versionLoading = false })
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
this.versionLoading = false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
goPlanSpec(row) {
|
goPlanSpec(row) {
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
path: `/process/processSpec/planSpec`,
|
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>
|
||||||
</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="main-layout">
|
||||||
<!-- 左侧段分组 -->
|
<!-- 左侧段分组 -->
|
||||||
<div class="left-tree">
|
<div class="left-tree">
|
||||||
@@ -95,9 +85,19 @@
|
|||||||
>模板导入</el-button>
|
>模板导入</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- DR 道次参数同步提示 -->
|
||||||
|
<el-alert
|
||||||
|
v-if="drSyncLoading"
|
||||||
|
title="正在从双机架版本同步道次参数,请稍候…"
|
||||||
|
type="info"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
style="margin-bottom:10px"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 参数平铺表 -->
|
<!-- 参数平铺表 -->
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="planLoading || allParamLoading"
|
v-loading="planLoading || allParamLoading || drSyncLoading"
|
||||||
:data="filteredFlatRows"
|
:data="filteredFlatRows"
|
||||||
size="small"
|
size="small"
|
||||||
border
|
border
|
||||||
@@ -113,17 +113,6 @@
|
|||||||
<el-table-column label="设定值" prop="targetValue" align="right" width="82" />
|
<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="lowerLimit" align="right" width="72" />
|
||||||
<el-table-column label="上限" prop="upperLimit" 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">
|
<el-table-column label="操作" align="right" width="100" fixed="right">
|
||||||
<template slot-scope="{ row }">
|
<template slot-scope="{ row }">
|
||||||
<el-button type="text" size="mini" @click="editParamFromFlat(row)">编辑</el-button>
|
<el-button type="text" size="mini" @click="editParamFromFlat(row)">编辑</el-button>
|
||||||
@@ -134,98 +123,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- 方案点位 dialog -->
|
||||||
<el-dialog :title="planTitle" :visible.sync="planOpen" width="520px" append-to-body @close="planForm = {}">
|
<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">
|
<el-form ref="planFormRef" :model="planForm" :rules="planRules" label-width="90px" size="small">
|
||||||
@@ -392,11 +289,9 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import * as XLSX from 'xlsx'
|
import * as XLSX from 'xlsx'
|
||||||
import * as echarts from 'echarts'
|
|
||||||
import { listProcessPlan, addProcessPlan, updateProcessPlan, delProcessPlan } from '@/api/wms/processPlan'
|
import { listProcessPlan, addProcessPlan, updateProcessPlan, delProcessPlan } from '@/api/wms/processPlan'
|
||||||
import { listProcessPlanParam, addProcessPlanParam, updateProcessPlanParam, delProcessPlanParam } from '@/api/wms/processPlanParam'
|
import { listProcessPlanParam, addProcessPlanParam, updateProcessPlanParam, delProcessPlanParam } from '@/api/wms/processPlanParam'
|
||||||
import { listAllProcessAnomaly } from '@/api/wms/processAnomaly'
|
import { listDrRecipe, listDrRecipeVersions, getDrRecipeVersionDetail } from '@/api/wms/drMill'
|
||||||
import { listProcessCoilRecord } from '@/api/wms/processCoilRecord'
|
|
||||||
|
|
||||||
/** 表单内可选段类型(新建/编辑仍支持全部枚举) */
|
/** 表单内可选段类型(新建/编辑仍支持全部枚举) */
|
||||||
const SEGMENT_FORM_OPTIONS = [
|
const SEGMENT_FORM_OPTIONS = [
|
||||||
@@ -428,6 +323,8 @@ export default {
|
|||||||
versionId: undefined,
|
versionId: undefined,
|
||||||
versionCode: '',
|
versionCode: '',
|
||||||
specId: undefined,
|
specId: undefined,
|
||||||
|
specCode: '', // 规程编码,DR 规程以 "DR-" 开头
|
||||||
|
drSyncLoading: false, // DR 道次参数同步中
|
||||||
configMode: 'configurable',
|
configMode: 'configurable',
|
||||||
/** 左侧:段类型;空=全部 */
|
/** 左侧:段类型;空=全部 */
|
||||||
activeSegmentType: '',
|
activeSegmentType: '',
|
||||||
@@ -457,19 +354,6 @@ export default {
|
|||||||
paramCode: [{ required: true, message: '参数编码不能为空', trigger: 'blur' }],
|
paramCode: [{ required: true, message: '参数编码不能为空', trigger: 'blur' }],
|
||||||
paramName: [{ 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,
|
importOpen: false,
|
||||||
file: null,
|
file: null,
|
||||||
@@ -487,6 +371,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
/** 当前规程是否为双机架规程(specCode 以 DR- 开头) */
|
||||||
|
isDrSpec() {
|
||||||
|
return typeof this.specCode === 'string' && this.specCode.startsWith('DR-')
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* 左侧 Tab:仅从点位数据汇总「段类型」,展示用语义名称(入口段/工艺段/出口段)
|
* 左侧 Tab:仅从点位数据汇总「段类型」,展示用语义名称(入口段/工艺段/出口段)
|
||||||
*/
|
*/
|
||||||
@@ -555,16 +443,6 @@ export default {
|
|||||||
extra.sort((a, b) => String(a.label).localeCompare(String(b.label), 'zh-CN'))
|
extra.sort((a, b) => String(a.label).localeCompare(String(b.label), 'zh-CN'))
|
||||||
return [...SEGMENT_FORM_OPTIONS, ...extra]
|
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 的段/点位信息 */
|
/** 所有 plan 的参数平铺成一行,带 plan 的段/点位信息 */
|
||||||
flatRows() {
|
flatRows() {
|
||||||
const SEG_LABELS = { INLET: '入口段', PROCESS: '工艺段', OUTLET: '出口段' }
|
const SEG_LABELS = { INLET: '入口段', PROCESS: '工艺段', OUTLET: '出口段' }
|
||||||
@@ -629,124 +507,16 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
syncFromRoute() {
|
syncFromRoute() {
|
||||||
const q = this.$route.query
|
const q = this.$route.query
|
||||||
this.versionId = q.versionId || undefined
|
this.versionId = q.versionId || undefined
|
||||||
this.versionCode = q.versionCode || ''
|
this.versionCode = q.versionCode || ''
|
||||||
this.specId = q.specId || undefined
|
this.specId = q.specId || undefined
|
||||||
|
this.specCode = q.specCode || ''
|
||||||
|
this._drSyncAttempted = false // 路由切换时重置,避免重复同步
|
||||||
if (this.versionId) {
|
if (this.versionId) {
|
||||||
this.loadPlans()
|
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) },
|
goBack() { this.$router.go(-1) },
|
||||||
jumpToAnomaly(coilId) {
|
|
||||||
this.coilAnomalyCoilId = coilId
|
|
||||||
this.coilAnomalyDialog = true
|
|
||||||
},
|
|
||||||
selectSegmentType(val) {
|
selectSegmentType(val) {
|
||||||
this.activeSegmentType = val === undefined || val === null ? '' : val
|
this.activeSegmentType = val === undefined || val === null ? '' : val
|
||||||
this.activeSegmentName = ''
|
this.activeSegmentName = ''
|
||||||
@@ -768,9 +538,100 @@ export default {
|
|||||||
this.planLoading = true
|
this.planLoading = true
|
||||||
listProcessPlan({ versionId: this.versionId, pageNum: 1, pageSize: 500 }).then(res => {
|
listProcessPlan({ versionId: this.versionId, pageNum: 1, pageSize: 500 }).then(res => {
|
||||||
this.planList = res.rows || []
|
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 })
|
}).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() {
|
async loadAllParams() {
|
||||||
if (!this.planList.length) { this.allParamList = []; return }
|
if (!this.planList.length) { this.allParamList = []; return }
|
||||||
this.allParamLoading = true
|
this.allParamLoading = true
|
||||||
@@ -1486,27 +1347,6 @@ export default {
|
|||||||
.seg-process { background: #f0f9eb; color: #3a7a2a; border: 1px solid #b3e19d; }
|
.seg-process { background: #f0f9eb; color: #3a7a2a; border: 1px solid #b3e19d; }
|
||||||
.seg-outlet { background: #fdf6ec; color: #a86a00; border: 1px solid #f5dab1; }
|
.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 {
|
.import-container {
|
||||||
@@ -1564,34 +1404,4 @@ export default {
|
|||||||
margin-bottom: 20px;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -155,52 +155,14 @@ export default {
|
|||||||
},
|
},
|
||||||
dicts: ['product_coil_status', 'coil_material', 'coil_itemname', 'coil_manufacturer', 'coil_quality_status'],
|
dicts: ['product_coil_status', 'coil_material', 'coil_itemname', 'coil_manufacturer', 'coil_quality_status'],
|
||||||
data() {
|
data() {
|
||||||
// 工具函数:个位数补零
|
|
||||||
const addZero = (num) => num.toString().padStart(2, '0')
|
const addZero = (num) => num.toString().padStart(2, '0')
|
||||||
|
|
||||||
// 获取当前日期(默认选中当天)
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const currentDate = `${now.getFullYear()}-${addZero(now.getMonth() + 1)}`
|
const yesterday = new Date(now)
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1)
|
||||||
|
|
||||||
/**
|
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`
|
||||||
* @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)
|
|
||||||
return {
|
return {
|
||||||
lossList: [],
|
lossList: [],
|
||||||
outList: [],
|
outList: [],
|
||||||
|
|||||||
@@ -61,7 +61,13 @@ public class DrMillProcessRecipeController extends BaseController {
|
|||||||
|
|
||||||
@GetMapping("/version/list/{recipeId}")
|
@GetMapping("/version/list/{recipeId}")
|
||||||
public R<List<DrMillProcessRecipeVersion>> versionList(@PathVariable Long 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}")
|
@GetMapping("/version/{versionId}")
|
||||||
@@ -71,7 +77,14 @@ public class DrMillProcessRecipeController extends BaseController {
|
|||||||
|
|
||||||
@PostMapping("/version")
|
@PostMapping("/version")
|
||||||
public R<Long> addVersion(@RequestBody DrMillProcessRecipeVersion 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")
|
@PutMapping("/version")
|
||||||
@@ -83,6 +96,15 @@ public class DrMillProcessRecipeController extends BaseController {
|
|||||||
@PutMapping("/version/activate/{versionId}")
|
@PutMapping("/version/activate/{versionId}")
|
||||||
public R<Void> activate(@PathVariable Long versionId) {
|
public R<Void> activate(@PathVariable Long versionId) {
|
||||||
versionService.activate(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();
|
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.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
import com.klp.domain.DrMillProcessRecipe;
|
import com.klp.domain.DrMillProcessRecipe;
|
||||||
|
import com.klp.domain.DrMillProcessRecipeVersion;
|
||||||
import com.klp.domain.WmsProcessSpec;
|
import com.klp.domain.WmsProcessSpec;
|
||||||
|
import com.klp.domain.WmsProcessSpecVersion;
|
||||||
import com.klp.domain.WmsProductionLine;
|
import com.klp.domain.WmsProductionLine;
|
||||||
import com.klp.mapper.WmsProcessSpecMapper;
|
import com.klp.mapper.WmsProcessSpecMapper;
|
||||||
|
import com.klp.mapper.WmsProcessSpecVersionMapper;
|
||||||
import com.klp.mapper.WmsProductionLineMapper;
|
import com.klp.mapper.WmsProductionLineMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -35,8 +38,9 @@ public class DrRecipeSyncService {
|
|||||||
/** spec_code 前缀,区分同名方案号属于哪条产线 */
|
/** spec_code 前缀,区分同名方案号属于哪条产线 */
|
||||||
private static final String SPEC_CODE_PREFIX = "DR-";
|
private static final String SPEC_CODE_PREFIX = "DR-";
|
||||||
|
|
||||||
private final WmsProcessSpecMapper specMapper;
|
private final WmsProcessSpecMapper specMapper;
|
||||||
private final WmsProductionLineMapper lineMapper;
|
private final WmsProcessSpecVersionMapper specVersionMapper;
|
||||||
|
private final WmsProductionLineMapper lineMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查并补充 wms_process_spec:
|
* 检查并补充 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。
|
* 查找或创建双机架产线记录,返回 line_id。
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user