yusheng 1 week ago
parent
commit
8d3da7a31e

+ 11 - 0
src/api/isp/ispRealtime/monitor/index.js

@@ -156,3 +156,14 @@ export async function getCameraInfo(params) {
   }
   return Promise.reject(new Error(res.data.message));
 }
+
+/**
+ * 分页查询摄像头信息
+ */
+export async function cameraPage(data) {
+  const res = await request.post(`/main/camera-info/pageCameraInfoList`, data);
+  if (res.data.code == 0 || res.data.code == 200) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}

+ 265 - 0
src/views/monitor/components/video-cell.vue

@@ -0,0 +1,265 @@
+<template>
+  <div
+    class="video-cell"
+    :class="{ active: isActive }"
+    @click="$emit('click', device)"
+  >
+    <div v-if="device" class="cell-header">
+      <span class="status-dot" :class="statusClass"></span>
+      <div class="cell-info">
+        <div class="cell-name">{{ device.name }}</div>
+        <div class="cell-code">{{ device.deviceCode || device.code }}</div>
+      </div>
+    </div>
+    <div v-else class="cell-empty">
+      <!-- 空占位 -->
+    </div>
+    <div :id="device.id" class="player-container"></div>
+    <div v-if="!playing && device" class="play-overlay">
+      <i class="el-icon-video-play play-btn"></i>
+    </div>
+  </div>
+</template>
+
+<script>
+  import Player from 'xgplayer';
+  import FlvPlugin from 'xgplayer-flv';
+  import 'xgplayer/dist/index.min.css'; // 引入播放器默认样式
+
+  export default {
+    name: 'VideoCell',
+    props: {
+      device: {
+        type: Object,
+        default: () => null
+      },
+      isActive: {
+        type: Boolean,
+        default: false
+      },
+      url: {
+        type: String,
+        default: ''
+      }
+    },
+    data() {
+      return {
+        player: null,
+        playing: false
+        // _id: 'vc_' + Math.random().toString(36).substr(2, 9)
+      };
+    },
+    computed: {
+      // playerId() {
+      //   console.log(this._id, 'this._id');
+      //   return this._id;
+      // },
+      statusClass() {
+        if (!this.device) return '';
+        return this.device.status === 1 || this.device.online === true
+          ? 'online'
+          : 'offline';
+      }
+    },
+    watch: {
+      url(val) {
+        console.log(val, 'val');
+        if (val && this.device) {
+          this.startPlay(val);
+        }
+      }
+    },
+    beforeDestroy() {
+      this.destroyPlayer();
+    },
+    methods: {
+      startPlay(url) {
+        const el = document.getElementById(this.device.id);
+        if (!el) return;
+        const rect = el.getBoundingClientRect();
+        if (!rect.width || !rect.height) {
+          this.$nextTick(() => this.startPlay(url));
+          return;
+        }
+        this.destroyPlayer();
+        try {
+          var config = {
+            id: this.device.id,
+            url: url,
+            width: rect.width,
+            height: rect.height,
+            autoplay: true,
+            autoplayMuted: true,
+            closeVideoClick: false,
+            closeInactive: true,
+            fitVideoSize: 'auto',
+            plugins: [FlvPlugin],
+            errorTips: '<span style="color:red">播放失败</span>',
+            playbackRate: [1],
+            defaultPlaybackRate: 1,
+            flv: {
+              retryCount: 2,
+              retryDelay: 1000,
+              loadTimeout: 10000,
+              preloadTime: 3
+            }
+          };
+          this.player = new Player(config);
+          console.log(this.player, ' this.player');
+          this.player.on('error', () => {
+            this.playing = false;
+          });
+          this.player.on('play', () => {
+            this.playing = true;
+          });
+          this.player.on('pause', () => {
+            this.playing = false;
+          });
+        } catch (e) {
+          console.error('player init error', e);
+          this.playing = false;
+        }
+      },
+      stopPlay() {
+        if (this.player) {
+          try {
+            this.player.pause();
+          } catch (e) {}
+        }
+        this.playing = false;
+      },
+      destroyPlayer() {
+        if (this.player) {
+          try {
+            this.player.destroy();
+          } catch (e) {}
+          this.player = null;
+        }
+        this.playing = false;
+      }
+    }
+  };
+</script>
+
+<style scoped lang="scss">
+  .video-cell {
+    position: relative;
+    width: 100%;
+    aspect-ratio: 16 / 9;
+    background-color: #0a1628;
+    border: 1px solid #1a2b44;
+    cursor: pointer;
+    overflow: hidden;
+    transition: border-color 0.2s;
+
+    &:hover {
+      border-color: #2c8ae0;
+    }
+
+    &.active {
+      border-color: #40c8ff;
+      box-shadow: 0 0 6px rgba(64, 200, 255, 0.3);
+    }
+  }
+
+  .cell-header {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    padding: 6px 10px;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    z-index: 10;
+    background: linear-gradient(to bottom, rgba(0, 0, 0, 0.65), transparent);
+  }
+
+  .status-dot {
+    width: 7px;
+    height: 7px;
+    border-radius: 50%;
+    flex-shrink: 0;
+
+    &.online {
+      background-color: #ff4d4f;
+      box-shadow: 0 0 4px #ff4d4f;
+    }
+
+    &.offline {
+      background-color: #666;
+    }
+  }
+
+  .cell-info {
+    text-align: right;
+    flex: 1;
+    min-width: 0;
+  }
+
+  .cell-name {
+    font-size: 12px;
+    color: #fff;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+
+  .cell-code {
+    font-size: 10px;
+    color: rgba(255, 255, 255, 0.55);
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+
+  .cell-empty {
+    width: 100%;
+    height: 100%;
+  }
+
+  .player-container {
+    width: 100% !important;
+    height: 100% !important;
+
+    ::v-deep .xgplayer-skin-default {
+      background: #000;
+    }
+
+    ::v-deep video {
+      // object-fit: cover !important;
+      width: 100%;
+      height: 100%;
+    }
+
+    ::v-deep .xgplayer-progress {
+      display: none !important;
+    }
+
+    ::v-deep .xgplayer-start {
+      display: none !important;
+    }
+
+    // ::v-deep .xgplayer-controls {
+    //   display: none !important;
+    // }
+  }
+
+  .play-overlay {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    z-index: 10;
+    pointer-events: none;
+  }
+
+  .play-btn {
+    font-size: 32px;
+    color: rgba(255, 255, 255, 0.75);
+
+    &::before {
+      content: '\e7c0';
+    }
+  }
+</style>

+ 47 - 263
src/views/monitor/index copy.vue

@@ -11,207 +11,14 @@
 
     <div class="center">
       <div class="monitor">
-        <el-carousel
-          ref="carouselRef"
-          indicator-position="outside"
-          style="height: calc(100% - 42px)"
-          :autoplay="false"
-          trigger="click"
-          @change="carouselChange"
-        >
-          <el-carousel-item
-            v-for="(page, pageIndex) in pageNum"
-            :key="pageIndex"
-          >
-            <div class="page">
-              <div
-                :style="itemStyle"
-                v-for="(video, videoIndex) in itemNum(pageIndex)"
-                :key="videoIndex"
-              >
-                <div
-                  v-if="pageIndex == carouselIndex"
-                  style="width: 100%; height: 100%"
-                  @click="
-                    deviceCode =
-                      videoList[pageIndex * layout * layout + videoIndex][
-                        'cameraCode'
-                      ]
-                  "
-                  :class="{
-                    active:
-                      deviceCode ==
-                      videoList[pageIndex * layout * layout + videoIndex][
-                        'cameraCode'
-                      ]
-                  }"
-                  class="box-border videoItem"
-                >
-                  <myVideo
-                    :video-url="
-                      videoList[pageIndex * layout * layout + videoIndex]['url']
-                    "
-                    :index="pageIndex * layout * layout + videoIndex"
-                  />
-                  <div class="mask1">
-                    <img
-                      @click.stop="
-                        handleScreenshot(
-                          pageIndex * layout * layout + videoIndex,
-                          $event
-                        )
-                      "
-                      :src="require('@/assets/svgs/centerctl/videoSur/camera.svg')"
-                    />
-                  </div>
-                  <div
-                    class="mask2"
-                    v-if="
-                      stopList.includes(
-                        pageIndex * layout * layout + videoIndex
-                      )
-                    "
-                  >
-                    <img
-                      class="play"
-                      @click.stop="
-                        play(pageIndex * layout * layout + videoIndex)
-                      "
-                      :src="require('@/assets/svgs/centerctl/videoSur/play.svg')"
-                      alt=""
-                    />
-                  </div>
-                </div>
-              </div>
-            </div>
-          </el-carousel-item>
-        </el-carousel>
-
-        <div class="center-handle">
-          <div
-            class="layout-type"
-            id="layout"
-            @click="getFocus"
-            @blur="blur"
-            tabindex="0"
-          >
-            <div style="width: 100%; height: 100%" @click="toggle" tabindex="0">
-              <div class="layout">
-                <div
-                  v-for="(item, index) in layout * layout"
-                  :key="index"
-                  :style="{
-                    width: `calc((100% - 1px * (${layout} - 1)) / ${layout})`,
-                    height: `calc((100% - 1px * (${layout} - 1)) / ${layout})`,
-                    backgroundColor: '#404446'
-                  }"
-                ></div>
-              </div>
-              <i class="el-icon-caret-bottom"></i>
-            </div>
+        <myVideo
+          :width="960"
+          :height="540"
+          show-reload-btn
+          @setLoading="setLoading"
+          ref="myVideoRef"
+        />
 
