You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1286 lines
41 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="app-container">
<el-row :gutter="20">
<!-- 左侧设备树 -->
<el-col :span="5" :xs="24">
<div class="head-container">
<el-input
v-model="deviceTreeFilter"
placeholder="请输入设备名称"
clearable
size="small"
prefix-icon="el-icon-search"
style="margin-bottom: 20px"
/>
</div>
<div class="head-container">
<el-tree
:data="deviceTreeOptions"
:props="deviceTreeProps"
:expand-on-click-node="false"
:filter-node-method="filterDeviceNode"
ref="deviceTree"
node-key="id"
default-expand-all
highlight-current
@node-click="handleDeviceNodeClick">
</el-tree>
</div>
</el-col>
<!-- 右侧内容区 -->
<el-col :span="19" :xs="24">
<!-- 统计卡片区域 -->
<div class="stats-section">
<div class="stats-grid">
<div
class="stat-card total clickable"
@click="navigateToDeviceList"
v-hasPermi="['ems/base:baseMonitorInfo:list']">
<div class="stat-icon">
<i class="el-icon-monitor"></i>
</div>
<div class="stat-content">
<div class="stat-number">{{ totalDeviceCount }}</div>
<div class="stat-label">设备总数</div>
</div>
</div>
<div
class="stat-card alarm-rule clickable"
@click="navigateToAlarmRules"
v-hasPermi="['ems/record:recordAlarmRule:list']">
<div class="stat-icon">
<i class="el-icon-warning"></i>
</div>
<div class="stat-content">
<div class="stat-number">{{ alarmRuleTotalCount }}</div>
<div class="stat-label">异常规则数量</div>
</div>
</div>
<div
class="stat-card alarm-data clickable"
@click="navigateToAlarmData"
v-hasPermi="['ems/record:recordAlarmData:list']">
<div class="stat-icon">
<i class="el-icon-bell"></i>
</div>
<div class="stat-content">
<div class="stat-number">{{ alarmDataTotalCount }}</div>
<div class="stat-label">异常数据数量</div>
</div>
</div>
</div>
</div>
<!-- 设备监控数据区域 -->
<div class="monitoring-section">
<div class="section-header">
<h2 class="section-title">设备监控数据</h2>
<div class="section-actions">
<span v-if="selectedNodeName" class="selected-node">
当前节点:{{ selectedNodeName }}
<span v-if="selectedNodeType" class="node-type-info">({{ getNodeTypeText(selectedNodeType) }})</span>
</span>
<el-button
type="primary"
icon="el-icon-refresh"
size="small"
@click="refreshData"
:loading="loading">
</el-button>
</div>
</div>
<!-- 设备数据卡片网格 -->
<div class="device-grid" v-loading="loading">
<div
v-for="device in filteredDeviceList"
:key="device.monitorId"
class="device-card"
:class="getDeviceStatus(device)">
<!-- 设备头部信息 -->
<div class="device-header">
<div class="device-info">
<div class="device-title-row">
<h3 class="device-name">{{ device.monitorName || '未知设备' }}</h3>
<span v-if="isCurrentNodeDevice(device)" class="node-type-badge current">当前节点</span>
<span v-else class="node-type-badge child">子节点</span>
</div>
<span class="device-id">{{ device.monitorId }}</span>
</div>
<div class="device-status">
<span class="status-indicator" :class="getDeviceStatus(device)"></span>
<span class="status-text">{{ getStatusText(device) }}</span>
</div>
</div>
<!-- 设备数据展示 -->
<div class="device-data">
<!-- type=5: 只显示温度 -->
<template v-if="selectedNodeType === 5">
<div class="data-row" v-if="device.temperature !== null && device.temperature !== undefined">
<div class="data-item">
<i class="data-icon el-icon-sunny" style="color: #ff6b35;"></i>
<span class="data-label">温度</span>
<span class="data-value">{{ formatValue(device.temperature, '°C') }}</span>
</div>
</div>
</template>
<!-- type=6: 显示温度和湿度 -->
<template v-else-if="selectedNodeType === 6">
<div class="data-row" v-if="device.temperature !== null && device.temperature !== undefined">
<div class="data-item">
<i class="data-icon el-icon-sunny" style="color: #ff6b35;"></i>
<span class="data-label">温度</span>
<span class="data-value">{{ formatValue(device.temperature, '°C') }}</span>
</div>
</div>
<div class="data-row" v-if="device.humidity !== null && device.humidity !== undefined">
<div class="data-item">
<i class="data-icon el-icon-cloudy"></i>
<span class="data-label">湿度</span>
<span class="data-value">{{ formatValue(device.humidity, '%') }}</span>
</div>
</div>
</template>
<!-- type=7: 只显示噪声 -->
<template v-else-if="selectedNodeType === 7">
<div class="data-row" v-if="device.noise !== null && device.noise !== undefined">
<div class="data-item">
<i class="data-icon el-icon-microphone"></i>
<span class="data-label">噪声</span>
<span class="data-value">{{ formatValue(device.noise, 'dB') }}</span>
</div>
</div>
</template>
<!-- type=10: 显示振动相关数据 -->
<template v-else-if="selectedNodeType === 10">
<div class="data-row" v-if="device.vibrationSpeed !== null && device.vibrationSpeed !== undefined">
<div class="data-item">
<i class="data-icon el-icon-s-operation"></i>
<span class="data-label">振动速度</span>
<span class="data-value">{{ formatValue(device.vibrationSpeed, 'mm/s') }}</span>
</div>
</div>
<div class="data-row" v-if="device.vibrationDisplacement !== null && device.vibrationDisplacement !== undefined">
<div class="data-item">
<i class="data-icon el-icon-s-operation"></i>
<span class="data-label">振动位移</span>
<span class="data-value">{{ formatValue(device.vibrationDisplacement, 'um') }}</span>
</div>
</div>
<div class="data-row" v-if="device.vibrationAcceleration !== null && device.vibrationAcceleration !== undefined">
<div class="data-item">
<i class="data-icon el-icon-s-operation"></i>
<span class="data-label">振动加速度</span>
<span class="data-value">{{ formatValue(device.vibrationAcceleration, 'g') }}</span>
</div>
</div>
<div class="data-row" v-if="device.vibrationTemp !== null && device.vibrationTemp !== undefined">
<div class="data-item">
<i class="data-icon el-icon-sunny" style="color: #ff6b35;"></i>
<span class="data-label">振动温度</span>
<span class="data-value">{{ formatValue(device.vibrationTemp, '℃') }}</span>
</div>
</div>
</template>
<!-- 默认情况:显示所有可用数据 -->
<template v-else>
<div class="data-row" v-if="device.temperature !== null && device.temperature !== undefined">
<div class="data-item">
<i class="data-icon el-icon-sunny" style="color: #ff6b35;"></i>
<span class="data-label">温度</span>
<span class="data-value">{{ formatValue(device.temperature, '°C') }}</span>
</div>
</div>
<div class="data-row" v-if="device.humidity !== null && device.humidity !== undefined">
<div class="data-item">
<i class="data-icon el-icon-cloudy"></i>
<span class="data-label">湿度</span>
<span class="data-value">{{ formatValue(device.humidity, '%') }}</span>
</div>
</div>
<div class="data-row" v-if="device.noise !== null && device.noise !== undefined">
<div class="data-item">
<i class="data-icon el-icon-microphone"></i>
<span class="data-label">噪声</span>
<span class="data-value">{{ formatValue(device.noise, 'dB') }}</span>
</div>
</div>
<div class="data-row" v-if="device.vibrationSpeed !== null && device.vibrationSpeed !== undefined">
<div class="data-item">
<i class="data-icon el-icon-s-operation"></i>
<span class="data-label">振动速度</span>
<span class="data-value">{{ formatValue(device.vibrationSpeed, 'mm/s') }}</span>
</div>
</div>
<div class="data-row" v-if="device.vibrationDisplacement !== null && device.vibrationDisplacement !== undefined">
<div class="data-item">
<i class="data-icon el-icon-s-operation"></i>
<span class="data-label">振动位移</span>
<span class="data-value">{{ formatValue(device.vibrationDisplacement, 'um') }}</span>
</div>
</div>
<div class="data-row" v-if="device.vibrationAcceleration !== null && device.vibrationAcceleration !== undefined">
<div class="data-item">
<i class="data-icon el-icon-s-operation"></i>
<span class="data-label">振动加速度</span>
<span class="data-value">{{ formatValue(device.vibrationAcceleration, 'g') }}</span>
</div>
</div>
<div class="data-row" v-if="device.vibrationTemp !== null && device.vibrationTemp !== undefined">
<div class="data-item">
<i class="data-icon el-icon-sunny" style="color: #ff6b35;"></i>
<span class="data-label">振动温度</span>
<span class="data-value">{{ formatValue(device.vibrationTemp, '℃') }}</span>
</div>
</div>
</template>
<!-- 无数据提示 -->
<div v-if="!hasDataForType(device, selectedNodeType)" class="no-data">
<i class="el-icon-warning-outline"></i>
<span>当天无最新数据</span>
</div>
</div>
<!-- 设备底部信息 -->
<div class="device-footer" v-if="device.recodeTime">
<span class="update-time">
<i class="el-icon-time"></i>
记录时间:{{ formatTime(device.recodeTime) }}
</span>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="!loading && filteredDeviceList.length === 0 && selectedNodeName" class="empty-state">
<i class="el-icon-box"></i>
<p>{{ getEmptyStateMessage() }}</p>
</div>
<!-- 未选择提示 -->
<div v-if="!selectedNodeName && !loading" class="empty-state">
<i class="el-icon-s-grid"></i>
<p>请在设备树中选择节点查看设备数据</p>
<span class="empty-tip"></span>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import { getLatestRecords, getLatestRecordsByParentId } from '@/api/ems/record/recordIotenvInstant'
import {getAlarmDataTotalCount, saveWebSocketAlarmData} from "@/api/ems/record/recordAlarmData";
import {getEmsRecordAlarmRuleTotalCount} from "@/api/ems/record/recordAlarmRule";
import { getMonitorInfoTree } from '@/api/ems/base/baseMonitorInfo';
export default {
name: 'Dashboard',
data() {
return {
loading: false,
deviceList: [],
alarmRuleTotalCount: 0,
alarmDataTotalCount: 0,
totalDeviceCount: 0, // 设备总数
// 设备树相关
deviceTreeOptions: [],
deviceTreeFilter: '',
selectedNodeId: null, // 当前选中的节点ID
selectedNodeName: null, // 当前选中的节点名称
selectedNodeType: null, // 当前选中的节点类型
selectedNodeCode: null, // 当前选中的节点Code
deviceTreeProps: {
children: 'children',
label: 'label'
}
}
},
computed: {
// 根据选择的设备树节点过滤设备列表
filteredDeviceList() {
return this.deviceList
}
},
watch: {
// 根据名称筛选设备树
deviceTreeFilter(val) {
this.$refs.deviceTree.filter(val)
}
},
mounted() {
this.loadDeviceTree()
this.loadStatistics() // 初始加载统计数据
// 监听全局WebSocket事件
this.$bus.$on('websocket-device-data', this.handleDeviceData)
this.$bus.$on('websocket-connected', this.onWebSocketConnected)
this.$bus.$on('websocket-disconnected', this.onWebSocketDisconnected)
},
beforeDestroy() {
// 清理事件监听
this.$bus.$off('websocket-device-data', this.handleDeviceData)
this.$bus.$off('websocket-connected', this.onWebSocketConnected)
this.$bus.$off('websocket-disconnected', this.onWebSocketDisconnected)
},
methods: {
async loadStatistics() {
try {
this.alarmDataTotalCount = await getAlarmDataTotalCount();
this.alarmRuleTotalCount = await getEmsRecordAlarmRuleTotalCount();
// 获取所有设备数据用于统计设备总数
const response = await getLatestRecords()
if (response.code === 200) {
const rawData = response.data || []
const allDevices = rawData.filter(device => {
return device.monitorName !== '胶东机场' &&
device.monitorId &&
device.monitorName
})
// 更新设备总数
this.totalDeviceCount = allDevices.length
}
} catch (error) {
console.error('获取统计数据失败:', error)
}
},
async loadDeviceDataByNode(nodeId) {
this.loading = true
try {
console.log('=== 开始加载节点数据 ===')
console.log('节点ID:', nodeId)
// 同时获取当前节点和子节点的数据
const [currentNodeResponse, childNodesResponse] = await Promise.all([
// 获取当前节点自身的数据(通过获取所有数据然后筛选)
getLatestRecords(),
// 获取子节点的数据
getLatestRecordsByParentId(nodeId)
])
let allDeviceData = []
// 处理当前节点自身的数据
if (currentNodeResponse.code === 200) {
const currentNodeData = currentNodeResponse.data || []
console.log('所有设备数据总数:', currentNodeData.length)
console.log('当前节点信息 - ID:', this.selectedNodeId, '名称:', this.selectedNodeName, 'Code:', this.selectedNodeCode)
// 筛选出当前节点对应的设备数据
// 优先通过节点code匹配设备monitorId叶子节点的正确匹配方式
const currentDevices = currentNodeData.filter(device => {
const matchByCode = this.selectedNodeCode && device.monitorId === this.selectedNodeCode
const matchById = device.monitorId === nodeId
const matchByName = device.monitorName === this.selectedNodeName
const isValid = device.monitorName !== '胶东机场' &&
device.monitorId &&
device.monitorName
const matches = (matchByCode || matchById || matchByName) && isValid
if (matches) {
console.log('找到当前节点设备:', device.monitorName, device.monitorId,
matchByCode ? '(通过Code匹配)' :
matchById ? '(通过ID匹配)' : '(通过名称匹配)')
}
return matches
})
console.log('当前节点设备数量:', currentDevices.length)
allDeviceData = [...allDeviceData, ...currentDevices]
}
// 处理子节点数据
if (childNodesResponse.code === 200) {
const childNodesData = childNodesResponse.data || []
console.log('子节点原始数据数量:', childNodesData.length)
const childDevices = childNodesData.filter(device => {
const matches = device.monitorName !== '胶东机场' &&
device.monitorId &&
device.monitorName
if (matches) {
console.log('找到子节点设备:', device.monitorName, device.monitorId)
}
return matches
})
console.log('子节点有效设备数量:', childDevices.length)
allDeviceData = [...allDeviceData, ...childDevices]
}
console.log('合并后设备数据总数:', allDeviceData.length)
// 去重处理基于monitorId
const uniqueDevices = []
const seenIds = new Set()
for (const device of allDeviceData) {
if (!seenIds.has(device.monitorId)) {
seenIds.add(device.monitorId)
uniqueDevices.push(device)
}
}
this.deviceList = uniqueDevices
console.log('最终设备列表数量:', this.deviceList.length)
// 统计当前节点和子节点的设备数量
const currentNodeDeviceCount = this.deviceList.filter(d => {
return (this.selectedNodeCode && d.monitorId === this.selectedNodeCode) ||
d.monitorId === nodeId ||
d.monitorName === this.selectedNodeName
}).length
const childNodeDeviceCount = this.deviceList.length - currentNodeDeviceCount
console.log('当前节点数据数量:', currentNodeDeviceCount)
console.log('子节点数据数量:', childNodeDeviceCount)
console.log('=== 数据加载完成 ===')
} catch (error) {
console.error('获取设备数据失败:', error)
this.$message.error('获取设备数据失败')
this.deviceList = []
} finally {
this.loading = false
}
},
refreshData() {
if (this.selectedNodeId !== null) {
this.loadDeviceDataByNode(this.selectedNodeId)
}
// 删除统计数据的手动刷新统计数据现在通过WebSocket实时更新
// this.loadStatistics()
},
// 加载设备树
async loadDeviceTree() {
const response = await getMonitorInfoTree({})
this.deviceTreeOptions = response.data || []
console.log('设备树数据:', this.deviceTreeOptions)
},
// 设备树节点筛选
filterDeviceNode(value, data) {
if (!value) return true
return data.label.indexOf(value) !== -1
},
// 设备树节点点击事件
handleDeviceNodeClick(data) {
console.log('点击设备树节点:', data)
// 设置选中的节点信息
this.selectedNodeId = data.id
this.selectedNodeName = data.label
this.selectedNodeType = data.type
this.selectedNodeCode = data.code // 添加节点code字段
console.log('选中节点ID:', this.selectedNodeId, '节点名称:', this.selectedNodeName, '节点类型:', this.selectedNodeType, '节点Code:', this.selectedNodeCode)
// 根据选中的节点ID加载该节点下的设备数据
this.loadDeviceDataByNode(this.selectedNodeId)
},
hasData(device) {
// 修正判断逻辑检查是否有任何传感器数据包括0值
return (device.temperature !== null && device.temperature !== undefined) ||
(device.humidity !== null && device.humidity !== undefined) ||
(device.noise !== null && device.noise !== undefined) ||
(device.illuminance !== null && device.illuminance !== undefined) ||
(device.concentration !== null && device.concentration !== undefined) ||
(device.vibrationSpeed !== null && device.vibrationSpeed !== undefined) ||
(device.recodeTime !== null && device.recodeTime !== undefined)
},
hasDataForType(device, type) {
// 根据设备类型检查是否有对应的传感器数据
console.log('检查设备数据类型:', device.monitorName, 'type:', type, 'device:', device)
switch (type) {
case 5: // 温度类型
const hasTemp = device.temperature !== null && device.temperature !== undefined
console.log('温度数据检查:', hasTemp, device.temperature)
return hasTemp
case 6: // 温湿度类型
const hasTempOrHumidity = (device.temperature !== null && device.temperature !== undefined) ||
(device.humidity !== null && device.humidity !== undefined)
console.log('温湿度数据检查:', hasTempOrHumidity, 'temp:', device.temperature, 'humidity:', device.humidity)
return hasTempOrHumidity
case 7: // 噪声类型
const hasNoise = device.noise !== null && device.noise !== undefined
console.log('噪声数据检查:', hasNoise, device.noise)
return hasNoise
case 10: // 振动类型
const hasVibration = (device.vibrationSpeed !== null && device.vibrationSpeed !== undefined) ||
(device.vibrationDisplacement !== null && device.vibrationDisplacement !== undefined) ||
(device.vibrationAcceleration !== null && device.vibrationAcceleration !== undefined) ||
(device.vibrationTemp !== null && device.vibrationTemp !== undefined)
console.log('振动数据检查:', hasVibration, 'speed:', device.vibrationSpeed, 'displacement:', device.vibrationDisplacement)
return hasVibration
default: // 默认情况,检查所有传感器数据
const hasAnyData = this.hasData(device)
console.log('默认数据检查:', hasAnyData)
return hasAnyData
}
},
getEmptyStateMessage() {
// 根据设备类型返回合适的空状态提示信息
switch (this.selectedNodeType) {
case 5:
return '该温度监测节点下暂无有效的温度传感器数据'
case 6:
return '该温湿度监测节点下暂无有效的温湿度传感器数据'
case 7:
return '该噪声监测节点下暂无有效的噪声传感器数据'
case 10:
return '该振动监测节点下暂无有效的振动传感器数据'
default:
return '该节点下暂无有效的传感器设备数据,请检查设备连接状态或选择其他节点'
}
},
getNodeTypeText(type) {
// 返回节点类型的中文描述
switch (type) {
case 5:
return '温度监测'
case 6:
return '温湿度监测'
case 7:
return '噪声监测'
case 10:
return '振动监测'
default:
return '监测设备'
}
},
getDeviceStatus(device) {
if (!this.hasData(device)) {
return 'offline'
}
// 根据数据值判断设备状态
// if (device.temperature && (device.temperature > 40 || device.temperature < -10)) {
// return 'error'
// }
// if (device.humidity && (device.humidity > 90 || device.humidity < 10)) {
// return 'warning'
// }
// if (device.noise && device.noise > 80) {
// return 'warning'
// }
// if (device.concentration && device.concentration > 10) {
// return 'error'
// }
return 'normal'
},
getStatusText(device) {
const status = this.getDeviceStatus(device)
const statusMap = {
'normal': '正常',
// 'warning': '警告',
// 'error': '异常',
'offline': '离线'
}
return statusMap[status] || '未知'
},
formatValue(value, unit) {
if (value === null || value === undefined) {
return `0.0 ${unit}`
}
return `${Number(value).toFixed(1)} ${unit}`
},
formatTime(time) {
if (!time) return '--'
// 使用原生Date对象格式化时间避免moment.js依赖问题
try {
const date = new Date(time)
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 `${month}-${day} ${hours}:${minutes}:${seconds}`
} catch (error) {
console.error('时间格式化失败:', error)
return '--'
}
},
// 保存WebSocket告警数据到数据库
async saveRealtimeAlarmData(alarmData) {
try {
// 构建符合EmsRecordAlarmData实体的数据列表
// 每个触发的告警规则对应一条EmsRecordAlarmData记录
const alarmDataList = []
if (!alarmData.alarmRules || alarmData.alarmRules.length === 0) {
console.warn('告警数据中没有告警规则')
return
}
// 获取当前时间并格式化为后端期望的格式
const getCurrentTimeForBackend = () => {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const hours = String(now.getHours()).padStart(2, '0')
const minutes = String(now.getMinutes()).padStart(2, '0')
const seconds = String(now.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
// 为每个触发的告警规则创建一条记录
for (const rule of alarmData.alarmRules) {
// 获取对应字段的实际数值
const actualValue = this.getActualValueFromDeviceParam(alarmData.deviceParam, rule.monitorField)
const alarmRecord = {
// 设备相关信息
monitorId: alarmData.monitorId,
collectTime: getCurrentTimeForBackend(), // 使用当前时间并格式化为后端期望格式
// 告警类型根据规则的triggerRule设置0=大于阈值1=小于阈值)
alarmType: rule.triggerRule || 0,
// 告警状态1=未处理
alarmStatus: 1,
// 告警数据:实际触发告警的数值
alarmData: actualValue ? String(actualValue) : '',
// 告警原因:使用字段名称
cause: this.getFieldName(rule.monitorField),
// 其他字段可以为空,后端会设置默认值
operationName: null,
operationTime: null,
notifyUser: null
}
alarmDataList.push(alarmRecord)
console.log('构建告警记录:', {
设备ID: alarmRecord.monitorId,
告警字段: alarmRecord.cause,
实际数值: alarmRecord.alarmData,
告警类型: alarmRecord.alarmType === 0 ? '大于阈值' : '小于阈值',
规则阈值: rule.triggerValue,
记录时间: alarmRecord.collectTime
})
}
if (alarmDataList.length === 0) {
console.warn('没有有效的告警记录可保存')
return
}
// 发送到后端保存
const response = await saveWebSocketAlarmData(alarmDataList)
if (response.code === 200) {
console.log('告警数据保存成功:', response.msg)
// 告警数据保存成功新的告警记录会通过WebSocket实时推送无需手动刷新
// this.getAlarmDataList()
} else {
console.error('告警数据保存失败:', response.msg)
this.$message.error('告警数据保存失败: ' + response.msg)
}
} catch (error) {
console.error('保存告警数据异常:', error)
this.$message.error('保存告警数据异常')
}
},
// 从设备参数中获取指定字段的实际数值
getActualValueFromDeviceParam(deviceParam, monitorField) {
if (!deviceParam || monitorField === null || monitorField === undefined) {
return null
}
switch (monitorField) {
case 0: // 温度
return deviceParam.temperature
case 1: // 湿度
return deviceParam.humidity
case 2: // 振动-速度(mm/s)
return deviceParam.vibrationSpeed || deviceParam.VibrationSpeed
case 3: // 振动-位移(um)
return deviceParam.vibrationDisplacement || deviceParam.VibrationDisplacement
case 4: // 振动-加速度(g)
return deviceParam.vibrationAcceleration || deviceParam.VibrationAcceleration
case 5: // 振动-温度(℃)
return deviceParam.vibrationTemp || deviceParam.VibrationTemp
case 6: // 噪音
return deviceParam.noise
case 7: // 照度
return deviceParam.illuminance
case 8: // 气体浓度
return deviceParam.concentration
default:
console.warn('未知的监测字段:', monitorField)
return null
}
},
// 获取监测字段名称
getFieldName(fieldCode) {
const fieldMap = {
0: '温度',
1: '湿度',
2: '振动-速度(mm/s)',
3: '振动-位移(um)',
4: '振动-加速度(g)',
5: '振动-温度(℃)',
6: '噪音',
7: '照度',
8: '气体浓度'
}
return fieldMap[fieldCode] || '未知字段'
},
// 处理全局WebSocket设备数据
handleDeviceData(data) {
const deviceParam = data.deviceParam
if (!deviceParam || !deviceParam.monitorId) {
return
}
// 如果当前选中了节点,只更新该节点下的设备数据
if (this.selectedNodeId !== null) {
this.updateDeviceDataInList(deviceParam)
} else {
// 如果没有选中节点,可以选择是否更新全局设备列表
console.log('收到设备数据但未选中节点:', deviceParam.monitorId)
}
// 如果有告警数据,更新告警统计
if (data.isFlag === 1 && data.alarmRules && data.alarmRules.length > 0) {
this.alarmDataTotalCount += data.alarmRules.length
console.log('实时更新告警统计,新增告警数量:', data.alarmRules.length, '当前总数:', this.alarmDataTotalCount)
}
},
// 更新设备列表中的数据
updateDeviceDataInList(deviceParam) {
const index = this.deviceList.findIndex(device => device.monitorId === deviceParam.monitorId)
if (index !== -1) {
// 更新现有设备数据
const updatedDevice = {
...this.deviceList[index],
temperature: deviceParam.temperature,
humidity: deviceParam.humidity,
illuminance: deviceParam.illuminance,
noise: deviceParam.noise,
concentration: deviceParam.concentration,
vibrationSpeed: deviceParam.vibrationSpeed || deviceParam.VibrationSpeed,
vibrationDisplacement: deviceParam.vibrationDisplacement || deviceParam.VibrationDisplacement,
vibrationAcceleration: deviceParam.vibrationAcceleration || deviceParam.VibrationAcceleration,
vibrationTemp: deviceParam.vibrationTemp || deviceParam.VibrationTemp,
collectTime: deviceParam.collectTime,
recodeTime: deviceParam.recordTime
}
// 使用Vue.set确保响应式更新
this.$set(this.deviceList, index, updatedDevice)
console.log('更新设备数据:', deviceParam.monitorId)
} else {
console.log('设备不在当前节点列表中:', deviceParam.monitorId)
}
},
// WebSocket连接状态变化处理
onWebSocketConnected() {
console.log('Dashboard: WebSocket连接已建立')
this.$message.success('实时数据连接已建立')
},
onWebSocketDisconnected() {
console.log('Dashboard: WebSocket连接已断开')
this.$message.warning('实时数据连接已断开')
},
navigateToDeviceList() {
// 跳转到设备列表页面
this.$router.push('/ems/base/baseMonitorInfoIOTDevice/index')
},
navigateToAlarmRules() {
// 跳转到异常规则列表页面
this.$router.push('/ems/record/recordAlarmRule/index')
},
navigateToAlarmData() {
// 跳转到异常数据列表页面
this.$router.push('/ems/record/recordAlarmData/index')
},
// 判断设备是否属于当前选中的节点
isCurrentNodeDevice(device) {
// 优先通过code匹配叶子节点的正确方式
if (this.selectedNodeCode && device.monitorId === this.selectedNodeCode) {
return true
}
// 其次通过ID匹配
if (device.monitorId === this.selectedNodeId) {
return true
}
// 最后通过名称匹配
if (device.monitorName === this.selectedNodeName) {
return true
}
return false
},
}
}
</script>
<style lang="scss" scoped>
.app-container {
padding: 20px;
background-color: #f5f7fa;
//min-height: 100vh;
height: calc(100vh - 134px);
overflow: auto;
}
// 左侧设备树样式
.head-container {
background: white;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.el-tree {
background: transparent;
}
.el-tree-node__content {
height: 32px;
line-height: 32px;
border-radius: 6px;
margin-bottom: 2px;
&:hover {
background-color: #f5f7fa;
}
}
.el-tree-node.is-current > .el-tree-node__content {
background-color: #ecf5ff;
color: #409eff;
}
.el-tree-node__expand-icon {
color: #c0c4cc;
&.is-leaf {
color: transparent;
}
}
}
// 统计卡片区域
.stats-section {
margin-bottom: 20px;
.stats-grid {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.stat-card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
transition: transform 0.3s ease, box-shadow 0.3s ease;
min-width: 200px;
flex: 1;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
&.clickable {
cursor: pointer;
&:hover {
transform: translateY(-3px);
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.2);
}
&:active {
transform: translateY(-1px);
}
}
.stat-icon {
width: 50px;
height: 50px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 14px;
i {
font-size: 20px;
color: white;
}
}
.stat-content {
flex: 1;
.stat-number {
font-size: 24px;
font-weight: 700;
line-height: 1;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
color: #7f8c8d;
}
}
&.total {
.stat-icon {
background: linear-gradient(135deg, #409eff, #66b1ff);
}
.stat-number {
color: #409eff;
}
}
&.alarm-rule {
.stat-icon {
background: linear-gradient(135deg, #e6a23c, #f0c040);
}
.stat-number {
color: #e6a23c;
}
}
&.alarm-data {
.stat-icon {
background: linear-gradient(135deg, #f56c6c, #ff8080);
}
.stat-number {
color: #f56c6c;
}
}
}
}
// 设备监控数据区域
.monitoring-section {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #e9ecef;
.section-title {
font-size: 18px;
font-weight: 600;
color: #2c3e50;
margin: 0;
}
.section-actions {
display: flex;
align-items: center;
gap: 12px;
.selected-node {
font-size: 14px;
color: #409eff;
background: #ecf5ff;
padding: 4px 12px;
border-radius: 4px;
border: 1px solid #d9ecff;
.node-type-info {
font-size: 12px;
color: #67c23a;
font-weight: 500;
margin-left: 4px;
}
}
}
}
.device-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.device-card {
background: #f8f9fa;
border-radius: 10px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
border-left: 4px solid #e9ecef;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
&.normal {
border-left-color: #67c23a;
}
&.warning {
border-left-color: #e6a23c;
}
&.error {
border-left-color: #f56c6c;
}
&.offline {
border-left-color: #909399;
opacity: 0.8;
}
}
.device-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
.device-info {
flex: 1;
.device-title-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.device-name {
font-size: 16px;
font-weight: 600;
color: #2c3e50;
margin: 0;
flex: 1;
}
.node-type-badge {
font-size: 10px;
padding: 2px 6px;
border-radius: 10px;
font-weight: 500;
white-space: nowrap;
&.current {
background: linear-gradient(135deg, #409eff, #66b1ff);
color: white;
}
&.child {
background: linear-gradient(135deg, #67c23a, #85ce61);
color: white;
}
}
.device-id {
font-size: 12px;
color: #7f8c8d;
background: white;
padding: 2px 8px;
border-radius: 4px;
}
}
.device-status {
display: flex;
align-items: center;
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
&.normal {
background-color: #67c23a;
}
&.warning {
background-color: #e6a23c;
}
&.error {
background-color: #f56c6c;
}
&.offline {
background-color: #909399;
}
}
.status-text {
font-size: 12px;
color: #7f8c8d;
}
}
}
.device-data {
.data-row {
margin-bottom: 8px;
&:last-child {
margin-bottom: 0;
}
}
.data-item {
display: flex;
align-items: center;
padding: 6px 0;
.data-icon {
width: 20px;
color: #409eff;
margin-right: 8px;
}
.data-label {
flex: 1;
font-size: 14px;
color: #606266;
}
.data-value {
font-size: 14px;
font-weight: 600;
color: #2c3e50;
}
}
.no-data {
text-align: center;
padding: 20px;
color: #909399;
i {
font-size: 24px;
margin-bottom: 8px;
display: block;
}
}
}
.device-footer {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e9ecef;
.update-time {
font-size: 11px;
color: #7f8c8d;
i {
margin-right: 4px;
}
}
}
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #909399;
i {
font-size: 48px;
margin-bottom: 16px;
display: block;
}
p {
font-size: 16px;
margin: 0 0 8px 0;
color: #606266;
}
.empty-tip {
font-size: 14px;
color: #909399;
font-style: italic;
}
}
// 响应式设计
@media (max-width: 768px) {
.app-container {
padding: 16px;
}
.stats-grid {
flex-direction: column;
}
.device-grid {
grid-template-columns: 1fr;
}
.section-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
.section-actions {
width: 100%;
justify-content: space-between;
}
}
}
</style>