工艺规程增强

This commit is contained in:
2026-05-12 17:15:29 +08:00
parent b44d9d9daf
commit 38138a828c
27 changed files with 1903 additions and 259 deletions

View File

@@ -161,7 +161,7 @@ redisson:
# 客户端名称
clientName: ${klp.name}
# 最小空闲连接数
connectionMinimumIdleSize: 32
connectionMinimumIdleSize: 4
# 连接池大小
connectionPoolSize: 64
# 连接空闲超时,单位:毫秒

View File

@@ -0,0 +1,13 @@
import request from '@/utils/request'
export function listProcessAnomaly(query) {
return request({ url: '/wms/processAnomaly/list', method: 'get', params: query })
}
export function listAllProcessAnomaly(query) {
return request({ url: '/wms/processAnomaly/listAll', method: 'get', params: query })
}
export function batchAddProcessAnomaly(data) {
return request({ url: '/wms/processAnomaly/batchAdd', method: 'post', data })
}

View File

@@ -0,0 +1,17 @@
import request from '@/utils/request'
export function listProcessCoilRecord(query) {
return request({ url: '/wms/processCoilRecord/list', method: 'get', params: query })
}
export function listAllProcessCoilRecord(query) {
return request({ url: '/wms/processCoilRecord/listAll', method: 'get', params: query })
}
export function countProcessCoilRecord(versionId) {
return request({ url: '/wms/processCoilRecord/count', method: 'get', params: { versionId } })
}
export function upsertProcessCoilRecord(data) {
return request({ url: '/wms/processCoilRecord/upsert', method: 'post', data })
}

View File