-            <div class="group" v-show="isShow">
-              <div
-                class="layout"
-                v-show="index + 1 != layout"
-                v-for="(item, index) in maxLayout"
-                :key="index"
-              >
-                <div
-                  @click="choose(index)"
-                  v-for="n in (index + 1) * (index + 1)"
-                  :key="n"
-                  :style="{
-                    width: `calc((100% - 1px * (${index + 1} - 1)) / ${
-                      index + 1
-                    })`,
-                    height: `calc((100% - 1px * (${index + 1} - 1)) / ${
-                      index + 1
-                    })`,
-                    backgroundColor: '#404446'
-                  }"
-                ></div>
-              </div>
-            </div>
-          </div>
-          <div class="play">
-            <el-tooltip
-              class="item"
-              effect="dark"
-              content="第一个"
-              placement="top"
-            >
-              <img
-                @click="home"
-                :src="require('@/assets/svgs/centerctl/videoSur/last.svg')"
-                alt=""
-              />
-            </el-tooltip>
-            <el-tooltip
-              class="item"
-              effect="dark"
-              content="上一个"
-              placement="top"
-            >
-              <img
-                @click="last"
-                :src="
-                  require('@/assets/svgs/centerctl/videoSur/fast-backward.svg')
-                "
-                alt=""
-              />
-            </el-tooltip>
-            <el-tooltip
-              class="item"
-              effect="dark"
-              content="播放/暂停"
-              placement="top"
-            >
-              <img
-                @click="playStop"
-                :src="require('@/assets/svgs/centerctl/videoSur/play-stop.svg')"
-                alt=""
-              />
-            </el-tooltip>
-            <el-tooltip
-              class="item"
-              effect="dark"
-              content="下一个"
-              placement="top"
-            >
-              <img
-                @click="next"
-                :src="
-                  require('@/assets/svgs/centerctl/videoSur/fast-forward.svg')
-                "
-                alt=""
-              />
-            </el-tooltip>
-            <el-tooltip
-              class="item"
-              effect="dark"
-              content="最后一个"
-              placement="top"
-            >
-              <img
-                @click="end"
-                :src="require('@/assets/svgs/centerctl/videoSur/next.svg')"
-                alt=""
-              />
-            </el-tooltip>
-          </div>
-
-          <div class="video-handle">
-            <img
-              :src="require('@/assets/svgs/centerctl/videoSur/volume-x.svg')"
-              alt=""
-            />
-            <img
-              :src="require('@/assets/svgs/centerctl/videoSur/big.svg')"
-              alt=""
-            />
-          </div>
-        </div>
       </div>
     </div>
     <div style="height: 100%; padding: 0">
@@ -332,7 +139,7 @@
   import myVideo from '../components/video.vue';
   import * as realTime from '@/api/isp/ispRealtime/monitor/index';
   import * as deviceApi from '@/api/isp/deviceManage/robot';
