zhudie 2 vuotta sitten
vanhempi
commit
b559adc938

+ 1 - 0
package.json

@@ -66,6 +66,7 @@
     "url-join": "5.0.0",
     "vue": "3.3.4",
     "vue-hooks-plus": "1.8.6",
+    "vue-konva": "^3.0.2",
     "vue-router": "4.1.2",
     "vue-types": "4.1.1",
     "vuedraggable": "4.1.0",

+ 14 - 2
pnpm-lock.yaml

@@ -39,7 +39,7 @@ dependencies:
     specifier: 2.19.0
     version: 2.19.0
   canvg:
-    specifier: ^4.0.1
+    specifier: 4.0.1
     version: 4.0.1
   cropperjs:
     specifier: 1.5.12
@@ -75,7 +75,7 @@ dependencies:
     specifier: 1.1.0
     version: 1.1.0
   mpegts.js:
-    specifier: ^1.7.3
+    specifier: 1.7.3
     version: 1.7.3
   nprogress:
     specifier: 0.2.0
@@ -110,6 +110,9 @@ dependencies:
   vue-hooks-plus:
     specifier: 1.8.6
     version: 1.8.6(vue@3.3.4)
+  vue-konva:
+    specifier: ^3.0.2
+    version: 3.0.2(konva@9.3.0)
   vue-router:
     specifier: 4.1.2
     version: 4.1.2(vue@3.3.4)
@@ -8267,6 +8270,15 @@ packages:
       vue: 3.3.4
     dev: false
 
+  /vue-konva@3.0.2(konva@9.3.0):
+    resolution: {integrity: sha512-FNWKtPPVDihNuQcq7F4GzuVPkPaEfg8lnWezpRqgiC2VetdvONOiYOFD800jNL5kt/lEYnSOYvhqqdTPbv2c8w==}
+    engines: {node: '>= 4.0.0', npm: '>= 3.0.0'}
+    peerDependencies:
+      konva: '>7'
+    dependencies:
+      konva: 9.3.0
+    dev: false
+
   /vue-router@4.1.2(vue@3.3.4):
     resolution: {integrity: sha512-5BP1qXFncVRwgV/XnqzsKApdMjQPqWIpoUBdL1ynz8HyLxIX/UDAx7Ql2BjmA5CXT/p61JfZvkpiFWFpaqcfag==}
     peerDependencies:

+ 2 - 1
src/main.ts

@@ -7,6 +7,7 @@ import 'nprogress/nprogress.css';
 
 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';
@@ -38,7 +39,7 @@ async function bootstrap() {
 
   // 路由准备就绪后挂载APP实例
   await router.isReady();
-
+  app.use(VueKonva);
   app.mount('#app', true);
 }
 

+ 231 - 0
src/views/map-config/mini-map/MapBase/KonvaMap.vue

