|
|
@ -1,100 +1,575 @@
|
|
|
|
<!--
|
|
|
|
|
|
|
|
<template>
|
|
|
|
<template>
|
|
|
|
<div class="dashboard-editor-container">
|
|
|
|
<div class="dashboard-container">
|
|
|
|
<!– PanelGroup 组件,绑定了 handleSetLineChartData 方法 –>
|
|
|
|
<!-- 页面标题 -->
|
|
|
|
<panel-group @handleSetLineChartData="handleSetLineChartData" />
|
|
|
|
<div class="page-header">
|
|
|
|
|
|
|
|
<h1 class="page-title">机场行李系统设备健康监测系统</h1>
|
|
|
|
|
|
|
|
<div class="page-subtitle">首页</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!– 折线图部分 –>
|
|
|
|
<!-- 统计卡片区域 -->
|
|
|
|
<el-row style="background:#fff;padding:16px 16px 0;margin-bottom:32px;">
|
|
|
|
<div class="stats-section">
|
|
|
|
<line-chart :chart-data="lineChartData" />
|
|
|
|
<div class="stats-grid">
|
|
|
|
</el-row>
|
|
|
|
<div class="stat-card total">
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!– <el-row :gutter="32">–>
|
|
|
|
<!-- 设备监控数据区域 -->
|
|
|
|
<!– <el-col :xs="24" :sm="24" :lg="8">–>
|
|
|
|
<div class="monitoring-section">
|
|
|
|
<!– <div class="chart-wrapper">–>
|
|
|
|
<div class="section-header">
|
|
|
|
<!– <raddar-chart />–>
|
|
|
|
<h2 class="section-title">设备监控数据</h2>
|
|
|
|
<!– </div>–>
|
|
|
|
<div class="section-actions">
|
|
|
|
<!– </el-col>–>
|
|
|
|
<el-button
|
|
|
|
<!– <el-col :xs="24" :sm="24" :lg="8">–>
|
|
|
|
type="primary"
|
|
|
|
<!– <div class="chart-wrapper">–>
|
|
|
|
icon="el-icon-refresh"
|
|
|
|
<!– <pie-chart />–>
|
|
|
|
size="small"
|
|
|
|
<!– </div>–>
|
|
|
|
@click="refreshData"
|
|
|
|
<!– </el-col>–>
|
|
|
|
:loading="loading">
|
|
|
|
<!– <el-col :xs="24" :sm="24" :lg="8">–>
|
|
|
|
刷新数据
|
|
|
|
<!– <div class="chart-wrapper">–>
|
|
|
|
</el-button>
|
|
|
|
<!– <bar-chart />–>
|
|
|
|
</div>
|
|
|
|
<!– </div>–>
|
|
|
|
</div>
|
|
|
|
<!– </el-col>–>
|
|
|
|
|
|
|
|
<!– </el-row>–>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 设备数据卡片网格 -->
|
|
|
|
|
|
|
|
<div class="device-grid" v-loading="loading">
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
|
|
v-for="device in deviceList"
|
|
|
|
|
|
|
|
:key="device.monitorId"
|
|
|
|
|
|
|
|
class="device-card"
|
|
|
|
|
|
|
|
:class="getDeviceStatus(device)">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 设备头部信息 -->
|
|
|
|
|
|
|
|
<div class="device-header">
|
|
|
|
|
|
|
|
<div class="device-info">
|
|
|
|
|
|
|
|
<h3 class="device-name">{{ device.monitorName || '未知设备' }}</h3>
|
|
|
|
|
|
|
|
<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">
|
|
|
|
|
|
|
|
<div class="data-row" v-if="device.temperature !== null && device.temperature !== undefined">
|
|
|
|
|
|
|
|
<div class="data-item">
|
|
|
|
|
|
|
|
<i class="data-icon el-icon-thermometer"></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.illuminance !== null && device.illuminance !== undefined">
|
|
|
|
|
|
|
|
<div class="data-item">
|
|
|
|
|
|
|
|
<i class="data-icon el-icon-sunny"></i>
|
|
|
|
|
|
|
|
<span class="data-label">照度</span>
|
|
|
|
|
|
|
|
<span class="data-value">{{ formatValue(device.illuminance, 'lx') }}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="data-row" v-if="device.concentration !== null && device.concentration !== undefined">
|
|
|
|
|
|
|
|
<div class="data-item">
|
|
|
|
|
|
|
|
<i class="data-icon el-icon-warning-outline"></i>
|
|
|
|
|
|
|
|
<span class="data-label">硫化氢</span>
|
|
|
|
|
|
|
|
<span class="data-value">{{ formatValue(device.concentration, 'ppm') }}</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 v-if="!hasData(device)" 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 && deviceList.length === 0" class="empty-state">
|
|
|
|
|
|
|
|
<i class="el-icon-box"></i>
|
|
|
|
|
|
|
|
<p>暂无设备数据</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
<script>
|
|
|
|
import PanelGroup from './dashboard/PanelGroup'
|
|
|
|
import { getLatestRecords } from '@/api/ems/record/recordIotenvInstant'
|
|
|
|
import LineChart from './dashboard/LineChart'
|
|
|
|
|
|
|
|
import RaddarChart from './dashboard/RaddarChart'
|
|
|
|
|
|
|
|
import PieChart from './dashboard/PieChart'
|
|
|
|
|
|
|
|
import BarChart from './dashboard/BarChart'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const lineChartData = {
|
|
|
|
|
|
|
|
newVisitis: {
|
|
|
|
|
|
|
|
expectedData: [100, 120, 161, 134, 105, 160, 165],
|
|
|
|
|
|
|
|
actualData: [120, 82, 91, 154, 162, 140, 145]
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
messages: {
|
|
|
|
|
|
|
|
expectedData: [200, 192, 120, 144, 160, 130, 140],
|
|
|
|
|
|
|
|
actualData: [180, 160, 151, 106, 145, 150, 130]
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
purchases: {
|
|
|
|
|
|
|
|
expectedData: [80, 100, 121, 104, 105, 90, 100],
|
|
|
|
|
|
|
|
actualData: [120, 90, 100, 138, 142, 130, 130]
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
shoppings: {
|
|
|
|
|
|
|
|
expectedData: [130, 140, 141, 142, 145, 150, 160],
|
|
|
|
|
|
|
|
actualData: [120, 82, 91, 154, 162, 140, 130]
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
export default {
|
|
|
|
name: 'Index',
|
|
|
|
name: 'Dashboard',
|
|
|
|
components: {
|
|
|
|
|
|
|
|
PanelGroup,
|
|
|
|
|
|
|
|
LineChart,
|
|
|
|
|
|
|
|
RaddarChart,
|
|
|
|
|
|
|
|
PieChart,
|
|
|
|
|
|
|
|
BarChart
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
data() {
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
lineChartData: lineChartData.newVisitis
|
|
|
|
loading: false,
|
|
|
|
|
|
|
|
deviceList: [],
|
|
|
|
|
|
|
|
refreshTimer: null
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
computed: {
|
|
|
|
|
|
|
|
totalDeviceCount() {
|
|
|
|
|
|
|
|
return this.deviceList.length
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
mounted() {
|
|
|
|
|
|
|
|
this.loadDeviceData()
|
|
|
|
|
|
|
|
// 设置自动刷新,每1分钟刷新一次
|
|
|
|
|
|
|
|
this.refreshTimer = setInterval(() => {
|
|
|
|
|
|
|
|
this.loadDeviceData()
|
|
|
|
|
|
|
|
}, 60000)
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
beforeDestroy() {
|
|
|
|
|
|
|
|
if (this.refreshTimer) {
|
|
|
|
|
|
|
|
clearInterval(this.refreshTimer)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
methods: {
|
|
|
|
handleSetLineChartData(type) {
|
|
|
|
async loadDeviceData() {
|
|
|
|
this.lineChartData = lineChartData[type]
|
|
|
|
this.loading = true
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const response = await getLatestRecords()
|
|
|
|
|
|
|
|
if (response.code === 200) {
|
|
|
|
|
|
|
|
// 过滤掉异常数据
|
|
|
|
|
|
|
|
const rawData = response.data || []
|
|
|
|
|
|
|
|
this.deviceList = rawData.filter(device => {
|
|
|
|
|
|
|
|
// 过滤条件:
|
|
|
|
|
|
|
|
// 1. 排除monitorName为"胶东机场"的记录
|
|
|
|
|
|
|
|
// 2. 确保设备有基本信息(monitorId和monitorName不为空)
|
|
|
|
|
|
|
|
return device.monitorName !== '胶东机场' &&
|
|
|
|
|
|
|
|
device.monitorId &&
|
|
|
|
|
|
|
|
device.monitorName
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
console.log('原始设备数据:', rawData)
|
|
|
|
|
|
|
|
console.log('过滤后设备数据:', this.deviceList)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 如果过滤掉了数据,给出提示
|
|
|
|
|
|
|
|
const filteredCount = rawData.length - this.deviceList.length
|
|
|
|
|
|
|
|
if (filteredCount > 0) {
|
|
|
|
|
|
|
|
console.log(`已过滤掉 ${filteredCount} 条异常数据`)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
this.$message.error(response.msg || '获取设备数据失败')
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
|
|
console.error('获取设备数据失败:', error)
|
|
|
|
|
|
|
|
this.$message.error('获取设备数据失败')
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
|
|
this.loading = false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
refreshData() {
|
|
|
|
|
|
|
|
this.loadDeviceData()
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 '--'
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
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 '--'
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
.dashboard-editor-container {
|
|
|
|
.dashboard-container {
|
|
|
|
padding: 32px;
|
|
|
|
padding: 20px;
|
|
|
|
background-color: rgb(240, 242, 245);
|
|
|
|
background-color: #f5f7fa;
|
|
|
|
position: relative;
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chart-wrapper {
|
|
|
|
.page-header {
|
|
|
|
background: #fff;
|
|
|
|
text-align: center;
|
|
|
|
padding: 16px 16px 0;
|
|
|
|
margin-bottom: 30px;
|
|
|
|
margin-bottom: 32px;
|
|
|
|
|
|
|
|
|
|
|
|
.page-title {
|
|
|
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
|
|
color: #2c3e50;
|
|
|
|
|
|
|
|
margin: 0 0 8px 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.page-subtitle {
|
|
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
|
|
color: #7f8c8d;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width:1024px) {
|
|
|
|
.stats-section {
|
|
|
|
.chart-wrapper {
|
|
|
|
margin-bottom: 30px;
|
|
|
|
padding: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
.stats-grid {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.stat-card {
|
|
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
|
|
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: 250px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
|
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.stat-icon {
|
|
|
|
|
|
|
|
width: 60px;
|
|
|
|
|
|
|
|
height: 60px;
|
|
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
|
|
margin-right: 16px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
i {
|
|
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.stat-content {
|
|
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.stat-number {
|
|
|
|
|
|
|
|
font-size: 32px;
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.monitoring-section {
|
|
|
|
|
|
|
|
.section-header {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
|
|
color: #2c3e50;
|
|
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.device-grid {
|
|
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.device-card {
|
|
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
|
|
border-left: 4px solid #e9ecef;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
|
|
|
box-shadow: 0 4px 20px 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-name {
|
|
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
|
|
color: #2c3e50;
|
|
|
|
|
|
|
|
margin: 0 0 4px 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.device-id {
|
|
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
|
|
color: #7f8c8d;
|
|
|
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
|
|
|
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: 12px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&:last-child {
|
|
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.data-item {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
padding: 8px 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: 16px;
|
|
|
|
|
|
|
|
padding-top: 16px;
|
|
|
|
|
|
|
|
border-top: 1px solid #f0f0f0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.update-time {
|
|
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
|
|
.dashboard-container {
|
|
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.stats-grid {
|
|
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.device-grid {
|
|
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.section-header {
|
|
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
|
|
gap: 12px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</style>
|
|
|
|
-->
|
|
|
|
|
|
|
|