修复酸轧实绩提交问题,规程重新完成逻辑

This commit is contained in:
2026-05-21 13:41:21 +08:00
parent 2c9cc6241f
commit eb5601ade3
6 changed files with 677 additions and 557 deletions

View File

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

View File

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