@@ -0,0 +1,231 @@
+<template>
+  <div id="drawContainer">
+    <v-stage :config="stageConfig" @click="handleStageClick">
+      <v-layer>
+        <v-image :config="bgConfig" v-if="props.bgImgUrl" />
+        <v-group
+          v-for="camera in cameras"
+          :key="camera.id"
+          :attrs="camera"
+          @dblclick="() => handleCameraDblClick(camera)"
+        >
+          <v-image :config="cameraConfig" @dragend="(e) => handleCameraDragEnd(camera, e)" />
+        </v-group>
+        <v-image :config="defaultIconConfig" v-if="defaultIconVisible" />
+      </v-layer>
+    </v-stage>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, onMounted, onBeforeUnmount } from 'vue';
+  import Konva from 'konva';
+  import { ElMessage } from 'element-plus';
+  import { useGlobSetting } from '@/hooks/setting';
+  import urlJoin from 'url-join';
+  import cameraImgSrc from '@/assets/camera/cameraImg.png';
+  import favoritesImgSrc from '@/assets/camera/favorites.png';
+
+  const props = defineProps<{
+    bgImgUrl?: string;
+    bar?: number;
+  }>();
+
+  const globSetting = useGlobSetting();
+
+  interface caremasType {
+    id?: string;
+  }
+
+  const stageConfig = {
+    width: 800,
+    height: 600,
+  };
+
+  const cameraConfig = {};
+
+  const bgImg = new Image();
+  const cameraImg = new Image();
+  const favoritesImg = new Image();
+  // bgImg.src = '';
+  cameraImg.src = cameraImgSrc;
+  favoritesImg.src = favoritesImgSrc;
+
+  const bgImgUrl = ref('');
+  const cameras = ref<caremasType[]>([]);
+  const defaultCameraId = ref('');
+  const defaultIconVisible = ref(false);
+
+  const bgConfig = ref({
+    x: 0,
+    y: 0,
+    id: 'bgImg',
+    width: bgImg.width,
+    height: bgImg.height,
+    image: bgImg,
+  });
+
+  const defaultIconConfig = {
+    x: 18,
+    y: -16,
+    width: 16,
+    height: 16,
+    visible: defaultIconVisible.value,
+  };
+
+  const handleStageClick = (e: any) => {
+    console.log('e', e);
+
+    // if (e.target === stage) {
+    //   stage.find('Transformer').destroy();
+    //   layer.batchDraw();
+    // }
+  };
+
+  const handleCameraDblClick = (camera: any) => {
+    const index = cameras.value.findIndex((c) => c.id === camera.id);
+    if (index === -1) return;
+    const updatedCameras = [...cameras.value];
+    updatedCameras.splice(index, 1);
+    cameras.value = updatedCameras;
+  };
+
+  const deleteCamera = () => {
+    const index = cameras.value.findIndex((camera) => camera.id === activeCameraId.value);
+    if (index === -1) return;
+    const updatedCameras = [...cameras.value];
+    updatedCameras.splice(index, 1);
+    cameras.value = updatedCameras;
+  };
+
+  const addCamera = (x: number, y: number) => {
+    const id = Math.random().toString(36).substr(2, 9);
+    cameras.value.push({ id, x, y, rotation: 0, scaleX: 1, scaleY: 1 });
+  };
+
+  const setDefaultCamera = (camera: any) => {
+    defaultCameraId.value = camera.id;
+    defaultIconVisible.value = true;
+  };
+
+  const handleCameraDragEnd = (camera: any, e: any) => {
+    if (camera.id === defaultCameraId.value) {
+      const tr = layer.findOne(`#tr_${camera.id}`);
+      tr.nodes([e.target]);
+      layer.batchDraw();
+    }
+  };
+
+  const handleFileChange = (e: any) => {
+    const file = e.target.files[0];
+    if (!file) return;
+    const reader = new FileReader();
+    reader.onload = (event) => {
+      bgImgUrl.value = event.target?.result;
+      addBg();
+    };
+    reader.readAsDataURL(file);
+  };
+
+  const addBg = () => {
+    // bgImgUrl.value = bgImg;
+    bgImgUrl.value = urlJoin(globSetting.imgUrl!, props.bgImgUrl!);
+    console.log('bgImgUrl.value22 ', bgImgUrl.value);
+    // console.log('imgUrl', imgUrl);
+    bgImg.src = bgImgUrl.value;
+    // const img = new Image();
+    bgImg.onload = () => {
+      bgConfig.value.width = bgImg.width;
+      bgConfig.value.height = bgImg.height;
+      // bgImg.src = bgImgUrl.value;
+    };
+  };
+
+  const saveLayout = () => {
+    const layout = {
+      bgInfo: {
+        bgImg: bgImgUrl.value,
+        width: bgImg.width,
+        height: bgImg.height,
+      },
+      defaultCameraId: defaultCameraId.value,
+      cameraList: cameras.value,
+    };
+    const jsonString = JSON.stringify(layout);
+    console.log(jsonString);
+    // Here you can do something with the layout JSON string, such as sending it to a server or saving it locally
+  };
+  const handleKeyDown = () => {};
+
+  /** 导入布局json */
+  const createMap = (layout) => {
+    // const layout = JSON.parse(json);
+    bgImgUrl.value = layout.bgInfo.bgImg;
+    console.log('bgImgUrl.value11 ', bgImgUrl.value);
+
+    addBg();
+    // layout.cameraList.forEach((camera) => {
+    //   const group = new Konva.Group({
+    //     x: camera.x,
+    //     y: camera.y,
+    //     id: camera.cameraId,
+    //     rotation: camera.rotation,
+    //     scaleX: camera.scaleX,
+    //     scaleY: camera.scaleY,
+    //     draggable: false,
+    //     name: 'group',
+    //   });
+    //   const camImg = new Image();
+    //   camImg.onload = () => {
+    //     const cameraIcon = new Konva.Image({
+    //       width: 52,
+    //       height: 37,
+    //       image: camImg,
+    //       name: 'image',
+    //     });
+    //     group.add(cameraIcon);
+    //     layer?.add(group);
+    //     bindBaseEvt(cameraIcon);
+    //     addedCameras.value.push(camera.cameraId);
+
+    //     if (camera.cameraId === layout.defaultCameraId) {
+    //       setDefaultCamera(group);
+    //       defaultIcon?.show();
+    //     }
+
+    //     if (addedCameras.value.length === layout.cameraList.length) {
+    //       layer?.batchDraw();
+    //     }
+    //   };
+    //   camImg.src = cameraImg;
+    // });
+  };
+
+  defineExpose({ addBg, createMap });
+
+  onMounted(() => {
+    // Add initial cameras, if needed
+  });
+
+  onBeforeUnmount(() => {
+    window.removeEventListener('keydown', handleKeyDown);
+  });
+</script>
+
+<style scoped>
+  #drawContainer {
+    position: relative;
+  }
+
+  input[type='file'] {
+    position: absolute;
+    top: 10px;
+    left: 10px;
+  }
+
+  button {
+    position: absolute;
+    top: 40px;
+    left: 10px;
+  }
+</style>

