Files
pickling-mes/frontend/src/views/Material.vue
wangyu 86b2f277e1 refactor(material): 扁平布局 + 修复 SVG 总图 + 彻底消除横向溢出
- SVG: viewBox 改回正向坐标 (0,0,1900,310),所有 y 整体下移 30,
  段位色带在顶部 4-26 区,避免负坐标渲染异常导致设备图形丢失
- 布局: 去掉所有 sub-card 嵌套,扁平为 sec-title-bar + pane(kpi-row auto-fit)
- 酸洗/清洗各槽改为 tank-card 自适应排列;所有表格包 tbl-scroll 防止溢出
- 出口段抛弃表格,改 KPI 网格统一处理三辊 VFD + 平整 + 收卷
- 段位色带 + 标题左边框 + sec-pill 一致着色

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-26 14:15:49 +08:00

879 lines
47 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="mat-page">
<!-- 顶部状态条 -->
<div class="status-bar">
<div class="status-item">
<span class="kv-label">当前卷号</span>
<span class="kv-value">{{ current.coil_no || '—' }}</span>
</div>
<div class="status-item">
<span class="kv-label">工艺段速度</span>
<span class="kv-value">{{ current.speed.toFixed(1) }} <span class="kv-unit">m/min</span></span>
</div>
<div class="status-item">
<span class="kv-label">焊缝位置</span>
<span class="kv-value">{{ (weld.position * 100).toFixed(1) }} <span class="kv-unit">%</span></span>
</div>
<div class="status-item">
<span class="kv-label">当前设备</span>
<span class="kv-value">{{ currentEquipment.label }}</span>
</div>
<div class="status-item">
<span class="kv-label">开卷张力</span>
<span class="kv-value">{{ uncoiler.tension.toFixed(1) }} <span class="kv-unit">kN</span></span>
</div>
<div class="status-item">
<span class="kv-label">收卷张力</span>
<span class="kv-value">{{ recoiler.tension.toFixed(1) }} <span class="kv-unit">kN</span></span>
</div>
<div class="status-item" style="margin-left:auto;">
<span :class="['badge', l1Online ? 'badge-green' : 'badge-yellow']">{{ l1Online ? 'L1 在线' : '模拟数据' }}</span>
<span class="kv-label" style="margin-left:8px;">{{ rtItems.length }} 测点</span>
</div>
</div>
<!-- 产线总图 -->
<div class="line-wrap card">
<div class="card-header">推拉酸洗线 - 物料跟踪总图</div>
<div class="line-body">
<svg viewBox="0 0 1900 310" preserveAspectRatio="xMidYMid meet" class="line-svg">
<rect x="0" y="0" width="1900" height="310" fill="#0a1218" />
<!-- 段位标签带 -->
<g v-for="s in sections" :key="'sec-'+s.name">
<rect :x="s.bandX" y="4" :width="s.bandW" height="22" :fill="s.color" opacity="0.18" rx="3"/>
<rect :x="s.bandX" y="4" :width="s.bandW" height="22" fill="none" :stroke="s.color" stroke-width="1" opacity="0.7" rx="3"/>
<text :x="s.labelX" y="19" text-anchor="middle" font-size="12" font-weight="bold" :fill="s.color"
font-family="Arial,sans-serif">{{ s.name }}</text>
</g>
<!-- 顶部设备标签 -->
<g v-for="eq in equipments" :key="'lab-'+eq.k" font-family="Arial,sans-serif">
<text :x="eq.x" y="44" text-anchor="middle" font-size="10.5" fill="#c8d4e0">{{ eq.label }}</text>
</g>
<!-- 主带钢线 -->
<path d="M 40 190 L 1860 190" stroke="#5a6a75" stroke-width="3" fill="none"/>
<path d="M 40 190 L 1860 190" stroke="#aabbcc" stroke-width="1.2" fill="none" stroke-dasharray="6 10">
<animate attributeName="stroke-dashoffset" from="16" to="0" dur="0.7s" repeatCount="indefinite"/>
</path>
<!-- 各设备图形 -->
<g v-for="eq in equipments" :key="eq.k" :transform="`translate(${eq.x}, 190)`">
<!-- 开卷机 -->
<template v-if="eq.type==='coiler'">
<circle r="38" fill="#1a232c" stroke="#3a4a55" stroke-width="2"/>
<circle r="22" fill="#0a1218" stroke="#5a6a75" stroke-width="1.5"/>
<circle r="8" fill="#2a3a48" stroke="#5a7090" stroke-width="1"/>
<path d="M-38 0 a38 38 0 0 1 76 0" stroke="#00c8ff" stroke-width="1" fill="none" opacity="0.5">
<animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="3s" repeatCount="indefinite"/>
</path>
<text y="58" text-anchor="middle" font-size="10" fill="#b8c4cf">DC-1</text>
</template>
<!-- 九辊矫直机5上4下 -->
<template v-else-if="eq.type==='rolls9'">
<rect x="-44" y="-26" width="88" height="52" fill="#1a232c" stroke="#3a4a55" stroke-width="1.5" rx="3"/>
<g v-for="i in 5" :key="'t'+i">
<circle :cx="-36 + (i-1)*18" cy="-10" r="6" fill="#2a3540" stroke="#7090a8" stroke-width="1"/>
</g>
<g v-for="i in 4" :key="'b'+i">
<circle :cx="-27 + (i-1)*18" cy="10" r="6" fill="#2a3540" stroke="#7090a8" stroke-width="1"/>
</g>
<text y="44" text-anchor="middle" font-size="9" fill="#b8c4cf">STR-9</text>
</template>
<!-- 切头/切尾剪 -->
<template v-else-if="eq.type==='shear'">
<rect x="-30" y="-26" width="60" height="52" fill="#1a232c" stroke="#3a4a55" stroke-width="1.5" rx="2"/>
<line x1="-18" y1="-16" x2="18" y2="16" stroke="#da3633" stroke-width="2.2"/>
<line x1="-18" y1="16" x2="18" y2="-16" stroke="#da3633" stroke-width="2.2"/>
<circle cx="-18" cy="-16" r="3" fill="#da3633"/>
<circle cx="18" cy="-16" r="3" fill="#da3633"/>
<circle cx="0" cy="0" r="3" fill="#ffdd44"/>
<text y="44" text-anchor="middle" font-size="9" fill="#b8c4cf">{{ eq.code }}</text>
</template>
<!-- 酸洗槽 -->
<template v-else-if="eq.type==='acid'">
<path d="M -32 -24 L 32 -24 L 28 26 L -28 26 Z" fill="#3a2a18" stroke="#a06030" stroke-width="2"/>
<path d="M -30 -10 L 30 -10 L 27 24 L -27 24 Z" fill="#ffaa44" opacity="0.55">
<animate attributeName="opacity" values="0.5;0.7;0.5" dur="2.5s" repeatCount="indefinite"/>
</path>
<path d="M -22 -10 q 4 -6 8 0 t 8 0 t 8 0 t 8 0" stroke="#ffd28a" stroke-width="1" fill="none" opacity="0.7"/>
<!-- 蒸汽 -->
<g opacity="0.6">
<circle cx="-12" cy="-30" r="3" fill="#cccccc">
<animate attributeName="cy" values="-30;-46;-30" dur="2s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0.6;0;0.6" dur="2s" repeatCount="indefinite"/>
</circle>
<circle cx="6" cy="-32" r="2.5" fill="#cccccc">
<animate attributeName="cy" values="-32;-50;-32" dur="2.3s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0.5;0;0.5" dur="2.3s" repeatCount="indefinite"/>
</circle>
</g>
<text x="0" y="44" text-anchor="middle" font-size="9" fill="#ffaa44">{{ acid[eq.idx].temp.toFixed(0) }}°C · {{ acid[eq.idx].conc.toFixed(0) }}g/L</text>
</template>
<!-- 漂洗段 -->
<template v-else-if="eq.type==='rinse'">
<path d="M -34 -24 L 34 -24 L 30 26 L -30 26 Z" fill="#142a2e" stroke="#4080a0" stroke-width="2"/>
<path d="M -32 -8 L 32 -8 L 29 24 L -29 24 Z" fill="#3aa0c8" opacity="0.55">
<animate attributeName="opacity" values="0.5;0.7;0.5" dur="2.5s" repeatCount="indefinite"/>
</path>
<path d="M -22 -8 q 4 -5 8 0 t 8 0 t 8 0 t 8 0" stroke="#bce4f0" stroke-width="1" fill="none" opacity="0.7"/>
<text y="44" text-anchor="middle" font-size="9" fill="#3aa0c8">5级逆流</text>
</template>
<!-- 热风烘干段 -->
<template v-else-if="eq.type==='dryer'">
<rect x="-36" y="-26" width="72" height="52" fill="#2a2010" stroke="#a08030" stroke-width="2" rx="3"/>
<g stroke="#ffaa00" stroke-width="1.6" fill="none">
<path d="M -26 -12 q 4 -6 8 0 t 8 0 t 8 0 t 8 0 t 8 0">
<animate attributeName="opacity" values="0.4;1;0.4" dur="1.4s" repeatCount="indefinite"/>
</path>
<path d="M -26 4 q 4 -6 8 0 t 8 0 t 8 0 t 8 0 t 8 0">
<animate attributeName="opacity" values="0.7;0.3;0.7" dur="1.4s" repeatCount="indefinite"/>
</path>
</g>
<text y="44" text-anchor="middle" font-size="9" fill="#ffaa00">{{ dryer.t1.toFixed(0) }}/{{ dryer.t2.toFixed(0) }}/{{ dryer.t3.toFixed(0) }}°C</text>
</template>
<!-- 夹送辊 / 挤干辊 (两辊上下) -->
<template v-else-if="eq.type==='pinch'">
<rect x="-30" y="-26" width="60" height="52" fill="#1a232c" stroke="#3a4a55" stroke-width="1.5" rx="2"/>
<ellipse cx="0" cy="-12" rx="22" ry="6" fill="#2a3540" stroke="#7090a8" stroke-width="1.2"/>
<ellipse cx="0" cy="12" rx="22" ry="6" fill="#2a3540" stroke="#7090a8" stroke-width="1.2"/>
<line x1="-22" y1="-12" x2="-22" y2="12" stroke="#5a6a75" stroke-width="1"/>
<line x1="22" y1="-12" x2="22" y2="12" stroke="#5a6a75" stroke-width="1"/>
<text y="44" text-anchor="middle" font-size="9" fill="#b8c4cf">{{ eq.code }}</text>
</template>
<!-- 活套坑 -->
<template v-else-if="eq.type==='loop'">
<rect x="-40" y="-26" width="80" height="58" fill="#1a232c" stroke="#3a4a55" stroke-width="2" rx="3"/>
<path d="M -32 -16 Q -20 32 -8 -16 Q 4 32 16 -16 Q 28 32 36 -16" stroke="#00c8ff" stroke-width="1.8" fill="none">
<animate attributeName="opacity" values="0.6;1;0.6" dur="1.6s" repeatCount="indefinite"/>
</path>
<text y="48" text-anchor="middle" font-size="9" fill="#b8c4cf">LOOP</text>
</template>
<!-- 三辊张力装置 -->
<template v-else-if="eq.type==='tension3'">
<rect x="-32" y="-26" width="64" height="52" fill="#1a232c" stroke="#3a4a55" stroke-width="1.5" rx="3"/>
<circle cx="-16" cy="-8" r="8" fill="#2a3540" stroke="#7090a8" stroke-width="1.2"/>
<circle cx="16" cy="-8" r="8" fill="#2a3540" stroke="#7090a8" stroke-width="1.2"/>
<circle cx="0" cy="12" r="9" fill="#2a3540" stroke="#7090a8" stroke-width="1.2"/>
<text y="44" text-anchor="middle" font-size="9" fill="#b8c4cf">TEN-3</text>
</template>
<!-- 平整机 -->
<template v-else-if="eq.type==='leveler'">
<rect x="-34" y="-26" width="68" height="52" fill="#1a232c" stroke="#3a4a55" stroke-width="2" rx="2"/>
<circle cx="0" cy="-14" r="11" fill="#3a4a55" stroke="#90a0b0" stroke-width="1.4"/>
<circle cx="0" cy="14" r="11" fill="#3a4a55" stroke="#90a0b0" stroke-width="1.4"/>
<line x1="-28" y1="0" x2="-12" y2="0" stroke="#7090a8" stroke-width="1"/>
<line x1="12" y1="0" x2="28" y2="0" stroke="#7090a8" stroke-width="1"/>
<text y="44" text-anchor="middle" font-size="9" fill="#b8c4cf">SPM</text>
</template>
<!-- 静电涂油机 -->
<template v-else-if="eq.type==='oiler'">
<rect x="-26" y="-26" width="52" height="52" fill="#1a232c" stroke="#3a4a55" stroke-width="1.5" rx="2"/>
<path d="M 0 -14 L -10 4 L 10 4 Z" fill="#3a4a55" stroke="#90a0b0" stroke-width="1"/>
<g fill="#88ccff">
<circle cx="-6" cy="10" r="1.6">
<animate attributeName="cy" values="6;22;6" dur="1.2s" repeatCount="indefinite"/>
</circle>
<circle cx="0" cy="14" r="1.4">
<animate attributeName="cy" values="8;22;8" dur="1.4s" repeatCount="indefinite"/>
</circle>
<circle cx="6" cy="10" r="1.6">
<animate attributeName="cy" values="6;22;6" dur="1.3s" repeatCount="indefinite"/>
</circle>
</g>
<text y="44" text-anchor="middle" font-size="9" fill="#b8c4cf">EOL</text>
</template>
<!-- 卷取机 -->
<template v-else-if="eq.type==='recoiler'">
<circle r="38" fill="#1a232c" stroke="#3a4a55" stroke-width="2"/>
<circle r="22" fill="#0a1218" stroke="#5a6a75" stroke-width="1.5"/>
<circle r="8" fill="#2a3a48" stroke="#5a7090" stroke-width="1"/>
<path d="M-38 0 a38 38 0 0 1 76 0" stroke="#00c8ff" stroke-width="1" fill="none" opacity="0.5">
<animateTransform attributeName="transform" type="rotate" from="360" to="0" dur="3s" repeatCount="indefinite"/>
</path>
<text y="58" text-anchor="middle" font-size="10" fill="#b8c4cf">REC-1</text>
</template>
<!-- 当前设备高亮光环 -->
<circle v-if="eq.k === currentEquipment.k" r="48" fill="none" stroke="#ffdd44" stroke-width="2" stroke-dasharray="4 4" opacity="0.7">
<animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="6s" repeatCount="indefinite"/>
</circle>
</g>
<!-- 焊缝标记 -->
<g :transform="`translate(${weldX}, 190)`">
<circle r="11" fill="#ffdd00" opacity="0.35">
<animate attributeName="r" values="9;22;9" dur="1.0s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0.7;0.05;0.7" dur="1.0s" repeatCount="indefinite"/>
</circle>
<circle r="6" fill="#ffee44">
<animate attributeName="fill" values="#ffee44;#ff7700;#ffee44" dur="0.6s" repeatCount="indefinite"/>
</circle>
<text y="-18" text-anchor="middle" font-size="11" fill="#ffdd44" font-weight="bold">WELD</text>
</g>
<!-- 图例 -->
<g transform="translate(20,295)" font-size="10" fill="#8b949e">
<circle cx="6" cy="-3" r="5" fill="#ffee44"/>
<text x="18" y="0">焊缝位置 {{ (weld.position * 100).toFixed(1) }}%</text>
<rect x="160" y="-7" width="12" height="8" fill="#ffaa44" opacity="0.5"/>
<text x="178" y="0">酸洗液</text>
<rect x="230" y="-7" width="12" height="8" fill="#3aa0c8" opacity="0.5"/>
<text x="248" y="0">漂洗水</text>
<circle cx="310" cy="-3" r="5" fill="none" stroke="#ffdd44" stroke-width="1.5" stroke-dasharray="2 2"/>
<text x="322" y="0">当前设备</text>
<text x="420" y="0" fill="#aabbcc"> 带钢运行方向 </text>
</g>
</svg>
</div>
</div>
<!-- 入口段 -->
<section class="sec">
<div class="sec-title-bar" :style="{ borderLeftColor: sectionColor('入口段') }">
<span class="sec-pill" :style="{ background: sectionColor('入口段') }">入口段</span>
在线 {{ onlinePlans.length }} / 生产中 {{ producingPlan ? 1 : 0 }}
</div>
<div class="pane">
<!-- 在线计划 / 移动 -->
<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>
</div>
<div class="tbl-scroll">
<table class="data-table compact" v-if="onlinePlans.length">
<thead><tr><th>冷卷号</th><th>钢种</th><th>厚度</th><th>宽度</th><th>分卷</th><th style="width:90px;">移动</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><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 class="kpi-row">
<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>
</section>
<!-- 酸洗段 -->
<section class="sec">
<div class="sec-title-bar" :style="{ borderLeftColor: sectionColor('酸洗段') }">
<span class="sec-pill" :style="{ background: sectionColor('酸洗段') }">酸洗段</span>
5 槽串联
</div>
<div class="pane">
<div class="tank-row">
<div v-for="(a, i) in acid" :key="'a'+i" class="tank-card">
<div class="tank-title" :style="{ color: sectionColor('酸洗段') }">{{ i+1 }}# 酸洗槽</div>
<div class="kpi-row two">
<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="kpi-row tight">
<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>
</section>
<!-- 清洗段 + 烘干段 -->
<section class="sec">
<div class="sec-title-bar" :style="{ borderLeftColor: sectionColor('清洗段') }">
<span class="sec-pill" :style="{ background: sectionColor('清洗段') }">清洗段</span>
<span class="sec-pill" :style="{ background: sectionColor('烘干段') }">烘干段</span>
5 级逆流 + 3 段热风
</div>
<div class="pane">
<div class="tank-row">
<div v-for="(r, i) in rinse" :key="'r'+i" class="tank-card">
<div class="tank-title" :style="{ color: sectionColor('清洗段') }">{{ i+1 }}# 漂洗</div>
<div class="kpi-row 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="kpi-row tight">
<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" :style="{ color: sectionColor('烘干段') }">烘干 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" :style="{ color: sectionColor('烘干段') }">烘干 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" :style="{ color: sectionColor('烘干段') }">烘干 3 </div><div class="kpi-value">{{ fix(dryer.t3,0) }}<span class="kpi-unit">°C</span></div></div>
</div>
</div>
</section>
<!-- 出口段 -->
<section class="sec">
<div class="sec-title-bar" :style="{ borderLeftColor: sectionColor('出口段') }">
<span class="sec-pill" :style="{ background: sectionColor('出口段') }">出口段</span>
三辊张力 + 平整 + 涂油 + 收卷
</div>
<div class="pane">
<div class="kpi-row">
<div v-for="(v, i) in tension_vfd" :key="'vfd'+i" class="kpi">
<div class="kpi-label">三辊 VFD-{{ i+1 }} 速度</div>
<div class="kpi-value">{{ fix(v.speed,1) }}<span class="kpi-unit">m/min</span></div>
</div>
<div v-for="(v, i) in tension_vfd" :key="'vfdc'+i" class="kpi">
<div class="kpi-label">三辊 VFD-{{ i+1 }} 电流</div>
<div class="kpi-value">{{ fix(v.current,0) }}<span class="kpi-unit">A</span></div>
</div>
<div v-for="(v, i) in tension_vfd" :key="'vfdq'+i" class="kpi">
<div class="kpi-label">三辊 VFD-{{ i+1 }} 扭矩</div>
<div class="kpi-value">{{ fix(v.torque,2) }}<span class="kpi-unit">kN·m</span></div>
</div>
<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>
</section>
<!-- 跟踪表 -->
<section class="sec">
<div class="sec-title-bar"><span class="sec-pill" style="background:#5a6a78;">物料跟踪表</span> {{ equipments.length }} 台设备</div>
<div class="pane">
<div class="tbl-scroll">
<table class="data-table compact tracking-table">
<thead>
<tr>
<th style="width:32px;">#</th>
<th style="width:72px;"></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>
<span class="sec-tag" :style="{ color: sectionColor(eq.section), borderColor: sectionColor(eq.section) }">{{ eq.section }}</span>
</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>
</section>
</div>
</template>
<script>
import { getPlans, startProducing } from '@/api'
function rnd(base, amp) { return base + (Math.random() - 0.5) * amp }
function fix(v, n = 1) { return Number(v).toFixed(n) }
const EQUIPMENTS = [
{ k:'uncoiler', label:'开卷机', type:'coiler', code:'DC-1', section:'入口段' },
{ k:'straightener', label:'九辊矫直机', type:'rolls9', code:'STR-9', section:'入口段' },
{ k:'crop_shear', label:'切头剪', type:'shear', code:'CRP', section:'入口段' },
{ k:'acid1', label:'酸洗槽1', type:'acid', idx:0, section:'酸洗段' },
{ k:'acid2', label:'酸洗槽2', type:'acid', idx:1, section:'酸洗段' },
{ k:'acid3', label:'酸洗槽3', type:'acid', idx:2, section:'酸洗段' },
{ k:'acid4', label:'酸洗槽4', type:'acid', idx:3, section:'酸洗段' },
{ k:'acid5', label:'酸洗槽5', type:'acid', idx:4, section:'酸洗段' },
{ k:'rinse', label:'漂洗段', type:'rinse', section:'清洗段' },
{ k:'dryer', label:'热风烘干段', type:'dryer', section:'烘干段' },
{ k:'br1', label:'1号夹送辊', type:'pinch', code:'BR-1', section:'出口段' },
{ k:'loop', label:'活套坑', type:'loop', section:'出口段' },
{ k:'br2', label:'2号夹送辊', type:'pinch', code:'BR-2', section:'出口段' },
{ k:'br3', label:'3号夹送辊', type:'pinch', code:'BR-3', section:'出口段' },
{ k:'tension', label:'三辊张力装置', type:'tension3', code:'TEN-3', section:'出口段' },
{ k:'leveler', label:'平整机', type:'leveler', code:'SPM', section:'出口段' },
{ k:'tail_shear', label:'切尾剪', type:'shear', code:'TLS', section:'出口段' },
{ k:'oiler', label:'静电涂油机', type:'oiler', code:'EOL', section:'出口段' },
{ k:'recoiler', label:'卷取机', type:'recoiler', code:'REC-1', section:'出口段' },
]
const SECTION_COLORS = {
'入口段': '#5a8fc8',
'酸洗段': '#ffaa44',
'清洗段': '#3aa0c8',
'烘干段': '#e87a3a',
'出口段': '#88c070',
}
// 默认辊缝值 (mm)
const DEFAULT_GAP = {
straightener: 4.20,
br1: 3.80, br2: 3.80, br3: 3.80,
tension: 4.00,
leveler: 3.50,
}
export default {
name: 'Material',
data() {
return {
l1Online: false,
current: { coil_no: '26053552', speed: 95.0 },
prev_coil_no: '26053551',
weld: { position: 0.08 },
uncoiler: { tension: 18.5, speed: 92.0, current: 240, torque: 1.8, diameter: 1450 },
straightener: { speed: 92.0, current: 165, torque: 1.5, gap: 4.20 },
br1: { speed: 92.0, current: 145, torque: 1.3, gap: 3.80 },
br2: { speed: 92.0, current: 142, torque: 1.3, gap: 3.80 },
br3: { speed: 92.0, current: 140, torque: 1.3, gap: 3.80 },
tension_vfd: [
{ speed: 92.0, current: 158, torque: 1.6 },
{ speed: 92.0, current: 156, torque: 1.5 },
{ speed: 92.0, current: 154, torque: 1.5 },
],
tension_gap: 4.00,
leveler: { gap: 3.50, force: 280, elongation: 0.45 },
recoiler: { tension: 22.4, diameter: 980, speed: 95 },
acid: [
{ temp: 82, conc: 198, level: 0.97, cond: 215, tank_conc: 195, tank_cond: 210 },
{ temp: 81, conc: 188, level: 1.03, cond: 205, tank_conc: 185, tank_cond: 200 },
{ temp: 81, conc: 175, level: 0.94, cond: 192, tank_conc: 172, tank_cond: 188 },
{ temp: 80, conc: 162, level: 0.74, cond: 178, tank_conc: 158, tank_cond: 175 },
{ temp: 74, conc: 148, level: 0.71, cond: 162, tank_conc: 145, tank_cond: 160 },
],
acid_mist: { ph: 6.8, vfd_speed: 48.5, vfd_current: 32.6 },
acid_cond: { level: 1.85, temp: 42.5, cond: 12.5 },
rinse_tank_temp: [65, 62, 58, 54, 48],
rinse: [
{ conc: 0.5, cond: 18.5, level: 0.45, tank_conc: 0.4, tank_cond: 17.5 },
{ conc: 0.3, cond: 12.2, level: 0.54, tank_conc: 0.3, tank_cond: 11.8 },
{ conc: 0.2, cond: 6.8, level: 0.18, tank_conc: 0.2, tank_cond: 6.5 },
{ conc: 0.1, cond: 2.5, level: 0.77, tank_conc: 0.1, tank_cond: 2.4 },
{ conc: 0.0, cond: 0.8, level: 0.81, tank_conc: 0.0, tank_cond: 0.7 },
],
rinse_mist: { ph: 7.0, vfd_speed: 45.2, vfd_current: 28.4 },
rinse_cond: { level: 2.10, temp: 38.6, cond: 4.5 },
dryer: { t1: 145, t2: 168, t3: 152 },
_timer: null,
_plansTimer: null,
plans: [],
moving: false,
}
},
computed: {
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') },
producingPlan() { return this.plans.find(p => p.status === 'producing') || null },
equipments() {
const n = EQUIPMENTS.length
const xStart = 50, xEnd = 1850
const step = (xEnd - xStart) / (n - 1)
return EQUIPMENTS.map((e, i) => ({ ...e, x: xStart + step * i }))
},
sections() {
const eqs = this.equipments
const groups = []
let cur = null
eqs.forEach((e, i) => {
if (!cur || cur.name !== e.section) {
if (cur) groups.push(cur)
cur = { name: e.section, color: SECTION_COLORS[e.section] || '#9aa8b6',
startIdx: i, endIdx: i, x0: e.x, x1: e.x }
} else {
cur.endIdx = i
cur.x1 = e.x
}
})
if (cur) groups.push(cur)
const half = (eqs[1].x - eqs[0].x) / 2
return groups.map(g => ({
...g,
bandX: g.x0 - half + 4,
bandW: (g.x1 - g.x0) + half * 2 - 8,
labelX: (g.x0 + g.x1) / 2,
}))
},
weldX() {
const p = Math.max(0, Math.min(1, this.weld.position))
return 50 + (1850 - 50) * p
},
currentEquipment() {
const n = this.equipments.length
const idx = Math.max(0, Math.min(n - 1, Math.floor(this.weld.position * n)))
return { ...this.equipments[idx], idx }
},
rtItems() {
const items = []
const push = (k, label, val, unit) => items.push({ k, label, val, unit })
push('u_t', '开卷机 开卷张力', fix(this.uncoiler.tension, 1), 'kN')
push('u_s', '开卷机 速度反馈', fix(this.uncoiler.speed, 1), 'm/min')
push('u_c', '开卷机 电流反馈', fix(this.uncoiler.current, 0), 'A')
push('u_q', '开卷机 扭矩反馈', fix(this.uncoiler.torque, 2), 'kN·m')
push('st_s', '九辊矫直机 速度反馈',fix(this.straightener.speed, 1), 'm/min')
push('st_c', '九辊矫直机 电流反馈',fix(this.straightener.current, 0), 'A')
push('st_q', '九辊矫直机 扭矩反馈',fix(this.straightener.torque, 2), 'kN·m')
for (const [k, name] of [['br1','1号夹送辊'], ['br2','2号夹送辊'], ['br3','3号夹送辊']]) {
push(k+'_s', `${name} 速度反馈`, fix(this[k].speed, 1), 'm/min')
push(k+'_c', `${name} 电流反馈`, fix(this[k].current, 0), 'A')
push(k+'_q', `${name} 扭矩反馈`, fix(this[k].torque, 2), 'kN·m')
}
this.tension_vfd.forEach((v, i) => {
push(`tv${i}s`, `三辊张力 变频器${i+1} 速度反馈`, fix(v.speed, 1), 'm/min')
push(`tv${i}c`, `三辊张力 变频器${i+1} 电流反馈`, fix(v.current, 0), 'A')
push(`tv${i}q`, `三辊张力 变频器${i+1} 扭矩反馈`, fix(v.torque, 2), 'kN·m')
})
push('r_t', '收卷机 收卷张力', fix(this.recoiler.tension, 1), 'kN')
this.acid.forEach((a, i) => {
push(`at${i}`, `酸洗${i+1}# 槽温度`, fix(a.temp, 1), '°C')
push(`al${i}`, `酸洗${i+1}# 罐液位`, fix(a.level, 2), 'm')
push(`ac${i}`, `酸洗${i+1}# 槽浓度`, fix(a.conc, 1), 'g/L')
push(`ae${i}`, `酸洗${i+1}# 槽电导率`, fix(a.cond, 1), 'mS/cm')
push(`atc${i}`,`酸洗${i+1}# 罐浓度`, fix(a.tank_conc, 1), 'g/L')
push(`ate${i}`,`酸洗${i+1}# 罐电导率`, fix(a.tank_cond, 1), 'mS/cm')
})
push('amp', '酸雾塔 PH', fix(this.acid_mist.ph, 2), '')
push('ams', '酸雾塔 变频器频率', fix(this.acid_mist.vfd_speed, 1), 'Hz')
push('amc', '酸雾塔 变频器电流', fix(this.acid_mist.vfd_current,1),'A')
push('acl', '酸侧冷凝水罐 液位', fix(this.acid_cond.level, 2), 'm')
push('act', '酸侧冷凝水罐 温度', fix(this.acid_cond.temp, 1), '°C')
push('acc', '酸侧冷凝水罐 电导率', fix(this.acid_cond.cond, 1), 'μS/cm')
this.rinse.forEach((r, i) => {
const t = this.rinse_tank_temp[i]
push(`rt${i}`, `漂洗${i+1}# 槽温度`, fix(t, 1), '°C')
push(`rl${i}`, `漂洗${i+1}# 罐液位`, fix(r.level, 2), 'm')
push(`rc${i}`, `漂洗${i+1}# 槽浓度`, fix(r.conc, 2), 'g/L')
push(`re${i}`, `漂洗${i+1}# 槽电导率`, fix(r.cond, 2), 'μS/cm')
push(`rtc${i}`,`漂洗${i+1}# 罐浓度`, fix(r.tank_conc, 2), 'g/L')
push(`rte${i}`,`漂洗${i+1}# 罐电导率`, fix(r.tank_cond, 2), 'μS/cm')
})
push('rmp', '漂洗酸雾塔 PH', fix(this.rinse_mist.ph, 2), '')
push('rms', '漂洗酸雾塔 变频器频率', fix(this.rinse_mist.vfd_speed, 1), 'Hz')
push('rmc', '漂洗酸雾塔 变频器电流', fix(this.rinse_mist.vfd_current,1),'A')
push('rcl', '漂洗冷凝水罐 液位', fix(this.rinse_cond.level, 2), 'm')
push('rct', '漂洗冷凝水罐 温度', fix(this.rinse_cond.temp, 1), '°C')
push('rcc', '漂洗冷凝水罐 电导率', fix(this.rinse_cond.cond, 2), 'μS/cm')
push('lvg', '平整机 辊缝', fix(this.leveler.gap, 2), 'mm')
push('lvf', '平整机 轧制力', fix(this.leveler.force, 0), 'kN')
push('lve', '平整机 延伸率', fix(this.leveler.elongation,2), '%')
push('dt1','烘干1段温度', fix(this.dryer.t1, 0), '°C')
push('dt2','烘干2段温度', fix(this.dryer.t2, 0), '°C')
push('dt3','烘干3段温度', fix(this.dryer.t3, 0), '°C')
return items
},
},
methods: {
fmt(v, n = 2) { return v != null && v !== '' ? Number(v).toFixed(n) : '—' },
fix(v, n = 1) { return Number(v).toFixed(n) },
sectionColor(s) { return SECTION_COLORS[s] || '#9aa8b6' },
fmtTime(t) { return t ? t.slice(0, 16).replace('T', ' ') : '—' },
async loadPlans() {
try {
const res = await getPlans({ page: 1, page_size: 50 })
this.plans = res.data.items || []
// 把生产中的卷号同步到产线显示
if (this.producingPlan && this.producingPlan.cold_coil_no) {
this.current.coil_no = this.producingPlan.cold_coil_no
}
} catch (e) { /* ignore */ }
},
async movePlan(p) {
if (this.moving) return
this.moving = true
try {
await startProducing(p.id)
this.$message && this.$message.success(`已开始生产 ${p.cold_coil_no || p.plan_no}`)
await this.loadPlans()
} catch (e) {
this.$message && this.$message.error('移动失败')
} finally {
this.moving = false
}
},
// 一行的展示数据:根据设备状态决定卷号/速度/辊缝/辅助列
rowOf(eq, i) {
const curIdx = this.currentEquipment.idx
const isHere = i === curIdx
const passed = i < curIdx
const cur = this.current.coil_no || '—'
const prev = this.prev_coil_no || '—'
let coil = '—'
if (isHere) coil = cur
else if (passed) coil = cur // 已被本卷穿过
else coil = prev // 还在上一卷尾部
const speed = (isHere || passed) ? this.current.speed.toFixed(1) : prev !== '—' ? '0.0' : '—'
let gap = '—'
let aux = '—'
switch (eq.type) {
case 'coiler':
gap = '—'
aux = this.uncoiler.tension.toFixed(1) + ' kN'
break
case 'recoiler':
gap = '—'
aux = this.recoiler.tension.toFixed(1) + ' kN'
break
case 'rolls9':
gap = this.straightener.gap.toFixed(2)
aux = this.straightener.torque.toFixed(2) + ' kN·m'
break
case 'pinch':
gap = this[eq.k].gap.toFixed(2)
aux = this[eq.k].torque.toFixed(2) + ' kN·m'
break
case 'tension3':
gap = this.tension_gap.toFixed(2)
aux = this.tension_vfd[0].torque.toFixed(2) + ' kN·m'
break
case 'leveler':
gap = this.leveler.gap.toFixed(2)
aux = this.leveler.force.toFixed(0) + ' kN'
break
case 'acid':
gap = '—'
aux = this.acid[eq.idx].temp.toFixed(1) + ' °C'
break
case 'rinse':
gap = '—'
aux = this.rinse_tank_temp[0].toFixed(1) + ' °C'
break
case 'dryer':
gap = '—'
aux = this.dryer.t2.toFixed(0) + ' °C'
break
case 'shear':
case 'oiler':
case 'loop':
gap = '—'
aux = '—'
break
}
return { coil, gap, speed, aux }
},
tick() {
this.weld.position = (this.weld.position + 0.012) % 1
// 新一卷开始时滚动卷号
if (this.weld.position < 0.012) {
this.prev_coil_no = this.current.coil_no
const n = parseInt(this.current.coil_no || '26053552', 10) + 1
this.current.coil_no = String(n)
}
this.current.speed = Math.max(0, rnd(this.current.speed, 4))
const wig = (o, key, amp) => { o[key] = rnd(o[key], amp) }
wig(this.uncoiler, 'tension', 0.4); wig(this.uncoiler, 'speed', 2)
wig(this.uncoiler, 'current', 6); wig(this.uncoiler, 'torque', 0.1)
wig(this.straightener, 'speed', 2); wig(this.straightener, 'current', 5)
wig(this.straightener, 'torque', 0.1); wig(this.straightener, 'gap', 0.01)
;['br1','br2','br3'].forEach(k => {
wig(this[k], 'speed', 2); wig(this[k], 'current', 5)
wig(this[k], 'torque', 0.1); wig(this[k], 'gap', 0.01)
})
this.tension_vfd.forEach(v => { wig(v, 'speed', 2); wig(v, 'current', 5); wig(v, 'torque', 0.1) })
this.tension_gap = rnd(this.tension_gap, 0.01)
wig(this.leveler, 'gap', 0.005); wig(this.leveler, 'force', 8); wig(this.leveler, 'elongation', 0.02)
wig(this.recoiler, 'tension', 0.4)
this.acid.forEach(a => {
wig(a, 'temp', 0.3); wig(a, 'conc', 1); wig(a, 'cond', 0.8); wig(a, 'level', 0.02)
wig(a, 'tank_conc', 1); wig(a, 'tank_cond', 0.8)
})
wig(this.acid_mist, 'ph', 0.05); wig(this.acid_mist, 'vfd_speed', 0.6); wig(this.acid_mist, 'vfd_current', 0.4)
wig(this.acid_cond, 'level', 0.02); wig(this.acid_cond, 'temp', 0.3); wig(this.acid_cond, 'cond', 0.2)
this.rinse.forEach(r => {
wig(r, 'conc', 0.05); wig(r, 'cond', 0.3); wig(r, 'level', 0.02)
wig(r, 'tank_conc', 0.05); wig(r, 'tank_cond', 0.3)
})
for (let i = 0; i < this.rinse_tank_temp.length; i++) this.rinse_tank_temp[i] = rnd(this.rinse_tank_temp[i], 0.4)
wig(this.rinse_mist, 'ph', 0.05); wig(this.rinse_mist, 'vfd_speed', 0.6); wig(this.rinse_mist, 'vfd_current', 0.4)
wig(this.rinse_cond, 'level', 0.02); wig(this.rinse_cond, 'temp', 0.3); wig(this.rinse_cond, 'cond', 0.1)
wig(this.dryer, 't1', 2); wig(this.dryer, 't2', 2); wig(this.dryer, 't3', 2)
},
},
created() {
this.tick()
this._timer = setInterval(this.tick, 2000)
this.loadPlans()
this._plansTimer = setInterval(this.loadPlans, 10000)
},
beforeDestroy() {
if (this._timer) clearInterval(this._timer)
if (this._plansTimer) clearInterval(this._plansTimer)
},
}
</script>
<style lang="scss" scoped>
@import '@/assets/styles/variables';
.mat-page { display: flex; flex-direction: column; gap: 10px; min-width: 0; overflow-x: hidden; }
.sec-tag {
display: inline-block; font-size: 10.5px; padding: 1px 6px;
border: 1px solid; border-radius: 3px; background: rgba(0,0,0,.25);
font-weight: 600; letter-spacing: 0.5px;
}
.status-bar {
display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
padding: 8px 16px;
background: $bg-card; border: 1px solid $border; border-radius: 6px;
}
.status-item { display: flex; align-items: center; gap: 6px; font-size: 12px; }
.status-item .kv-label { color: $text-muted; font-size: 11px; }
.status-item .kv-value { color: $sms-highlight; font-weight: 600; }
.status-item .kv-unit { color: $text-muted; font-size: 10px; margin-left: 2px; }
.line-wrap { padding: 0; }
.line-body { padding: 6px 10px 10px; background: #0a1218; }
.line-svg { width: 100%; height: 310px; display: block; }
.sec {
background: $bg-card; border: 1px solid $border; border-radius: 6px;
overflow: hidden; min-width: 0;
}
.sec-title-bar {
display: flex; align-items: center; gap: 8px;
padding: 7px 12px; font-size: 12px; color: $text-muted; font-weight: 500;
background: #161d24; border-bottom: 1px solid $border;
border-left: 3px solid transparent;
}
.sec-pill {
display: inline-block; padding: 2px 10px;
font-size: 11.5px; font-weight: 700; color: #0a1218;
border-radius: 3px; letter-spacing: 1px;
}
.pane { padding: 10px 12px; min-width: 0; }
.kpi-row {
display: grid; gap: 6px 8px;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.kpi-row.two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.kpi-row.tight {
margin-top: 8px; padding-top: 8px;
border-top: 1px dashed $border;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.kpi { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.kpi-label { color: #8b9aab; font-size: 10.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.kpi-value {
color: #00c8ff; font-family: monospace; font-size: 13.5px; font-weight: 600;
background: #0a1218; border: 1px solid #2a3540; border-radius: 3px;
padding: 3px 8px; text-align: right;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.kpi-unit { color: #6b7c8d; font-size: 10px; font-weight: 400; margin-left: 4px; }
.tank-row {
display: grid; gap: 8px;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
margin-bottom: 4px;
}
.tank-card { background: #0f161c; border: 1px solid $border; border-radius: 4px; padding: 6px 8px; min-width: 0; }
.tank-title { font-size: 11.5px; font-weight: 600; margin-bottom: 5px; }
.tbl-scroll { width: 100%; overflow-x: auto; margin-bottom: 8px; }
.tbl-scroll:last-child { margin-bottom: 0; }
.producing-row {
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
padding: 4px 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; }
}
.empty-row { text-align: center; padding: 10px; font-size: 12px; }
.btn-sm { padding: 2px 10px; font-size: 11px; }
.hd-cnt { font-size: 11px; color: #6b7c8d; font-weight: 400; }
.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 td { color: #ffdd44 !important; font-weight: 600; }
.tracking-table tr.row-passed td { color: #6b8aaa; }
.tracking-table tr.row-pending td { color: #5a6a78; }
</style>