Преглед изворни кода

feat: 增加小地图下载底图功能

louhangfei пре 1 година
родитељ
комит
2d649335a9
1 измењених фајлова са 425 додато и 375 уклоњено
  1. 425 375
      src/views/map-config/mini-map/MiniMapConfig.vue

+ 425 - 375
src/views/map-config/mini-map/MiniMapConfig.vue

@@ -2,17 +2,28 @@
   <div class="min-map">
     <header class="min-map__header">
       <section class="min-map__btn" @click="router.back">
-        <img :src="rollback">
+        <img :src="rollback" />
         <span>返回</span>
       </section>
       <section class="workshop-name">{{ selectedName }}</section>
       <section class="operate-btn">
-        <el-upload class="avatar-uploader" :action="actionUrl" :show-file-list="false" :on-success="handleAvatarSuccess"
-          :with-credentials="true" name="file" :headers="getHeaders()">
+        <a @click="downloadImage(imgUrlBg, selectedName + '.png')" v-if="imgUrlBg">下载底图</a>
+        <el-upload
+          class="avatar-uploader"
+          :action="actionUrl"
+          :show-file-list="false"
+          :on-success="handleAvatarSuccess"
+          :with-credentials="true"
+          name="file"
+          :headers="getHeaders()"
+        >
           <el-button :icon="UploadFilled" :disabled="!hasBg"> 替换照片 </el-button>
         </el-upload>
-        <el-button :icon="Refresh" @click="openMessageBox('提示', '是否重置当前设置', refreshUploadBg, '重置成功!')"
-          :disabled="!hasBg">
+        <el-button
+          :icon="Refresh"
+          @click="openMessageBox('提示', '是否重置当前设置', refreshUploadBg, '重置成功!')"
+          :disabled="!hasBg"
+        >
           重置布局
         </el-button>
         <el-button @click="handleSave" type="primary">保存</el-button>
@@ -24,14 +35,18 @@
         <header class="camera-list__title">相机列表:</header>
         <ElInput class="camera-list__search" placeholder="请输入搜索内容" v-model="searchKey" :suffix-icon="Search" />
         <main class="camera-item">
-          <span class="camera-item__empty" v-show="filterShopCameraList.length == 0">
-            提示:该车间还未配置相机
-          </span>
-          <div v-for="(item, index) in filterShopCameraList" :key="item.code" class="camera-item__list" :class="{
-            isAdded: isAddedCamera(item.code),
-            isActive: item.code === caremaActiveId,
-            integrationState: item.integrationState === 1,
-          }" @click="handleAddCamera(item.code, index)">
+          <span class="camera-item__empty" v-show="filterShopCameraList.length == 0"> 提示:该车间还未配置相机 </span>
+          <div
+            v-for="(item, index) in filterShopCameraList"
+            :key="item.code"
+            class="camera-item__list"
+            :class="{
+              isAdded: isAddedCamera(item.code),
+              isActive: item.code === caremaActiveId,
+              integrationState: item.integrationState === 1,
+            }"
+            @click="handleAddCamera(item.code, index)"
+          >
             <span class="camera-id">{{ item.name }}</span>
             <el-popover placement="right-start" trigger="hover" :content="item.workSpaceName" :teleported="false">
               <template #reference>
@@ -43,413 +58,448 @@
       </section>
 
       <section class="workshop-map" ref="drawContainer">
-        <el-upload v-if="!hasBg" :action="actionUrl" :show-file-list="false" :before-upload="handleBeforeUpload"
-          :on-success="handleAvatarSuccess" :with-credentials="true" name="file" :headers="getHeaders()">
+        <el-upload
+          v-if="!hasBg"
+          :action="actionUrl"
+          :show-file-list="false"
+          :before-upload="handleBeforeUpload"
+          :on-success="handleAvatarSuccess"
+          :with-credentials="true"
+          name="file"
+          :headers="getHeaders()"
+        >
           <img src="~@/assets/images/img-upload.png" />
         </el-upload>