+ 319 - 0
src/views/map-config/mini-map/MiniMapConfig copy.vue

@@ -0,0 +1,319 @@
+<template>
+  <div class="page" @click="handleWholeClick">
+    <div class="page-head flex items-center">
+      <el-icon size="20"><ArrowLeft /></el-icon>
+      <div class="head-opt flex-1 flex justify-between items-center">
+        <div>
+          <span>场景:</span>
+          <el-tree-select
+            v-model="selectedShopCode"
+            :data="scenesTree"
+            accordion
+            :render-after-expand="false"
+            :default-expand-all="true"
+            :teleported="false"
+            placeholder="请选择相关场景"
+            @change="changeShop"
+          />
+        </div>
+        <div class="flex">
+          <!-- <el-button @click="mapEditor.toJson">tojson</el-button> -->
+          <el-upload
+            class="avatar-uploader flex justify-center items-center"
+            action="/skyeye-admin-api/layout/uploadPicture"
+            :show-file-list="false"
+            :on-success="handleAvatarSuccess"
+            :with-credentials="true"
+            name="file"
+            :data="{ workshopId: selectedShopDetail?.id }"
+          >
+            <el-button style="font-size: 12px" :icon="Refresh" :disabled="!hasBg">
+              替换照片
+            </el-button>
+          </el-upload>
+
+          <el-button
+            @click="handleSave"
+            style="margin-left: 40px"
+            type="primary"
+            :disabled="!selectedShopCode"
+            >保存为布局
+          </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 === activeCameraId,
+            }"
+            @click="handleAddCamera(item.code)"
+          >
+            <span class="camera-id">{{ item.name }}</span>
+            <el-popover
+              placement="bottom-start"
+              trigger="hover"
+              :content="item.name"
+              :teleported="false"
+            >
+              <template #reference>
+                <span class="space-name">{{ item.workSpaceName }}</span>
+              </template>
+            </el-popover>
+          </div>
+        </el-scrollbar>
+      </div>
+      <div ref="drawContainer" id="drawContainer" class="draw-container">
+        <div id="editContainer" v-moveable:1></div>
+        <el-upload
+          v-if="!hasBg"
+          class="upload-icon flex justify-center items-center"
+          action="/skyeye-admin-api/layout/uploadPicture"
+          :show-file-list="false"
+          :before-upload="handleBeforeUpload"
+          :on-success="handleAvatarSuccess"
+          :with-credentials="true"
+          name="file"
+          :data="{ workshopId: selectedShopDetail?.id }"
+        >
+          <img src="~@/assets/images/img-upload.png" />
+        </el-upload>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import useMiniMap from './use-mini-map';
+  import { storeToRefs } from 'pinia';
+  import { ElMessage, ElInput } from 'element-plus';
+  import { onMounted, ref } from 'vue';
+  import { updateMinMapViewLayoutApi } from '@/api/scene/scene';
+  import { computed } from 'vue';
+  import { Search, Refresh } from '@element-plus/icons-vue';
+  import useMapEditor from './hooks/useMapEditor';
+
+  const mapEditor = useMapEditor();
+  const { activeCameraId, addedCameras, bgImgUrl } = mapEditor;
+  const miniMap = useMiniMap();
+  const { scenesTree, shopCameraList, selectedShopCode, selectedShopDetail } = storeToRefs(miniMap);
+  const { getScenesTree, getShowCameras, getMapLayout } = miniMap;
+
+  const drawContainer = ref<HTMLDivElement>();
+
+  const searchKey = ref('');
+  // 是否已有背景图
+  const hasBg = ref(false);
+
+  const handleBeforeUpload = () => {
+    if (!selectedShopCode.value) {
+      ElMessage.error({
+        message: '请先选择车间',
+      });
+      return false;
+    }
+  };
+
+  /** 判断相机是否已经添加 */
+  const isAddedCamera = (cameraId: string) => {
+    const index = addedCameras.value.findIndex((item) => item === cameraId);
+    return index >= 0;
+  };
+
+  const handleAvatarSuccess = (e) => {
+    bgImgUrl.value = e.data;
+    mapEditor.addBg();
+    hasBg.value = true;
+  };
+
+  const changeShop = (code: string) => {
+    mapEditor.resetMap();
+    getShopContent(code);
+    hasBg.value = false;
+  };
+
+  const getShopContent = (code: string) => {
+    getShowCameras(code);
+    getMapLayout(code).then((res) => {
+      if (!res) {
+        return;
+      }
+      hasBg.value = true;
+      mapEditor.createMap(res);
+    });
+  };
+
+  const handleWholeClick = (e) => {
+    if (e.button === 0) {
+      mapEditor.destoryOptBlock();
+    }
+  };
+
+  onMounted(() => {
+    getScenesTree({ level: 2, valueKey: 'code', labelKey: 'name', disabled: true });
+    mapEditor.initContainer({
+      container: 'editContainer',
+      width: drawContainer.value!.clientWidth,
+      height: drawContainer.value!.clientHeight,
+    });
+    if (selectedShopCode.value) {
+      getShopContent(selectedShopCode.value);
+    }
+  });
+
+  const filterShopCameraList = computed(() => {
+    const k = searchKey.value.trim();
+    if (!k) return shopCameraList.value;
+    return shopCameraList.value.filter((x) => x.code?.includes(k) || x.workSpaceName?.includes(k));
+  });
+
+  const handleAddCamera = (cameraId: string) => {
+    console.log('cameraId', cameraId);
+
+    if (!hasBg.value) {
+      ElMessage.warning({
+        message: '请先添加背景图片',
+      });
+      return;
+    }
+    mapEditor.addCamera(cameraId);
+  };
+
+  const handleSave = () => {
+    const layout = mapEditor.toJson();
+    updateMinMapViewLayoutApi({ layout, targetId: String(selectedShopDetail.value?.id) }).then(
+      (res) => {
+        console.log('updateMinMapViewLayoutApi', res);
+        ElMessage.success('保存成功');
+      },
+    );
+  };
+</script>
+
+<style scoped lang="scss">
+  .page {
+  }
+  .page-head {
+    height: 54px;
+    padding-left: 15px;
+    background-color: #ffffff;
+  }
+  .head-opt {
+    margin-left: 20px;
+    padding-right: 15px;
+    font-size: 14px;
+    color: #3f3f3f;
+  }
+
+  .avatar-uploader {
+    /* width: 120px; */
+    /* height: 30px; */
+    /* border: 1px solid #eee; */
+    border-radius: 4px;
+    margin-left: 30px;
+  }
+  .upload-icon {
+    position: absolute;
+    top: 0;
+    right: 0;
+    left: 0;
+    bottom: 0;
+    margin: auto;
+  }
+
+  .paint-tool {
+    position: relative;
+    height: calc(100vh - 138px);
+    margin-top: 2px;
+  }
+
+  .camera-list {
+    width: 250px;
+    padding: 0 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;
+    }
+  }
+  .isAdded {
+    color: #1890ff;
+    cursor: not-allowed;
+  }
+  .isActive {
+    background-color: #e6f7ff;
+    color: #1890ff;
+  }
+  .camera-item-disabled {
+    color: #c6c6c6;
+  }
+  .camera-id {
+    width: 110px;
+  }
+  .space-name {
+    width: 120px;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+
+  .draw-container {
+    position: relative;
+    width: calc(100% - 300px);
+    margin: 20px;
+    overflow: hidden;
+  }
+
+  :deep(.search-put .el-input__wrapper) {
+    background-color: #f0f2f5;
+  }
+  :deep(.el-popper__arrow) {
+    display: none;
+  }
+  :deep(.el-tree-node__content:hover) {
+    background: #e6f4ff;
+  }
+  :deep(.el-button--primary) {
+    --el-button-disabled-bg-color: #bfbfbf;
+  }
+  :deep(.el-popover) {
+    width: unset !important;
+    min-width: 110px;
+    text-align: center;
+    font-weight: 400;
+  }
+</style>

+ 30 - 11
src/views/map-config/mini-map/MiniMapConfig.vue

@@ -64,7 +64,7 @@
             class="camera-item flex justify-start items-center"
             :class="{
               isAdded: isAddedCamera(item.code),
-              isActive: item.code === activeCameraId,
+              isActive: item.code === caremaActiveId,
             }"
             @click="handleAddCamera(item.code)"
           >
