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

2057 lines
81 KiB

<template class="">
<div class="index-box">
<div id="app" class="dashboard" style="overflow-x: hidden;">
<div class="header">
<h1>唐山市设备监控中心大屏 - 智能管理版</h1>
<div class="time-display">{{ currentTime }}</div>
</div>
<div class="main-content">
<!-- 左侧面板 -->
<div class="left-panel">
<div class="panel">
<div class="panel-title">
设备状态监控
<div class="panel-actions">
<div class="panel-btn" @click="refreshDeviceStatus">刷新</div>
</div>
</div>
<div class="device-list" style="height: calc(100% - 50px); overflow-y: auto;">
<div v-for="device in deviceStatus || []"
:key="device.id || Math.random()"
class="device-item"
@click="showDeviceDetail(device)">
<div :class="['device-status', `status-${device.status || 'normal'}`]"></div>
<div class="device-name">{{ device.equManuName || device.name || '未知设备' }}</div>
<div class="device-value">{{ device.cpuUtilization || 0 }}%</div>
</div>
</div>
</div>
<div class="panel">
<div class="panel-title">
楼层设备分布情况
</div>
<div class="floor-device-container">
<div class="floor-device-grid">
<div v-for="floor in floorDeviceDistribution"
:key="floor.level"
class="floor-device-card">
<div class="floor-level">{{ floor.floor }}</div>
<div class="floor-count">{{ floor.count }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧面板 -->
<div class="right-panel">
<div class="panel">
<div class="panel-title">设备信息统计</div>
<div class="device-stats-container">
<div class="device-stat-item">
<div class="device-stat-value">{{ totalDevices }}</div>
<div class="device-stat-label">设备总数</div>
</div>
<div class="device-stat-item">
<div class="device-stat-value">{{ onlineDevices }}</div>
<div class="device-stat-label">在线设备数</div>
</div>
</div>
<div class="chart-container" ref="pieChart"></div>
</div>
<div class="panel">
<div class="panel-title">
预警信息统计
</div>
<div class="warning-stats">
<div class="warning-row">
<div class="stat-item time-item" @click="filterByWarningLevel('annual')">
<div class="stat-value stat-annual">{{ warningStats.annual }}</div>
<div class="stat-label">年度预警</div>
</div>
<div class="stat-item time-item" @click="filterByWarningLevel('monthly')">
<div class="stat-value stat-monthly">{{ warningStats.monthly }}</div>
<div class="stat-label">月度预警</div>
</div>
</div>
<div class="warning-row">
<div class="stat-item level-item" @click="filterByWarningLevel('level1')">
<div class="stat-value stat-level1">{{ warningStats.level1 }}</div>
<div class="stat-label">1级预警</div>
</div>
<div class="stat-item level-item" @click="filterByWarningLevel('level2')">
<div class="stat-value stat-level2">{{ warningStats.level2 }}</div>
<div class="stat-label">2级预警</div>
</div>
<div class="stat-item level-item" @click="filterByWarningLevel('level3')">
<div class="stat-value stat-level3">{{ warningStats.level3 }}</div>
<div class="stat-label">3级预警</div>
</div>
</div>
</div>
</div>
</div>
<!-- 中间面板 - 楼层房间布局展示 -->
<div class="center-panel">
<div class="panel">
<div class="panel-title">
近期预警信息
</div>
<el-scrollbar class="warning-scrollbar">
<div v-for="warning in filteredWarnings"
:key="warning.id"
class="warning-item"
@click="showDeviceDetail(warning)">
<div class="warning-header">
<span :class="['warning-type', `level-${warning.displayLevel}`]">{{ warning.name }}</span>
</div>
<div class="warning-time">预警时间: {{ warning.time }}</div>
<div class="warning-position">设备位置: {{ warning.location }}</div>
<div class="warning-detail">预警信息: {{ warning.ruleName }}</div>
</div>
</el-scrollbar>
</div>
</div>
</div>
<div class="footer">
<div>唐山市监控系统 v4.0 - 智能管理版</div>
<div>最后更新: <span class="update-time">{{ updateTime }}</span></div>
<div>监控设备总数: {{ totalDevices }} | 在线: {{ onlineDevices }} </div>
</div>
<!-- 设备详情模态框 -->
<div class="modal-overlay" v-if="showDeviceModal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">设备详情 - {{ selectedDevice.name }}</h2>
<button class="modal-close" @click="closeDeviceModal">&times;</button>
</div>
<div class="modal-body">
<div class="device-detail-grid">
<div class="detail-section">
<h3>基本信息</h3>
<div class="detail-item">
<span class="detail-label">设备名称:</span>
<span class="detail-value">{{ selectedDevice.name }}</span>
</div>
<div class="detail-item">
<span class="detail-label">设备ID:</span>
<span class="detail-value">{{ selectedDevice.id }}</span>
</div>
<div class="detail-item">
<span class="detail-label">所在位置:</span>
<span class="detail-value">{{ selectedDevice.location || '未知' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">设备类型:</span>
<span class="detail-value">{{ selectedDevice.type || '服务器' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">设备状态:</span>
<span class="detail-value" :style="{color: selectedDevice.status === '1' ? 'green' : 'red'}">
{{ getStatusText(selectedDevice.status) }}
</span>
</div>
<!-- <div class="detail-item">
<span class="detail-label">当前值:</span>
<span class="detail-value">{{ selectedDevice.value || 'N/A' }}</span>
</div> -->
</div>
<div class="detail-section">
<h3>运行状态</h3>
<div class="detail-item">
<span class="detail-label">运行时间:</span>
<span class="detail-value">{{ formatDuration(selectedDevice.contWorkPeriod) || '0天' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">CPU使用率:</span>
<span class="detail-value">{{ selectedDevice.cpuUtilization || '0' }}%</span>
</div>
<div class="detail-item">
<span class="detail-label">内存使用率:</span>
<span class="detail-value">{{ selectedDevice.memoryUtilization || '0' }}%</span>
</div>
<div class="detail-item">
<span class="detail-label">风扇转速:</span>
<span class="detail-value">{{ selectedDevice.fanSpeed || '0' }}'RPM/s'</span>
</div>
<div class="detail-item">
<span class="detail-label">温度:</span>
<span class="detail-value">{{ selectedDevice.temperature || '0' }}°C</span>
</div>
<!-- <div class="detail-item">
<span class="detail-label">最后维护:</span>
<span class="detail-value">{{ selectedDevice.lastMaintenance || '2024-09-15' }}</span>
</div> -->
<div class="control-buttons">
<button class="control-btn btn-restart" @click="controlDevice('restart')">重启设备</button>
<button class="control-btn btn-shutdown" @click="controlDevice('shutdown')">关机</button>
<button class="control-btn btn-reset" @click="controlDevice('reset')">重置配置</button>
</div>
</div>
</div>
<div class="detail-section">
<h3>历史数据
<div class="search-controls">
<input type="date" class="date-input" v-model="historyStartDate">
<span></span>
<input type="date" class="date-input" v-model="historyEndDate">
<button class="panel-btn" @click="loadHistoryData">查询</button>
</div>
</h3>
<div class="history-chart" ref="historyChart"></div>
</div>
</div>
</div>
</div>
<!-- 房间设备列表模态框 -->
<div class="modal-overlay" v-if="showRoomModal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">房间设备 - {{ selectedRoom.name }}</h2>
<button class="modal-close" @click="closeRoomModal">&times;</button>
</div>
<div class="modal-body">
<div class="device-list">
<div v-for="device in getRoomDevices()"
:key="device.id"
class="device-item"
@click="showDeviceDetail(device)">
<div :class="['device-status', `status-${device.status}`]"></div>
<div class="device-name">{{ device.name }}</div>
<div class="device-value">{{ device.value || 'N/A' }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
10 months ago
</template>
10 months ago
<script>
import { getInfo, historyQuery } from "@/api/inMonitoring/devicesState"
import { byEquType, statistic, alertMessageTimes, floorStatistic } from "@/api/index/index"
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
import "splitpanes/dist/splitpanes.css"
import * as echarts from 'echarts'
import { monitorpageList } from "@/api/inMonitoring/devicesState"
require('echarts/theme/macarons') // echarts theme
10 months ago
export default {
name: 'Index',
10 months ago
data() {
return {
currentTime: '',
updateTime: '',
totalDevices: 104,
onlineDevices: 96,
soundAlert: true,
autoSwitch: false,
autoSwitchInterval: null,
showDeviceModal: false,
showRoomModal: false,
showWarningHistory: false,
selectedDevice: {},
selectedRoom: {},
historyStartDate: '',
historyEndDate: '',
warningLevelFilter: 'all',
warningStats: {
annual: 156,
monthly: 18,
level1: 4,
level2: 7,
level3: 7
},
deviceStatus: [],
deviceWarnings: [
{ id: 1, name: 'test1', level: 'error', time: '2025-10-10 20:32:56', deviceId: 11, deviceName: 'test1设备端口21', location: '华峰', description: '区域状态为22' },
{ id: 2, name: 'test1', level: 'error', time: '2025-10-10 20:32:56', deviceId: 11, deviceName: 'test1设备端口', location: '华峰', description: '状态异常,需要立即处理' },
{ id: 3, name: '网络设备异常', level: 'error', time: '2025-10-10 18:15:32', deviceId: 23, deviceName: '交换机端口', location: '数据中心', description: '状态异常,连接数超过阈值' },
{ id: 4, name: '服务器负载过高', level: 'error', time: '2025-10-10 14:23:07', deviceId: 67, deviceName: '核心服务器', location: '服务器机房A', description: 'CPU使用率达到95%,需要优化' },
{ id: 5, name: '网络延迟异常', level: 'warning', time: '2025-10-10 12:05:41', deviceId: 89, deviceName: '路由器网络', location: '网络中心', description: '延迟超过阈值,影响数据传输' },
{ id: 6, name: '安全设备告警', level: 'warning', time: '2025-10-10 10:30:15', deviceId: 56, deviceName: '防火墙', location: '安全区域', description: '检测到异常访问尝试' }
],
currentFloor: 1,
floorDeviceDistribution: [
{ floor: "1层", count: 6 },
],
floors: [
{
level: 1,
name: '一层 - 数据中心',
totalDevices: 32,
onlineDevices: 30,
warningDevices: 2,
rooms: [
{ id: 101, name: '服务器机房A', position: { left: 5, top: 10, width: 25, height: 35 } },
{ id: 102, name: '服务器机房B', position: { left: 35, top: 10, width: 25, height: 35 } },
{ id: 103, name: '存储机房', position: { left: 65, top: 10, width: 30, height: 35 } },
{ id: 104, name: '网络设备间', position: { left: 5, top: 50, width: 25, height: 25 } },
{ id: 105, name: 'UPS电源室', position: { left: 35, top: 50, width: 25, height: 25 } },
{ id: 106, name: '空调机房', position: { left: 65, top: 50, width: 30, height: 25 } },
{ id: 107, name: '走廊', position: { left: 0, top: 80, width: 100, height: 15 } }
],
devices: [
{ id: 101, name: '核心服务器1', status: 'normal', room: '服务器机房A', position: { x: 15, y: 20 }, type: '服务器' },
{ id: 102, name: '核心服务器2', status: 'normal', room: '服务器机房A', position: { x: 15, y: 30 }, type: '服务器' },
{ id: 103, name: '存储阵列A', status: 'warning', room: '存储机房', position: { x: 75, y: 20 }, type: '存储设备' },
{ id: 104, name: '网络交换机', status: 'normal', room: '网络设备间', position: { x: 15, y: 60 }, type: '网络设备' },
{ id: 105, name: '安全审计系统', status: 'error', room: '服务器机房B', position: { x: 45, y: 20 }, type: '安全设备' },
{ id: 106, name: 'UPS电源A', status: 'normal', room: 'UPS电源室', position: { x: 45, y: 60 }, type: '电源设备' },
{ id: 107, name: '备份服务器', status: 'normal', room: '服务器机房B', position: { x: 45, y: 30 }, type: '服务器' },
{ id: 108, name: '数据库集群', status: 'normal', room: '存储机房', position: { x: 75, y: 30 }, type: '服务器' }
]
},
{
level: 2,
name: '二层 - 网络中心',
totalDevices: 28,
onlineDevices: 26,
warningDevices: 2,
rooms: [
{ id: 201, name: '核心交换机房', position: { left: 10, top: 10, width: 35, height: 40 } },
{ id: 202, name: '防火墙区域', position: { left: 50, top: 10, width: 40, height: 40 } },
{ id: 203, name: '监控区域', position: { left: 10, top: 55, width: 35, height: 30 } },
{ id: 204, name: '设备维护室', position: { left: 50, top: 55, width: 40, height: 30 } },
{ id: 205, name: '走廊', position: { left: 0, top: 90, width: 100, height: 5 } }
],
devices: [
{ id: 201, name: '路由器主', status: 'normal', room: '核心交换机房', position: { x: 25, y: 25 }, type: '网络设备' },
{ id: 202, name: '防火墙1', status: 'normal', room: '防火墙区域', position: { x: 65, y: 25 }, type: '安全设备' },
{ id: 203, name: '负载均衡器', status: 'warning', room: '核心交换机房', position: { x: 25, y: 35 }, type: '网络设备' },
{ id: 204, name: 'VPN网关', status: 'normal', room: '防火墙区域', position: { x: 65, y: 35 }, type: '安全设备' },
{ id: 205, name: 'IDS系统', status: 'warning', room: '监控区域', position: { x: 25, y: 70 }, type: '安全设备' },
{ id: 206, name: '网络监控', status: 'normal', room: '监控区域', position: { x: 25, y: 60 }, type: '监控设备' },
{ id: 207, name: '交换机A', status: 'normal', room: '设备维护室', position: { x: 65, y: 70 }, type: '网络设备' },
{ id: 208, name: '交换机B', status: 'normal', room: '设备维护室', position: { x: 75, y: 70 }, type: '网络设备' }
]
},
{
level: 3,
name: '三层 - 监控中心',
totalDevices: 24,
onlineDevices: 22,
warningDevices: 2,
rooms: [
{ id: 301, name: '大屏监控区', position: { left: 5, top: 5, width: 60, height: 45 } },
{ id: 302, name: '操作台区域', position: { left: 70, top: 5, width: 25, height: 45 } },
{ id: 303, name: '存储区', position: { left: 5, top: 55, width: 40, height: 35 } },
{ id: 304, name: '设备间', position: { left: 50, top: 55, width: 45, height: 35 } },
{ id: 305, name: '走廊', position: { left: 0, top: 95, width: 100, height: 5 } }
],
devices: [
{ id: 301, name: '监控服务器', status: 'normal', room: '大屏监控区', position: { x: 30, y: 25 }, type: '服务器' },
{ id: 302, name: '录像设备', status: 'error', room: '存储区', position: { x: 20, y: 70 }, type: '存储设备' },
{ id: 303, name: '分析服务器', status: 'normal', room: '大屏监控区', position: { x: 45, y: 25 }, type: '服务器' },
{ id: 304, name: '报警主机', status: 'normal', room: '操作台区域', position: { x: 80, y: 25 }, type: '安全设备' },
{ id: 305, name: '门禁控制器', status: 'normal', room: '设备间', position: { x: 65, y: 70 }, type: '安防设备' },
{ id: 306, name: '环境监测', status: 'warning', room: '设备间', position: { x: 75, y: 70 }, type: '监控设备' },
{ id: 307, name: '摄像头控制', status: 'normal', room: '操作台区域', position: { x: 80, y: 35 }, type: '安防设备' },
{ id: 308, name: '报警系统', status: 'normal', room: '设备间', position: { x: 65, y: 80 }, type: '安全设备' }
]
},
{
level: 4,
name: '四层 - 办公区域',
totalDevices: 20,
onlineDevices: 18,
warningDevices: 2,
rooms: [
{ id: 401, name: '办公区A', position: { left: 5, top: 10, width: 40, height: 35 } },
{ id: 402, name: '办公区B', position: { left: 50, top: 10, width: 45, height: 35 } },
{ id: 403, name: '会议室', position: { left: 5, top: 50, width: 40, height: 35 } },
{ id: 404, name: '打印区', position: { left: 50, top: 50, width: 20, height: 35 } },
{ id: 405, name: '茶水间', position: { left: 75, top: 50, width: 20, height: 35 } },
{ id: 406, name: '走廊', position: { left: 0, top: 90, width: 100, height: 5 } }
],
devices: [
{ id: 401, name: '办公服务器', status: 'normal', room: '办公区A', position: { x: 25, y: 25 }, type: '服务器' },
{ id: 402, name: '打印机', status: 'warning', room: '打印区', position: { x: 60, y: 65 }, type: '办公设备' },
{ id: 403, name: '网络终端', status: 'normal', room: '办公区B', position: { x: 70, y: 25 }, type: '网络设备' },
{ id: 404, name: '视频会议', status: 'normal', room: '会议室', position: { x: 25, y: 65 }, type: '会议设备' },
{ id: 405, name: '电话系统', status: 'warning', room: '办公区A', position: { x: 25, y: 35 }, type: '通信设备' },
{ id: 406, name: '考勤系统', status: 'normal', room: '办公区B', position: { x: 70, y: 35 }, type: '安防设备' },
{ id: 407, name: '门禁系统', status: 'normal', room: '走廊', position: { x: 50, y: 92 }, type: '安防设备' },
{ id: 408, name: '投影设备', status: 'normal', room: '会议室', position: { x: 25, y: 55 }, type: '会议设备' }
]
}
],
dataFlows: [],
barChart: null,
pieChart: null,
historyChart: null,
highlightedDevice: null,
totalDevices: 0,
onlineDevices: 0,
deviceTypeData: [],
alertStatistics: {},
alertTimes: {},
floorDeviceDistribution: []
}},
computed: {
// 直接使用deviceStatus,不再需要过滤计算属性
// 移除filteredDeviceStatus计算属性,直接使用deviceStatus
filteredWarnings() {
if (this.warningLevelFilter === 'all') {
return this.deviceWarnings;
}
// 根据不同的过滤条件进行处理
switch (this.warningLevelFilter) {
case 'level1':
// 过滤1级预警(level为1)
return this.deviceWarnings.filter(warning => warning.level === 1 || warning.level === '1');
case 'level2':
// 过滤2级预警(level为2)
return this.deviceWarnings.filter(warning => warning.level === 2 || warning.level === '2');
case 'level3':
// 过滤3级预警(level为3)
return this.deviceWarnings.filter(warning => warning.level === 3 || warning.level === '3' || warning.level === 'error');
case 'annual':
case 'monthly':
// 按时间范围过滤(这里可以根据实际需求实现时间过滤逻辑)
// 暂时返回所有数据
return this.deviceWarnings;
case 'error':
// 过滤错误级别的预警
return this.deviceWarnings.filter(warning => warning.level > 2 || warning.level === 'error');
case 'warning':
// 过滤警告级别的预警
return this.deviceWarnings.filter(warning => warning.level <= 2 || warning.level === 'warning');
default:
return this.deviceWarnings;
}
}
},
mounted() {
this.updateTimeDisplay();
setInterval(this.updateTimeDisplay, 1000);
// 初始化日期
const today = new Date();
this.historyEndDate = today.toISOString().split('T')[0];
const oneWeekAgo = new Date(today);
oneWeekAgo.setDate(today.getDate() - 7);
this.historyStartDate = oneWeekAgo.toISOString().split('T')[0];
this.generateDataFlows();
// 使用nextTick确保DOM已完全渲染
this.$nextTick(() => {
this.initPieChart();
});
// 监听窗口大小变化
window.addEventListener('resize', this.handleResize);
// 首次进入时自动加载设备状态数据
this.refreshDeviceStatus();
// 加载设备类型统计数据
this.loadDeviceTypeData();
// 加载预警统计数据
this.loadAlertStatistics();
// 加载预警次数统计
this.loadAlertTimes();
// 加载楼层设备分布
this.loadFloorDeviceDistribution();
// 定期更新设备状态数据(每分钟更新一次)
setInterval(() => {
this.refreshDeviceStatus();
}, 60000);
// 模拟WebSocket连接
this.simulateWebSocket();
},
beforeDestroy() {
// 移除事件监听
window.removeEventListener('resize', this.handleResize);
if (this.autoSwitchInterval) {
clearInterval(this.autoSwitchInterval);
}
},
methods: {
formatDuration(input) {
const regex = /(\d+)\s*days,\s*(\d+):/;
const match = input.match(regex);
return match
? `${match[1]}${match[2]}小时`
: "格式不匹配";
},
updateTimeDisplay() {
const now = new Date();
this.currentTime = now.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
if (!this.updateTime) {
this.updateTime = this.currentTime;
}
},
loadDeviceTypeData() {
byEquType().then(response => {
const data = response.data || {};
// 直接从返回数据中获取设备总数和在线设备数
this.totalDevices = data.totalCount || 0;
this.onlineDevices = data.onlineCount || 0;
// 准备饼图数据,从list数组中提取
const colors = ['#4dabff', '#62f7ff', '#ffa726', '#4CAF50', '#a0aec0', '#ff6b6b', '#95e1d3'];
this.deviceTypeData = (data.list || []).map((item, index) => ({
name: item.equTypeName || '未知类型',
value: item.count || 0,
itemStyle: { color: colors[index % colors.length] }
}));
// 更新饼图
this.initPieChart();
}).catch(error => {
console.error('获取设备类型数据失败:', error);
});
},
// 加载预警统计数据
loadAlertStatistics() {
statistic().then(response => {
this.alertStatistics = response.data || {};
// 处理rows数据并更新deviceWarnings
if (response.rows && Array.isArray(response.rows)) {
this.deviceWarnings = response.rows.map((row, index) => ({
id: row.id || index + 1,
name: row.name || '未命名设备',
level: row.level, // 保留原始的level值
displayLevel: row.level > 2 ? 'error' : 'warning', // 用于显示的级别
time: row.occurTime ? this.formatOccurTime(row.occurTime) : '',
location: row.location || '未知位置',
ruleName: row.ruleName || '-',
message: row.message || '-'
}));
}
}).catch(error => {
console.error('获取预警统计数据失败:', error);
});
},
// 格式化occurTime时间
formatOccurTime(timeString) {
if (!timeString) return '';
const date = new Date(timeString);
if (isNaN(date.getTime())) return timeString;
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
},
// 加载预警次数统计
loadAlertTimes() {
alertMessageTimes().then(response => {
this.alertTimes = response.data || {};
// 根据接口返回的数据更新warningStats
if (response.code === 200 && response.data) {
const data = response.data;
// 更新年度和月度预警数据
this.warningStats.annual = data.yearCount || 0;
this.warningStats.monthly = data.monthCount || 0;
// 从levelCount中提取1-3级预警数据
if (data.levelCount && Array.isArray(data.levelCount)) {
// 初始化1-3级预警数据为0
this.warningStats.level1 = 0;
this.warningStats.level2 = 0;
this.warningStats.level3 = 0;
// 根据level字段更新对应级别的数据
data.levelCount.forEach(item => {
if (item.level === '1') {
this.warningStats.level1 = item.count || 0;
} else if (item.level === '2') {
this.warningStats.level2 = item.count || 0;
} else if (item.level === '3') {
this.warningStats.level3 = item.count || 0;
}
});
}
}
}).catch(error => {
console.error('获取预警次数数据失败:', error);
});
},
// 加载楼层设备分布
loadFloorDeviceDistribution() {
floorStatistic().then(response => {
// 创建1-12层的默认数组,初始count为0,使用floor字段存储楼层名称
const floorData = [];
for (let i = 1; i <= 12; i++) {
floorData.push({ floor: i + '层', count: 0 });
}
// 如果API返回了数据,更新对应楼层的count值
if (response && response.code === 200 && response.data && Array.isArray(response.data)) {
response.data.forEach(item => {
// 查找对应的楼层索引
const index = floorData.findIndex(f => f.floor === item.floor);
if (index !== -1) {
floorData[index].count = item.count || 0;
}
});
}
this.floorDeviceDistribution = floorData;
}).catch(error => {
console.error('获取楼层设备分布失败:', error);
// 出错时仍显示1-12层,count为0
const floorData = [];
for (let i = 1; i <= 12; i++) {
floorData.push({ floor: i + '层', count: 0 });
}
this.floorDeviceDistribution = floorData;
});
},
initPieChart() {
if (!this.$refs.pieChart) return;
this.pieChart = echarts.init(this.$refs.pieChart);
const option = {
backgroundColor: 'transparent',
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
right: 10,
top: 'center',
textStyle: {
color: '#a0aec0'
}
},
series: [
{
name: '设备类型',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#0a1633',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold',
color: '#62f7ff'
}
},
labelLine: {
show: false
},
data: this.deviceTypeData.length > 0 ? this.deviceTypeData : [
{ value: 0, name: '暂无数据', itemStyle: { color: '#a0aec0' } }
]
}
]
};
this.pieChart.setOption(option);
},
initHistoryChart() {
if (!this.$refs.historyChart) return;
this.historyChart = echarts.init(this.$refs.historyChart);
const option = {
backgroundColor: 'transparent',
tooltip: {
trigger: 'axis'
},
legend: {
data: ['CPU使用率', '内存使用率', '网络流量'],
textStyle: {
color: '#a0aec0'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['10-18', '10-19', '10-20', '10-21', '10-22', '10-23', '10-24'],
axisLine: {
lineStyle: {
color: '#4dabff'
}
},
axisLabel: {
color: '#a0aec0'
}
},
yAxis: {
type: 'value',
axisLine: {
lineStyle: {
color: '#4dabff'
}
},
axisLabel: {
color: '#a0aec0',
formatter: '{value}%'
},
splitLine: {
lineStyle: {
color: 'rgba(77, 171, 255, 0.2)'
}
}
},
series: [
{
name: 'CPU使用率',
type: 'line',
smooth: true,
data: [45, 52, 48, 60, 55, 58, 62],
itemStyle: {
color: '#4dabff'
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [{
offset: 0, color: 'rgba(77, 171, 255, 0.3)'
}, {
offset: 1, color: 'rgba(77, 171, 255, 0.1)'
}]
}
}
},
{
name: '内存使用率',
type: 'line',
smooth: true,
data: [62, 58, 65, 70, 68, 72, 75],
itemStyle: {
color: '#62f7ff'
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [{
offset: 0, color: 'rgba(98, 247, 255, 0.3)'
}, {
offset: 1, color: 'rgba(98, 247, 255, 0.1)'
}]
}
}
},
{
name: '网络流量',
type: 'line',
smooth: true,
data: [78, 82, 75, 90, 85, 88, 95],
itemStyle: {
color: '#ffa726'
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [{
offset: 0, color: 'rgba(255, 167, 38, 0.3)'
}, {
offset: 1, color: 'rgba(255, 167, 38, 0.1)'
}]
}
}
}
]
};
this.historyChart.setOption(option);
},
generateDataFlows() {
// 生成数据流动画元素
const flows = [];
const width = window.innerWidth;
const height = window.innerHeight;
for (let i = 0; i < 20; i++) {
const startX = Math.random() * width;
const startY = Math.random() * height;
const targetX = Math.random() * width;
const targetY = Math.random() * height;
flows.push({
x: startX,
y: startY,
tx: targetX - startX,
ty: targetY - startY,
delay: Math.random() * 5
});
}
this.dataFlows = flows;
},
handleResize() {
// 重新生成数据流
this.generateDataFlows();
// 重新调整图表大小
if (this.barChart) {
this.barChart.resize();
}
if (this.pieChart) {
this.pieChart.resize();
}
if (this.historyChart) {
this.historyChart.resize();
}
},
// 新增功能方法
showDeviceDetail(device) {
this.showDeviceModal = true;
this.loading = true;
// 获取设备详细信息
getInfo(device.id).then(response => {
this.selectedDevice = { ...this.selectedDevice, ...response.data };
this.loading = false;
}).catch(() => {
this.loading = false;
});
// 获取设备历史数据
const now = new Date();
const oneMonthAgo = new Date();
oneMonthAgo.setMonth(now.getMonth() - 1);
const historyQueryList = {
maxTime: now.toISOString().replace('T', ' ').substring(0, 19),
minTime: oneMonthAgo.toISOString().replace('T', ' ').substring(0, 19),
pageNum: 1,
pageSize: 1000
};
historyQuery({ id: device.id, ...historyQueryList }).then(response => {
this.historyQueryData = response.data;
this.$nextTick(() => {
this.initHistoryChart();
});
});
},
closeDeviceModal() {
this.showDeviceModal = false;
this.selectedDevice = {};
},
showRoomDevices(room) {
this.selectedRoom = room;
this.showRoomModal = true;
},
closeRoomModal() {
this.showRoomModal = false;
this.selectedRoom = {};
},
getRoomDevices() {
const currentFloor = this.getCurrentFloor();
return currentFloor.devices.filter(device => device.room === this.selectedRoom.name);
},
controlDevice(action) {
alert(`执行设备控制: ${action} - ${this.selectedDevice.name}`);
// 这里可以添加实际的设备控制逻辑
},
loadHistoryData() {
alert(`加载历史数据: ${this.historyStartDate}${this.historyEndDate}`);
// 这里可以添加实际的历史数据加载逻辑
},
filterByWarningLevel(level) {
if (this.warningLevelFilter === level) {
this.warningLevelFilter = 'all';
} else {
this.warningLevelFilter = level;
}
},
refreshDeviceStatus() {
// 调用monitorpageList接口获取实际设备状态数据
monitorpageList().then(response => {
console.log("%c 🍑: refreshDeviceStatus -> response ", "font-size:16px;background-color:#0dcaac;color:white;", response)
if (response.code === 200 && response.rows) {
// 直接使用接口返回的数据,添加必要的属性映射
this.deviceStatus = response.rows.map(device => ({
...device,
// 添加显示所需的计算属性
equManuName: device.equManuName || device.name || '未知设备',
cpuUtilization: device.cpuUtilization || 0,
status: (() => {
// 根据health字段的值确定设备状态
const health = parseInt(device.health);
if (health === 1 || health === 2) {
return 'normal';
} else if (health === 3) {
return 'warning';
} else if (health === 4) {
return 'error';
}
return 'normal'; // 默认状态
})(),
type: device.equTypeName || '其他设备',
location: device.location || '未知位置'
}));
console.log("%c 🇧🇴: refreshDeviceStatus -> this.deviceStatus ", "font-size:16px;background-color:#a56846;color:white;", this.deviceStatus)
} else {
console.error('获取设备状态数据失败', response);
}
}).catch(error => {
console.error('获取设备状态数据异常', error);
});
},
// 移除updateFloorDevicesBasedOnApi方法,简化为直接使用接口数据
toggleAutoSwitch() {
this.autoSwitch = !this.autoSwitch;
if (this.autoSwitch) {
this.autoSwitchInterval = setInterval(() => {
this.currentFloor = this.currentFloor % this.floors.length + 1;
}, 5000);
} else {
if (this.autoSwitchInterval) {
clearInterval(this.autoSwitchInterval);
this.autoSwitchInterval = null;
}
}
},
exportWarningData() {
alert('导出预警数据');
// 这里可以添加实际的数据导出逻辑
},
getStatusColor(status) {
switch(status) {
case 'normal': return '#4CAF50';
case 'warning': return '#FF9800';
case 'error': return '#F44336';
default: return '#a0aec0';
}
},
getStatusText(status) {
switch(status) {
case '1': return '正常';
case '0': return '故障';
default: return '未知';
}
},
simulateWebSocket() {
// 模拟WebSocket实时数据推送
setInterval(() => {
// 随机更新一个设备状态
const floorIndex = Math.floor(Math.random() * this.floors.length);
const deviceIndex = Math.floor(Math.random() * this.floors[floorIndex].devices.length);
const device = this.floors[floorIndex].devices[deviceIndex];
const oldStatus = device.status;
if (Math.random() > 0.7) {
device.status = Math.random() > 0.5 ? 'warning' : 'error';
} else if (oldStatus !== 'normal' && Math.random() > 0.8) {
device.status = 'normal';
}
}, 3000);
},
// 楼层设备展示相关方法
switchFloor(floorLevel) {
this.currentFloor = floorLevel;
},
highlightDevice(deviceId) {
this.highlightedDevice = deviceId;
},
resetDeviceHighlight() {
this.highlightedDevice = null;
},
getCurrentFloor() {
return this.floors.find(floor => floor.level === this.currentFloor) || this.floors[0];
},
getCurrentFloorDevices() {
return this.getCurrentFloor().devices;
}
}
}
</script>
<style scoped>
/* 全局重置样式,但使用深度选择器限制在组件内 */
:deep(*) {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Microsoft YaHei", sans-serif;
}
.index-box {
background-color: #0a1633;
color: #e6e6e6;
overflow: hidden;
padding: 1.5vh 1.5vw;
}
.dashboard {
width: 100vw;
height: 100vh;
padding: 1.5vh 1.5vw;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
min-width: 1200px;
min-height: 700px;
background-color: #0a1633;
color: #e6e6e6;
overflow: hidden;
}
.header {
height: 10vh;
min-height: 70px;
max-height: 100px;
background: linear-gradient(90deg, rgba(10, 26, 58, 0.8), rgba(13, 36, 82, 0.8), rgba(10, 26, 58, 0.8));
border-radius: 1vh;
margin-bottom: 1.5vh;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 2vw;
border: 1px solid #1a3a7a;
box-shadow: 0 0 15px rgba(26, 58, 122, 0.5);
position: relative;
overflow: hidden;
backdrop-filter: blur(5px);
}
.header::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
animation: headerShine 8s infinite;
}
@keyframes headerShine {
0% { left: -100%; }
100% { left: 100%; }
}
.header h1 {
font-size: clamp(1.5rem, 2.5vw, 2.5rem);
background: linear-gradient(90deg, #4dabff, #62f7ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 0 10px rgba(77, 171, 255, 0.5);
letter-spacing: 2px;
}
.time-display {
font-size: clamp(1rem, 1.5vw, 1.5rem);
color: #62f7ff;
text-shadow: 0 0 5px rgba(98, 247, 255, 0.7);
}
.main-content {
flex: 1;
display: flex;
gap: 1.5vh;
min-height: 0;
}
.left-panel, .right-panel {
width: 35%;
min-width: 280px;
display: flex;
flex-direction: column;
gap: 1.5vh;
min-height: 0;
}
.center-panel {
flex: 1;
display: flex;
flex-direction: column;
gap: 1.5vh;
min-height: 0;
min-width: 500px;
}
.panel {
background: rgba(16, 31, 63, 0.7);
border-radius: 1vh;
border: 1px solid #1a3a7a;
box-shadow: 0 0 10px rgba(26, 58, 122, 0.3);
padding: 1.5vh;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
backdrop-filter: blur(5px);
flex: 1;
}
.panel::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(90deg, #4dabff, #62f7ff);
box-shadow: 0 0 10px rgba(98, 247, 255, 0.7);
}
.panel-title {
font-size: clamp(0.9rem, 1.2vw, 1.3rem);
color: #62f7ff;
margin-bottom: 1.5vh;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.panel-title::before {
content: '';
display: inline-block;
width: 4px;
height: 18px;
background: #4dabff;
margin-right: 8px;
border-radius: 2px;
}
.panel-actions {
display: flex;
gap: 10px;
}
.panel-btn {
background: rgba(26, 58, 122, 0.5);
border: 1px solid #2a4a94;
border-radius: 4px;
padding: 4px 8px;
font-size: 0.7rem;
color: #a0aec0;
cursor: pointer;
transition: all 0.3s;
}
.panel-btn:hover {
background: rgba(77, 171, 255, 0.3);
border-color: #4dabff;
color: #62f7ff;
}
.warning-stats {
display: flex;
flex-direction: column;
gap: 15px;
}
.warning-row {
display: flex;
gap: 15px;
}
.time-item {
flex: 1;
}
.level-item {
flex: 1;
}
.floor-device-container {
max-height: 400px;
overflow-y: auto;
padding-right: 4px;
}
.floor-device-container::-webkit-scrollbar {
width: 6px;
}
.floor-device-container::-webkit-scrollbar-track {
background: rgba(26, 58, 122, 0.3);
border-radius: 3px;
}
.floor-device-container::-webkit-scrollbar-thumb {
background: rgba(125, 211, 252, 0.6);
border-radius: 3px;
}
.floor-device-container::-webkit-scrollbar-thumb:hover {
background: rgba(125, 211, 252, 0.8);
}
.floor-device-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.floor-device-card {
background: linear-gradient(135deg, rgba(26, 58, 122, 0.7), rgba(17, 39, 77, 0.7));
border: 1px solid #2a4a94;
border-radius: 8px;
padding: 16px 12px;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
backdrop-filter: blur(5px);
display: flex;
justify-content: space-between;
align-items: center;
}
.floor-device-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(90deg, #4dabff, #62f7ff);
box-shadow: 0 0 10px rgba(98, 247, 255, 0.7);
}
.floor-device-card:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(77, 171, 255, 0.3);
border-color: #4dabff;
}
.floor-level {
font-size: 16px;
color: #ffffff;
font-weight: 500;
}
.floor-count {
font-size: 16px;
color: #62f7ff;
font-weight: bold;
text-shadow: 0 0 10px rgba(98, 247, 255, 0.5);
}
.stat-item {
background: rgba(26, 58, 122, 0.3);
border-radius: 8px;
padding: 15px;
border: 1px solid #2a4a94;
display: flex;
flex-direction: column;
align-items: center;
transition: all 0.3s;
cursor: pointer;
}
.stat-item:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(26, 58, 122, 0.5);
border-color: #4dabff;
}
.stat-value {
font-size: clamp(1.5rem, 2.5vw, 2.2rem);
font-weight: bold;
margin: 0.5vh 0;
}
.stat-annual {
color: #62f7ff;
}
.stat-monthly {
color: #4dabff;
}
.stat-level1 {
color: #ff6b6b;
}
.stat-level2 {
color: #ffa726;
}
.stat-level3 {
color: #4dabff;
}
.stat-label {
font-size: clamp(0.7rem, 0.9vw, 0.9rem);
color: #a0aec0;
}
.device-list {
height: 30vh;
min-height: 200px;
overflow-y: auto;
flex-shrink: 0;
}
.device-list::-webkit-scrollbar {
width: 5px;
}
.device-list::-webkit-scrollbar-thumb {
background: #4dabff;
border-radius: 10px;
}
.device-item {
display: flex;
align-items: center;
padding: 1.2vh 1vh;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s;
cursor: pointer;
}
.device-item:hover {
background: rgba(26, 58, 122, 0.3);
transform: translateX(5px);
}
.device-status {
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 10px;
}
.warning-scrollbar {
height: 100%;
min-height: 100px;
flex-shrink: 0;
}
.warning-scrollbar .el-scrollbar__wrap {
padding-right: 10px;
}
.warning-scrollbar .el-scrollbar__thumb {
background: #4dabff;
border-radius: 10px;
}
.warning-item {
padding: 15px;
margin-bottom: 10px;
background: linear-gradient(135deg, rgba(26, 58, 122, 0.2), rgba(17, 39, 77, 0.2));
border: 1px solid rgba(42, 74, 148, 0.5);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.warning-item:hover {
background: linear-gradient(135deg, rgba(26, 58, 122, 0.4), rgba(17, 39, 77, 0.4));
border-color: #4dabff;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(77, 171, 255, 0.3);
}
.warning-header {
margin-bottom: 8px;
}
.warning-type {
font-weight: bold;
color: #ff6b6b;
display: inline-flex;
align-items: center;
}
.warning-type::before {
content: '•';
margin-right: 6px;
font-size: 18px;
}
.warning-type.level-error {
color: #ff6b6b;
}
.warning-type.level-warning {
color: #ffa726;
}
.warning-time,
.warning-position,
.warning-detail {
font-size: 13px;
color: rgba(255, 255, 255, 0.85);
margin-bottom: 5px;
line-height: 1.5;
}
.status-normal {
background: #4CAF50;
box-shadow: 0 0 5px #4CAF50;
}
.status-warning {
background: #FF9800;
box-shadow: 0 0 5px #FF9800;
}
.status-error {
background: #F44336;
box-shadow: 0 0 5px #F44336;
animation: blink 1.5s infinite;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0.3; }
}
.device-name {
flex: 1;
font-size: clamp(0.8rem, 1vw, 1rem);
}
.device-value {
color: #62f7ff;
font-weight: bold;
font-size: clamp(0.8rem, 1vw, 1rem);
}
/* 楼宇容器样式 */
.building-container {
flex: 1;
position: relative;
display: flex;
min-height: 40vh;
}
.floor-plans-container {
width: 70%;
height: 100%;
background: rgba(10, 22, 51, 0.8);
border-radius: 8px;
position: relative;
overflow: hidden;
}
.floor-plan {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: opacity 0.5s ease;
opacity: 0;
}
.floor-plan.active {
opacity: 1;
}
.floor-title {
font-size: 1.5rem;
color: #62f7ff;
margin-bottom: 20px;
text-align: center;
}
.floor-layout {
width: 90%;
height: 80%;
position: relative;
background: rgba(16, 31, 63, 0.5);
border-radius: 8px;
border: 1px solid #2a4a94;
overflow: hidden;
}
.room {
position: absolute;
background: rgba(26, 58, 122, 0.3);
border: 1px solid #2a4a94;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: all 0.3s;
overflow: hidden;
cursor: pointer;
}
.room:hover {
background: rgba(26, 58, 122, 0.5);
border-color: #4dabff;
}
.room-label {
font-size: 0.7rem;
color: #a0aec0;
text-align: center;
padding: 2px;
}
.device-marker {
position: absolute;
width: 16px;
height: 16px;
border-radius: 50%;
transform: translate(-50%, -50%);
box-shadow: 0 0 6px currentColor;
display: flex;
align-items: center;
justify-content: center;
font-size: 8px;
color: white;
z-index: 10;
transition: all 0.3s;
cursor: pointer;
}
.device-marker:hover {
transform: translate(-50%, -50%) scale(1.4);
z-index: 20;
}
.device-normal {
background: #4CAF50;
color: #4CAF50;
}
.device-warning {
background: #FF9800;
color: #FF9800;
}
.device-error {
background: #F44336;
color: #F44336;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(244, 67, 54, 0.7); }
70% { box-shadow: 0 0 0 8px rgba(244, 67, 54, 0); }
100% { box-shadow: 0 0 0 0 rgba(244, 67, 54, 0); }
}
.floor-info {
width: 30%;
padding-left: 15px;
display: flex;
flex-direction: column;
}
.floor-selector {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 20px;
}
.floor-btn {
padding: 10px 15px;
background: rgba(26, 58, 122, 0.5);
border: 1px solid #2a4a94;
border-radius: 6px;
cursor: pointer;
text-align: center;
transition: all 0.3s;
color: #a0aec0;
}
.floor-btn:hover {
background: rgba(26, 58, 122, 0.8);
border-color: #4dabff;
color: #62f7ff;
}
.floor-btn.active {
background: rgba(77, 171, 255, 0.3);
border-color: #4dabff;
color: #62f7ff;
box-shadow: 0 0 10px rgba(77, 171, 255, 0.5);
}
.floor-details h3 {
color: #62f7ff;
margin-bottom: 15px;
font-size: 1.1rem;
}
.floor-stats {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 20px;
}
.floor-stat {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.stat-label {
color: #a0aec0;
}
.stat-value {
color: #4dabff;
font-weight: bold;
}
.stat-value.online {
color: #4CAF50;
}
.stat-value.warning {
color: #FF9800;
}
.device-list-mini {
max-height: 200px;
overflow-y: auto;
}
.device-item-mini {
display: flex;
align-items: center;
padding: 6px 8px;
margin-bottom: 5px;
background: rgba(26, 58, 122, 0.3);
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
font-size: 0.8rem;
}
.device-item-mini:hover {
background: rgba(77, 171, 255, 0.2);
transform: translateX(3px);
}
.device-dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 8px;
}
.device-name-mini {
flex: 1;
color: #e6e6e6;
}
.device-position {
color: #a0aec0;
font-size: 0.7rem;
}
.device-stats-container {
display: flex;
gap: 15px;
margin-bottom: 15px;
}
.device-stat-item {
flex: 1;
background: linear-gradient(135deg, rgba(26, 58, 122, 0.7), rgba(17, 39, 77, 0.7));
border: 1px solid #2a4a94;
border-radius: 8px;
padding: 15px 10px;
text-align: center;
}
.device-stat-value {
font-size: 24px;
font-weight: bold;
color: #62f7ff;
margin-bottom: 5px;
}
.device-stat-label {
font-size: 14px;
color: #a0aec0;
}
.chart-container {
height: 30vh;
min-height: 200px;
flex-shrink: 0;
}
.footer {
height: 5vh;
min-height: 30px;
max-height: 50px;
display: flex;
justify-content: space-between;
align-items: center;
color: #a0aec0;
font-size: clamp(0.7rem, 0.9vw, 0.9rem);
margin-top: 1vh;
flex-shrink: 0;
}
.update-time {
color: #62f7ff;
}
.data-flow {
position: absolute;
width: 4px;
height: 4px;
border-radius: 50%;
background: #62f7ff;
box-shadow: 0 0 8px #62f7ff;
animation: flow 3s linear infinite;
z-index: 1;
}
@keyframes flow {
0% { transform: translate(0, 0); opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { transform: translate(var(--tx), var(--ty)); opacity: 0; }
}
/* 模态框样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(5px);
}
.modal-content {
background: rgba(16, 31, 63, 0.95);
border-radius: 1vh;
border: 1px solid #1a3a7a;
box-shadow: 0 0 30px rgba(26, 58, 122, 0.8);
width: 80%;
max-width: 800px;
max-height: 80%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 1.5vh 2vw;
border-bottom: 1px solid #1a3a7a;
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(10, 26, 58, 0.8);
}
.modal-title {
font-size: 1.3rem;
color: #62f7ff;
}
.modal-close {
background: none;
border: none;
color: #a0aec0;
font-size: 1.5rem;
cursor: pointer;
transition: all 0.3s;
}
.modal-close:hover {
color: #62f7ff;
}
.modal-body {
padding: 2vh 2vw;
flex: 1;
overflow-y: auto;
}
.device-detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.detail-section {
margin-bottom: 20px;
}
.detail-section h3 {
color: #62f7ff;
margin-bottom: 10px;
font-size: 1.1rem;
border-bottom: 1px solid #1a3a7a;
padding-bottom: 5px;
}
.detail-item {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.detail-label {
color: #a0aec0;
}
.detail-value {
color: #e6e6e6;
font-weight: bold;
}
.control-buttons {
display: flex;
gap: 10px;
margin-top: 15px;
}
.control-btn {
padding: 8px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
font-weight: bold;
}
.btn-restart {
background: #ffa726;
color: #fff;
}
.btn-restart:hover {
background: #ff9800;
}
.btn-shutdown {
background: #f44336;
color: #fff;
}
.btn-shutdown:hover {
background: #d32f2f;
}
.btn-reset {
background: #4dabff;
color: #fff;
}
.btn-reset:hover {
background: #2196f3;
}
.history-chart {
height: 200px;
margin-top: 15px;
}
.search-controls {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.search-input {
flex: 1;
background: rgba(26, 58, 122, 0.3);
border: 1px solid #2a4a94;
border-radius: 4px;
padding: 8px 12px;
color: #e6e6e6;
}
.search-input:focus {
outline: none;
border-color: #4dabff;
}
.date-input {
background: rgba(26, 58, 122, 0.3);
border: 1px solid #2a4a94;
border-radius: 4px;
padding: 8px 12px;
color: #e6e6e6;
}
/* 响应式设计 - 中等屏幕 */
@media screen and (max-width: 1400px) {
.left-panel, .right-panel {
width: 28%;
}
.center-panel {
width: 44%;
}
}
/* 响应式设计 - 小屏幕 */
@media screen and (max-width: 1200px) {
.dashboard {
min-width: 1000px;
}
.left-panel, .right-panel {
min-width: 250px;
}
.center-panel {
min-width: 450px;
}
}
/* 响应式设计 - 超小屏幕 */
@media screen and (max-width: 1000px) {
.dashboard {
min-width: 800px;
padding: 1vh 1vw;
}
.main-content {
flex-direction: column;
gap: 1vh;
}
.left-panel, .right-panel, .center-panel {
width: 100%;
min-width: auto;
}
.left-panel, .right-panel {
flex-direction: row;
}
.panel {
flex: 1;
}
}
/* 响应式设计 - 移动端 */
@media screen and (max-width: 768px) {
.dashboard {
min-width: auto;
min-height: auto;
padding: 0.5vh 0.5vw;
}
.header {
height: 8vh;
padding: 0 1vw;
margin-bottom: 1vh;
}
.main-content {
gap: 1vh;
}
.left-panel, .right-panel {
flex-direction: column;
}
.footer {
height: 4vh;
font-size: 0.7rem;
}
}
</style>