refactor(frontend): 调整菜单与页面内容,重构物料跟踪页面为实时参数页面

1.  调整侧边栏菜单的路由标题与图标,交换入口跟踪和物料跟踪的配置
2.  重构Material.vue页面:移除原有计划队列和产线跟踪总图,改为按设备分组的实时参数仪表盘
3.  简化页面脚本逻辑,移除多余的API调用和定时器
4.  统一页面样式类名,调整布局样式
This commit is contained in:
砂糖
2026-07-06 15:31:28 +08:00
parent 2d44bbfb2c
commit 6282c04f35
7 changed files with 14080 additions and 608 deletions

41
.gitignore vendored Normal file
View File

@@ -0,0 +1,41 @@
# ===== OS =====
.DS_Store
Thumbs.db
desktop.ini
# ==== AI / AGENT =====
ai_agent/
.reasonix/
.trae/
.claude/
# ===== IDE / Editor =====
.idea/
.vscode/
*.swp
*.swo
*~
# ===== Python =====
__pycache__/
*.py[cod]
*.egg-info/
dist/
build/
.env
*.egg
venv/
.venv/
# ===== Node / Frontend =====
node_modules/
frontend/dist/
*.log
npm-debug.log*
# ===== Docker =====
docker-compose.override.yml
# ===== Project =====
pickling_mes.db
*.pdf

35
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,35 @@
version: '3.8'
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: pickling_mes
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
backend:
build:
context: ./backend
dockerfile: Dockerfile
command: uvicorn app.main:app --host 0.0.0.0 --port 8000
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql+asyncpg://postgres:password@postgres:5432/pickling_mes
REDIS_URL: redis://redis:6379/0
depends_on:
- postgres
- redis
volumes:
- ./backend:/app
volumes:
postgres_data:

13119
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -20,13 +20,13 @@ const routes = [
path: 'material',
name: 'Material',
component: () => import('@/views/Material.vue'),
meta: { title: '物料跟踪', icon: 'el-icon-box', requiresAuth: true }
meta: { title: '实时参数', icon: 'el-icon-s-data', requiresAuth: true }
},
{
path: 'entry-tracking',
name: 'EntryTracking',
component: () => import('@/views/EntryTracking.vue'),
meta: { title: '入口跟踪', icon: 'el-icon-position', requiresAuth: true }
meta: { title: '物料跟踪', icon: 'el-icon-box', requiresAuth: true }
},
{
path: 'production',

View File

@@ -1,18 +1,300 @@
<template>
<div class="entry-page">
<div class="mat-page">
<!-- 入口跟踪8 个设备工位 + 下方单个上卷鞍座 -->
<div class="card">
<div class="card-header">
入口跟踪
<span class="ch-badge">{{ saddle ? '上卷鞍座:预备生产' : '上卷鞍座:空闲' }}</span>
<span style="margin-left:auto;display:flex;gap:8px;align-items:center;">
<button v-if="saddle" class="btn btn-primary" @click="commit(saddle)">投入生产</button>
<button class="btn btn-outline" @click="refreshAll">刷新</button>
</span>
<!-- 顶部状态条 -->
<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;">{{ saddle ? '鞍座就绪' : '鞍座空闲' }}</span>
</div>
</div>
<div class="entry-grid">
<!-- 当前生产 + 在线计划 -->
<div class="card" style="margin-bottom:8px;">
<div class="card-header">
当前生产
<span class="ch-badge" v-if="producingPlan">生产中</span>
<span class="ch-badge" v-else>无生产计划</span>
<span style="margin-left:auto;display:flex;gap:6px;align-items:center;">
<button v-if="saddle" class="btn btn-primary btn-xs" @click="commitProduction(saddle)">鞍座投入生产</button>
<button class="btn btn-outline btn-xs" @click="refreshAll">刷新</button>
</span>
</div>
<!-- 生产中卷进度 -->
<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">{{ fmt(producingPlan.run_speed, 0) }}<span class="kv-unit"> m/min</span></span>
<span class="kv-label">带头</span><span class="kv-value">{{ fmt(prodLength, 0) }}<span class="kv-unit"> / 2000 m</span></span>
<div class="prog-bar-wrap" style="flex:1;min-width:120px;height:8px;">
<div class="prog-bar-fill" :style="{ width: prodPct + '%', background: prodPct >= 100 ? 'var(--accent-green)' : 'var(--sms-teal)' }"></div>
</div>
<span class="kv-value">{{ prodPct.toFixed(0) }}%</span>
</div>
<!-- 在线计划表 -->
<table class="data-table compact" v-if="onlinePlans.length" style="margin-top:8px;">
<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="openMove(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 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="#f2f5f9" />
<!-- 段位标签带 -->
<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="#303133">{{ eq.label }}</text>
</g>
<!-- 主带钢线 -->
<path d="M 40 185 L 1860 185" stroke="#9aa4b0" stroke-width="3" fill="none"/>
<path d="M 40 185 L 1860 185" stroke="#c03639" stroke-width="1.4" 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="#e9eef4" stroke="#aab4c2" stroke-width="2"/>
<circle r="22" fill="#f2f5f9" stroke="#9aa4b0" stroke-width="1.5"/>
<circle r="8" fill="#cdd6e0" stroke="#9aa4b0" stroke-width="1"/>
<path d="M-38 0 a38 38 0 0 1 76 0" stroke="#c03639" 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="#5b6470">DC-1</text>
</template>
<!-- 九辊矫直机5上4下 -->
<template v-else-if="eq.type==='rolls9'">
<rect x="-44" y="-26" width="88" height="52" fill="#e9eef4" stroke="#aab4c2" 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="#dde4ec" stroke="#8b97a7" stroke-width="1"/>
</g>
<g v-for="i in 4" :key="'b'+i">
<circle :cx="-27 + (i-1)*18" cy="10" r="6" fill="#dde4ec" stroke="#8b97a7" stroke-width="1"/>
</g>
<text y="44" text-anchor="middle" font-size="9" fill="#5b6470">STR-9</text>
</template>
<!-- 切头/切尾剪 -->
<template v-else-if="eq.type==='shear'">
<rect x="-30" y="-26" width="60" height="52" fill="#e9eef4" stroke="#aab4c2" 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="#c03639"/>
<text y="44" text-anchor="middle" font-size="9" fill="#5b6470">{{ 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="#f0e3cf" stroke="#cf9a5a" 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="#b8c0cc">
<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="#b8c0cc">
<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="#c97e2a">{{ 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="#e2eef4" stroke="#5a9ec0" 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="#f4ead2" stroke="#a08030" stroke-width="2" rx="3"/>
<g stroke="#e89426" 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="#e89426">{{ 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="#e9eef4" stroke="#aab4c2" stroke-width="1.5" rx="2"/>
<ellipse cx="0" cy="-12" rx="22" ry="6" fill="#dde4ec" stroke="#8b97a7" stroke-width="1.2"/>
<ellipse cx="0" cy="12" rx="22" ry="6" fill="#dde4ec" stroke="#8b97a7" stroke-width="1.2"/>
<line x1="-22" y1="-12" x2="-22" y2="12" stroke="#9aa4b0" stroke-width="1"/>
<line x1="22" y1="-12" x2="22" y2="12" stroke="#9aa4b0" stroke-width="1"/>
<text y="44" text-anchor="middle" font-size="9" fill="#5b6470">{{ eq.code }}</text>
</template>
<!-- 活套坑 -->
<template v-else-if="eq.type==='loop'">
<rect x="-40" y="-26" width="80" height="58" fill="#e9eef4" stroke="#aab4c2" 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="#c03639" 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="#5b6470">LOOP</text>
</template>
<!-- 三辊张力装置 -->
<template v-else-if="eq.type==='tension3'">
<rect x="-32" y="-26" width="64" height="52" fill="#e9eef4" stroke="#aab4c2" stroke-width="1.5" rx="3"/>
<circle cx="-16" cy="-8" r="8" fill="#dde4ec" stroke="#8b97a7" stroke-width="1.2"/>
<circle cx="16" cy="-8" r="8" fill="#dde4ec" stroke="#8b97a7" stroke-width="1.2"/>
<circle cx="0" cy="12" r="9" fill="#dde4ec" stroke="#8b97a7" stroke-width="1.2"/>
<text y="44" text-anchor="middle" font-size="9" fill="#5b6470">TEN-3</text>
</template>
<!-- 平整机 -->
<template v-else-if="eq.type==='leveler'">
<rect x="-34" y="-26" width="68" height="52" fill="#e9eef4" stroke="#aab4c2" stroke-width="2" rx="2"/>
<circle cx="0" cy="-14" r="11" fill="#aab4c2" stroke="#8b97a7" stroke-width="1.4"/>
<circle cx="0" cy="14" r="11" fill="#aab4c2" stroke="#8b97a7" stroke-width="1.4"/>
<line x1="-28" y1="0" x2="-12" y2="0" stroke="#8b97a7" stroke-width="1"/>
<line x1="12" y1="0" x2="28" y2="0" stroke="#8b97a7" stroke-width="1"/>
<text y="44" text-anchor="middle" font-size="9" fill="#5b6470">SPM</text>
</template>
<!-- 静电涂油机 -->
<template v-else-if="eq.type==='oiler'">
<rect x="-26" y="-26" width="52" height="52" fill="#e9eef4" stroke="#aab4c2" stroke-width="1.5" rx="2"/>
<path d="M 0 -14 L -10 4 L 10 4 Z" fill="#aab4c2" stroke="#8b97a7" stroke-width="1"/>
<g fill="#5a9bd4">
<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="#5b6470">EOL</text>
</template>
<!-- 卷取机 -->
<template v-else-if="eq.type==='recoiler'">
<circle r="38" fill="#e9eef4" stroke="#aab4c2" stroke-width="2"/>
<circle r="22" fill="#f2f5f9" stroke="#9aa4b0" stroke-width="1.5"/>
<circle r="8" fill="#cdd6e0" stroke="#9aa4b0" stroke-width="1"/>
<path d="M-38 0 a38 38 0 0 1 76 0" stroke="#c03639" 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="#5b6470">REC-1</text>
</template>
<!-- 当前设备高亮光环 -->
<circle v-if="eq.k === currentEquipment.k" r="48" fill="none" stroke="#c03639" 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="#ff8c1a" 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="#ff8c1a">
<animate attributeName="fill" values="#ff8c1a;#ff7700;#ff8c1a" dur="0.6s" repeatCount="indefinite"/>
</circle>
<text y="-18" text-anchor="middle" font-size="11" fill="#c03639" font-weight="bold">WELD</text>
</g>
<!-- 图例 -->
<g transform="translate(20,290)" font-size="10" fill="#606266">
<circle cx="6" cy="-3" r="5" fill="#ff8c1a"/>
<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="#c03639" stroke-width="1.5" stroke-dasharray="2 2"/>
<text x="322" y="0">当前设备</text>
<text x="420" y="0" fill="#909399"> 带钢运行方向 </text>
</g>
</svg>
</div>
</div>
<!-- 下半: 入口设备 | 物料跟踪表 -->
<div class="split-row">
<div class="card split-left">
<div class="card-header">
入口设备
<span class="hd-cnt">上卷鞍座 · 2×4 工位</span>
<span v-if="saddle" style="margin-left:auto;">
<button class="btn btn-primary btn-xs" @click="commitProduction(saddle)">投入生产</button>
</span>
</div>
<div class="card-body" style="padding:6px 8px;">
<!-- 8 个设备工位 -->
<div v-for="(row, ri) in equipRows" :key="ri" class="entry-row">
<div
@@ -37,8 +319,7 @@
<div v-if="occupantOf(pos)" class="pos-act"><span class="action-link" @click="openMove(occupantOf(pos))">移动</span></div>
</div>
</div>
<!-- 下方单个上卷鞍座预备生产位投入生产后离开鞍座进入物料跟踪 -->
<!-- 上卷鞍座 -->
<div :class="['pos-cell', 'saddle-cell', { filled: !!saddle }]">
<div class="pos-title saddle-title">上卷鞍座<span class="st-tag">预备生产位</span></div>
<div v-if="saddle" class="saddle-body">
@@ -59,10 +340,48 @@
</div>
</div>
<!-- 入口队列表 -->
<div class="card split-right">
<div class="card-header">物料跟踪表 <span class="hd-cnt"> {{ equipments.length }} 台设备</span></div>
<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">
<div class="card-header">
入口队列
计划列表
<span class="ch-badge">{{ queuePlans.length }} </span>
</div>
<div class="table-scroll">
@@ -87,14 +406,14 @@
<td><span class="action-link" @click="openMove(p)">移动</span></td>
</tr>
<tr v-if="!queuePlans.length">
<td colspan="10" class="td-muted" style="text-align:center;padding:14px;">暂无在线/准备好的计划</td>
<td colspan="10" class="td-muted" style="text-align:center;padding:14px;">暂无计划</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 移动-位置选择弹窗 -->
<!-- 移动弹窗 -->
<div v-if="moveDialog.visible" class="modal-mask" @click.self="moveDialog.visible=false">
<div class="modal-box" style="width:520px;">
<div class="modal-header">
@@ -124,6 +443,7 @@
<script>
import { getPlans, getSaddle, movePlan, commitProducing } from '@/api'
// ─── 常量 ───
const STATUS_LABEL = { ready: '准备好', online: '在线', producing: '生产中', produced: '生产完成' }
const STATUS_BADGE = { ready: 'badge-gray', online: 'badge-green', producing: 'badge-yellow', produced: 'badge-blue' }
const SADDLE = '上卷鞍座'
@@ -132,55 +452,178 @@ const EQUIP_ROWS = [
['2#上卷小车', '2#称重位', '2#地辊', '2#倒卷小车'],
]
const POSITIONS = [...EQUIP_ROWS[0], ...EQUIP_ROWS[1], SADDLE]
// SVG产线设备布局
const LINE_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 = {
'入口段': '#4a7fbe', '酸洗段': '#d98a2a', '清洗段': '#2f93b8',
'烘干段': '#d96a2a', '出口段': '#5ba84e',
}
const TARGET = 2000
function rnd(base, amp) { return base + (Math.random() - 0.5) * amp }
export default {
name: 'EntryTracking',
data() {
return {
plans: [], saddle: null, TARGET, SADDLE,
positions: POSITIONS, equipRows: EQUIP_ROWS,
timer: null, fastTimer: null, moving: false,
l1Online: false,
now: Date.now(),
moving: false,
plans: [],
saddle: null,
moveDialog: { visible: false, plan: null, target: '' },
// 生产模拟数据
prodBase: null,
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,
_saddleTimer: null,
}
},
computed: {
// ─── 计划筛选 ───
onlinePlans() { return this.plans.filter(p => p.status === 'online' && p.on_saddle !== 1) },
queuePlans() {
return this.plans
.filter(p => (p.status === 'online' || p.status === 'ready') && p.on_saddle !== 1)
.sort((a, b) => (a.status === 'online' ? 0 : 1) - (b.status === 'online' ? 0 : 1))
},
producingPlan() { return this.plans.find(p => p.status === 'producing') || null },
// ─── 生产进度 ───
prodLength() {
const p = this.producingPlan
if (!p) return 0
const b = this.prodBase
if (b && b.id === p.id) {
return Math.min(2000, b.len + b.speed / 60 * Math.max(0, (this.now - b.at) / 1000))
}
return p.run_length_m || 0
},
created() {
this.refreshAll()
this.fastTimer = setInterval(this.fetchSaddle, 2000)
this.timer = setInterval(this.fetchPlans, 5000)
prodPct() { return this.producingPlan ? Math.max(0, Math.min(100, this.prodLength / 2000 * 100)) : 0 },
// ─── SVG产线 ───
equipments() {
const n = LINE_EQUIPMENTS.length
const xStart = 50, xEnd = 1850
const step = (xEnd - xStart) / (n - 1)
return LINE_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 }
},
// ─── 入口常量 ───
positions() { return POSITIONS },
equipRows() { return EQUIP_ROWS },
SADDLE() { return SADDLE },
},
beforeDestroy() { clearInterval(this.timer); clearInterval(this.fastTimer) },
methods: {
refreshAll() { this.fetchPlans(); this.fetchSaddle() },
async fetchPlans() {
try {
const res = await getPlans({ page: 1, page_size: 100 })
this.plans = res.data.items || []
} catch (e) { /* ignore */ }
},
async fetchSaddle() {
try {
const res = await getSaddle()
this.saddle = res.data || null
} catch (e) { /* ignore */ }
},
// ─── 通用格式化 ───
fmt(v, n = 2) { return v != null && v !== '' ? Number(v).toFixed(n) : '—' },
fmtTime(t) { return t ? t.slice(0, 16).replace('T', ' ') : '—' },
sectionColor(s) { return SECTION_COLORS[s] || '#9aa8b6' },
statusLabel(s) { return STATUS_LABEL[s] || s || '—' },
badgeOf(s) { return STATUS_BADGE[s] || 'badge-gray' },
progPct(p) { return Math.max(0, Math.min(100, (p.run_length_m || 0) / TARGET * 100)) },
progColor(p) { return this.progPct(p) >= 100 ? 'var(--accent-green)' : 'var(--sms-teal)' },
// ─── 入口工位 ───
occupantOf(pos) {
return this.plans.find(p => p.position === pos && p.on_saddle !== 1) || null
},
occ(pos, key) { const p = this.occupantOf(pos); return p ? (p[key] || '—') : '' },
occNum(pos, key, n) { const p = this.occupantOf(pos); return p && p[key] != null ? Number(p[key]).toFixed(n) : '' },
// ─── 移动弹窗 ───
openMove(plan) { this.moveDialog = { visible: true, plan, target: plan.position || '' } },
async confirmMove() {
const { plan, target } = this.moveDialog
@@ -195,15 +638,118 @@ export default {
this.$message.error(e?.response?.data?.detail || '操作失败')
} finally { this.moving = false }
},
async commit(p) {
// ─── 投入生产(鞍座→产线) ───
async commitProduction(p) {
try {
await commitProducing(p.id)
this.$message.success('已投入生产,转入物料跟踪')
this.$message.success('已投入生产')
this.refreshAll()
} catch (e) {
this.$message.error(e?.response?.data?.detail || '操作失败')
}
},
// ─── 跟踪表行数据 ───
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 = '—', 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
default: /* shear / oiler / loop */ break
}
return { coil, gap, speed, aux }
},
// ─── 数据加载 ───
async refreshAll() { this.fetchPlans(); this.fetchSaddle() },
async fetchPlans() {
try {
const res = await getPlans({ page: 1, page_size: 100 })
this.plans = res.data.items || []
const pp = this.producingPlan
if (pp && pp.cold_coil_no) this.current.coil_no = pp.cold_coil_no
this.prodBase = pp ? { id: pp.id, len: pp.run_length_m || 0, at: Date.now(), speed: pp.run_speed || 0 } : null
} catch (e) { /* ignore */ }
},
async fetchSaddle() {
try {
const res = await getSaddle()
this.saddle = res.data || null
} catch (e) { /* ignore */ }
},
// ─── 仿真Tick ───
tick() {
this.now = Date.now()
const pp = this.producingPlan
if (pp) {
this.weld.position = Math.max(0, Math.min(1, this.prodLength / 2000))
this.current.speed = pp.run_speed || 0
} else {
this.weld.position = 0
this.current.speed = 0
}
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.refreshAll()
this.tick()
this._timer = setInterval(this.tick, 2000)
this._plansTimer = setInterval(this.fetchPlans, 5000)
this._saddleTimer = setInterval(this.fetchSaddle, 2000)
},
beforeDestroy() {
if (this._timer) clearInterval(this._timer)
if (this._plansTimer) clearInterval(this._plansTimer)
if (this._saddleTimer) clearInterval(this._saddleTimer)
},
}
</script>
@@ -211,14 +757,41 @@ export default {
<style lang="scss" scoped>
@import '@/assets/styles/variables';
.entry-page { display: flex; flex-direction: column; gap: 12px; }
.mat-page { display: flex; flex-direction: column; gap: 10px; }
// ── 入口设备网格 ──
.entry-grid { padding: 8px; display: flex; flex-direction: column; gap: 6px; overflow-x: auto; }
.entry-row { display: grid; grid-template-columns: repeat(4, minmax(140px, 1fr)); gap: 6px; }
// ─── 状态条 ───
.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; }
// ─── 产线SVG ───
.line-wrap { padding: 0; }
.line-body { padding: 6px 10px 10px; background: #f2f5f9; }
.line-svg { width: 100%; height: 305px; display: block; }
// ─── 两列布局 ───
.split-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; align-items: stretch; }
.split-left, .split-right { display: flex; flex-direction: column; min-height: 540px; }
.track-scroll { max-height: 680px; 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; }
}
// ─── 入口设备网格 ───
.entry-row { display: grid; grid-template-columns: repeat(4, minmax(130px, 1fr)); gap: 4px; margin-bottom: 4px; }
.pos-cell {
background: $bg-panel; border: 1px solid $border; border-radius: 4px; padding: 4px 8px 4px;
background: $bg-panel; border: 1px solid $border; border-radius: 4px; padding: 4px 6px 4px;
height: 192px; overflow: hidden; display: flex; flex-direction: column;
&.filled { border-color: $sms-teal; background: rgba($sms-teal, .04); }
}
@@ -230,30 +803,47 @@ export default {
width: 100%; border-collapse: collapse; font-size: 10.5px; line-height: 1.3;
td { padding: 0 0 1px; vertical-align: baseline; white-space: nowrap; }
td.k { color: $text-muted; text-align: left; font-size: 10px; padding-right: 6px; }
td.v { color: $sms-teal; text-align: right; font-family: $font-mono; font-weight: 600; width: 100%; }
td.v { color: $sms-teal; text-align: right; font-family: monospace; font-weight: 600; width: 100%; }
}
.pos-act { margin-top: auto; text-align: right; padding-top: 3px; border-top: 1px dashed $border; flex-shrink: 0; }
.pos-act { margin-top: auto; text-align: right; padding-top: 2px; border-top: 1px dashed $border; flex-shrink: 0; }
.pos-empty { flex: 1; display: flex; align-items: center; justify-content: center; color: $text-muted; font-size: 11px; }
// ── 下方单个上卷鞍座(全宽) ──
// ─── 上卷鞍座 ───
.saddle-cell {
height: 132px;
height: 132px; margin-top: 2px;
&.filled { border-color: $accent-yellow; background: rgba($accent-yellow, .05); }
}
.saddle-title { border-bottom-color: rgba($accent-yellow, .4); display: flex; align-items: center; justify-content: center; gap: 8px;
.st-tag { font-size: 9px; color: $accent-yellow; border: 1px solid rgba($accent-yellow, .5); border-radius: 2px; padding: 0 5px; font-weight: 600; } }
.saddle-title {
border-bottom-color: rgba($accent-yellow, .4); display: flex; align-items: center; justify-content: center; gap: 8px;
.st-tag { font-size: 9px; color: $accent-yellow; border: 1px solid rgba($accent-yellow, .5); border-radius: 2px; padding: 0 5px; font-weight: 600; }
}
.saddle-body { flex: 1; display: grid; grid-template-columns: 3fr 2fr; gap: 16px; align-items: center; }
.sb-info { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px 18px;
span { font-size: 12px; color: $text-primary; font-family: $font-mono; font-weight: 600; display: flex; gap: 6px;
span { font-size: 12px; color: $text-primary; font-family: monospace; font-weight: 600; display: flex; gap: 6px;
i { color: $text-muted; font-style: normal; font-family: $font-main; font-weight: 400; min-width: 56px; } }
}
.sb-stage { display: flex; flex-direction: column; align-items: center; gap: 8px; justify-content: center;
border-left: 1px solid $border; padding-left: 16px;
.sb-hint { font-size: 11px; color: $text-muted; text-align: center; } }
border-left: 1px solid $border; padding-left: 16px; }
// ─── 物料跟踪表 ───
.data-table.compact th, .data-table.compact td { padding: 5px 8px; font-size: 11.5px; }
.tracking-table tr.row-active { background: rgba($sms-teal, 0.08); }
.tracking-table tr.row-active td { color: $sms-teal !important; font-weight: 600; }
.tracking-table tr.row-passed td { color: $text-muted; }
.tracking-table tr.row-pending td { color: $text-muted; }
.sec-tag { display: inline-block; font-size: 10.5px; padding: 1px 6px;
border: 1px solid; border-radius: 3px; background: rgba(0,0,0,.02);
font-weight: 600; letter-spacing: 0.5px; white-space: nowrap; }
.tracking-table th, .tracking-table td { white-space: nowrap; }
.hd-cnt { font-size: 11px; color: $text-muted; margin-left: 8px; font-weight: 400; }
// ─── 通用 ───
.btn-sm { padding: 2px 10px; font-size: 11px; }
.btn-xs { padding: 1px 10px; font-size: 11px; }
.action-link { color: $accent-green; cursor: pointer; font-size: 12px; &:hover { text-decoration: underline; } }
// ── 移动弹窗 ──
// ── 移动弹窗 ──
.pos-pick { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
.pick-item {
padding: 9px 12px; font-size: 12px; text-align: center; border-radius: 6px; cursor: pointer;

View File

@@ -71,8 +71,8 @@ const IC = {
const MENU = [
{ path: '/plan', title: '计划管理', icon: IC.plan },
{ path: '/entry-tracking',title: '入口跟踪', icon: IC.material },
{ path: '/material', title: '物料跟踪', icon: IC.material },
{ path: '/entry-tracking',title: '物料跟踪', icon: IC.material },
{ path: '/material', title: '实时参数', icon: IC.material },
{ path: '/production', title: '实绩管理', icon: IC.production },
{ path: '/process-model', title: '工艺段模型', icon: IC.process },
{ path: '/tension-model', title: '张力模型', icon: IC.tension },

View File

@@ -1,11 +1,7 @@
<template>
<div class="mat-page">
<div class="param-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>
@@ -32,299 +28,162 @@
</div>
</div>
<!-- 在线计划队列 + 入口移动 -->
<div class="card" style="margin-bottom:8px;">
<!-- 数据仪表盘按设备/工艺段分组 -->
<div class="card">
<div class="card-header">
在线计划入口队列
<span class="ch-badge">在线 {{ onlinePlans.length }} / 生产中 {{ producingPlan ? 1 : 0 }}</span>
实时参数总览
<span class="hd-cnt">{{ rtItems.length }} </span>
<span style="margin-left:auto;">
<button class="btn btn-primary btn-xs" @click="expanded = !expanded">{{ expanded ? '折叠全部' : '展开全部' }}</button>
</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">{{ fmt(producingPlan.run_speed, 0) }}<span class="kv-unit"> m/min</span></span>
<span class="kv-label">带头</span><span class="kv-value">{{ fmt(prodLength, 0) }}<span class="kv-unit"> / 2000 m</span></span>
<div class="prog-bar-wrap" style="flex:1;min-width:120px;height:8px;">
<div class="prog-bar-fill" :style="{ width: prodPct + '%', background: prodPct >= 100 ? 'var(--accent-green)' : 'var(--sms-teal)' }"></div>
</div>
<span class="kv-value">{{ prodPct.toFixed(0) }}%</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="#f2f5f9" />
<!-- 段位标签带 -->
<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="#303133">{{ eq.label }}</text>
</g>
<!-- 主带钢线 -->
<path d="M 40 185 L 1860 185" stroke="#9aa4b0" stroke-width="3" fill="none"/>
<path d="M 40 185 L 1860 185" stroke="#c03639" stroke-width="1.4" 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)`">
<div class="card-body">
<!-- 开卷机 -->
<template v-if="eq.type==='coiler'">
<circle r="38" fill="#e9eef4" stroke="#aab4c2" stroke-width="2"/>
<circle r="22" fill="#f2f5f9" stroke="#9aa4b0" stroke-width="1.5"/>
<circle r="8" fill="#cdd6e0" stroke="#9aa4b0" stroke-width="1"/>
<path d="M-38 0 a38 38 0 0 1 76 0" stroke="#c03639" 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="#5b6470">DC-1</text>
</template>
<!-- 九辊矫直机5上4下 -->
<template v-else-if="eq.type==='rolls9'">
<rect x="-44" y="-26" width="88" height="52" fill="#e9eef4" stroke="#aab4c2" 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="#dde4ec" stroke="#8b97a7" stroke-width="1"/>
</g>
<g v-for="i in 4" :key="'b'+i">
<circle :cx="-27 + (i-1)*18" cy="10" r="6" fill="#dde4ec" stroke="#8b97a7" stroke-width="1"/>
</g>
<text y="44" text-anchor="middle" font-size="9" fill="#5b6470">STR-9</text>
</template>
<!-- 切头/切尾剪 -->
<template v-else-if="eq.type==='shear'">
<rect x="-30" y="-26" width="60" height="52" fill="#e9eef4" stroke="#aab4c2" 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="#c03639"/>
<text y="44" text-anchor="middle" font-size="9" fill="#5b6470">{{ 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="#f0e3cf" stroke="#cf9a5a" 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="#b8c0cc">
<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="#b8c0cc">
<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="#c97e2a">{{ 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="#e2eef4" stroke="#5a9ec0" 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="#f4ead2" stroke="#a08030" stroke-width="2" rx="3"/>
<g stroke="#e89426" 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="#e89426">{{ 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="#e9eef4" stroke="#aab4c2" stroke-width="1.5" rx="2"/>
<ellipse cx="0" cy="-12" rx="22" ry="6" fill="#dde4ec" stroke="#8b97a7" stroke-width="1.2"/>
<ellipse cx="0" cy="12" rx="22" ry="6" fill="#dde4ec" stroke="#8b97a7" stroke-width="1.2"/>
<line x1="-22" y1="-12" x2="-22" y2="12" stroke="#9aa4b0" stroke-width="1"/>
<line x1="22" y1="-12" x2="22" y2="12" stroke="#9aa4b0" stroke-width="1"/>
<text y="44" text-anchor="middle" font-size="9" fill="#5b6470">{{ eq.code }}</text>
</template>
<!-- 活套坑 -->
<template v-else-if="eq.type==='loop'">
<rect x="-40" y="-26" width="80" height="58" fill="#e9eef4" stroke="#aab4c2" 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="#c03639" 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="#5b6470">LOOP</text>
</template>
<!-- 三辊张力装置 -->
<template v-else-if="eq.type==='tension3'">
<rect x="-32" y="-26" width="64" height="52" fill="#e9eef4" stroke="#aab4c2" stroke-width="1.5" rx="3"/>
<circle cx="-16" cy="-8" r="8" fill="#dde4ec" stroke="#8b97a7" stroke-width="1.2"/>
<circle cx="16" cy="-8" r="8" fill="#dde4ec" stroke="#8b97a7" stroke-width="1.2"/>
<circle cx="0" cy="12" r="9" fill="#dde4ec" stroke="#8b97a7" stroke-width="1.2"/>
<text y="44" text-anchor="middle" font-size="9" fill="#5b6470">TEN-3</text>
</template>
<!-- 平整机 -->
<template v-else-if="eq.type==='leveler'">
<rect x="-34" y="-26" width="68" height="52" fill="#e9eef4" stroke="#aab4c2" stroke-width="2" rx="2"/>
<circle cx="0" cy="-14" r="11" fill="#aab4c2" stroke="#8b97a7" stroke-width="1.4"/>
<circle cx="0" cy="14" r="11" fill="#aab4c2" stroke="#8b97a7" stroke-width="1.4"/>
<line x1="-28" y1="0" x2="-12" y2="0" stroke="#8b97a7" stroke-width="1"/>
<line x1="12" y1="0" x2="28" y2="0" stroke="#8b97a7" stroke-width="1"/>
<text y="44" text-anchor="middle" font-size="9" fill="#5b6470">SPM</text>
</template>
<!-- 静电涂油机 -->
<template v-else-if="eq.type==='oiler'">
<rect x="-26" y="-26" width="52" height="52" fill="#e9eef4" stroke="#aab4c2" stroke-width="1.5" rx="2"/>
<path d="M 0 -14 L -10 4 L 10 4 Z" fill="#aab4c2" stroke="#8b97a7" stroke-width="1"/>
<g fill="#5a9bd4">
<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="#5b6470">EOL</text>
</template>
<!-- 卷取机 -->
<template v-else-if="eq.type==='recoiler'">
<circle r="38" fill="#e9eef4" stroke="#aab4c2" stroke-width="2"/>
<circle r="22" fill="#f2f5f9" stroke="#9aa4b0" stroke-width="1.5"/>
<circle r="8" fill="#cdd6e0" stroke="#9aa4b0" stroke-width="1"/>
<path d="M-38 0 a38 38 0 0 1 76 0" stroke="#c03639" 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="#5b6470">REC-1</text>
</template>
<!-- 当前设备高亮光环 -->
<circle v-if="eq.k === currentEquipment.k" r="48" fill="none" stroke="#c03639" 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="#ff8c1a" 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="#ff8c1a">
<animate attributeName="fill" values="#ff8c1a;#ff7700;#ff8c1a" dur="0.6s" repeatCount="indefinite"/>
</circle>
<text y="-18" text-anchor="middle" font-size="11" fill="#c03639" font-weight="bold">WELD</text>
</g>
<!-- 图例 -->
<g transform="translate(20,290)" font-size="10" fill="#606266">
<circle cx="6" cy="-3" r="5" fill="#ff8c1a"/>
<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="#c03639" stroke-width="1.5" stroke-dasharray="2 2"/>
<text x="322" y="0">当前设备</text>
<text x="420" y="0" fill="#909399"> 带钢运行方向 </text>
</g>
</svg>
<div class="param-group">
<div class="pg-header" @click="toggleGroup('uncoiler')">
<span :class="['pg-arrow', { open: groups.uncoiler }]"></span>
开卷机
<span class="pg-badge">4 </span>
</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 v-show="groups.uncoiler" class="pg-body">
<div class="dg">
<div v-for="it in rtItems" :key="it.k" class="dg-item">
<div v-for="it in filterItems('u_')" :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 class="param-group">
<div class="pg-header" @click="toggleGroup('straightener')">
<span :class="['pg-arrow', { open: groups.straightener }]"></span>
九辊矫直机
<span class="pg-badge">3 </span>
</div>
<div v-show="groups.straightener" class="pg-body">
<div class="dg">
<div v-for="it in filterItems('st_')" :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 class="param-group">
<div class="pg-header" @click="toggleGroup('pinch')">
<span :class="['pg-arrow', { open: groups.pinch }]"></span>
夹送辊1#/2#/3#
<span class="pg-badge">9 </span>
</div>
<div v-show="groups.pinch" class="pg-body">
<div class="dg">
<div v-for="it in filterItems(['br1_','br2_','br3_'])" :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 class="param-group">
<div class="pg-header" @click="toggleGroup('tension')">
<span :class="['pg-arrow', { open: groups.tension }]"></span>
三辊张力装置
<span class="pg-badge">9 </span>
</div>
<div v-show="groups.tension" class="pg-body">
<div class="dg">
<div v-for="it in filterItems('tv')" :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 class="param-group">
<div class="pg-header" @click="toggleGroup('recoiler')">
<span :class="['pg-arrow', { open: groups.recoiler }]"></span>
收卷机
<span class="pg-badge">1 </span>
</div>
<div v-show="groups.recoiler" class="pg-body">
<div class="dg">
<div v-for="it in filterItems('r_t')" :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 class="param-group">
<div class="pg-header" @click="toggleGroup('acid')">
<span :class="['pg-arrow', { open: groups.acid }]"></span>
酸洗段
<span class="pg-badge">12 </span>
</div>
<div v-show="groups.acid" class="pg-body">
<div class="dg">
<div v-for="it in filterItems(['at','al','ac','ae','atc','ate','amp','ams','amc','acl','act','acc'])" :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 class="param-group">
<div class="pg-header" @click="toggleGroup('rinse')">
<span :class="['pg-arrow', { open: groups.rinse }]"></span>
漂洗段
<span class="pg-badge">12 </span>
</div>
<div v-show="groups.rinse" class="pg-body">
<div class="dg">
<div v-for="it in filterItems(['rt','rl','rc','re','rtc','rte','rmp','rms','rmc','rcl','rct','rcc'])" :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 class="param-group">
<div class="pg-header" @click="toggleGroup('leveler')">
<span :class="['pg-arrow', { open: groups.leveler }]"></span>
平整机
<span class="pg-badge">3 </span>
</div>
<div v-show="groups.leveler" class="pg-body">
<div class="dg">
<div v-for="it in filterItems('lv')" :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 class="param-group">
<div class="pg-header" @click="toggleGroup('dryer')">
<span :class="['pg-arrow', { open: groups.dryer }]"></span>
热风烘干段
<span class="pg-badge">3 </span>
</div>
<div v-show="groups.dryer" class="pg-body">
<div class="dg">
<div v-for="it in filterItems('dt')" :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>
@@ -334,17 +193,17 @@
</div>
</div>
</div>
</div>
</template>
<script>
import { getPlans, startProducing, saveRuntime } 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:'uncoiler', label:'开卷机', type:'coiler', section:'入口段' },
{ k:'straightener', label:'九辊矫直机', type:'rolls9', section:'入口段' },
{ k:'crop_shear', label:'切头剪', type:'shear', 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:'酸洗段' },
@@ -363,31 +222,18 @@ const EQUIPMENTS = [
{ k:'recoiler', label:'卷取机', type:'recoiler', code:'REC-1', section:'出口段' },
]
const SECTION_COLORS = {
'入口段': '#4a7fbe',
'酸洗段': '#d98a2a',
'清洗段': '#2f93b8',
'烘干段': '#d96a2a',
'出口段': '#5ba84e',
}
// 默认辊缝值 (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,
now: Date.now(),
prodBase: null, // { id, len, at, speed } 用于客户端平滑外推生产进度
current: { coil_no: '26053552', speed: 95.0 },
prev_coil_no: '26053551',
expanded: true,
groups: {
uncoiler: true, straightener: true, pinch: true, tension: true,
recoiler: true, acid: true, rinse: true, leveler: true, dryer: true,
},
current: { speed: 95.0 },
weld: { position: 0.08 },
uncoiler: { tension: 18.5, speed: 92.0, current: 240, torque: 1.8, diameter: 1450 },
@@ -400,9 +246,8 @@ export default {
{ 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 },
recoiler: { tension: 22.4 },
acid: [
{ temp: 82, conc: 198, level: 0.97, cond: 215, tank_conc: 195, tank_cond: 210 },
@@ -426,61 +271,13 @@ export default {
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.on_saddle !== 1) },
producingPlan() { return this.plans.find(p => p.status === 'producing') || null },
prodLength() {
const p = this.producingPlan
if (!p) return 0
const b = this.prodBase
if (b && b.id === p.id) {
return Math.min(2000, b.len + b.speed / 60 * Math.max(0, (this.now - b.at) / 1000))
}
return p.run_length_m || 0
},
prodPct() { return this.producingPlan ? Math.max(0, Math.min(100, this.prodLength / 2000 * 100)) : 0 },
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 n = EQUIPMENTS.length
const idx = Math.max(0, Math.min(n - 1, Math.floor(this.weld.position * n)))
return { ...this.equipments[idx], idx }
return { ...EQUIPMENTS[idx], idx }
},
rtItems() {
const items = []
@@ -549,132 +346,31 @@ export default {
},
},
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 || []
// 把生产中的卷号同步到产线显示 + 记录进度外推基准
const pp = this.producingPlan
if (pp && pp.cold_coil_no) this.current.coil_no = pp.cold_coil_no
this.prodBase = pp ? { id: pp.id, len: pp.run_length_m || 0, at: Date.now(), speed: pp.run_speed || 0 } : null
// 持久化实时数据快照到在产计划
if (pp) this.persistRuntime(pp)
} catch (e) { /* ignore */ }
},
async persistRuntime(pp) {
try {
const snapshot = {
time: new Date().toISOString(),
coil_no: pp.cold_coil_no || pp.plan_no,
line_speed: Number(this.current.speed.toFixed(1)),
weld_position: Number((this.weld.position * 100).toFixed(1)),
items: this.rtItems.map(it => ({ label: it.label, value: it.val, unit: it.unit })),
}
await saveRuntime(pp.id, snapshot)
} 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 }
toggleGroup(name) { this.groups[name] = !this.groups[name] },
filterItems(prefixes) {
if (typeof prefixes === 'string') prefixes = [prefixes]
return this.rtItems.filter(it => prefixes.some(p => it.k.startsWith(p)))
},
tick() {
this.now = Date.now()
// 焊缝(带头)位置与线速度跟随实际在产卷进度(带头 = run_length / 2000
const pp = this.producingPlan
if (pp) {
this.weld.position = Math.max(0, Math.min(1, this.prodLength / 2000))
this.current.speed = pp.run_speed || 0
// 模拟焊缝前进
if (this.weld.position < 1) {
this.weld.position = Math.min(1, this.weld.position + 0.005)
} else {
this.weld.position = 0
this.current.speed = 0
}
this.current.speed = 92 + Math.random() * 6
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)
wig(this.straightener, 'torque', 0.1)
;['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)
wig(this[k], 'torque', 0.1)
})
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 => {
@@ -696,12 +392,9 @@ export default {
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>
@@ -709,7 +402,7 @@ export default {
<style lang="scss" scoped>
@import '@/assets/styles/variables';
.mat-page { display: flex; flex-direction: column; gap: 10px; }
.param-page { display: flex; flex-direction: column; gap: 10px; }
.status-bar {
display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
@@ -721,25 +414,27 @@ export default {
.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: #f2f5f9; }
.line-svg { width: 100%; height: 305px; display: block; }
.card-body { padding: 10px 14px; }
.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; }
// ─── 可折叠分组 ───
.param-group {
margin-bottom: 2px;
&:last-child { margin-bottom: 0; }
}
.pg-header {
display: flex; align-items: center; gap: 8px;
padding: 5px 8px; font-size: 12px; font-weight: 600;
color: $text-primary; background: $bg-panel;
border: 1px solid $border; border-radius: 4px;
cursor: pointer; user-select: none;
&:hover { background: darken($bg-panel, 2%); }
}
.pg-arrow { font-size: 9px; color: $text-muted; transition: transform .15s; &.open { transform: rotate(90deg); } }
.pg-badge { font-size: 10px; font-weight: 400; color: $text-muted; margin-left: 4px; }
.pg-body {
padding: 6px 8px 10px;
border: 1px solid $border; border-top: none; border-radius: 0 0 4px 4px;
}
.btn-sm { padding: 2px 10px; font-size: 11px; }
.hd-cnt { font-size: 11px; color: $text-muted; margin-left: 8px; font-weight: 400; }
.sec-body { padding: 10px 14px; background: $bg-card; }
.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: $text-primary; padding: 2px 0; }
@@ -751,13 +446,5 @@ export default {
}
.dg-item .unit { color: $text-muted; 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($sms-teal, 0.08); }
.tracking-table tr.row-active td { color: $sms-teal !important; font-weight: 600; }
.tracking-table tr.row-passed td { color: $text-muted; }
.tracking-table tr.row-pending td { color: $text-muted; }
.sec-tag { display: inline-block; font-size: 10.5px; padding: 1px 6px;
border: 1px solid; border-radius: 3px; background: rgba(0,0,0,.02);
font-weight: 600; letter-spacing: 0.5px; white-space: nowrap; }
.tracking-table th, .tracking-table td { white-space: nowrap; }
.btn-xs { padding: 1px 10px; font-size: 11px; }
</style>