Przeglądaj źródła

feat: 新增视频回看左侧树收起功能,改版违规问题详情页图片视频查看模式

bxy 1 rok temu
rodzic
commit
5c40b0d18b

+ 2 - 0
package.json

@@ -58,8 +58,10 @@
     "print-js": "1.6.0",
     "qrcode": "1.5.1",
     "qs": "6.11.0",
+    "swiper": "^11.2.5",
     "uid": "2.0.2",
     "url-join": "5.0.0",
+    "v-viewer": "^3.0.21",
     "vue": "3.3.4",
     "vue-echarts": "^6.7.3",
     "vue-hooks-plus": "1.8.6",

+ 0 - 2
src/main.ts

@@ -9,12 +9,10 @@ import VueKonva from 'vue-konva';
 
 import { createApp } from 'vue';
 import App from './App.vue';
-// import VueKonva from 'vue-konva';
 import router, { setupRouter } from './router';
 import { setupStore } from '@/store';
 import 'virtual:svg-icons-register';
 import { setupElement, setupDirectives, setupCustomComponents } from '@/plugins';
-// import VueKonva from 'vue-konva';
 
 async function bootstrap() {
   const app = createApp(App);

+ 24 - 109
src/views/datamanager/alertformdata/components/common/DetailDialog.vue

@@ -10,43 +10,17 @@
     >
       <div class="description-box">
         <div class="title">问题描述</div>
-        <p>{{ description }}</p>
+        <p>{{ detailDescription }}</p>
       </div>
       <div>
         <div class="title">问题图片/视频</div>
-        <div class="media-box">
-          <div class="video-box" v-if="videoPaths && videoPaths.length != 0">
-            <img
-              src="@/assets/images/alert/video-play.png"
-              @click="handleOpenVideo"
-              style="
-                object-fit: contain;
-                width: 200px;
-                height: 200px;
-                border: solid 1px #ccc;
-                cursor: pointer;
-              "
-            />
-          </div>
-          <div class="img-box" v-for="(imagePath, index) in imagePaths" :key="index">
-            <el-image
-              style="width: 200px; height: 200px; border: solid 1px #ccc"
-              :src="imagePath"
-              :preview-src-list="imagePaths"
-              :zoom-rate="1.2"
-              :max-scale="7"
-              :min-scale="0.2"
-              :initial-index="index"
-              fit="cover"
-            />
-          </div>
-        </div>
+        <SwiperThumbsGallery v-if="updateSwiper" />
       </div>
       <template #footer>
         <div class="dialog-footer">
           <span class="footer-tip">提示:可切换查看问题列表当前分页内数据</span>
-          <el-button @click="handlePrevious" :disabled="!hasPrevious">上一个</el-button>
-          <el-button type="primary" @click="handleNext" :disabled="!hasNext">下一个</el-button>
+          <el-button @click="handlePrevious" :disabled="!hasPreviousRow">上一个</el-button>
+          <el-button type="primary" @click="handleNext" :disabled="!hasNextRow">下一个</el-button>
           <el-checkbox
             class="footer-checkbox"
             v-model="curHasBeenChosen"
@@ -56,48 +30,27 @@
         </div>
       </template>
     </el-dialog>
-
-    <el-dialog v-model="videoDialogVisible" class="video-dialog" align-center destroy-on-close>
-      <video
-        type="video/mp4"
-        muted="true"
-        preload="auto"
-        :controls="true"
-        autoplay
-        style="object-fit: contain"
-      >
-        <source :src="tempVideoUrl" />
-      </video>
-    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
-  import { computed, ref } from 'vue';
-
-  const props = defineProps({
-    hasBeenChosen: Boolean,
-    description: String,
-    imagePaths: Array<string>,
-    videoPaths: Array<string>,
-    hasPrevious: Boolean,
-    hasNext: Boolean,
-  });
+  import { computed, nextTick, ref } from 'vue';
+  import { storeToRefs } from 'pinia';
+  import { useCurImgVideoUrlStore } from '../../store/useCurImgVideoUrl';
+  import SwiperThumbsGallery from './SwiperThumbsGallery.vue';
+
+  const curImgVideoUrl = useCurImgVideoUrlStore();
+  const { detailRowChosen, hasPreviousRow, hasNextRow, detailDescription } = storeToRefs(curImgVideoUrl);
 
   const emits = defineEmits(['close', 'update:previous', 'update:next', 'update:choose']);
+
   const visible = ref(true);
-  const videoDialogVisible = ref(false);
+  const updateSwiper = ref(true);
 
   const curHasBeenChosen = computed(() => {
-    return props.hasBeenChosen;
+    return detailRowChosen.value;
   });
 
-  const tempVideoUrl = ref('');
-  const handleOpenVideo = () => {
-    videoDialogVisible.value = true;
-    if (props.videoPaths) tempVideoUrl.value = props.videoPaths[0];
-  };
-
   const handleClose = () => {
     emits('close');
   };
@@ -108,10 +61,18 @@
 
   const handleNext = () => {
     emits('update:next');
+    updateSwiper.value = false;
+    nextTick(() => {
+      updateSwiper.value = true;
+    });
   };
 
   const handleChooseStatus = () => {
     emits('update:choose', curHasBeenChosen.value);
+    updateSwiper.value = false;
+    nextTick(() => {
+      updateSwiper.value = true;
+    });
   };
 </script>
 
@@ -142,8 +103,8 @@
     }
 
     .el-dialog__body {
-      height: 472px;
-      padding: 40px;
+      height: 690px;
+      padding: 20px 40px 0 40px;
       overflow: auto;
     }
 