-
+  // import FLVPlayer from "./FLVPlayer.vue";
   export default {
     name: 'MonitorPage',
     components: {
@@ -344,6 +151,7 @@
     data() {
       return {
         layout: 1,
+        loading: false,
         videoList: [],
         carouselIndex: 0,
         isShow: false,
@@ -357,6 +165,11 @@
         monitorData: []
       };
     },
+    beforeDestroy() {
+      if (this.deviceCode) {
+        realTime.delStreamProxy(this.deviceCode);
+      }
+    },
     computed: {
       maxLayout() {
         if (this.videoList.length > 8) {
@@ -385,11 +198,17 @@
     },
     watch: {
       deviceCode(val, oldVal) {
+        if (oldVal) {
+          realTime.delStreamProxy(oldVal);
+        }
         this.getPreset();
       }
     },
     mounted() {},
     methods: {
+      setLoading(loading) {
+        this.loading = loading;
+      },
       itemNum(pageIndex) {
         if (this.pageNum != 1 && pageIndex == this.pageNum - 1) {
           return this.count - this.layout * this.layout * (this.pageNum - 1);
@@ -529,25 +348,28 @@
         }
       },
       playStop() {
-        let index = Math.floor(
-          this.videoList.findIndex(
-            (item) => item.cameraCode == this.deviceCode
-          ) /
-            (this.layout * this.layout)
-        );
-        let videoElement = document.getElementById('video' + index);
-        if (videoElement && videoElement.paused) {
-          videoElement.play();
-          this.stopList = this.stopList.filter((item) => item != index);
-        } else {
-          if (videoElement) videoElement.pause();
-          this.stopList.push(index);
-        }
+        this.$refs.myVideoRef.playStop();
+        // let index = Math.floor(
+        //   this.videoList.findIndex(
+        //     (item) => item.cameraCode == this.deviceCode
+        //   ) /
+        //     (this.layout * this.layout)
+        // );
+        // let videoElement = document.getElementById('video' + index);
+        // if (videoElement && videoElement.paused) {
+        //   videoElement.play();
+        //   this.stopList = this.stopList.filter((item) => item != index);
+        // } else {
+        //   if (videoElement) videoElement.pause();
+        //   this.stopList.push(index);
+        // }
       },
       play(index) {
-        let videoElement = document.getElementById('video' + index);
-        if (videoElement) videoElement.play();
-        this.stopList = this.stopList.filter((item) => item != index);
+        this.$refs.myVideoRef.play();
+
+        // let videoElement = document.getElementById('video' + index);
+        // if (videoElement) videoElement.play();
+        // this.stopList = this.stopList.filter((item) => item != index);
       },
       async getCameraList(list) {
         this.monitorData = list;
@@ -555,7 +377,7 @@
           this.codeList.push(item.deviceCode);
         });
 
-        this.getClickId(list[0]);
+        // this.getClickId(list[0]);
       },
       async getClickId(device) {
         this.camera = device;
@@ -578,7 +400,9 @@
       async getCameraUrl() {
         await realTime.delStreamProxy(this.deviceCode);
         const res = await realTime.getCameraUrl([this.deviceCode]);
-        this.videoList = res;
+        this.$refs.myVideoRef.startPlay(res[0].url);
+        // this.videoList = res;
+        console.log(this.videoList);
       }
     }
   };
@@ -725,49 +549,7 @@
       height: calc(100% - 27px);
     }
   }
