Files
pickling-mes/frontend/src/views/Material.vue
砂糖 6282c04f35 refactor(frontend): 调整菜单与页面内容,重构物料跟踪页面为实时参数页面
1.  调整侧边栏菜单的路由标题与图标,交换入口跟踪和物料跟踪的配置
2.  重构Material.vue页面:移除原有计划队列和产线跟踪总图,改为按设备分组的实时参数仪表盘
3.  简化页面脚本逻辑,移除多余的API调用和定时器
4.  统一页面样式类名,调整布局样式
2026-07-06 15:31:28 +08:00

451 lines
21 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="param-page">
<!-- 顶部状态条 -->
<div class="status-bar">
<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">
<div class="card-header">
实时参数总览
<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 class="card-body">
<!-- 开卷机 -->
<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 v-show="groups.uncoiler" class="pg-body">
<div class="dg">
<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>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
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', 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:'酸洗段' },
{ 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:'出口段' },
]
export default {
name: 'Material',
data() {
return {
l1Online: false,
now: Date.now(),
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 },
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 },
],
leveler: { gap: 3.50, force: 280, elongation: 0.45 },
recoiler: { tension: 22.4 },
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 },
}
},
computed: {
currentEquipment() {
const n = EQUIPMENTS.length
const idx = Math.max(0, Math.min(n - 1, Math.floor(this.weld.position * n)))
return { ...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')
const avg = (arr, k) => arr.reduce((s, x) => s + x[k], 0) / arr.length
push('at', '酸洗 槽温度均值', fix(avg(this.acid, 'temp'), 1), '°C')
push('al', '酸洗 罐液位均值', fix(avg(this.acid, 'level'), 2), 'm')
push('ac', '酸洗 槽浓度均值', fix(avg(this.acid, 'conc'), 1), 'g/L')
push('ae', '酸洗 槽电导率均值',fix(avg(this.acid, 'cond'), 1), 'mS/cm')
push('atc','酸洗 罐浓度均值', fix(avg(this.acid, 'tank_conc'), 1), 'g/L')
push('ate','酸洗 罐电导率均值',fix(avg(this.acid, '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')
const rtAvg = this.rinse_tank_temp.reduce((s, x) => s + x, 0) / this.rinse_tank_temp.length
push('rt', '漂洗 槽温度均值', fix(rtAvg, 1), '°C')
push('rl', '漂洗 罐液位均值', fix(avg(this.rinse, 'level'), 2), 'm')
push('rc', '漂洗 槽浓度均值', fix(avg(this.rinse, 'conc'), 2), 'g/L')
push('re', '漂洗 槽电导率均值',fix(avg(this.rinse, 'cond'), 2), 'μS/cm')
push('rtc','漂洗 罐浓度均值', fix(avg(this.rinse, 'tank_conc'), 2), 'g/L')
push('rte','漂洗 罐电导率均值',fix(avg(this.rinse, '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: {
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()
// 模拟焊缝前进
if (this.weld.position < 1) {
this.weld.position = Math.min(1, this.weld.position + 0.005)
} else {
this.weld.position = 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)
;['br1','br2','br3'].forEach(k => {
wig(this[k], 'speed', 2); wig(this[k], 'current', 5)
wig(this[k], 'torque', 0.1)
})
this.tension_vfd.forEach(v => { wig(v, 'speed', 2); wig(v, 'current', 5); wig(v, 'torque', 0.1) })
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)
},
beforeDestroy() {
if (this._timer) clearInterval(this._timer)
},
}
</script>
<style lang="scss" scoped>
@import '@/assets/styles/variables';
.param-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; }
.card-body { padding: 10px 14px; }
// ─── 可折叠分组 ───
.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;
}
.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; }
.dg-item .lbl { color: $text-muted; flex: 1; min-width: 140px; }
.dg-item .vbox {
background: $bg-panel; border: 1px solid $border; padding: 1px 8px;
min-width: 70px; text-align: right; font-family: monospace;
color: $sms-teal; border-radius: 2px;
}
.dg-item .unit { color: $text-muted; font-size: 11px; min-width: 44px; }
.btn-xs { padding: 1px 10px; font-size: 11px; }
</style>