@@ -137,7 +137,7 @@
</div>
</el-tab-pane>
<!-- 带钢板形 热力图 -->
<!-- 带钢板形 -->
<el-tab-pane label="带钢板形" name="flatness3d">
<div v-if="!selectedRow" class="no-data-hint">请在上方选择钢卷</div>
<div v-else-if="realtimeLoading" class="no-data-hint">加载中</div>
@@ -203,8 +203,14 @@ import {
getExcoilList,
getExcoilCount,
getTimingSegByEncoilId,
getTimingRealtimeData
getTimingRealtimeData,
getPresetSetupByCoilId
} from '@/api/l2/timing'
import { listProcessSpecVersion } from '@/api/wms/processSpecVersion'
import { listProcessPlan, addProcessPlan } from '@/api/wms/processPlan'
import { listProcessPlanParam, addProcessPlanParam, updateProcessPlanParam } from '@/api/wms/processPlanParam'
import { upsertProcessCoilRecord } from '@/api/wms/processCoilRecord'
import { batchAddProcessAnomaly } from '@/api/wms/processAnomaly'
// 趋势参数树结构,对应 PLTCM_PRO_SEG 列名
const TREND_GROUPS = [
@@ -265,10 +271,26 @@ const SHAPE_SCALAR_COLS = [
{ col: 'IRBEND', title: '中间辊弯辊力 [kN]' }
]
/**
* 从数据数组计算合理的 Y 轴范围(带 15% 上下边距)。
* echarts 默认 scale 在数据变化幅度极小时会把轴拉到很大范围,导致曲线看起来是横线。
*/
// 参数单位映射
const TREND_UNIT_MAP = {
PORTENS: 'N/mm²', ENLTENS: 'N/mm²', TLTENS: 'N/mm²', PLTENS: 'N/mm²',
CXLTENS: 'N/mm²', TRIMTENS: 'N/mm²', TRTENS: 'N/mm²', TELTENS: 'N/mm²',
PORSPEED: 'm/min', PLSPEED: 'm/min', TRIMSPEED: 'm/min',
MILLENTRYSPEED: 'm/min', MILLEXITSPEED: 'm/min',
TLMESH1: 'mm', TLMESH2: 'mm', TLMESH3: 'mm',
TLELONG: '%',
TK1TEMP: '℃', TK2TEMP: '℃', TK3TEMP: '℃', RINSETEMP: '℃'
}
// PLTCM_PRO_SEG 列 → PLTCM_PRESET_SETUP 设定值列
const TREND_PRESET_MAP = {
PORTENS: 'POR_TEN', ENLTENS: 'CEL_TEN', TLTENS: 'TLV_TEN',
PLTENS: 'CPL_TEN', CXLTENS: 'CXL_TEN', TRIMTENS: 'TRIM_TEN',
TRTENS: 'TR_TEN', TELTENS: 'TEL_TEN',
TLMESH1: 'TLV_MESH_1', TLMESH2: 'TLV_MESH_2', TLMESH3: 'TLV_MESH_3',
TLELONG: 'TLV_ELONG', PLSPEED: 'CPL_MAX_SPEED'
}
function calcYRange(vals) {
const nums = vals.filter(v => v != null && isFinite(Number(v))).map(Number)
if (!nums.length) return {}
@@ -279,37 +301,48 @@ function calcYRange(vals) {
return { min: parseFloat((min - base * 0.2).toFixed(4)), max: parseFloat((max + base * 0.2).toFixed(4)) }
}
const pad = (max - min) * 0.15
return {
min: parseFloat((min - pad).toFixed(4)),
max: parseFloat((max + pad).toFixed(4))
}
return { min: parseFloat((min - pad).toFixed(4)), max: parseFloat((max + pad).toFixed(4)) }
}
function makeLine(title, xData, yData) {
const range = calcYRange(yData)
/**
* 生成折线图 option。
* extras: [{ name, data, color, dash }] — 上下限或参考线
*/
function makeLine(title, xData, yData, extras = []) {
const allVals = [yData, ...extras.map(e => e.data)].flat()
const range = calcYRange(allVals)
const hasExtras = extras.length > 0
const mainSeries = {
name: title, type: 'line', smooth: false, symbol: 'none',
lineStyle: { width: 1.5, color: '#409EFF' }, data: yData, z: 3
}
const extraSeries = extras.map(e => ({
name: e.name,
type: 'line', smooth: false, symbol: 'none',
lineStyle: { width: 1, color: e.color || '#E6A23C', type: e.dash !== false ? 'dashed' : 'solid' },
data: e.data, z: 2
}))
return {
title: { text: title, textStyle: { fontSize: 12, fontWeight: 'normal' }, top: 4, left: 8 },
legend: hasExtras
? { data: [title, ...extras.map(e => e.name)], top: 4, right: 4,
textStyle: { fontSize: 9 }, itemWidth: 14, itemHeight: 8 }
: { show: false },
tooltip: { trigger: 'axis' },
grid: { top: 36, bottom: 28, left: 8, right: 16, containLabel: true },
grid: { top: hasExtras ? 44 : 36, bottom: 28, left: 8, right: 16, containLabel: true },
xAxis: {
type: 'category', data: xData,
name: 'pos(m)', nameTextStyle: { fontSize: 10 }, axisLabel: { fontSize: 10 }
},
yAxis: {
type: 'value',
min: range.min,
max: range.max,
nameTextStyle: { fontSize: 10 },
axisLabel: { fontSize: 10 }
type: 'value', min: range.min, max: range.max,
nameTextStyle: { fontSize: 10 }, axisLabel: { fontSize: 10 }
},
dataZoom: [
{ type: 'inside', xAxisIndex: 0, zoomOnMouseWheel: true, moveOnMouseMove: true },
{ type: 'inside', yAxisIndex: 0, zoomOnMouseWheel: false, moveOnMouseWheel: true }
],
series: [{
name: title, type: 'line', smooth: false, symbol: 'none',
lineStyle: { width: 1 }, data: yData
}]
series: [mainSeries, ...extraSeries]
}
}
@@ -343,6 +376,7 @@ export default {
expandedGroups: { '张力': true, '速度': true, '拉矫机': true, '酸洗段': true },
selectedTrendParam: null,
trendChartInst: null,
presetData: null,
// 查找
searchType: 'coil',
searchCoilId: '',
@@ -355,6 +389,7 @@ export default {
}
},
created() {
this._clickRev = 0
this.loadExcoilCount()
this.loadExcoilList()
},
@@ -382,23 +417,31 @@ export default {
this.loadExcoilList()
},
async handleRowClick(row) {
// 快速点击防重:每次点击递增版本号,旧的 sync 任务检测到版本号变更后自动放弃
const clickRev = ++this._clickRev
this.selectedRow = row
this.segData = null
this.gaugeRows = null
this.shapeRows = null
this.presetData = null
this.selectedTrendParam = null
this.disposeAllCharts()
// PLTCM_PRO_SEG 用 ENCOILID 查(主键前缀)
const encoilId = row.ENCOILID || row.encoilid
// V_VBDA_GAUGE / V_VBDA_SHAPE 用 EXCOILID 作为 MATID
const excoilId = row.EXCOILID || row.excoilid
await Promise.all([
encoilId ? this.loadSeg(encoilId) : Promise.resolve(),
encoilId ? this.loadPreset(encoilId) : Promise.resolve(),
excoilId ? this.loadRealtime(excoilId) : Promise.resolve()
])
// 如果期间又点击了其他行则放弃后续操作
if (this._clickRev !== clickRev) return
// 后台静默同步到规程(不阻塞 UI
this.autoSyncToActiveSpec(excoilId || encoilId, clickRev)
await this.$nextTick()
// 加载完成后自动选中第一个趋势参数
if (this.activeTab === 'trend' && this.segData) {
@@ -434,6 +477,15 @@ export default {
}
},
async loadPreset(coilId) {
try {
const res = await getPresetSetupByCoilId(coilId)
this.presetData = res?.data?.data || null
} catch (_) {
this.presetData = null
}
},
// ── 趋势参数树 ──────────────────────────────
toggleGroup(label) {
this.$set(this.expandedGroups, label, !this.expandedGroups[label])
@@ -457,9 +509,31 @@ export default {
window.addEventListener('resize', resizeFn)
this._trendResizeFn = resizeFn
}
const col = this.selectedTrendParam.col
const x = this.segX()
const yData = this.seg(this.selectedTrendParam.col)
this.trendChartInst.setOption(makeLine(this.selectedTrendParam.label, x, yData), true)
const yData = this.seg(col)
// 实测最大值 / 最小值:来自 PLTCM_PRO_SEG 的段内统计 MAX / MIN
const maxData = this.seg(col + 'MAX')
const minData = this.seg(col + 'MIN')
const extras = []
if (maxData.some(v => v != null)) {
extras.push({ name: '最大值', data: maxData, color: '#F56C6C', dash: true })
}
if (minData.some(v => v != null)) {
extras.push({ name: '最小值', data: minData, color: '#67C23A', dash: true })
}
// 设定值:来自 PLTCM_PRESET_SETUP若有映射
const presetCol = TREND_PRESET_MAP[col]
if (presetCol && this.presetData) {
const setVal = this.presetData[presetCol] !== undefined
? this.presetData[presetCol]
: this.presetData[presetCol.toLowerCase()]
if (setVal != null && Number(setVal) !== 0) {
const sv = Number(Number(setVal).toFixed(3))
extras.push({ name: '设定值', data: new Array(x.length).fill(sv), color: '#E6A23C', dash: true })
}
}
this.trendChartInst.setOption(makeLine(this.selectedTrendParam.label, x, yData, extras), true)
},
// ── Tab 切换 ────────────────────────────────
@@ -533,14 +607,38 @@ export default {
const rows = this.gaugeRows
if (!rows || !rows.length) return
const xData = xLocData(rows)
const refs = ['chartGauge1', 'chartGauge2', 'chartGauge3', 'chartGauge4']
const charts = refs.map((ref, i) => {
// THICK0~THICK4 对应的参考列
const refColMap = { THICK0: 'THICK0REF', THICK1: 'THICK1REF', THICK4: 'THICK4REF', THICK5: 'THICK5REF' }
const chartRefs = ['chartGauge1', 'chartGauge2', 'chartGauge3', 'chartGauge4']
const charts = chartRefs.map((ref, i) => {
const { col, title } = GAUGE_COLS[i]
const yData = rows.map(r => {
const v = getRowVal(r, col)
return v == null ? null : parseFloat(v.toFixed(4))
})
return this.makeChart(ref, makeLine(title, xData, yData))
const extras = []
const refCol = refColMap[col]
if (refCol) {
const refData = rows.map(r => {
const rv = getRowVal(r, refCol)
return rv == null ? null : parseFloat(rv.toFixed(4))
})
// 上限 = REF + TOPLIMIT下限 = REF + BOTLIMITTOPLIMIT/BOTLIMIT 单位与测厚仪一致)
const upData = rows.map((r, j) => {
const rv = getRowVal(r, refCol)
const tl = getRowVal(r, 'TOPLIMIT')
return rv == null ? null : parseFloat((rv + (tl ?? 3)).toFixed(4))
})
const loData = rows.map((r, j) => {
const rv = getRowVal(r, refCol)
const bl = getRowVal(r, 'BOTLIMIT')
return rv == null ? null : parseFloat((rv + (bl ?? -3)).toFixed(4))
})
if (refData.some(v => v != null)) extras.push({ name: '目标值', data: refData, color: '#909399', dash: false })
if (upData.some(v => v != null)) extras.push({ name: '上限', data: upData, color: '#F56C6C', dash: true })
if (loData.some(v => v != null)) extras.push({ name: '下限', data: loData, color: '#67C23A', dash: true })
}
return this.makeChart(ref, makeLine(title, xData, yData, extras))
})
this.chartInstances = charts.filter(Boolean)
this.setupResize()
@@ -739,6 +837,181 @@ export default {
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 {
const r = await addProcessPlan({
versionId,
segmentType: 'PROCESS',
segmentName: item.groupLabel,
pointName: item.pointName,
pointCode: item.pointCode,
sortOrder: 0
})
if (guard()) return
planId = r.data
}
// 查已存储参数
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
},
calcLengthPerTon(row) {
const len = parseFloat(row.EXIT_LENGTH || row.exit_length)
const wt = parseFloat(row.MEAS_EXIT_WEIGHT || row.meas_exit_weight)
@@ -748,7 +1021,8 @@ export default {
formatDate(v) {
if (!v) return '—'
return String(v).replace('T', ' ').substring(0, 19)
}
},
}
}
</script>
@@ -957,4 +1231,5 @@ export default {
color: #c0c4cc;
font-size: 13px;
}
</style>

View File

@@ -1,75 +1,127 @@
<template>
<div class="spec-version-page" v-loading="pageLoading">
<!-- 头部 -->
<div class="page-header">
<span class="page-title">规程版本管理</span>
<el-button type="primary" size="small" icon="el-icon-plus" style="margin-left:auto"
@click="openSpecDialog()">新建规程</el-button>
<div class="spec-page" v-loading="pageLoading">
<!-- 主体左规程列表 + 右版本面板 -->
<div class="master-detail">
<!-- 规程列表 -->
<div class="master-panel">
<!-- 筛选区 -->
<div class="filter-area">
<div class="filter-row">
<span class="filter-label">规程类型</span>
<dict-select
class="filter-select"
renderType="radio"
v-model="queryParams.specType"
dict-type="wms_process_spec_type"
:kisv="false"
:editable="true"
@change="loadSpecs"
/>
</div>
<div>
<el-form>
<el-form-item label="规程类型">
<dict-select @change="loadSpecs" renderType="radio" v-model="queryParams.specType"
dict-type="wms_process_spec_type" :kisv="false" :editable="true"></dict-select>
</el-form-item>
<el-form-item label="产线">
<dict-select @change="loadSpecs" renderType="radio" v-model="queryParams.lineId"
dict-type="wms_process_spec_line" :kisv="false" :editable="true"></dict-select>
</el-form-item>
</el-form>
<div class="filter-row">
<span class="filter-label">产线</span>
<dict-select
class="filter-select"
renderType="radio"
v-model="queryParams.lineId"
dict-type="wms_process_spec_line"
:kisv="false"
:editable="true"
@change="loadSpecs"
/>
</div>
<!-- 规程列表 -->
<div class="section-wrapper">
<div class="section-title">规程列表</div>
<el-table :data="specList" size="small" highlight-current-row @row-click="onSpecRowClick"
:row-class-name="tableRowClassName">
<el-table-column label="规程编码" prop="specCode" width="150" />
<el-table-column label="规程名称" prop="specName" />
<el-table-column label="创建时间" prop="createTime" width="180" />
<el-table-column label="操作" align="right" width="180">
</div>
<!-- 列表 -->
<div class="panel-hd">
<span class="panel-title">规程列表</span>
<span class="total-badge">{{ total }} </span>
<el-button type="primary" size="mini" icon="el-icon-plus" style="margin-left:auto" @click="openSpecDialog()">新建</el-button>
</div>
<el-table
:data="specList"
size="small"
highlight-current-row
@row-click="onSpecRowClick"
:row-class-name="tableRowClassName"
style="width:100%"
>
<el-table-column label="编码" prop="specCode" width="110" show-overflow-tooltip />
<el-table-column label="名称" prop="specName" show-overflow-tooltip />
<el-table-column label="创建时间" prop="createTime" width="100" show-overflow-tooltip>
<template slot-scope="{ row }">{{ (row.createTime || '').substring(0, 10) }}</template>
</el-table-column>
<el-table-column label="" align="right" width="80">
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click.stop="openSpecDialog(row)">编辑</el-button>
<el-button type="text" size="mini" class="btn-danger" @click.stop="removeSpec(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
@pagination="loadSpecs" />
</div>
<!-- 版本列表 -->
<div class="section-wrapper" v-if="currentSpec" v-loading="versionLoading">
<div class="section-title">
版本列表 - {{ currentSpec.specName }}
<!-- 版本面板 -->
<div class="detail-panel">
<template v-if="currentSpec">
<div class="panel-hd" v-loading="versionLoading">
<div class="detail-title-row">
<span class="panel-title">{{ currentSpec.specName }}</span>
<span class="spec-code-tag">{{ currentSpec.specCode }}</span>
</div>
<el-button type="primary" size="mini" icon="el-icon-plus" @click="openVersionDialog()">新建版本</el-button>
</div>
<el-table :data="versionList" size="small" highlight-current-row @row-click="onVersionRowClick">
<el-table-column label="版本号" prop="versionCode" />
<el-table-column label="状态" prop="status" />
<el-table-column label="创建时间" prop="createTime" />
<el-table-column label="生效" align="center">
<template slot-scope="{ row }">
<el-switch :value="row.isActive === 1" active-color="#5F7BA0" @click.native.stop
@change="handleActiveChange(row, $event)" />
</template>
</el-table-column>
<el-table-column label="操作" align="right">
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click.stop="openVersionDialog(row)">编辑</el-button>
<el-button type="text" size="mini" class="btn-danger" @click.stop="removeVersion(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!versionList.length && !versionLoading" description="暂无版本,请新建" style="padding:40px 0" />
<div v-if="!versionLoading && !versionList.length" class="empty-versions">
<i class="el-icon-document" style="font-size:32px;color:#dcdfe6;margin-bottom:8px;display:block" />
<span>暂无版本点击新建版本开始</span>
</div>
<div class="version-list" v-loading="versionLoading">
<div
v-for="v in versionList"
:key="v.versionId"
class="version-card"
@click="goPlanSpec(v)"
>
<div class="vc-left">
<div class="vc-top">
<span class="vc-code">{{ v.versionCode }}</span>
<el-tag :type="statusType(v.status)" size="mini" effect="plain" class="vc-status">
{{ statusLabel(v.status) }}
</el-tag>
<el-tag v-if="v.isActive === 1" type="success" size="mini" effect="dark" class="vc-active">当前生效</el-tag>
</div>
<div class="vc-meta">
创建于 {{ (v.createTime || '').substring(0, 16) || '—' }}
<span v-if="v.updateTime && v.updateTime !== v.createTime" style="margin-left:8px">
· 更新于 {{ (v.updateTime || '').substring(0, 16) }}
</span>
</div>
</div>
<div class="vc-right" @click.stop>
<el-switch
:value="v.isActive === 1"
active-color="#5F7BA0"
@change="handleActiveChange(v, $event)"
/>
<el-button type="text" size="mini" @click="openVersionDialog(v)">编辑</el-button>
<el-button type="text" size="mini" class="btn-danger" @click="removeVersion(v)">删除</el-button>
<el-button type="text" size="mini" class="btn-view" @click="goPlanSpec(v)">方案 </el-button>
</div>
</div>
</div>
</template>
<div v-else class="detail-empty">
<i class="el-icon-d-arrow-left" style="font-size:24px;color:#c0c4cc;margin-bottom:12px;display:block" />
<span>请在左侧选择一个规程查看其版本</span>
</div>
</div>
</div>
<div v-else class="empty-hint">请选择一个规程查看其版本</div>
<!-- 新建/编辑规程 -->
<el-dialog :title="specTitle" :visible.sync="specOpen" width="500px" append-to-body @close="specForm = {}">
<el-dialog :title="specTitle" :visible.sync="specOpen" width="460px" append-to-body @close="specForm = {}">
<el-form ref="specFormRef" :model="specForm" :rules="specRules" label-width="88px" size="small">
<el-form-item label="规程编码" prop="specCode">
<el-input v-model="specForm.specCode" placeholder="请输入规程编码" maxlength="64" />
@@ -78,7 +130,7 @@
<el-input v-model="specForm.specName" placeholder="请输入规程名称" maxlength="200" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="specForm.remark" type="textarea" rows="2" maxlength="500" show-word-limit />
<el-input v-model="specForm.remark" type="textarea" :rows="2" maxlength="500" show-word-limit />
</el-form-item>
</el-form>
<div slot="footer">
@@ -88,21 +140,21 @@
</el-dialog>
<!-- 新建/编辑版本 -->
<el-dialog :title="versionTitle" :visible.sync="versionOpen" width="500px" append-to-body @close="versionForm = {}">
<el-dialog :title="versionTitle" :visible.sync="versionOpen" width="460px" append-to-body @close="versionForm = {}">
<el-form ref="versionFormRef" :model="versionForm" :rules="versionRules" label-width="88px" size="small">
<el-form-item label="版本号" prop="versionCode">
<el-input v-model="versionForm.versionCode" placeholder="如 V1.0" maxlength="64" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="versionForm.status" style="width:100%">
<el-option v-for="s in statusOptions" :key="s" :label="s" :value="s" />
<el-option v-for="s in STATUS_OPTIONS" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
</el-form-item>
<el-form-item label="保存后生效">
<el-form-item label="设为生效">
<el-switch v-model="versionForm.isActive" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="versionForm.remark" type="textarea" rows="2" maxlength="500" show-word-limit />
<el-input v-model="versionForm.remark" type="textarea" :rows="2" maxlength="500" show-word-limit />
</el-form-item>
</el-form>
<div slot="footer">
@@ -114,7 +166,7 @@
</template>
<script>
import { listProcessSpec, getProcessSpec, addProcessSpec, updateProcessSpec, delProcessSpec } from '@/api/wms/processSpec'
import { listProcessSpec, addProcessSpec, updateProcessSpec, delProcessSpec } from '@/api/wms/processSpec'
import {
listProcessSpecVersion,
addProcessSpecVersion,
@@ -123,28 +175,26 @@ import {
activateProcessSpecVersion
} from '@/api/wms/processSpecVersion'
const STATUS_OPTIONS = [
{ value: 'DRAFT', label: '草稿' },
{ value: 'PUBLISHED', label: '已发布' },
{ value: 'OBSOLETE', label: '已作废' }
]
export default {
name: 'SpecVersionManage',
data() {
return {
STATUS_OPTIONS,
pageLoading: false,
specList: [],
currentSpec: null,
currentSpecId: null,
versionList: [],
versionLoading: false,
statusOptions: ['DRAFT', 'PUBLISHED', 'OBSOLETE'],
// 分页相关
total: 0,
queryParams: {
pageNum: 1,
pageSize: 10,
specType: '',
lineId: ''
},
queryParams: { pageNum: 1, pageSize: 10, specType: '', lineId: '' },
// 规程相关
specOpen: false,
specTitle: '',
specSubmitLoading: false,
@@ -154,7 +204,6 @@ export default {
specName: [{ required: true, message: '规程名称不能为空', trigger: 'blur' }]
},
// 版本相关
versionOpen: false,
versionTitle: '',
versionSubmitLoading: false,
@@ -169,12 +218,17 @@ export default {
this.loadSpecs()
},
methods: {
// 表格行样式
tableRowClassName({ row }) {
return row.specId === this.currentSpecId ? 'current-row' : ''
},
statusType(status) {
return { DRAFT: '', PUBLISHED: 'success', OBSOLETE: 'info' }[status] || ''
},
statusLabel(status) {
const hit = STATUS_OPTIONS.find(s => s.value === status)
return hit ? hit.label : (status || '—')
},
// 加载规程列表
loadSpecs() {
this.pageLoading = true
listProcessSpec(this.queryParams).then(res => {
@@ -182,7 +236,7 @@ export default {
this.specList = res.rows || []
if (this.specList.length > 0 && !this.currentSpec) {
this.selectSpec(this.specList[0])
} else {
} else if (!this.specList.length) {
this.currentSpec = null
this.currentSpecId = null
this.versionList = []
@@ -190,19 +244,13 @@ export default {
}).catch(e => console.error(e)).finally(() => { this.pageLoading = false })
},
// 选择规程
selectSpec(spec) {
this.currentSpec = spec
this.currentSpecId = spec.specId
this.loadVersions()
},
onSpecRowClick(row) { this.selectSpec(row) },
// 点击规程行
onSpecRowClick(row) {
this.selectSpec(row)
},
// 加载版本列表
loadVersions() {
if (!this.currentSpecId) return
this.versionLoading = true
@@ -211,46 +259,29 @@ export default {
}).catch(e => console.error(e)).finally(() => { this.versionLoading = false })
},
// 点击版本行
onVersionRowClick(row) {
this.goPlanSpec(row)
},
// 跳转到方案详情
goPlanSpec(row) {
const basePath = this.$route.path.replace(/\/[^/]*$/, '')
console.log(basePath)
this.$router.push({
path: `/process/processSpec/planSpec`,
query: { specId: this.currentSpecId, versionId: String(row.versionId), versionCode: row.versionCode }
})
},
// 生效切换
handleActiveChange(row, val) {
if (!val) {
this.$message.info('请激活其他版本来替换当前生效版本')
return
}
if (!val) { this.$message.info('请激活其他版本来替换当前生效版本'); return }
this.$modal.confirm('确认将版本"' + row.versionCode + '"设为当前生效版本?').then(() => {
return activateProcessSpecVersion(row.versionId)
}).then(() => {
this.$modal.msgSuccess('已生效')
this.loadVersions()
}).catch(() => { })
}).catch(() => {})
},
// 规程对话框
openSpecDialog(row) {
this.specForm = row
? { ...row }
: { specCode: undefined, specName: undefined, remark: undefined }
this.specForm = row ? { ...row } : { specCode: undefined, specName: undefined, remark: undefined }
this.specTitle = row ? '编辑规程' : '新建规程'
this.specOpen = true
this.$nextTick(() => this.$refs.specFormRef && this.$refs.specFormRef.clearValidate())
},
// 提交规程
submitSpec() {
this.$refs.specFormRef.validate(ok => {
if (!ok) return
@@ -263,23 +294,18 @@ export default {
}).catch(e => console.error(e)).finally(() => { this.specSubmitLoading = false })
})
},
// 删除规程
removeSpec(row) {
this.$modal.confirm('确认删除规程"' + row.specName + '"').then(() => {
return delProcessSpec(row.specId)
}).then(() => {
this.$modal.msgSuccess('删除成功')
if (this.currentSpecId === row.specId) {
this.currentSpec = null
this.currentSpecId = null
this.versionList = []
this.currentSpec = null; this.currentSpecId = null; this.versionList = []
}
this.loadSpecs()
}).catch(() => { })
}).catch(() => {})
},
// 版本对话框
openVersionDialog(row) {
this.versionForm = row
? { ...row }
@@ -288,8 +314,6 @@ export default {
this.versionOpen = true
this.$nextTick(() => this.$refs.versionFormRef && this.$refs.versionFormRef.clearValidate())
},
// 提交版本
submitVersion() {
this.$refs.versionFormRef.validate(ok => {
if (!ok) return
@@ -304,116 +328,173 @@ export default {
}).catch(e => console.error(e)).finally(() => { this.versionSubmitLoading = false })
})
},
// 删除版本
removeVersion(row) {
this.$modal.confirm('确认删除版本"' + row.versionCode + '"').then(() => {
return delProcessSpecVersion(row.versionId)
}).then(() => {
this.$modal.msgSuccess('删除成功')
this.loadVersions()
}).catch(() => { })
}).catch(() => {})
}
}
}
</script>
<style scoped>
.spec-version-page {
padding: 16px 20px;
.spec-page {
padding: 12px 16px;
min-height: 100%;
display: flex;
flex-direction: column;
}
.page-header {
/* ── 主体布局 ── */
.master-detail {
display: flex;
gap: 12px;
flex: 1;
min-height: 0;
align-items: flex-start;
}
/* ── 左:规程列表 ── */
.master-panel {
width: 380px;
flex-shrink: 0;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
}
/* 筛选区 */
.filter-area {
padding: 10px 12px 8px;
background: #fafafa;
border-bottom: 1px solid #f0f2f5;
display: flex;
flex-direction: column;
gap: 6px;
}
.filter-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #ebeef5;
gap: 8px;
}
.filter-label {
font-size: 12px;
color: #909399;
white-space: nowrap;
width: 52px;
flex-shrink: 0;
}
.filter-select { flex: 1; }
.page-title {
font-size: 15px;
.panel-hd {
display: flex;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid #f0f2f5;
background: #fff;
}
.panel-title {
font-size: 13px;
font-weight: 600;
color: #303133;
}
.section-wrapper {
background: #fff;
border-radius: 4px;
padding: 12px;
margin-bottom: 16px;
border: 1px solid #ebeef5;
.total-badge {
font-size: 12px;
color: #909399;
margin-left: 6px;
}
.section-title {
/* ── 右:版本面板 ── */
.detail-panel {
flex: 1;
min-width: 0;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
}
.detail-title-row {
display: flex;
align-items: center;
gap: 8px;
}
.spec-code-tag {
font-size: 11px;
color: #909399;
background: #f0f2f5;
padding: 1px 8px;
border-radius: 10px;
}
.detail-empty, .empty-versions {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 0;
color: #909399;
font-size: 13px;
font-weight: 600;
color: #606266;
margin-bottom: 12px;
}
/* ── 版本卡片 ── */
.version-list {
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.version-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px;
border: 1px solid #ebeef5;
border-radius: 4px;
cursor: pointer;
transition: border-color .15s, box-shadow .15s;
background: #fff;
}
.empty-hint {
text-align: center;
padding: 60px 0;
color: #909399;
.version-card:hover {
border-color: #5F7BA0;
box-shadow: 0 2px 8px rgba(95,123,160,.12);
}
.vc-left { display: flex; flex-direction: column; gap: 4px; }
.vc-top { display: flex; align-items: center; gap: 6px; }
.vc-code {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.el-table {
border-radius: 0;
.vc-status, .vc-active { border-radius: 10px !important; }
.vc-meta { font-size: 11px; color: #c0c4cc; }
.vc-right {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.btn-view { color: #5F7BA0 !important; }
::v-deep .el-table .current-row {
background: #f0f7ff !important;
}
/* ── 表格行高亮 ── */
::v-deep .el-table .current-row { background: #f0f7ff !important; }
::v-deep .el-table .current-row td { background: #f0f7ff !important; }
/* ── 按钮主色覆盖 ── */
::v-deep .el-button--primary {
color: #fff !important;
background: #5F7BA0 !important;
border-color: #5F7BA0 !important;
}
::v-deep .el-button--primary:hover,
::v-deep .el-button--primary:focus {
background: #4d6a8e !important;
border-color: #4d6a8e !important;
}
::v-deep .el-button--primary:active {
background: #4a6585 !important;
border-color: #4a6585 !important;
}
::v-deep .el-button--primary.is-disabled {
opacity: .5;
}
::v-deep .el-button:not(.el-button--primary):not(.el-button--text):not(.el-button--danger) {
color: #606266 !important;
background: #fff !important;
border-color: #dcdfe6 !important;
}
::v-deep .el-button:not(.el-button--primary):not(.el-button--text):not(.el-button--danger):hover {
color: #5F7BA0 !important;
border-color: #5F7BA0 !important;
}
::v-deep .el-button--text {
background: transparent !important;
border-color: transparent !important;
}
::v-deep .el-button--text.btn-danger {
color: #f56c6c !important;
}
.btn-danger {
color: #f56c6c;
}
::v-deep .el-button--primary.is-disabled { opacity: .5; }
::v-deep .el-button--text { background: transparent !important; border-color: transparent !important; }
::v-deep .el-button--text.btn-danger { color: #f56c6c !important; }
.btn-danger { color: #f56c6c; }
</style>

View File

@@ -2,22 +2,29 @@
<div class="plan-spec-page" v-loading="pageLoading">
<!-- 头部 -->
<div class="page-header">
<el-button type="text" icon="el-icon-arrow-left" @click="goBack">返回</el-button>
<el-button type="text" icon="el-icon-arrow-left" class="back-btn" @click="goBack">返回</el-button>
<div class="header-divider" />
<span class="page-title">方案详情</span>
<span v-if="versionCode" class="version-badge">版本 {{ versionCode }}</span>
</div>
<span v-if="versionCode" class="version-badge">
<i class="el-icon-price-tag" style="margin-right:3px" />版本 {{ versionCode }}
</span>
<div style="flex:1" />
<!-- 可配置 / 不可配置 切换 -->
<div class="config-tabs">
<span
:class="['config-tab', { active: configMode === 'configurable' }]"
@click="configMode = 'configurable'"
>可配置</span>
<span
:class="['config-tab', { active: configMode === 'readonly' }]"
@click="configMode = 'readonly'"
>不可配置</span>
<span :class="['config-tab', { active: configMode === 'configurable' }]" @click="configMode = 'configurable'">可配置</span>
<span :class="['config-tab', { active: configMode === 'readonly' }]" @click="configMode = 'readonly'">不可配置</span>
</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">
<!-- 左侧段分组 -->
@@ -96,11 +103,7 @@
<template slot-scope="{ row }">{{ segLabel(row.segmentType) }} {{ row.segmentName || '—' }}</template>
</el-table-column>
<el-table-column label="点位名称" prop="pointName" show-overflow-tooltip />
<el-table-column label="实际值ID" prop="actualValueId" show-overflow-tooltip />
<el-table-column label="L1设定值ID" prop="l1SetValueId" show-overflow-tooltip />
<el-table-column label="设定值" prop="targetValue" align="center" />
<el-table-column label="下限" prop="lowerLimit" align="center" />
<el-table-column label="上限" prop="upperLimit" align="center" />
<el-table-column label="点位编码" prop="pointCode" show-overflow-tooltip />
<el-table-column label="操作" align="right">
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click.stop="openPlanDialog(row)">编辑</el-button>
@@ -117,12 +120,32 @@
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openParamDialog()">新建参数</el-button>
</div>
<el-table v-loading="paramLoading" :data="paramList" size="small">
<el-table-column label="参数编码" prop="paramCode" show-overflow-tooltip />
<el-table-column label="参数编码" prop="paramCode" width="110" show-overflow-tooltip />
<el-table-column label="参数名称" prop="paramName" show-overflow-tooltip />
<el-table-column label="设定值" prop="targetValue" align="center" />
<el-table-column label="下限" prop="lowerLimit" align="center" />
<el-table-column label="上限" prop="upperLimit" align="center" />
<el-table-column label="单位" prop="unit" align="center" />
<el-table-column label="实际值ID" prop="actualSrcId" width="140" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.actualSrcId || '—' }}</template>
</el-table-column>
<el-table-column label="L1设定值ID" prop="presetSrcId" width="140" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.presetSrcId || '—' }}</template>
</el-table-column>
<el-table-column label="设定值" prop="targetValue" align="right" width="80" />
<el-table-column label="最小值" prop="lowerLimit" align="right" width="80" />
<el-table-column label="最大值" prop="upperLimit" align="right" width="80" />
<el-table-column label="单位" prop="unit" align="center" width="60" />
<el-table-column label="更新时间" align="center" width="136">
<template slot-scope="{ row }">{{ (row.updateTime || row.createTime || '').substring(0, 16) || '—' }}</template>
</el-table-column>
<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">
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click="openParamDialog(null, row)">编辑</el-button>
@@ -130,10 +153,166 @@
</template>
</el-table-column>
</el-table>
<!-- 偏差分析区块 -->
<template v-if="planAnomalies.length">
<div class="anomaly-section-header">
<i class="el-icon-warning" style="color:#E6A23C;margin-right:4px" />
实际生产偏差分析
<el-tag type="warning" size="mini" effect="plain" style="margin-left:8px">{{ planAnomalies.length }} 项异常</el-tag>
<el-button type="text" size="mini" style="margin-left:auto" @click="anomalyExpanded = !anomalyExpanded">
{{ anomalyExpanded ? '收起' : '展开' }}
</el-button>
</div>
<div v-show="anomalyExpanded">
<el-table :data="planAnomalies" size="small" border>
<el-table-column label="参数" prop="paramName" width="110" show-overflow-tooltip />
<el-table-column label="规程设定值" align="right" width="96">
<template slot-scope="{ row }">{{ row.storedTarget != null ? row.storedTarget : '—' }}</template>
</el-table-column>
<el-table-column label="规程最大值" align="right" width="96">
<template slot-scope="{ row }">{{ row.storedUpper != null ? row.storedUpper : '—' }}</template>
</el-table-column>
<el-table-column label="规程最小值" align="right" width="96">
<template slot-scope="{ row }">{{ row.storedLower != null ? row.storedLower : '—' }}</template>
</el-table-column>
<el-table-column label="实际最大值" align="right" width="96">
<template slot-scope="{ row }">
<span :class="(row.anomalyType === 'OVER_MAX' || row.anomalyType === 'BOTH') ? 'val-over' : ''">
{{ row.actualMax != null ? row.actualMax : '—' }}
</span>
</template>
</el-table-column>
<el-table-column label="实际最小值" align="right" width="96">
<template slot-scope="{ row }">
<span :class="(row.anomalyType === 'UNDER_MIN' || row.anomalyType === 'BOTH') ? 'val-under' : ''">
{{ row.actualMin != null ? row.actualMin : '—' }}
</span>
</template>
</el-table-column>
<el-table-column label="最大偏差" align="right" width="88">
<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="88">
<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="coilId" show-overflow-tooltip width="120" />
<el-table-column label="检测时间" prop="detectedAt" width="140" show-overflow-tooltip>
<template slot-scope="{ row }">{{ (row.detectedAt || '').substring(0, 16) || '—' }}</template>
</el-table-column>
<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>
<!-- 对比图 -->
<div ref="anomalyChart" class="anomaly-chart" />
</div>
</template>
</template>
</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">
@@ -151,11 +330,11 @@
<el-form-item label="点位编码" prop="pointCode">
<el-input v-model="planForm.pointCode" maxlength="64" />
</el-form-item>
<el-form-item label="实际值ID" prop="actualValueId">
<el-input v-model="planForm.actualValueId" maxlength="64" />
<el-form-item label="实际值ID" prop="actualSrcId">
<el-input v-model="planForm.actualSrcId" maxlength="64" />
</el-form-item>
<el-form-item label="L1设定值ID" prop="l1SetValueId">
<el-input v-model="planForm.l1SetValueId" maxlength="64" />
<el-form-item label="L1设定值ID" prop="presetSrcId">
<el-input v-model="planForm.presetSrcId" maxlength="64" />
</el-form-item>
<el-form-item label="设定值" prop="targetValue">
<el-input v-model="planForm.targetValue" />
@@ -289,9 +468,12 @@
</template>
<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 { listProcessPlanParam, addProcessPlanParam, updateProcessPlanParam, delProcessPlanParam } from '@/api/wms/processPlanParam'
import { listAllProcessAnomaly } from '@/api/wms/processAnomaly'
import { listProcessCoilRecord } from '@/api/wms/processCoilRecord'
/** 表单内可选段类型(新建/编辑仍支持全部枚举) */
const SEGMENT_FORM_OPTIONS = [
@@ -352,6 +534,19 @@ 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,
@@ -437,6 +632,22 @@ export default {
extra.sort((a, b) => String(a.label).localeCompare(String(b.label), 'zh-CN'))
return [...SEGMENT_FORM_OPTIONS, ...extra]
},
/** 当前选中点位下的异常条目 */
planAnomalies() {
if (!this.selectedPlan) return []
return this.allAnomalies.filter(a => a.paramCode === this.selectedPlan.pointCode ||
this.paramList.some(p => p.paramCode === a.paramCode))
},
/** 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)
},
filteredPlans() {
const type = this.activeSegmentType
const sub = this.activeSegmentName
@@ -464,6 +675,18 @@ export default {
},
watch: {
$route: { immediate: true, handler() { this.syncFromRoute() } },
planAnomalies: {
handler(list) {
if (list.length && this.anomalyExpanded) {
this.$nextTick(() => this.renderAnomalyChart())
}
}
},
anomalyExpanded(val) {
if (val && this.planAnomalies.length) {
this.$nextTick(() => this.renderAnomalyChart())
}
},
segmentTypeTabOptions: {
handler() {
const a = this.activeSegmentType
@@ -495,9 +718,121 @@ export default {
this.versionId = q.versionId || undefined
this.versionCode = q.versionCode || ''
this.specId = q.specId || undefined
if (this.versionId) this.loadPlans()
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 = ''
@@ -549,8 +884,8 @@ export default {
segmentName: undefined,
pointName: undefined,
pointCode: undefined,
actualValueId: undefined,
l1SetValueId: undefined,
actualSrcId: undefined,
presetSrcId: undefined,
targetValue: undefined,
lowerLimit: undefined,
upperLimit: undefined,
@@ -1061,26 +1396,43 @@ export default {
align-items: center;
gap: 10px;
margin-bottom: 12px;
background: #fff;
border-radius: 4px;
padding: 8px 12px;
border: 1px solid #ebeef5;
}
.back-btn {
color: #5F7BA0 !important;
padding: 0 !important;
font-size: 13px !important;
}
.header-divider {
width: 1px;
height: 16px;
background: #dcdfe6;
flex-shrink: 0;
}
.page-title {
font-size: 15px;
font-weight: 600;
font-size: 14px;
font-weight: 700;
color: #303133;
}
.version-badge {
font-size: 12px;
color: #909399;
background: #f0f2f5;
padding: 2px 8px;
color: #5F7BA0;
background: #edf2f8;
padding: 2px 10px;
border-radius: 10px;
border: 1px solid #c8d8ea;
}
.config-tabs {
display: flex;
gap: 0;
margin-bottom: 14px;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
@@ -1206,6 +1558,28 @@ export default {
.btn-danger { color: #f56c6c; }
/* ── 偏差分析 ── */
.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 {
padding: 20px;
@@ -1261,4 +1635,35 @@ export default {
.import-error {
margin-bottom: 20px;
}
/* ── 服役钢卷记录 ── */
.coil-record-section {
margin-top: 16px;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
}
.coil-record-hd {
display: flex;
align-items: center;
padding: 8px 14px;
font-size: 13px;
font-weight: 600;
color: #303133;
border-bottom: 1px solid #f0f2f5;
background: #fafafa;
}
.coil-total-badge {
font-size: 12px;
font-weight: normal;
color: #606266;
margin-left: 8px;
}
.coil-anomaly-badge {
font-size: 12px;
font-weight: normal;
color: #f56c6c;
margin-left: 6px;
}
</style>

View File

@@ -0,0 +1,45 @@
package com.klp.controller;
import com.klp.common.core.controller.BaseController;
import com.klp.common.core.domain.PageQuery;
import com.klp.common.core.domain.R;
import com.klp.common.core.page.TableDataInfo;
import com.klp.domain.bo.WmsProcessAnomalyBo;
import com.klp.domain.vo.WmsProcessAnomalyVo;
import com.klp.service.IWmsProcessAnomalyService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 工艺参数异常记录
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/wms/processAnomaly")
public class WmsProcessAnomalyController extends BaseController {
private final IWmsProcessAnomalyService wmsProcessAnomalyService;
/** 分页列表(规程方案页按版本/参数/钢卷查询) */
@GetMapping("/list")
public TableDataInfo<WmsProcessAnomalyVo> list(WmsProcessAnomalyBo bo, PageQuery pageQuery) {
return wmsProcessAnomalyService.queryPageList(bo, pageQuery);
}
/** 不分页列表(用于规程方案页全量加载当前版本异常) */
@GetMapping("/listAll")
public R<List<WmsProcessAnomalyVo>> listAll(WmsProcessAnomalyBo bo) {
return R.ok(wmsProcessAnomalyService.queryList(bo));
}
/** 批量新增异常记录(实绩页检测到异常后调用) */
@PostMapping("/batchAdd")
public R<Void> batchAdd(@RequestBody List<WmsProcessAnomalyBo> boList) {
wmsProcessAnomalyService.batchInsert(boList);
return R.ok();
}
}

View File

@@ -0,0 +1,51 @@
package com.klp.controller;
import com.klp.common.core.controller.BaseController;
import com.klp.common.core.domain.PageQuery;
import com.klp.common.core.domain.R;
import com.klp.common.core.page.TableDataInfo;
import com.klp.domain.bo.WmsProcessCoilRecordBo;
import com.klp.domain.vo.WmsProcessCoilRecordVo;
import com.klp.service.IWmsProcessCoilRecordService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 版本钢卷服役记录
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/wms/processCoilRecord")
public class WmsProcessCoilRecordController extends BaseController {
private final IWmsProcessCoilRecordService wmsProcessCoilRecordService;
/** 分页列表(规程版本页查钢卷历史) */
@GetMapping("/list")
public TableDataInfo<WmsProcessCoilRecordVo> list(WmsProcessCoilRecordBo bo, PageQuery pageQuery) {
return wmsProcessCoilRecordService.queryPageList(bo, pageQuery);
}
/** 不分页列表 */
@GetMapping("/listAll")
public R<List<WmsProcessCoilRecordVo>> listAll(WmsProcessCoilRecordBo bo) {
return R.ok(wmsProcessCoilRecordService.queryList(bo));
}
/** 版本下服役钢卷总数 */
@GetMapping("/count")
public R<Long> count(@RequestParam Long versionId) {
return R.ok(wmsProcessCoilRecordService.countByVersion(versionId));
}
/** 新增或更新(幂等接口,前端每次点击钢卷行时调用) */
@PostMapping("/upsert")
public R<Void> upsert(@RequestBody WmsProcessCoilRecordBo bo) {
wmsProcessCoilRecordService.upsert(bo);
return R.ok();
}
}

View File

@@ -0,0 +1,84 @@
package com.klp.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
* 工艺参数异常记录 wms_process_anomaly
*/
@Data
@TableName("wms_process_anomaly")
public class WmsProcessAnomaly {
private static final long serialVersionUID = 1L;
@TableId(value = "anomaly_id")
private Long anomalyId;
/** 规程版本ID */
private Long versionId;
/** 方案点位ID */
private Long planId;
/** 参数ID */
private Long paramId;
/** 出口钢卷号 */
private String coilId;
/** 入口钢卷号 */
private String enCoilId;
/** 参数编码 */
private String paramCode;
/** 参数名称 */
private String paramName;
/** 单位 */
private String unit;
/** 异常类型: OVER_MAX / UNDER_MIN / BOTH */
private String anomalyType;
/** 规程存储的设定值 */
private BigDecimal storedTarget;
/** 规程存储的上限 */
private BigDecimal storedUpper;
/** 规程存储的下限 */
private BigDecimal storedLower;
/** 本次L1实际设定值 */
private BigDecimal actualTarget;
/** 本次实际最大值 */
private BigDecimal actualMax;
/** 本次实际最小值 */
private BigDecimal actualMin;
/** 最大值偏差 actual_max - stored_upper */
private BigDecimal deviationMax;
/** 最小值偏差 actual_min - stored_lower */
private BigDecimal deviationMin;
/** 检测时间 */
private Date detectedAt;
private String createBy;
private Date createTime;
@TableLogic
private Integer delFlag;
private String remark;
}

View File

@@ -0,0 +1,47 @@
package com.klp.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.klp.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
/**
* 版本钢卷服役记录 wms_process_coil_record
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("wms_process_coil_record")
public class WmsProcessCoilRecord extends BaseEntity {
private static final long serialVersionUID = 1L;
@TableId(value = "record_id")
private Long recordId;
/** 规程版本ID */
private Long versionId;
/** 出口钢卷号 */
private String coilId;
/** 入口钢卷号 */
private String enCoilId;
/** 是否存在异常 0否1是 */
private Integer hasAnomaly;
/** 异常参数数量 */
private Integer anomalyCnt;
/** 检测/服役时间 */
private Date processTime;
@TableLogic
private Integer delFlag;
private String remark;
}

View File

@@ -59,6 +59,16 @@ public class WmsProcessPlanParam extends BaseEntity {
*/
private String unit;
/**
* 实际值来源钢卷号(首次写入时的 ENCOILID
*/
private String actualSrcId;
/**
* L1 预设值来源钢卷号(首次写入时的 COILID
*/
private String presetSrcId;
@TableLogic
private Integer delFlag;

View File

@@ -0,0 +1,35 @@
package com.klp.domain.bo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
* 工艺参数异常记录业务对象
*/
@Data
public class WmsProcessAnomalyBo {
private Long versionId;
private Long planId;
private Long paramId;
private String coilId;
private String enCoilId;
private String paramCode;
private String paramName;
private String unit;
private String anomalyType;
private BigDecimal storedTarget;
private BigDecimal storedUpper;
private BigDecimal storedLower;
private BigDecimal actualTarget;
private BigDecimal actualMax;
private BigDecimal actualMin;
private BigDecimal deviationMax;
private BigDecimal deviationMin;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC")
private Date detectedAt;
private String remark;
}

View File

@@ -0,0 +1,33 @@
package com.klp.domain.bo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.klp.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
/**
* 版本钢卷服役记录业务对象
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class WmsProcessCoilRecordBo extends BaseEntity {
private Long recordId;
private Long versionId;
private String coilId;
private String enCoilId;
private Integer hasAnomaly;
private Integer anomalyCnt;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC")
private Date processTime;
private String remark;
}

View File

@@ -39,5 +39,9 @@ public class WmsProcessPlanParamBo extends BaseEntity {
private String unit;
private String actualSrcId;
private String presetSrcId;
private String remark;
}

View File

@@ -0,0 +1,41 @@
package com.klp.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
* 工艺参数异常记录视图对象
*/
@Data
public class WmsProcessAnomalyVo {
private Long anomalyId;
private Long versionId;
private Long planId;
private Long paramId;
private String coilId;
private String enCoilId;
private String paramCode;
private String paramName;
private String unit;
private String anomalyType;
private BigDecimal storedTarget;
private BigDecimal storedUpper;
private BigDecimal storedLower;
private BigDecimal actualTarget;
private BigDecimal actualMax;
private BigDecimal actualMin;
private BigDecimal deviationMax;
private BigDecimal deviationMin;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date detectedAt;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
private String remark;
}

View File

@@ -0,0 +1,28 @@
package com.klp.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
/**
* 版本钢卷服役记录视图对象
*/
@Data
public class WmsProcessCoilRecordVo {
private Long recordId;
private Long versionId;
private String coilId;
private String enCoilId;
private Integer hasAnomaly;
private Integer anomalyCnt;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date processTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
private String remark;
}

View File

@@ -41,6 +41,12 @@ public class WmsProcessPlanParamVo {
@ExcelProperty(value = "单位")
private String unit;
@ExcelProperty(value = "实际值来源钢卷号")
private String actualSrcId;
@ExcelProperty(value = "L1设定值来源钢卷号")
private String presetSrcId;
@ExcelProperty(value = "备注")
private String remark;

View File

@@ -0,0 +1,11 @@
package com.klp.mapper;
import com.klp.common.core.mapper.BaseMapperPlus;
import com.klp.domain.WmsProcessAnomaly;
import com.klp.domain.vo.WmsProcessAnomalyVo;
/**
* 工艺参数异常记录 Mapper
*/
public interface WmsProcessAnomalyMapper extends BaseMapperPlus<WmsProcessAnomalyMapper, WmsProcessAnomaly, WmsProcessAnomalyVo> {
}

View File

@@ -0,0 +1,11 @@
package com.klp.mapper;
import com.klp.common.core.mapper.BaseMapperPlus;
import com.klp.domain.WmsProcessCoilRecord;
import com.klp.domain.vo.WmsProcessCoilRecordVo;
/**
* 版本钢卷服役记录 Mapper
*/
public interface WmsProcessCoilRecordMapper extends BaseMapperPlus<WmsProcessCoilRecordMapper, WmsProcessCoilRecord, WmsProcessCoilRecordVo> {
}

View File

@@ -0,0 +1,21 @@
package com.klp.service;
import com.klp.common.core.domain.PageQuery;
import com.klp.common.core.page.TableDataInfo;
import com.klp.domain.bo.WmsProcessAnomalyBo;
import com.klp.domain.vo.WmsProcessAnomalyVo;
import java.util.List;
/**
* 工艺参数异常记录 Service
*/
public interface IWmsProcessAnomalyService {
TableDataInfo<WmsProcessAnomalyVo> queryPageList(WmsProcessAnomalyBo bo, PageQuery pageQuery);
List<WmsProcessAnomalyVo> queryList(WmsProcessAnomalyBo bo);
/** 批量插入异常记录 */
void batchInsert(List<WmsProcessAnomalyBo> boList);
}

View File

@@ -0,0 +1,24 @@
package com.klp.service;
import com.klp.common.core.domain.PageQuery;
import com.klp.common.core.page.TableDataInfo;
import com.klp.domain.bo.WmsProcessCoilRecordBo;
import com.klp.domain.vo.WmsProcessCoilRecordVo;
import java.util.List;
/**
* 版本钢卷服役记录 Service
*/
public interface IWmsProcessCoilRecordService {
TableDataInfo<WmsProcessCoilRecordVo> queryPageList(WmsProcessCoilRecordBo bo, PageQuery pageQuery);
List<WmsProcessCoilRecordVo> queryList(WmsProcessCoilRecordBo bo);
/** 按版本统计服役钢卷总数(含异常数) */
long countByVersion(Long versionId);
/** 新增或更新(按 version_id + coil_id 唯一键) */
void upsert(WmsProcessCoilRecordBo bo);
}

View File

@@ -0,0 +1,67 @@
package com.klp.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.klp.common.core.domain.PageQuery;
import com.klp.common.core.page.TableDataInfo;
import com.klp.common.utils.StringUtils;
import com.klp.domain.WmsProcessAnomaly;
import com.klp.domain.bo.WmsProcessAnomalyBo;
import com.klp.domain.vo.WmsProcessAnomalyVo;
import com.klp.mapper.WmsProcessAnomalyMapper;
import com.klp.service.IWmsProcessAnomalyService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* 工艺参数异常记录 Service 实现
*/
@RequiredArgsConstructor
@Service
public class WmsProcessAnomalyServiceImpl implements IWmsProcessAnomalyService {
private final WmsProcessAnomalyMapper baseMapper;
@Override
public TableDataInfo<WmsProcessAnomalyVo> queryPageList(WmsProcessAnomalyBo bo, PageQuery pageQuery) {
Page<WmsProcessAnomalyVo> result = baseMapper.selectVoPage(pageQuery.build(), buildQueryWrapper(bo));
return TableDataInfo.build(result);
}
@Override
public List<WmsProcessAnomalyVo> queryList(WmsProcessAnomalyBo bo) {
return baseMapper.selectVoList(buildQueryWrapper(bo));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void batchInsert(List<WmsProcessAnomalyBo> boList) {
if (boList == null || boList.isEmpty()) return;
Date now = new Date();
List<WmsProcessAnomaly> entities = boList.stream().map(bo -> {
WmsProcessAnomaly e = BeanUtil.toBean(bo, WmsProcessAnomaly.class);
if (e.getDetectedAt() == null) e.setDetectedAt(now);
e.setCreateTime(now);
return e;
}).collect(Collectors.toList());
entities.forEach(baseMapper::insert);
}
private LambdaQueryWrapper<WmsProcessAnomaly> buildQueryWrapper(WmsProcessAnomalyBo bo) {
LambdaQueryWrapper<WmsProcessAnomaly> lqw = Wrappers.lambdaQuery();
lqw.eq(bo.getVersionId() != null, WmsProcessAnomaly::getVersionId, bo.getVersionId());
lqw.eq(bo.getPlanId() != null, WmsProcessAnomaly::getPlanId, bo.getPlanId());
lqw.eq(StringUtils.isNotBlank(bo.getCoilId()), WmsProcessAnomaly::getCoilId, bo.getCoilId());
lqw.eq(StringUtils.isNotBlank(bo.getParamCode()), WmsProcessAnomaly::getParamCode, bo.getParamCode());
lqw.eq(StringUtils.isNotBlank(bo.getAnomalyType()), WmsProcessAnomaly::getAnomalyType, bo.getAnomalyType());
lqw.orderByDesc(WmsProcessAnomaly::getDetectedAt);
return lqw;
}
}

View File

@@ -0,0 +1,71 @@
package com.klp.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.klp.common.core.domain.PageQuery;
import com.klp.common.core.page.TableDataInfo;
import com.klp.domain.WmsProcessCoilRecord;
import com.klp.domain.bo.WmsProcessCoilRecordBo;
import com.klp.domain.vo.WmsProcessCoilRecordVo;
import com.klp.mapper.WmsProcessCoilRecordMapper;
import com.klp.service.IWmsProcessCoilRecordService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 版本钢卷服役记录 Service 实现
*/
@RequiredArgsConstructor
@Service
public class WmsProcessCoilRecordServiceImpl implements IWmsProcessCoilRecordService {
private final WmsProcessCoilRecordMapper baseMapper;
@Override
public TableDataInfo<WmsProcessCoilRecordVo> queryPageList(WmsProcessCoilRecordBo bo, PageQuery pageQuery) {
Page<WmsProcessCoilRecordVo> result = baseMapper.selectVoPage(pageQuery.build(), buildQueryWrapper(bo));
return TableDataInfo.build(result);
}
@Override
public List<WmsProcessCoilRecordVo> queryList(WmsProcessCoilRecordBo bo) {
return baseMapper.selectVoList(buildQueryWrapper(bo));
}
@Override
public long countByVersion(Long versionId) {
return baseMapper.selectCount(Wrappers.<WmsProcessCoilRecord>lambdaQuery()
.eq(WmsProcessCoilRecord::getVersionId, versionId));
}
@Override
public void upsert(WmsProcessCoilRecordBo bo) {
LambdaQueryWrapper<WmsProcessCoilRecord> lqw = Wrappers.lambdaQuery();
lqw.eq(WmsProcessCoilRecord::getVersionId, bo.getVersionId());
lqw.eq(WmsProcessCoilRecord::getCoilId, bo.getCoilId());
WmsProcessCoilRecord existing = baseMapper.selectOne(lqw);
if (existing != null) {
existing.setHasAnomaly(bo.getHasAnomaly());
existing.setAnomalyCnt(bo.getAnomalyCnt());
existing.setProcessTime(bo.getProcessTime());
existing.setEnCoilId(bo.getEnCoilId());
baseMapper.updateById(existing);
} else {
WmsProcessCoilRecord record = BeanUtil.toBean(bo, WmsProcessCoilRecord.class);
baseMapper.insert(record);
}
}
private LambdaQueryWrapper<WmsProcessCoilRecord> buildQueryWrapper(WmsProcessCoilRecordBo bo) {
LambdaQueryWrapper<WmsProcessCoilRecord> lqw = Wrappers.lambdaQuery();
lqw.eq(bo.getVersionId() != null, WmsProcessCoilRecord::getVersionId, bo.getVersionId());
lqw.eq(bo.getCoilId() != null, WmsProcessCoilRecord::getCoilId, bo.getCoilId());
lqw.eq(bo.getHasAnomaly() != null, WmsProcessCoilRecord::getHasAnomaly, bo.getHasAnomaly());
lqw.orderByDesc(WmsProcessCoilRecord::getProcessTime);
return lqw;
}
}

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.klp.mapper.WmsProcessAnomalyMapper">
<resultMap type="com.klp.domain.WmsProcessAnomaly" id="WmsProcessAnomalyResult">
<result property="anomalyId" column="anomaly_id"/>
<result property="versionId" column="version_id"/>
<result property="planId" column="plan_id"/>
<result property="paramId" column="param_id"/>
<result property="coilId" column="coil_id"/>
<result property="enCoilId" column="en_coil_id"/>
<result property="paramCode" column="param_code"/>
<result property="paramName" column="param_name"/>
<result property="unit" column="unit"/>
<result property="anomalyType" column="anomaly_type"/>
<result property="storedTarget" column="stored_target"/>
<result property="storedUpper" column="stored_upper"/>
<result property="storedLower" column="stored_lower"/>
<result property="actualTarget" column="actual_target"/>
<result property="actualMax" column="actual_max"/>
<result property="actualMin" column="actual_min"/>
<result property="deviationMax" column="deviation_max"/>
<result property="deviationMin" column="deviation_min"/>
<result property="detectedAt" column="detected_at"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="delFlag" column="del_flag"/>
<result property="remark" column="remark"/>
</resultMap>
</mapper>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.klp.mapper.WmsProcessCoilRecordMapper">
<resultMap type="com.klp.domain.WmsProcessCoilRecord" id="WmsProcessCoilRecordResult">
<result property="recordId" column="record_id"/>
<result property="versionId" column="version_id"/>
<result property="coilId" column="coil_id"/>
<result property="enCoilId" column="en_coil_id"/>
<result property="hasAnomaly" column="has_anomaly"/>
<result property="anomalyCnt" column="anomaly_cnt"/>
<result property="processTime" column="process_time"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<result property="delFlag" column="del_flag"/>
<result property="remark" column="remark"/>
</resultMap>
</mapper>

View File

@@ -11,6 +11,8 @@
<result property="lowerLimit" column="lower_limit"/>
<result property="upperLimit" column="upper_limit"/>
<result property="unit" column="unit"/>
<result property="actualSrcId" column="actual_src_id"/>
<result property="presetSrcId" column="preset_src_id"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>

View File

@@ -0,0 +1,210 @@
create table wms_process_plan
(
plan_id bigint not null comment '主键'
primary key,
version_id bigint not null comment '规程版本ID',
segment_type varchar(32) not null comment '段类型(INLET/PROCESS/OUTLET)',
segment_name varchar(100) null comment '段名称',
point_name varchar(200) not null comment '点位名称',
point_code varchar(64) not null comment '点位编码',
sort_order int default 0 not null comment '排序',
create_by varchar(64) null comment '创建人',
create_time datetime default CURRENT_TIMESTAMP null comment '创建时间',
update_by varchar(64) null comment '更新人',
update_time datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',
del_flag tinyint default 0 not null comment '删除标志(0正常2删除)',
remark varchar(500) null comment '备注',
constraint uk_plan_version_point_code
unique (version_id, point_code)
)
comment '方案点位表';
create index idx_plan_sort
on wms_process_plan (version_id, sort_order);
create index idx_plan_version
on wms_process_plan (version_id);
create table wms_process_plan_param
(
param_id bigint not null comment '主键'
primary key,
plan_id bigint not null comment '方案点位ID',
param_code varchar(64) not null comment '参数编码',
param_name varchar(200) not null comment '参数名称',
target_value decimal(24, 6) null comment '设定值(L1预设值)',
lower_limit decimal(24, 6) null comment '下限(首次检测实际最小值)',
upper_limit decimal(24, 6) null comment '上限(首次检测实际最大值)',
unit varchar(32) null comment '单位',
actual_src_id varchar(64) null comment '实际值来源钢卷号(首次写入时的ENCOILID)',
preset_src_id varchar(64) null comment 'L1设定值来源钢卷号(首次写入时的COILID)',
create_by varchar(64) null comment '创建人',
create_time datetime default CURRENT_TIMESTAMP null comment '创建时间',
update_by varchar(64) null comment '更新人',
update_time datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',
del_flag tinyint default 0 not null comment '删除标志(0正常2删除)',
remark varchar(500) null comment '备注',
constraint uk_plan_param_code
unique (plan_id, param_code)
)
comment '方案参数表';
create index idx_plan_param_plan
on wms_process_plan_param (plan_id);
create table wms_process_spec
(
spec_id bigint auto_increment comment '主键'
primary key,
spec_code varchar(64) not null comment '规程编号',
spec_name varchar(200) not null comment '规程名称',
spec_type varchar(32) default 'PROCESS' not null comment '类型(PROCESS=工艺规程,STANDARD=标准)',
line_id bigint not null comment '产线ID',
product_type varchar(100) null comment '产品类型',
is_enabled tinyint default 1 not null comment '是否启用(0否1是)',
create_by varchar(64) null comment '创建人',
create_time datetime default CURRENT_TIMESTAMP null comment '创建时间',
update_by varchar(64) null comment '更新人',
update_time datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',
del_flag tinyint default 0 not null comment '删除标志(0正常2删除与全局逻辑删除配置一致)',
remark varchar(500) null comment '备注',
constraint uk_wms_process_spec_code
unique (spec_code)
)
comment '规程主表';
create index idx_wms_process_spec_line
on wms_process_spec (line_id);
create index idx_wms_process_spec_type
on wms_process_spec (spec_type);
create table wms_process_spec_version
(
version_id bigint not null comment '主键'
primary key,
spec_id bigint not null comment '规程主表ID',
version_code varchar(64) not null comment '版本号',
is_active tinyint default 0 not null comment '是否当前生效(0否1是)',
status varchar(32) default 'DRAFT' not null comment '状态(DRAFT草稿/PUBLISHED已发布/OBSOLETE作废等)',
create_by varchar(64) null comment '创建人',
create_time datetime default CURRENT_TIMESTAMP null comment '创建时间',
update_by varchar(64) null comment '更新人',
update_time datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',
del_flag tinyint default 0 not null comment '删除标志(0正常2删除)',
remark varchar(500) null comment '备注',
constraint uk_spec_version_code
unique (spec_id, version_code)
)
comment '规程版本表';
create index idx_spec_version_active
on wms_process_spec_version (spec_id, is_active);
create index idx_spec_version_spec
on wms_process_spec_version (spec_id);
-- ─────────────────────────────────────────────────────────────
-- 钢卷服役记录表:记录哪些钢卷经过了某版本规程的检测/生产
-- ─────────────────────────────────────────────────────────────
create table wms_process_coil_record
(
record_id bigint not null comment '主键'
primary key,
version_id bigint not null comment '规程版本ID',
coil_id varchar(64) not null comment '出口钢卷号(EXCOILID)',
en_coil_id varchar(64) null comment '入口钢卷号(ENCOILID)',
has_anomaly tinyint default 0 not null comment '本次是否检测到参数异常(0否1是)',
anomaly_cnt int default 0 not null comment '异常参数数量',
process_time datetime null comment '检测时间',
create_by varchar(64) null comment '创建人',
create_time datetime default CURRENT_TIMESTAMP null comment '创建时间',
update_by varchar(64) null comment '更新人',
update_time datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',
del_flag tinyint default 0 not null comment '删除标志(0正常2删除)',
remark varchar(500) null comment '备注',
constraint uk_coil_version
unique (version_id, coil_id)
)
comment '版本钢卷服役记录表';
create index idx_coil_record_version
on wms_process_coil_record (version_id);
create index idx_coil_record_coil
on wms_process_coil_record (coil_id);
create index idx_coil_record_anomaly
on wms_process_coil_record (version_id, has_anomaly);
-- ─────────────────────────────────────────────────────────────
-- 工艺参数异常记录表:持久化每次检测到的超限/欠限异常
-- ─────────────────────────────────────────────────────────────
create table wms_process_anomaly
(
anomaly_id bigint not null comment '主键'
primary key,
version_id bigint not null comment '规程版本ID',
plan_id bigint not null comment '方案点位ID',
param_id bigint null comment '参数ID(wms_process_plan_param)',
coil_id varchar(64) not null comment '触发异常的出口钢卷号',
en_coil_id varchar(64) null comment '入口钢卷号',
param_code varchar(64) not null comment '参数编码',
param_name varchar(200) null comment '参数名称',
unit varchar(32) null comment '单位',
anomaly_type varchar(32) not null comment '异常类型: OVER_MAX/UNDER_MIN/BOTH',
stored_target decimal(24, 6) null comment '规程中存储的设定值',
stored_upper decimal(24, 6) null comment '规程中存储的上限',
stored_lower decimal(24, 6) null comment '规程中存储的下限',
actual_target decimal(24, 6) null comment '本次L1实际设定值',
actual_max decimal(24, 6) null comment '本次实际最大值',
actual_min decimal(24, 6) null comment '本次实际最小值',
deviation_max decimal(24, 6) null comment '最大值偏差(actual_max - stored_upper, 正值表示超上限)',
deviation_min decimal(24, 6) null comment '最小值偏差(actual_min - stored_lower, 负值表示低于下限)',
detected_at datetime null comment '异常检测时间',
create_by varchar(64) null comment '创建人',
create_time datetime default CURRENT_TIMESTAMP null comment '创建时间',
del_flag tinyint default 0 not null comment '删除标志(0正常2删除)',
remark varchar(500) null comment '备注'
)
comment '工艺参数异常记录表';
create index idx_anomaly_version
on wms_process_anomaly (version_id);
create index idx_anomaly_coil
on wms_process_anomaly (coil_id);
create index idx_anomaly_param
on wms_process_anomaly (version_id, param_code);
create index idx_anomaly_detected
on wms_process_anomaly (detected_at);
-- ─────────────────────────────────────────────────────────────
-- 存量数据迁移(为 wms_process_plan_param 新增列打补丁)
-- 已有库执行此 ALTER新建库直接用上方 CREATE TABLE 即可
-- ─────────────────────────────────────────────────────────────
-- ALTER TABLE wms_process_plan_param
-- ADD COLUMN actual_src_id varchar(64) null comment '实际值来源钢卷号(首次写入时的ENCOILID)' AFTER unit,
-- ADD COLUMN preset_src_id varchar(64) null comment 'L1设定值来源钢卷号(首次写入时的COILID)' AFTER actual_src_id;
create table wms_process_task
(
task_id bigint auto_increment comment '工艺任务ID'
primary key,
plan_id bigint not null comment '关联生产计划ID',
process_id bigint not null comment '所需工艺ID',
product_id bigint not null comment '对应产品ID',
task_quantity int not null comment '任务数量',
task_status varchar(20) default 'pending' null comment '任务状态pending-待处理/processing-处理中/completed-已完成',
sequence int not null comment '工艺顺序',
del_flag tinyint(1) default 0 not null comment '删除标志0=正常1=已删除)',
remark varchar(255) null comment '备注',
create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间',
create_by varchar(50) null comment '创建人',
update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
update_by varchar(50) null comment '更新人'
)
comment '工艺任务表(生产计划所需工艺任务)' row_format = DYNAMIC;