-  .page {
-    display: flex;
-    align-content: space-between;
-    flex-wrap: wrap;
-    gap: 5px;
-    height: 100%;
-    .videoItem {
-      border: 4px solid transparent;
-      position: relative;
-      .mask1 {
-        position: absolute;
-        top: 0;
-        left: 0;
-        width: 50px;
-        height: 20px;
-        z-index: 111;
-      }
-      .mask2 {
-        position: absolute;
-        top: 50%;
-        left: 50%;
-        transform: translate(-50%, -50%);
-        width: 50px;
-        height: 50px;
-        z-index: 111;
-        background-color: #000000d3;
-        border-radius: 50%;
-        display: flex;
-        justify-content: center;
-        align-items: center;
-        .play {
-          width: 70%;
-          transform: translateX(3px);
-        }
-      }
-    }
-    .active {
-      border: 4px solid #2c8ae0;
-    }
-    img {
-      vertical-align: middle;
-    }
-  }
+
 
   .deviceInfo,
   .deviceInfo * {
@@ -925,6 +707,8 @@
 
     .play {
       display: flex;
+      justify-content: center;
+      width: 100%;
       align-items: flex-start;
       gap: 24px;
     }

+ 639 - 673
src/views/monitor/index.vue

@@ -1,746 +1,712 @@
 <template>
-  <div class="inspection">
-    <div class="left">
-      <div class="tree">
-        <monitorList
-          @getClickId="getClickId"
-          @getCameraList="getCameraList"
-        ></monitorList>
+  <div class="monitor-page" v-loading="pageLoading">
+    <!-- 视频网格区域 -->
+    <div
+      class="grid-area"
+      ref="gridArea"
+      @scroll="handleScroll"
+    >
+      <!-- 视频网格 -->
+      <div
+        class="video-grid"
+        :style="gridStyle"
+      >
+        <VideoCell
+          v-for="(item, index) in deviceList"
+          :key="item.deviceCode || item.code || index"
+          ref="cells"
+          :device="item"
+          :isActive="selectedIndex === index"
+          :url="playingUrls[item.deviceCode || item.code] || ''"
+          @click="onCellClick(item, index)"
+        />
+      </div>
+
+      <!-- 加载提示 -->
+      <div class="load-tip" v-if="loadingMore">
+        <i class="el-icon-loading"></i> 加载中...
+      </div>
+      <div class="load-tip" v-else-if="!hasMore && deviceList.length">
+        没有更多设备了
+      </div>
+      <div class="load-tip" v-else-if="!deviceList.length && !pageLoading">
+        暂无设备
       </div>
     </div>
 
-    <div class="center">
-      <div class="monitor">
-        <myVideo
-          :width="960"
-          :height="540"
-          show-reload-btn
-          @setLoading="setLoading"
-          ref="myVideoRef"
-        />
+    <!-- 底部工具栏 -->
+    <div class="toolbar">
+      <div class="toolbar-left">
+        <div class="layout-group">
+          <div
+            v-for="size in layoutOptions"
+            :key="size"
+            class="layout-btn"
+            :class="{ active: gridSize === size }"
+            :title="size + '分屏'"
+            @click="changeLayout(size)"
+          >
+            <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
+              <rect v-for="r in getLayoutRects(size)" :key="r.i" :x="r.x" :y="r.y" :width="r.w" :height="r.h" rx="0.5" fill="currentColor"/>
+            </svg>
+          </div>
+        </div>
+      </div>
+
+      <div class="toolbar-center">
+        <el-button type="text" icon="el-icon-video-play" class="tool-btn" @click="batchPlay">批量播放</el-button>
+        <el-button type="text" icon="el-icon-video-pause" class="tool-btn" @click="batchStop">取消拉流</el-button>
+      </div>
 
+      <div class="toolbar-right">
+        <el-button type="text" icon="el-icon-rank" class="tool-btn" @click="togglePanel">
+          {{ panelVisible ? '隐藏控制台' : '控制台' }}
+        </el-button>
+        <el-button type="text" icon="el-icon-full-screen" class="tool-btn" @click="toggleFullscreen"></el-button>
       </div>
     </div>
-    <div style="height: 100%; padding: 0">
-      <div class="deviceInfo">
-        <div class="frame-48095487">
-          <div class="div">设备信息</div>
-        </div>
-        <div class="row">
-          <div class="div">摄像机名称</div>
-          <div class="name">{{ camera.name }}</div>
-        </div>
-        <div class="row">
-          <div class="div">摄像机编号</div>
-          <div class="div">{{ camera.deviceCode }}</div>
-        </div>
-        <div class="row">
-          <div class="div">设备品牌</div>
-          <div class="div">{{ camera.brandName }}</div>
+
+    <!-- 右侧控制面板(默认隐藏) -->
+    <transition name="slide-panel">
+      <div v-if="panelVisible && selectedDevice" class="control-panel">
+        <div class="panel-header">
+          <span>设备控制</span>
+          <i class="el-icon-close panel-close" @click="panelVisible = false"></i>
         </div>
-        <div class="row">
-          <div class="div">设备型号</div>
-          <div class="div">{{ camera.modelName }}</div>
+
+        <!-- 设备信息 -->
+        <div class="panel-section">
+          <div class="section-title">设备信息</div>
+          <div class="info-row">
+            <label>摄像机名称</label>
+            <div>{{ selectedDevice.name }}</div>
+          </div>
+          <div class="info-row">
+            <label>摄像机编号</label>
+            <div>{{ selectedDevice.deviceCode || selectedDevice.code }}</div>
+          </div>
+          <div class="info-row" v-if="selectedDevice.brandName">
+            <label>设备品牌</label>
+            <div>{{ selectedDevice.brandName }}</div>
+          </div>
+          <div class="info-row" v-if="selectedDevice.modelName">
+            <label>设备型号</label>
+            <div>{{ selectedDevice.modelName }}</div>
+          </div>
         </div>
-      </div>
 
-      <div class="handle">
-        <direction @handle="handle" />
-
-        <div class="slider">
-          <el-slider
-            v-model="slider"
-            :min="1"
-            :max="7"
-            style="width: 154px"
-          ></el-slider>
-          <div class="sliderNum">{{ slider }}</div>
+        <!-- 云台方向控制 -->
+        <div class="panel-section">
+          <div class="section-title">云台控制</div>
+          <direction @handle="ptzHandle" />
+          <div class="speed-slider">
+            <span>速度</span>
+            <el-slider v-model="slider" :min="1" :max="7" style="flex:1; margin: 0 10px;" />
+            <span class="speed-val">{{ slider }}</span>
+          </div>
         </div>
 
-        <div class="moreHandle">
-          <div class="row">
-            <div class="item normal">
-              <el-tooltip effect="dark" content="开关灯" placement="top">
-                <img
-                  :src="
-                    require('@/assets/svgs/isp/ispRealTime/video/light.svg')
-                  "
-                  alt=""
-                  @click="switchLamp(camera.deviceCode)"
-                />
-              </el-tooltip>
-            </div>
-            <div class="item">
-              <img
-                :src="require('@/assets/svgs/isp/ispRealTime/video/yushua.svg')"
-                alt=""
-              />
-            </div>
-            <div class="item">
-              <img
-                :src="require('@/assets/svgs/isp/ispRealTime/video/jvjiao.svg')"
-                alt=""
-              />
-            </div>
-            <div class="item">
-              <img
-                :src="
-                  require('@/assets/svgs/isp/ispRealTime/video/chushihua.svg')
-                "
-                alt=""
-              />
-            </div>
-            <div class="item">
+        <!-- 快捷操作 -->
+        <div class="panel-section">
+          <div class="section-title">快捷操作</div>
+          <div class="quick-actions">
+            <el-tooltip content="开关灯" placement="top">
               <img
-                :src="require('@/assets/svgs/isp/ispRealTime/video/menu.svg')"
+                :src="require('@/assets/svgs/isp/ispRealTime/video/light.svg')"
                 alt=""
+                @click="switchLamp(selectedDevice.deviceCode)"
+                class="action-icon"
               />
-            </div>
-          </div>
-
-          <div class="line"></div>
-
-          <div class="row">
-            <div class="item normal" v-loading="screenshotVal">
-              <el-tooltip effect="dark" content="截图" placement="top">
-                <img
-                  @click="screenshot(camera.deviceCode)"
-                  :src="require('@/assets/svgs/isp/ispRealTime/video/cw.svg')"
-                  alt=""
-                />
-              </el-tooltip>
-            </div>
-            <div class="item">
+            </el-tooltip>
+            <el-tooltip content="截图" placement="top">
               <img
-                :src="require('@/assets/svgs/isp/ispRealTime/video/3d.svg')"
+                v-loading="screenshotVal"
+                :src="require('@/assets/svgs/isp/ispRealTime/video/cw.svg')"
                 alt=""
+                @click="screenshot(selectedDevice.deviceCode)"
+                class="action-icon"
               />
-            </div>
-            <div class="item"></div>
-            <div class="item"></div>
-            <div class="item"></div>
+            </el-tooltip>
           </div>
         </div>
 
-        <PresetPositions
-          :PresetList="PresetList"
-          :deviceCode="deviceCode"
-          @getPreset="getPreset"
-        ></PresetPositions>
+        <!-- 预置点 -->
+        <!-- <div class="panel-section preset-section">
+          <PresetPositions
+            :PresetList="PresetList"
+            :deviceCode="selectedDevice.deviceCode || selectedDevice.code"
+            :boxHeight="presetHeight"
+            @getPreset="getPreset"
+          />
+        </div> -->
       </div>
-    </div>
+    </transition>
+
+    <!-- 遮罩(点击关闭面板,滚轮穿透到网格区域) -->
+    <!-- <div v-if="panelVisible" class="panel-mask" @click="panelVisible = false" @wheel.prevent="onMaskWheel"></div> -->
   </div>
 </template>
 
 <script>
-  import PresetPositions from './components/Preset-positions.vue';
-  import monitorList from './components/monitorList.vue';
-  import direction from './components/direction.vue';
-  import myVideo from '../components/video.vue';
-  import * as realTime from '@/api/isp/ispRealtime/monitor/index';
-  import * as deviceApi from '@/api/isp/deviceManage/robot';
-  // import FLVPlayer from "./FLVPlayer.vue";
-  export default {
-    name: 'MonitorPage',
-    components: {
-      PresetPositions,
-      monitorList,
-      direction,
-      myVideo
+import VideoCell from './components/video-cell.vue';
+import direction from './components/direction.vue';
+import PresetPositions from './components/Preset-positions.vue';
+import * as realTime from '@/api/isp/ispRealtime/monitor/index';
+import * as deviceApi from '@/api/isp/deviceManage/robot';
+
+export default {
+  name: 'MonitorPage',
+  components: { VideoCell, direction, PresetPositions },
+  data() {
+    return {
+      layoutOptions: [9, 16, 25, 36],
+      gridSize: 9,
+      deviceList: [],
+      dataPageNum: 1,
+      hasMore: true,
+      loadingMore: false,
+      pageLoading: false,
+      playingUrls: {},
+      selectedIndex: -1,
+      panelVisible: false,
+      selectedDevice: null,
+      slider: 1,
+      screenshotVal: false,
+      PresetList: [],
+      presetHeight: 200
+    };
+  },
+  computed: {
+    gridCols() {
+      return Math.sqrt(this.gridSize);
     },
-    data() {
+    gridStyle() {
       return {
-        layout: 1,
-        loading: false,
-        videoList: [],
-        carouselIndex: 0,
-        isShow: false,
-        deviceCode: undefined,
-        camera: {},
-        stopList: [],
-        slider: 1,
-        screenshotVal: false,
-        PresetList: [],
-        codeList: [],
-        monitorData: []
+        gridTemplateColumns: `repeat(${this.gridCols}, 1fr)`
       };
+    }
+  },
+  watch: {
+    gridSize() {
+      this.loadCameraData();
     },
-    beforeDestroy() {
-      if (this.deviceCode) {
-        realTime.delStreamProxy(this.deviceCode);
+    panelVisible(val) {
+      if (val) {
+        this.$nextTick(() => {
+          const panel = document.querySelector('.control-panel');
+          if (panel) {
+            this.presetHeight = Math.max(200, panel.clientHeight - 420);
+          }
+        });
       }
+    }
+  },
+  mounted() {
+    this.loadCameraData();
+  },
+  beforeDestroy() {
+    this.batchStop();
+  },
+  methods: {
+    // ========== 布局相关 ==========
+    changeLayout(size) {
+      this.gridSize = size;
     },
-    computed: {
-      maxLayout() {
-        if (this.videoList.length > 8) {
-          return 4;
-        } else if (this.videoList.length > 3) {
-          return 3;
-        } else if (this.videoList.length > 1) {
-          return 2;
-        } else {
-          return 1;
+    getLayoutRects(size) {
+      const n = Math.sqrt(size);
+      const gap = 2;
+      const cell = (24 - gap * (n - 1)) / n;
+      const rects = [];
+      for (let r = 0; r < n; r++) {
+        for (let c = 0; c < n; c++) {
+          rects.push({
+            i: r * n + c,
+            x: c * (cell + gap),
+            y: r * (cell + gap),
+            w: cell,
+            h: cell
+          });
         }
-      },
-      count() {
-        return this.videoList.length;
-      },
-      pageNum() {
-        let num = this.count / (this.layout * this.layout);
-        return Math.ceil(num);
-      },
-      itemStyle() {
-        return {
-          width: `calc((100% - 10px * (${this.layout} - 1)) / ${this.layout})`,
-          height: `calc((100% - 10px * (${this.layout} - 1)) / ${this.layout})`
-        };
       }
+      return rects;
     },
-    watch: {
-      deviceCode(val, oldVal) {
-        if (oldVal) {
-          realTime.delStreamProxy(oldVal);
-        }
-        this.getPreset();
+
+    // ========== 数据加载(滚动分页) ==========
+    async loadCameraData(more = false) {
+      if (this.loadingMore) return;
+      if (more && !this.hasMore) return;
+
+      this.loadingMore = true;
+      if (!more) {
+        this.pageLoading = true;
+        this.deviceList = [];
+        this.dataPageNum = 1;
+        this.hasMore = true;
+        this.playingUrls = {};
+        this.selectedIndex = -1;
+        this.selectedDevice = null;
+      }
+
+      try {
+        const res = await realTime.cameraPage({
+          size: this.gridSize,
+          pageNum: this.dataPageNum
+        });
+        const list = Array.isArray(res.records) ? res.records : [];
+        const total = res.total || 0;
+        this.deviceList = more ? this.deviceList.concat(list) : list;
+        this.dataPageNum++;
+        this.hasMore = this.deviceList.length < total;
+      } catch (e) {
+        console.error('loadCameraData error', e);
+        this.$message.error('获取设备列表失败');
+        if (!more) this.deviceList = [];
+      } finally {
+        this.loadingMore = false;
+        this.pageLoading = false;
       }
     },
-    mounted() {},
-    methods: {
-      setLoading(loading) {
-        this.loading = loading;
-      },
-      itemNum(pageIndex) {
-        if (this.pageNum != 1 && pageIndex == this.pageNum - 1) {
-          return this.count - this.layout * this.layout * (this.pageNum - 1);
-        } else if (this.pageNum == 1) {
-          return this.count;
-        } else {
-          return this.layout * this.layout;
-        }
-      },
-      carouselChange(index) {
-        this.carouselIndex = index;
-        if (this.layout == 1) this.getClickId(this.monitorData[index]);
-      },
-      toggle(e) {
-        this.isShow = !this.isShow;
-      },
-      getFocus() {
-        const layout = document.getElementById('layout');
-        if (layout) layout.focus();
-      },
-      blur() {
-        this.isShow = false;
-      },
-      choose(index) {
-        this.layout = index + 1;
-        this.isShow = false;
-      },
-      home() {
-        this.carouselIndex = 0;
-        this.$refs.carouselRef && this.$refs.carouselRef.setActiveItem(0);
-      },
-      last() {
-        if (this.carouselIndex > 0) {
-          this.carouselIndex--;
-          this.$refs.carouselRef &&
-            this.$refs.carouselRef.setActiveItem(this.carouselIndex);
-        } else {
-          this.carouselIndex = this.pageNum - 1;
-          this.$refs.carouselRef &&
-            this.$refs.carouselRef.setActiveItem(this.pageNum - 1);
-        }
-      },
-      next() {
-        if (this.carouselIndex < this.pageNum - 1) {
-          this.carouselIndex++;
-          this.$refs.carouselRef &&
-            this.$refs.carouselRef.setActiveItem(this.carouselIndex);
-        } else {
-          this.carouselIndex = 0;
-          this.$refs.carouselRef && this.$refs.carouselRef.setActiveItem(0);
-        }
-      },
-      end() {
-        this.carouselIndex = this.pageNum - 1;
-        this.$refs.carouselRef &&
-          this.$refs.carouselRef.setActiveItem(this.pageNum - 1);
-      },
-      handleScreenshot(index, e) {
-        const videoPlayback = document.getElementById('video' + index);
-        if (!videoPlayback) return;
-        const canvas = document.createElement('canvas');
-        canvas.width = videoPlayback.videoWidth;
-        canvas.height = videoPlayback.videoHeight;
-        const ctx = canvas.getContext('2d');
-        if (ctx) {
-          ctx.drawImage(videoPlayback, 0, 0, canvas.width, canvas.height);
+
+    // ========== 滚动加载更多 ==========
+    handleScroll(e) {
+      const el = e.target;
+      const threshold = 100;
+      if (el.scrollHeight - el.scrollTop - el.clientHeight < threshold) {
+        this.loadCameraData(true);
+      }
+    },
+
+    // ========== 点击视频格 ==========
+    async onCellClick(device, index) {
+      this.selectedIndex = index;
+      this.selectedDevice = device;
+      await this.playDevice(device);
+      if (!this.panelVisible) {
+        this.panelVisible = true;
+      }
+      this.loadPreset(device);
+    },
+    async playDevice(device) {
+      const code = device.deviceCode || device.code;
+      if (!code) return;
+      try {
+        await realTime.delStreamProxy(code).catch(() => {});
+        const urls = await realTime.getCameraUrl([code]);
+        if (urls && urls[0] && urls[0].url) {
+          this.$set(this.playingUrls, code, urls[0].url);
+          console.log(this.playingUrls,'this.playingUrls')
         }
-        const img = new Image();
-        img.src = canvas.toDataURL('image/png');
-        const a = document.createElement('a');
-        a.href = img.src;
-        a.download = 'screenshot.png';
-        document.body.appendChild(a);
-        a.click();
-        document.body.removeChild(a);
-        e.stopPropagation();
-      },
-      async handle(SDK) {
-        if (SDK == 'reset') {
-          let params = {
-            cameraCode: this.deviceCode,
-            wPresetNum: 34,
-            command: 'GOTO_PRESET'
-          };
-          const res = await realTime.oprPreset(params);
-          if (res == 'SUCCESS') {
-            this.$message.success('操作成功');
-          } else {
-            this.$message.error(res);
-          }
-          return;
+      } catch (e) {
+        console.error('playDevice error', e);
+        this.$message.error('播放失败');
+      }
+    },
+
+    // ========== 批量操作 ==========
+    async batchPlay() {
+      const list = this.deviceList.filter(Boolean);
+      if (!list.length) {
+        this.$message.warning('没有可播放的设备');
+        return;
+      }
+      for (const item of list) {
+        const code = item.deviceCode || item.code;
+        if (code && !this.playingUrls[code]) {
+          try {
+            await realTime.delStreamProxy(code).catch(() => {});
+            const urls = await realTime.getCameraUrl([code]);
+            if (urls && urls[0] && urls[0].url) {
+              this.$set(this.playingUrls, code, urls[0].url);
+            }
+          } catch (e) {}
         }
-        let res = await realTime.playControl({
-          cameraCode: this.deviceCode,
-          mill: this.slider * 1000,
-          command: SDK
+      }
+      this.$message.success(`正在批量播放 ${list.length} 个设备`);
+    },
+    async batchStop() {
+      const codes = Object.keys(this.playingUrls);
+      if (!codes.length) {
+        this.$message.warning('没有正在播放的设备');
+        return;
+      }
+      for (const code of codes) {
+        try {
+          await realTime.delStreamProxy(code).catch(() => {});
+        } catch (e) {}
+      }
+      this.playingUrls = {};
+      this.$message.success('已停止所有拉流');
+      if (this.$refs.cells) {
+        this.$refs.cells.forEach(cell => {
+          if (cell && cell.destroyPlayer) cell.destroyPlayer();
         });
-        if (res == 'SUCCESS') {
+      }
+    },
+
+    // ========== 全屏 ==========
+    toggleFullscreen() {
+      const el = document.querySelector('.monitor-page') || document.documentElement;
+      if (document.fullscreenElement) {
+        document.exitFullscreen();
+      } else {
+        el.requestFullscreen().catch(() => {});
+      }
+    },
+    togglePanel() {
+      this.panelVisible = !this.panelVisible;
+    },
+    onMaskWheel(e) {
+      const grid = this.$refs.gridArea;
+      if (grid) {
+        grid.scrollTop += e.deltaY;
+      }
+    },
+
+    // ========== 云台控制 ==========
+    async ptzHandle(SDK) {
+      const code = this.selectedDevice?.deviceCode || this.selectedDevice?.code;
+      if (!code) {
+        this.$message.warning('请先选择设备');
+        return;
+      }
+      if (SDK === 'reset') {
+        const params = {
+          cameraCode: code,
+          wPresetNum: 34,
+          command: 'GOTO_PRESET'
+        };
+        const res = await realTime.oprPreset(params);
+        if (res === 'SUCCESS') {
           this.$message.success('操作成功');
         } else {
-          this.$message.error(res);
+          this.$message.error(res || '操作失败');
         }
-      },
-      async switchLamp(code) {
-        if (!code) return this.$message.warning('当前设备异常');
-        const data = await deviceApi.playControl({
-          cameraCode: code,
-          mill: 1000,
-          command: 'LIGHT_PWRON'
-        });
-        if (data == 'SUCCESS') {
-          this.$message.success('指令下发成功');
+        return;
+      }
+      const res = await realTime.playControl({
+        cameraCode: code,
+        mill: this.slider * 1000,
+        command: SDK
+      });
+      if (res === 'SUCCESS') {
+        this.$message.success('操作成功');
+      } else {
+        this.$message.error(res || '操作失败');
+      }
+    },
+    async switchLamp(code) {
+      if (!code) return this.$message.warning('当前设备异常');
+      const data = await deviceApi.playControl({
+        cameraCode: code,
+        mill: 1000,
+        command: 'LIGHT_PWRON'
+      });
+      if (data === 'SUCCESS') {
+        this.$message.success('指令下发成功');
+      } else {
+        this.$message.error('指令下发失败');
+      }
+    },
+    async screenshot(code) {
+      if (!code) return this.$message.warning('当前设备异常');
+      this.screenshotVal = true;
+      try {
+        const data = await deviceApi.catchPic(code);
+        if (data.size < 100) {
+          this.$message.warning('截图失败');
         } else {
-          this.$message.error('指令下发失败');
-        }
-      },
-      async screenshot(code) {
-        if (!code) return this.$message.warning('当前设备异常');
-        this.screenshotVal = true;
-        try {
-          const data = await deviceApi.catchPic(code);
-          if (data.size < 100) {
-            this.$message.warning('截图失败');
-          } else {
-            const dlink = document.createElement('a');
-            dlink.download = '截图.png';
-            dlink.style.display = 'none';
-            dlink.href = URL.createObjectURL(data);
-            document.body.appendChild(dlink);
-            dlink.click();
-            URL.revokeObjectURL(dlink.href);
-            document.body.removeChild(dlink);
-            this.$message.success('截图下载成功');
-          }
-        } finally {
-          this.screenshotVal = false;
+          const dlink = document.createElement('a');
+          dlink.download = '截图.png';
+          dlink.style.display = 'none';
+          dlink.href = URL.createObjectURL(data);
+          document.body.appendChild(dlink);
+          dlink.click();
+          URL.revokeObjectURL(dlink.href);
+          document.body.removeChild(dlink);
+          this.$message.success('截图下载成功');
         }
-      },
-      playStop() {
-        this.$refs.myVideoRef.playStop();
-        // let index = Math.floor(
-        //   this.videoList.findIndex(
-        //     (item) => item.cameraCode == this.deviceCode
-        //   ) /
-        //     (this.layout * this.layout)
-        // );
-        // let videoElement = document.getElementById('video' + index);
-        // if (videoElement && videoElement.paused) {
-        //   videoElement.play();
-        //   this.stopList = this.stopList.filter((item) => item != index);
-        // } else {
-        //   if (videoElement) videoElement.pause();
-        //   this.stopList.push(index);
-        // }
-      },
-      play(index) {
-        this.$refs.myVideoRef.play();
-
-        // let videoElement = document.getElementById('video' + index);
-        // if (videoElement) videoElement.play();
-        // this.stopList = this.stopList.filter((item) => item != index);
-      },
-      async getCameraList(list) {
-        this.monitorData = list;
-        list.forEach((item) => {
-          this.codeList.push(item.deviceCode);
-        });
-
-        // this.getClickId(list[0]);
-      },
-      async getClickId(device) {
-        this.camera = device;
-        this.deviceCode = device.deviceCode;
-        await this.getCameraUrl();
-
-        let index = Math.floor(
-          this.videoList.findIndex(
-            (item) => item.cameraCode == device.deviceCode
-          ) /
-            (this.layout * this.layout)
-        );
-        this.$refs.carouselRef && this.$refs.carouselRef.setActiveItem(index);
-        this.carouselIndex = index;
-      },
-      async getPreset() {
-        const res = await realTime.getPreset(this.deviceCode);
-        this.PresetList = res;
-      },
-      async getCameraUrl() {
-        await realTime.delStreamProxy(this.deviceCode);
-        const res = await realTime.getCameraUrl([this.deviceCode]);
-        this.$refs.myVideoRef.startPlay(res[0].url);
-        // this.videoList = res;
-        console.log(this.videoList);
+      } catch (e) {
+        this.$message.error('截图失败');
+      } finally {
+        this.screenshotVal = false;
+      }
+    },
+    async loadPreset(device) {
+      const code = device?.deviceCode || device?.code;
+      if (!code) return;
+      try {
+        this.PresetList = await realTime.getPreset(code);
+      } catch (e) {
+        this.PresetList = [];
       }
+    },
+    getPreset() {
+      this.loadPreset(this.selectedDevice);
     }
-  };
+  }
+};
 </script>
 
 <style lang="scss" scoped>
-  .inspection {
-    display: flex;
-    width: 100%;
-    .left {
-      padding-right: 0;
-      height: 100%;
-      width: 232px;
-    }
-    .center {
-      width: calc(100%);
-      padding-left: 10px;
-      padding-right: 10px;
-      padding-bottom: 20px;
-    }
+.monitor-page {
+  position: relative;
+  width: 100%;
+  height: calc(100vh - 96px);
+  background-color: #0b1a28;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+}
+
+// ====== 视频网格区域 ======
+.grid-area {
+  flex: 1;
+  position: relative;
+  overflow-y: auto;
+  overflow-x: hidden;
+  padding: 12px;
+
+  &::-webkit-scrollbar {
+    width: 6px;
   }
 
-  .missonList,
-  .missonList * {
-    box-sizing: border-box;
+  &::-webkit-scrollbar-thumb {
+    background-color: #2c3e5a;
+    border-radius: 3px;
   }
-  .missonList {
-    background: #fff;
-    padding: 10px;
-    display: flex;
-    flex-direction: column;
-    gap: 4px;
-    align-items: flex-start;
-    justify-content: flex-start;
-    align-self: stretch;
-    flex-shrink: 0;
-    position: relative;
-    width: 232px;
-    .list {
-      width: 100%;
-      ::v-deep .el-scrollbar__thumb {
-        display: none;
-      }
-    }
-    .title {
-      background: #f2f4f5;
-      display: flex;
-      flex-direction: row;
-      gap: 1px;
-      align-items: center;
-      justify-content: center;
-      align-self: stretch;
-      flex-shrink: 0;
-      height: 24px;
-      position: relative;
-      > div {
-        color: #404446;
-        text-align: left;
-        font-family: 'Alibaba PuHuiTi 2.0', sans-serif;
-        font-size: 14px;
-        line-height: 16px;
-        font-weight: 400;
-        position: relative;
-      }
-    }
 
-    .row {
-      padding: 4px 0px 4px 0px;
-      display: flex;
-      flex-direction: row;
-      align-items: center;
-      justify-content: space-between;
-      align-self: stretch;
-      flex-shrink: 0;
-      position: relative;
-      &:hover {
-        background: linear-gradient(
-          270deg,
-          rgba(44, 138, 224, 0.44) 0%,
-          rgba(44, 138, 224, 0.22) 119.59%
-        );
-      }
-    }
-    .name {
-      color: rgb(64, 68, 70);
-      text-align: left;
-      font-family: var(
-        --small-none-regular-font-family,
-        'Alibaba PuHuiTi 2.0',
-        sans-serif
-      );
-      font-size: var(--small-none-regular-font-size, 14px);
-      line-height: var(--small-none-regular-line-height, 16px);
-      font-weight: var(--small-none-regular-font-weight, 400);
-      position: relative;
-    }
-    .done {
-      color: #32a2d4;
-      text-align: left;
-      font-family: var(
-        --small-none-regular-font-family,
-        'Alibaba PuHuiTi 2.0',
-        sans-serif
-      );
-      font-size: var(--small-none-regular-font-size, 14px);
-      line-height: var(--small-none-regular-line-height, 16px);
-      font-weight: var(--small-none-regular-font-weight, 400);
-      position: relative;
-    }
-    .doing {
-      color: #ffb323;
-      text-align: left;
-      font-family: var(
-        --small-none-regular-font-family,
-        'Alibaba PuHuiTi 2.0',
-        sans-serif
-      );
-      font-size: var(--small-none-regular-font-size, 14px);
-      line-height: var(--small-none-regular-line-height, 16px);
-      font-weight: var(--small-none-regular-font-weight, 400);
-      position: relative;
-    }
-    .todo {
-      color: #ff4d4f;
-      text-align: left;
-      font-family: var(
-        --small-none-regular-font-family,
-        'Alibaba PuHuiTi 2.0',
-        sans-serif
-      );
-      font-size: var(--small-none-regular-font-size, 14px);
-      line-height: var(--small-none-regular-line-height, 16px);
-      font-weight: var(--small-none-regular-font-weight, 400);
-      position: relative;
-    }
+  &::-webkit-scrollbar-track {
+    background: transparent;
+  }
+}
+
+.video-grid {
+  display: grid;
+  gap: 12px;
+}
+
+// ====== 加载提示 ======
+.load-tip {
+  text-align: center;
+  padding: 16px 0;
+  color: rgba(255, 255, 255, 0.4);
+  font-size: 13px;
+}
+
+// ====== 底部工具栏 ======
+.toolbar {
+  flex-shrink: 0;
+  height: 48px;
+  background-color: #0d1f30;
+  border-top: 1px solid #1a3047;
+  display: flex;
+  align-items: center;
+  padding: 0 16px;
+}
+
+.toolbar-left,
+.toolbar-center,
+.toolbar-right {
+  display: flex;
+  align-items: center;
+}
+
+.toolbar-left { flex: 1; }
+.toolbar-center { justify-content: center; gap: 16px; }
+.toolbar-right { justify-content: flex-end; }
+
+.layout-group {
+  display: flex;
+  gap: 6px;
+}
+
+.layout-btn {
+  width: 32px;
+  height: 32px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  border-radius: 3px;
+  border: 1px solid transparent;
+  color: rgba(255, 255, 255, 0.35);
+  transition: all 0.2s;
+
+  &:hover {
+    color: rgba(255, 255, 255, 0.65);
+    background: rgba(44, 138, 224, 0.1);
   }
-  .tree {
-    background-color: #fff;
-    margin-top: 16px;
+
+  &.active {
+    color: #40c8ff;
+    border-color: #40c8ff;
+    background: rgba(64, 200, 255, 0.08);
   }
 
-  .el-carousel {
-    ::v-deep .el-carousel__container {
-      height: calc(100% - 27px);
-    }
+  svg {
+    width: 18px;
+    height: 18px;
   }
+}
 
+.tool-btn {
+  color: rgba(255, 255, 255, 0.7) !important;
+  font-size: 14px;
 
-  .deviceInfo,
-  .deviceInfo * {
-    box-sizing: border-box;
+  &:hover {
+    color: #40c8ff !important;
   }
-  .deviceInfo {
-    background: #fff;
-    padding: 10px;
-    display: flex;
-    flex-direction: column;
-    gap: 4px;
-    align-items: flex-start;
-    justify-content: flex-start;
-    align-self: stretch;
-    flex-shrink: 0;
-    position: relative;
-    width: 232px;
-    .frame-48095487 {
-      background: #f2f4f5;
-      display: flex;
-      flex-direction: row;
-      gap: 1px;
-      align-items: center;
-      justify-content: center;
-      align-self: stretch;
-      flex-shrink: 0;
-      height: 24px;
-      position: relative;
-    }
-    .div {
-      color: rgb(64, 68, 70);
-      text-align: left;
-      font-family: 'Alibaba PuHuiTi 2.0', sans-serif;
-      font-size: 14px;
-      line-height: 16px;
-      font-weight: 400;
-      position: relative;
-    }
-    .row {
-      padding: 4px 0px 4px 0px;
-      display: flex;
-      flex-direction: row;
-      align-items: center;
-      justify-content: space-between;
-      align-self: stretch;
-      flex-shrink: 0;
-      position: relative;
-    }
-    .name {
-      color: #32a2d4;
-      text-align: left;
-      font-family: 'Alibaba PuHuiTi 2.0', sans-serif;
-      font-size: 14px;
-      line-height: 16px;
-      font-weight: 400;
-      position: relative;
-    }
+}
+
+// ====== 右侧控制面板 ======
+.control-panel {
+  position: fixed;
+  top: 0;
+  right: 15px;
+  bottom: 48px;
+  width: 280px;
+  max-height: calc(100vh - 120px);
+  background-color: #0d1f30;
+  border-left: 1px solid #1a3047;
+  z-index: 100;
+  overflow-y: auto;
+  display: flex;
+  flex-direction: column;
+  box-shadow: -4px 0 20px rgba(0, 0, 0, 0.3);
+
+  &::-webkit-scrollbar {
+    width: 5px;
   }
-  .handle {
-    display: flex;
-    padding: 10px;
-    flex-direction: column;
-    align-items: flex-start;
-    gap: 16px;
-    flex: 1 0 0;
-    background: #fff;
-    margin-top: 10px;
+
+  &::-webkit-scrollbar-thumb {
+    background-color: #2c3e5a;
+    border-radius: 3px;
   }
-  .slider {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    align-self: stretch;
-    padding-left: 10px;
-    .sliderNum {
-      display: flex;
-      height: 17px;
-      padding: 10px;
-      flex-direction: column;
-      justify-content: center;
-      align-items: center;
-      gap: 10px;
-      border: 1px solid #cdcfd0;
-      width: 29px;
-      box-sizing: border-box;
-      ::v-deep .el-slider__button {
-        background-color: #fff;
-        border-color: #ff7b30;
-      }
-    }
+
+  &::-webkit-scrollbar-track {
+    background: transparent;
   }
-  .moreHandle {
-    display: flex;
-    width: 200px;
-    padding: 5px 0px;
-    flex-direction: column;
-    justify-content: center;
-    align-items: center;
-    gap: 4px;
-    position: relative;
-    border-radius: 4px;
-    border: 2px solid #e3e5e6;
-    .row {
-      display: flex;
-      align-items: flex-start;
-      align-self: stretch;
-    }
-    .line {
-      width: 180px;
-      height: 1px;
-      background: #e3e5e6;
-      position: absolute;
-      top: 50%;
-      left: 50%;
-      transform: translate(-50%, -50%);
-    }
-    .item {
-      display: flex;
-      height: 21px;
-      padding: 0px 9px;
-      justify-content: center;
-      align-items: center;
-      width: 39px;
-      cursor: not-allowed;
-      border-left: 1px solid #e3e5e6;
-      border-right: 1px solid #e3e5e6;
-      &:first-child {
-        border-left: 0;
-      }
-      &:last-child {
-        border-right: 0;
-      }
-    }
-    .normal {
-      cursor: pointer;
-    }
+}
+
+.panel-header {
+  flex-shrink: 0;
+  height: 46px;
+  padding: 0 16px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  border-bottom: 1px solid #1a3047;
+  font-size: 15px;
+  font-weight: 600;
+  color: #e0ecf7;
+}
+
+.panel-close {
+  cursor: pointer;
+  color: rgba(255, 255, 255, 0.45);
+  transition: color 0.2s;
+
+  &:hover {
+    color: #fff;
   }
+}
 
-  .center-handle {
-    display: flex;
-    height: 42px;
-    justify-content: space-between;
-    align-items: center;
-    align-self: stretch;
-    width: 100%;
-    .layout-type {
-      display: flex;
-      width: 98px;
-      align-items: center;
-      position: relative;
-      > div {
-        display: flex;
-      }
-    }
+.panel-section {
+  padding: 12px 16px;
+  border-bottom: 1px solid #13243a;
+}
 
-    .play {
-      display: flex;
-      justify-content: center;
-      width: 100%;
-      align-items: flex-start;
-      gap: 24px;
-    }
+.section-title {
+  font-size: 13px;
+  font-weight: 600;
+  color: #8ab4d6;
+  margin-bottom: 10px;
+}
 
-    .video-handle {
-      display: flex;
-      align-items: flex-end;
-      gap: 16px;
-    }
+.info-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 5px 0;
+  font-size: 13px;
+
+  label {
+    color: rgba(255, 255, 255, 0.45);
   }
-  .monitor {
-    height: 100%;
-    padding: 10px;
+
+  value {
+    color: #e0ecf7;
+    max-width: 60%;
+    text-align: right;
+    word-break: break-all;
+  }
+}
+
+.speed-slider {
+  display: flex;
+  align-items: center;
+  margin-top: 10px;
+  font-size: 12px;
+  color: rgba(255, 255, 255, 0.55);
+
+  ::v-deep .el-slider__runway {
+    background-color: #1a3047;
   }
-  .layout {
-    width: 20px;
-    height: 15px;
-    padding: 1px;
-    display: flex;
-    flex-wrap: wrap;
-    gap: 1px;
-    border: 1px #404446 solid;
+
+  ::v-deep .el-slider__button {
+    border-color: #40c8ff;
   }
-  .group {
-    display: flex;
-    padding: 4px;
-    align-items: flex-start;
-    gap: 4px;
-    position: absolute;
-    top: 20px;
-    border-radius: 4px;
-    border: 0.5px solid #404446;
-    background: #fff;
+}
+
+.speed-val {
+  width: 22px;
+  text-align: center;
+  color: #40c8ff;
+  font-weight: 600;
+}
+
+.quick-actions {
+  display: flex;
+  gap: 12px;
+  margin-top: 6px;
+}
+
+.action-icon {
+  width: 36px;
+  height: 36px;
+  cursor: pointer;
+  opacity: 0.75;
+  transition: opacity 0.2s;
+
+  &:hover {
+    opacity: 1;
   }
+}
+
+.preset-section {
+  flex: 1;
+  min-height: 0;
+  border-bottom: none;
+}
+
+.panel-mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 280px;
+  bottom: 48px;
+  z-index: 99;
+  background: transparent;
+}
+
+// ====== 动画 ======
+.slide-panel-enter-active,
+.slide-panel-leave-active {
+  transition: transform 0.3s ease;
+}
+
+.slide-panel-enter,
+.slide-panel-leave-to {
+  transform: translateX(100%);
+}
 </style>