Files
pickling-mes/frontend/src/views/Material.vue

735 lines
37 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="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="card-header">推拉酸洗线 - 物料跟踪总图</div>
<div class="line-body">
<svg viewBox="0 0 1900 305" preserveAspectRatio="xMidYMid meet" class="line-svg">
<rect x="0" y="0" width="1900" height="305" fill="#0a1218" />
<!-- 段位标签带 -->
<g v-for="s in sections" :key="'sec-'+s.name">
<rect :x="s.bandX" y="2" :width="s.bandW" height="20" :fill="s.color" opacity="0.18" rx="3"/>
<rect :x="s.bandX" y="2" :width="s.bandW" height="20" fill="none" :stroke="s.color" stroke-width="1" opacity="0.7" rx="3"/>
<text :x="s.labelX" y="16" text-anchor="middle" font-size="11" 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="40" text-anchor="middle" font-size="10.5" fill="#c8d4e0">{{ eq.label }}</text>
</g>
<!-- 主带钢线 -->
<path d="M 40 185 L 1860 185" stroke="#5a6a75" stroke-width="3" fill="none"/>
<path d="M 40 185 L 1860 185" 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}, 185)`">
<!-- 开卷机 -->
<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}, 185)`">
<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,290)" 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>
<!-- 下半: 跟踪表 | 实时数据 -->
<div class="split-row">
<div class="card split-left">
<div class="card-header">物料跟踪表 <span class="hd-cnt"> {{ equipments.length }} 台设备</span></div>
<div class="card-body" style="padding:0;">
<div class="track-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:78px;">张力/温度</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>
</div>
<div class="card split-right">
<div class="card-header">实时数据 <span class="hd-cnt">{{ rtItems.length }} </span></div>
<div class="card-body sec-body">
<div class="dg">
<div v-for="it in rtItems" :key="it.k" class="dg-item">
<span class="lbl">{{ it.label }}</span>
<span class="vbox">{{ it.val }}</span>
<span v-if="it.unit" class="unit">{{ it.unit }}</span>
</div>
</div>
</div>
</div>
</div>
</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: {
onlinePlans() { return this.plans.filter(p => p.status === 'online' || p.status === 'ready') },
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
if (!eqs.length) return []
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',
x0: e.x, x1: e.x }
} else { cur.x1 = e.x }
})
if (cur) groups.push(cur)
const half = eqs.length > 1 ? (eqs[1].x - eqs[0].x) / 2 : 30
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) : '—' },
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; }
.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: 305px; display: block; }
.split-row { display: grid; grid-template-columns: 1fr 3fr; gap: 10px; align-items: stretch; }
.split-left, .split-right { display: flex; flex-direction: column; min-height: 540px; }
.split-right .card-body { flex: 1; overflow-y: auto; }
.track-scroll { max-height: 640px; overflow: auto; }
.producing-row { display: flex; align-items: center; gap: 10px; padding: 6px 4px 10px; font-size: 12px; border-bottom: 1px dashed $border; margin-bottom: 6px;
.kv-label { color: $text-muted; font-size: 11px; margin-left: 6px; }
.kv-value { color: $sms-highlight; font-weight: 600; }
}
.btn-sm { padding: 2px 10px; font-size: 11px; }
.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; }
.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; }
.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; white-space: nowrap; }
.tracking-table th, .tracking-table td { white-space: nowrap; }
</style>