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:
@@ -176,7 +176,57 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="calcResult.warning" class="warn-box mb12">{{ calcResult.warning }}</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">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>酸槽</th><th>停留时间 (s)</th><th>累计PI (%)</th><th>进度</th></tr>
|
<tr><th>酸槽</th><th>停留时间 (s)</th><th>累计PI (%)</th><th>进度</th></tr>
|
||||||
@@ -356,6 +406,17 @@ export default {
|
|||||||
acidHistory() {
|
acidHistory() {
|
||||||
return this.calibHistory.filter(h => h.model === 'acid_speed').slice(0, 10)
|
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() {
|
async mounted() {
|
||||||
await this.fetchL1Data()
|
await this.fetchL1Data()
|
||||||
@@ -534,6 +595,54 @@ export default {
|
|||||||
border-left: 2px solid $border;
|
border-left: 2px solid $border;
|
||||||
padding-left: 8px;
|
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 {
|
.calib-predict-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user