| | |
| | | <div class="node-name">{{ device.deviceName || device.deviceCode }}</div> |
| | | <div class="node-type">{{ getDeviceTypeLabel(device.deviceType) }}</div> |
| | | <div class="node-status"> |
| | | <el-tag :type="getStatusType(device.status)" size="small"> |
| | | {{ getStatusLabel(device.status) }} |
| | | <el-tag :type="getStatusType(getDeviceStatus(device))" size="small"> |
| | | {{ getStatusLabel(getDeviceStatus(device)) }} |
| | | </el-tag> |
| | | </div> |
| | | <div class="node-actions"> |
| | | <el-button |
| | | size="small" |
| | | text |
| | | @click.stop="clearPlc(device)" |
| | | :loading="clearingDeviceId === (device.deviceId || device.id)" |
| | | > |
| | | 清空 PLC |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | {{ getDeviceTypeLabel(selectedDevice.deviceType) }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="状态"> |
| | | <el-tag :type="getStatusType(selectedDevice.status)"> |
| | | {{ getStatusLabel(selectedDevice.status) }} |
| | | </el-tag> |
| | | <div class="status-control"> |
| | | <el-tag :type="getStatusType(getDeviceStatus(selectedDevice))"> |
| | | {{ getStatusLabel(getDeviceStatus(selectedDevice)) }} |
| | | </el-tag> |
| | | <el-switch |
| | | v-if="isLoadVehicleDevice(selectedDevice)" |
| | | :model-value="Boolean(selectedDevice.onlineState)" |
| | | active-text="联机" |
| | | inactive-text="脱机" |
| | | :loading="togglingDeviceId === (selectedDevice.deviceId || selectedDevice.id)" |
| | | size="small" |
| | | @change="(val) => toggleOnlineState(selectedDevice, val)" |
| | | /> |
| | | <el-button |
| | | size="small" |
| | | text |
| | | @click="clearPlc(selectedDevice)" |
| | | :loading="clearingDeviceId === (selectedDevice.deviceId || selectedDevice.id)" |
| | | > |
| | | 清空 PLC |
| | | </el-button> |
| | | </div> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="PLC IP" v-if="selectedDevice.plcIp"> |
| | | {{ selectedDevice.plcIp }} |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref, watch } from 'vue' |
| | | import { computed, ref, watch, onMounted, onUnmounted } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { |
| | | Refresh, |
| | |
| | | Box, |
| | | Folder |
| | | } from '@element-plus/icons-vue' |
| | | import { deviceGroupApi } from '@/api/device/deviceManagement' |
| | | import { deviceGroupApi, deviceInteractionApi } from '@/api/device/deviceManagement' |
| | | |
| | | const props = defineProps({ |
| | | group: { |
| | |
| | | const devices = ref([]) |
| | | const layoutMode = ref('horizontal') // 'horizontal' | 'vertical' |
| | | const selectedDevice = ref(null) |
| | | const togglingDeviceId = ref(null) |
| | | const clearingDeviceId = ref(null) |
| | | const refreshIntervalMs = 5000 |
| | | let refreshTimer = null |
| | | |
| | | const fetchDevices = async () => { |
| | | if (!props.group) { |
| | |
| | | ? rawList.data |
| | | : [] |
| | | // 按执行顺序排序 |
| | | devices.value = deviceList.sort((a, b) => { |
| | | const orderA = a.executionOrder || a.order || 0 |
| | | const orderB = b.executionOrder || b.order || 0 |
| | | return orderA - orderB |
| | | }) |
| | | devices.value = deviceList |
| | | .map((device) => normalizeDevice(device)) |
| | | .sort((a, b) => { |
| | | const orderA = a.executionOrder || a.order || 0 |
| | | const orderB = b.executionOrder || b.order || 0 |
| | | return orderA - orderB |
| | | }) |
| | | syncSelectedDevice() |
| | | } catch (error) { |
| | | ElMessage.error(error?.message || '加载设备列表失败') |
| | | devices.value = [] |
| | |
| | | |
| | | const handleRefresh = () => { |
| | | fetchDevices() |
| | | } |
| | | |
| | | const stopAutoRefresh = () => { |
| | | if (refreshTimer) { |
| | | clearInterval(refreshTimer) |
| | | refreshTimer = null |
| | | } |
| | | } |
| | | |
| | | const startAutoRefresh = () => { |
| | | stopAutoRefresh() |
| | | if (!props.group) return |
| | | refreshTimer = setInterval(() => { |
| | | fetchDevices() |
| | | }, refreshIntervalMs) |
| | | } |
| | | |
| | | const toggleLayout = () => { |
| | |
| | | () => { |
| | | fetchDevices() |
| | | selectedDevice.value = null |
| | | startAutoRefresh() |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | |
| | | onMounted(() => { |
| | | startAutoRefresh() |
| | | }) |
| | | |
| | | onUnmounted(() => { |
| | | stopAutoRefresh() |
| | | }) |
| | | |
| | | // 点击节点选择设备 |
| | | const handleNodeClick = (device) => { |
| | | selectedDevice.value = device |
| | | } |
| | | |
| | | const syncSelectedDevice = () => { |
| | | if (!selectedDevice.value) return |
| | | const deviceId = selectedDevice.value.deviceId || selectedDevice.value.id |
| | | if (!deviceId) return |
| | | const updated = devices.value.find( |
| | | (item) => (item.deviceId || item.id) === deviceId |
| | | ) |
| | | if (updated) { |
| | | selectedDevice.value = updated |
| | | } |
| | | } |
| | | |
| | | const isLoadVehicleDevice = (device) => { |
| | | if (!device || !device.deviceType) return false |
| | | const type = device.deviceType.toUpperCase() |
| | | return type.includes('VEHICLE') || type.includes('大车') |
| | | } |
| | | |
| | | const normalizeDevice = (device) => { |
| | | if (!device) return device |
| | | const normalized = { ...device } |
| | | if (normalized.onlineState !== undefined) { |
| | | normalized.onlineState = toBoolean(normalized.onlineState) |
| | | } else if (normalized.isOnline === true || normalized.isOnline === false) { |
| | | normalized.onlineState = normalized.isOnline |
| | | } else if (normalized.status) { |
| | | normalized.onlineState = String(normalized.status).toUpperCase() === 'ONLINE' |
| | | } |
| | | if (isLoadVehicleDevice(normalized) && normalized.onlineState !== undefined) { |
| | | normalized.status = normalized.onlineState ? 'ONLINE' : 'OFFLINE' |
| | | } |
| | | return normalized |
| | | } |
| | | |
| | | const toBoolean = (value) => { |
| | | if (value === true || value === false) return value |
| | | if (typeof value === 'number') return value !== 0 |
| | | const str = String(value).trim().toLowerCase() |
| | | if (str === 'true' || str === '1') return true |
| | | if (str === 'false' || str === '0') return false |
| | | return Boolean(value) |
| | | } |
| | | |
| | | const getDeviceStatus = (device) => { |
| | | if (!device) return 'UNKNOWN' |
| | | if (isLoadVehicleDevice(device) && device.onlineState !== undefined) { |
| | | return device.onlineState ? 'ONLINE' : 'OFFLINE' |
| | | } |
| | | if (device.isOnline === true || device.isOnline === false) { |
| | | return device.isOnline ? 'ONLINE' : 'OFFLINE' |
| | | } |
| | | if (device.status) return device.status |
| | | if (device.deviceStatus) return device.deviceStatus |
| | | return 'UNKNOWN' |
| | | } |
| | | |
| | | const toggleOnlineState = async (device, value) => { |
| | | if (!device) return |
| | | const deviceId = device.deviceId || device.id |
| | | if (!deviceId) { |
| | | ElMessage.warning('设备ID不存在,无法设置联机状态') |
| | | return |
| | | } |
| | | try { |
| | | togglingDeviceId.value = deviceId |
| | | await deviceInteractionApi.executeOperation({ |
| | | deviceId, |
| | | operation: 'setOnlineState', |
| | | params: { |
| | | onlineState: value |
| | | } |
| | | }) |
| | | device.onlineState = value |
| | | device.status = value ? 'ONLINE' : 'OFFLINE' |
| | | if (selectedDevice.value && (selectedDevice.value.deviceId === deviceId || selectedDevice.value.id === deviceId)) { |
| | | selectedDevice.value.onlineState = device.onlineState |
| | | selectedDevice.value.status = device.status |
| | | } |
| | | ElMessage.success(`已将 ${device.deviceName || device.deviceCode} 设置为${value ? '联机' : '脱机'}`) |
| | | } catch (error) { |
| | | ElMessage.error(error?.message || '设置联机状态失败') |
| | | } finally { |
| | | togglingDeviceId.value = null |
| | | } |
| | | } |
| | | |
| | | const clearPlc = async (device) => { |
| | | if (!device) return |
| | | const deviceId = device.deviceId || device.id |
| | | if (!deviceId) { |
| | | ElMessage.warning('设备ID不存在,无法清空PLC') |
| | | return |
| | | } |
| | | try { |
| | | clearingDeviceId.value = deviceId |
| | | await deviceInteractionApi.executeOperation({ |
| | | deviceId, |
| | | operation: 'clearPlc' |
| | | }) |
| | | ElMessage.success(`已清空 ${device.deviceName || device.deviceCode} 的PLC数据`) |
| | | } catch (error) { |
| | | ElMessage.error(error?.message || '清空PLC失败') |
| | | } finally { |
| | | clearingDeviceId.value = null |
| | | } |
| | | } |
| | | |
| | | defineExpose({ |
| | |
| | | justify-content: center; |
| | | } |
| | | |
| | | .node-actions { |
| | | margin-top: 6px; |
| | | display: flex; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .node-order { |
| | | position: absolute; |
| | | top: -8px; |
| | |
| | | justify-content: center; |
| | | } |
| | | |
| | | .status-control { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | } |
| | | |
| | | /* 水平布局:箭头在节点右侧 */ |
| | | .topology-node-wrapper.layout-horizontal .node-arrow { |
| | | margin-left: 20px; |