@@ -182,50 +143,4 @@
       color: #606266;
     }
   }
-
-  .media-box {
-    display: grid;
-    grid-template-columns: repeat(auto-fill, 200px);
-    column-gap: 10px;
-    row-gap: 10px;
-
-    .video-box {
-      width: 200px;
-      height: 200px;
-      margin-right: 10px;
-    }
-
-    .img-box {
-      position: relative;
-      width: 200px;
-      height: 200px;
-      margin-right: 10px;
-    }
-
-    .cover-box {
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      position: absolute;
-      top: 0;
-      width: 200px;
-      height: 200px;
-      background-color: rgba(0, 0, 0, 0.5);
-    }
-  }
-
-  :deep(.video-dialog) {
-    .el-dialog__header,
-    .el-dialog__footer {
-      display: none;
-    }
-
-    .el-dialog__body {
-      // height: 100%;
-      padding: 0;
-      display: flex;
-      justify-content: center;
-      background-color: #000;
-    }
-  }
 </style>

+ 149 - 0
src/views/datamanager/alertformdata/components/common/SwiperThumbsGallery.vue

@@ -0,0 +1,149 @@
+<template>
+  <div class="thumb-example">
+    <swiper
+      class="top-swiper"
+      :style="{
+        '--swiper-navigation-color': '#fff',
+        '--swiper-pagination-color': '#fff',
+      }"
+      :space-between="10"
+      :navigation="true"
+      :thumbs="{ swiper: thumbsSwiper }"
+      :modules="modules"
+    >
+      <swiper-slide class="slide" v-for="(item, index) in fileList" :key="index">
+        <template v-if="item.type === 'image'">
+          <img :src="item.url" alt="" @click="handleOpenPicViewer(index)" />
+        </template>
+        <template v-else-if="item.type === 'video'">
+          <video
+            type="video/mp4"
+            muted="true"
+            preload="auto"
+            :controls="true"
+            disablepictureinpicture
+            style="object-fit: contain; width: 100%; height: 432px"
+            :key="item.url"
+          >
+            <source :src="item.url" />
+          </video>
+        </template>
+      </swiper-slide>
+    </swiper>
+
+    <swiper
+      class="thumbs-swiper"
+      :space-between="10"
+      :slides-per-view="10"
+      :watch-slides-progress="true"
+      :modules="modules"
+      @swiper="setThumbsSwiper"
+    >
+      <swiper-slide class="slide" v-for="(item, index) in fileList" :key="index">
+        <img :src="item.url" alt="" v-if="item.type === 'image'" />
+        <div v-else-if="item.type === 'video'" style="pointer-events: none">
+          <video
+            type="video/mp4"
+            muted="true"
+            :key="item.url"
+            preload="auto"
+            disablepictureinpicture
+            style="object-fit: contain"
+          >
+            <source :src="item.url" />
+          </video>
+        </div>
+      </swiper-slide>
+    </swiper>
+  </div>
+  <el-image-viewer
+    v-if="imgViewerVisible"
+    :url-list="detailPictures"
+    :initial-index="imgSrcIndex"
+    @close="handleClosePicViewer"
+  />
+</template>
+
+<script setup>
+  import { ref, computed } from 'vue';
+  import { storeToRefs } from 'pinia';
+  import { ElImageViewer } from 'element-plus';
+  import { Swiper, SwiperSlide } from 'swiper/vue';
+  import { Navigation, Thumbs } from 'swiper/modules';
+  import 'swiper/css';
+  import 'swiper/css/navigation';
+  import 'swiper/css/thumbs';
+  import { useCurImgVideoUrlStore } from '../../store/useCurImgVideoUrl';
+
+  const curImgVideoUrl = useCurImgVideoUrlStore();
+  const { detailPictures, detailVideos } = storeToRefs(curImgVideoUrl);
+
+  const modules = [Navigation, Thumbs];
+
+  const fileList = computed(() => {
+    const images = detailPictures.value.map((url) => ({ type: 'image', url }));
+    const videos = detailVideos.value.map((url) => ({ type: 'video', url, poster: ref('') }));
+    return [...videos, ...images];
+  });
+
+  const thumbsSwiper = ref(null);
+
+  const imgSrcIndex = ref(0);
+  const imgViewerVisible = ref(false);
+
+  const handleOpenPicViewer = (curImgIndex) => {
+    imgSrcIndex.value = detailVideos.value.length > 0 ? curImgIndex - 1 : curImgIndex;
+    imgViewerVisible.value = true;
+  };
+
+  const handleClosePicViewer = () => {
+    imgViewerVisible.value = false;
+  };
+
+  const setThumbsSwiper = (swiper) => {
+    thumbsSwiper.value = swiper;
+  };
+</script>
+
+<style lang="scss" scoped>
+  .thumb-example {
+    height: 540px;
+    background-color: rgba(0, 0, 0);
+  }
+
+  .top-swiper,
+  .thumbs-swiper {
+    .slide {
+      img {
+        display: block;
+        width: 100%;
+        height: 100%;
+        object-fit: contain;
+      }
+    }
+  }
+
+  .top-swiper {
+    height: 80%;
+    width: 100%;
+  }
+
+  .thumbs-swiper {
+    height: 20%;
+    box-sizing: border-box;
+    padding: 10px;
+
+    .slide {
+      width: 25%;
+      height: 100%;
+      opacity: 1;
+      border: 1px solid #fff;
+      overflow: hidden;
+
+      &:not(.swiper-slide-thumb-active) {
+        opacity: 0.8;
+        border: none;
+      }
+    }
+  }
+</style>

