feat(prediction): 工艺参数沙盘 — PI 累积曲线 + 最大速度安全区间条

预测页酸洗模型计算结果加 SVG 可视化:
- PI 累积曲线(入口零点 → 5 槽累计 PI,含目标线 dashed 标注)
- 速度安全区间条(20-180 m/min 区间内 max_speed 位置 + 数值标签)
零依赖,纯 SVG 内联。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 09:51:29 +08:00
parent 9cf422ef0d
commit 475c169d33

View File

@@ -176,7 +176,57 @@
</div>
</div>
<div v-if="calcResult.warning" class="warn-box mb12">{{ calcResult.warning }}</div>
<div class="sec-title">各槽酸洗详情</div>
<div class="sec-title">速度安全区间</div>
<div class="speed-bar-wrap">
<div class="speed-bar-track">
<div class="speed-bar-fill" :style="{ width: speedPct + '%' }"></div>
<div class="speed-bar-tick" style="left:0%;">20</div>
<div class="speed-bar-tick" style="left:100%;transform:translateX(-100%);">180</div>
<div class="speed-bar-marker" :style="{ left: speedPct + '%' }">
<div class="speed-bar-marker-label">{{ calcResult.max_speed }} m/min</div>
</div>
</div>
</div>
<div class="sec-title mt8">PI 累积曲线</div>
<svg class="pi-chart" viewBox="0 0 400 180" preserveAspectRatio="none">
<!-- 网格 -->
<g stroke="rgba(255,255,255,0.06)" stroke-width="1">
<line v-for="y in [0,25,50,75,100]" :key="'g'+y"
x1="36" :y1="160 - y*1.3" x2="392" :y2="160 - y*1.3"/>
</g>
<!-- Y 轴刻度 -->
<g fill="#7a8794" font-size="9" font-family="monospace">
<text v-for="y in [0,25,50,75,100]" :key="'yl'+y"
x="30" :y="163 - y*1.3" text-anchor="end">{{ y }}</text>
</g>
<!-- X 轴槽标签 -->
<g fill="#7a8794" font-size="10" font-family="monospace" text-anchor="middle">
<text v-for="i in 5" :key="'xl'+i" :x="36 + i*71.2" y="174">{{ i }}#</text>
</g>
<!-- 目标线 -->
<line :x1="36" :y1="160 - calc.target_pi*1.3" :x2="392" :y2="160 - calc.target_pi*1.3"
stroke="#f0a500" stroke-width="1" stroke-dasharray="4 3"/>
<text :x="392" :y="160 - calc.target_pi*1.3 - 3"
fill="#f0a500" font-size="9" text-anchor="end">目标 {{ calc.target_pi }}%</text>
<!-- 曲线 -->
<polyline fill="none" stroke="#00c8ff" stroke-width="2" :points="chartPoints"/>
<!-- 数据点 + 标签 -->
<g>
<g v-for="(pi, i) in calcResult.pi_per_tank" :key="'p'+i">
<circle :cx="36 + (i+1)*71.2" :cy="160 - pi*1.3" r="3.5" fill="#00c8ff"/>
<text :x="36 + (i+1)*71.2" :y="160 - pi*1.3 - 7"
fill="#00c8ff" font-size="9" text-anchor="middle">{{ pi }}</text>
</g>
</g>
<!-- 入口零点 -->
<circle cx="36" cy="160" r="2.5" fill="#7a8794"/>
<polyline fill="none" stroke="#00c8ff" stroke-width="2" stroke-dasharray="2 2"
:points="`36,160 ${36+71.2},${160 - calcResult.pi_per_tank[0]*1.3}`"/>
</svg>
<div class="sec-title mt8">各槽酸洗详情</div>
<table class="data-table">
<thead>
<tr><th>酸槽</th><th>停留时间 (s)</th><th>累计PI (%)</th><th>进度</th></tr>
@@ -356,6 +406,17 @@ export default {
acidHistory() {
return this.calibHistory.filter(h => h.model === 'acid_speed').slice(0, 10)
},
speedPct() {
if (!this.calcResult) return 0
const v = this.calcResult.max_speed
return Math.max(0, Math.min(100, ((v - 20) / 160) * 100))
},
chartPoints() {
if (!this.calcResult) return ''
return this.calcResult.pi_per_tank
.map((pi, i) => `${36 + (i+1)*71.2},${160 - pi*1.3}`)
.join(' ')
},
},
async mounted() {
await this.fetchL1Data()
@@ -534,6 +595,54 @@ export default {
border-left: 2px solid $border;
padding-left: 8px;
}
.pi-chart {
width: 100%;
height: 180px;
display: block;
background: rgba(0,0,0,.18);
border: 1px solid $border;
border-radius: 4px;
}
.speed-bar-wrap { padding: 6px 8px 22px; }
.speed-bar-track {
position: relative;
height: 8px;
background: rgba(255,255,255,.06);
border-radius: 4px;
}
.speed-bar-fill {
position: absolute;
left: 0; top: 0; bottom: 0;
background: linear-gradient(90deg, #28a745, #00c8ff);
border-radius: 4px;
transition: width .25s;
}
.speed-bar-tick {
position: absolute;
top: 12px;
font-size: 10px;
color: $text-muted;
font-family: monospace;
}
.speed-bar-marker {
position: absolute;
top: -3px;
width: 2px;
height: 14px;
background: #f0a500;
transform: translateX(-1px);
transition: left .25s;
}
.speed-bar-marker-label {
position: absolute;
top: 16px;
left: 50%;
transform: translateX(-50%);
font-size: 10px;
color: #f0a500;
font-family: monospace;
white-space: nowrap;
}
.calib-predict-row {
display: flex;
align-items: center;