Просмотр исходного кода

fefactor: 重构车间小地图模块

chauncey 1 год назад
Родитель
Сommit
f2c483fb44

+ 118 - 51
src/views/map-config/mini-map/MapBase/KonvaMap.vue

@@ -1,56 +1,67 @@
 <template>
-  <div class="konva-map" ref="konvaMapRef">
-    <v-stage ref="stageAll" :config="stageConfig" @click="handleStageClick">
-      <v-layer ref="layer">
-        <v-image :config="bgConfig" v-show="bgImgUrl" />
-        <v-group v-for="camera in cameras" :key="camera.id" :id="camera.id" :config="camera.groupConfig"
-          @click="handleCameraClick(camera)" @mouseover="(e) => handleMouseOver(e)" @mouseleave="handleMouseLeave()"
-          @dragstart="handleDragStart()">
-          <v-image :config="camera.config" />
-          <v-image v-if="camera.isDefault" ref="defaultIcon" :config="defaultIconConfig" />
-        </v-group>
-        <v-transformer :config="transformerConfig" ref="transformer" />
-      </v-layer>
-    </v-stage>
-    <div v-show="defaultShow" class="opt-container"
-      :style="{ position: 'absolute', left: posX + 'px', top: posY + 'px' }">
-      <div class="opt-item" :class="{ disabled: disabledSet }" @click="setDefaultCamera">设为默认相机</div>
-      <div class="opt-item" @click="previewCamera">预览相机</div>
-      <div class="opt-item" @click="handleDeleteLabel">删除标签</div>
+  <div class="konva-map">
+    <div class="map-container">
+      <div v-moveable:1 v-if="!isKnovaDestroy">
+        <v-stage ref="stageAll" :config="stageConfig" @click="handleStageClick">
+          <v-layer ref="layer">
+            <v-image :config="bgConfig" v-show="bgImgUrl" />
+            <v-group v-for="camera in cameras" :key="camera.id" :id="camera.id" :config="camera.groupConfig"
+              @click="handleCameraClick(camera)" @mouseover="(e) => handleMouseOver(e)" @mouseleave="handleMouseLeave()"
+              @dragstart="handleDragStart()">
+              <v-image :config="camera.config" />
+              <v-image v-if="camera.isDefault" ref="defaultIcon" :config="defaultIconConfig" />
+            </v-group>
+            <v-transformer :config="transformerConfig" ref="transformer" />
+          </v-layer>
+        </v-stage>
+        <div v-show="defaultShow" class="opt-container"
+          :style="{ position: 'absolute', left: posX + 'px', top: posY + 'px' }">
+          <div class="opt-item" :class="{ disabled: disabledSet }" @click="setDefaultCamera">设为默认相机</div>
+          <div class="opt-item" @click="previewCamera">预览相机</div>
+          <div class="opt-item" @click="handleDeleteLabel">删除标签</div>
+        </div>
+
+        <CameraPreview v-if="isShow" :last-pos-x="posX!" :last-pos-y="posY!" :video-url="videoUrl"
+          @close="closePreview" />
+
+        <DefaultTip v-show="selectCameraId" :position="pos" :is-default="isDefaultCamera" :camera-info="cameraInfo"
+          :style="{ position: 'absolute', left: posTipX + 'px', top: posTipY + 'px' }" />
+      </div>
     </div>
-
-    <CameraPreview v-if="isShow" :last-pos-x="posX!" :last-pos-y="posY!" :video-url="videoUrl" @close="closePreview" />
-
-    <DefaultTip v-show="selectCameraId" :position="pos" :is-default="isDefaultCamera" :camera-info="cameraInfo"
-      :style="{ position: 'absolute', left: posTipX + 'px', top: posTipY + 'px' }" />
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted, watch } from 'vue';
+import { ref, onMounted, watch, computed } from 'vue';
 import { ElMessage } from 'element-plus';
 import DefaultTip from '../components/DefaultTip.vue';
 import cameraImgSrc from '@/assets/camera/cameraImg.png';
 import favoritesImgSrc from '@/assets/camera/favorites.png';
-import { TipPositionEnum, camerasGroupType } from '../type';
+import { TipPositionEnum, camerasGroupType, cameraInfoType } from '../type';
 import { cloneDeep } from 'lodash-es';