@@ -82,7 +82,9 @@
           </div>
         </el-scrollbar>
       </div>
-      <div ref="drawContainer" id="drawContainer" class="draw-container">
+      <!-- <div ref="drawContainer" id="drawContainer" class="draw-container"> -->
+      <div class="draw-container">
+        <KonvaMap ref="konvaMap" :bg-img-url="imgUrlBg" class="draw-container" />
         <div id="editContainer" v-moveable:1></div>
         <el-upload
           v-if="!hasBg"
@@ -106,11 +108,13 @@
   import useMiniMap from './use-mini-map';
   import { storeToRefs } from 'pinia';
   import { ElMessage, ElInput } from 'element-plus';
-  import { onMounted, ref } from 'vue';
+  import { onMounted, ref, nextTick } from 'vue';
   import { updateMinMapViewLayoutApi } from '@/api/scene/scene';
   import { computed } from 'vue';
   import { Search, Refresh } from '@element-plus/icons-vue';
   import useMapEditor from './hooks/useMapEditor';
+  import KonvaMap from './MapBase/KonvaMap.vue';
+  // import { nextTick } from 'process';
 
   const mapEditor = useMapEditor();
   const { activeCameraId, addedCameras, bgImgUrl } = mapEditor;
@@ -120,6 +124,11 @@
 
   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);
