2026-05-27 16:38:40 +08:00
|
|
|
|
<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>流量 (m³/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/m²)</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>
|
2026-05-27 17:04:48 +08:00
|
|
|
|
<input v-model="calib.note" type="text" class="kv-input" />
|
2026-05-27 16:38:40 +08:00
|
|
|
|
</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>
|