-// import useMiniMap from '../use-mini-map';
-// import { storeToRefs } from 'pinia';
 import { updateMinMapViewLayoutApi } from '@/api/scene/scene';
 import CameraPreview from './CameraPreview.vue';
 import { ShopMapCamera } from '@/types/scene/type'
 import { openMessageBox } from '@/views/system-config/business-scene/components/MessageBox';
 
-// const miniMap = useMiniMap();
-// const { shopCameraList } = storeToRefs(miniMap);
-
 const emit = defineEmits(['changeDefaultCamera', 'sendCameraId', 'change']);
-const props = defineProps<{ filterData: ShopMapCamera[], cameraList: ShopMapCamera[] }>();
+interface MapConfigType {
+  width: number;
+  height: number
+}
+const props = defineProps<{ filterData: ShopMapCamera[], cameraList: ShopMapCamera[], mapConfig: MapConfigType, isKnovaDestroy: boolean }>();
+const transformerScale = computed(() => {
+  if (stageConfig.value.width === 0 || stageConfig.value.height === 0) return 1;
+  return Math.min(
+    props.mapConfig.width / stageConfig.value.width,
+    props.mapConfig.height / stageConfig.value.height
+  )
+})
 const camImg = new Image();
 
 const stageConfig = ref({
-  width: 800,
-  height: 600,
+  width: 0,
+  height: 0,
 });
 
 const bgImg = new Image();
@@ -69,7 +80,7 @@ const lastClickedGroupId = ref<string | null>(null);
 const lastClickedVideoUrl = ref<string | null>(null);
 
 const bgImgUrl = ref<string | null>('');
-const cameraInfo = ref<camerasGroupType>();
+const cameraInfo = ref<cameraInfoType>();
 const cameras = ref<camerasGroupType[]>([]);
 //默认相机id
 const defaultCameraId = ref('');
