|
|
@@ -1,7 +1,6 @@
|
|
|
<template>
|
|
|
<div class="device-monitor-container">
|
|
|
|
|
|
-
|
|
|
<!-- 控制面板 -->
|
|
|
<div class="control-panel">
|
|
|
<div class="control-left">
|
|
|
@@ -72,14 +71,14 @@
|
|
|
<!-- 分类统计卡片 -->
|
|
|
<div class="category-stats">
|
|
|
<div class="stat-card">
|
|
|
- <div class="stat-number camera">12</div>
|
|
|
+ <div class="stat-number camera">{{ cameraList.length }}</div>
|
|
|
<div class="stat-label">摄像头</div>
|
|
|
- <div class="stat-detail">11在线 / 1离线</div>
|
|
|
+ <div class="stat-detail">{{ getOnlineCameraCount() }}在线 / {{ getOfflineCameraCount() }}离线</div>
|
|
|
</div>
|
|
|
<div class="stat-card">
|
|
|
- <div class="stat-number sensor">15</div>
|
|
|
+ <div class="stat-number sensor">{{ deviceList.length }}</div>
|
|
|
<div class="stat-label">传感器</div>
|
|
|
- <div class="stat-detail">14在线 / 1告警</div>
|
|
|
+ <div class="stat-detail">{{ getDeviceCountByStatus('online') }}在线 / {{ getDeviceCountByStatus('warning') }}告警</div>
|
|
|
</div>
|
|
|
<div class="stat-card">
|
|
|
<div class="stat-number weather">8</div>
|
|
|
@@ -107,20 +106,20 @@
|
|
|
<div class="video-header-left">
|
|
|
<h2 class="section-title">视频监控</h2>
|
|
|
<div class="video-count">
|
|
|
- 共 2 个摄像头
|
|
|
+ 共 {{ cameraList.length }} 个摄像头
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="video-header-right">
|
|
|
<div class="video-nav-controls">
|
|
|
- <button class="video-nav-btn" @click="prevPage" :disabled="currentPage === 1" title="上一页">
|
|
|
+ <button class="video-nav-btn" @click="prevPage" :disabled="pageIndex === 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>
|
|
|
+ <span>{{ getPageRange() }} / {{ cameraList.length }}</span>
|
|
|
</div>
|
|
|
- <button class="video-nav-btn" @click="nextPage" :disabled="currentPage === totalPages" title="下一页">
|
|
|
+ <button class="video-nav-btn" @click="nextPage" :disabled="pageIndex === totalCameraPages" 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>
|
|
|
@@ -134,8 +133,56 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <iframe style="width:600px;height: 400px;" src="http://121.4.16.100:28080/#/play/wasm/ws%3A%2F%2F121.4.16.100%3A6080%2Frtp%2F34020000001110000001_34020000001320000012.live.flv"></iframe>
|
|
|
- <iframe style="width:600px;height: 400px;" :src="iframeSrc"></iframe>
|
|
|
+ <!-- 视频卡片网格 -->
|
|
|
+ <div class="camera-grid">
|
|
|
+ <div
|
|
|
+ class="camera-card"
|
|
|
+ v-for="cam in pageCameras"
|
|
|
+ :key="cam.id"
|
|
|
+ >
|
|
|
+ <div class="card-header">
|
|
|
+ <span class="title">{{ cam.name }}</span>
|
|
|
+ <el-tag size="mini" :type="cam.status === '在线' ? 'success' : 'info'">
|
|
|
+ {{ cam.status || '离线' }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 预览区:优先使用 cam.previewUrl(iframe),否则渲染厂商 SDK 容器 cam.domId;若都没有,显示占位 -->
|
|
|
+ <div class="card-preview">
|
|
|
+ <iframe
|
|
|
+ v-if="cam.previewUrl && cam.status === '在线'"
|
|
|
+ class="player-iframe"
|
|
|
+ :src="cam.previewUrl"
|
|
|
+ frameborder="0"
|
|
|
+ allow="autoplay; encrypted-media"
|
|
|
+ allowfullscreen
|
|
|
+ ></iframe>
|
|
|
+ <div
|
|
|
+ v-else-if="cam.domId && cam.status === '在线'"
|
|
|
+ class="player-sdk"
|
|
|
+ :id="cam.domId"
|
|
|
+ ></div>
|
|
|
+ <!-- 离线状态显示 -->
|
|
|
+ <div v-else-if="cam.status === '离线'" class="player-offline">
|
|
|
+ <svg class="offline-camera-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
|
|
+ <path d="M23 7l-7 5 7 5V7z"/>
|
|
|
+ <rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
|
|
|
+ <line x1="1" y1="1" x2="23" y2="23"/>
|
|
|
+ </svg>
|
|
|
+ <span class="offline-text">设备离线</span>
|
|
|
+ </div>
|
|
|
+ <!-- 其他情况显示占位 -->
|
|
|
+ <div v-else class="player-empty">
|
|
|
+ <i class="el-icon-video-camera"></i>
|
|
|
+ <span>暂无预览</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="card-footer">
|
|
|
+ <span class="location">{{ cam.location || '-' }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
@@ -248,6 +295,12 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- 摄像头预览对话框 -->
|
|
|
+ <CameraPreview
|
|
|
+ v-model="previewVisible"
|
|
|
+ :camera="currentCamera"
|
|
|
+ />
|
|
|
+
|
|
|
<!-- 单个摄像头全屏模式 -->
|
|
|
<div v-if="singleCameraFullscreen.show" class="single-camera-fullscreen">
|
|
|
<div class="video-area">
|
|
|
@@ -364,7 +417,7 @@
|
|
|
<div class="fullscreen-header">
|
|
|
<div class="fullscreen-title">
|
|
|
<h2 class="fullscreen-title-text">视频监控</h2>
|
|
|
- <div class="fullscreen-subtitle">共 {{ videoList.length }} 个摄像头</div>
|
|
|
+ <div class="fullscreen-subtitle">共 {{ cameraList.length }} 个摄像头</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 分页控制 -->
|
|
|
@@ -375,7 +428,7 @@
|
|
|
</svg>
|
|
|
</button>
|
|
|
<div class="fullscreen-page-indicator">
|
|
|
- <span>{{ gridPageRange }} / {{ videoList.length }}</span>
|
|
|
+ <span>{{ gridPageRange }} / {{ cameraList.length }}</span>
|
|
|
<div class="fullscreen-page-dots">
|
|
|
<span
|
|
|
v-for="page in gridTotalPages"
|
|
|
@@ -416,8 +469,8 @@
|
|
|
</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="单独查看">
|
|
|
+ <div v-for="camera in gridPaginatedVideos" :key="camera.id" class="fullscreen-video-card">
|
|
|
+ <button class="fullscreen-camera-btn" @click="openVideoFullscreen(camera)" 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>
|
|
|
@@ -431,11 +484,11 @@
|
|
|
</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 class="fullscreen-video-name">{{ camera.name }}</div>
|
|
|
+ <div class="fullscreen-video-location">{{ camera.location }}</div>
|
|
|
</div>
|
|
|
- <span class="fullscreen-video-status" :class="video.status">
|
|
|
- {{ video.status === 'online' ? '在线' : '离线' }}
|
|
|
+ <span class="fullscreen-video-status" :class="camera.status === '在线' ? 'online' : 'offline'">
|
|
|
+ {{ camera.status === '在线' ? '在线' : '离线' }}
|
|
|
</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -506,8 +559,10 @@ export default {
|
|
|
timeRange: 'realtime',
|
|
|
autoRefresh: '30',
|
|
|
lastUpdateTime: '刚刚',
|
|
|
- currentPage: 1,
|
|
|
- videosPerPage: 3,
|
|
|
+ // 摄像头相关
|
|
|
+ cameraList: [],
|
|
|
+ pageIndex: 1,
|
|
|
+ pageSize: 3,
|
|
|
|
|
|
// 设备监控相关
|
|
|
deviceFilter: 'all',
|
|
|
@@ -517,6 +572,10 @@ export default {
|
|
|
devicePerPage: 8,
|
|
|
iframeSrc: '',
|
|
|
|
|
|
+ // 预览对话框状态
|
|
|
+ previewVisible: false,
|
|
|
+ currentCamera: null,
|
|
|
+
|
|
|
// 单个摄像头全屏模式
|
|
|
singleCameraFullscreen: {
|
|
|
show: false,
|
|
|
@@ -553,20 +612,6 @@ export default {
|
|
|
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' }
|
|
|
- ],
|
|
|
|
|
|
// 轮播控制状态
|
|
|
carouselStates: {},
|
|
|
@@ -740,29 +785,31 @@ export default {
|
|
|
},
|
|
|
|
|
|
computed: {
|
|
|
- totalPages() {
|
|
|
- return Math.ceil(this.videoList.length / this.videosPerPage)
|
|
|
+ // 摄像头分页
|
|
|
+ pageCameras() {
|
|
|
+ const start = (this.pageIndex - 1) * this.pageSize
|
|
|
+ const end = start + this.pageSize
|
|
|
+ return this.cameraList.slice(start, end)
|
|
|
},
|
|
|
- paginatedVideos() {
|
|
|
- const start = (this.currentPage - 1) * this.videosPerPage
|
|
|
- const end = start + this.videosPerPage
|
|
|
- return this.videoList.slice(start, end)
|
|
|
+ totalCameraPages() {
|
|
|
+ return Math.ceil(this.cameraList.length / this.pageSize)
|
|
|
},
|
|
|
-
|
|
|
+
|
|
|
// 网格全屏模式计算属性
|
|
|
gridTotalPages() {
|
|
|
- return Math.ceil(this.videoList.length / this.gridFullscreen.videosPerPage)
|
|
|
+ return Math.ceil(this.cameraList.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)
|
|
|
+ return this.cameraList.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)
|
|
|
+ const end = Math.min(this.gridFullscreen.currentPage * this.gridFullscreen.videosPerPage, this.cameraList.length)
|
|
|
return `${start}-${end}`
|
|
|
},
|
|
|
+
|
|
|
|
|
|
// 设备监控计算属性
|
|
|
filteredDevices() {
|
|
|
@@ -820,20 +867,26 @@ export default {
|
|
|
},
|
|
|
|
|
|
prevPage() {
|
|
|
- if (this.currentPage > 1) {
|
|
|
- this.currentPage--
|
|
|
+ if (this.pageIndex > 1) {
|
|
|
+ this.pageIndex--
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.initSdkPlayer()
|
|
|
+ })
|
|
|
}
|
|
|
},
|
|
|
|
|
|
nextPage() {
|
|
|
- if (this.currentPage < this.totalPages) {
|
|
|
- this.currentPage++
|
|
|
+ if (this.pageIndex < this.totalCameraPages) {
|
|
|
+ this.pageIndex++
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.initSdkPlayer()
|
|
|
+ })
|
|
|
}
|
|
|
},
|
|
|
|
|
|
getPageRange() {
|
|
|
- const start = (this.currentPage - 1) * this.videosPerPage + 1
|
|
|
- const end = Math.min(this.currentPage * this.videosPerPage, this.videoList.length)
|
|
|
+ const start = (this.pageIndex - 1) * this.pageSize + 1
|
|
|
+ const end = Math.min(this.pageIndex * this.pageSize, this.cameraList.length)
|
|
|
return `${start}-${end}`
|
|
|
},
|
|
|
|
|
|
@@ -860,15 +913,15 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
|
|
|
- openVideoFullscreen(video) {
|
|
|
- console.log('打开单个视频全屏:', video.name)
|
|
|
- console.log('Video data:', video)
|
|
|
+ openVideoFullscreen(camera) {
|
|
|
+ console.log('打开单个视频全屏:', camera.name)
|
|
|
+ console.log('Camera data:', camera)
|
|
|
// 如果是从网格全屏模式进入,先关闭网格全屏
|
|
|
if (this.gridFullscreen.show) {
|
|
|
this.gridFullscreen.show = false
|
|
|
}
|
|
|
this.singleCameraFullscreen.show = true
|
|
|
- this.singleCameraFullscreen.camera = video
|
|
|
+ this.singleCameraFullscreen.camera = camera
|
|
|
// 确保数据更新后再强制更新视图
|
|
|
this.$nextTick(() => {
|
|
|
console.log('Fullscreen camera:', this.singleCameraFullscreen.camera)
|
|
|
@@ -895,6 +948,179 @@ export default {
|
|
|
return this.deviceList.filter(device => device.status === status).length
|
|
|
},
|
|
|
|
|
|
+ // 摄像头统计方法
|
|
|
+ getOnlineCameraCount() {
|
|
|
+ return this.cameraList.filter(camera => camera.status === '在线').length
|
|
|
+ },
|
|
|
+
|
|
|
+ getOfflineCameraCount() {
|
|
|
+ return this.cameraList.filter(camera => camera.status === '离线').length
|
|
|
+ },
|
|
|
+
|
|
|
+ // 打开摄像头预览
|
|
|
+ openCameraPreview(camera) {
|
|
|
+ this.currentCamera = camera
|
|
|
+ this.previewVisible = true
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取摄像头列表
|
|
|
+ async fetchCameras() {
|
|
|
+ try {
|
|
|
+ // 这里调用真实接口
|
|
|
+ // const response = await getCameraList()
|
|
|
+ // this.cameraList = response.data.map(item => ({
|
|
|
+ // id: item.id,
|
|
|
+ // name: item.name,
|
|
|
+ // status: item.status === 1 ? '在线' : '离线',
|
|
|
+ // location: item.location,
|
|
|
+ // previewUrl: item.previewUrl,
|
|
|
+ // domId: item.domId
|
|
|
+ // }))
|
|
|
+
|
|
|
+ // 兜底 mock 数据
|
|
|
+ this.cameraList = [
|
|
|
+ {
|
|
|
+ id: 1,
|
|
|
+ name: '东区一号摄像头',
|
|
|
+ status: '在线',
|
|
|
+ location: '东区1号地块',
|
|
|
+ previewUrl: 'http://121.4.16.100:28080/#/play/wasm/' + encodeURIComponent('ws://121.4.16.100:6080/rtp/34020000001110000001_34020000001320000012.live.flv'),
|
|
|
+ domId: 'camera-1'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 2,
|
|
|
+ name: '东区2号摄像头',
|
|
|
+ status: '在线',
|
|
|
+ location: '东区2号地块',
|
|
|
+ previewUrl: 'http://121.4.16.100:28080/#/play/wasm/' + encodeURIComponent('ws://121.4.16.100:6080/rtp/34020000001320000001.live.flv'),
|
|
|
+ domId: 'camera-2'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 3,
|
|
|
+ name: '东区3号摄像头',
|
|
|
+ status: '离线',
|
|
|
+ location: '东区3号地块',
|
|
|
+ previewUrl: '',
|
|
|
+ domId: 'camera-3'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 4,
|
|
|
+ name: '西区1号摄像头',
|
|
|
+ status: '在线',
|
|
|
+ location: '西区1号地块',
|
|
|
+ previewUrl: '',
|
|
|
+ domId: 'camera-4'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 5,
|
|
|
+ name: '西区2号摄像头',
|
|
|
+ status: '在线',
|
|
|
+ location: '西区2号地块',
|
|
|
+ previewUrl: '',
|
|
|
+ domId: 'camera-5'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 6,
|
|
|
+ name: '西区3号摄像头',
|
|
|
+ status: '在线',
|
|
|
+ location: '西区3号地块',
|
|
|
+ previewUrl: '',
|
|
|
+ domId: 'camera-6'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 7,
|
|
|
+ name: '南区1号摄像头',
|
|
|
+ status: '在线',
|
|
|
+ location: '南区1号地块',
|
|
|
+ previewUrl: '',
|
|
|
+ domId: 'camera-7'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 8,
|
|
|
+ name: '南区2号摄像头',
|
|
|
+ status: '离线',
|
|
|
+ location: '南区2号地块',
|
|
|
+ previewUrl: '',
|
|
|
+ domId: 'camera-8'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 9,
|
|
|
+ name: '南区3号摄像头',
|
|
|
+ status: '在线',
|
|
|
+ location: '南区3号地块',
|
|
|
+ previewUrl: '',
|
|
|
+ domId: 'camera-9'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 10,
|
|
|
+ name: '北区1号摄像头',
|
|
|
+ status: '在线',
|
|
|
+ location: '北区1号地块',
|
|
|
+ previewUrl: '',
|
|
|
+ domId: 'camera-10'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 11,
|
|
|
+ name: '北区2号摄像头',
|
|
|
+ status: '在线',
|
|
|
+ location: '北区2号地块',
|
|
|
+ previewUrl: '',
|
|
|
+ domId: 'camera-11'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 12,
|
|
|
+ name: '北区3号摄像头',
|
|
|
+ status: '在线',
|
|
|
+ location: '北区3号地块',
|
|
|
+ previewUrl: '',
|
|
|
+ domId: 'camera-12'
|
|
|
+ }
|
|
|
+ ]
|
|
|
+
|
|
|
+ // 初始化当前页的 SDK 播放器
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.initSdkPlayer()
|
|
|
+ })
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取摄像头列表失败:', error)
|
|
|
+ // 接口超时兜底 mock
|
|
|
+ this.cameraList = [
|
|
|
+ { id: 1, name: '东区1号摄像头', status: '在线', location: '东区1号地块', previewUrl: '', domId: 'camera-1' },
|
|
|
+ { id: 2, name: '东区2号摄像头', status: '在线', location: '东区2号地块', previewUrl: '', domId: 'camera-2' },
|
|
|
+ { id: 3, name: '东区3号摄像头', status: '离线', location: '东区3号地块', previewUrl: '', domId: 'camera-3' }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 初始化 SDK 播放器
|
|
|
+ initSdkPlayer() {
|
|
|
+ this.pageCameras.forEach(cam => {
|
|
|
+ if (cam.domId && cam.status === '在线' && !cam.previewUrl) {
|
|
|
+ // 这里调用厂商 SDK 初始化逻辑
|
|
|
+ // 例如:initHikvisionPlayer(cam.domId, cam.streamUrl)
|
|
|
+ console.log(`初始化摄像头 ${cam.id} 的 SDK 播放器,容器ID: ${cam.domId}`)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ // 处理预览
|
|
|
+ handlePreview(cam) {
|
|
|
+ this.currentCamera = {
|
|
|
+ id: cam.id,
|
|
|
+ name: cam.name,
|
|
|
+ location: cam.location,
|
|
|
+ status: cam.status === '在线' ? 'online' : 'offline'
|
|
|
+ }
|
|
|
+ this.previewVisible = true
|
|
|
+ },
|
|
|
+
|
|
|
+ // 处理全屏
|
|
|
+ handleFullscreen(cam) {
|
|
|
+ // 简化实现:通过打开对话框实现
|
|
|
+ this.handlePreview(cam)
|
|
|
+ },
|
|
|
+
|
|
|
prevDevicePage() {
|
|
|
if (this.deviceCurrentPage > 1) {
|
|
|
this.deviceCurrentPage--
|
|
|
@@ -1278,6 +1504,9 @@ export default {
|
|
|
// 初始化轮播
|
|
|
this.initializeCarousels()
|
|
|
|
|
|
+ // 获取摄像头列表
|
|
|
+ this.fetchCameras()
|
|
|
+
|
|
|
// 监听键盘事件
|
|
|
document.addEventListener('keydown', (e) => {
|
|
|
if (e.key === 'Escape') {
|
|
|
@@ -1687,68 +1916,302 @@ export default {
|
|
|
padding: 0;
|
|
|
}
|
|
|
|
|
|
-/* 视频网格 */
|
|
|
-.video-grid {
|
|
|
+/* 摄像头网格 */
|
|
|
+.camera-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(3, 1fr);
|
|
|
- gap: 1rem;
|
|
|
+ grid-gap: 16px;
|
|
|
+ margin-top: 12px;
|
|
|
}
|
|
|
|
|
|
-.video-card {
|
|
|
- position: relative;
|
|
|
- background: white;
|
|
|
- border: 1px solid #e5e7eb;
|
|
|
+.camera-card {
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #eef1f5;
|
|
|
border-radius: 12px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0,0,0,.04);
|
|
|
overflow: hidden;
|
|
|
- transition: all 0.3s ease;
|
|
|
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
}
|
|
|
|
|
|
-.video-card:hover {
|
|
|
- border-color: #10b981;
|
|
|
- transform: translateY(-4px);
|
|
|
- box-shadow: 0 10px 20px -5px rgba(0, 0, 0, 0.15);
|
|
|
+.card-header {
|
|
|
+ padding: 12px 16px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+}
|
|
|
+
|
|
|
+.card-footer {
|
|
|
+ padding: 12px 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.card-header .title {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1f2937;
|
|
|
+}
|
|
|
+
|
|
|
+.card-preview {
|
|
|
+ height: 180px; /* 统一预览高度,避免卡片跳动 */
|
|
|
+ background: #f6f8fa;
|
|
|
+ position: relative;
|
|
|
}
|
|
|
|
|
|
-/* 摄像头全屏按钮 */
|
|
|
-.camera-fullscreen-btn {
|
|
|
+.player-iframe, .player-sdk {
|
|
|
position: absolute;
|
|
|
- top: 12px;
|
|
|
- right: 12px;
|
|
|
- width: 32px;
|
|
|
- height: 32px;
|
|
|
+ inset: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.player-empty {
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
- background: rgba(255, 255, 255, 0.9);
|
|
|
- border: 1px solid rgba(229, 231, 235, 0.8);
|
|
|
+ color: #8c9aa3;
|
|
|
+ gap: 6px;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.player-offline {
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: #f5f5f5;
|
|
|
+ color: #9ca3af;
|
|
|
+ gap: 8px;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.offline-camera-icon {
|
|
|
+ width: 48px;
|
|
|
+ height: 48px;
|
|
|
+ color: #9ca3af;
|
|
|
+ stroke-width: 1.5;
|
|
|
+}
|
|
|
+
|
|
|
+.offline-text {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #6b7280;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.card-footer .location {
|
|
|
+ color: #6b7280;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 1200px) {
|
|
|
+ .camera-grid {
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .camera-grid {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+/* CameraPreview 预览对话框样式 */
|
|
|
+.camera-preview-modal {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: rgba(0, 0, 0, 0.8);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: 10000;
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-dialog {
|
|
|
+ background: white;
|
|
|
+ border-radius: 16px;
|
|
|
+ width: 100%;
|
|
|
+ max-width: 1200px;
|
|
|
+ max-height: 90vh;
|
|
|
+ overflow: hidden;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.2);
|
|
|
+}
|
|
|
+
|
|
|
+.preview-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 20px 24px;
|
|
|
+ border-bottom: 1px solid #e5e7eb;
|
|
|
+ background: #f8fafc;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-title h3 {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1f2937;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-status {
|
|
|
+ font-size: 12px;
|
|
|
+ padding: 4px 8px;
|
|
|
+ border-radius: 12px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-status.online {
|
|
|
+ background-color: rgba(16, 185, 129, 0.15);
|
|
|
+ color: #059669;
|
|
|
+ border: 1px solid rgba(16, 185, 129, 0.4);
|
|
|
+}
|
|
|
+
|
|
|
+.preview-status.offline {
|
|
|
+ background-color: rgba(239, 68, 68, 0.15);
|
|
|
+ color: #dc2626;
|
|
|
+ border: 1px solid rgba(239, 68, 68, 0.4);
|
|
|
+}
|
|
|
+
|
|
|
+.close-btn {
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: transparent;
|
|
|
+ border: 1px solid #d1d5db;
|
|
|
border-radius: 8px;
|
|
|
color: #6b7280;
|
|
|
- transition: all 0.2s ease;
|
|
|
- z-index: 10;
|
|
|
cursor: pointer;
|
|
|
- backdrop-filter: blur(8px);
|
|
|
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
+ transition: all 0.2s ease;
|
|
|
}
|
|
|
|
|
|
-.camera-fullscreen-btn:hover {
|
|
|
- background: rgba(16, 185, 129, 0.8);
|
|
|
- color: white;
|
|
|
- border-color: #10b981;
|
|
|
+.close-btn:hover {
|
|
|
+ background: #f3f4f6;
|
|
|
+ border-color: #9ca3af;
|
|
|
+ color: #374151;
|
|
|
}
|
|
|
|
|
|
-.video-preview {
|
|
|
+.close-btn svg {
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-video-container {
|
|
|
+ flex: 1;
|
|
|
+ background: #000;
|
|
|
position: relative;
|
|
|
- padding-top: 56.25%; /* 16:9 aspect ratio */
|
|
|
- background: linear-gradient(135deg, #f0fdf4 0%, #f8fafc 100%);
|
|
|
+ min-height: 400px;
|
|
|
}
|
|
|
|
|
|
-.camera-placeholder {
|
|
|
- position: absolute;
|
|
|
- top: 0;
|
|
|
- left: 0;
|
|
|
+.preview-video {
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
+ object-fit: contain;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-offline {
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: #f9fafb;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-controls {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 16px 24px;
|
|
|
+ background: #f8fafc;
|
|
|
+ border-top: 1px solid #e5e7eb;
|
|
|
+}
|
|
|
+
|
|
|
+.controls-left,
|
|
|
+.controls-right {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.control-btn {
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: white;
|
|
|
+ border: 1px solid #d1d5db;
|
|
|
+ border-radius: 8px;
|
|
|
+ color: #6b7280;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.control-btn:hover {
|
|
|
+ background: #10b981;
|
|
|
+ border-color: #10b981;
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+
|
|
|
+.control-btn svg {
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.volume-control {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.volume-control svg {
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ color: #6b7280;
|
|
|
+}
|
|
|
+
|
|
|
+.volume-slider {
|
|
|
+ width: 100px;
|
|
|
+ height: 4px;
|
|
|
+ background: #e5e7eb;
|
|
|
+ border-radius: 2px;
|
|
|
+ outline: none;
|
|
|
+ appearance: none;
|
|
|
+}
|
|
|
+
|
|
|
+.volume-slider::-webkit-slider-thumb {
|
|
|
+ appearance: none;
|
|
|
+ width: 16px;
|
|
|
+ height: 16px;
|
|
|
+ background: #10b981;
|
|
|
+ border-radius: 50%;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.volume-slider::-moz-range-thumb {
|
|
|
+ width: 16px;
|
|
|
+ height: 16px;
|
|
|
+ background: #10b981;
|
|
|
+ border-radius: 50%;
|
|
|
+ cursor: pointer;
|
|
|
+ border: none;
|
|
|
}
|
|
|
|
|
|
.camera-icon {
|