工艺规程增强
This commit is contained in:
@@ -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 + BOTLIMIT(TOPLIMIT/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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 规程列表 -->
|
||||
<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">
|
||||
<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 }}
|
||||
<el-button type="primary" size="mini" icon="el-icon-plus" @click="openVersionDialog()">新建版本</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 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>
|
||||
<!-- 列表头 -->
|
||||
<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="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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 可配置 / 不可配置 切换 -->
|
||||
<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>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user