@@ -116,7 +127,7 @@ const transformerConfig = ref({
 const resizeContainer = (width, height) => {
   stageConfig.value.width = width;
   stageConfig.value.height = height;
-  layer.value.getNode().draw();
+  // layer.value.getNode().draw();
 };
 
 const defaultIconConfig = ref({
@@ -137,9 +148,19 @@ const handleMouseOver = (e) => {
 
   const group = e.target.parent;
   selectCameraId.value = group.id();
-  cameraInfo.value = cameras.value.find((camera) => camera.id === selectCameraId.value)
   let defaultNode;
   const stage = transformer.value.getNode().getStage();
+  const groups = stage.find('.group');
+  const selectedCameraAttrs = groups.find((item) => item.attrs.id === selectCameraId.value).attrs;
+  cameraInfo.value = {
+    x: selectedCameraAttrs.x,
+    y: selectedCameraAttrs.y,
+    sizeX: selectCameraId.value === defaultCameraId.value ? cameraIconSize.width + defaultIconConfig.value.width : cameraIconSize.width,
+    sizeY: cameraIconSize.height,
+    scaleX: selectedCameraAttrs.scaleX,
+    scaleY: selectedCameraAttrs.scaleY,
+    rotation: selectedCameraAttrs.rotation
+  }
   if (selectCameraId.value === defaultCameraId.value) {
     isDefaultCamera.value = true;
     defaultNode = stage.findOne('#defaultIcon');
@@ -232,9 +253,46 @@ const handleCameraClick = (camera) => {
   transformerNode.moveToTop();
   emit('change', true);
 };
+/**
+ * @description 根据相机Icon的大小动态配置相机Icon的拖拽边界
+ * @param configWidth 相机Icon的宽度
+ * @param configHeight 相机Icon的高度
+ * @author chauncey
+ */
+function dymamicdragBoundFunc(configWidth: number, configHeight: number) {
+  return (pos: { x: number, y: number }) => {
+    const RestrictedX = Math.max(0, Math.min(bgConfig.value.width - configWidth, pos.x));
+    const RestrictedY = Math.max(0, Math.min(bgConfig.value.height - configHeight, pos.y))
+    return {
+      x: RestrictedX,
+      y: RestrictedY
+    }
+  }
+}
+/**
+ * @description 根据index动态配置相机出现位置
+ * @param index 
+ * @author chauncey
+ */
+
+function dymamicCameraConfig(index: number) {
+  const maxColumn = Math.floor((bgConfig.value.height - 16) / cameraIconSize.height);
+  const column = Math.floor(index / maxColumn);
+  const row = index % maxColumn;
+  const x = column * cameraIconSize.width;
+  const y = row * cameraIconSize.height + 16;
+  const dragBoundFunc = dymamicdragBoundFunc(cameraIconSize.width, cameraIconSize.height)
+  return {
+    x,
+    y,
+    draggable: true,
+    name: 'group',
+    dragBoundFunc
+  }
+}
 
 //添加相机
-const addCamera = (id: string) => {
+const addCamera = (id: string, index: number) => {
   const existingCamera = cameras.value.find((camera) => camera.id === id);
   if (existingCamera) return;
   const config = {
@@ -244,12 +302,7 @@ const addCamera = (id: string) => {
     name: 'image',
     id: id,
   };
-  const groupConfig = {
-    x: 50,
-    y: 50,
-    draggable: true,
-    name: 'group',
-  };
+  const groupConfig = dymamicCameraConfig(index);
   const cameraDetail = {
     id,
     groupConfig,
@@ -394,12 +447,6 @@ const handleDeleteLabel = () => {
   }
   emit('change', true);
 }
-//删除相机
-// const handleKeyDown = (e) => {
-//   if (e.keyCode === 46 || e.code === 'Delete' || e.keyCode === 8 || e.code === 'Backspace') {
-//     handleDeleteLabel();
-//   }
-// };
 
 //重置
 const resetMap = () => {
@@ -452,8 +499,17 @@ const createMap = (layout, selectId) => {
         viewType: 2
       });
     }
-
-    cameras.value = layout.cameraList;
+    cameras.value = layout.cameraList.map((camera) => {
+      const width = camera.config.width * camera.groupConfig.scaleX;
+      const height = camera.config.height * camera.groupConfig.scaleY;
+      return {
+        ...camera,
+        groupConfig: {
+          ...camera.groupConfig,
+          dragBoundFunc: dymamicdragBoundFunc(width, height)
+        }
+      }
+    })
   });
 };
 
@@ -473,6 +529,17 @@ onMounted(() => {
 </script>
 
 <style scoped lang="scss">
+.konva-map {
+  background: #0009;
+  scale: v-bind(transformerScale);
+}
+
+.map-container {
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+}
+
 .opt-container {
   width: 160px;
   padding: 10px;

+ 179 - 236
src/views/map-config/mini-map/MiniMapConfig.vue

@@ -1,124 +1,94 @@
 <template>
-  <div class="page">
-    <div class="page-head flex items-center">
-      <div class="page-head__btn" @click="router.back">
-        <img :src="rollback" />
+  <div class="min-map">
+    <header class="min-map__header">
+      <section class="min-map__btn" @click="router.back">
+        <img :src="rollback">
         <span>返回</span>
-      </div>
-      <div class="head-opt flex-1 flex justify-between items-center">
-        <div class="workshop-name">{{ selectedName }}</div>
-        <div class="flex">
-          <el-upload class="avatar-uploader flex justify-center items-center" :action="actionUrl"
-            :show-file-list="false" :on-success="handleAvatarSuccess" :with-credentials="true" name="file"
-            :headers="getHeaders()">
-            <el-button :icon="Refresh" :disabled="!hasBg"> 替换照片 </el-button>
-          </el-upload>
-          <el-button :icon="Refresh" @click="openMessageBox('提示', '是否重置当前设置', refreshUploadBg, '重置成功!')"
-            style="margin-left: 40px" :disabled="!hasBg">
-            重置布局
-          </el-button>
-          <el-button @click="handleSave" style="margin-left: 40px" type="primary">保存为布局
-          </el-button>
-        </div>
-      </div>
-    </div>
-    <div class="paint-tool flex">
-      <div class="camera-list">
-        <div>
-          <span class="label-text flex">相机列表:</span>
-          <ElInput class="search-put" style="margin: 10px 0; width: 230px" placeholder="请输入搜索内容" v-model="searchKey"
-            :suffix-icon="Search" />
-        </div>
-        <span v-if="filterShopCameraList.length == 0" class="ml-1" style="color: #3f3f3f">
-          提示:该车间还未配置相机
-        </span>
-        <el-scrollbar v-else style="position: relative; height: calc(100% - 90px)">
-          <div v-for="item in filterShopCameraList" :key="item.code" class="camera-item flex justify-start items-center"
-            :class="{
-              isAdded: isAddedCamera(item.code),
-              isActive: item.code === caremaActiveId,
-              integrationState: item.integrationState === 1,
-            }" @click="handleAddCamera(item.code)">
+      </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()">
+          <el-button :icon="UploadFilled" :disabled="!hasBg"> 替换照片 </el-button>
+        </el-upload>
+        <el-button :icon="Refresh" @click="openMessageBox('提示', '是否重置当前设置', refreshUploadBg, '重置成功!')"
+          :disabled="!hasBg">
+          重置布局
+        </el-button>
+        <el-button @click="handleSave" type="primary"><img :src="SaveIcon">保存为布局</el-button>
+      </section>
+    </header>
+    <main class="min-map__main">
+      <section class="camera-list">
+        <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-id">{{ item.name }}</span>
-            <el-popover placement="bottom-start" trigger="hover" :content="item.name" :teleported="false">
+            <el-popover placement="right-start" trigger="hover" :content="item.workSpaceName" :teleported="false">
               <template #reference>
-                <span class="space-name">{{ item.workSpaceName }}</span>
+                <span class="camera-space">{{ item.workSpaceName }}</span>
               </template>
             </el-popover>
           </div>
-        </el-scrollbar>
-      </div>
-      <div ref="drawContainer" v-show="isUploadBg || shopCameraList.length !== 1" class="draw-container"
-        :class="{ 'bg-background': hasBg ? true : false }">
-        <KonvaMap ref="konvaMap" :filter-data="filterShopCameraList" :camera-list="shopCameraList"
-          @change-default-camera="changeDefault" @send-camera-id="sendCameras" @change="changeMap" v-moveable:1 />
-        <div id="editContainer" v-moveable:1></div>
-        <el-upload v-if="!hasBg" class="upload-icon flex justify-center items-center" :action="actionUrl"
-          :show-file-list="false" :before-upload="handleBeforeUpload" :on-success="handleAvatarSuccess"
-          :with-credentials="true" name="file" :headers="getHeaders()">
+        </main>
+      </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()">
           <img src="~@/assets/images/img-upload.png" />
         </el-upload>
-      </div>
-      <!-- <div v-show="!isUploadBg && shopCameraList.length === 1" class="camera-one-tip">仅此一个相机,无需添加背景图片</div> -->
-    </div>
+        <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 useMiniMap from './use-mini-map';
-// import { storeToRefs } from 'pinia';
 import { ElMessage, ElInput, ElMessageBox } from 'element-plus';
-import { onMounted, onUnmounted, ref } from 'vue';
+import { Search, Refresh, UploadFilled } from '@element-plus/icons-vue';
+import SaveIcon from '@/assets/images/camera/save.png'
+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 { computed } from 'vue';
-import { Search, Refresh } from '@element-plus/icons-vue';
 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 rollback from '@/assets/rollback.png'
 import router from '@/router';
 import { ShopMapCamera } from '@/types/scene/type'
 import { ViewType } from '@/types/page-config/type';
 const cameraStatus = useCameraStatus();
 const { openIntervalNew, closeInterval } = cameraStatus;
 import { openMessageBox } from '@/views/system-config/business-scene/components/MessageBox'
-
-// const miniMap = useMiniMap();
-// const { scenesTree, selectedShopCode, selectedShopDetail } = storeToRefs(miniMap);
-// const { getScenesTree, getMapLayout } = miniMap;
-// const { scenesTree, shopCameraList, selectedShopCode, selectedShopDetail } = storeToRefs(miniMap);
-// const { getScenesTree, getShowCameras, getMapLayout } = miniMap;
-
 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 isSave = computed(() => selectedShopId.value && hasBg.value);
-
 //是否修改
 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];
@@ -129,12 +99,10 @@ function updataState(data, updateData) {
     }
   }
 }
-
 const refreshUploadBg = () => {
   konvaMap.value.resetMap();
   hasBg.value = false;
 };
-
 const handleBeforeUpload = () => {
   if (!selectedShopId.value) {
     ElMessage.error({
@@ -148,28 +116,25 @@ const sendCameras = (camerasList) => {
     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;
-  konvaMap.value.addBg(imgUrlBg.value);
   hasBg.value = true;
+  nextTick(() => {
+    konvaMap.value.addBg(imgUrlBg.value).then(() => {
+      isKnovaDestroy.value = false;
+    });
+  })
 };
-
-// const changeShop = (code: string) => {
-//   konvaMap.value.resetMap();
-//   getShopContent(code);
-//   hasBg.value = false;
-// };
 const getMapLayoutAPIMap = {
   [ViewType.minimap_PC]: getWorkshopMiniMapLayoutPCApi,
   [ViewType.minimap_phone]: getWorkshopMiniMapLayoutMobileApi,
@@ -188,22 +153,7 @@ const getShopContent = async (id: number) => {
   if (!res) return;
   hasBg.value = true;
   isMap.value = res.isUploadBg;
-  if (res.isUploadBg) {
-    isUploadBg.value = true;
-    konvaMap.value.createMap(res, selectedShopId.value);
-  } else {
-    hasBg.value = false;
-    isUploadBg.value = res.isUploadBg;
-  }
-  openIntervalNew(idList, (targetData) => {
-    updataState(filterShopCameraList.value, targetData);
-  });
-  getMapLayout(id).then((res) => {
-    if (!res) {
-      return;
-    }
-    hasBg.value = true;
-    isMap.value = res.isUploadBg;
+  nextTick(() => {
     if (res.isUploadBg) {
       isUploadBg.value = true;
       konvaMap.value.createMap(res, selectedShopId.value);
@@ -211,7 +161,10 @@ const getShopContent = async (id: number) => {
       hasBg.value = false;
       isUploadBg.value = res.isUploadBg;
     }
-  });
+    openIntervalNew(idList, (targetData) => {
+      updataState(filterShopCameraList.value, targetData);
+    });
+  })
 };
 const selectedShopId = ref();
 const selectedName = ref();
@@ -228,19 +181,17 @@ const getShowCameras = async (id: number) => {
     })
   })
 };
+const mapConfig = reactive({
+  width: 0,
+  height: 0
+})
 onMounted(async () => {
-  // getScenesTree({ level: 2, valueKey: 'code', labelKey: 'name', disabled: true });
-  konvaMap.value.resizeContainer(
-    drawContainer.value!.clientWidth,
-    drawContainer.value!.clientHeight,
-  );
   selectedShopId.value = Number(route.query.workshopId);
   selectedName.value = route.query.workshopName;
   viewType.value = route.query.viewType;
-  await getShopContent(selectedShopId.value)
-  // if (selectedShopCode.value) {
-  //   getShopContent(selectedShopCode.value);
-  // }
+  mapConfig.width = drawContainer.value?.clientWidth || 0;
+  mapConfig.height = drawContainer.value?.clientHeight || 0;
+  await getShopContent(selectedShopId.value);
 });
 
 onUnmounted(() => {
@@ -255,7 +206,7 @@ const filterShopCameraList = computed(() => {
   );
 });
 
-const handleAddCamera = (cameraId: string) => {
+const handleAddCamera = (cameraId: string, index: number) => {
   if (isAddedCamera(cameraId)) {
     const camera = konvaMap.value.findCamera(cameraId);
     konvaMap.value.handleCameraClick(camera);
@@ -267,25 +218,10 @@ const handleAddCamera = (cameraId: string) => {
     });
     return;
   }
-  konvaMap.value.addCamera(cameraId);
+  konvaMap.value.addCamera(cameraId, index);
 };
 
 const handleSave = () => {
-  // if (shopCameraList.value.length === 1 && !isUploadBg.value) {
-  //   isMap.value = false;
-  //   const layout = JSON.stringify({
-  //     isUploadBg: isUploadBg.value,
-  //     defaultCameraId: shopCameraList.value[0].code,
-  //   });
-  //   console.log(layout)
-  //   updateMinMapViewLayoutApi({
-  //     layout,
-  //     targetId: String(selectedShopId.value),
-  //     viewType: viewType.value,
-  //   }).then(() => {
-  //     ElMessage.success('保存成功');
-  //   });
-  // } else {
   isMap.value = true;
   const layout = konvaMap.value.saveLayout();
   const cameraList = JSON.parse(layout).cameraList;
@@ -300,7 +236,6 @@ const handleSave = () => {
   }).then(() => {
     ElMessage.success('保存成功');
   });
-  // }
 };
 
 const changeMap = (val) => {
@@ -327,10 +262,26 @@ onBeforeRouteLeave(async () => {
 </script>
 
 <style scoped lang="scss">
-.page-head {
-  height: 54px;
-  padding-left: 15px;
-  background-color: #ffffff;
+.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;
+  }
 
   &__btn {
     display: flex;
@@ -343,142 +294,134 @@ onBeforeRouteLeave(async () => {
       width: 14px;
     }
   }
+
+  &__main {
+    display: flex;
+    gap: 5px;
+    width: 100%;
+    height: calc(100% - 60px);
+    border-radius: 6px;
+  }
 }
 
-.head-opt {
-  margin-left: 20px;
-  padding-right: 15px;
+.workshop-name {
   font-size: 14px;
   color: #3f3f3f;
-
-  .workshop-name {
-    font-weight: 600;
-  }
+  font-weight: 600;
 }
 
-.upload-option {
+.operate-btn {
   display: flex;
-}
-
-.upload-title {
-  margin-top: 13px;
-}
-
-.camera-one-tip {
-  color: rgb(197, 97, 20);
-  font-size: 30px;
-  margin: 200px auto;
-}
-
-.avatar-uploader {
-  border-radius: 4px;
-  margin-left: 30px;
-}
+  justify-content: flex-end;
+  align-items: center;
+  flex: 1;
+  height: inherit;
+  gap: 20px;
 
-.upload-icon {
-  position: absolute;
-  top: 0;
-  right: 0;
-  left: 0;
-  bottom: 0;
-  margin: auto;
-}
+  .el-button {
+    margin: 0;
 
-.paint-tool {
-  position: relative;
-  height: calc(100vh - 138px);
-  margin-top: 2px;
+    img {
+      width: 14px;
+      margin-right: 6px;
+    }
+  }
 }
 
 .camera-list {
   width: 250px;
-  padding: 0 10px;
+  height: 100%;
+  border-radius: 0 0 0 6px;
+  padding: 10px 10px;
   background-color: #ffffff;
-}
 
-.label-text {
-  font-size: 14px;
-  font-weight: 600;
-  margin: 10px 0 5px 10px;
-}
-
-.camera-item {
-  height: 32px;
-  font-size: 14px;
-  padding-left: 8px;
-  font-weight: 400;
-  color: #404040;
-  line-height: 14px;
-  cursor: pointer;
-
-  &:hover {
-    background-color: #e6f7ff;
-    color: #1890ff;
+  &__title {
+    margin-left: 5px;
+    font-size: 14px;
+    font-weight: 600;
   }
-}
 
-.isAdded {
-  color: #1890ff;
-  //cursor: not-allowed;
-}
+  &__search {
+    margin-top: 10px;
 
-.isActive {
-  background-color: #e6f7ff;
-  color: #1890ff;
+    :deep(.el-input__wrapper) {
+      background-color: #f0f2f5;
+    }
+  }
 }
 
-.camera-item-disabled {
-  color: #c6c6c6;
+.workshop-map {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex: 1;
+  height: 100%;
+  overflow: hidden;
 }
 
-.camera-id {
-  width: 110px;
-}
+.camera-item {
+  width: 100%;
+  max-height: calc(100% - 65px);
+  overflow-y: auto;
+  margin-top: 10px;
+  margin-left: 5px;
+
+  &__empty {
+    color: #3f3f3f;
+  }
 
-.space-name {
-  width: 120px;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-}
+  &__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;
 
-.draw-container {
-  position: relative;
-  width: calc(100% - 300px);
-  margin: 20px;
-  overflow: hidden;
-  //background-color: rgba(0, 0, 0, 0.6);
-}
+    &:hover {
+      background-color: #e6f7ff;
+      color: #1890ff;
+    }
 
-.bg-background {
-  background-color: rgba(0, 0, 0, 0.6);
-}
+    .camera-id {
+      width: 110px;
+    }
 
-.integrationState {
-  cursor: not-allowed;
-  color: #ccc;
+    .camera-space {
+      width: 120px;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+  }
 }
 
-:deep(.search-put .el-input__wrapper) {
-  background-color: #f0f2f5;
+:deep(.el-popover) {
+  width: unset !important;
+  min-width: 110px;
+  text-align: center;
+  font-weight: 400;
 }
 
 :deep(.el-popper__arrow) {
   display: none;
 }
 
-:deep(.el-tree-node__content:hover) {
-  background: #e6f4ff;
+.isAdded {
+  color: #1890ff;
 }
 
-:deep(.el-button--primary) {
-  --el-button-disabled-bg-color: #bfbfbf;
+.isActive {
+  background-color: #e6f7ff;
+  color: #1890ff;
 }
 
-:deep(.el-popover) {
-  width: unset !important;
-  min-width: 110px;
-  text-align: center;
-  font-weight: 400;
+.integrationState {
+  cursor: not-allowed;
+  color: #ccc;
 }
 </style>

+ 30 - 16
src/views/map-config/mini-map/components/DefaultTip.vue

@@ -3,6 +3,7 @@
     <span v-show="isDefault">默认摄像头</span>
     <span>{{ positionText }}</span>
     <span>{{ sizeText }}</span>
+    <span>{{ rotationText }}</span>
     <div v-if="props.position === TipPositionEnum.TOP" class="tip-narrow bottom"></div>
     <div v-else-if="props.position === TipPositionEnum.BOTTOM" class="tip-narrow top"></div>
     <div v-else-if="props.position === TipPositionEnum.RIGHT" class="tip-narrow left"></div>
@@ -12,27 +13,40 @@
 
 <script setup lang="ts">
 import { computed } from 'vue';
-import { TipPositionEnum, camerasGroupType } from '../type';
+import { TipPositionEnum, cameraInfoType } from '../type';
 
-const props = withDefaults(defineProps<{ position: TipPositionEnum; isDefault: boolean, cameraInfo: camerasGroupType | undefined }>(), {
+const props = withDefaults(defineProps<{ position: TipPositionEnum; isDefault: boolean, cameraInfo: cameraInfoType | undefined }>(), {
   position: TipPositionEnum.TOP,
 });
+const getCameraInfoText = (info, format) => {
+  if (!info) return '';
+  return format(info);
+};
+
 const positionText = computed(() => {
-  const cameraInfo = props.cameraInfo;
-  if (!cameraInfo) return ''
-  const x = cameraInfo.groupConfig.x?.toFixed(2);
-  const y = cameraInfo.groupConfig.y?.toFixed(2);
-  return `位置:(x:${x},y:${y})`
-})
+  return getCameraInfoText(props.cameraInfo, (cameraInfo) => {
+    const x = cameraInfo.x.toFixed(2);
+    const y = cameraInfo.y.toFixed(2);
+    return `位置:(x:${x},y:${y})`;
+  });
+});
+
 const sizeText = computed(() => {
-  const cameraInfo = props.cameraInfo;
-  if (!cameraInfo) return ''
-  const scaleX = cameraInfo.groupConfig.scaleX ? cameraInfo.groupConfig.scaleX : 1;
-  const scaleY = cameraInfo.groupConfig.scaleY ? cameraInfo.groupConfig.scaleY : 1;
-  const w = Math.round(cameraInfo.config.width! * scaleX)
-  const h = Math.round(cameraInfo.config.height! * scaleY)
-  return `大小:(w:${w},h:${h})`
-})
+  return getCameraInfoText(props.cameraInfo, (cameraInfo) => {
+    const scaleX = cameraInfo.scaleX;
+    const scaleY = cameraInfo.scaleY;
+    const w = Math.round(cameraInfo.sizeX * scaleX);
+    const h = Math.round(cameraInfo.sizeY * scaleY);
+    return `大小:(w:${w},h:${h})`;
+  });
+});
+
+const rotationText = computed(() => {
+  return getCameraInfoText(props.cameraInfo, (cameraInfo) => {
+    const rotation = cameraInfo.rotation.toFixed(1);
+    return `角度:${rotation}°`;
+  });
+});
 </script>
 
 <style scoped lang="scss">

+ 9 - 0
src/views/map-config/mini-map/type.ts

@@ -26,3 +26,12 @@ export interface camerasGroupType {
   config: camerasImgType;
   isDefault?: boolean;
 }
+export interface cameraInfoType {
+  x: number;
+  y: number;
+  sizeX: number;
+  sizeY: number;
+  scaleX: number;
+  scaleY: number;
+  rotation: number;
+}