|
|
@@ -0,0 +1,2885 @@
|
|
|
+<template>
|
|
|
+ <div class="device-monitor-container">
|
|
|
+ <!-- 控制面板 -->
|
|
|
+ <div class="control-panel">
|
|
|
+ <div class="control-left">
|
|
|
+ <div class="form-group">
|
|
|
+ <label>农场选择</label>
|
|
|
+ <select v-model="selectedFarm">
|
|
|
+ <option value="all">全部农场</option>
|
|
|
+ <option value="farm1">东湖智慧农场</option>
|
|
|
+ <option value="farm2">西湖有机农场</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div class="form-group">
|
|
|
+ <label>地块选择</label>
|
|
|
+ <select v-model="selectedArea">
|
|
|
+ <option value="all">全部地块</option>
|
|
|
+ <option value="area1">1号地块</option>
|
|
|
+ <option value="area2">2号地块</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div class="form-group">
|
|
|
+ <label>时间范围</label>
|
|
|
+ <select v-model="timeRange">
|
|
|
+ <option value="realtime">实时数据</option>
|
|
|
+ <option value="1h">近1小时</option>
|
|
|
+ <option value="24h">近24小时</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="control-right">
|
|
|
+ <button class="refresh-btn" @click="refreshData">
|
|
|
+ <i class="icon-refresh"></i>
|
|
|
+ 刷新数据
|
|
|
+ </button>
|
|
|
+ <div class="auto-refresh">
|
|
|
+ <span>自动刷新:</span>
|
|
|
+ <select v-model="autoRefresh" class="auto-refresh-select">
|
|
|
+ <option value="15">15秒</option>
|
|
|
+ <option value="30">30秒</option>
|
|
|
+ <option value="60">60秒</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div class="last-update">
|
|
|
+ 最后更新:<span>{{ lastUpdateTime }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 设备统计 -->
|
|
|
+ <div class="stats-container">
|
|
|
+ <!-- 综合统计卡片 -->
|
|
|
+ <div class="overview-stat-card">
|
|
|
+ <div class="overview-stats">
|
|
|
+ <div class="overview-item">
|
|
|
+ <div class="stat-number online">42</div>
|
|
|
+ <div class="stat-label">在线设备</div>
|
|
|
+ </div>
|
|
|
+ <div class="overview-item">
|
|
|
+ <div class="stat-number offline">3</div>
|
|
|
+ <div class="stat-label">离线设备</div>
|
|
|
+ </div>
|
|
|
+ <div class="overview-item">
|
|
|
+ <div class="stat-number alert">5</div>
|
|
|
+ <div class="stat-label">告警设备</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 分类统计卡片 -->
|
|
|
+ <div class="category-stats">
|
|
|
+ <div class="stat-card">
|
|
|
+ <div class="stat-number camera">12</div>
|
|
|
+ <div class="stat-label">摄像头</div>
|
|
|
+ <div class="stat-detail">11在线 / 1离线</div>
|
|
|
+ </div>
|
|
|
+ <div class="stat-card">
|
|
|
+ <div class="stat-number sensor">15</div>
|
|
|
+ <div class="stat-label">传感器</div>
|
|
|
+ <div class="stat-detail">14在线 / 1告警</div>
|
|
|
+ </div>
|
|
|
+ <div class="stat-card">
|
|
|
+ <div class="stat-number weather">8</div>
|
|
|
+ <div class="stat-label">气象设备</div>
|
|
|
+ <div class="stat-detail">7在线 / 1离线</div>
|
|
|
+ </div>
|
|
|
+ <div class="stat-card">
|
|
|
+ <div class="stat-number control">10</div>
|
|
|
+ <div class="stat-label">控制设备</div>
|
|
|
+ <div class="stat-detail">10在线</div>
|
|
|
+ </div>
|
|
|
+ <div class="stat-card">
|
|
|
+ <div class="stat-number alert">7</div>
|
|
|
+ <div class="stat-label">今日告警</div>
|
|
|
+ <div class="stat-detail"><span class="processed">5已处理</span></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 主要内容区域 -->
|
|
|
+ <div class="main-content">
|
|
|
+ <!-- 视频监控 -->
|
|
|
+ <div class="content-section">
|
|
|
+ <div class="video-header">
|
|
|
+ <div class="video-header-left">
|
|
|
+ <h2 class="section-title">视频监控</h2>
|
|
|
+ <div class="video-count">
|
|
|
+ 共 {{ videoList.length }} 个摄像头
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="video-header-right">
|
|
|
+ <div class="video-nav-controls">
|
|
|
+ <button class="video-nav-btn" @click="prevPage" :disabled="currentPage === 1" title="上一页">
|
|
|
+ <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
|
|
|
+ <path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ <div class="page-indicator">
|
|
|
+ <span>{{ getPageRange() }} / {{ videoList.length }}</span>
|
|
|
+ </div>
|
|
|
+ <button class="video-nav-btn" @click="nextPage" :disabled="currentPage === totalPages" title="下一页">
|
|
|
+ <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
|
|
|
+ <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <button class="video-nav-btn" @click="toggleFullscreen" title="全屏显示">
|
|
|
+ <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
|
|
|
+ <path fill-rule="evenodd" d="M3 4a1 1 0 011-1h4a1 1 0 010 2H6.414l2.293 2.293a1 1 0 11-1.414 1.414L5 6.414V8a1 1 0 01-2 0V4zm9 1a1 1 0 010-2h4a1 1 0 011 1v4a1 1 0 01-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L13.586 5H12zm-9 7a1 1 0 012 0v1.586l2.293-2.293a1 1 0 111.414 1.414L6.414 15H8a1 1 0 010 2H4a1 1 0 01-1-1v-4zm13-1a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 010-2h1.586l-2.293-2.293a1 1 0 111.414-1.414L15 13.586V12a1 1 0 011-1z" clip-rule="evenodd" />
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="video-container">
|
|
|
+ <div class="video-grid">
|
|
|
+ <div v-for="video in paginatedVideos" :key="video.id" class="video-card">
|
|
|
+ <button class="camera-fullscreen-btn" @click="openVideoFullscreen(video)" title="全屏查看">
|
|
|
+ <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
|
|
|
+ <path fill-rule="evenodd" d="M3 4a1 1 0 011-1h4a1 1 0 010 2H6.414l2.293 2.293a1 1 0 11-1.414 1.414L5 6.414V8a1 1 0 01-2 0V4zm9 1a1 1 0 010-2h4a1 1 0 011 1v4a1 1 0 01-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L13.586 5H12zm-9 7a1 1 0 012 0v1.586l2.293-2.293a1 1 0 111.414 1.414L6.414 15H8a1 1 0 010 2H4a1 1 0 01-1-1v-4zm13-1a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 010-2h1.586l-2.293-2.293a1 1 0 111.414-1.414L15 13.586V12a1 1 0 011-1z" clip-rule="evenodd" />
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ <div class="video-preview">
|
|
|
+ <div class="camera-placeholder centered-container">
|
|
|
+ <svg class="camera-icon centered-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
+ <path d="M23 7l-7 5 7 5V7z"/>
|
|
|
+ <rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <div class="video-info-overlay">
|
|
|
+ <div class="video-details">
|
|
|
+ <div class="video-name">{{ video.name }}</div>
|
|
|
+ <div class="video-location">{{ video.location }}</div>
|
|
|
+ </div>
|
|
|
+ <span class="video-status" :class="video.status">
|
|
|
+ {{ video.status === 'online' ? '在线' : '离线' }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 设备监控 -->
|
|
|
+ <div class="content-section">
|
|
|
+ <!-- 顶部控制栏 -->
|
|
|
+ <div class="device-monitor-header">
|
|
|
+ <div class="device-monitor-title-section">
|
|
|
+ <h2 class="section-title">设备监控</h2>
|
|
|
+ <div class="filter-buttons">
|
|
|
+ <button class="filter-btn" :class="{ active: deviceFilter === 'all' }" @click="setFilter('all')">
|
|
|
+ 全部设备
|
|
|
+ <span class="filter-count">({{ deviceList.length }})</span>
|
|
|
+ </button>
|
|
|
+ <button class="filter-btn" :class="{ active: deviceFilter === 'warning' }" @click="setFilter('warning')">
|
|
|
+ <span class="status-dot warning"></span>
|
|
|
+ 告警设备
|
|
|
+ <span class="filter-count">({{ getDeviceCountByStatus('warning') }})</span>
|
|
|
+ </button>
|
|
|
+ <button class="filter-btn" :class="{ active: deviceFilter === 'offline' }" @click="setFilter('offline')">
|
|
|
+ <span class="status-dot offline"></span>
|
|
|
+ 离线设备
|
|
|
+ <span class="filter-count">({{ getDeviceCountByStatus('offline') }})</span>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div class="filter-divider"></div>
|
|
|
+ <div class="type-filters">
|
|
|
+ <button class="filter-btn" :class="{ active: deviceTypeFilter === 'soil' }" @click="setTypeFilter('soil')">土壤监测</button>
|
|
|
+ <button class="filter-btn" :class="{ active: deviceTypeFilter === 'water' }" @click="setTypeFilter('water')">水质监测</button>
|
|
|
+ <button class="filter-btn" :class="{ active: deviceTypeFilter === 'weather' }" @click="setTypeFilter('weather')">气象监测</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="device-monitor-controls">
|
|
|
+ <select class="sort-select" v-model="sortBy">
|
|
|
+ <option value="alert">告警优先</option>
|
|
|
+ <option value="time">更新时间</option>
|
|
|
+ <option value="name">设备名称</option>
|
|
|
+ </select>
|
|
|
+ <div class="device-pagination">
|
|
|
+ <button class="page-btn" @click="prevDevicePage" :disabled="deviceCurrentPage === 1">
|
|
|
+ <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
|
|
|
+ <path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ <span class="page-indicator">{{ getDevicePageRange() }} / {{ filteredDevices.length }}</span>
|
|
|
+ <button class="page-btn" @click="nextDevicePage" :disabled="deviceCurrentPage === deviceTotalPages">
|
|
|
+ <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
|
|
|
+ <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 设备卡片网格 -->
|
|
|
+ <div class="device-grid">
|
|
|
+ <div v-for="device in paginatedDevices" :key="device.id" class="device-card" :class="device.status">
|
|
|
+ <!-- 右上角数据按钮 -->
|
|
|
+ <button class="data-btn" @click="showDeviceHistory(device)" title="查看数据">
|
|
|
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
+ <path d="M12 20v-6M6 20V10M18 20V4"/>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <div class="device-header">
|
|
|
+ <div class="device-title-row">
|
|
|
+ <div class="device-name">{{ device.name }}</div>
|
|
|
+ <div class="device-status" :class="device.status">{{ device.statusText }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="device-location">{{ device.location }}</div>
|
|
|
+ <div class="device-metrics">
|
|
|
+ <div class="metric">
|
|
|
+ <div class="metric-label">{{ device.metrics[0].name }}</div>
|
|
|
+ <div class="metric-value" :class="{ 'metric-warning': device.status === 'warning' && device.metrics[0].isAlert }">
|
|
|
+ {{ device.metrics[0].value }}{{ device.metrics[0].unit }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="metric">
|
|
|
+ <div class="metric-label">{{ device.metrics[1].name }}</div>
|
|
|
+ <div class="metric-value" :class="{ 'metric-warning': device.status === 'warning' && device.metrics[1].isAlert }">
|
|
|
+ {{ device.metrics[1].value }}{{ device.metrics[1].unit }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="device-footer">
|
|
|
+ <div class="last-update" :class="{ 'text-warning': device.status === 'warning', 'text-offline': device.status === 'offline' }">
|
|
|
+ {{ device.lastUpdate }}
|
|
|
+ </div>
|
|
|
+ <button class="detail-btn" :class="device.status">
|
|
|
+ {{ device.status === 'warning' ? '处理告警' : '查看详情' }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 单个摄像头全屏模式 -->
|
|
|
+ <div v-if="singleCameraFullscreen.show" class="single-camera-fullscreen">
|
|
|
+ <div class="video-area">
|
|
|
+ <div class="fullscreen-video-container">
|
|
|
+ <!-- 左上角摄像头名称 -->
|
|
|
+ <div class="camera-title-overlay" v-if="singleCameraFullscreen.camera">
|
|
|
+ <h3 class="camera-title-text">{{ singleCameraFullscreen.camera.name }}</h3>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 右上角关闭按钮 -->
|
|
|
+ <div class="close-button-overlay">
|
|
|
+ <button class="video-nav-btn" @click="exitSingleFullscreen" title="退出全屏">
|
|
|
+ <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
|
|
|
+ <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 视频画面区域 -->
|
|
|
+ <div class="fullscreen-video-preview">
|
|
|
+ <svg class="fullscreen-camera-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
+ <path d="M23 7l-7 5 7 5V7z"/>
|
|
|
+ <rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="control-panel">
|
|
|
+ <div class="control-sections">
|
|
|
+ <!-- 云台控制 -->
|
|
|
+ <div class="control-section">
|
|
|
+ <h4 class="control-title">云台控制</h4>
|
|
|
+ <div class="ptz-controls">
|
|
|
+ <button class="ptz-btn" title="左上">↖</button>
|
|
|
+ <button class="ptz-btn" title="向上">↑</button>
|
|
|
+ <button class="ptz-btn" title="右上">↗</button>
|
|
|
+ <button class="ptz-btn" title="向左">←</button>
|
|
|
+ <button class="ptz-btn" title="停止">●</button>
|
|
|
+ <button class="ptz-btn" title="向右">→</button>
|
|
|
+ <button class="ptz-btn" title="左下">↙</button>
|
|
|
+ <button class="ptz-btn" title="向下">↓</button>
|
|
|
+ <button class="ptz-btn" title="右下">↘</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 变倍控制 -->
|
|
|
+ <div class="control-section">
|
|
|
+ <h4 class="control-title">变倍控制</h4>
|
|
|
+ <div class="zoom-controls">
|
|
|
+ <button class="ptz-btn zoom-btn" title="放大">
|
|
|
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v6m3-3H7"/>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ <button class="ptz-btn zoom-btn" title="缩小">
|
|
|
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM7 10h6"/>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 预置位 -->
|
|
|
+ <div class="control-section">
|
|
|
+ <h4 class="control-title">预置位</h4>
|
|
|
+ <div class="preset-grid">
|
|
|
+ <button v-for="n in 6" :key="n" class="preset-btn">位置{{ n }}</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 回放控制 -->
|
|
|
+ <div class="control-section">
|
|
|
+ <h4 class="control-title">回放控制</h4>
|
|
|
+ <div class="playback-controls">
|
|
|
+ <button class="ptz-btn playback-btn" title="开始回放">
|
|
|
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/>
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ <button class="ptz-btn playback-btn" title="暂停回放">
|
|
|
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 其他控制 -->
|
|
|
+ <div class="control-section">
|
|
|
+ <h4 class="control-title">其他控制</h4>
|
|
|
+ <div class="other-controls">
|
|
|
+ <button class="ptz-btn other-btn" title="截图">
|
|
|
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"/>
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ <button class="ptz-btn other-btn" title="录制">
|
|
|
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
+ <circle cx="12" cy="12" r="10" stroke-width="2"/>
|
|
|
+ <circle cx="12" cy="12" r="3" fill="currentColor"/>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 网格全屏模式 -->
|
|
|
+ <div v-if="gridFullscreen.show" class="grid-fullscreen-mode">
|
|
|
+ <div class="fullscreen-header">
|
|
|
+ <div class="fullscreen-title">
|
|
|
+ <h2 class="fullscreen-title-text">视频监控</h2>
|
|
|
+ <div class="fullscreen-subtitle">共 {{ videoList.length }} 个摄像头</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 分页控制 -->
|
|
|
+ <div class="fullscreen-pagination" v-if="gridTotalPages > 1">
|
|
|
+ <button class="fullscreen-nav-btn" @click="gridPrevPage" :disabled="gridFullscreen.currentPage === 1" title="上一页">
|
|
|
+ <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
|
|
|
+ <path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ <div class="fullscreen-page-indicator">
|
|
|
+ <span>{{ gridPageRange }} / {{ videoList.length }}</span>
|
|
|
+ <div class="fullscreen-page-dots">
|
|
|
+ <span
|
|
|
+ v-for="page in gridTotalPages"
|
|
|
+ :key="page"
|
|
|
+ class="page-dot"
|
|
|
+ :class="{ active: page === gridFullscreen.currentPage }"
|
|
|
+ @click="gridFullscreen.currentPage = page"
|
|
|
+ ></span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <button class="fullscreen-nav-btn" @click="gridNextPage" :disabled="gridFullscreen.currentPage === gridTotalPages" title="下一页">
|
|
|
+ <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
|
|
|
+ <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="fullscreen-controls">
|
|
|
+ <!-- 键盘快捷键提示 -->
|
|
|
+ <div class="keyboard-hints" v-if="gridTotalPages > 1">
|
|
|
+ <div class="hint-item">
|
|
|
+ <kbd>←</kbd><kbd>→</kbd> 翻页
|
|
|
+ </div>
|
|
|
+ <div class="hint-item">
|
|
|
+ <kbd>1-9</kbd> 跳转
|
|
|
+ </div>
|
|
|
+ <div class="hint-item">
|
|
|
+ <kbd>ESC</kbd> 退出
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <button class="fullscreen-close-btn" @click="exitGridFullscreen" title="退出全屏 (ESC)">
|
|
|
+ <svg class="w-6 h-6" viewBox="0 0 20 20" fill="currentColor">
|
|
|
+ <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="fullscreen-video-grid">
|
|
|
+ <div v-for="video in gridPaginatedVideos" :key="video.id" class="fullscreen-video-card">
|
|
|
+ <button class="fullscreen-camera-btn" @click="openVideoFullscreen(video)" title="单独查看">
|
|
|
+ <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
|
|
|
+ <path fill-rule="evenodd" d="M3 4a1 1 0 011-1h4a1 1 0 010 2H6.414l2.293 2.293a1 1 0 11-1.414 1.414L5 6.414V8a1 1 0 01-2 0V4zm9 1a1 1 0 010-2h4a1 1 0 011 1v4a1 1 0 01-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L13.586 5H12zm-9 7a1 1 0 012 0v1.586l2.293-2.293a1 1 0 111.414 1.414L6.414 15H8a1 1 0 010 2H4a1 1 0 01-1-1v-4zm13-1a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 010-2h1.586l-2.293-2.293a1 1 0 111.414-1.414L15 13.586V12a1 1 0 011-1z" clip-rule="evenodd" />
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ <div class="fullscreen-video-preview">
|
|
|
+ <div class="fullscreen-camera-placeholder">
|
|
|
+ <svg class="fullscreen-camera-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
+ <path d="M23 7l-7 5 7 5V7z"/>
|
|
|
+ <rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <div class="fullscreen-video-info">
|
|
|
+ <div class="fullscreen-video-details">
|
|
|
+ <div class="fullscreen-video-name">{{ video.name }}</div>
|
|
|
+ <div class="fullscreen-video-location">{{ video.location }}</div>
|
|
|
+ </div>
|
|
|
+ <span class="fullscreen-video-status" :class="video.status">
|
|
|
+ {{ video.status === 'online' ? '在线' : '离线' }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 设备历史数据模态框 -->
|
|
|
+ <div v-if="historyModal.show" class="history-modal-backdrop" @click="closeHistoryModal">
|
|
|
+ <div class="history-modal-content" @click.stop>
|
|
|
+ <!-- 模态框头部 -->
|
|
|
+ <div class="history-modal-header">
|
|
|
+ <div class="history-modal-title-section">
|
|
|
+ <h3 class="history-modal-title">{{ historyModal.device.name }} - 历史数据</h3>
|
|
|
+ <span class="device-status" :class="historyModal.device.status">{{ historyModal.device.statusText }}</span>
|
|
|
+ </div>
|
|
|
+ <button class="history-modal-close" @click="closeHistoryModal">
|
|
|
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 图表控制区域 -->
|
|
|
+ <div class="history-modal-controls">
|
|
|
+ <div class="time-range-buttons">
|
|
|
+ <button
|
|
|
+ v-for="range in timeRanges"
|
|
|
+ :key="range.value"
|
|
|
+ class="time-range-btn"
|
|
|
+ :class="{ active: historyModal.timeRange === range.value }"
|
|
|
+ @click="setTimeRange(range.value)"
|
|
|
+ >
|
|
|
+ {{ range.label }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div class="indicator-buttons">
|
|
|
+ <button
|
|
|
+ v-for="indicator in getDeviceIndicators()"
|
|
|
+ :key="indicator.value"
|
|
|
+ class="indicator-btn"
|
|
|
+ :class="{ active: historyModal.activeIndicator === indicator.value }"
|
|
|
+ @click="setActiveIndicator(indicator.value)"
|
|
|
+ >
|
|
|
+ {{ indicator.label }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 图表区域 -->
|
|
|
+ <div class="history-modal-body">
|
|
|
+ <div ref="chartContainer" class="chart-container"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+export default {
|
|
|
+ name: 'DeviceMonitor',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ selectedFarm: 'all',
|
|
|
+ selectedArea: 'all',
|
|
|
+ timeRange: 'realtime',
|
|
|
+ autoRefresh: '30',
|
|
|
+ lastUpdateTime: '刚刚',
|
|
|
+ currentPage: 1,
|
|
|
+ videosPerPage: 3,
|
|
|
+
|
|
|
+ // 设备监控相关
|
|
|
+ deviceFilter: 'all',
|
|
|
+ deviceTypeFilter: 'all',
|
|
|
+ sortBy: 'alert',
|
|
|
+ deviceCurrentPage: 1,
|
|
|
+ devicePerPage: 8,
|
|
|
+
|
|
|
+ // 单个摄像头全屏模式
|
|
|
+ singleCameraFullscreen: {
|
|
|
+ show: false,
|
|
|
+ camera: null
|
|
|
+ },
|
|
|
+
|
|
|
+ // 网格全屏模式
|
|
|
+ gridFullscreen: {
|
|
|
+ show: false,
|
|
|
+ currentPage: 1,
|
|
|
+ videosPerPage: 6
|
|
|
+ },
|
|
|
+
|
|
|
+ // 历史数据模态框
|
|
|
+ historyModal: {
|
|
|
+ show: false,
|
|
|
+ device: null,
|
|
|
+ timeRange: '24h',
|
|
|
+ activeIndicator: 'all'
|
|
|
+ },
|
|
|
+
|
|
|
+ // 时间范围选项
|
|
|
+ timeRanges: [
|
|
|
+ { label: '近24小时', value: '24h' },
|
|
|
+ { label: '近7天', value: '7d' },
|
|
|
+ { label: '近30天', value: '30d' }
|
|
|
+ ],
|
|
|
+
|
|
|
+ // 图表实例
|
|
|
+ chartInstance: null,
|
|
|
+
|
|
|
+ // 原始样式保存
|
|
|
+ originalBodyBg: '',
|
|
|
+ originalBodyColor: '',
|
|
|
+ originalAppBg: '',
|
|
|
+
|
|
|
+ videoList: [
|
|
|
+ { id: 1, name: '东区1号摄像头', location: '东区1号地块', status: 'online' },
|
|
|
+ { id: 2, name: '东区2号摄像头', location: '东区2号地块', status: 'online' },
|
|
|
+ { id: 3, name: '东区3号摄像头', location: '东区3号地块', status: 'offline' },
|
|
|
+ { id: 4, name: '西区1号摄像头', location: '西区1号地块', status: 'online' },
|
|
|
+ { id: 5, name: '西区2号摄像头', location: '西区2号地块', status: 'online' },
|
|
|
+ { id: 6, name: '西区3号摄像头', location: '西区3号地块', status: 'online' },
|
|
|
+ { id: 7, name: '南区1号摄像头', location: '南区1号地块', status: 'online' },
|
|
|
+ { id: 8, name: '南区2号摄像头', location: '南区2号地块', status: 'offline' },
|
|
|
+ { id: 9, name: '南区3号摄像头', location: '南区3号地块', status: 'online' },
|
|
|
+ { id: 10, name: '北区1号摄像头', location: '北区1号地块', status: 'online' },
|
|
|
+ { id: 11, name: '北区2号摄像头', location: '北区2号地块', status: 'online' },
|
|
|
+ { id: 12, name: '北区3号摄像头', location: '北区3号地块', status: 'online' }
|
|
|
+ ],
|
|
|
+
|
|
|
+ deviceList: [
|
|
|
+ {
|
|
|
+ id: 1,
|
|
|
+ name: '土壤监测器 #1',
|
|
|
+ location: '东区1号地块',
|
|
|
+ status: 'online',
|
|
|
+ statusText: '在线',
|
|
|
+ lastUpdate: '2分钟前更新',
|
|
|
+ type: 'soil',
|
|
|
+ metrics: [
|
|
|
+ { name: '土壤湿度', value: '32.5', unit: '%', isAlert: false },
|
|
|
+ { name: '土壤温度', value: '24.2', unit: '℃', isAlert: false }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 2,
|
|
|
+ name: '水质监测器 #2',
|
|
|
+ location: '西区1号地块',
|
|
|
+ status: 'warning',
|
|
|
+ statusText: '告警',
|
|
|
+ lastUpdate: 'pH值超标',
|
|
|
+ type: 'water',
|
|
|
+ metrics: [
|
|
|
+ { name: 'pH值', value: '8.5', unit: '', isAlert: true },
|
|
|
+ { name: '溶解氧', value: '6.2', unit: 'mg/L', isAlert: false }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 3,
|
|
|
+ name: '气象监测器 #1',
|
|
|
+ location: '东区气象站',
|
|
|
+ status: 'online',
|
|
|
+ statusText: '在线',
|
|
|
+ lastUpdate: '1分钟前更新',
|
|
|
+ type: 'weather',
|
|
|
+ metrics: [
|
|
|
+ { name: '温度', value: '26.5', unit: '℃', isAlert: false },
|
|
|
+ { name: '湿度', value: '68', unit: '%', isAlert: false }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 4,
|
|
|
+ name: '土壤监测器 #4',
|
|
|
+ location: '西区2号地块',
|
|
|
+ status: 'offline',
|
|
|
+ statusText: '离线',
|
|
|
+ lastUpdate: '设备离线 > 24h',
|
|
|
+ type: 'soil',
|
|
|
+ metrics: [
|
|
|
+ { name: '土壤湿度', value: '--', unit: '%', isAlert: false },
|
|
|
+ { name: '土壤温度', value: '--', unit: '℃', isAlert: false }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 5,
|
|
|
+ name: '水质监测器 #3',
|
|
|
+ location: '东区2号地块',
|
|
|
+ status: 'online',
|
|
|
+ statusText: '在线',
|
|
|
+ lastUpdate: '5分钟前更新',
|
|
|
+ type: 'water',
|
|
|
+ metrics: [
|
|
|
+ { name: 'pH值', value: '7.2', unit: '', isAlert: false },
|
|
|
+ { name: '溶解氧', value: '5.8', unit: 'mg/L', isAlert: false }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 6,
|
|
|
+ name: '气象监测器 #2',
|
|
|
+ location: '西区气象站',
|
|
|
+ status: 'warning',
|
|
|
+ statusText: '告警',
|
|
|
+ lastUpdate: '温度过高',
|
|
|
+ type: 'weather',
|
|
|
+ metrics: [
|
|
|
+ { name: '温度', value: '35.8', unit: '℃', isAlert: true },
|
|
|
+ { name: '湿度', value: '45', unit: '%', isAlert: false }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 7,
|
|
|
+ name: '土壤监测器 #5',
|
|
|
+ location: '南区1号地块',
|
|
|
+ status: 'online',
|
|
|
+ statusText: '在线',
|
|
|
+ lastUpdate: '3分钟前更新',
|
|
|
+ type: 'soil',
|
|
|
+ metrics: [
|
|
|
+ { name: '土壤湿度', value: '28.5', unit: '%', isAlert: false },
|
|
|
+ { name: '土壤温度', value: '22.8', unit: '℃', isAlert: false }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 8,
|
|
|
+ name: '水质监测器 #4',
|
|
|
+ location: '南区水质站',
|
|
|
+ status: 'online',
|
|
|
+ statusText: '在线',
|
|
|
+ lastUpdate: '1分钟前更新',
|
|
|
+ type: 'water',
|
|
|
+ metrics: [
|
|
|
+ { name: 'pH值', value: '7.1', unit: '', isAlert: false },
|
|
|
+ { name: '溶解氧', value: '6.5', unit: 'mg/L', isAlert: false }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 9,
|
|
|
+ name: '土壤监测器 #6',
|
|
|
+ location: '北区1号地块',
|
|
|
+ status: 'offline',
|
|
|
+ statusText: '离线',
|
|
|
+ lastUpdate: '设备离线 > 12h',
|
|
|
+ type: 'soil',
|
|
|
+ metrics: [
|
|
|
+ { name: '土壤湿度', value: '--', unit: '%', isAlert: false },
|
|
|
+ { name: '土壤温度', value: '--', unit: '℃', isAlert: false }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 10,
|
|
|
+ name: '气象监测器 #3',
|
|
|
+ location: '北区气象站',
|
|
|
+ status: 'online',
|
|
|
+ statusText: '在线',
|
|
|
+ lastUpdate: '30秒前更新',
|
|
|
+ type: 'weather',
|
|
|
+ metrics: [
|
|
|
+ { name: '温度', value: '28.2', unit: '℃', isAlert: false },
|
|
|
+ { name: '湿度', value: '72', unit: '%', isAlert: false }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ computed: {
|
|
|
+ totalPages() {
|
|
|
+ return Math.ceil(this.videoList.length / this.videosPerPage)
|
|
|
+ },
|
|
|
+ paginatedVideos() {
|
|
|
+ const start = (this.currentPage - 1) * this.videosPerPage
|
|
|
+ const end = start + this.videosPerPage
|
|
|
+ return this.videoList.slice(start, end)
|
|
|
+ },
|
|
|
+
|
|
|
+ // 网格全屏模式计算属性
|
|
|
+ gridTotalPages() {
|
|
|
+ return Math.ceil(this.videoList.length / this.gridFullscreen.videosPerPage)
|
|
|
+ },
|
|
|
+ gridPaginatedVideos() {
|
|
|
+ const start = (this.gridFullscreen.currentPage - 1) * this.gridFullscreen.videosPerPage
|
|
|
+ const end = start + this.gridFullscreen.videosPerPage
|
|
|
+ return this.videoList.slice(start, end)
|
|
|
+ },
|
|
|
+ gridPageRange() {
|
|
|
+ const start = (this.gridFullscreen.currentPage - 1) * this.gridFullscreen.videosPerPage + 1
|
|
|
+ const end = Math.min(this.gridFullscreen.currentPage * this.gridFullscreen.videosPerPage, this.videoList.length)
|
|
|
+ return `${start}-${end}`
|
|
|
+ },
|
|
|
+
|
|
|
+ // 设备监控计算属性
|
|
|
+ filteredDevices() {
|
|
|
+ let devices = this.deviceList
|
|
|
+
|
|
|
+ // 按状态过滤
|
|
|
+ if (this.deviceFilter !== 'all') {
|
|
|
+ devices = devices.filter(device => device.status === this.deviceFilter)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按类型过滤
|
|
|
+ if (this.deviceTypeFilter !== 'all') {
|
|
|
+ devices = devices.filter(device => device.type === this.deviceTypeFilter)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 排序
|
|
|
+ if (this.sortBy === 'alert') {
|
|
|
+ devices = devices.sort((a, b) => {
|
|
|
+ const statusOrder = { warning: 0, offline: 1, online: 2 }
|
|
|
+ return statusOrder[a.status] - statusOrder[b.status]
|
|
|
+ })
|
|
|
+ } else if (this.sortBy === 'name') {
|
|
|
+ devices = devices.sort((a, b) => a.name.localeCompare(b.name))
|
|
|
+ } else if (this.sortBy === 'time') {
|
|
|
+ devices = devices.sort((a, b) => {
|
|
|
+ // 简单的时间排序逻辑,实际应该根据真实时间戳
|
|
|
+ const getUpdateTime = (update) => {
|
|
|
+ if (update.includes('分钟前')) return parseInt(update.match(/\d+/)[0])
|
|
|
+ if (update.includes('刚刚')) return 0
|
|
|
+ return 999 // 其他情况放最后
|
|
|
+ }
|
|
|
+ return getUpdateTime(a.lastUpdate) - getUpdateTime(b.lastUpdate)
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ return devices
|
|
|
+ },
|
|
|
+
|
|
|
+ deviceTotalPages() {
|
|
|
+ return Math.ceil(this.filteredDevices.length / this.devicePerPage)
|
|
|
+ },
|
|
|
+
|
|
|
+ paginatedDevices() {
|
|
|
+ const start = (this.deviceCurrentPage - 1) * this.devicePerPage
|
|
|
+ const end = start + this.devicePerPage
|
|
|
+ return this.filteredDevices.slice(start, end)
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ methods: {
|
|
|
+ refreshData() {
|
|
|
+ console.log('刷新数据')
|
|
|
+ this.lastUpdateTime = '刚刚'
|
|
|
+ // 这里可以添加实际的数据刷新逻辑
|
|
|
+ },
|
|
|
+
|
|
|
+ prevPage() {
|
|
|
+ if (this.currentPage > 1) {
|
|
|
+ this.currentPage--
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ nextPage() {
|
|
|
+ if (this.currentPage < this.totalPages) {
|
|
|
+ this.currentPage++
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ getPageRange() {
|
|
|
+ const start = (this.currentPage - 1) * this.videosPerPage + 1
|
|
|
+ const end = Math.min(this.currentPage * this.videosPerPage, this.videoList.length)
|
|
|
+ return `${start}-${end}`
|
|
|
+ },
|
|
|
+
|
|
|
+ toggleFullscreen() {
|
|
|
+ console.log('切换网格全屏模式')
|
|
|
+ this.gridFullscreen.show = true
|
|
|
+ },
|
|
|
+
|
|
|
+ exitGridFullscreen() {
|
|
|
+ this.gridFullscreen.show = false
|
|
|
+ this.gridFullscreen.currentPage = 1
|
|
|
+ },
|
|
|
+
|
|
|
+ // 网格全屏分页方法
|
|
|
+ gridPrevPage() {
|
|
|
+ if (this.gridFullscreen.currentPage > 1) {
|
|
|
+ this.gridFullscreen.currentPage--
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ gridNextPage() {
|
|
|
+ if (this.gridFullscreen.currentPage < this.gridTotalPages) {
|
|
|
+ this.gridFullscreen.currentPage++
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ openVideoFullscreen(video) {
|
|
|
+ console.log('打开单个视频全屏:', video.name)
|
|
|
+ console.log('Video data:', video)
|
|
|
+ // 如果是从网格全屏模式进入,先关闭网格全屏
|
|
|
+ if (this.gridFullscreen.show) {
|
|
|
+ this.gridFullscreen.show = false
|
|
|
+ }
|
|
|
+ this.singleCameraFullscreen.show = true
|
|
|
+ this.singleCameraFullscreen.camera = video
|
|
|
+ // 确保数据更新后再强制更新视图
|
|
|
+ this.$nextTick(() => {
|
|
|
+ console.log('Fullscreen camera:', this.singleCameraFullscreen.camera)
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ exitSingleFullscreen() {
|
|
|
+ this.singleCameraFullscreen.show = false
|
|
|
+ this.singleCameraFullscreen.camera = null
|
|
|
+ },
|
|
|
+
|
|
|
+ // 设备监控方法
|
|
|
+ setFilter(filter) {
|
|
|
+ this.deviceFilter = filter
|
|
|
+ this.deviceCurrentPage = 1 // 重置到第一页
|
|
|
+ },
|
|
|
+
|
|
|
+ setTypeFilter(type) {
|
|
|
+ this.deviceTypeFilter = this.deviceTypeFilter === type ? 'all' : type
|
|
|
+ this.deviceCurrentPage = 1 // 重置到第一页
|
|
|
+ },
|
|
|
+
|
|
|
+ getDeviceCountByStatus(status) {
|
|
|
+ return this.deviceList.filter(device => device.status === status).length
|
|
|
+ },
|
|
|
+
|
|
|
+ prevDevicePage() {
|
|
|
+ if (this.deviceCurrentPage > 1) {
|
|
|
+ this.deviceCurrentPage--
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ nextDevicePage() {
|
|
|
+ if (this.deviceCurrentPage < this.deviceTotalPages) {
|
|
|
+ this.deviceCurrentPage++
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ getDevicePageRange() {
|
|
|
+ if (this.filteredDevices.length === 0) return '0-0'
|
|
|
+ const start = (this.deviceCurrentPage - 1) * this.devicePerPage + 1
|
|
|
+ const end = Math.min(this.deviceCurrentPage * this.devicePerPage, this.filteredDevices.length)
|
|
|
+ return `${start}-${end}`
|
|
|
+ },
|
|
|
+
|
|
|
+ showDeviceHistory(device) {
|
|
|
+ console.log('显示设备数据:', device.name)
|
|
|
+ this.historyModal.show = true
|
|
|
+ this.historyModal.device = device
|
|
|
+ this.historyModal.timeRange = '24h'
|
|
|
+ this.historyModal.activeIndicator = 'all'
|
|
|
+
|
|
|
+ // 等待模态框渲染完成后初始化图表
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.initChart()
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ closeHistoryModal() {
|
|
|
+ this.historyModal.show = false
|
|
|
+ this.historyModal.device = null
|
|
|
+ // 销毁图表实例
|
|
|
+ if (this.chartInstance) {
|
|
|
+ this.chartInstance.dispose()
|
|
|
+ this.chartInstance = null
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ setTimeRange(range) {
|
|
|
+ this.historyModal.timeRange = range
|
|
|
+ this.updateChart()
|
|
|
+ },
|
|
|
+
|
|
|
+ setActiveIndicator(indicator) {
|
|
|
+ this.historyModal.activeIndicator = indicator
|
|
|
+ this.updateChart()
|
|
|
+ },
|
|
|
+
|
|
|
+ getDeviceIndicators() {
|
|
|
+ if (!this.historyModal.device) return []
|
|
|
+
|
|
|
+ const indicators = {
|
|
|
+ soil: [
|
|
|
+ { label: '所有指标', value: 'all' },
|
|
|
+ { label: '土壤湿度', value: 'soilHumidity' },
|
|
|
+ { label: '土壤温度', value: 'soilTemperature' },
|
|
|
+ { label: 'EC值', value: 'ec' }
|
|
|
+ ],
|
|
|
+ water: [
|
|
|
+ { label: '所有指标', value: 'all' },
|
|
|
+ { label: 'pH值', value: 'ph' },
|
|
|
+ { label: '溶解氧', value: 'oxygen' },
|
|
|
+ { label: '电导率', value: 'conductivity' },
|
|
|
+ { label: '浊度', value: 'turbidity' }
|
|
|
+ ],
|
|
|
+ weather: [
|
|
|
+ { label: '所有指标', value: 'all' },
|
|
|
+ { label: '温度', value: 'temperature' },
|
|
|
+ { label: '湿度', value: 'humidity' },
|
|
|
+ { label: '光照', value: 'light' },
|
|
|
+ { label: '风速', value: 'windSpeed' },
|
|
|
+ { label: '降雨量', value: 'rainfall' }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+
|
|
|
+ return indicators[this.historyModal.device.type] || indicators.soil
|
|
|
+ },
|
|
|
+
|
|
|
+ initChart() {
|
|
|
+ if (!this.$refs.chartContainer) return
|
|
|
+
|
|
|
+ // 销毁已存在的图表实例
|
|
|
+ if (this.chartInstance) {
|
|
|
+ this.chartInstance.dispose()
|
|
|
+ this.chartInstance = null
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建新的图表实例
|
|
|
+ this.chartInstance = this.$echarts.init(this.$refs.chartContainer)
|
|
|
+ this.updateChart()
|
|
|
+ },
|
|
|
+
|
|
|
+ updateChart() {
|
|
|
+ if (!this.chartInstance) return
|
|
|
+
|
|
|
+ // 生成模拟数据
|
|
|
+ const data = this.generateMockData()
|
|
|
+ const seriesNames = this.getSeriesNames()
|
|
|
+
|
|
|
+ const option = {
|
|
|
+ backgroundColor: 'transparent',
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ backgroundColor: 'rgba(15, 23, 42, 0.9)',
|
|
|
+ borderColor: '#334155',
|
|
|
+ textStyle: { color: '#f1f5f9' }
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ data: seriesNames,
|
|
|
+ textStyle: { color: '#94a3b8' },
|
|
|
+ top: 0
|
|
|
+ },
|
|
|
+ grid: {
|
|
|
+ left: '3%',
|
|
|
+ right: '4%',
|
|
|
+ bottom: '3%',
|
|
|
+ containLabel: true,
|
|
|
+ top: 40
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ boundaryGap: false,
|
|
|
+ data: data.times,
|
|
|
+ axisLine: { lineStyle: { color: '#334155' } },
|
|
|
+ axisLabel: { color: '#94a3b8' }
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ axisLine: { lineStyle: { color: '#334155' } },
|
|
|
+ axisLabel: { color: '#94a3b8' },
|
|
|
+ splitLine: { lineStyle: { color: '#1e293b' } }
|
|
|
+ },
|
|
|
+ series: this.generateSeries(data)
|
|
|
+ }
|
|
|
+
|
|
|
+ this.chartInstance.setOption(option)
|
|
|
+ },
|
|
|
+
|
|
|
+ generateMockData() {
|
|
|
+ const times = []
|
|
|
+ const values = {}
|
|
|
+ const points = this.historyModal.timeRange === '24h' ? 24 : this.historyModal.timeRange === '7d' ? 7 : 30
|
|
|
+ const now = new Date()
|
|
|
+
|
|
|
+ // 根据设备类型生成不同的数据
|
|
|
+ const ranges = this.getDataRanges()
|
|
|
+
|
|
|
+ for (let i = points - 1; i >= 0; i--) {
|
|
|
+ const time = new Date(now - i * (this.historyModal.timeRange === '24h' ? 3600000 : 86400000))
|
|
|
+ times.push(this.historyModal.timeRange === '24h' ?
|
|
|
+ time.getHours() + ':00' :
|
|
|
+ (time.getMonth() + 1) + '/' + time.getDate())
|
|
|
+
|
|
|
+ ranges.forEach((range, index) => {
|
|
|
+ if (!values[`values${index + 1}`]) {
|
|
|
+ values[`values${index + 1}`] = []
|
|
|
+ }
|
|
|
+ values[`values${index + 1}`].push(
|
|
|
+ (Math.random() * (range[1] - range[0]) + range[0]).toFixed(1)
|
|
|
+ )
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ return { times, ...values }
|
|
|
+ },
|
|
|
+
|
|
|
+ getDataRanges() {
|
|
|
+ const ranges = {
|
|
|
+ soil: [[20, 40], [15, 30], [0.5, 2]], // 土壤湿度, 土壤温度, EC值
|
|
|
+ water: [[6.5, 8.5], [4, 8], [0.5, 2], [0, 10]], // pH值, 溶解氧, 电导率, 浊度
|
|
|
+ weather: [[15, 35], [40, 80], [0, 100000], [0, 10], [0, 50]] // 温度, 湿度, 光照, 风速, 降雨量
|
|
|
+ }
|
|
|
+ return ranges[this.historyModal.device.type] || ranges.soil
|
|
|
+ },
|
|
|
+
|
|
|
+ getSeriesNames() {
|
|
|
+ const names = {
|
|
|
+ soil: ['土壤湿度', '土壤温度', 'EC值'],
|
|
|
+ water: ['pH值', '溶解氧', '电导率', '浊度'],
|
|
|
+ weather: ['温度', '湿度', '光照', '风速', '降雨量']
|
|
|
+ }
|
|
|
+ const allNames = names[this.historyModal.device.type] || names.soil
|
|
|
+
|
|
|
+ if (this.historyModal.activeIndicator === 'all') {
|
|
|
+ return allNames
|
|
|
+ } else {
|
|
|
+ const indicatorMap = {
|
|
|
+ soilHumidity: '土壤湿度',
|
|
|
+ soilTemperature: '土壤温度',
|
|
|
+ ec: 'EC值',
|
|
|
+ ph: 'pH值',
|
|
|
+ oxygen: '溶解氧',
|
|
|
+ conductivity: '电导率',
|
|
|
+ turbidity: '浊度',
|
|
|
+ temperature: '温度',
|
|
|
+ humidity: '湿度',
|
|
|
+ light: '光照',
|
|
|
+ windSpeed: '风速',
|
|
|
+ rainfall: '降雨量'
|
|
|
+ }
|
|
|
+ return [indicatorMap[this.historyModal.activeIndicator] || allNames[0]]
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ generateSeries(data) {
|
|
|
+ const deviceType = this.historyModal.device.type
|
|
|
+ const activeIndicator = this.historyModal.activeIndicator
|
|
|
+
|
|
|
+ const indicators = {
|
|
|
+ soil: {
|
|
|
+ names: ['土壤湿度', '土壤温度', 'EC值'],
|
|
|
+ colors: ['#0ea5e9', '#10b981', '#f59e0b']
|
|
|
+ },
|
|
|
+ water: {
|
|
|
+ names: ['pH值', '溶解氧', '电导率', '浊度'],
|
|
|
+ colors: ['#0ea5e9', '#10b981', '#f59e0b', '#8b5cf6']
|
|
|
+ },
|
|
|
+ weather: {
|
|
|
+ names: ['温度', '湿度', '光照', '风速', '降雨量'],
|
|
|
+ colors: ['#0ea5e9', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899']
|
|
|
+ }
|
|
|
+ }[deviceType] || { names: [], colors: [] }
|
|
|
+
|
|
|
+ if (activeIndicator === 'all') {
|
|
|
+ return indicators.names.map((name, index) => ({
|
|
|
+ name: name,
|
|
|
+ type: 'line',
|
|
|
+ data: data[`values${index + 1}`] || [],
|
|
|
+ smooth: true,
|
|
|
+ showSymbol: false,
|
|
|
+ lineStyle: { width: 2 },
|
|
|
+ areaStyle: { opacity: 0.1 },
|
|
|
+ itemStyle: { color: indicators.colors[index] }
|
|
|
+ }))
|
|
|
+ } else {
|
|
|
+ const index = indicators.names.indexOf(this.getIndicatorLabel(activeIndicator))
|
|
|
+ return [{
|
|
|
+ name: this.getIndicatorLabel(activeIndicator),
|
|
|
+ type: 'line',
|
|
|
+ data: data[`values${index + 1}`] || [],
|
|
|
+ smooth: true,
|
|
|
+ showSymbol: false,
|
|
|
+ lineStyle: { width: 2 },
|
|
|
+ areaStyle: { opacity: 0.1 },
|
|
|
+ itemStyle: { color: indicators.colors[index] }
|
|
|
+ }]
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ getIndicatorLabel(value) {
|
|
|
+ const indicatorMap = {
|
|
|
+ soilHumidity: '土壤湿度',
|
|
|
+ soilTemperature: '土壤温度',
|
|
|
+ ec: 'EC值',
|
|
|
+ ph: 'pH值',
|
|
|
+ oxygen: '溶解氧',
|
|
|
+ conductivity: '电导率',
|
|
|
+ turbidity: '浊度',
|
|
|
+ temperature: '温度',
|
|
|
+ humidity: '湿度',
|
|
|
+ light: '光照',
|
|
|
+ windSpeed: '风速',
|
|
|
+ rainfall: '降雨量'
|
|
|
+ }
|
|
|
+ return indicatorMap[value] || value
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ mounted() {
|
|
|
+ // 保存原始背景色和设置深色主题
|
|
|
+ this.originalBodyBg = document.body.style.backgroundColor || ''
|
|
|
+ this.originalBodyColor = document.body.style.color || ''
|
|
|
+ this.originalAppBg = document.getElementById('app')?.style.backgroundColor || ''
|
|
|
+
|
|
|
+ // 应用深色主题
|
|
|
+ document.body.style.backgroundColor = '#0f172a'
|
|
|
+ document.body.style.color = '#f1f5f9'
|
|
|
+ const appEl = document.getElementById('app')
|
|
|
+ if (appEl) {
|
|
|
+ appEl.style.backgroundColor = '#0f172a'
|
|
|
+ }
|
|
|
+
|
|
|
+ // 监听键盘事件
|
|
|
+ document.addEventListener('keydown', (e) => {
|
|
|
+ if (e.key === 'Escape') {
|
|
|
+ if (this.historyModal.show) {
|
|
|
+ this.closeHistoryModal()
|
|
|
+ } else if (this.singleCameraFullscreen.show) {
|
|
|
+ this.exitSingleFullscreen()
|
|
|
+ } else if (this.gridFullscreen.show) {
|
|
|
+ this.exitGridFullscreen()
|
|
|
+ }
|
|
|
+ } else if (this.gridFullscreen.show) {
|
|
|
+ // 网格全屏模式下的键盘快捷键
|
|
|
+ if (e.key === 'ArrowLeft') {
|
|
|
+ e.preventDefault()
|
|
|
+ this.gridPrevPage()
|
|
|
+ } else if (e.key === 'ArrowRight') {
|
|
|
+ e.preventDefault()
|
|
|
+ this.gridNextPage()
|
|
|
+ } else if (e.key >= '1' && e.key <= '9') {
|
|
|
+ // 数字键快速跳转页面
|
|
|
+ const pageNum = parseInt(e.key)
|
|
|
+ if (pageNum <= this.gridTotalPages) {
|
|
|
+ this.gridFullscreen.currentPage = pageNum
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ beforeDestroy() {
|
|
|
+ // 恢复原始背景色
|
|
|
+ document.body.style.backgroundColor = this.originalBodyBg
|
|
|
+ document.body.style.color = this.originalBodyColor
|
|
|
+ const appEl = document.getElementById('app')
|
|
|
+ if (appEl) {
|
|
|
+ appEl.style.backgroundColor = this.originalAppBg
|
|
|
+ }
|
|
|
+
|
|
|
+ // 销毁图表实例
|
|
|
+ if (this.chartInstance) {
|
|
|
+ this.chartInstance.dispose()
|
|
|
+ this.chartInstance = null
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.device-monitor-container {
|
|
|
+ padding: 20px 20px 20px;
|
|
|
+ background-color: #0f172a;
|
|
|
+ color: #f1f5f9;
|
|
|
+ min-height: 100vh;
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+/* 控制面板 */
|
|
|
+.device-monitor-container > .control-panel {
|
|
|
+ background: #1e293b;
|
|
|
+ border: 1px solid #334155;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 1.5rem;
|
|
|
+ margin-bottom: 2rem;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ gap: 2rem;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.device-monitor-container > .control-panel:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
|
|
+ border-color: #0ea5e9;
|
|
|
+}
|
|
|
+
|
|
|
+.control-left {
|
|
|
+ display: flex;
|
|
|
+ gap: 1.5rem;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.form-group {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 0.5rem;
|
|
|
+}
|
|
|
+
|
|
|
+.form-group label {
|
|
|
+ font-size: 0.875rem;
|
|
|
+ color: #94a3b8;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.form-group select {
|
|
|
+ background-color: #1e293b;
|
|
|
+ border: 1px solid #334155;
|
|
|
+ color: #f1f5f9;
|
|
|
+ padding: 8px 12px;
|
|
|
+ border-radius: 6px;
|
|
|
+ min-width: 150px;
|
|
|
+}
|
|
|
+
|
|
|
+.form-group select:focus {
|
|
|
+ outline: none;
|
|
|
+ border-color: #0ea5e9;
|
|
|
+}
|
|
|
+
|
|
|
+.control-right {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 1rem;
|
|
|
+}
|
|
|
+
|
|
|
+.refresh-btn {
|
|
|
+ background-color: #0ea5e9;
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ padding: 10px 20px;
|
|
|
+ border-radius: 6px;
|
|
|
+ cursor: pointer;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.refresh-btn:hover {
|
|
|
+ background-color: #0369a1;
|
|
|
+}
|
|
|
+
|
|
|
+.auto-refresh {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 0.5rem;
|
|
|
+ font-size: 0.875rem;
|
|
|
+ color: #94a3b8;
|
|
|
+}
|
|
|
+
|
|
|
+.auto-refresh-select {
|
|
|
+ background-color: #1e293b;
|
|
|
+ border: 1px solid #334155;
|
|
|
+ color: #f1f5f9;
|
|
|
+ padding: 4px 8px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 0.875rem;
|
|
|
+}
|
|
|
+
|
|
|
+.auto-refresh-select:focus {
|
|
|
+ outline: none;
|
|
|
+ border-color: #0ea5e9;
|
|
|
+}
|
|
|
+
|
|
|
+.last-update {
|
|
|
+ font-size: 0.875rem;
|
|
|
+ color: #94a3b8;
|
|
|
+}
|
|
|
+
|
|
|
+.last-update span {
|
|
|
+ color: #f1f5f9;
|
|
|
+}
|
|
|
+
|
|
|
+/* 统计容器 */
|
|
|
+.stats-container {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 2fr 5fr;
|
|
|
+ gap: 1rem;
|
|
|
+ margin-bottom: 2rem;
|
|
|
+}
|
|
|
+
|
|
|
+/* 综合统计卡片 */
|
|
|
+.overview-stat-card {
|
|
|
+ background: #1e293b;
|
|
|
+ border: 1px solid #334155;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 1.5rem;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.overview-stat-card:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
|
|
+ border-color: #0ea5e9;
|
|
|
+}
|
|
|
+
|
|
|
+.overview-stats {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ gap: 1.5rem;
|
|
|
+}
|
|
|
+
|
|
|
+.overview-item {
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+/* 分类统计区域 */
|
|
|
+.category-stats {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(5, 1fr);
|
|
|
+ gap: 1rem;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-card {
|
|
|
+ background: #1e293b;
|
|
|
+ border: 1px solid #334155;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 1.5rem;
|
|
|
+ text-align: center;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-card:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
|
|
+ border-color: #0ea5e9;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-card.alert {
|
|
|
+ border-color: #ef4444;
|
|
|
+ background: linear-gradient(to right, rgba(239, 68, 68, 0.05), transparent);
|
|
|
+ animation: pulse 2s infinite;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes pulse {
|
|
|
+ 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); }
|
|
|
+ 70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); }
|
|
|
+ 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
|
|
|
+}
|
|
|
+
|
|
|
+.stat-number {
|
|
|
+ font-size: 2rem;
|
|
|
+ font-weight: 700;
|
|
|
+ margin-bottom: 0.5rem;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-number.online { color: #10b981; }
|
|
|
+.stat-number.offline { color: #ef4444; }
|
|
|
+.stat-number.alert { color: #ef4444; }
|
|
|
+.stat-number.camera { color: #3b82f6; }
|
|
|
+.stat-number.sensor { color: #10b981; }
|
|
|
+.stat-number.weather { color: #8b5cf6; }
|
|
|
+.stat-number.control { color: #f59e0b; }
|
|
|
+
|
|
|
+.stat-label {
|
|
|
+ font-size: 0.875rem;
|
|
|
+ color: #94a3b8;
|
|
|
+ margin-bottom: 0.25rem;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-detail {
|
|
|
+ font-size: 0.75rem;
|
|
|
+ color: #64748b;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-detail .processed {
|
|
|
+ color: #10b981;
|
|
|
+}
|
|
|
+
|
|
|
+/* 主要内容区域 */
|
|
|
+.main-content {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 2rem;
|
|
|
+}
|
|
|
+
|
|
|
+.content-section {
|
|
|
+ background: #1e293b;
|
|
|
+ border: 1px solid #334155;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 1.5rem;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.content-section:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
|
|
+ border-color: #0ea5e9;
|
|
|
+}
|
|
|
+
|
|
|
+.section-title {
|
|
|
+ font-size: 1.25rem;
|
|
|
+ font-weight: 600;
|
|
|
+ margin-bottom: 0;
|
|
|
+ color: #f1f5f9;
|
|
|
+}
|
|
|
+
|
|
|
+/* 视频头部样式 */
|
|
|
+.video-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 1.5rem;
|
|
|
+}
|
|
|
+
|
|
|
+.video-header-left {
|
|
|
+ display: flex;
|
|
|
+ align-items: baseline;
|
|
|
+ gap: 1rem;
|
|
|
+}
|
|
|
+
|
|
|
+.video-count {
|
|
|
+ font-size: 0.875rem;
|
|
|
+ color: #94a3b8;
|
|
|
+}
|
|
|
+
|
|
|
+.video-header-right {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 0.75rem;
|
|
|
+}
|
|
|
+
|
|
|
+.video-nav-controls {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 0.5rem;
|
|
|
+}
|
|
|
+
|
|
|
+.video-nav-btn {
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: rgba(15, 23, 42, 0.8);
|
|
|
+ border: 1px solid #334155;
|
|
|
+ border-radius: 4px;
|
|
|
+ color: #94a3b8;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.video-nav-btn:hover:not(:disabled) {
|
|
|
+ background: #1e293b;
|
|
|
+ color: #f1f5f9;
|
|
|
+ border-color: #0ea5e9;
|
|
|
+}
|
|
|
+
|
|
|
+.video-nav-btn:disabled {
|
|
|
+ opacity: 0.5;
|
|
|
+ cursor: not-allowed;
|
|
|
+}
|
|
|
+
|
|
|
+.page-indicator {
|
|
|
+ padding: 0 12px;
|
|
|
+ height: 32px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ background: rgba(15, 23, 42, 0.8);
|
|
|
+ border: 1px solid #334155;
|
|
|
+ border-radius: 4px;
|
|
|
+ color: #94a3b8;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 视频容器样式 */
|
|
|
+.video-container {
|
|
|
+ padding: 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 视频网格 */
|
|
|
+.video-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ gap: 1rem;
|
|
|
+}
|
|
|
+
|
|
|
+.video-card {
|
|
|
+ position: relative;
|
|
|
+ background: #0f172a;
|
|
|
+ border: 1px solid #334155;
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.video-card:hover {
|
|
|
+ border-color: #0ea5e9;
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
|
+}
|
|
|
+
|
|
|
+/* 摄像头全屏按钮 */
|
|
|
+.camera-fullscreen-btn {
|
|
|
+ position: absolute;
|
|
|
+ top: 12px;
|
|
|
+ right: 12px;
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
+ border-radius: 4px;
|
|
|
+ color: #94a3b8;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ z-index: 10;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.camera-fullscreen-btn:hover {
|
|
|
+ background: rgba(0, 0, 0, 0.7);
|
|
|
+ color: #f1f5f9;
|
|
|
+ border-color: #0ea5e9;
|
|
|
+}
|
|
|
+
|
|
|
+.video-preview {
|
|
|
+ position: relative;
|
|
|
+ padding-top: 56.25%; /* 16:9 aspect ratio */
|
|
|
+ background: #0f172a;
|
|
|
+}
|
|
|
+
|
|
|
+.camera-placeholder {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.camera-icon {
|
|
|
+ position: absolute;
|
|
|
+ width: 48px;
|
|
|
+ height: 48px;
|
|
|
+ color: #94a3b8;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+}
|
|
|
+
|
|
|
+/* 强制居中的新样式 */
|
|
|
+.centered-container {
|
|
|
+ display: flex !important;
|
|
|
+ align-items: center !important;
|
|
|
+ justify-content: center !important;
|
|
|
+}
|
|
|
+
|
|
|
+.centered-icon {
|
|
|
+ position: relative !important;
|
|
|
+ top: auto !important;
|
|
|
+ left: auto !important;
|
|
|
+ transform: none !important;
|
|
|
+ margin: auto !important;
|
|
|
+}
|
|
|
+
|
|
|
+.video-info-overlay {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent);
|
|
|
+ padding: 0.75rem;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: flex-end;
|
|
|
+ z-index: 5;
|
|
|
+}
|
|
|
+
|
|
|
+.video-details {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.video-name {
|
|
|
+ font-weight: 500;
|
|
|
+ margin-bottom: 0.25rem;
|
|
|
+ color: #f1f5f9;
|
|
|
+ font-size: 0.875rem;
|
|
|
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
|
|
+}
|
|
|
+
|
|
|
+.video-location {
|
|
|
+ font-size: 0.75rem;
|
|
|
+ color: #e2e8f0;
|
|
|
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
|
|
+}
|
|
|
+
|
|
|
+.video-status {
|
|
|
+ font-size: 0.75rem;
|
|
|
+ padding: 4px 8px;
|
|
|
+ border-radius: 12px;
|
|
|
+ font-weight: 500;
|
|
|
+ white-space: nowrap;
|
|
|
+ backdrop-filter: blur(4px);
|
|
|
+}
|
|
|
+
|
|
|
+.video-status.online {
|
|
|
+ background-color: rgba(16, 185, 129, 0.2);
|
|
|
+ color: #10b981;
|
|
|
+ border: 1px solid rgba(16, 185, 129, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.video-status.offline {
|
|
|
+ background-color: rgba(239, 68, 68, 0.2);
|
|
|
+ color: #ef4444;
|
|
|
+ border: 1px solid rgba(239, 68, 68, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+/* 设备监控头部 */
|
|
|
+.device-monitor-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 1.5rem;
|
|
|
+ gap: 2rem;
|
|
|
+}
|
|
|
+
|
|
|
+.device-monitor-title-section {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 1rem;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.section-title {
|
|
|
+ margin: 0;
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-buttons {
|
|
|
+ display: flex;
|
|
|
+ gap: 0.5rem;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-divider {
|
|
|
+ height: 24px;
|
|
|
+ width: 1px;
|
|
|
+ background-color: #334155;
|
|
|
+}
|
|
|
+
|
|
|
+.type-filters {
|
|
|
+ display: flex;
|
|
|
+ gap: 0.5rem;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-btn {
|
|
|
+ padding: 0.5rem 1rem;
|
|
|
+ border-radius: 6px;
|
|
|
+ font-size: 0.875rem;
|
|
|
+ color: #94a3b8;
|
|
|
+ background: transparent;
|
|
|
+ border: 1px solid #334155;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 0.5rem;
|
|
|
+ cursor: pointer;
|
|
|
+ height: 36px; /* 固定高度确保对齐 */
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-btn:hover {
|
|
|
+ background: rgba(255, 255, 255, 0.05);
|
|
|
+ border-color: #0ea5e9;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-btn.active {
|
|
|
+ background: #0ea5e9;
|
|
|
+ border-color: #0ea5e9;
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-count {
|
|
|
+ font-size: 0.75rem;
|
|
|
+ color: #64748b;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-btn.active .filter-count {
|
|
|
+ color: rgba(255, 255, 255, 0.8);
|
|
|
+}
|
|
|
+
|
|
|
+.status-dot {
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: inline-block;
|
|
|
+}
|
|
|
+
|
|
|
+.status-dot.warning {
|
|
|
+ background-color: #f59e0b;
|
|
|
+}
|
|
|
+
|
|
|
+.status-dot.offline {
|
|
|
+ background-color: #ef4444;
|
|
|
+}
|
|
|
+
|
|
|
+.device-monitor-controls {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 1rem;
|
|
|
+}
|
|
|
+
|
|
|
+.sort-select {
|
|
|
+ background-color: #1e293b;
|
|
|
+ border: 1px solid #334155;
|
|
|
+ color: #f1f5f9;
|
|
|
+ padding: 0.5rem 2rem 0.5rem 1rem;
|
|
|
+ border-radius: 6px;
|
|
|
+ font-size: 0.875rem;
|
|
|
+ appearance: none;
|
|
|
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2394a3b8'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
|
|
|
+ background-repeat: no-repeat;
|
|
|
+ background-position: right 0.75rem center;
|
|
|
+ background-size: 1rem;
|
|
|
+ height: 36px; /* 与按钮高度保持一致 */
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+
|
|
|
+.sort-select:focus {
|
|
|
+ outline: none;
|
|
|
+ border-color: #0ea5e9;
|
|
|
+}
|
|
|
+
|
|
|
+.device-pagination {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 0.5rem;
|
|
|
+}
|
|
|
+
|
|
|
+.page-btn {
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border-radius: 6px;
|
|
|
+ color: #94a3b8;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ background: #1e293b;
|
|
|
+ border: 1px solid #334155;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.page-indicator {
|
|
|
+ padding: 0 12px;
|
|
|
+ height: 32px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ color: #94a3b8;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.page-btn:hover:not(:disabled) {
|
|
|
+ background: #0ea5e9;
|
|
|
+ border-color: #0ea5e9;
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+
|
|
|
+.page-btn:disabled {
|
|
|
+ opacity: 0.5;
|
|
|
+ cursor: not-allowed;
|
|
|
+}
|
|
|
+
|
|
|
+/* 设备网格 */
|
|
|
+.device-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
|
|
+ gap: 1rem;
|
|
|
+ padding: 0.5rem;
|
|
|
+ min-height: 400px;
|
|
|
+}
|
|
|
+
|
|
|
+@media (min-width: 1280px) {
|
|
|
+ .device-grid {
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media (min-width: 1536px) {
|
|
|
+ .device-grid {
|
|
|
+ grid-template-columns: repeat(4, 1fr);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.device-card {
|
|
|
+ position: relative;
|
|
|
+ background: #1e293b;
|
|
|
+ border: 1px solid #334155;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 1.5rem;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ min-height: 250px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+/* 右上角数据按钮 */
|
|
|
+.data-btn {
|
|
|
+ position: absolute;
|
|
|
+ top: 1rem;
|
|
|
+ right: 1rem;
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: #0f172a;
|
|
|
+ border: 1px solid #334155;
|
|
|
+ border-radius: 6px;
|
|
|
+ color: #94a3b8;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ z-index: 1;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.data-btn:hover {
|
|
|
+ background: #0ea5e9;
|
|
|
+ border-color: #0ea5e9;
|
|
|
+ color: white;
|
|
|
+ transform: translateY(-1px);
|
|
|
+}
|
|
|
+
|
|
|
+.data-btn svg {
|
|
|
+ width: 16px;
|
|
|
+ height: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.device-card:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
|
|
+}
|
|
|
+
|
|
|
+.device-card.warning {
|
|
|
+ border-color: #ef4444;
|
|
|
+ background: linear-gradient(to right, rgba(239, 68, 68, 0.05), transparent);
|
|
|
+}
|
|
|
+
|
|
|
+.device-card.offline {
|
|
|
+ opacity: 0.8;
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+.device-header {
|
|
|
+ margin-bottom: 0.5rem;
|
|
|
+ padding-right: 3rem; /* 为右上角数据按钮留出空间 */
|
|
|
+}
|
|
|
+
|
|
|
+.device-title-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 0.75rem;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.device-name {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #f1f5f9;
|
|
|
+ font-size: 1.125rem;
|
|
|
+}
|
|
|
+
|
|
|
+.device-status {
|
|
|
+ font-size: 0.75rem;
|
|
|
+ padding: 2px 8px;
|
|
|
+ border-radius: 12px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.device-status.online {
|
|
|
+ background-color: rgba(16, 185, 129, 0.1);
|
|
|
+ color: #10b981;
|
|
|
+}
|
|
|
+
|
|
|
+.device-status.warning {
|
|
|
+ background-color: rgba(245, 158, 11, 0.1);
|
|
|
+ color: #f59e0b;
|
|
|
+}
|
|
|
+
|
|
|
+.device-status.offline {
|
|
|
+ background-color: rgba(239, 68, 68, 0.2);
|
|
|
+ color: #ef4444;
|
|
|
+}
|
|
|
+
|
|
|
+.device-location {
|
|
|
+ font-size: 0.875rem;
|
|
|
+ color: #94a3b8;
|
|
|
+ margin-bottom: 1.5rem;
|
|
|
+}
|
|
|
+
|
|
|
+.device-metrics {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ gap: 2rem;
|
|
|
+ margin-bottom: 1.5rem;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.metric {
|
|
|
+ text-align: left;
|
|
|
+}
|
|
|
+
|
|
|
+.metric-label {
|
|
|
+ font-size: 0.875rem;
|
|
|
+ color: #94a3b8;
|
|
|
+ margin-bottom: 0.25rem;
|
|
|
+}
|
|
|
+
|
|
|
+.metric-value {
|
|
|
+ font-size: 1.875rem;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #f1f5f9;
|
|
|
+ display: flex;
|
|
|
+ align-items: baseline;
|
|
|
+}
|
|
|
+
|
|
|
+.metric-value span {
|
|
|
+ font-size: 1.25rem;
|
|
|
+ margin-left: 0.25rem;
|
|
|
+}
|
|
|
+
|
|
|
+.metric-value.metric-warning {
|
|
|
+ color: #ef4444;
|
|
|
+}
|
|
|
+
|
|
|
+.device-card.offline .metric-value {
|
|
|
+ color: #6b7280;
|
|
|
+}
|
|
|
+
|
|
|
+.device-footer {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-top: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.last-update {
|
|
|
+ font-size: 0.875rem;
|
|
|
+ color: #94a3b8;
|
|
|
+}
|
|
|
+
|
|
|
+.last-update.text-warning {
|
|
|
+ color: #ef4444;
|
|
|
+}
|
|
|
+
|
|
|
+.last-update.text-offline {
|
|
|
+ color: #6b7280;
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+.detail-btn {
|
|
|
+ background: transparent;
|
|
|
+ border: 1px solid #334155;
|
|
|
+ color: #f1f5f9;
|
|
|
+ padding: 6px 12px;
|
|
|
+ border-radius: 6px;
|
|
|
+ font-size: 0.875rem;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-btn:hover {
|
|
|
+ border-color: #0ea5e9;
|
|
|
+ background-color: rgba(14, 165, 233, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.detail-btn.warning {
|
|
|
+ border-color: #ef4444;
|
|
|
+ color: #ef4444;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-btn.warning:hover {
|
|
|
+ background-color: #ef4444;
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+/* 单个摄像头全屏模式 */
|
|
|
+.single-camera-fullscreen {
|
|
|
+ position: fixed !important;
|
|
|
+ top: 0 !important;
|
|
|
+ left: 0 !important;
|
|
|
+ width: 100vw !important;
|
|
|
+ height: 100vh !important;
|
|
|
+ background-color: #0f172a !important;
|
|
|
+ z-index: 10000 !important;
|
|
|
+ display: flex !important;
|
|
|
+ margin: 0 !important;
|
|
|
+ padding: 0 !important;
|
|
|
+}
|
|
|
+
|
|
|
+.video-area {
|
|
|
+ flex: 1 !important;
|
|
|
+ min-width: 0 !important;
|
|
|
+ display: flex !important;
|
|
|
+ flex-direction: column !important;
|
|
|
+ position: relative !important;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-video-container {
|
|
|
+ flex: 1;
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: #0f172a;
|
|
|
+}
|
|
|
+
|
|
|
+/* 左上角摄像头名称 */
|
|
|
+.camera-title-overlay {
|
|
|
+ position: absolute !important;
|
|
|
+ top: 20px !important;
|
|
|
+ left: 20px !important;
|
|
|
+ z-index: 10001 !important;
|
|
|
+ background: rgba(0, 0, 0, 0.8) !important;
|
|
|
+ padding: 12px 20px !important;
|
|
|
+ border-radius: 6px !important;
|
|
|
+ backdrop-filter: blur(4px) !important;
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
|
|
+}
|
|
|
+
|
|
|
+.camera-title-text {
|
|
|
+ color: #ffffff;
|
|
|
+ font-size: 1.25rem;
|
|
|
+ font-weight: 600;
|
|
|
+ margin: 0;
|
|
|
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);
|
|
|
+ letter-spacing: 0.5px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 右上角关闭按钮 */
|
|
|
+.close-button-overlay {
|
|
|
+ position: absolute !important;
|
|
|
+ top: 20px !important;
|
|
|
+ right: 20px !important;
|
|
|
+ z-index: 10001 !important;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-video-preview {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: #0f172a;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-camera-icon {
|
|
|
+ width: 120px;
|
|
|
+ height: 120px;
|
|
|
+ color: #94a3b8;
|
|
|
+}
|
|
|
+
|
|
|
+.single-camera-fullscreen .control-panel {
|
|
|
+ width: 280px;
|
|
|
+ background: #1e293b;
|
|
|
+ border-left: 1px solid #334155;
|
|
|
+ padding: 1.5rem;
|
|
|
+ overflow-y: auto;
|
|
|
+ height: 100vh;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.control-sections {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 1.25rem;
|
|
|
+ flex: 1;
|
|
|
+ justify-content: flex-start;
|
|
|
+}
|
|
|
+
|
|
|
+.control-section {
|
|
|
+ background: transparent;
|
|
|
+}
|
|
|
+
|
|
|
+.control-title {
|
|
|
+ font-size: 0.875rem;
|
|
|
+ font-weight: 500;
|
|
|
+ margin-bottom: 0.75rem;
|
|
|
+ color: #f1f5f9;
|
|
|
+ text-align: left;
|
|
|
+}
|
|
|
+
|
|
|
+/* 云台控制按钮网格 */
|
|
|
+.ptz-controls {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ gap: 0.5rem;
|
|
|
+}
|
|
|
+
|
|
|
+.ptz-btn {
|
|
|
+ aspect-ratio: 1;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: #0f172a;
|
|
|
+ border: 1px solid #334155;
|
|
|
+ color: #94a3b8;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 1.25rem;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ cursor: pointer;
|
|
|
+ min-height: 44px;
|
|
|
+}
|
|
|
+
|
|
|
+.ptz-btn:hover {
|
|
|
+ background: #0ea5e9;
|
|
|
+ border-color: #0ea5e9;
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+
|
|
|
+.ptz-btn:active {
|
|
|
+ transform: scale(0.95);
|
|
|
+}
|
|
|
+
|
|
|
+/* 统一所有控制按钮的样式 */
|
|
|
+.zoom-btn, .playback-btn, .other-btn {
|
|
|
+ aspect-ratio: 1;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ min-height: 44px;
|
|
|
+}
|
|
|
+
|
|
|
+.zoom-btn svg, .playback-btn svg, .other-btn svg {
|
|
|
+ width: 16px !important;
|
|
|
+ height: 16px !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 变倍控制 */
|
|
|
+.zoom-controls {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
+ gap: 0.5rem;
|
|
|
+}
|
|
|
+
|
|
|
+/* 预置位按钮网格 */
|
|
|
+.preset-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
+ gap: 0.5rem;
|
|
|
+}
|
|
|
+
|
|
|
+.preset-btn {
|
|
|
+ padding: 0.75rem;
|
|
|
+ background: #0f172a;
|
|
|
+ border: 1px solid #334155;
|
|
|
+ color: #94a3b8;
|
|
|
+ border-radius: 8px;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ text-align: center;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 0.875rem;
|
|
|
+ min-height: 44px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.preset-btn:hover {
|
|
|
+ background: #0ea5e9;
|
|
|
+ border-color: #0ea5e9;
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+
|
|
|
+/* 回放控制 */
|
|
|
+.playback-controls {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
+ gap: 0.5rem;
|
|
|
+}
|
|
|
+
|
|
|
+/* 其他控制 */
|
|
|
+.other-controls {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
+ gap: 0.5rem;
|
|
|
+}
|
|
|
+
|
|
|
+/* 网格全屏模式 */
|
|
|
+.grid-fullscreen-mode {
|
|
|
+ position: fixed !important;
|
|
|
+ top: 0 !important;
|
|
|
+ left: 0 !important;
|
|
|
+ width: 100vw !important;
|
|
|
+ height: 100vh !important;
|
|
|
+ background-color: #0f172a !important;
|
|
|
+ z-index: 10000 !important;
|
|
|
+ display: flex !important;
|
|
|
+ flex-direction: column !important;
|
|
|
+ padding: 1rem !important;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 1rem 0;
|
|
|
+ border-bottom: 1px solid #334155;
|
|
|
+ margin-bottom: 1rem;
|
|
|
+ gap: 2rem;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: baseline;
|
|
|
+ gap: 1rem;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-title-text {
|
|
|
+ font-size: 1.5rem;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #f1f5f9;
|
|
|
+ margin: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-subtitle {
|
|
|
+ font-size: 0.875rem;
|
|
|
+ color: #94a3b8;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-close-btn {
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: rgba(15, 23, 42, 0.8);
|
|
|
+ border: 1px solid #334155;
|
|
|
+ border-radius: 6px;
|
|
|
+ color: #94a3b8;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-close-btn:hover {
|
|
|
+ background: #1e293b;
|
|
|
+ color: #f1f5f9;
|
|
|
+ border-color: #0ea5e9;
|
|
|
+}
|
|
|
+
|
|
|
+/* 全屏分页控制样式 */
|
|
|
+.fullscreen-pagination {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 1rem;
|
|
|
+ flex: 1;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-nav-btn {
|
|
|
+ width: 36px;
|
|
|
+ height: 36px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: rgba(15, 23, 42, 0.8);
|
|
|
+ border: 1px solid #334155;
|
|
|
+ border-radius: 6px;
|
|
|
+ color: #94a3b8;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-nav-btn:hover:not(:disabled) {
|
|
|
+ background: #1e293b;
|
|
|
+ color: #f1f5f9;
|
|
|
+ border-color: #0ea5e9;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-nav-btn:disabled {
|
|
|
+ opacity: 0.5;
|
|
|
+ cursor: not-allowed;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-page-indicator {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ gap: 0.5rem;
|
|
|
+ color: #94a3b8;
|
|
|
+ font-size: 0.875rem;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-page-dots {
|
|
|
+ display: flex;
|
|
|
+ gap: 0.5rem;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.page-dot {
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background-color: #334155;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.page-dot.active {
|
|
|
+ background-color: #0ea5e9;
|
|
|
+ transform: scale(1.2);
|
|
|
+}
|
|
|
+
|
|
|
+.page-dot:hover {
|
|
|
+ background-color: #64748b;
|
|
|
+}
|
|
|
+
|
|
|
+/* 全屏控制区域 */
|
|
|
+.fullscreen-controls {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 1rem;
|
|
|
+}
|
|
|
+
|
|
|
+/* 键盘快捷键提示 */
|
|
|
+.keyboard-hints {
|
|
|
+ display: flex;
|
|
|
+ gap: 1rem;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.hint-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 0.25rem;
|
|
|
+ font-size: 0.75rem;
|
|
|
+ color: #64748b;
|
|
|
+}
|
|
|
+
|
|
|
+.hint-item kbd {
|
|
|
+ background: #1e293b;
|
|
|
+ border: 1px solid #334155;
|
|
|
+ border-radius: 3px;
|
|
|
+ color: #94a3b8;
|
|
|
+ font-size: 0.75rem;
|
|
|
+ padding: 2px 6px;
|
|
|
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
|
+ font-family: monospace;
|
|
|
+ min-width: 20px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-video-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ grid-template-rows: repeat(2, 1fr);
|
|
|
+ gap: 1rem;
|
|
|
+ flex: 1;
|
|
|
+ height: calc(100vh - 120px);
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-video-card {
|
|
|
+ position: relative;
|
|
|
+ background: #1e293b;
|
|
|
+ border: 1px solid #334155;
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-video-card:hover {
|
|
|
+ border-color: #0ea5e9;
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-camera-btn {
|
|
|
+ position: absolute;
|
|
|
+ top: 12px;
|
|
|
+ right: 12px;
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
+ border-radius: 4px;
|
|
|
+ color: #94a3b8;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ z-index: 10;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-camera-btn:hover {
|
|
|
+ background: rgba(0, 0, 0, 0.7);
|
|
|
+ color: #f1f5f9;
|
|
|
+ border-color: #0ea5e9;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-video-preview {
|
|
|
+ flex: 1;
|
|
|
+ position: relative;
|
|
|
+ background: #0f172a;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-camera-placeholder {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-camera-icon {
|
|
|
+ width: 64px;
|
|
|
+ height: 64px;
|
|
|
+ color: #94a3b8;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-video-info {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent);
|
|
|
+ padding: 0.75rem;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: flex-end;
|
|
|
+ z-index: 5;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-video-details {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-video-name {
|
|
|
+ font-weight: 500;
|
|
|
+ margin-bottom: 0.25rem;
|
|
|
+ color: #f1f5f9;
|
|
|
+ font-size: 0.875rem;
|
|
|
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-video-location {
|
|
|
+ font-size: 0.75rem;
|
|
|
+ color: #e2e8f0;
|
|
|
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-video-status {
|
|
|
+ font-size: 0.75rem;
|
|
|
+ padding: 4px 8px;
|
|
|
+ border-radius: 12px;
|
|
|
+ font-weight: 500;
|
|
|
+ white-space: nowrap;
|
|
|
+ backdrop-filter: blur(4px);
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-video-status.online {
|
|
|
+ background-color: rgba(16, 185, 129, 0.2);
|
|
|
+ color: #10b981;
|
|
|
+ border: 1px solid rgba(16, 185, 129, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-video-status.offline {
|
|
|
+ background-color: rgba(239, 68, 68, 0.2);
|
|
|
+ color: #ef4444;
|
|
|
+ border: 1px solid rgba(239, 68, 68, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+/* 从网格全屏进入单摄像头全屏时的处理 */
|
|
|
+.grid-fullscreen-mode .single-camera-fullscreen {
|
|
|
+ position: fixed !important;
|
|
|
+ z-index: 10001 !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 网格全屏模式响应式设计 */
|
|
|
+@media (max-width: 1024px) {
|
|
|
+ .fullscreen-video-grid {
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
+ grid-template-rows: repeat(3, 1fr);
|
|
|
+ }
|
|
|
+
|
|
|
+ .fullscreen-camera-icon {
|
|
|
+ width: 48px;
|
|
|
+ height: 48px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .grid-fullscreen-mode {
|
|
|
+ padding: 0.5rem !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .fullscreen-header {
|
|
|
+ padding: 0.5rem 0;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 1rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .fullscreen-title-text {
|
|
|
+ font-size: 1.25rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .fullscreen-pagination {
|
|
|
+ order: 2;
|
|
|
+ gap: 0.5rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .fullscreen-controls {
|
|
|
+ order: 3;
|
|
|
+ gap: 0.5rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .keyboard-hints {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .fullscreen-video-grid {
|
|
|
+ grid-template-columns: repeat(1, 1fr);
|
|
|
+ grid-template-rows: repeat(6, 1fr);
|
|
|
+ gap: 0.5rem;
|
|
|
+ height: calc(100vh - 140px);
|
|
|
+ }
|
|
|
+
|
|
|
+ .fullscreen-camera-icon {
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .fullscreen-page-indicator {
|
|
|
+ font-size: 0.75rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .fullscreen-nav-btn {
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式设计 */
|
|
|
+@media (max-width: 1280px) {
|
|
|
+ .stats-container {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ gap: 1.5rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .category-stats {
|
|
|
+ grid-template-columns: repeat(4, 1fr);
|
|
|
+ }
|
|
|
+
|
|
|
+ .device-monitor-header {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: stretch;
|
|
|
+ gap: 1rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .device-monitor-title-section {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: stretch;
|
|
|
+ gap: 1rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .filter-buttons {
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .type-filters {
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .control-panel {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: stretch;
|
|
|
+ gap: 1rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .control-left {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 1rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .control-right {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: stretch;
|
|
|
+ gap: 1rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .form-group {
|
|
|
+ justify-content: space-between;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stats-container {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+
|
|
|
+ .overview-stats {
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ gap: 1rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .category-stats {
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
+ }
|
|
|
+
|
|
|
+ .video-grid {
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
+ }
|
|
|
+
|
|
|
+ .device-monitor-header {
|
|
|
+ gap: 0.75rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .device-monitor-title-section {
|
|
|
+ gap: 0.75rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .device-monitor-controls {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: stretch;
|
|
|
+ gap: 0.75rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .filter-btn {
|
|
|
+ padding: 0.375rem 0.75rem;
|
|
|
+ font-size: 0.8rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .device-pagination {
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .device-grid {
|
|
|
+ padding: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .device-metrics {
|
|
|
+ gap: 1rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-value {
|
|
|
+ font-size: 1.5rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .metric-value span {
|
|
|
+ font-size: 1rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .detail-btn {
|
|
|
+ width: 100%;
|
|
|
+ justify-content: center;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 历史数据模态框样式 */
|
|
|
+.history-modal-backdrop {
|
|
|
+ position: fixed !important;
|
|
|
+ top: 0 !important; /* 从页面顶部开始覆盖 */
|
|
|
+ left: 200px !important; /* 考虑左侧菜单栏宽度 */
|
|
|
+ right: 0 !important;
|
|
|
+ bottom: 0 !important;
|
|
|
+ background-color: rgba(15, 23, 42, 0.75) !important;
|
|
|
+ backdrop-filter: blur(4px) !important;
|
|
|
+ z-index: 1000 !important;
|
|
|
+ display: flex !important;
|
|
|
+ align-items: center !important;
|
|
|
+ justify-content: center !important;
|
|
|
+ padding: 1rem !important;
|
|
|
+ padding-top: 84px !important; /* 给顶部留出导航栏空间,但仍然覆盖 */
|
|
|
+ opacity: 1 !important;
|
|
|
+ transition: opacity 0.3s ease !important;
|
|
|
+ box-sizing: border-box !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 当侧边栏隐藏时的样式调整 */
|
|
|
+.hideSidebar .history-modal-backdrop {
|
|
|
+ left: 54px !important; /* 折叠后的侧边栏宽度 */
|
|
|
+}
|
|
|
+
|
|
|
+/* 移动端适配 */
|
|
|
+@media (max-width: 1024px) {
|
|
|
+ .history-modal-backdrop {
|
|
|
+ left: 0 !important;
|
|
|
+ padding-top: 50px !important;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.history-modal-content {
|
|
|
+ background-color: #0f172a !important;
|
|
|
+ border-radius: 8px !important;
|
|
|
+ width: 100% !important;
|
|
|
+ max-width: 1000px !important;
|
|
|
+ max-height: 90vh !important;
|
|
|
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04) !important;
|
|
|
+ border: 1px solid #334155 !important;
|
|
|
+ overflow: hidden !important;
|
|
|
+ display: flex !important;
|
|
|
+ flex-direction: column !important;
|
|
|
+}
|
|
|
+
|
|
|
+.history-modal-header {
|
|
|
+ display: flex !important;
|
|
|
+ justify-content: space-between !important;
|
|
|
+ align-items: center !important;
|
|
|
+ padding: 1rem 1.5rem !important;
|
|
|
+ border-bottom: 1px solid #334155 !important;
|
|
|
+ background: #1e293b !important;
|
|
|
+}
|
|
|
+
|
|
|
+.history-modal-title-section {
|
|
|
+ display: flex !important;
|
|
|
+ align-items: center !important;
|
|
|
+ gap: 0.75rem !important;
|
|
|
+}
|
|
|
+
|
|
|
+.history-modal-title {
|
|
|
+ font-size: 1.125rem !important;
|
|
|
+ font-weight: 500 !important;
|
|
|
+ color: #f1f5f9 !important;
|
|
|
+ margin: 0 !important;
|
|
|
+}
|
|
|
+
|
|
|
+.history-modal-close {
|
|
|
+ background: transparent !important;
|
|
|
+ border: none !important;
|
|
|
+ color: #94a3b8 !important;
|
|
|
+ cursor: pointer !important;
|
|
|
+ padding: 0.5rem !important;
|
|
|
+ transition: all 0.2s ease !important;
|
|
|
+ border-radius: 4px !important;
|
|
|
+ display: flex !important;
|
|
|
+ align-items: center !important;
|
|
|
+ justify-content: center !important;
|
|
|
+ width: 40px !important;
|
|
|
+ height: 40px !important;
|
|
|
+}
|
|
|
+
|
|
|
+.history-modal-close:hover {
|
|
|
+ color: #f1f5f9 !important;
|
|
|
+ background: rgba(255, 255, 255, 0.1) !important;
|
|
|
+}
|
|
|
+
|
|
|
+.history-modal-close svg {
|
|
|
+ width: 20px !important;
|
|
|
+ height: 20px !important;
|
|
|
+ stroke: currentColor !important;
|
|
|
+ fill: none !important;
|
|
|
+}
|
|
|
+
|
|
|
+.history-modal-controls {
|
|
|
+ display: flex !important;
|
|
|
+ justify-content: space-between !important;
|
|
|
+ align-items: center !important;
|
|
|
+ padding: 1rem 1.5rem !important;
|
|
|
+ border-bottom: 1px solid #334155 !important;
|
|
|
+ background: #1e293b !important;
|
|
|
+}
|
|
|
+
|
|
|
+.time-range-buttons {
|
|
|
+ display: flex !important;
|
|
|
+ gap: 0.5rem !important;
|
|
|
+}
|
|
|
+
|
|
|
+.indicator-buttons {
|
|
|
+ display: flex !important;
|
|
|
+ gap: 0.5rem !important;
|
|
|
+}
|
|
|
+
|
|
|
+.time-range-btn, .indicator-btn {
|
|
|
+ padding: 0.5rem 1rem !important;
|
|
|
+ border-radius: 6px !important;
|
|
|
+ font-size: 0.875rem !important;
|
|
|
+ color: #94a3b8 !important;
|
|
|
+ background: transparent !important;
|
|
|
+ border: 1px solid #334155 !important;
|
|
|
+ transition: all 0.2s ease !important;
|
|
|
+ cursor: pointer !important;
|
|
|
+}
|
|
|
+
|
|
|
+.time-range-btn:hover, .indicator-btn:hover {
|
|
|
+ background: rgba(255, 255, 255, 0.05) !important;
|
|
|
+ border-color: #0ea5e9 !important;
|
|
|
+}
|
|
|
+
|
|
|
+.time-range-btn.active, .indicator-btn.active {
|
|
|
+ background: #0ea5e9 !important;
|
|
|
+ border-color: #0ea5e9 !important;
|
|
|
+ color: white !important;
|
|
|
+}
|
|
|
+
|
|
|
+.history-modal-body {
|
|
|
+ flex: 1 !important;
|
|
|
+ padding: 1.5rem !important;
|
|
|
+ background: #0f172a !important;
|
|
|
+ overflow: hidden !important;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-container {
|
|
|
+ width: 100% !important;
|
|
|
+ height: 400px !important;
|
|
|
+ display: flex !important;
|
|
|
+ align-items: center !important;
|
|
|
+ justify-content: center !important;
|
|
|
+ background: transparent !important;
|
|
|
+ border-radius: 6px !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式设计 */
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .history-modal-content {
|
|
|
+ margin: 0.5rem !important;
|
|
|
+ max-width: calc(100vw - 1rem) !important;
|
|
|
+ max-height: calc(100vh - 1rem) !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .history-modal-controls {
|
|
|
+ flex-direction: column !important;
|
|
|
+ gap: 1rem !important;
|
|
|
+ align-items: stretch !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .time-range-buttons, .indicator-buttons {
|
|
|
+ flex-wrap: wrap !important;
|
|
|
+ justify-content: center !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .time-range-btn, .indicator-btn {
|
|
|
+ padding: 0.375rem 0.75rem !important;
|
|
|
+ font-size: 0.8rem !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart-container {
|
|
|
+ height: 300px !important;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|