|
|
<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>
|