feat(material): 物料跟踪页 Tab 化重构 + 入口段移动按钮

- 顶部「在线计划」卡移到入口段 Tab 内,移动按钮跟随
- 跟踪页下半区改为 Tab:入口段 / 酸洗段 / 漂洗段 / 出口段 / 物料跟踪表
- 入口段新增实时参数:开卷机张力、工艺段速度、矫直机/1号夹送辊等
- 酸洗 5 槽 + 漂洗 5 槽 改为 6 列 KPI 网格,单屏可视
- 删除 "(公用)" 描述

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 13:57:19 +08:00
parent 475c169d33
commit 45abd3586b

View File

@@ -32,41 +32,6 @@
</div> </div>
</div> </div>
<!-- 在线计划队列 + 入口移动 -->
<div class="card" style="margin-bottom:8px;">
<div class="card-header">
在线计划入口队列
<span class="ch-badge">在线 {{ onlinePlans.length }} / 生产中 {{ producingPlan ? 1 : 0 }}</span>
<span style="margin-left:auto;font-size:11px;color:var(--text-muted);">点击移动把队列卷推到入口并开始生产</span>
</div>
<div style="padding:8px 14px;">
<div v-if="producingPlan" class="producing-row">
<span class="badge badge-yellow">生产中</span>
<span class="kv-label">冷卷号</span><span class="kv-value">{{ producingPlan.cold_coil_no || producingPlan.plan_no }}</span>
<span class="kv-label">钢种</span><span class="kv-value">{{ producingPlan.steel_grade || '—' }}</span>
<span class="kv-label">规格</span><span class="kv-value">{{ fmt(producingPlan.product_thickness) }}×{{ fmt(producingPlan.product_width, 0) }}</span>
<span class="kv-label">分卷</span><span class="kv-value">{{ producingPlan.split_count || 1 }}</span>
</div>
<table class="data-table compact" v-if="onlinePlans.length">
<thead><tr><th>冷卷号</th><th>钢种</th><th>厚度</th><th>宽度</th><th>分卷</th><th>下达时间</th><th>操作</th></tr></thead>
<tbody>
<tr v-for="p in onlinePlans" :key="p.id">
<td class="td-num">{{ p.cold_coil_no || p.plan_no }}</td>
<td>{{ p.steel_grade || '—' }}</td>
<td class="td-num">{{ fmt(p.product_thickness) }}</td>
<td class="td-num">{{ fmt(p.product_width, 0) }}</td>
<td class="td-num">{{ p.split_count || 1 }}</td>
<td class="td-muted">{{ fmtTime(p.plan_date) }}</td>
<td>
<button class="btn btn-primary btn-sm" :disabled="moving" @click="movePlan(p)">移动 </button>
</td>
</tr>
</tbody>
</table>
<div v-else-if="!producingPlan" class="td-muted" style="text-align:center;padding:10px;font-size:12px;">暂无在线计划</div>
</div>
</div>
<!-- 产线总图 --> <!-- 产线总图 -->
<div class="line-wrap card"> <div class="line-wrap card">
<div class="card-header">推拉酸洗线 - 物料跟踪总图</div> <div class="card-header">推拉酸洗线 - 物料跟踪总图</div>
@@ -267,57 +232,199 @@
</div> </div>
</div> </div>
<!-- 下半: 跟踪表 | 实时数据 --> <!-- 下半: 分段 Tab -->
<div class="split-row"> <div class="card sec-card">
<div class="card split-left"> <div class="tab-bar">
<div class="card-header">物料跟踪表 <span class="hd-cnt"> {{ equipments.length }} 台设备</span></div> <div v-for="t in tabs" :key="t.k" :class="['tab', { active: tab === t.k }]" @click="tab = t.k">
<div class="card-body" style="padding:0;"> {{ t.label }}
<div class="track-scroll"> <span class="tab-cnt">{{ t.count }}</span>
<table class="data-table compact tracking-table"> </div>
<thead> <div class="tab-spacer"></div>
<tr> <span class="hd-cnt">{{ rtItems.length }} 项实时数据</span>
<th style="width:32px;">#</th> </div>
<th>设备</th>
<th style="width:64px;">状态</th> <!-- 入口段 -->
<th>当前钢卷</th> <div v-if="tab === 'entry'" class="pane">
<th style="width:80px;">辊缝 (mm)</th> <div class="pane-grid entry-grid">
<th style="width:78px;">速度</th> <!-- 在线计划 + 移动按钮 -->
<th style="width:78px;">张力/温度</th> <div class="sub-card">
</tr> <div class="sub-header">在线计划 · 入口移动
</thead> <span class="ch-badge">在线 {{ onlinePlans.length }} / 生产中 {{ producingPlan ? 1 : 0 }}</span>
<tbody> </div>
<tr v-for="(eq, i) in equipments" :key="eq.k" <div class="sub-body">
:class="{ 'row-active': eq.k === currentEquipment.k, 'row-passed': i < currentEquipment.idx, 'row-pending': i > currentEquipment.idx }"> <div v-if="producingPlan" class="producing-row">
<td class="td-num">{{ i + 1 }}</td> <span class="badge badge-yellow">生产中</span>
<td>{{ eq.label }}</td> <span class="kv-label">冷卷号</span><span class="kv-value">{{ producingPlan.cold_coil_no || producingPlan.plan_no }}</span>
<td> <span class="kv-label">钢种</span><span class="kv-value">{{ producingPlan.steel_grade || '—' }}</span>
<span v-if="eq.k === currentEquipment.k" class="badge badge-yellow">加工中</span> <span class="kv-label">规格</span><span class="kv-value">{{ fmt(producingPlan.product_thickness) }}×{{ fmt(producingPlan.product_width, 0) }}</span>
<span v-else-if="i < currentEquipment.idx" class="badge badge-blue">已过</span> </div>
<span v-else class="badge badge-gray">待入</span> <table class="data-table compact" v-if="onlinePlans.length">
</td> <thead><tr><th>冷卷号</th><th>钢种</th><th>厚度</th><th>宽度</th><th>分卷</th><th>操作</th></tr></thead>
<td class="td-num">{{ rowOf(eq, i).coil }}</td> <tbody>
<td class="td-num">{{ rowOf(eq, i).gap }}</td> <tr v-for="p in onlinePlans" :key="p.id">
<td class="td-num">{{ rowOf(eq, i).speed }}</td> <td class="td-num">{{ p.cold_coil_no || p.plan_no }}</td>
<td class="td-num">{{ rowOf(eq, i).aux }}</td> <td>{{ p.steel_grade || '—' }}</td>
</tr> <td class="td-num">{{ fmt(p.product_thickness) }}</td>
</tbody> <td class="td-num">{{ fmt(p.product_width, 0) }}</td>
</table> <td class="td-num">{{ p.split_count || 1 }}</td>
<td>
<button class="btn btn-primary btn-sm" :disabled="moving" @click="movePlan(p)">移动 </button>
</td>
</tr>
</tbody>
</table>
<div v-else-if="!producingPlan" class="td-muted empty-row">暂无在线计划</div>
</div>
</div>
<!-- 入口段实时参数 -->
<div class="sub-card">
<div class="sub-header">入口段实时参数</div>
<div class="sub-body">
<div class="kpi-grid">
<div v-for="it in entryItems" :key="it.k" class="kpi">
<div class="kpi-label">{{ it.label }}</div>
<div class="kpi-value">{{ it.val }}<span class="kpi-unit" v-if="it.unit">{{ it.unit }}</span></div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="card split-right"> <!-- 酸洗段 -->
<div class="card-header">实时数据 <span class="hd-cnt">{{ rtItems.length }} </span></div> <div v-if="tab === 'acid'" class="pane">
<div class="card-body sec-body"> <div class="tank-grid">
<div class="dg"> <div v-for="(a, i) in acid" :key="'a'+i" class="sub-card">
<div v-for="it in rtItems" :key="it.k" class="dg-item"> <div class="sub-header acid">{{ i+1 }}# 酸洗槽</div>
<span class="lbl">{{ it.label }}</span> <div class="sub-body">
<span class="vbox">{{ it.val }}</span> <div class="kpi-grid two">
<span v-if="it.unit" class="unit">{{ it.unit }}</span> <div class="kpi"><div class="kpi-label">槽温度</div><div class="kpi-value">{{ fix(a.temp,1) }}<span class="kpi-unit">°C</span></div></div>
<div class="kpi"><div class="kpi-label">罐液位</div><div class="kpi-value">{{ fix(a.level,2) }}<span class="kpi-unit">m</span></div></div>
<div class="kpi"><div class="kpi-label">槽浓度</div><div class="kpi-value">{{ fix(a.conc,1) }}<span class="kpi-unit">g/L</span></div></div>
<div class="kpi"><div class="kpi-label">槽电导率</div><div class="kpi-value">{{ fix(a.cond,1) }}<span class="kpi-unit">mS/cm</span></div></div>
<div class="kpi"><div class="kpi-label">罐浓度</div><div class="kpi-value">{{ fix(a.tank_conc,1) }}<span class="kpi-unit">g/L</span></div></div>
<div class="kpi"><div class="kpi-label">罐电导率</div><div class="kpi-value">{{ fix(a.tank_cond,1) }}<span class="kpi-unit">mS/cm</span></div></div>
</div>
</div>
</div>
<div class="sub-card">
<div class="sub-header">酸雾塔 + 冷凝水罐</div>
<div class="sub-body">
<div class="kpi-grid two">
<div class="kpi"><div class="kpi-label">酸雾塔 PH</div><div class="kpi-value">{{ fix(acid_mist.ph,2) }}</div></div>
<div class="kpi"><div class="kpi-label">变频器频率</div><div class="kpi-value">{{ fix(acid_mist.vfd_speed,1) }}<span class="kpi-unit">Hz</span></div></div>
<div class="kpi"><div class="kpi-label">变频器电流</div><div class="kpi-value">{{ fix(acid_mist.vfd_current,1) }}<span class="kpi-unit">A</span></div></div>
<div class="kpi"><div class="kpi-label">冷凝罐液位</div><div class="kpi-value">{{ fix(acid_cond.level,2) }}<span class="kpi-unit">m</span></div></div>
<div class="kpi"><div class="kpi-label">冷凝罐温度</div><div class="kpi-value">{{ fix(acid_cond.temp,1) }}<span class="kpi-unit">°C</span></div></div>
<div class="kpi"><div class="kpi-label">冷凝电导率</div><div class="kpi-value">{{ fix(acid_cond.cond,1) }}<span class="kpi-unit">μS/cm</span></div></div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- 漂洗段 -->
<div v-if="tab === 'rinse'" class="pane">
<div class="tank-grid">
<div v-for="(r, i) in rinse" :key="'r'+i" class="sub-card">
<div class="sub-header rinse">{{ i+1 }}# 漂洗</div>
<div class="sub-body">
<div class="kpi-grid two">
<div class="kpi"><div class="kpi-label">槽温度</div><div class="kpi-value">{{ fix(rinse_tank_temp[i],1) }}<span class="kpi-unit">°C</span></div></div>
<div class="kpi"><div class="kpi-label">罐液位</div><div class="kpi-value">{{ fix(r.level,2) }}<span class="kpi-unit">m</span></div></div>
<div class="kpi"><div class="kpi-label">槽浓度</div><div class="kpi-value">{{ fix(r.conc,2) }}<span class="kpi-unit">g/L</span></div></div>
<div class="kpi"><div class="kpi-label">槽电导率</div><div class="kpi-value">{{ fix(r.cond,2) }}<span class="kpi-unit">μS/cm</span></div></div>
<div class="kpi"><div class="kpi-label">罐浓度</div><div class="kpi-value">{{ fix(r.tank_conc,2) }}<span class="kpi-unit">g/L</span></div></div>
<div class="kpi"><div class="kpi-label">罐电导率</div><div class="kpi-value">{{ fix(r.tank_cond,2) }}<span class="kpi-unit">μS/cm</span></div></div>
</div>
</div>
</div>
<div class="sub-card">
<div class="sub-header">漂洗酸雾塔 + 冷凝罐 + 烘干</div>
<div class="sub-body">
<div class="kpi-grid two">
<div class="kpi"><div class="kpi-label">雾塔 PH</div><div class="kpi-value">{{ fix(rinse_mist.ph,2) }}</div></div>
<div class="kpi"><div class="kpi-label">变频器频率</div><div class="kpi-value">{{ fix(rinse_mist.vfd_speed,1) }}<span class="kpi-unit">Hz</span></div></div>
<div class="kpi"><div class="kpi-label">变频器电流</div><div class="kpi-value">{{ fix(rinse_mist.vfd_current,1) }}<span class="kpi-unit">A</span></div></div>
<div class="kpi"><div class="kpi-label">冷凝液位</div><div class="kpi-value">{{ fix(rinse_cond.level,2) }}<span class="kpi-unit">m</span></div></div>
<div class="kpi"><div class="kpi-label">冷凝温度</div><div class="kpi-value">{{ fix(rinse_cond.temp,1) }}<span class="kpi-unit">°C</span></div></div>
<div class="kpi"><div class="kpi-label">冷凝电导率</div><div class="kpi-value">{{ fix(rinse_cond.cond,2) }}<span class="kpi-unit">μS/cm</span></div></div>
<div class="kpi"><div class="kpi-label">烘干 1 </div><div class="kpi-value">{{ fix(dryer.t1,0) }}<span class="kpi-unit">°C</span></div></div>
<div class="kpi"><div class="kpi-label">烘干 2 </div><div class="kpi-value">{{ fix(dryer.t2,0) }}<span class="kpi-unit">°C</span></div></div>
<div class="kpi"><div class="kpi-label">烘干 3 </div><div class="kpi-value">{{ fix(dryer.t3,0) }}<span class="kpi-unit">°C</span></div></div>
</div>
</div>
</div>
</div>
</div>
<!-- 出口段 -->
<div v-if="tab === 'exit'" class="pane">
<div class="pane-grid two">
<div class="sub-card">
<div class="sub-header">三辊张力装置</div>
<div class="sub-body">
<table class="data-table compact">
<thead><tr><th>变频器</th><th>速度 m/min</th><th>电流 A</th><th>扭矩 kN·m</th></tr></thead>
<tbody>
<tr v-for="(v, i) in tension_vfd" :key="i">
<td>VFD-{{ i+1 }}</td>
<td class="td-num">{{ fix(v.speed,1) }}</td>
<td class="td-num">{{ fix(v.current,0) }}</td>
<td class="td-num">{{ fix(v.torque,2) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="sub-card">
<div class="sub-header">平整 + 涂油 + 收卷</div>
<div class="sub-body">
<div class="kpi-grid two">
<div class="kpi"><div class="kpi-label">平整辊缝</div><div class="kpi-value">{{ fix(leveler.gap,2) }}<span class="kpi-unit">mm</span></div></div>
<div class="kpi"><div class="kpi-label">轧制力</div><div class="kpi-value">{{ fix(leveler.force,0) }}<span class="kpi-unit">kN</span></div></div>
<div class="kpi"><div class="kpi-label">延伸率</div><div class="kpi-value">{{ fix(leveler.elongation,2) }}<span class="kpi-unit">%</span></div></div>
<div class="kpi"><div class="kpi-label">收卷张力</div><div class="kpi-value">{{ fix(recoiler.tension,1) }}<span class="kpi-unit">kN</span></div></div>
<div class="kpi"><div class="kpi-label">收卷直径</div><div class="kpi-value">{{ fix(recoiler.diameter,0) }}<span class="kpi-unit">mm</span></div></div>
<div class="kpi"><div class="kpi-label">收卷速度</div><div class="kpi-value">{{ fix(recoiler.speed,1) }}<span class="kpi-unit">m/min</span></div></div>
</div>
</div>
</div>
</div>
</div>
<!-- 跟踪表 -->
<div v-if="tab === 'track'" class="pane">
<table class="data-table compact tracking-table">
<thead>
<tr>
<th style="width:32px;">#</th>
<th>设备</th>
<th style="width:64px;">状态</th>
<th>当前钢卷</th>
<th style="width:80px;">辊缝 mm</th>
<th style="width:78px;">速度</th>
<th style="width:80px;">张力/温度</th>
</tr>
</thead>
<tbody>
<tr v-for="(eq, i) in equipments" :key="eq.k"
:class="{ 'row-active': eq.k === currentEquipment.k, 'row-passed': i < currentEquipment.idx, 'row-pending': i > currentEquipment.idx }">
<td class="td-num">{{ i + 1 }}</td>
<td>{{ eq.label }}</td>
<td>
<span v-if="eq.k === currentEquipment.k" class="badge badge-yellow">加工中</span>
<span v-else-if="i < currentEquipment.idx" class="badge badge-blue">已过</span>
<span v-else class="badge badge-gray">待入</span>
</td>
<td class="td-num">{{ rowOf(eq, i).coil }}</td>
<td class="td-num">{{ rowOf(eq, i).gap }}</td>
<td class="td-num">{{ rowOf(eq, i).speed }}</td>
<td class="td-num">{{ rowOf(eq, i).aux }}</td>
</tr>
</tbody>
</table>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -407,9 +514,38 @@ export default {
_plansTimer: null, _plansTimer: null,
plans: [], plans: [],
moving: false, moving: false,
tab: 'entry',
} }
}, },
computed: { computed: {
tabs() {
return [
{ k: 'entry', label: '入口段', count: this.onlinePlans.length },
{ k: 'acid', label: '酸洗段', count: 5 },
{ k: 'rinse', label: '漂洗段', count: 5 },
{ k: 'exit', label: '出口段', count: 3 },
{ k: 'track', label: '物料跟踪表', count: this.equipments.length },
]
},
entryItems() {
const f = (v, n=1) => Number(v).toFixed(n)
return [
{ k:'u_t', label:'开卷机张力', val: f(this.uncoiler.tension, 1), unit:'kN' },
{ k:'p_s', label:'工艺段速度', val: f(this.current.speed, 1), unit:'m/min' },
{ k:'u_s', label:'开卷机速度', val: f(this.uncoiler.speed, 1), unit:'m/min' },
{ k:'u_d', label:'开卷机卷径', val: f(this.uncoiler.diameter, 0),unit:'mm' },
{ k:'u_c', label:'开卷机电流', val: f(this.uncoiler.current, 0), unit:'A' },
{ k:'u_q', label:'开卷机扭矩', val: f(this.uncoiler.torque, 2), unit:'kN·m' },
{ k:'st_s',label:'九辊矫直机 速度', val: f(this.straightener.speed, 1), unit:'m/min' },
{ k:'st_g',label:'九辊矫直机 辊缝', val: f(this.straightener.gap, 2), unit:'mm' },
{ k:'st_c',label:'九辊矫直机 电流', val: f(this.straightener.current, 0),unit:'A' },
{ k:'st_q',label:'九辊矫直机 扭矩', val: f(this.straightener.torque, 2), unit:'kN·m' },
{ k:'b1s', label:'1号夹送辊 速度', val: f(this.br1.speed, 1), unit:'m/min' },
{ k:'b1g', label:'1号夹送辊 辊缝', val: f(this.br1.gap, 2), unit:'mm' },
{ k:'b1c', label:'1号夹送辊 电流', val: f(this.br1.current, 0), unit:'A' },
{ k:'b1q', label:'1号夹送辊 扭矩', val: f(this.br1.torque, 2), unit:'kN·m' },
]
},
onlinePlans() { return this.plans.filter(p => p.status === 'online') }, onlinePlans() { return this.plans.filter(p => p.status === 'online') },
producingPlan() { return this.plans.find(p => p.status === 'producing') || null }, producingPlan() { return this.plans.find(p => p.status === 'producing') || null },
equipments() { equipments() {
@@ -455,7 +591,7 @@ export default {
push('r_t', '收卷机 收卷张力', fix(this.recoiler.tension, 1), 'kN') push('r_t', '收卷机 收卷张力', fix(this.recoiler.tension, 1), 'kN')
this.acid.forEach((a, i) => { this.acid.forEach((a, i) => {
push(`at${i}`, `酸洗${i+1}# 槽/罐温度(公用)`, fix(a.temp, 1), '°C') push(`at${i}`, `酸洗${i+1}# 槽温度`, fix(a.temp, 1), '°C')
push(`al${i}`, `酸洗${i+1}# 罐液位`, fix(a.level, 2), 'm') push(`al${i}`, `酸洗${i+1}# 罐液位`, fix(a.level, 2), 'm')
push(`ac${i}`, `酸洗${i+1}# 槽浓度`, fix(a.conc, 1), 'g/L') push(`ac${i}`, `酸洗${i+1}# 槽浓度`, fix(a.conc, 1), 'g/L')
push(`ae${i}`, `酸洗${i+1}# 槽电导率`, fix(a.cond, 1), 'mS/cm') push(`ae${i}`, `酸洗${i+1}# 槽电导率`, fix(a.cond, 1), 'mS/cm')
@@ -471,7 +607,7 @@ export default {
this.rinse.forEach((r, i) => { this.rinse.forEach((r, i) => {
const t = this.rinse_tank_temp[i] const t = this.rinse_tank_temp[i]
push(`rt${i}`, `漂洗${i+1}# 槽/罐温度(公用)`, fix(t, 1), '°C') push(`rt${i}`, `漂洗${i+1}# 槽温度`, fix(t, 1), '°C')
push(`rl${i}`, `漂洗${i+1}# 罐液位`, fix(r.level, 2), 'm') push(`rl${i}`, `漂洗${i+1}# 罐液位`, fix(r.level, 2), 'm')
push(`rc${i}`, `漂洗${i+1}# 槽浓度`, fix(r.conc, 2), 'g/L') push(`rc${i}`, `漂洗${i+1}# 槽浓度`, fix(r.conc, 2), 'g/L')
push(`re${i}`, `漂洗${i+1}# 槽电导率`, fix(r.cond, 2), 'μS/cm') push(`re${i}`, `漂洗${i+1}# 槽电导率`, fix(r.cond, 2), 'μS/cm')
@@ -498,6 +634,7 @@ export default {
}, },
methods: { methods: {
fmt(v, n = 2) { return v != null && v !== '' ? Number(v).toFixed(n) : '—' }, fmt(v, n = 2) { return v != null && v !== '' ? Number(v).toFixed(n) : '—' },
fix(v, n = 1) { return Number(v).toFixed(n) },
fmtTime(t) { return t ? t.slice(0, 16).replace('T', ' ') : '—' }, fmtTime(t) { return t ? t.slice(0, 16).replace('T', ' ') : '—' },
async loadPlans() { async loadPlans() {
try { try {
@@ -656,31 +793,63 @@ export default {
.line-body { padding: 6px 10px 10px; background: #0a1218; } .line-body { padding: 6px 10px 10px; background: #0a1218; }
.line-svg { width: 100%; height: 280px; display: block; } .line-svg { width: 100%; height: 280px; display: block; }
.split-row { display: grid; grid-template-columns: 1.05fr 1fr; gap: 10px; align-items: stretch; } .sec-card { padding: 0; }
.split-left, .split-right { display: flex; flex-direction: column; min-height: 540px; } .tab-bar {
.split-right .card-body { flex: 1; overflow-y: auto; } display: flex; align-items: center; gap: 2px;
border-bottom: 1px solid $border; padding: 0 10px;
background: #161d24;
}
.tab {
padding: 9px 16px; font-size: 12.5px; cursor: pointer;
color: $text-muted; border-bottom: 2px solid transparent;
user-select: none;
}
.tab:hover { color: #c8d4e0; }
.tab.active { color: $sms-highlight; border-bottom-color: $sms-highlight; font-weight: 600; }
.tab-cnt {
display: inline-block; margin-left: 5px;
background: rgba(255,255,255,.06); border-radius: 9px;
padding: 0 6px; font-size: 10px; color: #9aa8b6;
}
.tab.active .tab-cnt { background: rgba(0,200,255,.15); color: #00c8ff; }
.tab-spacer { flex: 1; }
.track-scroll { max-height: 640px; overflow-y: auto; } .pane { padding: 10px 12px; }
.pane-grid { display: grid; grid-template-columns: 1fr 1.4fr; gap: 10px; }
.pane-grid.two { grid-template-columns: 1fr 1fr; }
.entry-grid { grid-template-columns: 1fr 1.3fr; }
.tank-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; }
.producing-row { display: flex; align-items: center; gap: 10px; padding: 6px 4px 10px; font-size: 12px; border-bottom: 1px dashed $border; margin-bottom: 6px; .sub-card { background: #0f161c; border: 1px solid $border; border-radius: 4px; display: flex; flex-direction: column; }
.kv-label { color: $text-muted; font-size: 11px; margin-left: 6px; } .sub-header {
padding: 6px 10px; font-size: 12px; color: #c8d4e0;
background: #161d24; border-bottom: 1px solid $border;
display: flex; align-items: center; gap: 8px;
}
.sub-header.acid { color: #ffaa44; }
.sub-header.rinse { color: #3aa0c8; }
.sub-body { padding: 8px 10px; flex: 1; }
.kpi-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px 10px; }
.kpi-grid.two { grid-template-columns: repeat(2, 1fr); }
.kpi { display: flex; flex-direction: column; gap: 2px; }
.kpi-label { color: #8b9aab; font-size: 10.5px; }
.kpi-value {
color: #00c8ff; font-family: monospace; font-size: 14px; font-weight: 600;
background: #0a1218; border: 1px solid #2a3540; border-radius: 3px;
padding: 3px 8px; text-align: right;
}
.kpi-unit { color: #6b7c8d; font-size: 10px; font-weight: 400; margin-left: 4px; }
.producing-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
padding: 6px 4px 8px; font-size: 12px;
border-bottom: 1px dashed $border; margin-bottom: 6px;
.kv-label { color: $text-muted; font-size: 11px; margin-left: 4px; }
.kv-value { color: $sms-highlight; font-weight: 600; } .kv-value { color: $sms-highlight; font-weight: 600; }
} }
.empty-row { text-align: center; padding: 10px; font-size: 12px; }
.btn-sm { padding: 2px 10px; font-size: 11px; } .btn-sm { padding: 2px 10px; font-size: 11px; }
.hd-cnt { font-size: 11px; color: #6b7c8d; font-weight: 400; }
.hd-cnt { font-size: 11px; color: #6b7c8d; margin-left: 8px; font-weight: 400; }
.sec-body { padding: 10px 14px; background: #161d24; }
.dg { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 4px 18px; }
.dg-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #c8d4e0; padding: 2px 0; }
.dg-item .lbl { color: #8b9aab; flex: 1; min-width: 140px; }
.dg-item .vbox {
background: #0e1418; border: 1px solid #2a3540; padding: 1px 8px;
min-width: 70px; text-align: right; font-family: monospace;
color: #00c8ff; border-radius: 2px;
}
.dg-item .unit { color: #6b7c8d; font-size: 11px; min-width: 44px; }
.data-table.compact th, .data-table.compact td { padding: 5px 8px; font-size: 11.5px; } .data-table.compact th, .data-table.compact td { padding: 5px 8px; font-size: 11.5px; }
.tracking-table tr.row-active { background: rgba(255, 221, 68, 0.10); } .tracking-table tr.row-active { background: rgba(255, 221, 68, 0.10); }