+ 22 - 29
src/views/datamanager/alertformdata/components/default-simple/Default.vue

@@ -12,9 +12,7 @@
     <div class="table-list">
       <div v-if="showActionBar" class="action-bar">
         <span class="num-text">已选{{ chooseNum }}项</span>
-        <el-button :class="isActiveDelete ? 'btn-active' : 'btn-normal'" @click="handleDeleteAll"
-          >删除</el-button
-        >
+        <el-button :class="isActiveDelete ? 'btn-active' : 'btn-normal'" @click="handleDeleteAll">删除</el-button>
         <span class="close-btn" @click="handleSelectNone"></span>
       </div>
       <AlertTableSimple
@@ -38,12 +36,6 @@
     </div>
     <DetailDialog
       v-if="isDetailDialogShow"
-      :has-been-chosen="detailRowChosen"
-      :description="detailDescription"
-      :image-paths="detailPictures"
-      :video-paths="detailVideos"
-      :has-previous="hasPreviousRow"
-      :has-next="hasNextRow"
       @close="closeDetailDialog"
       @update:previous="handleChangePrevious"
       @update:next="handleChangeNext"
@@ -53,24 +45,36 @@
 </template>
 
 <script setup lang="ts">
+  import { storeToRefs } from 'pinia';
   import { ref, onMounted, onBeforeMount } from 'vue';
   import { ElMessage, ElMessageBox } from 'element-plus';
