2511 lines
82 KiB
Vue
2511 lines
82 KiB
Vue
<template>
|
||
<div class="track-container">
|
||
<el-row :gutter="20">
|
||
<!-- 左侧:设备列表 -->
|
||
<el-col :span="16">
|
||
<!-- Loading Status / 加载状态 -->
|
||
<div v-if="isLoading" class="loading-container">
|
||
<el-icon class="is-loading"><i class="el-icon-loading"></i></el-icon>
|
||
<span>Loading...</span>
|
||
<!-- 加载中... -->
|
||
</div>
|
||
|
||
<!-- WebSocket Connection Status Indicator / WebSocket 连接状态指示 -->
|
||
<div v-if="!isLoading" class="ws-status-bar">
|
||
<el-tooltip content="Measurement Data" placement="top">
|
||
<!-- 测量数据 -->
|
||
<el-badge :is-dot="true" :type="socketStatus.measure ? 'success' : 'danger'">
|
||
<i class="el-icon-data-analysis"></i>
|
||
</el-badge>
|
||
</el-tooltip>
|
||
<el-tooltip content="Position Tracking" placement="top">
|
||
<!-- 位置追踪 -->
|
||
<el-badge :is-dot="true" :type="socketStatus.position ? 'success' : 'danger'">
|
||
<i class="el-icon-location"></i>
|
||
</el-badge>
|
||
</el-tooltip>
|
||
<el-tooltip content="Operation Signal" placement="top">
|
||
<!-- 操作信号 -->
|
||
<el-badge :is-dot="true" :type="socketStatus.signal ? 'success' : 'danger'">
|
||
<i class="el-icon-bell"></i>
|
||
</el-badge>
|
||
</el-tooltip>
|
||
<el-tooltip content="Material Mapping" placement="top">
|
||
<!-- 物料映射 -->
|
||
<el-badge :is-dot="true" :type="socketStatus.matmap ? 'success' : 'danger'">
|
||
<i class="el-icon-map-location"></i>
|
||
</el-badge>
|
||
</el-tooltip>
|
||
<el-tooltip content="Calculation Result" placement="top">
|
||
<!-- 计算结果 -->
|
||
<el-badge :is-dot="true" :type="socketStatus.calcSetup ? 'success' : 'danger'">
|
||
<i class="el-icon-s-marketing"></i>
|
||
</el-badge>
|
||
</el-tooltip>
|
||
</div>
|
||
|
||
<div v-if="!isLoading" class="device-layout">
|
||
<!-- Entry Section / 入口段区域 -->
|
||
<div class="section-area entry-area">
|
||
<div class="section-header">
|
||
Entry Section
|
||
<!-- 入口段 -->
|
||
<span class="section-info" v-if="positionData.entrySpeed">Speed: {{ positionData.entrySpeed.toFixed(1) }} m/min</span>
|
||
<!-- 速度 -->
|
||
</div>
|
||
<div class="section-summary" v-if="entrySectionMetrics.length">
|
||
<div class="summary-item" v-for="item in entrySectionMetrics" :key="item.label">
|
||
<span class="summary-label">{{ item.label }}:</span>
|
||
<span class="summary-value">{{ item.value }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="device-grid">
|
||
<div v-for="device in entryDevicesList" :key="device.positionNameEn" class="device-card" :class="{
|
||
active: selectedCard && selectedCard.positionNameEn === device.positionNameEn,
|
||
'has-data': hasMatId(device.positionNameEn)
|
||
}" @click="selectDevice(device.positionNameEn)">
|
||
<div class="device-name">{{ device.positionNameCn }}</div>
|
||
<div class="device-code">{{ device.positionNameEn }}</div>
|
||
<div class="device-status">
|
||
<span class="coil-id" :class="{ 'status-working': isDeviceWorking(device.positionNameEn), 'status-idle': !isDeviceWorking(device.positionNameEn) }">
|
||
{{ getDeviceStatus(device.positionNameEn) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Furnace Section / 熔炉段区域 -->
|
||
<div class="section-area furnace-area">
|
||
<div class="section-header">
|
||
Furnace Section
|
||
<!-- 熔炉段 -->
|
||
<span class="section-info" v-if="positionData.technologySpeed">Speed: {{ positionData.technologySpeed.toFixed(1) }} m/min</span>
|
||
<!-- 速度 -->
|
||
</div>
|
||
<div class="section-summary" v-if="furnaceSectionMetrics.length">
|
||
<div class="summary-item" v-for="item in furnaceSectionMetrics" :key="item.label">
|
||
<span class="summary-label">{{ item.label }}:</span>
|
||
<span class="summary-value">{{ item.value }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="device-grid">
|
||
<div v-for="device in furnaceDevicesList" :key="device.positionNameEn" class="device-card" :class="{
|
||
active: selectedCard && selectedCard.positionNameEn === device.positionNameEn,
|
||
'has-data': hasMatId(device.positionNameEn)
|
||
}" @click="selectDevice(device.positionNameEn)">
|
||
<div class="device-name">{{ device.positionNameCn }}</div>
|
||
<div class="device-code">{{ device.positionNameEn }}</div>
|
||
<div class="device-status">
|
||
<span class="coil-id" :class="{ 'status-working': isDeviceWorking(device.positionNameEn), 'status-idle': !isDeviceWorking(device.positionNameEn) }">
|
||
{{ getDeviceStatus(device.positionNameEn) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Coating Section / 涂层段区域 -->
|
||
<div class="section-area coat-area">
|
||
<div class="section-header">Coating Section</div>
|
||
<!-- 涂层段 -->
|
||
<div class="section-summary" v-if="coatSectionMetrics.length">
|
||
<div class="summary-item" v-for="item in coatSectionMetrics" :key="item.label">
|
||
<span class="summary-label">{{ item.label }}:</span>
|
||
<span class="summary-value">{{ item.value }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="device-grid">
|
||
<div v-for="device in coatDevicesList" :key="device.positionNameEn" class="device-card" :class="{
|
||
active: selectedCard && selectedCard.positionNameEn === device.positionNameEn,
|
||
'has-data': hasMatId(device.positionNameEn)
|
||
}" @click="selectDevice(device.positionNameEn)">
|
||
<div class="device-name">{{ device.positionNameCn }}</div>
|
||
<div class="device-code">{{ device.positionNameEn }}</div>
|
||
<div class="device-status">
|
||
<span class="coil-id" :class="{ 'status-working': isDeviceWorking(device.positionNameEn), 'status-idle': !isDeviceWorking(device.positionNameEn) }">
|
||
{{ getDeviceStatus(device.positionNameEn) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Exit Section / 出口段区域 -->
|
||
<div class="section-area exit-area">
|
||
<div class="section-header">
|
||
Exit Section
|
||
<!-- 出口段 -->
|
||
<span class="section-info" v-if="positionData.exitSpeed">Speed: {{ positionData.exitSpeed.toFixed(1) }} m/min</span>
|
||
<!-- 速度 -->
|
||
</div>
|
||
<div class="section-summary" v-if="exitSectionMetrics.length">
|
||
<div class="summary-item" v-for="item in exitSectionMetrics" :key="item.label">
|
||
<span class="summary-label">{{ item.label }}:</span>
|
||
<span class="summary-value">{{ item.value }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="device-grid">
|
||
<div v-for="device in exitDevicesList" :key="device.positionNameEn" class="device-card" :class="{
|
||
active: selectedCard && selectedCard.positionNameEn === device.positionNameEn,
|
||
'has-data': hasMatId(device.positionNameEn)
|
||
}" @click="selectDevice(device.positionNameEn)">
|
||
<div class="device-name">{{ device.positionNameCn }}</div>
|
||
<div class="device-code">{{ device.positionNameEn }}</div>
|
||
<div class="device-status">
|
||
<span class="coil-id" :class="{ 'status-working': isDeviceWorking(device.positionNameEn), 'status-idle': !isDeviceWorking(device.positionNameEn) }">
|
||
{{ getDeviceStatus(device.positionNameEn) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Other Section / 其他段区域 -->
|
||
<div class="section-area" v-if="exitOtherDevicesList.length">
|
||
<div class="section-header">Other Section</div>
|
||
<!-- 其他段 -->
|
||
<div class="device-grid other-exit-grid">
|
||
<div v-for="device in exitOtherDevicesList" :key="device.positionNameEn" class="device-card" :class="{
|
||
active: selectedCard && selectedCard.positionNameEn === device.positionNameEn,
|
||
'has-data': hasMatId(device.positionNameEn)
|
||
}" @click="selectDevice(device.positionNameEn)">
|
||
<div class="device-name">{{ device.positionNameCn }}</div>
|
||
<div class="device-code">{{ device.positionNameEn }}</div>
|
||
<div class="device-status">
|
||
<span class="coil-id" :class="{ 'status-working': isDeviceWorking(device.positionNameEn), 'status-idle': !isDeviceWorking(device.positionNameEn) }">
|
||
{{ getDeviceStatus(device.positionNameEn) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-col>
|
||
|
||
<!-- 右侧:信息面板 -->
|
||
<el-col :span="8">
|
||
<div class="info-panels">
|
||
<!-- Production Plan List / 生产计划列表 -->
|
||
<div class="panel">
|
||
<div class="panel-title">
|
||
<i class="el-icon-s-order"></i> Production Plan
|
||
<!-- 生产计划 -->
|
||
</div>
|
||
<div class="plan-list-vertical">
|
||
<el-empty v-if="planQueue.length === 0" description="No production plan" :image-size="80" />
|
||
<!-- 暂无生产计划 -->
|
||
<div
|
||
v-else
|
||
v-for="(plan, index) in planQueue"
|
||
:key="plan.id || plan.planid || index"
|
||
class="plan-item-vertical"
|
||
:class="getPlanItemClass(plan)"
|
||
@click="selectPlan(plan)"
|
||
>
|
||
<div class="plan-item-top">
|
||
<div class="plan-order-dot" :class="getPlanOrderClass(plan.status)">{{ index + 1 }}</div>
|
||
<div class="plan-title-text">
|
||
<div class="plan-id">Plan ID: {{ plan.planid || '-' }}</div>
|
||
<!-- 计划ID -->
|
||
<div class="plan-coil">Coil ID: {{ plan.coilid || '-' }}</div>
|
||
<!-- 钢卷号 -->
|
||
</div>
|
||
<el-tag :type="getPlanStatusTagType(plan.status)" size="mini">{{ getPlanStatusText(plan.status) }}</el-tag>
|
||
</div>
|
||
<div class="plan-item-bottom">
|
||
<span>Steel Grade: {{ plan.steelGrade || '-' }}</span>
|
||
<!-- 钢种 -->
|
||
<span>Sequence: {{ plan.seqid || '-' }}</span>
|
||
<!-- 顺序 -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Production Plan Detail / 生产计划详情 -->
|
||
<div class="panel" v-if="selectedPlan">
|
||
<div class="panel-title">
|
||
<i class="el-icon-document"></i> Plan Detail
|
||
<!-- 生产计划详情 -->
|
||
<el-button size="mini" type="text" icon="el-icon-close" @click="selectedPlan = null"></el-button>
|
||
</div>
|
||
<div class="plan-detail-content">
|
||
<!-- Position Info (if on production line) / 位置信息(如果在产线上) -->
|
||
<div v-if="selectedPlanPosition" class="position-alert">
|
||
<div class="position-icon">
|
||
<i class="el-icon-location"></i>
|
||
</div>
|
||
<div class="position-info">
|
||
<div class="position-label">Current Position</div>
|
||
<!-- 当前位置 -->
|
||
<div class="position-name">{{ selectedPlanPosition.positionNameCn }}</div>
|
||
<div class="position-code">{{ selectedPlanPosition.positionNameEn }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<el-descriptions :column="1" border size="small">
|
||
<el-descriptions-item label="Plan ID">{{ selectedPlan.planid || '-' }}</el-descriptions-item>
|
||
<!-- 计划ID -->
|
||
<el-descriptions-item label="Coil ID">{{ selectedPlan.coilid || '-' }}</el-descriptions-item>
|
||
<!-- 钢卷号 -->
|
||
<el-descriptions-item label="Sequence">{{ selectedPlan.seqid || '-' }}</el-descriptions-item>
|
||
<!-- 顺序号 -->
|
||
<el-descriptions-item label="Status">
|
||
<!-- 状态 -->
|
||
<el-tag :type="getPlanStatusTagType(selectedPlan.status)" size="small" effect="dark">
|
||
{{ getPlanStatusText(selectedPlan.status) }}
|
||
</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="Steel Grade">{{ selectedPlan.steelGrade || '-' }}</el-descriptions-item>
|
||
<!-- 钢种 -->
|
||
<el-descriptions-item label="Entry Thickness">
|
||
<!-- 入口厚度 -->
|
||
{{ selectedPlan.entryThick ? selectedPlan.entryThick + ' mm' : '-' }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="Entry Width">
|
||
<!-- 入口宽度 -->
|
||
{{ selectedPlan.entryWidth ? selectedPlan.entryWidth + ' mm' : '-' }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="Entry Weight">
|
||
<!-- 入口重量 -->
|
||
{{ selectedPlan.entryWeight ? selectedPlan.entryWeight + ' t' : '-' }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="Entry Length">
|
||
<!-- 入口长度 -->
|
||
{{ selectedPlan.entryLength ? selectedPlan.entryLength + ' mm' : '-' }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="Order No">{{ selectedPlan.orderNo || '-' }}</el-descriptions-item>
|
||
<!-- 订单号 -->
|
||
<el-descriptions-item label="Unit Code">{{ selectedPlan.unitCode || '-' }}</el-descriptions-item>
|
||
<!-- 机组号 -->
|
||
<el-descriptions-item label="Plan Type">{{ selectedPlan.planType || '-' }}</el-descriptions-item>
|
||
<!-- 计划类型 -->
|
||
</el-descriptions>
|
||
|
||
<!-- Time Information / 时间信息 -->
|
||
<div class="plan-time-info" v-if="selectedPlan.onlineDate || selectedPlan.startDate || selectedPlan.endDate">
|
||
<div class="info-subtitle">Time Information</div>
|
||
<!-- 时间信息 -->
|
||
<el-descriptions :column="1" border size="small">
|
||
<el-descriptions-item label="Online Time" v-if="selectedPlan.onlineDate">
|
||
<!-- 上线时间 -->
|
||
{{ formatDateTime(selectedPlan.onlineDate) }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="Start Time" v-if="selectedPlan.startDate">
|
||
<!-- 开始时间 -->
|
||
{{ formatDateTime(selectedPlan.startDate) }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="End Time" v-if="selectedPlan.endDate">
|
||
<!-- 结束时间 -->
|
||
{{ formatDateTime(selectedPlan.endDate) }}
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Recent Operation Signal / 最近操作信号 -->
|
||
<div class="panel" v-if="signalData">
|
||
<div class="panel-title">
|
||
<i class="el-icon-bell"></i> Recent Operation
|
||
<!-- 最近操作 -->
|
||
<el-tag
|
||
:type="getOperationTagType(signalData.operation)"
|
||
size="mini"
|
||
effect="dark">
|
||
{{ getOperationConfig(signalData.operation).icon }} {{ getOperationText(signalData.operation) }}
|
||
</el-tag>
|
||
</div>
|
||
<div class="signal-info" :class="getSignalPanelClass(signalData.operation)">
|
||
<el-tag :type="signalData.autoFlag === 1 ? 'warning' : 'info'" size="small">
|
||
{{ signalData.autoFlag === 1 ? 'Manual' : 'Auto' }}
|
||
<!-- {{ signalData.autoFlag === 1 ? '手动操作' : '自动操作' }} -->
|
||
</el-tag>
|
||
<div class="signal-detail">
|
||
<div><strong>Operation Type:</strong>
|
||
<!-- 操作类型 -->
|
||
<span :class="getOperationTextClass(signalData.operation)">
|
||
{{ getOperationText(signalData.operation) }}
|
||
</span>
|
||
</div>
|
||
<div><strong>Coil ID:</strong> {{ producingCoilId || signalData.entryMatId || '-' }}</div>
|
||
<!-- 钢卷号 -->
|
||
<div><strong>Plan ID:</strong> {{ signalData.planId || '-' }}</div>
|
||
<!-- 计划ID -->
|
||
<div v-if="signalData.porIdx !== null && signalData.porIdx !== undefined">
|
||
<strong>Pay-off Reel:</strong> {{ signalData.porIdx }}
|
||
<!-- 开卷机 -->
|
||
</div>
|
||
<div v-if="signalData.trIdx !== null && signalData.trIdx !== undefined">
|
||
<strong>Take-up Reel:</strong> {{ signalData.trIdx }}
|
||
<!-- 卷取机 -->
|
||
</div>
|
||
<div v-if="signalData.virtualPlanFlag">
|
||
<el-tag type="danger" size="mini">Virtual Coil</el-tag>
|
||
<!-- 虚拟卷 -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Operation Buttons / 操作按钮 -->
|
||
<div class="panel">
|
||
<div class="panel-title">Operations</div>
|
||
<!-- 操作 -->
|
||
<div class="btn-list">
|
||
<el-button class="action-btn" size="small" @click="handleOperate(selectedCard, 'ONLINE')">Coil Online</el-button>
|
||
<!-- 钢卷上线 -->
|
||
<el-button class="action-btn" size="small" @click="handleOperate(selectedCard, 'UNLOAD')">Manual Unload</el-button>
|
||
<!-- 手动卸卷 -->
|
||
<el-button class="action-btn" size="small"
|
||
@click="handleOperate(selectedCard, 'ALL_RETURN')">Full Return</el-button>
|
||
<!-- 整卷回退 -->
|
||
<el-button class="action-btn" size="small"
|
||
@click="handleOperate(selectedCard, 'HALF_RETURN')">Half Return</el-button>
|
||
<!-- 半卷回退 -->
|
||
<el-button class="action-btn" size="small" @click="handleOperate(selectedCard, 'BLOCK')">Unload & Block</el-button>
|
||
<!-- 卸卷并封闭 -->
|
||
</div>
|
||
</div>
|
||
<!-- Device Basic Info / 设备基本信息 -->
|
||
<div class="panel" v-if="selectedCard">
|
||
<div class="panel-title">Basic Info</div>
|
||
<!-- 基本信息 -->
|
||
<table class="info-table">
|
||
<tr>
|
||
<td>Position Name</td>
|
||
<!-- 位置名称 -->
|
||
<td>{{ selectedCard.positionNameCn || '-' }}</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Position Code</td>
|
||
<!-- 位置代号 -->
|
||
<td>{{ selectedCard.positionNameEn || '-' }}</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Coil ID</td>
|
||
<!-- 钢卷号 -->
|
||
<td>{{ selectedCard.matId || '-' }}</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Plan ID</td>
|
||
<!-- 计划ID -->
|
||
<td>{{ selectedCard.planId || '-' }}</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Plan No</td>
|
||
<!-- 计划号 -->
|
||
<td>{{ selectedCard.planNo || '-' }}</td>
|
||
</tr>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- 设备实时数据 -->
|
||
|
||
<div class="panel">
|
||
|
||
<!-- Adjustment Tool: Select two positions with two dropdowns / 调整工具,选择两个位置,两个下拉选,分别双向绑定 -->
|
||
<el-form :model="adjustForm" ref="adjustForm" label-width="80px">
|
||
<el-form-item label="Current Position" prop="current">
|
||
<!-- 当前位置 -->
|
||
<el-select v-model="adjustForm.current" placeholder="Please select current position">
|
||
<!-- 请选择当前位置 -->
|
||
<el-option v-for="item in matMapList" :key="item.positionNameEn" :label="item.positionNameCn"
|
||
:value="item.positionNameEn"></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="Target Position" prop="target">
|
||
<!-- 目标位置 -->
|
||
<el-select v-model="adjustForm.target" placeholder="Please select target position">
|
||
<!-- 请选择目标位置 -->
|
||
<el-option v-for="item in matMapList" :key="item.positionNameEn" :label="item.positionNameCn"
|
||
:value="item.positionNameEn"></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
<el-button type="primary" :disabled="!adjustForm.current || !adjustForm.target"
|
||
@click="handleConfirmAdjust">Confirm Adjustment</el-button>
|
||
<!-- 确认调整 -->
|
||
</div>
|
||
</div>
|
||
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<!-- Calculation Setup Result Dialog / 计算结果对话框 -->
|
||
<el-dialog
|
||
title="Calculation Setup Result"
|
||
:visible.sync="showCalcResultDialog"
|
||
width="80%"
|
||
v-if="calcSetupResult"
|
||
>
|
||
<div class="calc-result-header">
|
||
<el-tag :type="calcSetupResult.flag ? 'success' : 'danger'">
|
||
{{ calcSetupResult.flag ? 'Calculation Success' : 'Calculation Failed' }}
|
||
<!-- {{ calcSetupResult.flag ? '计算成功' : '计算失败' }} -->
|
||
</el-tag>
|
||
<span>Key: {{ calcSetupResult.key }}</span>
|
||
</div>
|
||
<el-table
|
||
v-if="calcSetupResult.flag && calcSetupResult.lists && calcSetupResult.lists.length > 0"
|
||
:data="calcSetupResult.lists"
|
||
border
|
||
stripe
|
||
max-height="500"
|
||
>
|
||
<el-table-column prop="passno" label="Pass No" width="80" fixed></el-table-column>
|
||
<!-- 道次号 -->
|
||
<el-table-column prop="entryThick" label="Entry Thickness (mm)" width="120"></el-table-column>
|
||
<!-- 入口厚度 -->
|
||
<el-table-column prop="exitThick" label="Exit Thickness (mm)" width="120"></el-table-column>
|
||
<!-- 出口厚度 -->
|
||
<el-table-column prop="reduction" label="Reduction (%)" width="100"></el-table-column>
|
||
<!-- 压下率 -->
|
||
<el-table-column prop="rollSpeed" label="Roll Speed" width="120"></el-table-column>
|
||
<!-- 轧制速度 -->
|
||
<el-table-column prop="rollForce" label="Roll Force (kN)" width="120"></el-table-column>
|
||
<!-- 轧制力 -->
|
||
<el-table-column prop="entryTension" label="Entry Tension" width="120"></el-table-column>
|
||
<!-- 入口张力 -->
|
||
<el-table-column prop="exitTension" label="Exit Tension" width="120"></el-table-column>
|
||
<!-- 出口张力 -->
|
||
</el-table>
|
||
<div v-else class="empty-msg">No calculation result data</div>
|
||
<!-- 无计算结果数据 -->
|
||
</el-dialog>
|
||
|
||
<el-dialog :visible.sync="operateMatStatus" :title="getOperateTitle" width="50%">
|
||
<el-form :model="operateMatForm" :rules="operateRules" ref="operateForm" label-width="120px">
|
||
<el-form-item label="Pay-off Reel No" prop="porIdx">
|
||
<!-- 开卷机编号 -->
|
||
<el-input v-model="operateMatForm.porIdx"></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="Take-up Reel No" prop="trIdx">
|
||
<!-- 卷取机编号 -->
|
||
<el-input v-model="operateMatForm.trIdx"></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="Plan ID" prop="planId">
|
||
<!-- 计划id -->
|
||
<el-input v-model="operateMatForm.planId" placeholder="Please enter Plan ID"></el-input>
|
||
<!-- 请输入计划ID -->
|
||
</el-form-item>
|
||
<el-form-item label="Coil ID" prop="entryMatId">
|
||
<!-- 钢卷号 -->
|
||
<el-input v-model="operateMatForm.entryMatId" placeholder="Please enter Coil ID"></el-input>
|
||
<!-- 请输入钢卷号 -->
|
||
</el-form-item>
|
||
<el-form-item label="Operation Type" prop="operation">
|
||
<!-- 操作类型 -->
|
||
<el-select v-model="operateMatForm.operation" disabled>
|
||
<el-option label="Coil Online" value="ONLINE"></el-option>
|
||
<!-- 钢卷上线 -->
|
||
<el-option label="Manual Unload" value="UNLOAD"></el-option>
|
||
<!-- 手动卸卷 -->
|
||
<el-option label="Full Return" value="ALL_RETURN"></el-option>
|
||
<!-- 整卷回退 -->
|
||
<el-option label="Half Return" value="HALF_RETURN"></el-option>
|
||
<!-- 半卷回退 -->
|
||
<el-option label="Unload & Block" value="BLOCK"></el-option>
|
||
<!-- 卸卷并封闭 -->
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<!-- Return Related Fields / 回退相关字段 -->
|
||
<template v-if="['ALL_RETURN', 'HALF_RETURN'].includes(operateMatForm.operation)">
|
||
<el-form-item label="Return Coil ID" prop="returnMatId">
|
||
<!-- 回退卷号 -->
|
||
<el-input v-model="operateMatForm.returnMatId" placeholder="Please enter Return Coil ID"></el-input>
|
||
<!-- 请输入回退卷号 -->
|
||
</el-form-item>
|
||
<el-form-item label="Return Weight" prop="returnWeight">
|
||
<!-- 回退重量 -->
|
||
<el-input v-model="operateMatForm.returnWeight" placeholder="Please enter Return Weight"></el-input>
|
||
<!-- 请输入回退重量 -->
|
||
</el-form-item>
|
||
<el-form-item label="Return Remark" prop="returnRemark">
|
||
<!-- 回退备注 -->
|
||
<el-input v-model="operateMatForm.returnRemark" rows="3"></el-input>
|
||
</el-form-item>
|
||
</template>
|
||
|
||
<!-- Output Length Fields / 产出长度字段 -->
|
||
<template v-if="['PRODUCING', 'PRODUCT'].includes(operateMatForm.operation)">
|
||
<el-form-item label="Output Coil Length" prop="coilLength">
|
||
<!-- 产出钢卷长度 -->
|
||
<el-input v-model="operateMatForm.coilLength" type="number" placeholder="Please enter Output Coil Length"></el-input>
|
||
<!-- 请输入产出钢卷长度 -->
|
||
</el-form-item>
|
||
</template>
|
||
</el-form>
|
||
<div slot="footer" class="dialog-footer">
|
||
<el-button @click="operateMatStatus = false">Cancel</el-button>
|
||
<!-- 取消 -->
|
||
<el-button type="primary" @click="submitOperateForm">Confirm</el-button>
|
||
<!-- 确定 -->
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { adjustPosition, getTrackMatPosition, operateMat, getBackData, getPlanQueue } from '@/api/l2/track'
|
||
import wsManager from '@/utils/websocketManager'
|
||
|
||
// Device metadata mapping based on backend DeviceEnum (section, source, parameter fields)
|
||
// 基于后端 DeviceEnum 的前端映射(区域、来源、参数字段)
|
||
const DEVICE_META = {
|
||
POR1: { sectionType: 'ENTRY', sourceType: 'ENTRY', paramFields: ['tensionPorBr1', 'stripSpeed'] },
|
||
POR2: { sectionType: 'ENTRY', sourceType: 'ENTRY', paramFields: ['tensionPorBr2', 'stripSpeed'] },
|
||
WELDER: { sectionType: 'ENTRY', sourceType: 'ENTRY', paramFields: ['weldStatus'] },
|
||
ENL1: { sectionType: 'PROCESS', sourceType: 'ENTRY', paramFields: ['celLength', 'celCapacity', 'tensionCel', 'celLengthMax', 'celLengthMin', 'bR4or5toBR6Tension'] },
|
||
ENL2: { sectionType: 'PROCESS', sourceType: 'ENTRY', paramFields: ['celLength', 'celCapacity', 'tensionCel', 'celLengthMax', 'celLengthMin', 'bR4or5toBR6Tension'] },
|
||
ENL3: { sectionType: 'PROCESS', sourceType: 'ENTRY', paramFields: ['celLength', 'celCapacity', 'tensionCel', 'celLengthMax', 'celLengthMin', 'bR4or5toBR6Tension'] },
|
||
|
||
CLEAN: { sectionType: 'PROCESS', sourceType: 'FURNACE', paramFields: ['cleaningVoltage', 'cleaningCurrent', 'alkaliConcentration', 'alkaliTemperature'] },
|
||
FUR1: { sectionType: 'PROCESS', sourceType: 'FURNACE', paramFields: ['phfExitStripTemp', 'potTemperature', 'gasConsumption'] },
|
||
FUR2: { sectionType: 'PROCESS', sourceType: 'FURNACE', paramFields: ['rtfExitStripTemp', 'zincPotPower'] },
|
||
FUR3: { sectionType: 'PROCESS', sourceType: 'FURNACE', paramFields: ['jcsExitStripTemp', 'coolingTowerStripTemp'] },
|
||
FUR4: { sectionType: 'PROCESS', sourceType: 'FURNACE', paramFields: ['scsExitStripTemp'] },
|
||
POT: { sectionType: 'PROCESS', sourceType: 'COAT', paramFields: ['scsExitStripTemp'] },
|
||
TOWER: { sectionType: 'PROCESS', sourceType: 'COAT', paramFields: ['scsExitStripTemp'] },
|
||
TM: { sectionType: 'PROCESS', sourceType: 'COAT', paramFields: ['tensionBr5Tm', 'stripSpeedTmExit'] },
|
||
TL: { sectionType: 'PROCESS', sourceType: 'COAT', paramFields: ['tlElongation', 'tensionTlBr7'] },
|
||
COAT: { sectionType: 'PROCESS', sourceType: 'COAT', paramFields: [
|
||
'avrCoatingWeightTop','stdCoatingWeightTop','maxCoatingWeightTop','minCoatingWeightTop',
|
||
'avrCoatingWeightBottom','stdCoatingWeightBottom','maxCoatingWeightBottom','minCoatingWeightBottom',
|
||
'airKnifePressure','airKnifeFlow','airKnifeGap','stripSpeedTmExit','tensionBr8Tm',
|
||
'tensionTmBr9','tensionBr8Br9','tmMask','tmElongation','rollForceOperator','rollForceDrive',
|
||
'motorTorque','bendingForce','antiCrimpingRollMesh','billyRollMesh',
|
||
'tensionTlBr10Br11','tensionBr9toBr10Br11','tlFlag','tlElongation','levelingUnit1Mesh','levelingUnit2Mesh',
|
||
'antiCrossBowUnitMesh','tensionBr10Br11toBr12','stripSpeedAfp','stripTempAfp'
|
||
] },
|
||
|
||
CXL1: { sectionType: 'EXIT', sourceType: 'EXIT', paramFields: ['cxlLength', 'cxlCapacity', 'tensionCxl'] },
|
||
CXL2: { sectionType: 'EXIT', sourceType: 'EXIT', paramFields: ['cxlLength', 'cxlCapacity', 'tensionCxl'] },
|
||
TR: { sectionType: 'EXIT', sourceType: 'EXIT', paramFields: ['coilLength', 'speedExitSection', 'tensionBr9Tr'] },
|
||
EXC: { sectionType: 'EXIT', sourceType: 'EXIT', paramFields: [] },
|
||
WEIGHT: { sectionType: 'EXIT', sourceType: 'EXIT', paramFields: [] }
|
||
}
|
||
|
||
// Parameter field labels (English with Chinese comments)
|
||
// 参数字段标签(英文显示,中文注释)
|
||
const PARAM_LABELS = {
|
||
tensionPorBr1: 'Tension POR-BR1', // 开卷张力1#
|
||
tensionPorBr2: 'Tension POR-BR2', // 开卷张力2#
|
||
stripSpeed: 'Strip Speed', // 带钢速度
|
||
weldStatus: 'Weld Status', // 焊机状态
|
||
celLength: 'Entry Loop Length', // 入口活套位置
|
||
celCapacity: 'Entry Loop Capacity', // 入口活套套量
|
||
tensionCel: 'Entry Loop Tension', // 入口活套张力
|
||
celLengthMax: 'Entry Loop Max Length', // 入口活套最大长度
|
||
celLengthMin: 'Entry Loop Min Length', // 入口活套最小长度
|
||
bR4or5toBR6Tension: 'Tension BR4/5-BR6', // BR4/5到BR6张力
|
||
cleaningVoltage: 'Cleaning Voltage', // 清洗电压
|
||
cleaningCurrent: 'Cleaning Current', // 清洗电流
|
||
alkaliConcentration: 'Alkali Concentration', // 碱液浓度
|
||
alkaliTemperature: 'Alkali Temperature', // 碱液温度
|
||
phfExitStripTemp: 'PH Exit Strip Temp', // PH炉出口温度
|
||
potTemperature: 'Pot Temperature', // 锌锅温度
|
||
gasConsumption: 'Gas Consumption', // 燃气消耗
|
||
rtfExitStripTemp: 'RTF Exit Strip Temp', // RTF炉出口温度
|
||
zincPotPower: 'Zinc Pot Power', // 锌锅功率
|
||
jcsExitStripTemp: 'JCS Exit Strip Temp', // 冷却段出口温度
|
||
coolingTowerStripTemp: 'Cooling Tower Temp', // 冷却塔温度
|
||
scsExitStripTemp: 'SCS Exit Strip Temp', // 均衡段出口温度
|
||
tensionBr5Tm: 'Tension BR5-TM', // BR5-TM张力
|
||
stripSpeedTmExit: 'TM Exit Speed', // TM出口速度
|
||
tmElongation: 'TM Elongation', // 光整延伸率
|
||
tensionTlBr7: 'Tension TL-BR7', // TL-BR7张力
|
||
tensionTlBr10Br11: 'Tension TL-BR10/11', // TL-BR10/11张力
|
||
tlElongation: 'TL Elongation', // 拉矫延伸率
|
||
tlFlag: 'TL Flag', // 拉矫机投用标志
|
||
cxlLength: 'Exit Loop Length', // 出口活套位置
|
||
cxlCapacity: 'Exit Loop Capacity', // 出口活套套量
|
||
tensionCxl: 'Exit Loop Tension', // 出口活套张力
|
||
coilLength: 'Coil Length', // 钢卷长度
|
||
speedExitSection: 'Exit Section Speed', // 出口段速度
|
||
tensionBr9Tr: 'Tension BR9-TR', // BR9-TR张力
|
||
tensionBr8Tm: 'Tension BR8-TM', // BR8-TM张力
|
||
tensionTmBr9: 'Tension TM-BR9', // TM-BR9张力
|
||
tensionBr8Br9: 'Tension BR8-BR9', // BR8-BR9张力
|
||
tensionBr9toBr10Br11: 'Tension BR9-BR10/11', // BR9到BR10/11张力
|
||
tensionBr10Br11toBr12: 'Tension BR10/11-BR12', // BR10/11到BR12张力
|
||
avrCoatingWeightTop: 'Avg Coating Weight Top', // 顶部平均涂层重量
|
||
stdCoatingWeightTop: 'Std Coating Weight Top', // 顶部标准涂层重量
|
||
maxCoatingWeightTop: 'Max Coating Weight Top', // 顶部最大涂层重量
|
||
minCoatingWeightTop: 'Min Coating Weight Top', // 顶部最小涂层重量
|
||
avrCoatingWeightBottom: 'Avg Coating Weight Bottom', // 底部平均涂层重量
|
||
stdCoatingWeightBottom: 'Std Coating Weight Bottom', // 底部标准涂层重量
|
||
maxCoatingWeightBottom: 'Max Coating Weight Bottom', // 底部最大涂层重量
|
||
minCoatingWeightBottom: 'Min Coating Weight Bottom', // 底部最小涂层重量
|
||
airKnifePressure: 'Air Knife Pressure', // 气刀压力
|
||
airKnifeFlow: 'Air Knife Flow', // 气刀流量
|
||
airKnifeGap: 'Air Knife Gap', // 气刀间隙
|
||
tmMask: 'TM Mask', // TM掩码
|
||
rollForceOperator: 'Roll Force Operator', // 操作侧辊压力
|
||
rollForceDrive: 'Roll Force Drive', // 驱动侧辊压力
|
||
motorTorque: 'Motor Torque', // 电机扭矩
|
||
bendingForce: 'Bending Force', // 总弯曲力
|
||
antiCrimpingRollMesh: 'Anti-Crimping Roll Mesh', // 防卷翘辊网距
|
||
billyRollMesh: 'Billy Roll Mesh', // Billy Roll网距
|
||
levelingUnit1Mesh: 'Leveling Unit 1 Mesh', // 矫直单元1网距
|
||
levelingUnit2Mesh: 'Leveling Unit 2 Mesh', // 矫直单元2网距
|
||
antiCrossBowUnitMesh: 'Anti Cross-Bow Unit Mesh', // 防横弯单元网距
|
||
stripSpeedAfp: 'AFP Strip Speed', // AFP段钢带速度
|
||
stripTempAfp: 'AFP Strip Temp' // AFP段钢带温度
|
||
}
|
||
|
||
const FIELD_ALIASES = {
|
||
phfExitStripTemp: ['phFurnaceTemperatureActual', 'phFurnaceTemperature'],
|
||
rtfExitStripTemp: ['nof1FurnaceTemperatureActual', 'nof1FurnaceTemperatureSet'],
|
||
jcsExitStripTemp: ['jcsExitStripTemp'],
|
||
scsExitStripTemp: ['scsExitStripTemp'],
|
||
potTemperature: ['potTemperature'],
|
||
zincPotPower: ['zincPotPower'],
|
||
gasConsumption: ['gasConsumption'],
|
||
coolingTowerStripTemp: ['coolingTowerTemperature', 'coolingTowerStripTemp'],
|
||
cleaningVoltage: ['cleaningVoltage'],
|
||
cleaningCurrent: ['cleaningCurrent'],
|
||
alkaliConcentration: ['alkaliConcentration'],
|
||
alkaliTemperature: ['alkaliTemperature']
|
||
}
|
||
|
||
const CLEANING_FIELDS = new Set(['cleaningVoltage', 'cleaningCurrent', 'alkaliConcentration', 'alkaliTemperature'])
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
// WebSocket 连接状态
|
||
socketStatus: {
|
||
measure: false,
|
||
position: false,
|
||
signal: false,
|
||
matmap: false,
|
||
calcSetup: false
|
||
},
|
||
matMapList: [],
|
||
selectedCard: null,
|
||
|
||
// 实时测量数据(track_measure)
|
||
realtimeData: {
|
||
entry: null,
|
||
furnace: null,
|
||
coat: null,
|
||
exit: null
|
||
},
|
||
|
||
// 位置追踪数据(track_position)
|
||
positionData: {
|
||
coilStripLocationList: [],
|
||
entryLoopLen: 0,
|
||
exitLoopLen: 0,
|
||
entryLoopPer: 0,
|
||
exitLoopLPer: 0,
|
||
porId: null,
|
||
trId: null,
|
||
entrySpeed: 0,
|
||
technologySpeed: 0,
|
||
exitSpeed: 0,
|
||
matMapList: [],
|
||
},
|
||
|
||
// 操作信号数据(track_signal)
|
||
signalData: null,
|
||
// 上一次收到的操作类型(用于检测连续生产中)
|
||
lastSignalOperation: null,
|
||
|
||
// 计算设定结果(calc_setup_result)
|
||
calcSetupResult: null,
|
||
showCalcResultDialog: false,
|
||
|
||
// 加载状态
|
||
isLoading: true,
|
||
|
||
// 计划队列tab
|
||
activeQueueTab: 'all',
|
||
|
||
// 生产计划队列
|
||
planQueue: [],
|
||
|
||
// 选中的生产计划
|
||
selectedPlan: null,
|
||
|
||
adjustForm: {
|
||
current: null,
|
||
target: null
|
||
},
|
||
adjustMode: false,
|
||
operateMatForm: {
|
||
porIdx: null,
|
||
trIdx: null,
|
||
planId: '',
|
||
entryMatId: '',
|
||
// planNo: '',
|
||
operation: '',
|
||
returnMatId: '',
|
||
returnWeight: null,
|
||
returnRemark: '',
|
||
coilLength: null
|
||
},
|
||
operateRules: {
|
||
planId: [{ required: true, message: '请输入计划id', trigger: 'blur' }],
|
||
entryMatId: [{ required: true, message: '请输入钢卷号', trigger: 'blur' }],
|
||
operation: [{ required: true, message: '请选择操作类型', trigger: 'change' }],
|
||
returnMatId: [
|
||
{
|
||
required: true,
|
||
message: '请输入回退卷号',
|
||
trigger: 'blur',
|
||
validator: (rule, val, cb) => {
|
||
if (['ALL_RETURN', 'HALF_RETURN'].includes(this.operateMatForm.operation) && !val) {
|
||
cb(new Error('请输入回退卷号'))
|
||
} else cb()
|
||
}
|
||
}
|
||
],
|
||
returnWeight: [
|
||
{
|
||
required: true,
|
||
message: '请输入回退重量',
|
||
trigger: 'blur',
|
||
validator: (rule, val, cb) => {
|
||
if (['ALL_RETURN', 'HALF_RETURN'].includes(this.operateMatForm.operation) && (val === null || val === '')) {
|
||
cb(new Error('请输入回退重量'))
|
||
} else cb()
|
||
}
|
||
}
|
||
]
|
||
},
|
||
operateMatStatus: false, // 操作对话框显示状态
|
||
returnInfo: {},
|
||
isLoadingReturn: false,
|
||
returnError: ''
|
||
}
|
||
},
|
||
computed: {
|
||
// 当前计划(ONLINE 或 PRODUCING状态)
|
||
currentPlans() {
|
||
return this.planQueue.filter(plan =>
|
||
['ONLINE', 'PRODUCING', 'PRODUCTING'].includes(plan.status)
|
||
)
|
||
},
|
||
|
||
// 当前生产中的计划(取状态为生产中的首条)
|
||
producingPlan() {
|
||
return this.planQueue.find(plan => ['PRODUCING', 'PRODUCTING'].includes(plan.status))
|
||
},
|
||
|
||
// 当前生产中的钢卷号
|
||
producingCoilId() {
|
||
if (this.producingPlan) {
|
||
return this.producingPlan.coilid || this.producingPlan.matId || ''
|
||
}
|
||
return ''
|
||
},
|
||
|
||
// 显示的计划列表(根据tab切换)
|
||
displayedPlans() {
|
||
if (this.activeQueueTab === 'current') {
|
||
return this.currentPlans
|
||
}
|
||
return this.planQueue
|
||
},
|
||
|
||
// 选中计划的位置信息
|
||
selectedPlanPosition() {
|
||
if (!this.selectedPlan || !this.matMapList || this.matMapList.length === 0) {
|
||
return null
|
||
}
|
||
|
||
// 从matMapList中查找匹配的位置
|
||
// matMapList中的字段是驼峰命名:planId, matId
|
||
// 计划对象中的字段可能是:planid, coilid
|
||
const position = this.matMapList.find(item =>
|
||
(item.planId && item.planId === this.selectedPlan.planid) ||
|
||
(item.planId && item.planId === this.selectedPlan.planId) ||
|
||
(item.matId && item.matId === this.selectedPlan.coilid) ||
|
||
(item.matId && item.matId === this.selectedPlan.matId)
|
||
)
|
||
|
||
return position
|
||
},
|
||
|
||
// Get operation title (English with Chinese comments)
|
||
// 获取操作标题(英文显示,中文注释)
|
||
getOperateTitle() {
|
||
const titleMap = {
|
||
'ONLINE': 'Coil Online', // 钢卷上线
|
||
'UNLOAD': 'Manual Unload', // 手动卸卷
|
||
'ALL_RETURN': 'Full Return', // 整卷回退
|
||
'HALF_RETURN': 'Half Return', // 半卷回退
|
||
'BLOCK': 'Unload & Block', // 卸卷并封闭
|
||
'THROW_TAIL': 'Throw Tail' // 甩尾
|
||
}
|
||
return titleMap[this.operateMatForm.operation] || 'Coil Operation' // 钢卷操作
|
||
},
|
||
// 分段汇总数据
|
||
entrySectionMetrics() {
|
||
return this.buildSectionMetrics('ENTRY', ['stripSpeed', 'tensionPorBr1', 'tensionPorBr2', 'celLength', 'celCapacity'])
|
||
},
|
||
furnaceSectionMetrics() {
|
||
return this.buildSectionMetrics('FURNACE', [
|
||
'cleaningVoltage', 'cleaningCurrent', 'alkaliConcentration', 'alkaliTemperature',
|
||
'phfExitStripTemp', 'rtfExitStripTemp', 'jcsExitStripTemp', 'scsExitStripTemp',
|
||
'potTemperature', 'zincPotPower', 'gasConsumption', 'coolingTowerStripTemp',
|
||
// 'LTH2FurnaceTemperatureActualValue', 'PHFurnacePressureActualValue',
|
||
// 'jcf1FurnaceTemperatureActual', 'jcfFan1ActualSpeed', 'jcfFan2ActualSpeed',
|
||
// 'jcfFan3ActualSpeed', 'jcfFan4ActualSpeed', 'lbzFurnaceTemperatureActual',
|
||
// 'lthFurnaceTemperatureActual', 'nof1AirFlowActual', 'nof1FurnaceTemperatureActual', 'nof1GasFlowActual',
|
||
// 'nof1UtheisaKongCombustionRatioActualValue', 'nof2AirFlowActual', 'nof2FurnaceTemperatureActual', 'nof2GasFlowActual',
|
||
// 'nof2UtheisaKongCombustionRatioActualValue', 'nof3AirFlowActual', 'nof3FurnaceTemperatureActual', 'nof3GasFlowActual',
|
||
// 'nof3UtheisaKongCombustionRatioActualValue', 'nof4AirFlowActual', 'nof4FurnaceTemperatureActual', 'nof4GasFlowActual',
|
||
// 'nof4UtheisaKongCombustionRatioActualValue', 'nofAirPressureActual', 'nofFurnacePressureActual', 'nofGasPressureActual',
|
||
// 'nofPlateTemperatureActual', 'phFurnaceTemperatureActual', 'rtf1AirFlowActual', 'rtf1FurnaceTemperatureActual',
|
||
// 'rtf1GasFlowActual', 'rtf2AirFlowActual', 'rtf2FurnaceTemperatureActual', 'rtf2GasFlowActual',
|
||
// 'rtf2UtheisaKongCombustionRatioActualValue', 'rtfAirPressureActual',
|
||
// 'rtfFurnacePressureActual', 'rtfGasPressureActual',
|
||
// 'rtfPlateTemperatureActual', 'sfAirFlowActual', 'sfFurnaceTemperatureActual',
|
||
// 'sfAirFlowActual', 'sfFurnaceTemperatureActual', 'sfUtheisaKongCombustionRatioActualValue',
|
||
// 'tdsFurnacePressureActual', 'tdsFurnaceTemperatureActual', 'tdsPlateTemperatureActual'
|
||
])
|
||
},
|
||
coatSectionMetrics() {
|
||
return this.buildSectionMetrics('COAT', ['stripSpeedTmExit', 'avrCoatingWeightTop', 'avrCoatingWeightBottom', 'tmElongation', 'tlElongation'])
|
||
},
|
||
exitSectionMetrics() {
|
||
return this.buildSectionMetrics('EXIT', ['speedExitSection', 'coilLength', 'cxlLength', 'cxlCapacity', 'tensionBr9Tr'])
|
||
},
|
||
// 入口段设备列表(依据 DeviceEnum.sectionType)
|
||
entryDevicesList() {
|
||
return this.getDevicesBySection('ENTRY')
|
||
},
|
||
// 熔炉段设备列表(PROCESS 且来源 FURNACE)
|
||
furnaceDevicesList() {
|
||
return this.getDevicesBySource('FURNACE')
|
||
},
|
||
// 涂层段设备列表(PROCESS 且来源 COAT)
|
||
coatDevicesList() {
|
||
return this.getDevicesBySource('COAT')
|
||
},
|
||
// 出口段设备列表(EXIT,常规设备)
|
||
exitDevicesList() {
|
||
const exitDevices = this.getDevicesBySection('EXIT')
|
||
return exitDevices.filter(d => !['INS', 'EXC', 'WEIGHT'].includes(d.positionNameEn))
|
||
},
|
||
// 出口其他设备列表
|
||
exitOtherDevicesList() {
|
||
return this.getDevicesBySection('EXIT').filter(d => ['INS', 'EXC', 'WEIGHT'].includes(d.positionNameEn))
|
||
},
|
||
// 根据选中设备获取详细数据
|
||
currentDeviceData() {
|
||
if (!this.selectedCard) return []
|
||
const deviceId = this.selectedCard.positionNameEn
|
||
const meta = DEVICE_META[deviceId]
|
||
if (!meta) return []
|
||
|
||
// 针对开卷机/入口活套/出口活套,先校验卷号匹配
|
||
if (!this.matchReel(deviceId)) return []
|
||
|
||
const source = this.getRealtimeSourceData(deviceId)
|
||
if (!source) return []
|
||
|
||
const data = []
|
||
// 通用字段:按 DeviceEnum.paramFields 取值
|
||
meta.paramFields.forEach(field => {
|
||
const value = this.getFieldValueWithAlias(source, field)
|
||
if (value !== null && value !== undefined && value !== '') {
|
||
data.push({
|
||
label: PARAM_LABELS[field] || field,
|
||
value: this.formatValue(value),
|
||
unit: this.getFieldUnit(field)
|
||
})
|
||
}
|
||
})
|
||
|
||
return data
|
||
}
|
||
},
|
||
|
||
methods: {
|
||
// ============ 辅助:设备元数据 ============
|
||
getDevicesBySection(sectionType) {
|
||
return this.matMapList.filter(d => DEVICE_META[d.positionNameEn]?.sectionType === sectionType)
|
||
},
|
||
getDevicesBySource(sourceType) {
|
||
return this.matMapList.filter(d => DEVICE_META[d.positionNameEn]?.sourceType === sourceType)
|
||
},
|
||
// 判断设备是否有物料(用于绿框)
|
||
hasMatId(deviceId) {
|
||
const info = this.getDeviceInfo(deviceId)
|
||
return !!(info && info.matId)
|
||
},
|
||
getRealtimeSourceData(deviceId) {
|
||
const sourceType = DEVICE_META[deviceId]?.sourceType
|
||
if (!sourceType) return null
|
||
if (sourceType === 'ENTRY') return this.realtimeData.entry
|
||
if (sourceType === 'FURNACE') return this.realtimeData.furnace
|
||
if (sourceType === 'COAT') return this.realtimeData.coat
|
||
if (sourceType === 'EXIT') return this.realtimeData.exit
|
||
return null
|
||
},
|
||
getFieldValueWithAlias(source, field) {
|
||
// 兼容后端字段命名差异(如大小写、别名)
|
||
if (!source) return null
|
||
// 按别名映射
|
||
if (FIELD_ALIASES[field]) {
|
||
for (const alias of FIELD_ALIASES[field]) {
|
||
if (alias in source) return source[alias]
|
||
}
|
||
}
|
||
if (field in source) return source[field]
|
||
const lower = field.charAt(0).toLowerCase() + field.slice(1)
|
||
if (lower in source) return source[lower]
|
||
return null
|
||
},
|
||
getFieldUnit(field) {
|
||
const unitMap = {
|
||
stripSpeed: 'm/min',
|
||
stripSpeedTmExit: 'm/min',
|
||
stripSpeedAfp: 'm/min',
|
||
celLength: 'm',
|
||
cxlLength: 'm',
|
||
celCapacity: '%',
|
||
cxlCapacity: '%',
|
||
tensionPorBr1: 'daN',
|
||
tensionPorBr2: 'daN',
|
||
tensionBr5Tm: 'daN',
|
||
tensionTlBr7: 'daN',
|
||
tensionCxl: 'daN',
|
||
coilLength: 'm',
|
||
speedExitSection: 'm/min',
|
||
tlElongation: '%',
|
||
tmElongation: '%',
|
||
cleaningVoltage: 'V',
|
||
cleaningCurrent: 'A',
|
||
alkaliConcentration: 'g/L',
|
||
alkaliTemperature: '℃',
|
||
phfExitStripTemp: '℃',
|
||
rtfExitStripTemp: '℃',
|
||
jcsExitStripTemp: '℃',
|
||
scsExitStripTemp: '℃',
|
||
potTemperature: '℃',
|
||
zincPotPower: 'kW',
|
||
gasConsumption: 'm³/h'
|
||
}
|
||
return unitMap[field] || ''
|
||
},
|
||
getSectionSource(sectionType) {
|
||
if (sectionType === 'ENTRY') return this.realtimeData.entry
|
||
if (sectionType === 'FURNACE') return this.realtimeData.furnace
|
||
if (sectionType === 'COAT') return this.realtimeData.coat
|
||
if (sectionType === 'EXIT') return this.realtimeData.exit
|
||
return null
|
||
},
|
||
buildSectionMetrics(sectionType, fields) {
|
||
const source = this.getSectionSource(sectionType)
|
||
return fields.map(f => {
|
||
const raw = source ? this.getFieldValueWithAlias(source, f) : null
|
||
const unit = this.getFieldUnit(f)
|
||
return {
|
||
label: PARAM_LABELS[f] || f,
|
||
value: `${this.formatValue(raw)}${unit ? ' ' + unit : ''}`
|
||
}
|
||
})
|
||
},
|
||
matchReel(deviceId) {
|
||
if (!['POR1', 'POR2', 'ENL1', 'ENL2', 'CXL1', 'CXL2'].includes(deviceId)) return true
|
||
if (!this.realtimeData.entry) return false
|
||
const reel = this.realtimeData.entry.payOffReelNumber
|
||
if ((deviceId === 'POR1' || deviceId === 'ENL1' || deviceId === 'CXL1') && reel === 1) return true
|
||
if ((deviceId === 'POR2' || deviceId === 'ENL2' || deviceId === 'CXL2') && reel === 2) return true
|
||
return false
|
||
},
|
||
// ============ WebSocket 连接管理 ============
|
||
|
||
initWebSockets() {
|
||
// 1. track_measure - 实时测量数据
|
||
wsManager.connect('track_measure', {
|
||
onMessage: this.processMeasureData,
|
||
onOpen: () => {
|
||
this.socketStatus.measure = true
|
||
console.log('✅ 测量数据连接成功')
|
||
},
|
||
onError: () => { this.socketStatus.measure = false },
|
||
onClose: () => { this.socketStatus.measure = false }
|
||
})
|
||
|
||
// 2. track_position - 钢卷位置追踪
|
||
wsManager.connect('track_position', {
|
||
onMessage: this.processPositionData,
|
||
onOpen: () => {
|
||
this.socketStatus.position = true
|
||
console.log('✅ 位置追踪连接成功')
|
||
},
|
||
onError: () => { this.socketStatus.position = false },
|
||
onClose: () => { this.socketStatus.position = false }
|
||
})
|
||
|
||
// 3. track_signal - 操作信号
|
||
wsManager.connect('track_signal', {
|
||
onMessage: this.processSignalData,
|
||
onOpen: () => {
|
||
this.socketStatus.signal = true
|
||
console.log('✅ 操作信号连接成功')
|
||
},
|
||
onError: () => { this.socketStatus.signal = false },
|
||
onClose: () => { this.socketStatus.signal = false }
|
||
})
|
||
|
||
// 4. track_matmap - 物料位置映射
|
||
wsManager.connect('track_matmap', {
|
||
onMessage: this.processMatmapData,
|
||
onOpen: () => {
|
||
this.socketStatus.matmap = true
|
||
console.log('✅ 物料映射连接成功')
|
||
},
|
||
onError: () => { this.socketStatus.matmap = false },
|
||
onClose: () => { this.socketStatus.matmap = false }
|
||
})
|
||
|
||
// 5. calc_setup_result - 计算设定结果
|
||
wsManager.connect('calc_setup_result', {
|
||
onMessage: this.processCalcSetupResult,
|
||
onOpen: () => {
|
||
this.socketStatus.calcSetup = true
|
||
console.log('✅ 计算结果连接成功')
|
||
},
|
||
onError: () => { this.socketStatus.calcSetup = false },
|
||
onClose: () => { this.socketStatus.calcSetup = false }
|
||
})
|
||
},
|
||
|
||
disconnectWebSockets() {
|
||
wsManager.disconnectAll()
|
||
this.socketStatus = {
|
||
measure: false,
|
||
position: false,
|
||
signal: false,
|
||
matmap: false,
|
||
calcSetup: false
|
||
}
|
||
},
|
||
|
||
// ============ 数据处理方法 ============
|
||
|
||
processMeasureData(data) {
|
||
if (data.appMeasureEntryMessage) {
|
||
this.$set(this.realtimeData, 'entry', data.appMeasureEntryMessage)
|
||
}
|
||
if (data.appMeasureFurnaceMessage) {
|
||
this.$set(this.realtimeData, 'furnace', data.appMeasureFurnaceMessage)
|
||
}
|
||
if (data.appMeasureCoatMessage) {
|
||
this.$set(this.realtimeData, 'coat', data.appMeasureCoatMessage)
|
||
}
|
||
if (data.appMeasureExitMessage) {
|
||
this.$set(this.realtimeData, 'exit', data.appMeasureExitMessage)
|
||
}
|
||
this.$forceUpdate()
|
||
},
|
||
|
||
processPositionData(data) {
|
||
this.positionData = {
|
||
coilStripLocationList: data.coilStripLocationList || [],
|
||
entryLoopLen: data.entryLoopLen || 0,
|
||
exitLoopLen: data.exitLoopLen || 0,
|
||
entryLoopPer: data.entryLoopPer || 0,
|
||
exitLoopLPer: data.exitLoopLPer || 0,
|
||
porId: data.porId,
|
||
trId: data.trId,
|
||
entrySpeed: data.entrySpeed || 0,
|
||
technologySpeed: data.technologySpeed || 0,
|
||
exitSpeed: data.exitSpeed || 0,
|
||
matMapList: data.matMapList || []
|
||
}
|
||
this.$forceUpdate()
|
||
},
|
||
|
||
processSignalData(data) {
|
||
// 先记录是否为连续生产中信号
|
||
const repeatProducing =this.lastSignalOperation === 'PRODUCING' && data.operation === 'PRODUCING'
|
||
|
||
this.signalData = data
|
||
const operationText = this.getOperationText(data.operation)
|
||
const autoFlagText = data.autoFlag === 1 ? '[手动]' : '[自动]'
|
||
const config = this.getOperationConfig(data.operation)
|
||
|
||
// 检测到上线、生产中、生产完成等关键操作时,刷新生产计划队列
|
||
if (['ONLINE', 'PRODUCING', 'PRODUCT'].includes(data.operation) ) {
|
||
console.log(`检测到${operationText}操作,刷新生产计划队列...`)
|
||
this.refreshPlanQueue()
|
||
}
|
||
|
||
// 右上角通知(所有操作都显示)
|
||
if(!repeatProducing){
|
||
this.$notify({
|
||
title: `${config.icon} ${config.title}`,
|
||
message: `${autoFlagText} ${operationText}\n钢卷号: ${data.entryMatId}\n计划ID: ${data.planId || '-'}`,
|
||
type: config.type,
|
||
duration: config.duration,
|
||
position: 'top-right',
|
||
showClose: true
|
||
})
|
||
|
||
}
|
||
|
||
// 如果需要弹窗确认(甩尾等重要操作)
|
||
if (config.needAlert) {
|
||
this.$alert(
|
||
`钢卷号: ${data.entryMatId}\n计划ID: ${data.planId || '-'}\n操作类型: ${operationText}\n操作方式: ${autoFlagText}`,
|
||
`${config.icon} ${config.title}`,
|
||
{
|
||
confirmButtonText: '知道了',
|
||
type: config.type,
|
||
center: true
|
||
}
|
||
)
|
||
}
|
||
|
||
// 如果物料映射 WebSocket 未连接,则刷新数据
|
||
if (!this.socketStatus.matmap) {
|
||
this.fetchData()
|
||
}
|
||
|
||
|
||
// 记录本次操作类型
|
||
this.lastSignalOperation = data.operation
|
||
},
|
||
|
||
processMatmapData(data) {
|
||
if (Array.isArray(data)) {
|
||
this.matMapList = data
|
||
this.$forceUpdate()
|
||
}
|
||
},
|
||
|
||
processCalcSetupResult(data) {
|
||
this.calcSetupResult = data
|
||
|
||
if (data.flag) {
|
||
this.$message.success(`计算完成 (Key: ${data.key})`)
|
||
this.showCalcResultDialog = true
|
||
} else {
|
||
this.$message.error(`计算失败 (Key: ${data.key})`)
|
||
}
|
||
},
|
||
|
||
// ============ 辅助方法 ============
|
||
|
||
// Get operation text (English with Chinese comments)
|
||
// 获取操作文本(英文显示,中文注释)
|
||
getOperationText(operation) {
|
||
const operationMap = {
|
||
ONLINE: 'Coil Online', // 钢卷上线
|
||
UNLOAD: 'Unload', // 卸卷
|
||
PRODUCING: 'Producing', // 生产中
|
||
PRODUCT: 'Completed', // 生产完成
|
||
PAY_OVER: 'Pay Over', // 甩尾
|
||
THROW_TAIL: 'Throw Tail', // 甩尾
|
||
ALL_RETURN: 'Full Return', // 整卷回退
|
||
HALF_RETURN: 'Half Return', // 半卷回退
|
||
BLOCK: 'Unload & Block' // 卸卷并封闭
|
||
}
|
||
return operationMap[operation] || operation
|
||
},
|
||
|
||
// 获取操作类型配置
|
||
getOperationConfig(operation) {
|
||
const configs = {
|
||
ONLINE: {
|
||
icon: '🎬',
|
||
title: '钢卷上线通知',
|
||
type: 'success',
|
||
duration: 4000,
|
||
needAlert: false
|
||
},
|
||
UNLOAD: {
|
||
icon: '📤',
|
||
title: '卸卷操作通知',
|
||
type: 'info',
|
||
duration: 3000,
|
||
needAlert: false
|
||
},
|
||
PRODUCING: {
|
||
icon: '⚙️',
|
||
title: '生产状态变更',
|
||
type: 'success',
|
||
duration: 3000,
|
||
needAlert: false
|
||
},
|
||
PRODUCT: {
|
||
icon: '✅',
|
||
title: '生产完成通知',
|
||
type: 'success',
|
||
duration: 4000,
|
||
needAlert: false
|
||
},
|
||
PAY_OVER: {
|
||
icon: '⚠️',
|
||
title: '甩尾操作提示',
|
||
type: 'warning',
|
||
duration: 5000,
|
||
needAlert: true
|
||
},
|
||
THROW_TAIL: {
|
||
icon: '⚠️',
|
||
title: '甩尾操作提示',
|
||
type: 'warning',
|
||
duration: 5000,
|
||
needAlert: true
|
||
},
|
||
ALL_RETURN: {
|
||
icon: '↩️',
|
||
title: '整卷回退通知',
|
||
type: 'warning',
|
||
duration: 4000,
|
||
needAlert: false
|
||
},
|
||
HALF_RETURN: {
|
||
icon: '↩️',
|
||
title: '半卷回退通知',
|
||
type: 'warning',
|
||
duration: 4000,
|
||
needAlert: false
|
||
},
|
||
BLOCK: {
|
||
icon: '🚫',
|
||
title: '封闭操作通知',
|
||
type: 'warning',
|
||
duration: 3000,
|
||
needAlert: false
|
||
}
|
||
}
|
||
return configs[operation] || {
|
||
icon: '📢',
|
||
title: '操作通知',
|
||
type: 'info',
|
||
duration: 3000,
|
||
needAlert: false
|
||
}
|
||
},
|
||
|
||
// ============ API 方法 ============
|
||
|
||
// 刷新生产计划队列(独立方法,可被信号触发)
|
||
async refreshPlanQueue() {
|
||
try {
|
||
// 只查询活动状态的计划,排除已完成和甩尾
|
||
const statuses = ['ONLINE', 'PRODUCING', 'READY', 'NEW']
|
||
const allPlans = []
|
||
|
||
// 并行查询所有状态
|
||
const requests = statuses.map(status =>
|
||
getPlanQueue({ status }).catch(err => {
|
||
console.warn(`查询${status}状态失败:`, err)
|
||
return { code: 500, data: [] }
|
||
})
|
||
)
|
||
|
||
const results = await Promise.all(requests)
|
||
|
||
// 合并所有查询结果
|
||
results.forEach((res, index) => {
|
||
if (res.code === 200 && res.data) {
|
||
allPlans.push(...res.data)
|
||
}
|
||
})
|
||
|
||
// 去重(根据id)
|
||
const uniquePlans = Array.from(
|
||
new Map(allPlans.map(plan => [plan.id, plan])).values()
|
||
)
|
||
|
||
// 排除已完成和甩尾的计划
|
||
const excludeStatuses = ['PRODUCT', 'PAY_OVER', 'COMPLETED', 'CANCELLED', 'UNLOAD']
|
||
const activePlans = uniquePlans.filter(plan =>
|
||
!excludeStatuses.includes(plan.status)
|
||
)
|
||
|
||
// 定义状态优先级
|
||
const statusPriority = {
|
||
'PRODUCING': 1,
|
||
'ONLINE': 2,
|
||
'READY': 3,
|
||
'NEW': 4
|
||
}
|
||
|
||
// 排序
|
||
const sortedPlans = activePlans.sort((a, b) => {
|
||
const priorityA = statusPriority[a.status] || 500
|
||
const priorityB = statusPriority[b.status] || 500
|
||
if (priorityA !== priorityB) {
|
||
return priorityA - priorityB
|
||
}
|
||
return (a.seqid || 0) - (b.seqid || 0)
|
||
})
|
||
|
||
// 最多显示 5 个计划
|
||
this.planQueue = sortedPlans.slice(0, 5)
|
||
|
||
console.log('查询到的总计划数:', uniquePlans.length, '条')
|
||
console.log('活动计划数:', activePlans.length, '条')
|
||
console.log('显示的计划:', this.planQueue.length, '条', this.planQueue)
|
||
|
||
} catch (err) {
|
||
console.error('获取生产计划队列失败:', err)
|
||
this.planQueue = this.generateMockPlanQueue()
|
||
}
|
||
},
|
||
|
||
async fetchData() {
|
||
try {
|
||
this.isLoading = true
|
||
const res = await getTrackMatPosition()
|
||
|
||
// 只在 WebSocket 未连接时使用 API 数据
|
||
if (!this.socketStatus.matmap) {
|
||
this.matMapList = res.data.matMapList || []
|
||
}
|
||
|
||
// 获取生产计划队列
|
||
await this.refreshPlanQueue()
|
||
|
||
} catch (err) {
|
||
console.error('获取钢卷数据失败:', err)
|
||
this.$message.error('获取数据失败,请重试')
|
||
} finally {
|
||
// 延迟关闭加载状态,确保界面渲染完成
|
||
setTimeout(() => {
|
||
this.isLoading = false
|
||
}, 500)
|
||
}
|
||
},
|
||
|
||
// 生成模拟计划队列数据(降级方案)
|
||
generateMockPlanQueue() {
|
||
// 从物料映射中提取有钢卷的数据作为计划队列
|
||
const plansFromMap = this.matMapList
|
||
.filter(item => item.matId)
|
||
.map((item, index) => ({
|
||
planid: item.planId || `PLAN_${index + 1}`,
|
||
coilid: item.matId,
|
||
steelGrade: `Q235-${index + 1}`,
|
||
status: index === 0 ? 'PRODUCING' : (index === 1 ? 'READY' : 'NEW'),
|
||
seqid: index + 1
|
||
}))
|
||
|
||
return plansFromMap.slice(0, 5) // 最多显示5个计划
|
||
},
|
||
|
||
// Get plan status text (English with Chinese comments)
|
||
// 获取计划状态文本(英文显示,中文注释)
|
||
getPlanStatusText(status) {
|
||
const statusMap = {
|
||
NEW: 'New', // 新建
|
||
READY: 'Ready', // 就绪
|
||
ONLINE: 'Online', // 上线
|
||
PRODUCING: 'Producing', // 生产中
|
||
PRODUCT: 'Completed', // 生产完成
|
||
PAY_OVER: 'Pay Over', // 甩尾
|
||
UNLOAD: 'Unload', // 卸卷
|
||
COMPLETED: 'Completed', // 已完成
|
||
CANCELLED: 'Cancelled' // 已取消
|
||
}
|
||
return statusMap[status] || status || 'Unknown' // 未知
|
||
},
|
||
|
||
// 获取计划状态标签类型
|
||
getPlanStatusTagType(status) {
|
||
const typeMap = {
|
||
NEW: 'info',
|
||
READY: 'primary',
|
||
ONLINE: 'warning',
|
||
PRODUCING: 'success',
|
||
PRODUCTING: 'success', // 兼容带T的写法
|
||
PRODUCT: 'info',
|
||
PAY_OVER: '',
|
||
UNLOAD: 'danger',
|
||
COMPLETED: 'info',
|
||
CANCELLED: 'danger'
|
||
}
|
||
return typeMap[status] || 'info'
|
||
},
|
||
|
||
// 获取计划卡片样式类
|
||
getPlanItemClass(plan) {
|
||
const classes = []
|
||
|
||
if (plan.status === 'PRODUCING' || plan.status === 'PRODUCTING') {
|
||
classes.push('plan-producing')
|
||
}
|
||
if (plan.status === 'ONLINE') {
|
||
classes.push('plan-online')
|
||
}
|
||
if (plan.status === 'READY') {
|
||
classes.push('plan-ready')
|
||
}
|
||
if (this.selectedPlan && this.selectedPlan.id === plan.id) {
|
||
classes.push('plan-selected')
|
||
}
|
||
|
||
return classes
|
||
},
|
||
|
||
// 获取计划序号样式类
|
||
getPlanOrderClass(status) {
|
||
if (status === 'PRODUCING' || status === 'PRODUCTING') return 'order-producing'
|
||
if (status === 'ONLINE') return 'order-online'
|
||
if (status === 'READY') return 'order-ready'
|
||
if (status === 'NEW') return 'order-new'
|
||
return ''
|
||
},
|
||
|
||
|
||
// 根据设备ID获取设备信息
|
||
getDeviceInfo(deviceId) {
|
||
return this.matMapList.find(item => item.positionNameEn === deviceId)
|
||
},
|
||
|
||
// 选择设备
|
||
selectDevice(deviceId) {
|
||
const deviceInfo = this.getDeviceInfo(deviceId)
|
||
if (deviceInfo) {
|
||
this.selectedCard = this.selectedCard === deviceInfo ? null : deviceInfo
|
||
if (this.selectedCard) {
|
||
this.adjustForm.current = this.selectedCard.positionNameEn
|
||
} else {
|
||
this.adjustForm.current = null
|
||
}
|
||
// 选择设备时清除选中的计划
|
||
this.selectedPlan = null
|
||
}
|
||
},
|
||
|
||
// 选择生产计划
|
||
selectPlan(plan) {
|
||
if (this.selectedPlan && this.selectedPlan.id === plan.id) {
|
||
this.selectedPlan = null
|
||
} else {
|
||
this.selectedPlan = plan
|
||
// 选择计划时清除选中的设备
|
||
this.selectedCard = null
|
||
|
||
// 调试:查找并显示位置信息
|
||
const position = this.matMapList.find(item =>
|
||
(item.planId && item.planId === plan.planid) ||
|
||
(item.planId && item.planId === plan.planId) ||
|
||
(item.matId && item.matId === plan.coilid) ||
|
||
(item.matId && item.matId === plan.matId)
|
||
)
|
||
|
||
if (position) {
|
||
console.log('✅ 找到计划位置:', position.positionNameCn, '(', position.positionNameEn, ')')
|
||
console.log('匹配详情 - matId:', position.matId, 'planId:', position.planId)
|
||
} else {
|
||
console.log('⚠️ 未找到计划位置')
|
||
console.log('matMapList有', this.matMapList.length, '个设备:', this.matMapList)
|
||
console.log('计划信息 - planid:', plan.planid, 'planId:', plan.planId, 'coilid:', plan.coilid, 'matId:', plan.matId)
|
||
}
|
||
}
|
||
},
|
||
|
||
// 格式化日期时间
|
||
formatDateTime(dateTime) {
|
||
if (!dateTime) return '-'
|
||
const date = new Date(dateTime)
|
||
if (isNaN(date.getTime())) return '-'
|
||
|
||
const year = date.getFullYear()
|
||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||
const day = String(date.getDate()).padStart(2, '0')
|
||
const hours = String(date.getHours()).padStart(2, '0')
|
||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||
|
||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||
},
|
||
|
||
// 获取操作标签类型
|
||
getOperationTagType(operation) {
|
||
const typeMap = {
|
||
ONLINE: 'success',
|
||
UNLOAD: 'info',
|
||
PRODUCING: 'success',
|
||
PRODUCT: 'success',
|
||
PAY_OVER: 'warning',
|
||
THROW_TAIL: 'warning',
|
||
ALL_RETURN: 'warning',
|
||
HALF_RETURN: 'warning',
|
||
BLOCK: 'danger'
|
||
}
|
||
return typeMap[operation] || 'info'
|
||
},
|
||
|
||
// 获取信号面板样式类
|
||
getSignalPanelClass(operation) {
|
||
if (operation === 'PAY_OVER' || operation === 'THROW_TAIL') {
|
||
return 'signal-warning'
|
||
}
|
||
if (operation === 'ONLINE') {
|
||
return 'signal-success'
|
||
}
|
||
if (operation === 'PRODUCT') {
|
||
return 'signal-complete'
|
||
}
|
||
return ''
|
||
},
|
||
|
||
// 获取操作文本样式类
|
||
getOperationTextClass(operation) {
|
||
if (operation === 'PAY_OVER' || operation === 'THROW_TAIL') {
|
||
return 'text-warning'
|
||
}
|
||
if (operation === 'ONLINE' || operation === 'PRODUCING' || operation === 'PRODUCT') {
|
||
return 'text-success'
|
||
}
|
||
return ''
|
||
},
|
||
|
||
// 判断设备是否有实时数据(基于 sourceType & 卷号匹配)
|
||
hasRealtimeData(deviceId) {
|
||
const source = this.getRealtimeSourceData(deviceId)
|
||
if (!source) return false
|
||
if (!this.matchReel(deviceId)) return false
|
||
const meta = DEVICE_META[deviceId]
|
||
if (!meta) return false
|
||
return meta.paramFields.some(f => {
|
||
const val = this.getFieldValueWithAlias(source, f)
|
||
return val !== null && val !== undefined
|
||
})
|
||
},
|
||
|
||
// 判断设备是否工作中(统一依据 paramFields 有效数值/状态)
|
||
isDeviceWorking(deviceId) {
|
||
const info = this.getDeviceInfo(deviceId)
|
||
if (info?.matId) return true
|
||
const source = this.getRealtimeSourceData(deviceId)
|
||
const meta = DEVICE_META[deviceId]
|
||
if (!source || !meta || !this.matchReel(deviceId)) return false
|
||
return meta.paramFields.some(f => {
|
||
const val = this.getFieldValueWithAlias(source, f)
|
||
if (val === null || val === undefined) return false
|
||
if (typeof val === 'number') return val > 0
|
||
if (typeof val === 'string') return val !== '' && val !== '--'
|
||
return true
|
||
})
|
||
},
|
||
|
||
// Get device status text (English with Chinese comments)
|
||
// 获取设备状态文本(英文显示,中文注释)
|
||
getDeviceStatus(deviceId) {
|
||
const deviceInfo = this.getDeviceInfo(deviceId)
|
||
|
||
// If there is material mapping coil ID, display coil ID
|
||
// 如果有物料映射的钢卷号,显示钢卷号
|
||
if (deviceInfo?.matId) {
|
||
return deviceInfo.matId
|
||
}
|
||
|
||
// Determine working status based on real-time data
|
||
// 根据实时数据判断工作状态
|
||
if (this.isDeviceWorking(deviceId)) {
|
||
return 'Working' // 工作中
|
||
}
|
||
|
||
return 'Idle' // 空闲
|
||
},
|
||
|
||
|
||
/**
|
||
* 获取回退信息
|
||
* @param {Number} posIdx - 位置索引(接口必填query参数)
|
||
*/
|
||
fetchReturnData(posIdx) {
|
||
// 1. 校验posIdx
|
||
if (!posIdx && posIdx !== 0) {
|
||
this.returnInfo = {};
|
||
this.returnError = '缺少位置索引(posIdx),无法获取回退信息';
|
||
return;
|
||
}
|
||
|
||
// 2. 加载状态初始化
|
||
this.isLoadingReturn = true;
|
||
this.returnError = '';
|
||
|
||
// 3. 调用回退接口(posIdx作为query参数传递)
|
||
getBackData({ posIdx })
|
||
.then(res => {
|
||
this.isLoadingReturn = false;
|
||
// 接口成功且有数据
|
||
if (res.code === 200 && res.data) {
|
||
this.returnInfo = res.data;
|
||
} else {
|
||
this.returnInfo = {};
|
||
this.returnError = res.msg || '获取回退信息失败';
|
||
}
|
||
})
|
||
.catch(err => {
|
||
this.isLoadingReturn = false;
|
||
this.returnInfo = {};
|
||
this.returnError = '获取回退信息出错,请重试';
|
||
console.error('回退信息接口异常:', err);
|
||
});
|
||
},
|
||
|
||
// 格式化数值
|
||
formatValue(value) {
|
||
if (value === null || value === undefined) return '0.00'
|
||
if (typeof value === 'number') return value.toFixed(2)
|
||
return value
|
||
},
|
||
|
||
|
||
// 确认调整位置
|
||
handleConfirmAdjust() {
|
||
const { current, target } = this.adjustForm
|
||
|
||
if (!current || !target) {
|
||
this.$message.warning('Please select current and target positions') // 请选择当前位置和目标位置
|
||
return
|
||
}
|
||
|
||
const params = {
|
||
currentPos: current,
|
||
targetPos: target,
|
||
}
|
||
|
||
this.$confirm(`Confirm to adjust coil from ${current} to ${target}?`, 'Confirm Adjustment', {
|
||
// 确定将 ${current} 的钢卷调整到 ${target}?, 确认调整
|
||
confirmButtonText: 'Confirm', // 确定
|
||
cancelButtonText: 'Cancel', // 取消
|
||
type: 'warning'
|
||
}).then(() => {
|
||
adjustPosition(params).then(() => {
|
||
this.$message.success('Adjustment successful') // 调整成功
|
||
this.fetchData()
|
||
}).catch(err => {
|
||
console.error('Adjustment failed:', err) // 调整失败
|
||
this.$message.error('Adjustment failed, please retry') // 调整失败,请重试
|
||
})
|
||
}).catch(() => {
|
||
this.$message.info('Adjustment cancelled') // 已取消调整
|
||
})
|
||
},
|
||
|
||
// 打开操作对话框
|
||
handleOperate(row, operation) {
|
||
this.$refs.operateForm?.resetFields()
|
||
console.log(this.selectCard, row.posIdx)
|
||
this.operateMatForm = {
|
||
porIdx: row.posIdx.toString() || '',
|
||
trIdx: row.posIdx.toString() || '',
|
||
planId: row.planId || '',
|
||
entryMatId: row.matId || '',
|
||
planNo: row.planNo || '',
|
||
operation: operation,
|
||
returnMatId: '',
|
||
returnWeight: null,
|
||
returnRemark: '',
|
||
coilLength: null
|
||
}
|
||
this.operateMatStatus = true
|
||
},
|
||
|
||
// 提交操作表单
|
||
submitOperateForm() {
|
||
this.$refs.operateForm.validate(valid => {
|
||
if (valid) {
|
||
operateMat(this.operateMatForm).then(() => {
|
||
this.$message.success('Operation successful') // 操作成功
|
||
this.operateMatStatus = false
|
||
this.fetchData()
|
||
}).catch(err => {
|
||
console.error('Operation failed:', err) // 操作失败
|
||
this.$message.error('Operation failed, please retry') // 操作失败,请重试
|
||
})
|
||
} else {
|
||
this.$message.warning('Please complete the form') // 请完善表单信息
|
||
}
|
||
})
|
||
}
|
||
},
|
||
|
||
async mounted() {
|
||
await this.fetchData()
|
||
this.initWebSockets()
|
||
},
|
||
|
||
beforeDestroy() {
|
||
this.disconnectWebSockets()
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.device-layout {
|
||
background: #f5f5f5;
|
||
border-radius: 4px;
|
||
padding: 15px;
|
||
height: calc(100vh - 60px);
|
||
overflow-y: auto;
|
||
}
|
||
|
||
/* 分区区域 */
|
||
.section-area {
|
||
margin-bottom: 20px;
|
||
background: #fafafa;
|
||
border-radius: 6px;
|
||
padding: 12px;
|
||
border: 1px solid #e8e8e8;
|
||
}
|
||
|
||
.section-header {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 12px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 2px solid #ddd;
|
||
}
|
||
|
||
/* 设备网格 */
|
||
.device-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||
gap: 12px;
|
||
}
|
||
|
||
/* 设备卡片 */
|
||
.device-card {
|
||
background: #fff;
|
||
border: 2px solid #e0e0e0;
|
||
border-radius: 6px;
|
||
padding: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
min-height: 120px;
|
||
}
|
||
|
||
.device-card:hover {
|
||
border-color: #409eff;
|
||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.device-card.active {
|
||
border-color: #409eff;
|
||
background: #ecf5ff;
|
||
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
|
||
}
|
||
|
||
.device-card.has-data {
|
||
border-color: #67c23a;
|
||
}
|
||
|
||
.device-card.has-data:hover {
|
||
border-color: #67c23a;
|
||
box-shadow: 0 2px 8px rgba(103, 194, 58, 0.3);
|
||
}
|
||
|
||
.device-card.has-data.active {
|
||
border-color: #409eff;
|
||
background: #ecf5ff;
|
||
}
|
||
|
||
.device-name {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.device-code {
|
||
font-size: 11px;
|
||
color: #999;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.device-status {
|
||
margin-top: 8px;
|
||
padding-top: 8px;
|
||
border-top: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.coil-id {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.status-working {
|
||
color: #67c23a;
|
||
}
|
||
|
||
.status-idle {
|
||
color: #909399;
|
||
}
|
||
|
||
.realtime-indicator {
|
||
margin-top: 8px;
|
||
padding-top: 8px;
|
||
border-top: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.mini-data {
|
||
font-size: 11px;
|
||
color: #666;
|
||
margin-bottom: 3px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.mini-label {
|
||
color: #999;
|
||
}
|
||
|
||
.mini-value {
|
||
color: #67c23a;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.section-summary {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||
gap: 10px;
|
||
margin: 8px 0 14px;
|
||
}
|
||
|
||
.summary-item {
|
||
background: linear-gradient(135deg, #f7f9fc 0%, #eef2f7 100%);
|
||
border: 1px solid #e0e6ed;
|
||
border-radius: 6px;
|
||
padding: 10px 12px;
|
||
font-size: 13px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.summary-label {
|
||
color: #606266;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.summary-value {
|
||
color: #1f2d3d;
|
||
font-weight: 700;
|
||
}
|
||
|
||
/* 段落色系 */
|
||
.entry-area .section-header { border-bottom-color: #409eff; }
|
||
.entry-area .summary-value { color: #409eff; }
|
||
.furnace-area .section-header { border-bottom-color: #e6a23c; }
|
||
.furnace-area .summary-value { color: #e6a23c; }
|
||
.coat-area .section-header { border-bottom-color: #67c23a; }
|
||
.coat-area .summary-value { color: #67c23a; }
|
||
.exit-area .section-header { border-bottom-color: #909399; }
|
||
.exit-area .summary-value { color: #909399; }
|
||
|
||
.panel {
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
padding: 15px;
|
||
margin-bottom: 15px;
|
||
background: #fff;
|
||
}
|
||
|
||
.panel-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 12px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
|
||
.info-table {
|
||
width: 100%;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.info-table tr {
|
||
border-bottom: 1px solid #f5f5f5;
|
||
}
|
||
|
||
.info-table tr:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.info-table td {
|
||
padding: 8px 0;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.info-table td:first-child {
|
||
color: #666;
|
||
width: 80px;
|
||
}
|
||
|
||
.info-table td:last-child {
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.empty-msg {
|
||
text-align: center;
|
||
padding: 30px;
|
||
color: #999;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.realtime-cards {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 10px;
|
||
}
|
||
|
||
.data-item {
|
||
border: 1px solid #e8e8e8;
|
||
border-radius: 4px;
|
||
padding: 12px;
|
||
text-align: center;
|
||
background: #fafafa;
|
||
}
|
||
|
||
.data-label {
|
||
font-size: 12px;
|
||
color: #666;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.data-value {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 4px;
|
||
font-family: 'Arial', sans-serif;
|
||
}
|
||
|
||
.data-unit {
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
|
||
.btn-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.action-btn {
|
||
margin-bottom: 8px;
|
||
width: 80px;
|
||
height: 40px;
|
||
}
|
||
|
||
/* 加载状态 */
|
||
.loading-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 100px 0;
|
||
color: #409eff;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.loading-container .el-icon-loading {
|
||
font-size: 40px;
|
||
margin-bottom: 15px;
|
||
animation: rotating 2s linear infinite;
|
||
}
|
||
|
||
@keyframes rotating {
|
||
from { transform: rotate(0deg); }
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* 生产计划队列面板 */
|
||
.plan-queue-panel {
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
margin-bottom: 15px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 队列头部 */
|
||
.queue-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 15px;
|
||
background: #f5f7fa;
|
||
border-bottom: 1px solid #e4e7ed;
|
||
}
|
||
|
||
.queue-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.queue-title i {
|
||
font-size: 16px;
|
||
color: #409eff;
|
||
}
|
||
|
||
/* Tab切换按钮组 */
|
||
.queue-tabs {
|
||
display: flex;
|
||
gap: 0;
|
||
}
|
||
|
||
.queue-tab {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
padding: 6px 12px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
font-size: 13px;
|
||
color: #606266;
|
||
background: transparent;
|
||
border-bottom: 2px solid transparent;
|
||
user-select: none;
|
||
}
|
||
|
||
.queue-tab:hover {
|
||
color: #409eff;
|
||
}
|
||
|
||
.queue-tab.active {
|
||
color: #409eff;
|
||
font-weight: 600;
|
||
border-bottom-color: #409eff;
|
||
}
|
||
|
||
.queue-tab i {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.queue-tab .tab-badge {
|
||
margin-left: 4px;
|
||
}
|
||
|
||
.plan-queue-panel .panel-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 15px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
/* 右侧纵向生产计划列表 */
|
||
.plan-list-vertical {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.plan-item-vertical {
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 6px;
|
||
padding: 12px;
|
||
background: #f7f9fc;
|
||
transition: all 0.2s ease;
|
||
cursor: pointer;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.plan-item-vertical:hover {
|
||
border-color: #409eff;
|
||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.plan-item-vertical.plan-producing {
|
||
background: #f0f9ff;
|
||
border-color: #67c23a;
|
||
animation: pulse-green 1.5s infinite;
|
||
}
|
||
|
||
.plan-item-vertical.plan-online {
|
||
background: #fdf6ec;
|
||
border-color: #e6a23c;
|
||
}
|
||
|
||
.plan-item-vertical.plan-ready {
|
||
background: #ecf5ff;
|
||
border-color: #409eff;
|
||
}
|
||
|
||
.plan-item-vertical.plan-selected {
|
||
border-color: #409eff;
|
||
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
|
||
}
|
||
|
||
.plan-item-top {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.plan-order-dot {
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: 700;
|
||
color: #fff;
|
||
background: #909399;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.plan-order-dot.order-producing { background: #67c23a; animation: blink-green-dot 1.2s infinite; }
|
||
.plan-order-dot.order-online { background: #e6a23c; }
|
||
.plan-order-dot.order-ready { background: #409eff; }
|
||
.plan-order-dot.order-new { background: #909399; }
|
||
|
||
.plan-title-text {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
font-size: 13px;
|
||
color: #303133;
|
||
}
|
||
|
||
.plan-id { font-weight: 600; }
|
||
.plan-coil { color: #606266; }
|
||
|
||
.plan-item-bottom {
|
||
display: flex;
|
||
gap: 16px;
|
||
font-size: 12px;
|
||
color: #606266;
|
||
}
|
||
|
||
.plan-queue-panel .panel-title i {
|
||
margin-right: 8px;
|
||
color: #409eff;
|
||
}
|
||
|
||
.plan-queue-list {
|
||
max-height: none;
|
||
overflow-y: visible;
|
||
padding: 15px 20px;
|
||
}
|
||
|
||
.plan-items {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
padding-bottom: 5px;
|
||
}
|
||
|
||
.plan-item {
|
||
min-width: 220px;
|
||
background: #f5f7fa;
|
||
border: 2px solid #e4e7ed;
|
||
border-radius: 6px;
|
||
padding: 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
transition: all 0.3s;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.plan-item:hover {
|
||
border-color: #409eff;
|
||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.plan-item.plan-producing {
|
||
background: #f0f9ff;
|
||
border-color: #67c23a;
|
||
animation: pulse-green 1.5s infinite;
|
||
}
|
||
|
||
.plan-item.plan-online {
|
||
background: #fdf6ec;
|
||
border-color: #e6a23c;
|
||
}
|
||
|
||
.plan-item.plan-ready {
|
||
background: #ecf5ff;
|
||
border-color: #409eff;
|
||
}
|
||
|
||
.plan-item.plan-selected {
|
||
background: #f0f9ff;
|
||
border-color: #409eff;
|
||
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
|
||
}
|
||
|
||
/* 绿色闪烁动画 */
|
||
@keyframes pulse-green {
|
||
0%, 100% {
|
||
border-color: #67c23a;
|
||
box-shadow: 0 0 0 0 rgba(103, 194, 58, 0.4);
|
||
}
|
||
50% {
|
||
border-color: #85ce61;
|
||
box-shadow: 0 0 0 4px rgba(103, 194, 58, 0.6), 0 0 15px rgba(103, 194, 58, 0.4);
|
||
}
|
||
}
|
||
|
||
.plan-order {
|
||
width: 28px;
|
||
height: 28px;
|
||
background: #909399;
|
||
color: #fff;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.plan-order.order-producing {
|
||
background: #67c23a;
|
||
animation: blink-green-dot 1.2s infinite;
|
||
}
|
||
|
||
.plan-order.order-online {
|
||
background: #e6a23c;
|
||
}
|
||
|
||
.plan-order.order-ready {
|
||
background: #409eff;
|
||
}
|
||
|
||
.plan-order.order-new {
|
||
background: #909399;
|
||
}
|
||
|
||
/* 绿色点闪烁动画 */
|
||
@keyframes blink-green-dot {
|
||
0%, 100% {
|
||
background: #67c23a;
|
||
box-shadow: 0 0 5px rgba(103, 194, 58, 0.5);
|
||
}
|
||
50% {
|
||
background: #85ce61;
|
||
box-shadow: 0 0 10px rgba(103, 194, 58, 0.8);
|
||
}
|
||
}
|
||
|
||
.plan-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.plan-row {
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.plan-label {
|
||
color: #909399;
|
||
min-width: 60px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.plan-value {
|
||
color: #303133;
|
||
font-weight: 500;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.plan-status {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
/* WebSocket 状态栏 */
|
||
.ws-status-bar {
|
||
display: flex;
|
||
gap: 15px;
|
||
padding: 10px;
|
||
background: #f5f7fa;
|
||
border-radius: 4px;
|
||
margin-bottom: 10px;
|
||
align-items: center;
|
||
}
|
||
|
||
.ws-status-bar i {
|
||
font-size: 18px;
|
||
color: #606266;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* 分区头部增强 */
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.section-info {
|
||
font-size: 13px;
|
||
color: #67c23a;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 信号信息面板 */
|
||
.signal-info {
|
||
padding: 10px 0;
|
||
}
|
||
|
||
.signal-info.signal-warning {
|
||
background: #fef0f0;
|
||
padding: 15px;
|
||
border-radius: 4px;
|
||
border: 1px solid #fde2e2;
|
||
}
|
||
|
||
.signal-info.signal-success {
|
||
background: #f0f9ff;
|
||
padding: 15px;
|
||
border-radius: 4px;
|
||
border: 1px solid #c6e2ff;
|
||
}
|
||
|
||
.signal-info.signal-complete {
|
||
background: #f0f9ff;
|
||
padding: 15px;
|
||
border-radius: 4px;
|
||
border: 1px solid #b3e19d;
|
||
}
|
||
|
||
.signal-detail {
|
||
margin-top: 10px;
|
||
font-size: 13px;
|
||
line-height: 2;
|
||
}
|
||
|
||
.signal-detail div {
|
||
padding: 4px 0;
|
||
}
|
||
|
||
.text-warning {
|
||
color: #e6a23c;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.text-success {
|
||
color: #67c23a;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* 计算结果头部 */
|
||
.calc-result-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 15px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 1px solid #ebeef5;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* 生产计划详情 */
|
||
.plan-detail-content {
|
||
overflow-y: auto;
|
||
}
|
||
|
||
/* 位置信息提示卡片 */
|
||
.position-alert {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px 15px;
|
||
margin-bottom: 15px;
|
||
background: #f0f9ff;
|
||
border: 1px solid #b3d8ff;
|
||
border-left: 4px solid #409eff;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.position-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 40px;
|
||
height: 40px;
|
||
background: #409eff;
|
||
border-radius: 50%;
|
||
color: #fff;
|
||
font-size: 20px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.position-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.position-label {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.position-name {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.position-code {
|
||
font-size: 12px;
|
||
color: #606266;
|
||
font-family: 'Courier New', monospace;
|
||
}
|
||
|
||
.plan-time-info {
|
||
margin-top: 15px;
|
||
padding-top: 15px;
|
||
border-top: 1px solid #ebeef5;
|
||
}
|
||
|
||
.info-subtitle {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #606266;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.panel-title {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.panel-title i {
|
||
margin-right: 5px;
|
||
}
|
||
</style>
|