Files
l2-g30/src/views/l2/track/rect.vue
砂糖 043098172f refactor(l2/track): 移除注释的熔炉相关指标代码
清理不再使用的熔炉温度、压力等指标代码注释,保持代码整洁
2026-01-03 09:08:20 +08:00

2511 lines
82 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="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>