+  import { TableQueryForm, getDefaultTableData, deleteDefaultTableData } from '@/api/datamanagement/alert-default';
+  import { useWorkLocation } from '../../hooks/useWorkLocation';
+  import { useIssueMainType } from '../../hooks/useIssueMainType';
+  import { useCurImgVideoUrlStore } from '../../store/useCurImgVideoUrl';
+  import Prequalification from '../common/Prequalification.vue';
   import QueryFormSimple from '../common/QueryFormSimple.vue';
   import AlertTableSimple, { DataSourceItem } from '../common/AlertTableSimple.vue';
   import DetailDialog from '../common/DetailDialog.vue';
   import Pagination from '../common/Pagination.vue';
-  import { useWorkLocation } from '../../hooks/useWorkLocation';
-  import { useIssueMainType } from '../../hooks/useIssueMainType';
-  import Prequalification from '../common/Prequalification.vue';
-  import {
-    TableQueryForm,
-    getDefaultTableData,
-    deleteDefaultTableData,
-  } from '@/api/datamanagement/alert-default';
 
   const { locationOptions, getLocationOptions } = useWorkLocation();
   const { aiMainOptions, getAIMainOptions } = useIssueMainType();
 
+  const curImgVideoUrl = useCurImgVideoUrlStore();
+  const {
+    detailRowChosen,
+    detailRow,
+    detailCurRowIndex,
+    detailPreviousRow,
+    detailNextRow,
+    hasPreviousRow,
+    hasNextRow,
+    detailDescription,
+    detailPictures,
+    detailVideos,
+  } = storeToRefs(curImgVideoUrl);
+
   const alertTableRef = ref<typeof AlertTableSimple>();
   const tableData = ref<DataSourceItem[]>([]);
   const showActionBar = ref(false);
@@ -80,16 +84,6 @@
   const isActiveDelete = ref(false);
   // 详情
   const isDetailDialogShow = ref(false);
-  const detailRowChosen = ref(false); // 当前行是否被选中
-  const detailRow = ref(); // 当前行
-  const detailCurRowIndex = ref(0); // 当前行index
-  const detailPreviousRow = ref(); // 上一行
-  const detailNextRow = ref(); // 下一行
-  const hasPreviousRow = ref(false); // 是否有上一行
-  const hasNextRow = ref(false); // 是否有下一行
-  const detailDescription = ref('');
-  const detailPictures = ref<string[]>([]);
-  const detailVideos = ref<string[]>([]);
   // 分页
   const total = ref(0);
 
@@ -184,8 +178,7 @@
     else hasPreviousRow.value = false;
     if (detailNextRow.value) hasNextRow.value = true;
     else hasNextRow.value = false;
