<template>
|
<div class="rgv-monitor">
|
<div class="monitor-header">
|
<div class="title">设备状态监控</div>
|
<div class="control-panel">
|
<!-- 监控类型切换 -->
|
<div class="monitor-type-switch">
|
<el-button-group>
|
<el-button :type="currentMonitorType === 'inbound' ? 'primary' : ''" @click="switchMonitorType('inbound')">
|
入库监控
|
</el-button>
|
<el-button :type="currentMonitorType === 'outbound' ? 'primary' : ''"
|
@click="switchMonitorType('outbound')">
|
出库监控
|
</el-button>
|
<el-button :type="currentMonitorType === 'safetydoor' ? 'primary' : ''"
|
@click="switchMonitorType('safetydoor')">
|
安全门监控
|
</el-button>
|
<el-button :type="currentMonitorType === 'platform' ? 'primary' : ''"
|
@click="switchMonitorType('platform')">
|
站台监控
|
</el-button>
|
</el-button-group>
|
</div>
|
|
<!-- 一键操作按钮(安全门和站台监控时不显示) -->
|
<div class="one-click-operations"
|
v-if="currentMonitorType !== 'safetydoor' && currentMonitorType !== 'platform'">
|
<el-button-group>
|
<el-button type="primary"
|
@mousedown="startOneClickInitHold"
|
@mouseup="cancelOneClickInitHold"
|
@mouseleave="cancelOneClickInitHold"
|
@touchstart.prevent="startOneClickInitHold"
|
@touchend="cancelOneClickInitHold"
|
@touchcancel="cancelOneClickInitHold"
|
:loading="oneClickLoading.init">
|
一键初始化
|
</el-button>
|
<el-button type="warning" @click="handleOneClickOperation('reset')" :loading="oneClickLoading.reset">
|
一键复位
|
</el-button>
|
<el-button type="success" @click="handleOneClickOperation('start')" :loading="oneClickLoading.start">
|
一键启动
|
</el-button>
|
<el-button type="danger" @click="handleOneClickOperation('stop')" :loading="oneClickLoading.stop">
|
一键暂停
|
</el-button>
|
</el-button-group>
|
</div>
|
|
<el-button :type="isMonitoring ? 'danger' : 'primary'" @click="toggleMonitoring">
|
{{ isMonitoring ? '停止监控' : '启动监控' }}
|
</el-button>
|
<el-button type="warning" @click="refreshData">刷新数据</el-button>
|
</div>
|
</div>
|
|
<!-- 监控状态显示 -->
|
<div class="monitor-status">
|
<el-alert :title="getMonitorStatusTitle()" :type="isMonitoring ? 'success' : 'info'" :closable="false"
|
show-icon />
|
</div>
|
|
<div class="devices-container">
|
<!-- 动态渲染设备 -->
|
<div class="device-card" v-for="(deviceData, deviceKey) in currentDeviceData" :key="deviceKey">
|
<div class="device-header">
|
<h3>{{ getDeviceDisplayName(deviceKey) }}</h3>
|
<div class="status-indicator" :class="getStatusClass(deviceData)"></div>
|
</div>
|
<div class="device-content">
|
<template v-if="deviceData && Object.keys(deviceData).length > 0">
|
<!-- RGV设备显示 -->
|
<template v-if="currentMonitorType !== 'safetydoor' && currentMonitorType !== 'platform'">
|
<!-- 初始化状态显示 -->
|
<div class="data-row" v-if="deviceData['初始化未完成标志位'] !== undefined">
|
<span class="label">初始化状态:</span>
|
<span class="value" :class="deviceData['初始化未完成标志位'] === 1 ? 'fault-text' : ''">
|
{{ deviceData['初始化未完成标志位'] === 0 ? '已完成' : '未完成' }}
|
</span>
|
</div>
|
|
<div class="data-row" v-for="(value, key) in deviceData" :key="key" v-if="key !== '初始化未完成标志位'">
|
<span class="label">{{ getFieldDisplayName(key) }}:</span>
|
<span class="value" :class="getValueClass(key, value, deviceKey)">
|
{{ getFormattedValue(key, value, deviceKey) }}
|
<!-- 为上升信号和下降信号添加指示灯 -->
|
<span v-if="key === '上升信号到位'" class="signal-indicator" :class="getSignalClass(value)"></span>
|
<span v-if="key === '下降信号到位'" class="signal-indicator" :class="getSignalClass(value)"></span>
|
</span>
|
</div>
|
</template>
|
|
<!-- 安全门设备显示 -->
|
<template v-else-if="currentMonitorType === 'safetydoor'">
|
<div class="data-row" v-for="(value, key) in deviceData" :key="key">
|
<span class="label">{{ getFieldDisplayName(key) }}:</span>
|
<span class="value" :class="getSafetyDoorValueClass(key, value)">
|
{{ getSafetyDoorFormattedValue(key, value) }}
|
<!-- 安全门状态指示灯 -->
|
<span v-if="key === '安全门指示灯状态'" class="safety-light" :class="getSafetyLightClass(value)"></span>
|
<span v-if="key === '安全门急停状态'" class="emergency-stop" :class="getEmergencyStopClass(value)"></span>
|
</span>
|
</div>
|
</template>
|
|
<!-- 站台设备显示 -->
|
<template v-else-if="currentMonitorType === 'platform'">
|
<div class="data-row" v-for="(value, key) in deviceData" :key="key">
|
<span class="label">{{ getFieldDisplayName(key) }}:</span>
|
<span class="value" :class="getPlatformValueClass(key, value)">
|
{{ getPlatformFormattedValue(key, value) }}
|
<!-- 站台光电信号指示灯 -->
|
<span v-if="key === '光电信号'" class="signal-indicator" :class="getSignalClass(value)"></span>
|
</span>
|
</div>
|
</template>
|
</template>
|
<div v-else class="no-data">
|
设备未连接或暂无数据
|
</div>
|
|
<!-- 操作按钮区域(安全门和站台监控时不显示) -->
|
<div class="operation-buttons"
|
v-if="currentMonitorType !== 'safetydoor' && currentMonitorType !== 'platform'">
|
<!-- 子车操作按钮 -->
|
<template v-if="getDeviceType(deviceKey) === 'child'">
|
<el-button type="primary" size="small"
|
@mousedown="startInitHold(deviceKey)"
|
@mouseup="cancelInitHold(deviceKey)"
|
@mouseleave="cancelInitHold(deviceKey)"
|
@touchstart.prevent="startInitHold(deviceKey)"
|
@touchend="cancelInitHold(deviceKey)"
|
@touchcancel="cancelInitHold(deviceKey)"
|
:loading="loadingStates[deviceKey]?.cs">
|
{{ getInitializationButtonText(deviceData) }}
|
</el-button>
|
<el-button :type="getModeButtonType(deviceData)" size="small" @click="handleModeToggle(deviceKey)"
|
:loading="loadingStates[deviceKey]?.modeToggle">
|
{{ getCurrentModeText(deviceData) }}
|
</el-button>
|
<el-button type="warning" size="small" @click="handleOperation(deviceKey, 'fw')"
|
:loading="loadingStates[deviceKey]?.fw">
|
复位
|
</el-button>
|
<!-- 长按:上升/下降(按下=1,松开=0) -->
|
<el-button type="success" size="small"
|
@pointerdown.prevent="handleLiftPress(deviceKey, 'ss')"
|
@pointerup="handleLiftRelease(deviceKey, 'ss')"
|
@pointercancel="handleLiftRelease(deviceKey, 'ss')"
|
:disabled="!isManualMode(deviceData) || deviceData['上升信号到位'] === 1"
|
:loading="activeLift && activeLift.deviceKey === deviceKey && activeLift.operationType === 'ss'">
|
上升
|
</el-button>
|
<el-button type="info" size="small"
|
@pointerdown.prevent="handleLiftPress(deviceKey, 'xj')"
|
@pointerup="handleLiftRelease(deviceKey, 'xj')"
|
@pointercancel="handleLiftRelease(deviceKey, 'xj')"
|
:disabled="!isManualMode(deviceData) || deviceData['下降信号到位'] === 1"
|
:loading="activeLift && activeLift.deviceKey === deviceKey && activeLift.operationType === 'xj'">
|
下降
|
</el-button>
|
<!-- 保障:手动停止按钮(点击即发0) -->
|
<el-button type="warning" size="small"
|
@click="stopLift(deviceKey, 'ss')"
|
:disabled="!isManualMode(deviceData)">
|
停止上升
|
</el-button>
|
<el-button type="warning" size="small"
|
@click="stopLift(deviceKey, 'xj')"
|
:disabled="!isManualMode(deviceData)">
|
停止下降
|
</el-button>
|
</template>
|
|
<!-- 母车操作按钮 -->
|
<template v-else-if="getDeviceType(deviceKey) === 'mother'">
|
<el-button type="primary" size="small"
|
@mousedown="startInitHold(deviceKey)"
|
@mouseup="cancelInitHold(deviceKey)"
|
@mouseleave="cancelInitHold(deviceKey)"
|
@touchstart.prevent="startInitHold(deviceKey)"
|
@touchend="cancelInitHold(deviceKey)"
|
@touchcancel="cancelInitHold(deviceKey)"
|
:loading="loadingStates[deviceKey]?.cs">
|
{{ getInitializationButtonText(deviceData) }}
|
</el-button>
|
<el-button :type="getModeButtonType(deviceData)" size="small" @click="handleModeToggle(deviceKey)"
|
:loading="loadingStates[deviceKey]?.modeToggle">
|
{{ getCurrentModeText(deviceData) }}
|
</el-button>
|
<el-button type="warning" size="small" @click="handleOperation(deviceKey, 'fw')"
|
:loading="loadingStates[deviceKey]?.fw">
|
复位
|
</el-button>
|
<!-- 雷达开关按钮 -->
|
<el-button :type="getRadarButtonType(deviceData)" size="small" @click="handleRadarToggle(deviceKey)"
|
:loading="loadingStates[deviceKey]?.radarToggle" :disabled="!isManualMode(deviceData)">
|
{{ getRadarButtonText(deviceData) }}
|
</el-button>
|
</template>
|
|
<!-- 原料车操作按钮 -->
|
<template v-else-if="getDeviceType(deviceKey) === 'material'">
|
<el-button type="primary" size="small"
|
@mousedown="startInitHold(deviceKey)"
|
@mouseup="cancelInitHold(deviceKey)"
|
@mouseleave="cancelInitHold(deviceKey)"
|
@touchstart.prevent="startInitHold(deviceKey)"
|
@touchend="cancelInitHold(deviceKey)"
|
@touchcancel="cancelInitHold(deviceKey)"
|
:loading="loadingStates[deviceKey]?.cs">
|
{{ getInitializationButtonText(deviceData) }}
|
</el-button>
|
<el-button :type="getModeButtonType(deviceData)" size="small" @click="handleModeToggle(deviceKey)"
|
:loading="loadingStates[deviceKey]?.modeToggle">
|
{{ getCurrentModeText(deviceData) }}
|
</el-button>
|
<el-button type="warning" size="small" @click="handleOperation(deviceKey, 'fw')"
|
:loading="loadingStates[deviceKey]?.fw">
|
复位
|
</el-button>
|
<!-- 雷达开关按钮 -->
|
<el-button :type="getRadarButtonType(deviceData)" size="small" @click="handleRadarToggle(deviceKey)"
|
:loading="loadingStates[deviceKey]?.radarToggle" :disabled="!isManualMode(deviceData)">
|
{{ getRadarButtonText(deviceData) }}
|
</el-button>
|
<!-- 长按:上升/下降(按下=1,松开=0) -->
|
<el-button type="success" size="small"
|
@pointerdown.prevent="handleLiftPress(deviceKey, 'ss')"
|
@pointerup="handleLiftRelease(deviceKey, 'ss')"
|
@mousedown.prevent="handleLiftPress(deviceKey, 'ss')"
|
@mouseup="handleLiftRelease(deviceKey, 'ss')"
|
@touchstart.prevent="handleLiftPress(deviceKey, 'ss')"
|
@touchend="handleLiftRelease(deviceKey, 'ss')"
|
@touchcancel="handleLiftRelease(deviceKey, 'ss')"
|
:disabled="!isManualMode(deviceData) || deviceData['上升信号到位'] === 1"
|
:loading="activeLift && activeLift.deviceKey === deviceKey && activeLift.operationType === 'ss'">
|
上升
|
</el-button>
|
<el-button type="info" size="small"
|
@pointerdown.prevent="handleLiftPress(deviceKey, 'xj')"
|
@pointerup="handleLiftRelease(deviceKey, 'xj')"
|
@pointercancel="handleLiftRelease(deviceKey, 'xj')"
|
:disabled="!isManualMode(deviceData) || deviceData['下降信号到位'] === 1"
|
:loading="activeLift && activeLift.deviceKey === deviceKey && activeLift.operationType === 'xj'">
|
下降
|
</el-button>
|
<!-- 保障:手动停止按钮(点击即发0) -->
|
<el-button type="warning" size="small"
|
@click="stopLift(deviceKey, 'ss')"
|
:disabled="!isManualMode(deviceData)">
|
停止上升
|
</el-button>
|
<el-button type="warning" size="small"
|
@click="stopLift(deviceKey, 'xj')"
|
:disabled="!isManualMode(deviceData)">
|
停止下降
|
</el-button>
|
</template>
|
</div>
|
|
<!-- 地址操作区域(安全门和站台监控时不显示) -->
|
<div class="address-operation"
|
v-if="currentMonitorType !== 'safetydoor' && currentMonitorType !== 'platform'">
|
<div class="address-title">地址操作</div>
|
<div class="address-input-group">
|
<el-select v-model="addressValues[deviceKey]" placeholder="选择目标地址" size="small"
|
style="width: 200px; margin-right: 10px;">
|
<el-option v-for="address in getDeviceAddresses(deviceKey)" :key="address.value" :label="address.label"
|
:value="address.value" />
|
</el-select>
|
<el-button type="info" size="small" @click="handleAddressOperation(deviceKey)"
|
:loading="loadingStates[deviceKey]?.dz" :disabled="!addressValues[deviceKey]">
|
前往地址
|
</el-button>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<!-- 全局操作面板(安全门和站台监控时不显示) -->
|
<div class="operation-panel" v-if="currentMonitorType !== 'safetydoor' && currentMonitorType !== 'platform'">
|
<!-- <el-button type="primary" @click="showOperationDialog = true">高级操作</el-button> -->
|
</div>
|
|
<!-- 操作对话框 -->
|
<el-dialog v-model="showOperationDialog" title="设备高级操作" width="600px">
|
<div>设备初始化、写入参数等高级操作界面</div>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script>
|
export default {
|
name: 'RgvMonitor',
|
data() {
|
return {
|
currentMonitorType: 'inbound', // 'inbound' 或 'outbound' 或 'safetydoor' 或 'platform'
|
isMonitoring: false,
|
monitoringLoading: false,
|
showOperationDialog: false,
|
pollingTimer: null,
|
loadingStates: {},
|
addressValues: {},
|
// 长按初始化相关
|
holdTimers: {}, // 设备级初始化长按定时器
|
oneClickInitTimer: null, // 一键初始化长按定时器
|
holdDuration: 3000, // 按住3秒触发
|
activeLift: null, // { deviceKey, operationType } 当前按下中的升降操作
|
monitoringPaused: false, // 长按期间暂停监控刷新
|
liftPressPromises: {}, // key: `${deviceKey}-${operationType}` -> Promise for press(1)
|
signalPollingTimer: null, // 升降长按期间的轻量信号轮询
|
signalPollInterval: 1200, // 轻量信号轮询间隔(ms)
|
lastLiftPressAt: {}, // 记录每个(deviceKey-op)最后按下的时间戳
|
liftReleaseInProgress: {}, // 正在发送释放(0)的标记,防重复
|
|
// 点击式升降控制状态
|
activeLiftStates: {}, // { [deviceKey]: { ss: boolean, xj: boolean } }
|
liftAutoStopTimeouts: {}, // { [deviceKey-op]: timeoutId }
|
liftCommandTimeoutMs: 10000, // 升降指令最大持续时间,超时自动停止
|
|
// 一键操作加载状态
|
oneClickLoading: {
|
init: false,
|
reset: false,
|
start: false,
|
stop: false
|
},
|
|
// 设备数据
|
inboundDeviceData: {
|
rgV101: null,
|
rgV103: null,
|
rgV104: null,
|
rgV105: null,
|
rgV107: null,
|
rgV108: null,
|
rgV109: null,
|
},
|
outboundDeviceData: {
|
rgV116: null,
|
rgV115: null,
|
rgV111: null,
|
rgV112: null,
|
rgV110: null,
|
rgV114: null,
|
rgV118: null
|
},
|
safetyDoorDeviceData: {
|
aqm001: null,
|
aqm002: null,
|
aqm003: null
|
},
|
platformDeviceData: {
|
'1001': null,
|
'1002': null,
|
'2016': null,
|
'2017': null,
|
'2018': null,
|
'2019': null,
|
'1021': null,
|
'1061': null,
|
'1131': null,
|
'1171': null
|
},
|
|
// 字段显示名称映射
|
fieldDisplayNames: {
|
'工作模式': '工作模式',
|
'当前位置': '当前位置',
|
'有货状态': '有货状态',
|
'目标地址': '目标地址',
|
'任务状态': '任务状态',
|
'rgV任务编号': '任务编号',
|
'故障代码': '故障代码',
|
'上升信号到位': '上升信号',
|
'下降信号到位': '下降信号',
|
'初始化未完成标志位': '初始化状态',
|
'雷达状态': '雷达状态',
|
'货叉状态': '货叉状态',
|
// 安全门字段
|
'安全门指示灯状态': '指示灯状态',
|
'安全门请求开门': '请求开门',
|
'安全门断电状态': '断电状态',
|
'安全门急停状态': '急停状态',
|
'安全门锁状态': '门锁状态',
|
'安全门复位状态': '复位状态',
|
'报警信息': '报警信息',
|
'开门信息': '开门信息',
|
// 站台字段
|
'光电信号': '光电信号',
|
'任务id': '任务ID'
|
},
|
|
// 设备显示名称映射
|
deviceDisplayNames: {
|
// 入库设备
|
'rgV101': 'RGV101 - 原料入库车',
|
'rgV103': 'RGV103 - 母车',
|
'rgV104': 'RGV104 - 子车',
|
'rgV105': 'RGV105 - 母车',
|
'rgV107': 'RGV107 - 子车',
|
'rgV108': 'RGV108 - 母车',
|
'rgV109': 'RGV109 - 母车',
|
// 出库设备
|
'rgV110': 'RGV110 - 母车',
|
'rgV111': 'RGV111 - 子车',
|
'rgV112': 'RGV112 - 母车',
|
'rgV114': 'RGV114 - 母车',
|
'rgV115': 'RGV115 - 母车',
|
'rgV116': 'RGV116 - 子车',
|
'rgV118': 'RGV118 - 原料出库车',
|
// 安全门设备
|
'aqm001': 'AQM001 - 1#安全门',
|
'aqm002': 'AQM002 - 2#安全门',
|
'aqm003': 'AQM003 - 3#安全门',
|
// 站台设备
|
'1001': '1001 - 1#站台',
|
'1002': '1002 - 2#站台',
|
'2016': '2016 - 3#站台',
|
'2017': '2017 - 4#站台',
|
'2018': '2018 - 5#站台',
|
'2019': '2019 - 6#站台',
|
'1021': '1021 - 7#站台',
|
'1061': '1061 - 8#站台',
|
'1131': '1131 - 9#站台',
|
'1171': '1171 - 10#站台'
|
},
|
|
// 操作类型映射
|
operationTypes: {
|
'cs': '初始化',
|
'sd': '切换到手动模式',
|
'zd': '切换到自动模式',
|
'dz': '前往地址',
|
'kld': '开雷达',
|
'gld': '关雷达',
|
'fw': '复位',
|
'ss': '上升',
|
'xj': '下降',
|
},
|
|
// 报警代码映射
|
motherCarAlarmCodes: {
|
0: '无报警',
|
1: 'RGV小车急停被按下',
|
2: '前进限位报警',
|
3: '后退限位报警',
|
4: 'PLC模块故障',
|
5: 'PLC扩展模块故障',
|
6: 'RGV长时间空转故障',
|
7: '目的地不等于实际位置故障',
|
8: '与总控通讯故障',
|
9: '行走变频器故障',
|
10: '取货时自身有货物报警',
|
11: '放货时自身无货物报警',
|
12: '停止时位置过冲报警'
|
},
|
|
childCarAlarmCodes: {
|
0: '无报警',
|
1: 'RGV小车急停被按下',
|
2: '前进限位报警',
|
3: '后退限位报警',
|
4: 'PLC模块故障',
|
5: 'PLC扩展模块故障',
|
6: '扫码定位故障',
|
7: 'RGV长时间空转故障',
|
8: '目的地不等于实际位置故障',
|
9: '与总控通讯故障',
|
10: '行走变频器故障',
|
11: '液压单元过载保护故障',
|
12: '液压上升超时报警',
|
13: '液压下降超时报警',
|
14: '取货时自身有货物报警',
|
15: '放货时自身无货物报警',
|
16: '取货检测不到货物报警'
|
},
|
|
// 在 data() 中修改 materialCarAlarmCodes
|
materialCarAlarmCodes: {
|
0: '无报警',
|
1: 'RGV小车急停被按下',
|
2: '正转雷达报警',
|
3: '反转雷达报警',
|
4: '前进限位报警',
|
5: '后退限位报警',
|
6: '',
|
7: 'PLC模块故障',
|
8: 'PLC扩展模块故障',
|
9: '称重模块故障',
|
10: '扫码定位故障',
|
11: 'RGV长时间空转故障',
|
12: '目的地不等于实际位置故障',
|
13: '与总控通讯故障',
|
14: '前雷达屏蔽警告',
|
15: '后雷达屏蔽警告',
|
16: '行走变频器故障',
|
17: '伸缩叉变频器故障',
|
18: '液压单元过载保护故障',
|
19: '液压上升超时报警',
|
20: '液压下降超时报警',
|
21: '伸缩叉伸出超时报警',
|
22: '伸缩叉缩回超时报警',
|
23: '外形检测报警',
|
24: '称重超重报警',
|
25: '货叉伸出极限限位报警',
|
26: '货叉缩回极限限位报警',
|
27: '取货时自身有货物报警',
|
28: '放货时自身无货物报警',
|
29: '货叉未回到初始位报警',
|
30: '触发仅移动命令时货叉不在初始位报警',
|
31: '货叉到达初始位但中位传感器未检测到报警',
|
32: '行走轴没到位禁止货叉伸出',
|
33: '取货异常报警',
|
34: '放货异常报警',
|
35: '外型检测-前超出报警',
|
36: '外型检测-后超出报警',
|
37: '外型检测-左超出报警',
|
38: '外型检测-右超出报警',
|
39: '外型检测-上超出报警'
|
},
|
|
// 安全门指示灯状态映射
|
safetyDoorLightCodes: {
|
0: '无输出',
|
1: '红灯常亮+蜂鸣',
|
2: '绿灯常亮',
|
3: '黄灯闪烁(2HZ)',
|
4: '黄灯常亮'
|
},
|
|
// 设备类型分类
|
deviceTypes: {
|
// 入库设备
|
motherCars: ['rgV103', 'rgV105', 'rgV108', 'rgV109'],
|
childCars: ['rgV104', 'rgV107'],
|
materialCars: ['rgV101'],
|
// 出库设备
|
outboundMotherCars: ['rgV110', 'rgV112', 'rgV114', 'rgV115'],
|
outboundChildCars: ['rgV111', 'rgV116'],
|
outboundMaterialCars: ['rgV118'],
|
// 安全门设备
|
safetyDoors: ['aqm001', 'aqm002', 'aqm003'],
|
// 站台设备
|
platforms: ['1001', '1002', '2016', '2017', '2018', '2019', '1021', '1061', '1131', '1171']
|
},
|
|
// 设备地址映射
|
deviceAddresses: {
|
// 入库设备地址
|
'rgV101': [
|
{ value: '1001', label: '1001 - 原料入库位' },
|
{ value: '1002', label: '1002 - 原料出库位' },
|
{ value: '1021', label: '1021 - 中转位' }
|
],
|
'rgV103': [
|
{ value: '1031', label: '1031 - 母车停车位1' },
|
{ value: '1032', label: '1032 - 母车停车位2' }
|
],
|
'rgV104': [
|
{ value: '3', label: '停靠点1' },
|
{ value: '5', label: '停靠点2' },
|
{ value: '1031', label: '1031 - 子车取货位' },
|
{ value: '1051', label: '1051 - 子车取货位' }
|
|
],
|
'rgV105': [
|
{ value: '1051', label: '1051 - 母车停车位1' },
|
{ value: '1052', label: '1052 - 母车停车位2' }
|
],
|
'rgV107': [
|
{ value: '3', label: '停靠点1' },
|
{ value: '5', label: '停靠点2' },
|
{ value: '1081', label: '1081 - 子车取货位' },
|
{ value: '1091', label: '1091 - 子车取货位' }
|
],
|
'rgV108': [
|
{ value: '1081', label: '1081 - 母车停车位1' },
|
{ value: '1082', label: '1082 - 母车停车位2' }
|
],
|
'rgV109': [
|
{ value: '1091', label: '1091 - 母车停车位1' },
|
{ value: '1092', label: '1092 - 母车停车位2' }
|
],
|
// 出库设备地址
|
'rgV110': [
|
{ value: '1101', label: '1101 - 母车停车位1' },
|
{ value: '1102', label: '1102 - 母车停车位2' }
|
],
|
'rgV111': [
|
{ value: '3', label: '停靠点1' },
|
{ value: '5', label: '停靠点2' },
|
{ value: '1101', label: '1101 - 子车取货位' },
|
{ value: '1121', label: '1121 - 子车取货位' }
|
],
|
'rgV112': [
|
{ value: '1121', label: '1121 - 母车停车位1' },
|
{ value: '1122', label: '1122 - 母车停车位2' }
|
],
|
'rgV114': [
|
{ value: '1141', label: '1141 - 母车停车位1' },
|
{ value: '1142', label: '1142 - 母车停车位2' },
|
],
|
'rgV115': [
|
{ value: '1151', label: '1151 - 母车停车位1' },
|
{ value: '1152', label: '1152 - 母车停车位2' }
|
],
|
'rgV116': [
|
{ value: '3', label: '停靠点1' },
|
{ value: '5', label: '停靠点2' },
|
{ value: '1151', label: '1151 - 子车取货位' },
|
{ value: '1141', label: '1141 - 子车取货位' }
|
],
|
'rgV118': [
|
{ value: '2016', label: '2016 - 原料处理位1' },
|
{ value: '2017', label: '2017 - 原料处理位2' },
|
{ value: '2018', label: '2018 - 原料处理位3' },
|
{ value: '2019', label: '2019 - 原料处理位4' }
|
]
|
},
|
|
// 特殊字段的值格式化
|
valueFormatters: {
|
'工作模式': (value) => {
|
const modes = { 0: '手动模式', 1: '自动模式' }
|
return modes[value] || `未知模式(${value})`
|
},
|
'有货状态': (value) => value === 0 ? '无货' : '有货',
|
'任务状态': (value) => {
|
const statusMap = { 0: '空闲', 1: '执行中', 2: '完成' }
|
return statusMap[value] || `未知状态(${value})`
|
},
|
'故障代码': (value, deviceKey) => {
|
return this.getAlarmText(value, deviceKey)
|
},
|
'上升信号到位': (value) => value === 0 ? '未到位' : '已到位',
|
'下降信号到位': (value) => value === 0 ? '未到位' : '已到位',
|
'初始化未完成标志位': (value) => value === 0 ? '已完成' : '未完成',
|
'雷达状态': (value) => value === 0 ? '关闭' : '开启',
|
'货叉状态': (value) => value === 0 ? '缩回' : '伸出'
|
},
|
|
// 安全门字段值格式化
|
safetyDoorValueFormatters: {
|
'安全门指示灯状态': (value) => {
|
return this.safetyDoorLightCodes[value] || `未知状态(${value})`
|
},
|
'安全门请求开门': (value) => value === 0 ? '正常' : '请求开门',
|
'安全门断电状态': (value) => value === 0 ? '断电' : '上电',
|
'安全门急停状态': (value) => value === 0 ? '急停' : '正常',
|
'安全门锁状态': (value) => value === 0 ? '开门' : '关门',
|
'安全门复位状态': (value) => value === 0 ? '正常' : '复位中',
|
'报警信息': (value) => value === 0 ? '正常' : '报警',
|
'开门信息': (value) => value === 0 ? '关门' : '开门'
|
},
|
|
// 站台字段值格式化
|
platformValueFormatters: {
|
'光电信号': (value) => value === 0 ? '无信号' : '有信号',
|
'任务id': (value) => value === 0 ? '无任务' : `任务${value}`
|
}
|
}
|
},
|
computed: {
|
// 当前显示的设备数据
|
currentDeviceData() {
|
switch (this.currentMonitorType) {
|
case 'inbound':
|
return this.inboundDeviceData
|
case 'outbound':
|
return this.outboundDeviceData
|
case 'safetydoor':
|
return this.safetyDoorDeviceData
|
case 'platform':
|
return this.platformDeviceData
|
default:
|
return this.inboundDeviceData
|
}
|
}
|
},
|
methods: {
|
// 暂停/恢复监控轮询
|
pauseMonitoring() {
|
this.monitoringPaused = true
|
if (this.pollingTimer) {
|
clearTimeout(this.pollingTimer)
|
this.pollingTimer = null
|
}
|
},
|
resumeMonitoring() {
|
// 仅当没有任何长按/升降在进行时才恢复
|
if (!this.activeLift && !this.isHoldActive()) {
|
this.monitoringPaused = false
|
if (this.isMonitoring && !this.pollingTimer) {
|
this.startPolling()
|
}
|
}
|
},
|
// 升降长按期间仅轮询当前设备的"上/下到位"信号,避免全量刷新(实例方法)
|
startLiftSignalPolling(deviceKey) {
|
this.stopLiftSignalPolling()
|
this.signalPollingTimer = setInterval(async () => {
|
// 仅在当前仍处于该设备的升降长按且监控开启时有效,否则自动停止
|
if (!this.isMonitoring || !this.activeLift || this.activeLift.deviceKey !== deviceKey) {
|
this.stopLiftSignalPolling()
|
return
|
}
|
try {
|
const response = await this.http.post('api/Rgvoperainform/GetDeviceStatusDto', {
|
off: 1,
|
monitorType: this.currentMonitorType
|
})
|
if (response && response.status && response.data) {
|
const keys = Object.keys(response.data)
|
const respKey = keys.find(k => k.toLowerCase() === String(deviceKey).toLowerCase())
|
const dev = respKey ? response.data[respKey] : null
|
if (!dev) return
|
const updateMap = (mapObj) => {
|
if (!mapObj) return null
|
const devOld = mapObj[deviceKey] || {}
|
const patch = { ...devOld }
|
const riseVal = Object.prototype.hasOwnProperty.call(dev, '上升信号到位') ? dev['上升信号到位']
|
: (Object.prototype.hasOwnProperty.call(dev, 'RGV_Risingsignalplace') ? dev['RGV_Risingsignalplace']
|
: (Object.prototype.hasOwnProperty.call(dev, 'RiseArrived') ? dev['RiseArrived'] : undefined))
|
const downVal = Object.prototype.hasOwnProperty.call(dev, '下降信号到位') ? dev['下降信号到位']
|
: (Object.prototype.hasOwnProperty.call(dev, 'RGV_Descentsignal') ? dev['RGV_Descentsignal']
|
: (Object.prototype.hasOwnProperty.call(dev, 'DescendArrived') ? dev['DescendArrived'] : undefined))
|
if (riseVal !== undefined) patch['上升信号到位'] = riseVal
|
if (downVal !== undefined) patch['下降信号到位'] = downVal
|
return { ...mapObj, [deviceKey]: patch }
|
}
|
|
let patched = false
|
const tryPatch = (getter, setter) => {
|
const next = updateMap(getter)
|
if (next) {
|
setter(next)
|
patched = true
|
}
|
}
|
|
if (this.currentMonitorType === 'inbound') tryPatch(this.inboundDeviceData, v => this.inboundDeviceData = v)
|
if (this.currentMonitorType === 'outbound') tryPatch(this.outboundDeviceData, v => this.outboundDeviceData = v)
|
if (this.currentMonitorType === 'safetydoor') tryPatch(this.safetyDoorDeviceData, v => this.safetyDoorDeviceData = v)
|
if (this.currentMonitorType === 'platform') tryPatch(this.platformDeviceData, v => this.platformDeviceData = v)
|
|
if (!patched) {
|
tryPatch(this.inboundDeviceData, v => this.inboundDeviceData = v)
|
tryPatch(this.outboundDeviceData, v => this.outboundDeviceData = v)
|
tryPatch(this.safetyDoorDeviceData, v => this.safetyDoorDeviceData = v)
|
tryPatch(this.platformDeviceData, v => this.platformDeviceData = v)
|
}
|
|
if (typeof this.$forceUpdate === 'function') this.$forceUpdate()
|
}
|
} catch (e) {
|
// 忽略轻量轮询错误
|
}
|
}, this.signalPollInterval)
|
},
|
stopLiftSignalPolling() {
|
if (this.signalPollingTimer) {
|
clearInterval(this.signalPollingTimer)
|
this.signalPollingTimer = null
|
}
|
},
|
// 是否存在长按中的初始化(设备级或一键)
|
isHoldActive() {
|
if (this.oneClickInitTimer) return true
|
// holdTimers 中有任何一个有效定时器即视为长按中
|
return Object.values(this.holdTimers || {}).some(t => !!t)
|
},
|
// 开始设备级初始化长按
|
startInitHold(deviceKey) {
|
this.pauseMonitoring()
|
if (this.holdTimers[deviceKey]) {
|
clearTimeout(this.holdTimers[deviceKey])
|
}
|
this.holdTimers[deviceKey] = setTimeout(() => {
|
this.handleOperation(deviceKey, 'cs')
|
this.holdTimers[deviceKey] = null
|
// 初始化触发完成后,恢复监控并确保无残留轻量轮询
|
this.stopLiftSignalPolling()
|
this.resumeMonitoring()
|
}, this.holdDuration)
|
},
|
|
// 取消设备级初始化长按
|
cancelInitHold(deviceKey) {
|
const t = this.holdTimers[deviceKey]
|
if (t) {
|
clearTimeout(t)
|
this.holdTimers[deviceKey] = null
|
// 取消初始化长按,恢复监控并停止轻量轮询
|
this.stopLiftSignalPolling()
|
this.resumeMonitoring()
|
}
|
},
|
|
// 开始一键初始化长按
|
startOneClickInitHold() {
|
this.pauseMonitoring()
|
if (this.oneClickInitTimer) {
|
clearTimeout(this.oneClickInitTimer)
|
}
|
this.oneClickInitTimer = setTimeout(() => {
|
this.handleOneClickOperation('init')
|
this.oneClickInitTimer = null
|
this.resumeMonitoring()
|
}, this.holdDuration)
|
},
|
|
// 取消一键初始化长按
|
cancelOneClickInitHold() {
|
if (this.oneClickInitTimer) {
|
clearTimeout(this.oneClickInitTimer)
|
this.oneClickInitTimer = null
|
this.resumeMonitoring()
|
}
|
},
|
|
// 按下升降:发送参数1
|
async handleLiftPress(deviceKey, operationType) {
|
// operationType: 'ss' or 'xj'
|
console.log('[LIFT][PRESS] event received', { deviceKey, operationType, ts: Date.now() })
|
if (this.activeLift && this.activeLift.deviceKey === deviceKey && this.activeLift.operationType === operationType) {
|
console.log('[LIFT][PRESS] skipped because same activeLift exists')
|
return
|
}
|
this.activeLift = { deviceKey, operationType }
|
// 仅当已开启监控时,才暂停全局轮询并开启轻量信号轮询
|
if (this.isMonitoring) {
|
this.pauseMonitoring()
|
this.startLiftSignalPolling(deviceKey)
|
}
|
// 记录按下时间戳
|
this.lastLiftPressAt[`${deviceKey}-${operationType}`] = Date.now()
|
// 不等待,序列化存储Promise,保证release时按顺序发送
|
const key = `${deviceKey}-${operationType}`
|
console.log('[LIFT][PRESS] sending 1', { key, DelKeys: [deviceKey, operationType, 1] })
|
this.liftPressPromises[key] = this.http.post('api/Rgvoperainform/DeviceOperation', {
|
DelKeys: [deviceKey, operationType, 1],
|
Extra: true
|
}).then((response) => {
|
console.log('[LIFT][PRESS] response', { key, status: response && response.status, message: response && response.message })
|
if (!response.status) {
|
this.$message.error(response.message || `${this.operationTypes[operationType]}失败`)
|
}
|
}).catch((error) => {
|
console.error('[LIFT][PRESS] error', { key, error })
|
this.$message.error(`${this.operationTypes[operationType]}请求失败: ` + error.message)
|
})
|
},
|
|
// 松开升降:发送参数0
|
async handleLiftRelease(deviceKey, operationType) {
|
console.log('[LIFT][RELEASE] event received', { deviceKey, operationType, ts: Date.now() })
|
if (!this.activeLift || this.activeLift.deviceKey !== deviceKey || this.activeLift.operationType !== operationType) {
|
console.log('[LIFT][RELEASE] skipped because activeLift not match', { activeLift: this.activeLift })
|
return
|
}
|
const key = `${deviceKey}-${operationType}`
|
if (this.liftReleaseInProgress[key]) {
|
// 已有释放请求在进行,避免重复发送0(可能来自元素事件+全局事件的双触发)
|
console.log('[LIFT][RELEASE] skipped because release in progress', { key })
|
return
|
}
|
this.liftReleaseInProgress[key] = true
|
// 释放一开始就停止轻量轮询,避免继续占用网络导致0发送延迟
|
this.stopLiftSignalPolling()
|
// 防止按下后极短时间内就触发释放导致0先于1或"看起来只发了0"
|
const pressedAt = this.lastLiftPressAt[key] || 0
|
const elapsed = Date.now() - pressedAt
|
console.log('[LIFT][RELEASE] timing', { key, pressedAt, elapsed })
|
if (elapsed < 120) {
|
console.log('[LIFT][RELEASE] delaying to ensure press first', { delayMs: 120 - elapsed })
|
await new Promise(r => setTimeout(r, 120 - elapsed))
|
}
|
try {
|
const pressPromise = this.liftPressPromises[key]
|
if (pressPromise) {
|
console.log('[LIFT][RELEASE] awaiting press promise', { key })
|
// 加超时保护,若press长时间未返回,也不要无限等,最多等300ms
|
const timeout = new Promise((_, rej) => setTimeout(() => rej(new Error('press wait timeout')), 300))
|
await Promise.race([pressPromise, timeout]).catch(err => {
|
console.warn('[LIFT][RELEASE] press await timeout or error, continue to send 0', err && err.message)
|
})
|
}
|
console.log('[LIFT][RELEASE] sending 0', { key, DelKeys: [deviceKey, operationType, 0] })
|
const response = await this.http.post('api/Rgvoperainform/DeviceOperation', {
|
DelKeys: [deviceKey, operationType, 0],
|
Extra: true
|
})
|
console.log('[LIFT][RELEASE] response', { key, status: response && response.status, message: response && response.message })
|
if (!response.status) {
|
this.$message.error(response.message || `${this.operationTypes[operationType]}停止失败`)
|
}
|
} catch (error) {
|
console.error('[LIFT][RELEASE] error', { key, error })
|
this.$message.error(`${this.operationTypes[operationType]}停止请求失败: ` + error.message)
|
} finally {
|
console.log('[LIFT][RELEASE] finalize cleanup', { key })
|
this.activeLift = null
|
delete this.liftPressPromises[key]
|
delete this.lastLiftPressAt[key]
|
delete this.liftReleaseInProgress[key]
|
this.stopLiftSignalPolling()
|
// 恢复全局2s轮询
|
this.resumeMonitoring()
|
}
|
},
|
// 入库继续任务
|
async handleInNormal(deviceKey) {
|
this.setLoadingState(deviceKey, 'inNormal', true);
|
|
try {
|
const response = await this.http.post("api/RgvOperation/WriteInNormal", {}, "数据处理中...");
|
|
if (response.status) {
|
this.$message.success('入库继续任务成功');
|
if (this.isMonitoring) this.refreshData();
|
} else {
|
this.$message.error(response.message || '入库继续任务失败');
|
}
|
} catch (error) {
|
this.$message.error('入库继续任务请求失败: ' + error.message);
|
} finally {
|
this.setLoadingState(deviceKey, 'inNormal', false);
|
}
|
},
|
|
// 入库异常排除
|
async handleInAbnormal(deviceKey) {
|
this.setLoadingState(deviceKey, 'inAbnormal', true);
|
|
try {
|
const response = await this.http.post("api/RgvOperation/WriteInAbnormal", {}, "数据处理中...");
|
|
if (response.status) {
|
this.$message.success('入库异常排除成功');
|
if (this.isMonitoring) this.refreshData();
|
} else {
|
this.$message.error(response.message || '入库异常排除失败');
|
}
|
} catch (error) {
|
this.$message.error('入库异常排除请求失败: ' + error.message);
|
} finally {
|
this.setLoadingState(deviceKey, 'inAbnormal', false);
|
}
|
},
|
|
// 获取监控状态标题
|
getMonitorStatusTitle() {
|
const typeNames = {
|
'inbound': '入库',
|
'outbound': '出库',
|
'safetydoor': '安全门',
|
'platform': '站台'
|
}
|
const status = this.isMonitoring ? '运行中' : '已停止'
|
return `${typeNames[this.currentMonitorType]}监控状态: ${status}`
|
},
|
|
// 安全门字段值格式化
|
getSafetyDoorFormattedValue(fieldKey, value) {
|
const formatter = this.safetyDoorValueFormatters[fieldKey]
|
return formatter ? formatter(value) : value
|
},
|
|
// 安全门字段值样式
|
getSafetyDoorValueClass(fieldKey, value) {
|
if (fieldKey === '安全门急停状态' && value === 0) {
|
return 'fault-text'
|
}
|
if (fieldKey === '安全门断电状态' && value === 0) {
|
return 'fault-text'
|
}
|
if (fieldKey === '报警信息' && value === 1) {
|
return 'fault-text'
|
}
|
return ''
|
},
|
|
// 站台字段值格式化
|
getPlatformFormattedValue(fieldKey, value) {
|
const formatter = this.platformValueFormatters[fieldKey]
|
return formatter ? formatter(value) : value
|
},
|
|
// 站台字段值样式
|
getPlatformValueClass(fieldKey, value) {
|
if (fieldKey === '光电信号' && value === 1) {
|
return 'normal-text'
|
}
|
return ''
|
},
|
|
// 安全门指示灯样式
|
getSafetyLightClass(value) {
|
const classMap = {
|
0: 'light-off',
|
1: 'light-red',
|
2: 'light-green',
|
3: 'light-yellow-blink',
|
4: 'light-yellow'
|
}
|
return classMap[value] || 'light-off'
|
},
|
|
// 急停状态样式
|
getEmergencyStopClass(value) {
|
return value === 0 ? 'emergency-stop-active' : 'emergency-stop-normal'
|
},
|
|
// 一键操作处理
|
async handleOneClickOperation(operationType) {
|
// 根据当前监控类型设置参数
|
const monitorType = this.currentMonitorType === 'inbound' ? 'Inbound' : 'Outbound'
|
|
this.oneClickLoading[operationType] = true
|
|
try {
|
const operationNames = {
|
init: '一键初始化',
|
reset: '一键复位',
|
start: '一键启动',
|
stop: '一键暂停'
|
}
|
|
const response = await this.http.post('api/Rgvoperainform/OneClickOperation', {
|
operationType: operationType,
|
monitorType: monitorType
|
}, `${operationNames[operationType]}中...`)
|
|
if (response.status) {
|
this.$message.success(`${operationNames[operationType]} ${monitorType}端设备成功`)
|
this.refreshData()
|
} else {
|
this.$message.error(response.message || `${operationNames[operationType]}失败`)
|
}
|
} catch (error) {
|
this.$message.error(`${operationNames[operationType]}请求失败: ` + error.message)
|
} finally {
|
this.oneClickLoading[operationType] = false
|
}
|
},
|
|
// 获取初始化按钮显示文本
|
getInitializationButtonText(deviceData) {
|
if (!deviceData) return '初始化'
|
return deviceData['初始化未完成标志位'] === 0 ? '已初始化' : '初始化'
|
},
|
|
// 点击式升降:状态查询
|
isLifting(deviceKey) {
|
const st = this.activeLiftStates[deviceKey]
|
return !!(st && st.ss === true)
|
},
|
isLowering(deviceKey) {
|
const st = this.activeLiftStates[deviceKey]
|
return !!(st && st.xj === true)
|
},
|
|
// 启动升降(点击一次发1)
|
async startLift(deviceKey, operationType) {
|
// operationType: 'ss' | 'xj'
|
const isManual = this.isManualMode && this.isManualMode(this.currentMonitorDataMap()?.[deviceKey])
|
if (!isManual) {
|
this.$message.warning('请切换为手动模式后再操作')
|
return
|
}
|
// 互斥与防重
|
const st = this.activeLiftStates[deviceKey] || { ss: false, xj: false }
|
if ((operationType === 'ss' && (st.ss || st.xj)) || (operationType === 'xj' && (st.xj || st.ss))) {
|
return
|
}
|
// 设置启动态
|
this.activeLiftStates = { ...this.activeLiftStates, [deviceKey]: { ...st, [operationType]: true } }
|
try {
|
const resp = await this.http.post('api/Rgvoperainform/DeviceOperation', {
|
DelKeys: [deviceKey, operationType, 1],
|
Extra: true
|
})
|
if (!resp.status) {
|
this.$message.error((this.operationTypes && this.operationTypes[operationType]) ? `${this.operationTypes[operationType]}失败` : '指令发送失败')
|
// 回滚状态
|
const cur = this.activeLiftStates[deviceKey] || {}
|
this.activeLiftStates = { ...this.activeLiftStates, [deviceKey]: { ...cur, [operationType]: false } }
|
return
|
}
|
// 成功后启动超时自动停止
|
const key = `${deviceKey}-${operationType}`
|
this.clearLiftTimeout(key)
|
this.liftAutoStopTimeouts[key] = setTimeout(() => {
|
// 超时自动发送0
|
this.stopLift(deviceKey, operationType, true)
|
this.$message.warning('升降动作超时,已自动停止')
|
}, this.liftCommandTimeoutMs)
|
} catch (e) {
|
this.$message.error('请求失败: ' + e.message)
|
// 回滚状态
|
const cur = this.activeLiftStates[deviceKey] || {}
|
this.activeLiftStates = { ...this.activeLiftStates, [deviceKey]: { ...cur, [operationType]: false } }
|
}
|
},
|
|
// 停止升降(点击一次发0)
|
async stopLift(deviceKey, operationType, fromTimeout = false) {
|
// 即使未记录为启动态,也允许发送0,保证停止按钮在手动模式下始终可用
|
const st = this.activeLiftStates[deviceKey] || { ss: false, xj: false }
|
try {
|
const resp = await this.http.post('api/Rgvoperainform/DeviceOperation', {
|
DelKeys: [deviceKey, operationType, 0],
|
Extra: true
|
})
|
if (!resp.status && !fromTimeout) {
|
this.$message.error((this.operationTypes && this.operationTypes[operationType]) ? `${this.operationTypes[operationType]}停止失败` : '停止指令失败')
|
return
|
}
|
} catch (e) {
|
if (!fromTimeout) this.$message.error('停止请求失败: ' + e.message)
|
} finally {
|
// 置为停止态并清理超时定时器
|
const cur = this.activeLiftStates[deviceKey] || {}
|
this.activeLiftStates = { ...this.activeLiftStates, [deviceKey]: { ...cur, [operationType]: false } }
|
const key = `${deviceKey}-${operationType}`
|
this.clearLiftTimeout(key)
|
}
|
},
|
|
clearLiftTimeout(key) {
|
if (this.liftAutoStopTimeouts[key]) {
|
clearTimeout(this.liftAutoStopTimeouts[key])
|
delete this.liftAutoStopTimeouts[key]
|
}
|
},
|
|
// 切换监控类型
|
switchMonitorType(type) {
|
if (this.currentMonitorType !== type) {
|
// 停止当前监控
|
if (this.isMonitoring) {
|
this.stopMonitoring()
|
}
|
this.currentMonitorType = type
|
// 如果之前是监控状态,自动启动新类型的监控
|
if (this.isMonitoring) {
|
this.startMonitoring()
|
}
|
}
|
},
|
|
// 获取信号指示灯类名
|
getSignalClass(value) {
|
return value === 1 ? 'signal-active' : 'signal-inactive'
|
},
|
|
// 监控控制方法
|
async toggleMonitoring() {
|
if (this.monitoringLoading) return
|
if (this.isMonitoring) {
|
this.stopMonitoring()
|
} else {
|
await this.startMonitoring()
|
}
|
},
|
|
async startMonitoring() {
|
try {
|
this.monitoringLoading = true
|
const param = {
|
off: 1,
|
monitorType: this.currentMonitorType
|
}
|
|
const response = await this.http.post('api/Rgvoperainform/GetDeviceStatusDto', param)
|
|
if (response.status) {
|
this.isMonitoring = true
|
// 根据监控类型更新对应的设备数据
|
switch (this.currentMonitorType) {
|
case 'inbound':
|
this.inboundDeviceData = response.data
|
break
|
case 'outbound':
|
this.outboundDeviceData = response.data
|
break
|
case 'safetydoor':
|
this.safetyDoorDeviceData = response.data
|
break
|
case 'platform':
|
this.platformDeviceData = response.data
|
break
|
}
|
this.startPolling()
|
this.$message.success(`${this.getMonitorStatusTitle()}已启动`)
|
} else {
|
this.$message.error(response.message || `启动${this.currentMonitorType}监控失败`)
|
}
|
} catch (error) {
|
this.$message.error('请求失败: ' + error.message)
|
} finally {
|
this.monitoringLoading = false
|
}
|
},
|
|
stopMonitoring() {
|
this.isMonitoring = false
|
if (this.pollingTimer) {
|
clearTimeout(this.pollingTimer)
|
this.pollingTimer = null
|
}
|
|
this.http.post('api/Rgvoperainform/GetDeviceStatusDto', {
|
off: 0,
|
monitorType: this.currentMonitorType
|
})
|
|
this.$message.info(`${this.getMonitorStatusTitle()}已停止`)
|
},
|
|
startPolling() {
|
this.pollingTimer = setTimeout(() => {
|
if (this.isMonitoring && !this.monitoringPaused) {
|
this.refreshData()
|
this.startPolling()
|
}
|
}, 2000)
|
},
|
|
async refreshData() {
|
if (!this.isMonitoring) return
|
if (this.monitoringPaused) return
|
|
try {
|
const response = await this.http.post('api/Rgvoperainform/GetDeviceStatusDto', {
|
off: 1,
|
monitorType: this.currentMonitorType
|
})
|
if (response.status) {
|
// 在升降长按时,仅合并当前设备的到位信号,避免大面积重绘造成打断
|
if (this.activeLift) {
|
const { deviceKey } = this.activeLift
|
const respKeys = Object.keys(response.data || {})
|
const respKey = respKeys.find(k => k.toLowerCase() === String(deviceKey).toLowerCase())
|
const dev = respKey ? response.data[respKey] : null
|
const mergeSignals = (mapObj, setter) => {
|
if (!mapObj) return false
|
const old = mapObj[deviceKey]
|
if (!old) return false
|
const patch = { ...old }
|
const riseVal = Object.prototype.hasOwnProperty.call(dev || {}, '上升信号到位') ? dev['上升信号到位']
|
: (Object.prototype.hasOwnProperty.call(dev || {}, 'RGV_Risingsignalplace') ? dev['RGV_Risingsignalplace']
|
: (Object.prototype.hasOwnProperty.call(dev || {}, 'RiseArrived') ? dev['RiseArrived'] : undefined))
|
const downVal = Object.prototype.hasOwnProperty.call(dev || {}, '下降信号到位') ? dev['下降信号到位']
|
: (Object.prototype.hasOwnProperty.call(dev || {}, 'RGV_Descentsignal') ? dev['RGV_Descentsignal']
|
: (Object.prototype.hasOwnProperty.call(dev || {}, 'DescendArrived') ? dev['DescendArrived'] : undefined))
|
if (riseVal !== undefined) patch['上升信号到位'] = riseVal
|
if (downVal !== undefined) patch['下降信号到位'] = downVal
|
setter({ ...mapObj, [deviceKey]: patch })
|
if (typeof this.$forceUpdate === 'function') this.$forceUpdate()
|
return true
|
}
|
|
let merged = false
|
if (this.currentMonitorType === 'inbound') merged = mergeSignals(this.inboundDeviceData, v => this.inboundDeviceData = v)
|
if (this.currentMonitorType === 'outbound') merged = mergeSignals(this.outboundDeviceData, v => this.outboundDeviceData = v)
|
if (this.currentMonitorType === 'safetydoor') merged = mergeSignals(this.safetyDoorDeviceData, v => this.safetyDoorDeviceData = v)
|
if (this.currentMonitorType === 'platform') merged = mergeSignals(this.platformDeviceData, v => this.platformDeviceData = v)
|
|
if (!merged) {
|
merged = mergeSignals(this.inboundDeviceData, v => this.inboundDeviceData = v) ||
|
mergeSignals(this.outboundDeviceData, v => this.outboundDeviceData = v) ||
|
mergeSignals(this.safetyDoorDeviceData, v => this.safetyDoorDeviceData = v) ||
|
mergeSignals(this.platformDeviceData, v => this.platformDeviceData = v)
|
}
|
} else {
|
// 非长按,正常替换数据集
|
switch (this.currentMonitorType) {
|
case 'inbound':
|
this.inboundDeviceData = response.data
|
break
|
case 'outbound':
|
this.outboundDeviceData = response.data
|
break
|
case 'safetydoor':
|
this.safetyDoorDeviceData = response.data
|
break
|
case 'platform':
|
this.platformDeviceData = response.data
|
break
|
}
|
// 点击式:检测到位后自动停止对应动作
|
this.autoStopLiftIfArrived()
|
}
|
}
|
} catch (error) {
|
console.error('刷新数据失败:', error)
|
}
|
},
|
|
// 若设备在点击式升降中,检测到上/下到位则自动发送停止
|
autoStopLiftIfArrived() {
|
const dataMap = this.currentMonitorDataMap()
|
if (!dataMap) return
|
Object.keys(this.activeLiftStates).forEach(deviceKey => {
|
const state = this.activeLiftStates[deviceKey] || {}
|
const dev = dataMap[deviceKey]
|
if (!dev) return
|
if (state.ss && (dev['上升信号到位'] === 1 || dev['RGV_Risingsignalplace'] === 1 || dev['RiseArrived'] === 1)) {
|
this.stopLift(deviceKey, 'ss')
|
}
|
if (state.xj && (dev['下降信号到位'] === 1 || dev['RGV_Descentsignal'] === 1 || dev['DescendArrived'] === 1)) {
|
this.stopLift(deviceKey, 'xj')
|
}
|
})
|
},
|
|
// 返回当前监控类型的数据映射
|
currentMonitorDataMap() {
|
switch (this.currentMonitorType) {
|
case 'inbound': return this.inboundDeviceData
|
case 'outbound': return this.outboundDeviceData
|
case 'safetydoor': return this.safetyDoorDeviceData
|
case 'platform': return this.platformDeviceData
|
default: return null
|
}
|
},
|
|
// 其他原有方法保持不变
|
getStatusClass(device) {
|
if (!device || Object.keys(device).length === 0) return 'offline'
|
|
// 安全门状态判断
|
if (this.currentMonitorType === 'safetydoor') {
|
if (device['安全门急停状态'] === 0) return 'fault'
|
if (device['安全门断电状态'] === 0) return 'offline'
|
if (device['报警信息'] === 1) return 'fault'
|
return 'normal'
|
}
|
|
// 站台状态判断
|
if (this.currentMonitorType === 'platform') {
|
if (device['光电信号'] === 1) return 'running'
|
return 'normal'
|
}
|
|
// RGV设备状态判断
|
if (device['故障代码'] !== 0) return 'fault'
|
if (device['任务状态'] === 1) return 'running'
|
if (device['工作模式'] === 0) return 'manual'
|
return 'normal'
|
},
|
|
getDeviceDisplayName(deviceKey) {
|
return this.deviceDisplayNames[deviceKey] || deviceKey.toUpperCase()
|
},
|
|
getFieldDisplayName(fieldKey) {
|
return this.fieldDisplayNames[fieldKey] || fieldKey
|
},
|
|
getFormattedValue(fieldKey, value, deviceKey) {
|
const formatter = this.valueFormatters[fieldKey]
|
return formatter ? formatter(value, deviceKey) : value
|
},
|
|
getValueClass(fieldKey, value, deviceKey) {
|
if (fieldKey === '故障代码' && value !== 0) {
|
return 'fault-text'
|
}
|
return ''
|
},
|
|
// 获取设备类型
|
getDeviceType(deviceKey) {
|
if (this.deviceTypes.motherCars.includes(deviceKey) ||
|
this.deviceTypes.outboundMotherCars.includes(deviceKey)) {
|
return 'mother'
|
} else if (this.deviceTypes.childCars.includes(deviceKey) ||
|
this.deviceTypes.outboundChildCars.includes(deviceKey)) {
|
return 'child'
|
} else if (this.deviceTypes.materialCars.includes(deviceKey) ||
|
this.deviceTypes.outboundMaterialCars.includes(deviceKey)) {
|
return 'material'
|
} else if (this.deviceTypes.safetyDoors.includes(deviceKey)) {
|
return 'safetydoor'
|
} else if (this.deviceTypes.platforms.includes(deviceKey)) {
|
return 'platform'
|
}
|
return 'unknown'
|
},
|
|
// 获取设备可用的目标地址
|
getDeviceAddresses(deviceKey) {
|
return this.deviceAddresses[deviceKey] || []
|
},
|
|
// 获取报警文本
|
getAlarmText(alarmCode, deviceKey) {
|
if (alarmCode === 0) return '无报警'
|
|
const deviceType = this.getDeviceType(deviceKey)
|
let alarmMap = {}
|
|
switch (deviceType) {
|
case 'mother':
|
alarmMap = this.motherCarAlarmCodes
|
break
|
case 'child':
|
alarmMap = this.childCarAlarmCodes
|
break
|
case 'material':
|
alarmMap = this.materialCarAlarmCodes
|
break
|
default:
|
alarmMap = this.motherCarAlarmCodes
|
}
|
|
return alarmMap[alarmCode] || `未连接(${alarmCode})`
|
},
|
|
// 获取当前模式显示文本
|
getCurrentModeText(deviceData) {
|
if (!deviceData || !deviceData['工作模式']) return '点击切换自动'
|
return deviceData['工作模式'] === 1 ? '点击切换手动' : '点击切换自动'
|
},
|
|
// 获取模式按钮类型(颜色)
|
getModeButtonType(deviceData) {
|
if (!deviceData || !deviceData['工作模式']) return 'success'
|
return deviceData['工作模式'] === 1 ? 'warning' : 'success'
|
},
|
|
// 判断是否为手动模式(仅当明确为1=自动时视为非手动;undefined/0都按手动处理)
|
isManualMode(deviceData) {
|
if (!deviceData) return false
|
return deviceData['工作模式'] !== 1
|
},
|
|
// 获取雷达按钮文本
|
getRadarButtonText(deviceData) {
|
if (!deviceData) return '开启雷达'
|
return deviceData['雷达状态'] === 1 ? '关闭雷达' : '开启雷达'
|
},
|
|
// 获取雷达按钮类型
|
getRadarButtonType(deviceData) {
|
if (!deviceData) return 'info'
|
return deviceData['雷达状态'] === 1 ? 'warning' : 'info'
|
},
|
|
// 设置加载状态
|
setLoadingState(deviceKey, operationType, loading) {
|
if (!this.loadingStates[deviceKey]) {
|
this.loadingStates[deviceKey] = {}
|
}
|
this.loadingStates[deviceKey][operationType] = loading
|
this.loadingStates = { ...this.loadingStates }
|
},
|
|
// 手动/自动模式切换
|
async handleModeToggle(deviceKey) {
|
this.setLoadingState(deviceKey, 'modeToggle', true)
|
|
try {
|
const deviceData = this.currentDeviceData[deviceKey]
|
const currentMode = deviceData?.['工作模式'] || 0
|
|
const operationType = currentMode === 0 ? 'zd' : 'sd'
|
|
const response = await this.http.post('api/Rgvoperainform/DeviceOperation', {
|
DelKeys: [deviceKey, operationType],
|
Extra: true
|
}, `${this.operationTypes[operationType]}中...`)
|
|
if (response.status) {
|
this.$message.success(`${this.getDeviceDisplayName(deviceKey)} ${this.operationTypes[operationType]}成功`)
|
if (this.isMonitoring) this.refreshData()
|
} else {
|
this.$message.error(response.message || `${this.operationTypes[operationType]}失败`)
|
}
|
} catch (error) {
|
this.$message.error(`模式切换请求失败: ` + error.message)
|
} finally {
|
this.setLoadingState(deviceKey, 'modeToggle', false)
|
}
|
},
|
|
// 雷达开关切换
|
async handleRadarToggle(deviceKey) {
|
this.setLoadingState(deviceKey, 'radarToggle', true)
|
|
try {
|
const deviceData = this.currentDeviceData[deviceKey]
|
const currentRadarState = deviceData?.['雷达状态'] || 0
|
|
const operationType = currentRadarState === 0 ? 'kld' : 'gld'
|
|
const response = await this.http.post('api/Rgvoperainform/DeviceOperation', {
|
DelKeys: [deviceKey, operationType],
|
Extra: true
|
}, `${this.operationTypes[operationType]}中...`)
|
|
if (response.status) {
|
this.$message.success(`${this.getDeviceDisplayName(deviceKey)} ${this.operationTypes[operationType]}成功`)
|
this.refreshData()
|
} else {
|
this.$message.error(response.message || `${this.operationTypes[operationType]}失败`)
|
}
|
} catch (error) {
|
this.$message.error(`雷达操作请求失败: ` + error.message)
|
} finally {
|
this.setLoadingState(deviceKey, 'radarToggle', false)
|
}
|
},
|
|
// 地址操作
|
async handleAddressOperation(deviceKey) {
|
const address = this.addressValues[deviceKey]
|
if (!address) {
|
this.$message.warning('请选择目标地址')
|
return
|
}
|
|
this.setLoadingState(deviceKey, 'dz', true)
|
|
try {
|
const response = await this.http.post('api/Rgvoperainform/DeviceOperation', {
|
DelKeys: [deviceKey, 'dz', address],
|
Extra: true
|
}, '前往地址中...')
|
|
if (response.status) {
|
this.$message.success(`${this.getDeviceDisplayName(deviceKey)} 前往地址 ${address} 成功`)
|
if (this.isMonitoring) this.refreshData()
|
} else {
|
this.$message.error(response.message || '前往地址失败')
|
}
|
} catch (error) {
|
this.$message.error('前往地址请求失败: ' + error.message)
|
} finally {
|
this.setLoadingState(deviceKey, 'dz', false)
|
}
|
},
|
|
// 统一操作处理函数
|
async handleOperation(deviceKey, operationType) {
|
this.setLoadingState(deviceKey, operationType, true)
|
|
try {
|
// 上升/下降需要第三个参数置位
|
const delKeys = (operationType === 'ss' || operationType === 'xj')
|
? [deviceKey, operationType, 1]
|
: [deviceKey, operationType]
|
|
const response = await this.http.post('api/Rgvoperainform/DeviceOperation', {
|
DelKeys: delKeys,
|
Extra: true
|
}, `${this.operationTypes[operationType]}中...`)
|
|
if (response.status) {
|
const operationName = this.operationTypes[operationType]
|
this.$message.success(`${this.getDeviceDisplayName(deviceKey)} ${operationName}操作成功`)
|
if (this.isMonitoring) this.refreshData()
|
} else {
|
this.$message.error(response.message || `${this.operationTypes[operationType]}操作失败`)
|
}
|
} catch (error) {
|
this.$message.error(`${this.operationTypes[operationType]}请求失败: ` + error.message)
|
} finally {
|
this.setLoadingState(deviceKey, operationType, false)
|
}
|
}
|
},
|
|
mounted() {
|
// 全局释放兜底,防止指针移出按钮区域或窗口导致无法发送 0
|
this._liftGlobalRelease = () => {
|
if (this.activeLift) {
|
const { deviceKey, operationType } = this.activeLift
|
this.handleLiftRelease(deviceKey, operationType)
|
}
|
}
|
window.addEventListener('mouseup', this._liftGlobalRelease)
|
window.addEventListener('touchend', this._liftGlobalRelease, { passive: true })
|
window.addEventListener('blur', this._liftGlobalRelease)
|
document.addEventListener('visibilitychange', () => {
|
if (document.hidden) this._liftGlobalRelease()
|
})
|
},
|
|
beforeUnmount() {
|
this.stopMonitoring()
|
if (this._liftGlobalRelease) {
|
window.removeEventListener('mouseup', this._liftGlobalRelease)
|
window.removeEventListener('touchend', this._liftGlobalRelease)
|
window.removeEventListener('blur', this._liftGlobalRelease)
|
}
|
}
|
}
|
</script>
|
|
<style scoped>
|
/* 添加站台正常文本样式 */
|
.normal-text {
|
color: #67C23A;
|
font-weight: bold;
|
}
|
|
/* 原有的样式保持不变,添加安全门相关样式 */
|
|
/* 安全门指示灯样式 */
|
.safety-light {
|
width: 12px;
|
height: 12px;
|
border-radius: 50%;
|
display: inline-block;
|
margin-left: 8px;
|
border: 1px solid #ccc;
|
}
|
|
.safety-light.light-off {
|
background-color: #909399;
|
}
|
|
.safety-light.light-red {
|
background-color: #F56C6C;
|
box-shadow: 0 0 6px #F56C6C;
|
animation: blink 1s infinite;
|
}
|
|
.safety-light.light-green {
|
background-color: #67C23A;
|
box-shadow: 0 0 6px #67C23A;
|
}
|
|
.safety-light.light-yellow {
|
background-color: #E6A23C;
|
box-shadow: 0 0 6px #E6A23C;
|
}
|
|
.safety-light.light-yellow-blink {
|
background-color: #E6A23C;
|
box-shadow: 0 0 6px #E6A23C;
|
animation: blink 0.5s infinite;
|
}
|
|
/* 急停状态指示 */
|
.emergency-stop {
|
width: 8px;
|
height: 8px;
|
border-radius: 50%;
|
display: inline-block;
|
margin-left: 8px;
|
}
|
|
.emergency-stop.emergency-stop-normal {
|
background-color: #67C23A;
|
box-shadow: 0 0 4px #67C23A;
|
}
|
|
.emergency-stop.emergency-stop-active {
|
background-color: #F56C6C;
|
box-shadow: 0 0 4px #F56C6C;
|
animation: blink 0.8s infinite;
|
}
|
|
/* 监控类型切换样式 */
|
.monitor-type-switch {
|
margin-right: 20px;
|
}
|
|
.one-click-operations {
|
margin-right: 20px;
|
}
|
|
.control-panel {
|
display: flex;
|
gap: 10px;
|
align-items: center;
|
}
|
|
/* 信号指示灯样式 */
|
.signal-indicator {
|
width: 8px;
|
height: 8px;
|
border-radius: 50%;
|
display: inline-block;
|
margin-left: 8px;
|
}
|
|
.signal-indicator.signal-active {
|
background-color: #67C23A;
|
box-shadow: 0 0 4px #67C23A;
|
}
|
|
.signal-indicator.signal-inactive {
|
background-color: #DCDFE6;
|
border: 1px solid #C0C4CC;
|
}
|
|
/* 修改value样式以容纳指示灯 */
|
.value {
|
color: #303133;
|
font-weight: 500;
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
}
|
|
/* 其他原有样式保持不变 */
|
.rgv-monitor {
|
padding: 20px;
|
background-color: #f5f7fa;
|
min-height: 100vh;
|
}
|
|
.monitor-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 20px;
|
padding: 0 10px;
|
}
|
|
.title {
|
font-size: 24px;
|
font-weight: bold;
|
color: #409EFF;
|
}
|
|
.monitor-status {
|
margin-bottom: 20px;
|
}
|
|
.devices-container {
|
display: grid;
|
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
gap: 20px;
|
margin-bottom: 20px;
|
}
|
|
.device-card {
|
background: white;
|
border-radius: 8px;
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
overflow: hidden;
|
transition: all 0.3s ease;
|
}
|
|
.device-card:hover {
|
transform: translateY(-2px);
|
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.15);
|
}
|
|
.device-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
padding: 15px 20px;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
color: white;
|
}
|
|
.device-header h3 {
|
margin: 0;
|
font-size: 16px;
|
}
|
|
.status-indicator {
|
width: 12px;
|
height: 12px;
|
border-radius: 50%;
|
}
|
|
.status-indicator.normal {
|
background-color: #67C23A;
|
box-shadow: 0 0 8px #67C23A;
|
}
|
|
.status-indicator.running {
|
background-color: #409EFF;
|
box-shadow: 0 0 8px #409EFF;
|
animation: pulse 1.5s infinite;
|
}
|
|
.status-indicator.fault {
|
background-color: #F56C6C;
|
box-shadow: 0 0 8px #F56C6C;
|
animation: blink 1s infinite;
|
}
|
|
.status-indicator.manual {
|
background-color: #E6A23C;
|
box-shadow: 0 0 8px #E6A23C;
|
animation: pulse 1.5s infinite;
|
}
|
|
.status-indicator.offline {
|
background-color: #909399;
|
}
|
|
.device-content {
|
padding: 20px;
|
}
|
|
.data-row {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
padding: 8px 0;
|
border-bottom: 1px solid #ebeef5;
|
}
|
|
.data-row:last-child {
|
border-bottom: none;
|
}
|
|
.label {
|
font-weight: 500;
|
color: #606266;
|
}
|
|
.fault-text {
|
color: #F56C6C;
|
font-weight: bold;
|
}
|
|
.no-data {
|
text-align: center;
|
color: #909399;
|
padding: 40px 0;
|
font-style: italic;
|
}
|
|
.operation-buttons {
|
margin-top: 15px;
|
padding-top: 15px;
|
border-top: 1px dashed #ebeef5;
|
display: flex;
|
flex-wrap: wrap;
|
gap: 8px;
|
justify-content: center;
|
}
|
|
.address-operation {
|
margin-top: 15px;
|
padding-top: 15px;
|
border-top: 1px dashed #ebeef5;
|
}
|
|
.address-title {
|
font-weight: 500;
|
color: #606266;
|
margin-bottom: 10px;
|
text-align: center;
|
}
|
|
.address-input-group {
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
gap: 10px;
|
}
|
|
.operation-panel {
|
text-align: center;
|
padding: 20px;
|
}
|
|
@keyframes pulse {
|
0% {
|
opacity: 1;
|
}
|
|
50% {
|
opacity: 0.5;
|
}
|
|
100% {
|
opacity: 1;
|
}
|
}
|
|
@keyframes blink {
|
|
0%,
|
50% {
|
opacity: 1;
|
}
|
|
51%,
|
100% {
|
opacity: 0.3;
|
}
|
}
|
|
/* 响应式设计 */
|
@media (max-width: 768px) {
|
.devices-container {
|
grid-template-columns: 1fr;
|
}
|
|
.monitor-header {
|
flex-direction: column;
|
gap: 15px;
|
align-items: flex-start;
|
}
|
|
.control-panel {
|
flex-direction: column;
|
align-items: flex-start;
|
width: 100%;
|
}
|
|
.monitor-type-switch,
|
.one-click-operations {
|
margin-right: 0;
|
margin-bottom: 10px;
|
width: 100%;
|
}
|
|
.monitor-type-switch .el-button-group,
|
.one-click-operations .el-button-group {
|
width: 100%;
|
}
|
|
.monitor-type-switch .el-button,
|
.one-click-operations .el-button {
|
flex: 1;
|
}
|
|
.operation-buttons {
|
flex-direction: column;
|
align-items: stretch;
|
}
|
|
.operation-buttons .el-button {
|
width: 100%;
|
}
|
|
.address-input-group {
|
flex-direction: column;
|
align-items: stretch;
|
}
|
|
.address-input-group .el-select,
|
.address-input-group .el-button {
|
width: 100%;
|
}
|
}
|
</style>
|