-        <KonvaMap ref="konvaMap" :filter-data="filterShopCameraList" :camera-list="shopCameraList"
-          :map-config="mapConfig" :is-knova-destroy="isKnovaDestroy" @change-default-camera="changeDefault"
-          @send-camera-id="sendCameras" @change="changeMap" v-else />
+        <KonvaMap
+          ref="konvaMap"
+          :filter-data="filterShopCameraList"
+          :camera-list="shopCameraList"
+          :map-config="mapConfig"
+          :is-knova-destroy="isKnovaDestroy"
+          @change-default-camera="changeDefault"
+          @send-camera-id="sendCameras"
+          @change="changeMap"
+          v-else
+        />
       </section>
     </main>
   </div>
 </template>
 
 <script setup lang="ts">
-import { ElMessage, ElInput, ElMessageBox } from 'element-plus';
-import { Search, Refresh, UploadFilled } from '@element-plus/icons-vue';
-import rollback from '@/assets/rollback.png'
-import { onMounted, onUnmounted, ref, computed, reactive, nextTick } from 'vue';
-import { updateMinMapViewLayoutApi, getCamerasByWorkShopId, getWorkshopMiniMapLayoutPCApi, getWorkshopMiniMapLayoutMobileApi } from '@/api/scene/scene';
-import KonvaMap from './MapBase/KonvaMap.vue';
-import useCameraStatus from '@/views/cameras/preview/store/useCameraStatus';
-import { onBeforeRouteLeave,useRoute } from 'vue-router';
-import urlJoin from 'url-join';
-import { useGlobSetting } from '@/hooks/setting';
-import { getHeaders } from '@/utils/http/axios';
-import router from '@/router';
-import { ShopMapCamera } from '@/types/scene/type'
-import { ViewType } from '@/types/page-config/type';
-import { openMessageBox } from '@/views/system-config/business-scene/components/MessageBox';
-
-const cameraStatus = useCameraStatus();
-const { openIntervalNew, closeInterval } = cameraStatus;
-
-const drawContainer = ref<HTMLDivElement>();
-const konvaMap = ref();
-const caremaActiveId = ref<string>('');
-const camerasAdded = ref<string[]>([]);
-const imgUrlBg = ref<string>('');
-const searchKey = ref('');
-
-// 是否已有背景图
-const hasBg = ref(false);
-//是否修改
-const isChange = ref<boolean>(false);
-//单个相机时是否上传图片
-const isUploadBg = ref<boolean>(true);
-
-const isMap = ref(false);
-const { urlPrefix } = useGlobSetting();
-
-const actionUrl = computed(() => {
-  return urlJoin(urlPrefix!, `/admin/minimap/uploadPicture`);
-});
-
-function updataState(data, updateData) {
-  for (let i = 0; i < data.length; i++) {
-    const camera = data[i];
-    const matchedCamera = updateData.find((item) => item.cameraCode === camera.code);
-    if (matchedCamera) {
-      camera.status = matchedCamera.status;
-      camera.integrationState = matchedCamera.integrationState;
+  import { ElMessage, ElInput, ElMessageBox } from 'element-plus';
+  import { Search, Refresh, UploadFilled } from '@element-plus/icons-vue';
+  import rollback from '@/assets/rollback.png';
+  import { onMounted, onUnmounted, ref, computed, reactive, nextTick } from 'vue';
+  import {
+    updateMinMapViewLayoutApi,
+    getCamerasByWorkShopId,
+    getWorkshopMiniMapLayoutPCApi,
+    getWorkshopMiniMapLayoutMobileApi,
+  } from '@/api/scene/scene';
+  import KonvaMap from './MapBase/KonvaMap.vue';
+  import useCameraStatus from '@/views/cameras/preview/store/useCameraStatus';
+  import { onBeforeRouteLeave, useRoute } from 'vue-router';
+  import urlJoin from 'url-join';
+  import { useGlobSetting } from '@/hooks/setting';
+  import { getHeaders } from '@/utils/http/axios';
+  import router from '@/router';
+  import { ShopMapCamera } from '@/types/scene/type';
+  import { ViewType } from '@/types/page-config/type';
+  import { openMessageBox } from '@/views/system-config/business-scene/components/MessageBox';
+
+  const cameraStatus = useCameraStatus();
+  const { openIntervalNew, closeInterval } = cameraStatus;
+
+  const drawContainer = ref<HTMLDivElement>();
+  const konvaMap = ref();
+  const caremaActiveId = ref<string>('');
+  const camerasAdded = ref<string[]>([]);
+  const imgUrlBg = ref<string>('');
+  const searchKey = ref('');
+
+  // 是否已有背景图
+  const hasBg = ref(false);
+  //是否修改
+  const isChange = ref<boolean>(false);
+  //单个相机时是否上传图片
+  const isUploadBg = ref<boolean>(true);
+
+  const isMap = ref(false);
+  const { urlPrefix } = useGlobSetting();
+
+  const actionUrl = computed(() => {
+    return urlJoin(urlPrefix!, `/admin/minimap/uploadPicture`);
+  });
+
+  function updataState(data, updateData) {
+    for (let i = 0; i < data.length; i++) {
+      const camera = data[i];
+      const matchedCamera = updateData.find((item) => item.cameraCode === camera.code);
+      if (matchedCamera) {
+        camera.status = matchedCamera.status;
+        camera.integrationState = matchedCamera.integrationState;
+      }
     }
   }
-}
 
-const refreshUploadBg = () => {
-  konvaMap.value.resetMap();
-  hasBg.value = false;
-};
+  function downloadImage(url: string, filename: string) {
+    const link = document.createElement('a');
+    link.href = url;
+    link.download = filename;
 
-const handleBeforeUpload = () => {
-  if (!selectedShopId.value) {
-    ElMessage.error({
-      message: '请先选择车间',
-    });
-    return false;
+    // 触发点击事件
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
   }
-};
 
-const sendCameras = (camerasList) => {
-  camerasAdded.value = camerasList.map((item) => {
-    return item.id;
-  });
-};
-
-/** 判断相机是否已经添加 */
-const isAddedCamera = (cameraId: string) => {
-  const index = camerasAdded.value.findIndex((item) => item === cameraId);
-  return index >= 0;
-};
-
-const changeDefault = (defaultCameraId) => {
-  caremaActiveId.value = defaultCameraId;
-};
-
-const isKnovaDestroy = ref<boolean>(false) //重新上传图片之后将knova销毁
-const handleAvatarSuccess = (e) => {
-  isKnovaDestroy.value = true;
-  imgUrlBg.value = e.data;
-  hasBg.value = true;
-  nextTick(() => {
-    konvaMap.value.addBg(imgUrlBg.value).then(() => {
-      isKnovaDestroy.value = false;
-      isChange.value = true;
-    });
-  })
-};
-
-const getMapLayoutAPIMap = {
-  [ViewType.minimap_PC]: getWorkshopMiniMapLayoutPCApi,
-  [ViewType.minimap_phone]: getWorkshopMiniMapLayoutMobileApi,
-}
-
-const getMapLayout = async (id: number) => {
-  const api = getMapLayoutAPIMap[viewType.value];
-  const res = await api(id);
-  if (!res) return;
-  const layoutJSON = JSON.parse(res.layout);
-  return layoutJSON;
-}
-
-const getShopContent = async (id: number) => {
-  await getShowCameras(id);
-  const idList = filterShopCameraList.value.map((item) => item.id);
-  const res = await getMapLayout(id);
-  if (!res) return;
-  hasBg.value = true;
-  isMap.value = res.isUploadBg;
-  nextTick(() => {
-    if (res.isUploadBg) {
-      isUploadBg.value = true;
-      konvaMap.value.createMap(res, selectedShopId.value);
-    } else {
-      hasBg.value = false;
-      isUploadBg.value = res.isUploadBg;
+  const refreshUploadBg = () => {
+    konvaMap.value.resetMap();
+    hasBg.value = false;
+  };
+
+  const handleBeforeUpload = () => {
+    if (!selectedShopId.value) {
+      ElMessage.error({
+        message: '请先选择车间',
+      });
+      return false;
     }
-    openIntervalNew(idList, (targetData) => {
-      updataState(filterShopCameraList.value, targetData);
+  };
+
+  const sendCameras = (camerasList) => {
+    camerasAdded.value = camerasList.map((item) => {
+      return item.id;
     });
-  })
-};
-
-const selectedShopId = ref();
-const selectedName = ref();
-const viewType = ref();
-const shopCameraList = ref<ShopMapCamera[]>([]);
-const route = useRoute();
-
-const getShowCameras = async (id: number) => {
-  const res = await getCamerasByWorkShopId({ workshopId: id });
-  if (!res) return;
-  res.children.forEach((item) => {
-    if (!item.children || item.children.length <= 0) return
-    item.children.forEach(camera => {
-      shopCameraList.value.push({ ...camera, isSet: 0, workSpaceName: item.name })
-    })
-  })
-};
-
-const mapConfig = reactive({
-  width: 0,
-  height: 0
-});
-
-onMounted(async () => {
-  selectedShopId.value = Number(route.query.workshopId);
-  selectedName.value = route.query.workshopName;
-  viewType.value = route.query.viewType;
-  mapConfig.width = drawContainer.value?.clientWidth || 0;
-  mapConfig.height = drawContainer.value?.clientHeight || 0;
-  await getShopContent(selectedShopId.value);
-});
-
-onUnmounted(() => {
-  closeInterval();
-});
-
-const filterShopCameraList = computed(() => {
-  const k = searchKey.value.trim();
-  if (!k) return shopCameraList.value;
-  return shopCameraList.value.filter(
-    (x) => x.code?.includes(k) || x.name?.includes(k) || x.workSpaceName?.includes(k),
-  );
-});
-
-const handleAddCamera = (cameraId: string, index: number) => {
-  if (isAddedCamera(cameraId)) {
-    const camera = konvaMap.value.findCamera(cameraId);
-    konvaMap.value.handleCameraClick(camera);
-    return;
-  }
-  if (!hasBg.value) {
-    ElMessage.warning({
-      message: '请先添加车间地图',
+  };
+
+  /** 判断相机是否已经添加 */
+  const isAddedCamera = (cameraId: string) => {
+    const index = camerasAdded.value.findIndex((item) => item === cameraId);
+    return index >= 0;
+  };
+
+  const changeDefault = (defaultCameraId) => {
+    caremaActiveId.value = defaultCameraId;
+  };
+
+  const isKnovaDestroy = ref<boolean>(false); //重新上传图片之后将knova销毁
+  const handleAvatarSuccess = (e) => {
+    isKnovaDestroy.value = true;
+    imgUrlBg.value = e.data;
+    hasBg.value = true;
+    nextTick(() => {
+      konvaMap.value.addBg(imgUrlBg.value).then(() => {
+        isKnovaDestroy.value = false;
+        isChange.value = true;
+      });
     });
-    return;
-  }
-  konvaMap.value.addCamera(cameraId, index);
-};
-
-const handleSave = () => {
-  isMap.value = true;
-  if(!hasBg.value && isMap.value){
-    ElMessage.error('请先添加车间地图');
-    return false;
-  }
-  const layout = konvaMap.value.saveLayout();
-  const cameraList = JSON.parse(layout).cameraList;
-  if (cameraList.length === 0 && hasBg.value) {
-    ElMessage.error('请至少添加1个相机标签后发布');
-    return false;
-  }
-  updateMinMapViewLayoutApi({
-    layout: JSON.stringify({ ...JSON.parse(layout), isUploadBg: hasBg.value }),
-    targetId: String(selectedShopId.value),
-    viewType: viewType.value,
-  }).then(() => {
-    ElMessage.success('保存成功');
-  });
-  return true;
-};
-
-const changeMap = (val) => {
-  isChange.value = val;
-};
-onBeforeRouteLeave((to,from,next)=>{
-  if(!isChange.value) {
-    next();
-    return;
-  }
-  setTimeout(()=>{
-    ElMessageBox.confirm('是否保存当前修改?', '提示', {
-      confirmButtonText: '是',
-      cancelButtonText: '否',
-      customClass: 'elMessageBox__custom--warning',
-    }).then(async () => {
-      const isSaveSuccess = await handleSave();
-      if(isSaveSuccess) {
-        next();
+  };
+
+  const getMapLayoutAPIMap = {
+    [ViewType.minimap_PC]: getWorkshopMiniMapLayoutPCApi,
+    [ViewType.minimap_phone]: getWorkshopMiniMapLayoutMobileApi,
+  };
+
+  const getMapLayout = async (id: number) => {
+    const api = getMapLayoutAPIMap[viewType.value];
+    const res = await api(id);
+    if (!res) return;
+    const layoutJSON = JSON.parse(res.layout);
+    return layoutJSON;
+  };
+
+  const getShopContent = async (id: number) => {
+    await getShowCameras(id);
+    const idList = filterShopCameraList.value.map((item) => item.id);
+    const res = await getMapLayout(id);
+    if (!res) return;
+    hasBg.value = true;
+    isMap.value = res.isUploadBg;
+    imgUrlBg.value = res.bgImgUrl;
+    nextTick(() => {
+      if (res.isUploadBg) {
+        isUploadBg.value = true;
+        konvaMap.value.createMap(res, selectedShopId.value);
+      } else {
+        hasBg.value = false;
+        isUploadBg.value = res.isUploadBg;
       }
-      next(false);
-    }).catch(() => {
-      next()
-    })
-  },200)
-})
-</script>
+      openIntervalNew(idList, (targetData) => {
+        updataState(filterShopCameraList.value, targetData);
+      });
+    });
+  };
+
+  const selectedShopId = ref();
+  const selectedName = ref();
+  const viewType = ref();
+  const shopCameraList = ref<ShopMapCamera[]>([]);
+  const route = useRoute();
+
+  const getShowCameras = async (id: number) => {
+    const res = await getCamerasByWorkShopId({ workshopId: id });
+    if (!res) return;
+    res.children.forEach((item) => {
+      if (!item.children || item.children.length <= 0) return;
+      item.children.forEach((camera) => {
+        shopCameraList.value.push({ ...camera, isSet: 0, workSpaceName: item.name });
+      });
+    });
+  };
 
-<style scoped lang="scss">
-.min-map {
-  display: flex;
-  flex-direction: column;
-  gap: 4px;
-  width: 100%;
-  height: calc(100vh - 64px - 14px);
-  background: #f5f7f9;
-  border-radius: 6px;
-  box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.12);
-
-  &__header {
-    display: flex;
-    align-items: center;
-    gap: 20px;
-    width: inherit;
-    height: 54px;
-    padding: 0 15px 0 15px;
-    background: #ffffff;
-    border-radius: 6px 6px 0 0;
-  }
+  const mapConfig = reactive({
+    width: 0,
+    height: 0,
+  });
 
-  &__btn {
-    display: flex;
-    gap: 10px;
-    align-items: center;
-    font-size: 14px;
-    cursor: pointer;
+  onMounted(async () => {
+    selectedShopId.value = Number(route.query.workshopId);
+    selectedName.value = route.query.workshopName;
+    viewType.value = route.query.viewType;
+    mapConfig.width = drawContainer.value?.clientWidth || 0;
+    mapConfig.height = drawContainer.value?.clientHeight || 0;
+    await getShopContent(selectedShopId.value);
+  });
 
-    img {
-      width: 14px;
+  onUnmounted(() => {
+    closeInterval();
+  });
+
+  const filterShopCameraList = computed(() => {
+    const k = searchKey.value.trim();
+    if (!k) return shopCameraList.value;
+    return shopCameraList.value.filter(
+      (x) => x.code?.includes(k) || x.name?.includes(k) || x.workSpaceName?.includes(k),
+    );
+  });
+
+  const handleAddCamera = (cameraId: string, index: number) => {
+    if (isAddedCamera(cameraId)) {
+      const camera = konvaMap.value.findCamera(cameraId);
+      konvaMap.value.handleCameraClick(camera);
+      return;
     }
-  }
+    if (!hasBg.value) {
+      ElMessage.warning({
+        message: '请先添加车间地图',
+      });
+      return;
+    }
+    konvaMap.value.addCamera(cameraId, index);
+  };
+
+  const handleSave = () => {
+    isMap.value = true;
+    if (!hasBg.value && isMap.value) {
+      ElMessage.error('请先添加车间地图');
+      return false;
+    }
+    const layout = konvaMap.value.saveLayout();
+    const cameraList = JSON.parse(layout).cameraList;
+    if (cameraList.length === 0 && hasBg.value) {
+      ElMessage.error('请至少添加1个相机标签后发布');
+      return false;
+    }
+    updateMinMapViewLayoutApi({
+      layout: JSON.stringify({ ...JSON.parse(layout), isUploadBg: hasBg.value }),
+      targetId: String(selectedShopId.value),
+      viewType: viewType.value,
+    }).then(() => {
+      ElMessage.success('保存成功');
+    });
+    return true;
+  };
+
+  const changeMap = (val) => {
+    isChange.value = val;
+  };
+  onBeforeRouteLeave((to, from, next) => {
+    if (!isChange.value) {
+      next();
+      return;
+    }
+    setTimeout(() => {
+      ElMessageBox.confirm('是否保存当前修改?', '提示', {
+        confirmButtonText: '是',
+        cancelButtonText: '否',
+        customClass: 'elMessageBox__custom--warning',
+      })
+        .then(async () => {
+          const isSaveSuccess = await handleSave();
+          if (isSaveSuccess) {
+            next();
+          }
+          next(false);
+        })
+        .catch(() => {
+          next();
+        });
+    }, 200);
+  });
+</script>
 
-  &__main {
+<style scoped lang="scss">
+  .min-map {
     display: flex;
-    gap: 5px;
+    flex-direction: column;
+    gap: 4px;
     width: 100%;
-    height: calc(100% - 60px);
+    height: calc(100vh - 64px - 14px);
+    background: #f5f7f9;
     border-radius: 6px;
-  }
-}
-
-.workshop-name {
-  font-size: 14px;
-  color: #3f3f3f;
-  font-weight: 600;
-}
-
-.operate-btn {
-  display: flex;
-  justify-content: flex-end;
-  align-items: center;
-  flex: 1;
-  height: inherit;
-  gap: 20px;
-
-  .el-button {
-    margin: 0;
-
-    img {
-      width: 14px;
+    box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.12);
+
+    &__header {
+      display: flex;
+      align-items: center;
+      gap: 20px;
+      width: inherit;
+      height: 54px;
+      padding: 0 15px 0 15px;
+      background: #ffffff;
+      border-radius: 6px 6px 0 0;
     }
-  }
-}
 
-.camera-list {
-  width: 250px;
-  height: 100%;
-  border-radius: 0 0 0 6px;
-  padding: 10px 10px;
-  background-color: #ffffff;
+    &__btn {
+      display: flex;
+      gap: 10px;
+      align-items: center;
+      font-size: 14px;
+      cursor: pointer;
 
-  &__title {
-    margin-left: 5px;
+      img {
+        width: 14px;
+      }
+    }
+
+    &__main {
+      display: flex;
+      gap: 5px;
+      width: 100%;
+      height: calc(100% - 60px);
+      border-radius: 6px;
+    }
+  }
+
+  .workshop-name {
     font-size: 14px;
+    color: #3f3f3f;
     font-weight: 600;
   }
 
-  &__search {
-    margin-top: 10px;
+  .operate-btn {
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    flex: 1;
+    height: inherit;
+    gap: 20px;
 
-    :deep(.el-input__wrapper) {
-      background-color: #f0f2f5;
+    .el-button {
+      margin: 0;
+
+      img {
+        width: 14px;
+      }
     }
   }
-}
-
-.workshop-map {
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  flex: 1;
-  height: 100%;
-  overflow: hidden;
-}
-
-.camera-item {
-  width: 100%;
-  max-height: calc(100% - 65px);
-  overflow-y: auto;
-  margin-top: 10px;
-  margin-left: 5px;
-
-  &__empty {
-    color: #3f3f3f;
+
+  .camera-list {
+    width: 250px;
+    height: 100%;
+    border-radius: 0 0 0 6px;
+    padding: 10px 10px;
+    background-color: #ffffff;
+
+    &__title {
+      margin-left: 5px;
+      font-size: 14px;
+      font-weight: 600;
+    }
+
+    &__search {
+      margin-top: 10px;
+
+      :deep(.el-input__wrapper) {
+        background-color: #f0f2f5;
+      }
+    }
   }
 
-  &__list {
+  .workshop-map {
     display: flex;
-    justify-content: space-between;
+    justify-content: center;
     align-items: center;
+    flex: 1;
+    height: 100%;
+    overflow: hidden;
+  }
+
+  .camera-item {
     width: 100%;
-    height: 32px;
-    font-size: 14px;
-    font-weight: 400;
-    color: #404040;
-    line-height: 14px;
-    cursor: pointer;
+    max-height: calc(100% - 65px);
+    overflow-y: auto;
+    margin-top: 10px;
+    margin-left: 5px;
 
-    &:hover {
-      background-color: #e6f7ff;
-      color: #1890ff;
+    &__empty {
+      color: #3f3f3f;
     }
 
-    .camera-id {
-      width: 110px;
-    }
+    &__list {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      width: 100%;
+      height: 32px;
+      font-size: 14px;
+      font-weight: 400;
+      color: #404040;
+      line-height: 14px;
+      cursor: pointer;
+
+      &:hover {
+        background-color: #e6f7ff;
+        color: #1890ff;
+      }
+
+      .camera-id {
+        width: 110px;
+      }
 
-    .camera-space {
-      width: 120px;
-      white-space: nowrap;
-      overflow: hidden;
-      text-overflow: ellipsis;
+      .camera-space {
+        width: 120px;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+      }
     }
   }
-}
-
-:deep(.el-popover) {
-  width: unset !important;
-  min-width: 110px;
-  text-align: center;
-  font-weight: 400;
-}
-
-:deep(.el-popper__arrow) {
-  display: none;
-}
-
-.isAdded {
-  color: #1890ff;
-}
-
-.isActive {
-  background-color: #e6f7ff;
-  color: #1890ff;
-}
-
-.integrationState {
-  cursor: not-allowed;
-  color: #ccc;
-}
+
+  :deep(.el-popover) {
+    width: unset !important;
+    min-width: 110px;
+    text-align: center;
+    font-weight: 400;
+  }
+
+  :deep(.el-popper__arrow) {
+    display: none;
+  }
+
+  .isAdded {
+    color: #1890ff;
+  }
+
+  .isActive {
+    background-color: #e6f7ff;
+    color: #1890ff;
+  }
+
+  .integrationState {
+    cursor: not-allowed;
+    color: #ccc;
+  }
 </style>