-    if (chooseRow.value.findIndex((item) => item.id === curRow.id) !== -1)
-      detailRowChosen.value = true;
+    if (chooseRow.value.findIndex((item) => item.id === curRow.id) !== -1) detailRowChosen.value = true;
     else detailRowChosen.value = false;
   };
   const handleDetail = (row) => {

+ 29 - 29
src/views/datamanager/alertformdata/components/default/Default.vue

@@ -66,12 +66,6 @@
     </div>
     <DetailDialog
       v-if="isDetailDialogShow"
-      :has-been-chosen="detailRowChosen"
-      :description="detailDescription"
-      :image-paths="detailPictures"
-      :video-paths="detailVideos"
-      :has-previous="hasPreviousRow"
-      :has-next="hasNextRow"
       @close="closeDetailDialog"
       @update:previous="handleChangePrevious"
       @update:next="handleChangeNext"
@@ -81,15 +75,12 @@
 </template>
 
 <script setup lang="ts">
+  import urlJoin from 'url-join';
+  import { storeToRefs } from 'pinia';
   import { ref, onMounted, onBeforeMount } from 'vue';
-  import { ElMessage, ElMessageBox } from 'element-plus';
   import axios, { AxiosRequestConfig } from 'axios';
-  import QueryForm from '../common/QueryForm.vue';
-  import AlertTable, { DataSourceItem } from '../common/AlertTable.vue';
-  import DetailDialog from '../common/DetailDialog.vue';
-  import Pagination from '../common/Pagination.vue';
-  import { useWorkLocation } from '../../hooks/useWorkLocation';
-  import { useIssueMainType } from '../../hooks/useIssueMainType';
+  import { getHeaders } from '@/utils/http/axios';
+  import { ElMessage, ElMessageBox } from 'element-plus';
   import {
     TableQueryForm,
     getDefaultTableData,
@@ -100,18 +91,37 @@
     updateDefaultPriority,
     updateDefaultPriorityAll,
   } from '@/api/datamanagement/alert-default';
-  import Prequalification from '../common/Prequalification.vue';
-  import { useUserStore } from '@/store/modules/user';
-  import { useGlobSetting } from '@/hooks/setting';
-  import urlJoin from 'url-join';
-  import { getHeaders } from '@/utils/http/axios';
   import { PERM_DATA } from '@/types/permission/constants';
+  import { useGlobSetting } from '@/hooks/setting';
+  import { useWorkLocation } from '../../hooks/useWorkLocation';
+  import { useIssueMainType } from '../../hooks/useIssueMainType';
+  import { useUserStore } from '@/store/modules/user';
+  import { useCurImgVideoUrlStore } from '../../store/useCurImgVideoUrl';
+  import Prequalification from '../common/Prequalification.vue';
+  import QueryForm from '../common/QueryForm.vue';
+  import AlertTable, { DataSourceItem } from '../common/AlertTable.vue';
+  import DetailDialog from '../common/DetailDialog.vue';
+  import Pagination from '../common/Pagination.vue';
+
+  const { urlPrefix } = useGlobSetting();
 
   const userStore = useUserStore();
   const { locationOptions, getLocationOptions } = useWorkLocation();
   const { aiMainOptions, manualMainOptions, getAIMainOptions, getManualMainOptions } = useIssueMainType();
 
-  const { urlPrefix } = useGlobSetting();
+  const curImgVideoUrl = useCurImgVideoUrlStore();
+  const {
+    detailRowChosen,
+    detailRow,
+    detailCurRowIndex,
+    detailPreviousRow,
+    detailNextRow,
+    hasPreviousRow,
+    hasNextRow,
+    detailDescription,
+    detailPictures,
+    detailVideos,
+  } = storeToRefs(curImgVideoUrl);
 
   const alertTableRef = ref<typeof AlertTable>();
   const tableData = ref<DataSourceItem[]>([]);
@@ -128,16 +138,6 @@
   const isActiveCopy = ref(false);
   // 详情
   const isDetailDialogShow = ref(false);
-  const detailRowChosen = ref(false); // 当前行是否被选中
-  const detailRow = ref(); // 当前行
-  const detailCurRowIndex = ref(0); // 当前行index
-  const detailPreviousRow = ref(); // 上一行
-  const detailNextRow = ref(); // 下一行
-  const hasPreviousRow = ref(false); // 是否有上一行
-  const hasNextRow = ref(false); // 是否有下一行
-  const detailDescription = ref('');
-  const detailPictures = ref<string[]>([]);
-  const detailVideos = ref<string[]>([]);
   // 分页
   const total = ref(0);
 

+ 22 - 22
src/views/datamanager/alertformdata/components/show/Show.vue

@@ -50,12 +50,6 @@
     </div>
     <DetailDialog
       v-if="isDetailDialogShow"
-      :has-been-chosen="detailRowChosen"
-      :description="detailDescription"
-      :image-paths="detailPictures"
-      :video-paths="detailVideos"
-      :has-previous="hasPreviousRow"
-      :has-next="hasNextRow"
       @close="closeDetailDialog"
       @update:previous="handleChangePrevious"
       @update:next="handleChangeNext"
@@ -67,27 +61,43 @@
 </template>
 
 <script setup lang="ts">
+  import { storeToRefs } from 'pinia';
   import { ref, onMounted, onBeforeMount } from 'vue';
   import { ElMessage, ElMessageBox } from 'element-plus';
   import { Plus } from '@element-plus/icons-vue';
+  import { PERM_DATA } from '@/types/permission/constants';
+  import { useWorkLocation } from '../../hooks/useWorkLocation';
+  import { useIssueMainType } from '../../hooks/useIssueMainType';
+  import { useUserStore } from '@/store/modules/user';
+  import { useCurImgVideoUrlStore } from '../../store/useCurImgVideoUrl';
+  import { TableQueryForm } from '@/api/datamanagement/alert-default';
+  import { getShowTableData, updateShowTableData, deleteShowTableData } from '@/api/datamanagement/alert-show';
   import QueryForm from '../common/QueryForm.vue';
   import AlertTable, { DataSourceItem } from '../common/AlertTable.vue';
   import DetailDialog from '../common/DetailDialog.vue';
   import Pagination from '../common/Pagination.vue';
   import AddDrawer from '../common/AddDrawer.vue';
   import EditDrawer from '../common/EditDrawer.vue';
-  import { useUserStore } from '@/store/modules/user';
-  import { useWorkLocation } from '../../hooks/useWorkLocation';
-  import { useIssueMainType } from '../../hooks/useIssueMainType';
-  import { getShowTableData, updateShowTableData, deleteShowTableData } from '@/api/datamanagement/alert-show';
-  import { TableQueryForm } from '@/api/datamanagement/alert-default';
-  import { PERM_DATA } from '@/types/permission/constants';
 
   const userStore = useUserStore();
 
   const { locationOptions, getLocationOptions } = useWorkLocation();
   const { aiMainOptions, manualMainOptions, getAIMainOptions, getManualMainOptions } = useIssueMainType();
 
+  const curImgVideoUrl = useCurImgVideoUrlStore();
+  const {
+    detailRowChosen,
+    detailRow,
+    detailCurRowIndex,
+    detailPreviousRow,
+    detailNextRow,
+    hasPreviousRow,
+    hasNextRow,
+    detailDescription,
+    detailPictures,
+    detailVideos,
+  } = storeToRefs(curImgVideoUrl);
+
   const alertTableRef = ref<typeof AlertTable>();
   const tableData = ref<DataSourceItem[]>([]);
   const showActionBar = ref(false);
@@ -99,16 +109,6 @@
   const isActiveDelete = ref(false);
   // 详情
   const isDetailDialogShow = ref(false);
-  const detailRowChosen = ref(false); // 当前行是否被选中
-  const detailRow = ref(); // 当前行
-  const detailCurRowIndex = ref(0); // 当前行index
-  const detailPreviousRow = ref(); // 上一行
-  const detailNextRow = ref(); // 下一行
-  const hasPreviousRow = ref(false); // 是否有上一行
-  const hasNextRow = ref(false); // 是否有下一行
-  const detailDescription = ref('');
-  const detailPictures = ref<string[]>([]);
-  const detailVideos = ref<string[]>([]);
   // 添加
   const isAddDrawer = ref(false);
   const isEditDrawer = ref(false);

+ 28 - 0
src/views/datamanager/alertformdata/store/useCurImgVideoUrl.ts

@@ -0,0 +1,28 @@
+import { ref } from 'vue';
+import { defineStore } from 'pinia';
+
+export const useCurImgVideoUrlStore = defineStore('curImgVideoUrl', () => {
+  const detailRowChosen = ref(false); // 当前行是否被选中
+  const detailRow = ref(); // 当前行
+  const detailCurRowIndex = ref(0); // 当前行index
+  const detailPreviousRow = ref(); // 上一行
+  const detailNextRow = ref(); // 下一行
+  const hasPreviousRow = ref(false); // 是否有上一行
+  const hasNextRow = ref(false); // 是否有下一行
+  const detailDescription = ref('');
+  const detailPictures = ref<string[]>([]);
+  const detailVideos = ref<string[]>([]);
+
+  return {
+    detailRowChosen,
+    detailRow,
+    detailCurRowIndex,
+    detailPreviousRow,
+    detailNextRow,
+    hasPreviousRow,
+    hasNextRow,
+    detailDescription,
+    detailPictures,
+    detailVideos,
+  };
+});

+ 28 - 1
src/views/datamanager/playback/Playback.vue

@@ -1,7 +1,7 @@
 <template>
   <div>
     <div class="camera-main">
-      <div class="camera-tree">
+      <div class="camera-tree" v-show="cameraTreeVisible">
         <CameraTreeCom
           :loading="presetListStore.loading"
           :camera-tree="cameraTree || []"
@@ -10,6 +10,12 @@
           :no-networking-num="noNetworkingNum"
         />
       </div>
+      <div v-if="cameraTreeVisible" class="arrow-icon" @click="cameraTreeVisible = false"
+        ><el-icon><DArrowLeft /></el-icon
+      ></div>
+      <div v-else class="arrow-icon" @click="cameraTreeVisible = true"
+        ><el-icon><DArrowRight /></el-icon
+      ></div>
       <div class="camera-placeholder" v-if="!cameraDetailStore.cameraId">请选择左侧相机</div>
       <div v-else class="camera-setting-wrapper">
         <NvrCameraView ref="nvrCameraViewRef" :camera-id="cameraDetailStore.cameraId" />
@@ -19,6 +25,8 @@
 </template>
 
 <script setup lang="ts">
+  import { ElIcon } from 'element-plus';
+  import { DArrowLeft, DArrowRight } from '@element-plus/icons-vue';
   import NvrCameraView from './components/NvrCameraView.vue';
   import CameraTreeCom from '@/views/cameras/preview/components/CameraTree/CameraTreeOldVersion.vue';
   import { onUnmounted, onBeforeUnmount, ref, watch } from 'vue';
@@ -44,6 +52,8 @@
   const noIntegrationNum = ref<number>(0);
   const nvrCameraViewRef = ref();
 
+  const cameraTreeVisible = ref(true);
+
   // const { allAlgoList } = storeToRefs(cameraAlgoStore);
 
   function updateNetworkingState(data, targetData) {
@@ -186,4 +196,21 @@
       width: 960px;
     }
   }
+
+  .arrow-icon {
+    width: 16px;
+    height: 48px;
+    margin: 320px 0;
+    border-radius: 15px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    cursor: pointer;
+    background-color: #bee2ff;
+  }
+
+  .arrow-icon:hover {
+    color: #fff;
+    background-color: #0052d9;
+  }
 </style>