Files
pickling-mes/frontend/src/views/ProcessModel.vue

547 lines
23 KiB
Vue
Raw Normal View History

<template>
<div>
<div class="flex-between" style="margin-bottom:4px;">
<div class="sec-title" style="margin-bottom:0;">🧪 酸洗工艺段模型</div>
<div class="flex-row">
<span class="kv-label">刷新时间{{ lastRefresh }}</span>
<span :class="['badge', l1Online ? 'badge-green' : 'badge-yellow']" style="margin-left:8px;">
{{ l1Online ? 'L1在线' : '无L1数据' }}
</span>
</div>
</div>
<!-- 酸槽 1-3 -->
<div class="sec-title">酸槽 1#3#</div>
<div class="grid-3">
<div v-for="i in [0,1,2]" :key="i" class="card">
<div class="card-header">
{{ i+1 }}# 酸槽
<span :class="['badge', tankBadge(tanks[i])]" style="margin-left:auto;">{{ tankStatus(tanks[i]) }}</span>
</div>
<div class="card-body">
<div class="kv-grid">
<span class="kv-label">HCl 浓度</span>
<span class="kv-value">{{ tanks[i].conc != null ? tanks[i].conc : '—' }} <span class="kv-unit">g/L</span></span>
<span class="kv-label">温度</span>
<span class="kv-value">{{ tanks[i].temp != null ? tanks[i].temp : '—' }} <span class="kv-unit">°C</span></span>
<span class="kv-label">Fe² 含量</span>
<span class="kv-value">{{ tanks[i].fe2 != null ? tanks[i].fe2 : '—' }} <span class="kv-unit">g/L</span></span>
<span class="kv-label">停留时间</span>
<span class="kv-value">{{ tanks[i].rt != null ? tanks[i].rt : '—' }} <span class="kv-unit">s</span></span>
</div>
<div class="mt8" v-if="tanks[i].eff != null">
<div class="flex-between" style="margin-bottom:4px;">
<span class="kv-label" style="font-size:11px;">酸洗效率模型估算</span>
<span style="font-size:11px;color:#00c8ff;">{{ tanks[i].eff }}%</span>
</div>
<div class="prog-bar-wrap">
<div class="prog-bar-fill" :style="{ width: tanks[i].eff + '%', background: effColor(tanks[i].eff) }"></div>
</div>
</div>
<div v-else class="kv-label" style="margin-top:8px;text-align:center;">等待 L1 数据</div>
</div>
</div>
</div>
<!-- 酸槽 4-6 -->
<div class="sec-title mt8">酸槽 4#6#</div>
<div class="grid-3">
<div v-for="i in [3,4,5]" :key="i" class="card">
<div class="card-header">
{{ i+1 }}# 酸槽
<span :class="['badge', tankBadge(tanks[i])]" style="margin-left:auto;">{{ tankStatus(tanks[i]) }}</span>
</div>
<div class="card-body">
<div class="kv-grid">
<span class="kv-label">HCl 浓度</span>
<span class="kv-value">{{ tanks[i].conc != null ? tanks[i].conc : '—' }} <span class="kv-unit">g/L</span></span>
<span class="kv-label">温度</span>
<span class="kv-value">{{ tanks[i].temp != null ? tanks[i].temp : '—' }} <span class="kv-unit">°C</span></span>
<span class="kv-label">Fe² 含量</span>
<span class="kv-value">{{ tanks[i].fe2 != null ? tanks[i].fe2 : '—' }} <span class="kv-unit">g/L</span></span>
<span class="kv-label">停留时间</span>
<span class="kv-value">{{ tanks[i].rt != null ? tanks[i].rt : '—' }} <span class="kv-unit">s</span></span>
</div>
<div class="mt8" v-if="tanks[i].eff != null">
<div class="flex-between" style="margin-bottom:4px;">
<span class="kv-label" style="font-size:11px;">酸洗效率模型估算</span>
<span style="font-size:11px;color:#00c8ff;">{{ tanks[i].eff }}%</span>
</div>
<div class="prog-bar-wrap">
<div class="prog-bar-fill" :style="{ width: tanks[i].eff + '%', background: effColor(tanks[i].eff) }"></div>
</div>
</div>
<div v-else class="kv-label" style="margin-top:8px;text-align:center;">等待 L1 数据</div>
</div>
</div>
</div>
<!-- 漂洗段 -->
<div class="sec-title mt8">漂洗段5级逆流</div>
<div class="card">
<div class="card-header">漂洗段状态</div>
<div class="card-body">
<div v-if="l1Online" class="table-scroll">
<table class="data-table">
<thead>
<tr><th>漂洗级</th><th>pH值</th><th>温度 (°C)</th><th>流量 (/h)</th><th>电导率 (μS/cm)</th><th>状态</th></tr>
</thead>
<tbody>
<tr v-for="(r, idx) in rinse" :key="idx">
<td class="td-num">{{ idx+1 }}# 漂洗</td>
<td>{{ r.ph != null ? r.ph : '—' }}</td>
<td class="td-num">{{ r.temp != null ? r.temp : '—' }}</td>
<td class="td-num">{{ r.flow != null ? r.flow : '—' }}</td>
<td class="td-num">{{ r.conductivity != null ? r.conductivity : '—' }}</td>
<td><span class="badge badge-green">正常</span></td>
</tr>
</tbody>
</table>
</div>
<div v-else style="text-align:center;padding:20px;color:var(--text-muted);">
漂洗段数据来自 L1 实时报文当前无 L1 连接
</div>
</div>
</div>
<!-- 模型计算面板 -->
<div class="section-row mt8">
<div class="card" style="flex:1;">
<div class="card-header"> 最优速度计算实时自动</div>
<div class="card-body">
<div class="grid-2" style="gap:12px;margin-bottom:14px;">
<div class="form-field">
<div class="kv-label">带钢厚度 (mm)</div>
<input v-model.number="calc.thickness" type="number" class="kv-input" step="0.1" min="2.0" max="4.5" @change="doCalc" />
</div>
<div class="form-field">
<div class="kv-label">带钢宽度 (mm)</div>
<input v-model.number="calc.width" type="number" class="kv-input" step="10" min="800" max="1250" @change="doCalc" />
</div>
<div class="form-field">
<div class="kv-label">钢种</div>
<select v-model="calc.steel_grade" class="kv-input" @change="doCalc">
<option v-for="g in steelGrades" :key="g" :value="g">{{ g }}</option>
</select>
</div>
<div class="form-field">
<div class="kv-label">目标酸洗指数 (%)</div>
<input v-model.number="calc.target_pi" type="number" class="kv-input" step="1" min="70" max="100" @change="doCalc" />
</div>
<div class="form-field">
<div class="kv-label">氧化铁皮重量 (g/)</div>
<input v-model.number="calc.scale_weight" type="number" class="kv-input" step="0.5" @change="doCalc" />
</div>
</div>
<div class="sec-title">各槽酸液参数
<span class="kv-label" style="font-size:10px;margin-left:8px;">
{{ l1Online ? '已从 L1 报文同步' : '手动输入L1 上线后自动同步)' }}
</span>
</div>
<div class="grid-3" style="gap:8px;margin-bottom:8px;">
<div v-for="i in 6" :key="i" class="form-field">
<div class="kv-label">{{ i }}# 槽浓度 (g/L)</div>
<input v-model.number="calc.acid_conc_list[i-1]" type="number" class="kv-input" step="5" @change="doCalc" />
</div>
</div>
<div class="grid-3" style="gap:8px;margin-bottom:14px;">
<div v-for="i in 6" :key="'t'+i" class="form-field">
<div class="kv-label">{{ i }}# 槽温度 (°C)</div>
<input v-model.number="calc.acid_temp_list[i-1]" type="number" class="kv-input" step="1" @change="doCalc" />
</div>
</div>
<div v-if="calculating" style="text-align:center;color:var(--text-muted);padding:8px;">计算中...</div>
</div>
</div>
<!-- 计算结果 -->
<div class="card" style="flex:1;" v-if="calcResult">
<div class="card-header">计算结果
<span :class="['badge', riskBadge(calcResult.under_pickling_risk)]" style="margin-left:auto;">
欠酸洗风险{{ calcResult.under_pickling_risk }}
</span>
</div>
<div class="card-body">
<div class="grid-2" style="gap:10px;margin-bottom:14px;">
<div class="metric-box">
<div class="mb-label">最大允许速度</div>
<div class="mb-value">{{ calcResult.max_speed }}</div>
<div class="mb-unit">m/min</div>
</div>
<div class="metric-box">
<div class="mb-label">综合酸洗指数</div>
<div class="mb-value">{{ calcResult.total_pi }}</div>
<div class="mb-unit">%</div>
</div>
</div>
<div v-if="calcResult.warning" class="warn-box mb12">{{ calcResult.warning }}</div>
<div class="sec-title">各槽酸洗详情</div>
<table class="data-table">
<thead>
<tr><th>酸槽</th><th>停留时间 (s)</th><th>累计PI (%)</th><th>进度</th></tr>
</thead>
<tbody>
<tr v-for="(pi, idx) in calcResult.pi_per_tank" :key="idx">
<td class="td-num">{{ idx+1 }}# </td>
<td class="td-num">{{ calcResult.residence_time_per_tank[idx] }}</td>
<td class="td-num">{{ pi }}</td>
<td>
<div class="prog-bar-wrap" style="width:80px;display:inline-block;">
<div class="prog-bar-fill" :style="{ width: pi + '%', background: effColor(pi) }"></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="card" style="flex:1;display:flex;align-items:center;justify-content:center;" v-else>
<div style="text-align:center;color:var(--text-muted);padding:40px;">
<div style="font-size:11px;letter-spacing:2px;color:var(--text-muted);margin-bottom:12px;">[ LOADING ]</div>
<div>{{ calculating ? '模型计算中...' : '正在加载...' }}</div>
</div>
</div>
</div>
<!-- 模型校准 -->
<div class="card mt8">
<div class="card-header">
酸洗速度模型校准
<span class="ch-badge" :style="{ background: kCalColor(calib.current_kcal) }">
K_cal = {{ calib.current_kcal }}
</span>
<button class="btn btn-outline" style="margin-left:auto;padding:2px 10px;font-size:11px;"
@click="resetCalib('acid_speed')">重置系数</button>
</div>
<div class="card-body">
<div class="calib-layout">
<!-- 录入表单 -->
<div class="calib-form">
<div class="calib-hint">当模型预测速度与实际可用速度存在偏差时录入实测数据进行修正</div>
<div class="flex-col" style="gap:8px;margin-top:10px;">
<div class="form-field">
<div class="kv-label">实测最高合格速度 (m/min)</div>
<input v-model.number="calib.actual_speed" type="number" class="kv-input" step="1" min="20" max="180" />
</div>
<div class="form-field">
<div class="kv-label">该速度下质量结果</div>
<select v-model="calib.quality_ok" class="kv-input">
<option :value="true">合格酸洗充分</option>
<option :value="false">欠酸洗出现缺陷</option>
</select>
</div>
<div class="form-field">
<div class="kv-label">备注可选</div>
<input v-model="calib.note" type="text" class="kv-input" />
</div>
<div v-if="calcResult" class="calib-predict-row">
<span class="kv-label">模型预测速度</span>
<span class="kv-value">{{ calcResult.max_speed }} <span class="kv-unit">m/min</span></span>
<span class="kv-label" style="margin-left:16px;">偏差</span>
<span class="kv-value" :style="{ color: Math.abs(calib.actual_speed - calcResult.max_speed) > 10 ? '#f0a500' : '#28a745' }">
{{ calib.actual_speed && calcResult ? (calib.actual_speed - calcResult.max_speed > 0 ? '+' : '') + (calib.actual_speed - calcResult.max_speed).toFixed(1) : '—' }} m/min
</span>
</div>
<button class="btn btn-primary fw" :disabled="calib.loading || !calib.actual_speed" @click="submitCalib">
{{ calib.loading ? '提交中...' : '提交修正数据' }}
</button>
</div>
</div>
<!-- 历史记录 -->
<div class="calib-history">
<div class="sec-title" style="margin-bottom:8px;">修正记录</div>
<table class="data-table">
<thead>
<tr><th>时间</th><th>K </th><th>K </th><th>实测速度</th><th>质量</th><th>备注</th></tr>
</thead>
<tbody>
<tr v-for="h in acidHistory" :key="h.ts">
<td class="td-muted">{{ h.ts.slice(5,16) }}</td>
<td class="td-num">{{ h.k_before }}</td>
<td class="td-num" :style="{ color: h.k_after > h.k_before ? '#28a745' : '#f0a500' }">{{ h.k_after }}</td>
<td class="td-num">{{ h.input.actual_speed }} m/min</td>
<td><span :class="['badge', h.input.quality_ok ? 'badge-green' : 'badge-red']">{{ h.input.quality_ok ? '合格' : '欠酸洗' }}</span></td>
<td class="td-muted">{{ h.note || '—' }}</td>
</tr>
<tr v-if="!acidHistory.length">
<td colspan="6" class="td-muted" style="text-align:center;padding:14px;">暂无修正记录</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 当前过程参数L1实时 -->
<div class="card mt8">
<div class="card-header">当前过程参数L1 实时</div>
<div class="card-body">
<div v-if="l1Online" class="grid-5">
<div class="metric-box">
<div class="mb-label">当前线速</div>
<div class="mb-value">{{ current.speed }}</div>
<div class="mb-unit">m/min</div>
</div>
<div class="metric-box">
<div class="mb-label">入口张力</div>
<div class="mb-value">{{ current.tension_inlet }}</div>
<div class="mb-unit">kN</div>
</div>
<div class="metric-box">
<div class="mb-label">出口张力</div>
<div class="mb-value">{{ current.tension_outlet }}</div>
<div class="mb-unit">kN</div>
</div>
<div class="metric-box">
<div class="mb-label">1#槽酸液温度</div>
<div class="mb-value">{{ current.acid_temp }}</div>
<div class="mb-unit">°C</div>
</div>
<div class="metric-box">
<div class="mb-label">当前卷号</div>
<div class="mb-value" style="font-size:14px;">{{ current.coil_no || '—' }}</div>
<div class="mb-unit">在线</div>
</div>
</div>
<div v-else style="text-align:center;padding:24px;color:var(--text-muted);">
<div style="font-size:11px;letter-spacing:2px;color:var(--text-muted);margin-bottom:8px;">[ NO SIGNAL ]</div>
<div>当前无 L1 实时数据机组启动并接入 UDP 报文后自动显示</div>
<div style="font-size:11px;margin-top:6px;">L2 监听地址0.0.0.0:9000</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { predictAcidSpeed, getMessageLogs, getCalibration, calibrateAcidSpeed, resetCalibration } from '@/api'
const STEEL_GRADES = ['Q195','Q215','Q235','SPHC','SPHD','SPHE','SS400','SAPH440','QSTE420TM']
export default {
name: 'ProcessModel',
data() {
return {
lastRefresh: '--:--:--',
l1Online: false,
tanks: Array.from({ length: 6 }, () => ({ conc: null, temp: null, fe2: null, rt: null, eff: null })),
rinse: Array.from({ length: 5 }, () => ({ ph: null, temp: null, flow: null, conductivity: null })),
current: { speed: null, tension_inlet: null, tension_outlet: null, acid_temp: null, coil_no: null },
steelGrades: STEEL_GRADES,
calc: {
thickness: 3.0,
width: 1000,
steel_grade: 'Q235',
target_pi: 95,
scale_weight: 8.5,
acid_conc_list: [200, 188, 175, 162, 148, 135],
acid_temp_list: [80, 78, 76, 75, 74, 72],
},
calculating: false,
calcResult: null,
_timer: null,
calib: {
current_kcal: 1.0,
actual_speed: null,
quality_ok: true,
note: '',
loading: false,
},
calibHistory: [],
}
},
computed: {
acidHistory() {
return this.calibHistory.filter(h => h.model === 'acid_speed').slice(0, 10)
},
},
async mounted() {
await this.fetchL1Data()
await this.doCalc()
await this.loadCalibration()
this._timer = setInterval(() => this.fetchL1Data(), 5000)
},
beforeDestroy() {
clearInterval(this._timer)
},
methods: {
async fetchL1Data() {
try {
// 拉最新一条 PC03 过程数据报文
const res = await getMessageLogs({ msg_type: 'PC03', page_size: 1 })
const logs = res.data?.items || []
if (logs.length && logs[0].parsed_data) {
const d = typeof logs[0].parsed_data === 'string'
? JSON.parse(logs[0].parsed_data) : logs[0].parsed_data
this.l1Online = true
this.current.speed = d.speed ?? null
this.current.tension_inlet = d.tension_inlet ?? null
this.current.tension_outlet = d.tension_outlet ?? null
this.current.acid_temp = d.acid_temp ?? null
this.current.coil_no = d.coil_no ?? null
// 如果报文中有各槽浓度/温度,同步到计算参数
if (d.acid_conc_list) this.calc.acid_conc_list = d.acid_conc_list
if (d.acid_temp_list) this.calc.acid_temp_list = d.acid_temp_list
// 更新槽显示(用计算参数中的值,真实值未来扩展报文后替换)
this.syncTanksFromCalc()
await this.doCalc()
} else {
this.l1Online = false
}
} catch (e) {
this.l1Online = false
}
this.lastRefresh = new Date().toTimeString().slice(0, 8)
},
syncTanksFromCalc() {
this.tanks = this.calc.acid_conc_list.map((conc, i) => {
const temp = this.calc.acid_temp_list[i]
const eff = +(Math.min(98, 50 + (conc / 200) * 35 + (temp - 60) / 25 * 15)).toFixed(1)
return { conc, temp, fe2: null, rt: null, eff }
})
},
async doCalc() {
this.calculating = true
try {
const res = await predictAcidSpeed({
thickness: this.calc.thickness,
width: this.calc.width,
steel_grade: this.calc.steel_grade,
acid_conc_list: this.calc.acid_conc_list,
acid_temp_list: this.calc.acid_temp_list,
scale_weight: this.calc.scale_weight,
target_pi: this.calc.target_pi,
})
this.calcResult = res.data
// 用计算结果的停留时间更新槽显示
if (res.data.residence_time_per_tank) {
res.data.residence_time_per_tank.forEach((rt, i) => {
if (this.tanks[i]) this.tanks[i].rt = rt
})
}
} catch (e) {
// silent — user will see calcResult stay null
} finally {
this.calculating = false
}
},
tankBadge(t) {
if (t.conc == null) return 'badge-yellow'
if (t.conc < 100 || t.temp < 65) return 'badge-red'
if (t.conc < 140 || t.temp < 72) return 'badge-yellow'
return 'badge-green'
},
tankStatus(t) {
if (t.conc == null) return '待接入'
if (t.conc < 100 || t.temp < 65) return '报警'
if (t.conc < 140 || t.temp < 72) return '预警'
return '正常'
},
riskBadge(r) {
if (r === 'HIGH') return 'badge-red'
if (r === 'MEDIUM') return 'badge-yellow'
return 'badge-green'
},
effColor(v) {
if (v >= 80) return '#28a745'
if (v >= 60) return '#f0a500'
return '#da3633'
},
async loadCalibration() {
try {
const res = await getCalibration()
this.calib.current_kcal = res.data?.acid_speed_kcal ?? 1.0
this.calibHistory = res.data?.history || []
} catch (e) { /* silent */ }
},
async submitCalib() {
if (!this.calib.actual_speed) return
this.calib.loading = true
try {
await calibrateAcidSpeed({
thickness: this.calc.thickness,
width: this.calc.width,
steel_grade: this.calc.steel_grade,
acid_conc_list: this.calc.acid_conc_list,
acid_temp_list: this.calc.acid_temp_list,
scale_weight: this.calc.scale_weight,
actual_max_speed: this.calib.actual_speed,
actual_quality_ok: this.calib.quality_ok,
note: this.calib.note,
})
this.$message.success('修正数据已提交,模型系数已更新')
this.calib.actual_speed = null
this.calib.note = ''
await this.loadCalibration()
await this.doCalc()
} catch (e) {
this.$message.error('提交失败:' + (e.response?.data?.detail || e.message))
} finally {
this.calib.loading = false
}
},
async resetCalib(model) {
try {
await this.$confirm('确认将校准系数重置为 1.0', '重置确认', { type: 'warning' })
await resetCalibration(model)
this.$message.success('已重置')
await this.loadCalibration()
await this.doCalc()
} catch (e) { /* cancelled */ }
},
kCalColor(k) {
const d = Math.abs(k - 1.0)
if (d < 0.05) return '#1a3a1f'
if (d < 0.15) return '#3a2a00'
return '#3a1a1a'
},
}
}
</script>
<style lang="scss" scoped>
@import '@/assets/styles/variables';
.form-field { display: flex; flex-direction: column; gap: 5px; }
.warn-box {
background: rgba(240,165,0,.1);
border: 1px solid $accent-yellow;
border-radius: 4px;
padding: 8px 12px;
color: $accent-yellow;
font-size: 12px;
}
.mb12 { margin-bottom: 12px; }
.mt8 { margin-top: 8px; }
.calib-layout {
display: flex;
gap: 20px;
align-items: flex-start;
}
.calib-form {
flex: 0 0 300px;
}
.calib-history {
flex: 1;
overflow-x: auto;
}
.calib-hint {
font-size: 11px;
color: $text-muted;
line-height: 1.6;
border-left: 2px solid $border;
padding-left: 8px;
}
.calib-predict-row {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: rgba(255,255,255,.03);
border-radius: 4px;
border: 1px solid $border;
}
</style>