新增app和跟踪页面,现已经调通

This commit is contained in:
2026-05-13 16:43:38 +08:00
parent 5fdaa89afd
commit ba7593e825
10 changed files with 1838 additions and 2 deletions

View File

@@ -0,0 +1,436 @@
<template>
<div class="tracking-wrap">
<!-- 工具栏 -->
<div class="track-toolbar">
<span class="track-title">跟踪管理</span>
<span class="track-update-time" v-if="lastUpdated">更新时间{{ lastUpdated }}</span>
<el-button size="mini" icon="el-icon-refresh" :loading="loading" circle @click="loadData" />
</div>
<div class="tracking-body">
<!-- 流程图 -->
<div class="flow-diagram">
<div class="flow-row">
<div v-for="eq in flowRow1" :key="eq.name" class="flow-eq" :class="eq.cls">
<div class="eq-name">{{ eq.name }}<span class="eq-sub"> / {{ eq.label }}</span></div>
<div v-for="(slot, si) in slotsFor(eq.displayNames)" :key="si"
class="eq-slot" :class="slot.matId ? 'slot-active' : 'slot-empty'">
{{ slot.matId || '' }}
</div>
</div>
</div>
<div class="flow-row flow-row-reverse">
<div v-for="eq in flowRow2" :key="eq.name" class="flow-eq" :class="eq.cls">
<div class="eq-name">{{ eq.name }}<span class="eq-sub"> / {{ eq.label }}</span></div>
<div v-for="(slot, si) in slotsFor(eq.displayNames)" :key="si"
class="eq-slot" :class="slot.matId ? 'slot-active' : 'slot-empty'">
{{ slot.matId || '' }}
</div>
</div>
</div>
<div class="flow-row flow-row-sparse">
<div v-for="eq in flowRow3" :key="eq.name" class="flow-eq" :class="eq.cls" :style="eq.style">
<div class="eq-name">{{ eq.name }}<span class="eq-sub"> / {{ eq.label }}</span></div>
<div v-for="(slot, si) in slotsFor(eq.displayNames)" :key="si"
class="eq-slot" :class="slot.matId ? 'slot-active' : 'slot-empty'">
{{ slot.matId || '' }}
</div>
</div>
</div>
<div class="flow-row">
<div v-for="eq in flowRow4" :key="eq.name" class="flow-eq" :class="eq.cls">
<div class="eq-name">{{ eq.name }}<span class="eq-sub"> / {{ eq.label }}</span></div>
<div v-for="(slot, si) in slotsFor(eq.displayNames)" :key="si"
class="eq-slot" :class="slot.matId ? 'slot-active' : 'slot-empty'">
{{ slot.matId || '' }}
</div>
</div>
</div>
</div>
<!-- 入口跟踪 -->
<div class="section-label">入口跟踪</div>
<div class="entry-wrap">
<div v-for="(lane, li) in entryLanes" :key="li" class="entry-lane">
<div v-for="pos in lane" :key="pos.displayName"
class="entry-card" :class="{ 'has-coil': !!coilAt(pos.displayName) }">
<div class="entry-card-header">{{ pos.label }}</div>
<template v-if="coilAt(pos.displayName)">
<div class="entry-kv"><span class="ek">冷卷号</span><span class="ev">{{ coilAt(pos.displayName).coilid }}</span></div>
<div class="entry-kv"><span class="ek">热卷号</span><span class="ev">{{ coilAt(pos.displayName).hot_coilid }}</span></div>
<div class="entry-kv"><span class="ek">钢种</span><span class="ev">{{ coilAt(pos.displayName).grade }}</span></div>
<div class="entry-kv"><span class="ek">来料厚[mm]</span><span class="ev">{{ coilAt(pos.displayName).entry_thick }}</span></div>
<div class="entry-kv"><span class="ek">成品厚[mm]</span><span class="ev">{{ coilAt(pos.displayName).exit_thick }}</span></div>
<div class="entry-kv"><span class="ek">厚差[mm]</span><span class="ev">{{ coilAt(pos.displayName).min_exit_thick }} / {{ coilAt(pos.displayName).max_exit_thick }}</span></div>
<div class="entry-kv"><span class="ek">来料宽[mm]</span><span class="ev">{{ coilAt(pos.displayName).entry_width }}</span></div>
<div class="entry-kv"><span class="ek">成品宽[mm]</span><span class="ev">{{ coilAt(pos.displayName).exit_width }}</span></div>
<div class="entry-kv"><span class="ek">重量[t]</span><span class="ev">{{ coilAt(pos.displayName).entry_weight }}</span></div>
<div class="entry-kv"><span class="ek">轧制模式</span><span class="ev">{{ coilAt(pos.displayName).roll_mode }}</span></div>
</template>
<div v-else class="entry-empty"></div>
</div>
</div>
</div>
<!-- 详细信息 -->
<div class="section-label">详细信息</div>
<el-table :data="detailTableRows" size="mini" border stripe style="width:100%">
<el-table-column prop="coilid" label="冷卷号" width="130" show-overflow-tooltip />
<el-table-column prop="hot_coilid" label="热卷号" width="130" show-overflow-tooltip />
<el-table-column prop="grade" label="钢种" width="100" />
<el-table-column prop="entry_thick" label="来料厚度" width="80" />
<el-table-column prop="exit_thick" label="产品厚度" width="80" />
<el-table-column prop="max_exit_thick" label="偏差上限" width="80" />
<el-table-column prop="min_exit_thick" label="偏差下限" width="80" />
<el-table-column prop="entry_width" label="来料宽度" width="80" />
<el-table-column prop="exit_width" label="产品宽度" width="80" />
<el-table-column prop="roll_mode" label="轧制模式" width="100" />
<el-table-column prop="park_type" label="包装要求" width="80" />
<el-table-column prop="side_trim" label="切边要求" width="80" />
<el-table-column prop="entry_weight" label="来料重量" width="80" />
<el-table-column prop="split_num" label="分卷数" width="70" />
<el-table-column prop="position" label="位置" width="120" show-overflow-tooltip />
<el-table-column prop="online_date" label="上线时间" width="160" show-overflow-tooltip />
</el-table>
</div>
</div>
</template>
<script>
import { getTrackData } from '@/api/l2/timing'
const DISPLAY_LABEL = {
UC1: '1#上卷小车', UC2: '2#上卷小车',
WEIT1: '1#称重位', WEIT2: '2#称重位',
CR1: '1#地辊', CR2: '2#地辊',
ENC1: '1#倒卷小车', ENC2: '2#倒卷小车',
ENC3: '3#倒卷小车', ENC4: '4#倒卷小车',
CS1: '1#上卷鞍座', CS2: '2#上卷鞍座',
CS3: '3#上卷鞍座', CS4: '4#上卷鞍座',
CS5: '5#上卷鞍座', CS6: '6#上卷鞍座',
CS7: '7#上卷鞍座', CS8: '8#上卷鞍座',
CS9: '9#上卷鞍座', CS10: '10#上卷鞍座',
CS11: '11#上卷鞍座', CS12: '12#上卷鞍座',
CS13: '13#上卷鞍座', CS14: '14#上卷鞍座',
CXL: '出口活套', CXL1: '出口活套1', CXL2: '出口活套2',
CPR: '酸洗段', CPR1: '酸洗段1', CPR2: '酸洗段2',
CEL: '入口活套', CEL1: '入口活套1', CEL2: '入口活套2',
TRIMMER: '圆盘剪',
RINSE: '清洗段',
TLV: '拉矫机', TLV1: '拉矫机1',
TEL: '联机活套', TEL1: '联机活套1', TEL2: '联机活套2',
EXAM: '检查站', EXAMC: '检查站',
MILL: '轧机',
COILER: '卷取机',
EXC: '卸卷小车',
DC0: '交接鞍座',
EXC1: '步进梁1', EXC2: '步进梁2'
}
const ENTRY_LANES = [
[
{ displayName: 'UC1', label: '1#上卷小车' },
{ displayName: 'WEIT1', label: '1#称重位' },
{ displayName: 'CR1', label: '1#地辊' },
{ displayName: 'CS13', label: '13#上卷鞍座' },
{ displayName: 'CS11', label: '11#上卷鞍座' },
{ displayName: 'CS9', label: '9#上卷鞍座' },
{ displayName: 'CS7', label: '7#上卷鞍座' },
{ displayName: 'ENC1', label: '1#倒卷小车' },
{ displayName: 'CS5', label: '5#上卷鞍座' },
{ displayName: 'CS3', label: '3#上卷鞍座' },
{ displayName: 'CS1', label: '1#上卷鞍座' }
],
[
{ displayName: 'UC2', label: '2#上卷小车' },
{ displayName: 'WEIT2', label: '2#称重位' },
{ displayName: 'CR2', label: '2#地辊' },
{ displayName: 'CS14', label: '14#上卷鞍座' },
{ displayName: 'CS12', label: '12#上卷鞍座' },
{ displayName: 'CS10', label: '10#上卷鞍座' },
{ displayName: 'CS8', label: '8#上卷鞍座' },
{ displayName: 'ENC2', label: '2#倒卷小车' },
{ displayName: 'CS6', label: '6#上卷鞍座' },
{ displayName: 'CS4', label: '4#上卷鞍座' },
{ displayName: 'CS2', label: '2#上卷鞍座' }
]
]
const FLOW_ROWS = {
row1: [
{ name: 'CXL', label: '出口活套', displayNames: ['CXL'], cls: 'eq-process' },
{ name: 'CPR', label: '酸洗段', displayNames: ['CPR'], cls: 'eq-process' },
{ name: 'CEL', label: '入口活套', displayNames: ['CEL','CEL1'], cls: 'eq-process' },
{ name: 'UC1', label: '开卷机', displayNames: ['UC1'], cls: 'eq-entry' },
{ name: 'WEIT1', label: '称重位', displayNames: ['WEIT1'], cls: 'eq-entry' },
{ name: 'CR1', label: '地辊', displayNames: ['CR1'], cls: 'eq-entry' }
],
row2: [
{ name: 'TRIMMER', label: '圆盘剪', displayNames: ['TRIMMER'], cls: 'eq-process' },
{ name: 'RINSE', label: '清洗段', displayNames: ['RINSE'], cls: 'eq-process' },
{ name: 'TLV', label: '拉矫机', displayNames: ['TLV'], cls: 'eq-process' },
{ name: 'UC2', label: '开卷机', displayNames: ['UC2'], cls: 'eq-entry' },
{ name: 'WEIT2', label: '称重位', displayNames: ['WEIT2'], cls: 'eq-entry' },
{ name: 'CR2', label: '地辊', displayNames: ['CR2'], cls: 'eq-entry' }
],
row3: [
{ name: 'TEL', label: '联机活套', displayNames: ['TEL'], cls: 'eq-mill', style: 'flex:1' },
{ name: 'EXAM', label: '检查站', displayNames: ['EXAM','EXAMC'], cls: 'eq-exit', style: 'flex:1;margin-left:auto' }
],
row4: [
{ name: 'MILL', label: '轧机', displayNames: ['MILL'], cls: 'eq-mill' },
{ name: 'COILER', label: '卷取机', displayNames: ['COILER'],cls: 'eq-mill' },
{ name: 'EXC', label: '卸卷小车',displayNames: ['EXC'], cls: 'eq-exit' },
{ name: 'DC0', label: '交接鞍座',displayNames: ['DC0'], cls: 'eq-exit' },
{ name: 'EXC1', label: '步进梁1', displayNames: ['EXC1'], cls: 'eq-exit' },
{ name: 'EXC2', label: '步进梁2', displayNames: ['EXC2'], cls: 'eq-exit' }
]
}
export default {
name: 'TrackingView',
data() {
return {
loading: false,
lastUpdated: '',
matMapRows: [],
entryTraceRows: [],
exitTraceRows: [],
pollTimer: null,
entryLanes: ENTRY_LANES,
flowRow1: FLOW_ROWS.row1,
flowRow2: FLOW_ROWS.row2,
flowRow3: FLOW_ROWS.row3,
flowRow4: FLOW_ROWS.row4
}
},
computed: {
matMapByDisplayName() {
const map = {}
for (const row of this.matMapRows) {
const dn = (row.displayname || row.DISPLAYNAME || '').toUpperCase()
if (!dn) continue
if (!map[dn]) map[dn] = []
map[dn].push({
matId: row.matid || row.MATID || null,
l1MapIdx: row.l1mapidx != null ? row.l1mapidx : row.L1MAPIDX
})
}
return map
},
entryByL1MapIdx() {
const map = {}
for (const row of this.entryTraceRows) {
const idx = row.l1mapidx != null ? row.l1mapidx : row.L1MAPIDX
if (idx != null) map[idx] = row
}
return map
},
detailTableRows() {
return this.entryTraceRows.map(row => {
const idx = row.l1mapidx != null ? row.l1mapidx : row.L1MAPIDX
const posRow = this.matMapRows.find(m => (m.l1mapidx != null ? m.l1mapidx : m.L1MAPIDX) === idx)
const dn = posRow ? (posRow.displayname || posRow.DISPLAYNAME || '').toUpperCase() : ''
return {
...row,
position: DISPLAY_LABEL[dn] || dn || ''
}
})
}
},
created() {
this.loadData()
this.pollTimer = setInterval(this.loadData, 5000)
},
beforeDestroy() {
if (this.pollTimer) clearInterval(this.pollTimer)
},
methods: {
async loadData() {
this.loading = true
try {
const res = await getTrackData()
const data = res?.data || {}
this.matMapRows = data.matMap || []
this.entryTraceRows = data.entryTrace || []
this.exitTraceRows = data.exitTrace || []
this.lastUpdated = new Date().toLocaleTimeString()
} catch (_) {}
finally { this.loading = false }
},
coilAt(displayName) {
const slots = this.matMapByDisplayName[displayName]
if (!slots || !slots.length) return null
const slot = slots[0]
if (!slot.matId) return null
return this.entryByL1MapIdx[slot.l1MapIdx] || null
},
slotsFor(displayNames) {
const result = []
for (const dn of displayNames) {
for (const s of (this.matMapByDisplayName[dn] || [])) {
result.push({ matId: s.matId })
}
}
while (result.length < 2) result.push({ matId: null })
return result.slice(0, 2)
}
}
}
</script>
<style scoped>
.tracking-wrap {
display: flex;
flex-direction: column;
height: calc(100vh - 130px);
background: #f5f7fa;
padding: 8px 12px;
box-sizing: border-box;
overflow: hidden;
}
.track-toolbar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
flex-shrink: 0;
}
.track-title {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.track-update-time {
font-size: 12px;
color: #909399;
margin-left: auto;
}
.tracking-body {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 10px;
}
.section-label {
font-size: 13px;
font-weight: 600;
color: #fff;
background: #2c5282;
padding: 4px 12px;
border-radius: 3px;
flex-shrink: 0;
}
/* ─── 流程图 ─── */
.flow-diagram {
background: #b8cde0;
border-radius: 6px;
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 6px;
flex-shrink: 0;
}
.flow-row {
display: flex;
gap: 6px;
align-items: stretch;
}
.flow-row-sparse {
justify-content: space-between;
}
.flow-eq {
min-width: 110px;
flex: 1;
border-radius: 4px;
padding: 6px 8px;
display: flex;
flex-direction: column;
gap: 3px;
}
.eq-name {
font-size: 11px;
font-weight: 600;
color: #1a2a4a;
white-space: nowrap;
}
.eq-sub { font-size: 10px; font-weight: normal; color: #2c4a7a; }
.eq-slot {
font-size: 11px;
font-weight: 600;
border-radius: 3px;
padding: 2px 6px;
text-align: center;
min-height: 20px;
line-height: 20px;
}
.slot-empty { background: #cdd8e8; }
.slot-active { color: #fff; background: #1a4a8a; }
.eq-process { background: #22d3ee44; border: 1px solid #06b6d4; }
.eq-process .slot-active { background: #0891b2; }
.eq-entry { background: #fbbf2444; border: 1px solid #f59e0b; }
.eq-entry .slot-active { background: #b45309; }
.eq-mill { background: #f8717144; border: 1px solid #ef4444; }
.eq-mill .slot-active { background: #dc2626; }
.eq-exit { background: #a3e63544; border: 1px solid #84cc16; }
.eq-exit .slot-active { background: #4d7c0f; }
/* ─── 入口跟踪 ─── */
.entry-wrap {
display: flex;
flex-direction: column;
gap: 6px;
flex-shrink: 0;
}
.entry-lane {
display: flex;
gap: 5px;
overflow-x: auto;
}
.entry-card {
min-width: 108px;
flex: 1;
background: #d9e6f7;
border: 1px solid #b0c8e8;
border-radius: 4px;
padding: 5px 7px;
font-size: 11px;
}
.entry-card.has-coil { background: #c5d8f0; }
.entry-card-header {
font-size: 11px;
font-weight: 600;
color: #2c3e6b;
text-align: center;
margin-bottom: 3px;
padding-bottom: 3px;
border-bottom: 1px solid #b0c8e8;
}
.entry-kv {
display: flex;
justify-content: space-between;
padding: 1px 0;
gap: 2px;
}
.ek { color: #5a6e99; flex-shrink: 0; }
.ev { color: #1a4a8a; font-weight: 500; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.entry-empty { text-align: center; color: #b0bec5; padding: 16px 0; }
</style>