Преглед на файлове

优化设备监控页面

yawuga преди 8 месеца
родител
ревизия
1c729e99a6
променени са 2 файла, в които са добавени 557 реда и са изтрити 91 реда
  1. 3 0
      public/index.html
  2. 554 91
      src/views/base/device/device-monitor.vue

+ 3 - 0
public/index.html

@@ -8,6 +8,9 @@
     <link rel="icon" href="<%= BASE_URL %>favicon.ico">
     <title><%= webpackConfig.name %></title>
     <!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
+    <!-- 视频播放器库 -->
+    <script src="https://cdn.jsdelivr.net/npm/hls.js@1.4.12/dist/hls.min.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/flv.js@1.6.2/dist/flv.min.js"></script>
     <!-- 高德地图API配置(由 vue-amap 统一管理) -->
 	  <style>
     html,

+ 554 - 91
src/views/base/device/device-monitor.vue

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