@@ -136,12 +145,20 @@
   /** 判断相机是否已经添加 */
   const isAddedCamera = (cameraId: string) => {
     const index = addedCameras.value.findIndex((item) => item === cameraId);
-    return index >= 0;
+    const indexRe = camerasAdded.value.findIndex((item) => item === cameraId);
+    return indexRe >= 0;
   };
 
   const handleAvatarSuccess = (e) => {
     bgImgUrl.value = e.data;
-    mapEditor.addBg();
+    //自己的
+    imgUrlBg.value = e.data;
+    console.log('imgUrlBg.value1', imgUrlBg.value);
+    nextTick(() => {
+      konvaMap.value.addBg();
+    });
+    // konvaMap.value.addBg();
+    // mapEditor.addBg();
     hasBg.value = true;
   };
 
@@ -158,6 +175,7 @@
         return;
       }
       hasBg.value = true;
+      konvaMap.value.createMap(res);
       mapEditor.createMap(res);
     });
   };
@@ -170,11 +188,11 @@
 
   onMounted(() => {
     getScenesTree({ level: 2, valueKey: 'code', labelKey: 'name', disabled: true });
-    mapEditor.initContainer({
-      container: 'editContainer',
-      width: drawContainer.value!.clientWidth,
-      height: drawContainer.value!.clientHeight,
-    });
+    // mapEditor.initContainer({
+    //   container: 'editContainer',
+    //   width: drawContainer.value!.clientWidth,
+    //   height: drawContainer.value!.clientHeight,
+    // });
     if (selectedShopCode.value) {
       getShopContent(selectedShopCode.value);
     }
@@ -187,6 +205,8 @@
   });
 
   const handleAddCamera = (cameraId: string) => {
+    console.log('cameraId', cameraId);
+
     if (!hasBg.value) {
       ElMessage.warning({
         message: '请先添加背景图片',
@@ -315,4 +335,3 @@
     font-weight: 400;
   }
 </style>
-./MapBase/useCameraMap ./MapBase/CameraMapBak

+ 2 - 0
src/views/map-config/mini-map/hooks/useMapEditor.ts

@@ -80,6 +80,8 @@ export function useMapEditor() {
   /** 添加背景 */
   const addBg = () => {
     const imgUrl = urlJoin(globSetting.imgUrl!, bgImgUrl.value);
+    console.log('imgUrl', imgUrl);
+
     const bgNode = layer?.find('#bgImg')[0] as Konva.Image;
     const bgImg = new Image();
     bgImg.onload = () => {