Jelajahi Sumber

Merge branch 'master' into mapConfig-shy

sunhongyao341504 2 tahun lalu
induk
melakukan
060b28d1fd

TEMPAT SAMPAH
src/assets/camera/favorites - 副本.png


TEMPAT SAMPAH
src/assets/camera/favorites.png


+ 71 - 0
src/views/map-config/mini-map/MapBase/CameraGroup-bak.ts

@@ -0,0 +1,71 @@
+import { fabric } from 'fabric';
+import cameraActiveImg from '@/assets/camera/camera-active.png';
+import cameraImg from '@/assets/camera/camera.png';
+import favoritesImg from '@/assets/camera/favorites.png';
+import { getRandomPosition } from './utils';
+import { CameraImage } from './types';
+
+class CameraGroup extends fabric.Group {
+  g: fabric.Group | null = null;
+  cameraImg: CameraImage | null = null;
+  favImg: CameraImage | null = null;
+  cameraId = '';
+
+  constructor() {
+    super();
+    fabric.Image.fromURL(cameraActiveImg, (cImg) => {
+      cImg.set({
+        left: 0,
+        top: 0,
+      });
+      this.cameraImg = cImg;
+      fabric.Image.fromURL(favoritesImg, (favImg) => {
+        favImg.set({
+          left: 50,
+          top: 0,
+        });
+        this.favImg = favImg as CameraImage;
+        this.g = new fabric.Group([cImg, favImg]);
+      });
+    });
+  }
+
+  init() {}
+
+  /** 提供copy功能,每次新建的时候执行copy就行了 */
+  clone(): Promise<CameraGroup> {
+    console.log('clone');
+    return new Promise((resolve) => {
+      this.g?.clone((e) => {
+        console.log('clone', e);
+        resolve(e as CameraGroup);
+      });
+    });
+  }
+
+  setSelected() {
+    this.cameraImg?.setSrc(cameraActiveImg);
+  }
+
+  setUnSelected() {
+    this.cameraImg?.setSrc(cameraImg);
+  }
+  /** 设为默认摄像头 */
+  setDefault() {
+    this.favImg?.set('visible', true);
+  }
+  /** 取消默认摄像头 */
+  cancelDefault() {
+    this.favImg?.set('visible', false);
+  }
+
+  setAttr(attr: { cameraId: string; left: number; top: number }) {
+    if (attr.cameraId) {
+      this.cameraId = attr.cameraId;
+    }
+    this.g?.set(attr);
+    return this.g;
+  }
+}
+
+export default CameraGroup;

+ 88 - 0
src/views/map-config/mini-map/MapBase/CameraGroup.ts

@@ -0,0 +1,88 @@
+import { fabric } from 'fabric';
+import cameraActiveImg from '@/assets/camera/camera-active.png';
+import cameraImg from '@/assets/camera/camera.png';
+import favoritesImg from '@/assets/camera/favorites.png';
+import { CameraImage } from './types';
+
+class CameraGroup extends fabric.Group {
+  g: fabric.Group;
+  cameraImg: CameraImage | null = null;
+  favImg: CameraImage | null = null;
+  cameraId = '';
+
+  constructor() {
+    super();
+    this.init();
+  }
+
+  init() {
+    fabric.Image.fromURL(cameraActiveImg, (cImg) => {
+      cImg.set({
+        left: 0,
+        top: 0,
+        width: 100,
+        height: 100,
+        imageName: 'cameraImage',
+      });
+      console.log('cameraActiveImg', cImg);
+      this.cameraImg = cImg;
+      fabric.Image.fromURL(favoritesImg, (favImg) => {
+        favImg.set({
+          width: 50,
+          height: 50,
+          left: 50,
+          top: 0,
+          imageName: 'favImage',
+        });
+        this.favImg = favImg as CameraImage;
+        this.g = new fabric.Group([cImg, favImg]);
+      });
+    });
+  }
+
+  /** 提供copy功能,每次新建的时候执行copy就行了 */
+  clone(): Promise<CameraGroup> {
+    console.log('clone');
+    console.log('this', this);
+    return new Promise((resolve) => {
+      this.g.clone((e) => {
+        console.log('clone', e);
+        const newG = e as fabric.Group;
+        const newGroup = new CameraGroup();
+        const cameraImg = newG.getObjects().find((x) => x.imageName === 'cameraImage');
+        const favImage = newG.getObjects().find((x) => x.imageName === 'favImage');
+        newGroup.cameraImg =                            ;
+        newGroup.favImg = favImage;
+        newGroup.g = e;
+        resolve(newGroup as CameraGroup);
+      });
+    });
+  }
+
+  setSelected() {
+    
+    this.cameraImg?.setSrc(cameraActiveImg);
+  }
+
+  setUnSelected() {
+    this.cameraImg?.setSrc(cameraImg);
+  }
+  /** 设为默认摄像头 */
+  setDefault() {
+    this.favImg?.set('visible', true);
+  }
+  /** 取消默认摄像头 */
+  cancelDefault() {
+    this.favImg?.set('visible', false);
+  }
+
+  setAttr(attr: { cameraId: string; left: number; top: number }) {
+    if (attr.cameraId) {
+      this.cameraId = attr.cameraId;
+    }
+    this.g?.set(attr);
+    return this.g;
+  }
+}
+
+export default new CameraGroup();

+ 64 - 49
src/views/map-config/mini-map/MapBase/CameraMap.ts

@@ -1,48 +1,49 @@
 import { fabric } from 'fabric';
+import { ref } from 'vue';
 import cameraActiveImg from '@/assets/camera/camera-active.png';
 import cameraImg from '@/assets/camera/camera.png';
-import { MapData } from './types';
+import favoritesImg from '@/assets/camera/favorites.png';
+import { CameraImage, MapData, OnMoving, OnRightClick, OnSelect, isCanvas } from './types';
 import { fabricSetting } from './fabricSetting';
+import { getRandomPosition } from './utils';
+// import templateGroup from './CameraGroup';
+import { createGroup, toggleGroupSelected, toggleCameraDefault } from './CameraStarGroup';
 
-export interface CameraImage extends fabric.Image {
-  cameraId: string;
-}
-
-type Canvas = fabric.Canvas;
-
-const isCanvas = (canvas: Canvas | null): canvas is Canvas => {
-  return Boolean(canvas);
-};
-
-type OnSelect = (image: CameraImage | null) => unknown;
-type OnRightClick = (e: fabric.IEvent<MouseEvent>) => unknown;
-fabricSetting(fabric);
+fabricSetting();
 
 class CameraMap {
-  public canvas: fabric.Canvas | null = null;
+  public canvas = ref<fabric.Canvas | null>();
   private onSelect: OnSelect;
   private onRightClick: OnRightClick;
-
-  constructor(param: { canvasId: string; onSelect: OnSelect; onRightClick: OnRightClick }) {
-    this.canvas = new fabric.Canvas(param.canvasId, {
+  private onMoving: OnMoving;
+
+  constructor(param: {
+    canvasId: string;
+    onSelect: OnSelect;
+    onRightClick: OnRightClick;
+    onMoving: OnMoving;
+  }) {
+    this.canvas.value = new fabric.Canvas(param.canvasId, {
       fireRightClick: true, // 启用右键,button的数字为3
       stopContextMenu: true, // 禁止默认右键菜单
     });
     this.addListener();
     this.onSelect = param.onSelect;
     this.onRightClick = param.onRightClick;
-    // window.canvas = this.canvas;
+    this.onMoving = param.onMoving;
+    window.canvas = this.canvas.value;
   }
 
   /** 监听点击事件 */
   private addListener() {
-    const canvas = this.canvas;
+    const canvas = this.canvas.value;
     if (!isCanvas(canvas)) return;
 
     canvas.on('mouse:down', (options) => {
       const target = options.target as CameraImage;
       const cameraId = target?.cameraId;
       // console.log('当前选中的id是', cameraId);
+      console.log('mouse:down');
       console.log(options);
 
       // 判断:右键,且在元素上右键
@@ -53,42 +54,47 @@ class CameraMap {
         return;
       }
 
-      canvas.forEachObject((object) => {
-        if (object === options.target) return;
-        (object as CameraImage).setSrc(cameraImg, () => {
-          canvas.renderAll();
-        });
-      });
+      this.setAllGroupsUnselected();
       if (!cameraId || !target) {
         this.onSelect(null);
         return;
       }
-      target.setSrc(cameraActiveImg, () => {
+      toggleGroupSelected(target, true).then(() => {
         canvas.renderAll();
       });
-
       this.onSelect(target);
     });
   }
 
+  private setAllGroupsUnselected() {
+    const canvas = this.canvas;
+    if (!isCanvas(canvas)) return;
+    canvas.getObjects('group').forEach((object) => {
+      // if (object === options.target) return;
+      toggleGroupSelected(object, false).then((res) => {
+        canvas.renderAll();
+      });
+    });
+  }
+
   /** 上传背景图 */
   public uploadBg(imgUrl: string) {
-    const canvas = this.canvas;
+    const canvas = this.canvas.value;
     if (!isCanvas(canvas)) return;
     fabric.Image.fromURL(imgUrl, (img) => {
       console.log('image', img);
-      this.canvas!.setWidth(img.width!);
-      this.canvas!.setHeight(img.height!);
+      canvas!.setWidth(img.width!);
+      canvas!.setHeight(img.height!);
       img.lockScalingX = true;
       img.lockScalingY = true;
       // 设置背景图
-      canvas.setBackgroundImage(img, this.canvas!.renderAll.bind(this.canvas));
+      canvas.setBackgroundImage(img, this.canvas.value!.renderAll.bind(this.canvas));
     });
   }
 
   /** 将所有的摄像头都设置为非激活状态 */
   private setAllCameraUnActive() {
-    const canvas = this.canvas;
+    const canvas = this.canvas.value;
     if (!isCanvas(canvas)) return;
     canvas.forEachObject((object) => {
       (object as fabric.Image).setSrc(cameraImg, () => {
@@ -97,24 +103,28 @@ class CameraMap {
     });
   }
 
-  private getRandomPosition() {
-    return 100 + Math.floor(Math.random() * 30);
-  }
-
   /** 增加一个摄像头 */
   public addCamera(cameraId: string): Promise<CameraImage> {
-    const canvas = this.canvas;
+    const canvas = this.canvas.value;
     if (!isCanvas(canvas)) return Promise.reject();
+    // eslint-disable-next-line @typescript-eslint/no-this-alias
+    const that = this;
     return new Promise((resolve) => {
       fabric.Image.fromURL(cameraActiveImg, (cImg) => {
-        const cameraImg: CameraImage = cImg as unknown as CameraImage;
+        const cameraImg = ref(cImg as unknown as CameraImage);
         this.setAllCameraUnActive();
-        cameraImg.set({
-          left: this.getRandomPosition(),
-          top: this.getRandomPosition(),
+        cameraImg.value.set({
+          left: getRandomPosition(),
+          top: getRandomPosition(),
           cameraId,
         });
-        canvas.add(cameraImg);
+        cameraImg.value.lockScalingX = true;
+        cameraImg.value.lockScalingY = true;
+        canvas.add(cameraImg.value);
+        cameraImg.value.on('moving', function (e) {
+          console.log('move', e);
+          that.onMoving(e);
+        });
         resolve(cameraImg);
       });
     });
@@ -122,7 +132,7 @@ class CameraMap {
 
   /** 删除一个摄像头 */
   public removeCamera(cameraImage: CameraImage) {
-    const canvas = this.canvas;
+    const canvas = this.canvas.value;
     if (!isCanvas(canvas)) return;
     console.log('removeCamera', cameraImage);
     canvas.remove(cameraImage);
@@ -130,7 +140,7 @@ class CameraMap {
 
   /** 导出JSON格式 */
   public toJSON() {
-    const canvas = this.canvas;
+    const canvas = this.canvas.value;
     if (!isCanvas(canvas)) return;
     const initialJSON = canvas.toJSON(['cameraId']);
     /** toJSON返回值的类型它写错了,应该是有backgroundImage的 */
@@ -158,7 +168,7 @@ class CameraMap {
 
   /** 从json中加载 */
   public loadFromJSON(json: MapData): Promise<void> {
-    const canvas = this.canvas;
+    const canvas = this.canvas.value;
     if (!isCanvas(canvas)) return Promise.reject();
     return new Promise((resolve) => {
       const { width, height } = json.backgroundImage;
@@ -178,24 +188,29 @@ class CameraMap {
 
   /** 更新摄像头的渲染 */
   public renderCamera() {
-    const canvas = this.canvas;
+    const canvas = this.canvas.value;
     if (!isCanvas(canvas)) return;
     canvas.renderAll();
   }
 
   public clear() {
-    this.canvas?.clear();
+    this.canvas.value?.clear();
   }
 
   /** 是否已经存在这个cameraId */
   public hasCamera(cameraId: string) {
-    const canvas = this.canvas;
+    const canvas = this.canvas.value;
     if (!isCanvas(canvas)) return;
     const cameraIds = canvas
       .toJSON(['cameraId'])
       .objects.map((item) => (item as CameraImage).cameraId);
     return cameraIds.includes(cameraId);
   }
+
+  /** 根据cameraId查找某个元素 */
+  public getCameraById(cameraId: string) {
+    return this.canvas.value?.getObjects().find((x) => (x as CameraImage).cameraId === cameraId);
+  }
 }
 
 export default CameraMap;

+ 73 - 0
src/views/map-config/mini-map/MapBase/CameraPreview.vue

@@ -0,0 +1,73 @@
+<template>
+  <div>
+    <div style="overflow: auto; position: relative">
+      <canvas width="400" height="400" ref="canvasRef" style="border: 1px solid #ccc"></canvas>
+      <DefaultCameraIcon :position="favPosition" />
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+  import { fabric } from 'fabric';
+  import { onMounted, ref, watch } from 'vue';
+  import cameraImg from '@/assets/camera/camera.png';
+  import { CameraImage, MapData } from './types';
+  import DefaultCameraIcon from './DefaultCameraIcon.vue';
+  import { getFavPositionByCamera } from './utils';
+
+  const props = defineProps<{ json: MapData }>();
+
+  let canvas;
+  const canvasRef = ref<HTMLCanvasElement>();
+
+  const favPosition = ref<{ left: number; top: number } | null>(null);
+
+  const createMap = () => {
+    if (!canvasRef.value) return;
+    canvas = new fabric.Canvas(canvasRef.value, {
+      fireRightClick: true, // 启用右键,button的数字为3
+      stopContextMenu: true, // 禁止默认右键菜单
+    });
+    canvas.selectable = false;
+    // window.cvs = canvas;
+  };
+
+  onMounted(() => {
+    createMap();
+  });
+
+  watch(
+    () => props.json,
+    () => {
+      const json = props.json;
+      console.log('props.json', props.json);
+      if (json) {
+        const { width, height } = json.backgroundImage;
+        canvas?.setWidth(width);
+        canvas?.setHeight(height);
+        const objects = json.objects.map((item) => {
+          return {
+            ...item,
+            src: cameraImg,
+            selectable: false,
+            hasControls: false,
+            hasBoards: false,
+          };
+        });
+        canvas?.loadFromJSON({ ...json, objects }, () => {
+          const defaultCamera = getCameraById(json.defaultCameraId);
+          if (defaultCamera) {
+            favPosition.value = getFavPositionByCamera(defaultCamera);
+          }
+        });
+      }
+    },
+    { deep: true },
+  );
+
+  const getCameraById = (cameraId: string) => {
+    return canvas
+      ?.getObjects()
+      .find((x) => (x as CameraImage).cameraId === cameraId) as CameraImage;
+  };
+</script>
+<style scoped></style>

+ 68 - 0
src/views/map-config/mini-map/MapBase/CameraStarGroup.ts

@@ -0,0 +1,68 @@
+import { fabric } from 'fabric';
+import cameraActiveImg from '@/assets/camera/camera-active.png';
+import cameraImg from '@/assets/camera/camera.png';
+import favoritesImg from '@/assets/camera/favorites.png';
+import { CameraImage } from './types';
+
+const cameraImageName = 'cameraImage';
+const favImageName = 'favImageImage';
+
+const cameraInfo = { width: 60, height: 40 };
+const favInfo = { width: 10, height: 10 };
+
+export function createGroup(): Promise<fabric.Group> {
+  return new Promise((resolve) => {
+    fabric.Image.fromURL(cameraActiveImg, (cImg) => {
+      cImg.set({
+        left: 0,
+        top: 0,
+        width: cameraInfo.width,
+        height: cameraInfo.height,
+        imageName: cameraImageName,
+      });
+      console.log('cameraActiveImg', cImg);
+
+      fabric.Image.fromURL(favoritesImg, (favImg) => {
+        favImg.set({
+          width: favInfo.width,
+          height: favInfo.height,
+          left: 40,
+          top: -10,
+          visible: false,
+          imageName: favImageName,
+        });
+        resolve(
+          new fabric.Group([cImg, favImg], {
+            width: cameraInfo.width,
+            height: cameraInfo.height,
+            // 不要缓存,否则无法修改图片地址
+            objectCaching: false,
+          }),
+        );
+      });
+    });
+  });
+}
+
+/** 设置是否是选中后的效果 */
+export function toggleGroupSelected(group: fabric.Group, isSelected: boolean) {
+  return new Promise((resolve, reject) => {
+    const groupCamera = group.getObjects()[0] as fabric.Image;
+    // .find((item) => item.imageName === cameraImageName) as fabric.Image;
+    if (!groupCamera) {
+      reject();
+      return;
+    }
+    const src = isSelected ? cameraActiveImg : cameraImg;
+    groupCamera.setSrc(src, () => {
+      resolve();
+    });
+  });
+}
+
+/** 设置是否为默认摄像头 */
+export function toggleCameraDefault(group: fabric.Group, visible: boolean) {
+  const star = group.getObjects()[1];
+  if (!star) return;
+  star.set({ visible });
+}

+ 21 - 0
src/views/map-config/mini-map/MapBase/DefaultCameraIcon.vue

@@ -0,0 +1,21 @@
+<template>
+  <!-- 默认选中图片的icon -->
+  <img
+    v-if="props.position"
+    :src="favIcon"
+    class="defaultCameraImg"
+    :style="{ left: props.position?.left + 'px', top: props.position?.top + 'px' }"
+  />
+</template>
+<script lang="ts" setup>
+  import favIcon from '@/assets/camera/favorites.png';
+
+  const props = defineProps<{ position: { left: number; top: number } | null }>();
+</script>
+<style scoped>
+  .defaultCameraImg {
+    position: absolute;
+    width: 15px;
+    /* height: 50px; */
+  }
+</style>

+ 31 - 1
src/views/map-config/mini-map/MapBase/fabricSetting.ts

@@ -1,4 +1,7 @@
-export function fabricSetting(fabric) {
+import { fabric } from 'fabric';
+// import favoritesImg from '@/assets/camera/favorites.png';
+
+export function fabricSetting() {
   fabric.Object.prototype.padding = 10;
 
   // 修改控制点的形状,默认为`rect`矩形,可选的值还有`circle`圆形
@@ -26,3 +29,30 @@ export function fabricSetting(fabric) {
 
   window.fabric = fabric;
 }
+
+// // 渲染元素的icon按钮
+// function renderIcon(icon) {
+//   return function (ctx, left, top, styleOverride, fabricObject) {
+//     const size = this.cornerSize;
+//     ctx.save();
+//     ctx.translate(left, top);
+//     ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
+//     ctx.drawImage(icon, -size / 2, -size / 2, size, size);
+//     ctx.restore();
+//   };
+// }
+// function setControlIcon() {
+//   const favImg = document.createElement('img');
+//   favImg.src = favoritesImg;
+//   // 删除按钮控件
+//   fabric.Object.prototype.controls.favIcon = new fabric.Control({
+//     x: 0.5,
+//     y: -0.5,
+//     offsetY: -16,
+//     offsetX: 26,
+//     cursorStyle: 'pointer',
+//     // mouseUpHandler: deleteObject,
+//     render: renderIcon(favImg),
+//     cornerSize: 24,
+//   });
+// }

+ 17 - 0
src/views/map-config/mini-map/MapBase/types.ts

@@ -10,6 +10,7 @@ export interface MapData {
     angle: number;
   };
   objects: CameraImgObject[];
+  defaultCameraId: string;
 }
 
 export interface CameraImgObject {
@@ -21,3 +22,19 @@ export interface CameraImgObject {
   angle: number;
   cameraId: string;
 }
+
+export interface CameraImage extends fabric.Image {
+  cameraId?: string;
+}
+
+export type Canvas = fabric.Canvas;
+
+export const isCanvas = (canvas: Canvas | null | undefined): canvas is Canvas => {
+  return Boolean(canvas);
+};
+
+export type OnSelect = (image: CameraImage | null) => unknown;
+export type OnRightClick = (e: fabric.IEvent<MouseEvent>) => unknown;
+export type OnMoving = (e: fabric.IEvent<MouseEvent>) => unknown;
+export type OnRotating = (e: fabric.IEvent<MouseEvent>) => unknown;
+export type SetSelectedCamera = (e: CameraImage | null) => unknown;

+ 249 - 0
src/views/map-config/mini-map/MapBase/useCameraMap.ts

@@ -0,0 +1,249 @@
+import { fabric } from 'fabric';
+import { ref } from 'vue';
+import cameraImg from '@/assets/camera/camera.png';
+import {
+  CameraImage,
+  MapData,
+  OnMoving,
+  OnRightClick,
+  OnSelect,
+  isCanvas,
+  OnRotating,
+  SetSelectedCamera,
+} from './types';
+import { fabricSetting } from './fabricSetting';
+import { getRandomPosition } from './utils';
+
+fabricSetting();
+
+interface Props {
+  onSelect: OnSelect;
+  onRightClick: OnRightClick;
+  onMoving: OnMoving;
+  onRotating: OnRotating;
+  setSelectedCamera: SetSelectedCamera;
+  onObjectsAdded: () => unknown;
+}
+
+function useCameraMap(props: Props) {
+  let canvas;
+
+  const createMap = (canvasId: string) => {
+    canvas = new fabric.Canvas(canvasId, {
+      fireRightClick: true, // 启用右键,button的数字为3
+      stopContextMenu: true, // 禁止默认右键菜单
+    });
+    addListener();
+    // window.canvas = canvas;
+  };
+
+  /** 监听点击事件 */
+  const addListener = () => {
+    if (!isCanvas(canvas)) return;
+
+    canvas.on('mouse:down', (options) => {
+      if (!canvas) return;
+      const target = options.target as CameraImage;
+      const cameraId = target?.cameraId;
+      // console.log('当前选中的id是', cameraId);
+      console.log('mouse:down');
+      console.log(options);
+
+      // 判断:右键,且在元素上右键
+      // opt.button: 1-左键;2-中键;3-右键
+      // 在画布上点击:opt.target 为 null
+      if (options.button === 3 && options.target) {
+        props.onRightClick(options);
+        return;
+      }
+
+      if (!cameraId || !target) {
+        props.onSelect(null);
+        return;
+      }
+
+      props.onSelect(target);
+    });
+
+    canvas.on('object:moving', (e) => {
+      // console.log('object moving', e);
+      props.onMoving(e);
+    });
+    canvas.on('object:rotating', function (e) {
+      props.onRotating(e);
+    });
+
+    /** 监听点击选中的时候 */
+    canvas.on('selection:created', function (e) {
+      console.log('selection created', e);
+      props.setSelectedCamera((e.selected?.[0] as CameraImage) || null);
+    });
+    /** 监听有选中更新的时候 */
+    canvas.on('selection:updated', function (e) {
+      console.log('selection updated', e);
+      props.setSelectedCamera((e.selected?.[0] as CameraImage) || null);
+    });
+    /** 监听选中取消的时候 */
+    canvas.on('selection:cleared', function (e) {
+      console.log('selection cleared', e);
+      props.setSelectedCamera(null);
+    });
+    /** 监听object增加的时候 */
+    canvas.on('object:added', function (e) {
+      console.log('object add', e);
+      props.onObjectsAdded();
+    });
+    /** 监听object删除的时候 */
+    canvas.on('object:removed', function (e) {
+      console.log('object removed', e);
+      props.onObjectsAdded();
+    });
+  };
+
+  /**  上传背景图 */
+  const uploadBg = (imgUrl: string) => {
+    if (!isCanvas(canvas)) return;
+    fabric.Image.fromURL(imgUrl, (img) => {
+      const refImg = ref(img);
+      console.log('image', img);
+      canvas!.setWidth(img.width!);
+      canvas!.setHeight(img.height!);
+      refImg.value.lockScalingX = true;
+      refImg.value.lockScalingY = true;
+      // 设置背景图
+      canvas?.setBackgroundImage(refImg.value, canvas!.renderAll.bind(canvas));
+    });
+  };
+
+  /**  增加一个摄像头 */
+  const addCamera = (cameraId: string): Promise<CameraImage> => {
+    if (!isCanvas(canvas)) return Promise.reject();
+    // eslint-disable-next-line @typescript-eslint/no-this-alias
+    return new Promise((resolve) => {
+      fabric.Image.fromURL(cameraImg, (img) => {
+        const cImg = ref(img as unknown as CameraImage);
+        cImg.value.set({
+          left: getRandomPosition(),
+          top: getRandomPosition(),
+          cameraId,
+        });
+        cImg.value.lockScalingX = true;
+        cImg.value.lockScalingY = true;
+
+        canvas?.add(cImg.value);
+        canvas?.setActiveObject(cImg.value);
+        // cImg.value.on('moving', function (e) {
+        //   props.onMoving(e);
+        // });
+        // cImg.value.on('rotating', function (e) {
+        //   props.onRotating(e);
+        // });
+
+        resolve(cImg.value);
+      });
+    });
+  };
+
+  /**  删除一个摄像头 */
+  const removeActiveCamera = () => {
+    if (!isCanvas(canvas)) return;
+    const activeObject = canvas?.getActiveObject();
+    if (!activeObject) return;
+    canvas.remove(activeObject);
+  };
+
+  /** 导出JSON格式 */
+  const toJSON = () => {
+    if (!isCanvas(canvas)) return;
+    const initialJSON = canvas.toJSON(['cameraId']);
+    /** toJSON返回值的类型它写错了,应该是有backgroundImage的 */
+    const { src, type, version, width, height, left, top, angle } =
+      (initialJSON as any).backgroundImage || {};
+
+    const newObjects = initialJSON.objects.map((item) => {
+      return {
+        type: item.type,
+        width: item.width,
+        height: item.height,
+        left: item.left,
+        top: item.top,
+        angle: item.angle,
+        cameraId: (item as CameraImage).cameraId,
+      };
+    });
+    const newJson = {
+      version: initialJSON.version,
+      backgroundImage: { src, type, version, width, height, left, top, angle },
+      objects: newObjects,
+    };
+    return newJson;
+  };
+
+  /**  从json中加载 */
+  const loadFromJSON = (json: MapData): Promise<void> => {
+    if (!isCanvas(canvas)) return Promise.reject();
+    canvas.clear();
+    return new Promise((resolve) => {
+      const { width, height } = json.backgroundImage;
+      canvas?.setWidth(width);
+      canvas?.setHeight(height);
+      const objects = json.objects.map((item) => {
+        return {
+          ...item,
+          src: cameraImg,
+        };
+      });
+      canvas?.loadFromJSON({ ...json, objects }, () => {
+        resolve();
+      });
+    });
+  };
+
+  /**  更新摄像头的渲染 */
+  const renderCamera = () => {
+    canvas?.renderAll();
+  };
+  /**  */
+  const clear = () => {
+    canvas?.clear();
+  };
+
+  /**  是否已经存在这个cameraId */
+  const hasCamera = (cameraId: string) => {
+    const cameraIds =
+      canvas?.toJSON(['cameraId']).objects.map((item) => (item as CameraImage).cameraId) || [];
+    return cameraIds.includes(cameraId);
+  };
+
+  const getObjects = () => {
+    return canvas?.getObjects() as CameraImage[];
+  };
+  const getActiveObject = () => {
+    return canvas?.getActiveObject() as CameraImage;
+  };
+
+  /**  根据cameraId查找某个元素 */
+  const getCameraById = (cameraId: string) => {
+    return canvas
+      ?.getObjects()
+      .find((x) => (x as CameraImage).cameraId === cameraId) as CameraImage;
+  };
+
+  return {
+    canvas,
+    createMap,
+    uploadBg,
+    addCamera,
+    removeActiveCamera,
+    toJSON,
+    loadFromJSON,
+    renderCamera,
+    clear,
+    hasCamera,
+    getCameraById,
+    getObjects,
+    getActiveObject,
+  };
+}
+
+export default useCameraMap;

+ 22 - 0
src/views/map-config/mini-map/MapBase/utils.ts

@@ -0,0 +1,22 @@
+import { fabric } from 'fabric';
+export function getRandomPosition() {
+  return 100 + Math.floor(Math.random() * 30);
+}
+
+/** 根据camera位置得到fav icon的位置 */
+export function getFavPositionByCamera(target?: fabric.Object | null) {
+  if (!target || !target.oCoords) return null;
+  return { left: target.oCoords?.tr.x, top: target.oCoords?.tr.y };
+}
+
+export function createSelectedPositionHash(target) {
+  if (!target) return '';
+  return (
+    target.cameraId +
+    String(target.oCoords.tr.x) +
+    '_' +
+    String(target.oCoords.tr.y) +
+    '_' +
+    String(target.angle)
+  );
+}

+ 152 - 98
src/views/map-config/mini-map/MiniMapConfig.vue

@@ -59,8 +59,11 @@
           <div
             v-for="item in filterShopCameraList"
             :key="item.code"
-            class="camera-item flex justify-start items-center"
-            :class="{ isAdded: isAddedToMap(item.code), 'camera-item-disabled': !hasBg }"
+            class="camera-item flex justify-start"
+            :class="{
+              isAdded: isAddedToMap(item.code),
+              isActive: item.code === selectedCamera?.cameraId,
+            }"
             @click="handleAddCamera(item.code)"
           >
             <span class="camera-id">{{ item.name }}</span>
@@ -78,44 +81,58 @@
         </el-scrollbar>
       </div>
       <div class="draw-container">
-        <!-- <div style="display: flex; margin-bottom: 20px">
-          <div>
-            默认摄像头:
-            <ElSelect v-model="defaultCameraId">
-              <ElOption
-                :key="item.value"
-                :label="item.value"
-                :value="item.value"
-                v-for="item in cameraOptions"
-              />
-            </ElSelect>
-          </div>
-        </div> -->
-        <!-- <div>
+        <div style="display: flex; margin-bottom: 20px">
+          <el-upload
+            class="avatar-uploader flex justify-center items-center"
+            action="/temp/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">+ 更换/上传背景图片</el-button>
+          </el-upload>
+
+          <el-button
+            @click="handleSave"
+            style="margin-left: 40px"
+            type="primary"
+            :disabled="!selectedShopCode"
+            >保存布局</el-button
+          >
+        </div>
+        <div>
           <div style="height: 20px; margin-bottom: 10px">
             <div v-if="selectedCamera">
-              已选中相机<span>: {{ selectedCamera?.cameraId }}</span>
-              <span style="margin-left: 20px">
-                工位:{{ selectedCameraDetail?.workSpaceName }}
-              </span>
+              <SelectedCameraToolbar
+                @render-map="renderMap"
+                :selected-camera="selectedCamera!"
+                :key="selectedPositionHash"
+              />
             </div>
           </div>
-        </div> -->
+        </div>
+        -->
         <div style="overflow: auto">
           <canvas width="400" height="400" id="mapEditCanvas"></canvas>
         </div>
-        <el-upload
-          v-if="!hasBg"
-          class="upload-icon flex justify-center items-center"
-          action="/temp/api/layout/uploadPicture"
-          :show-file-list="false"
-          :on-success="handleAvatarSuccess"
-          :with-credentials="true"
-          name="file"
-          :data="{ workshopId: selectedShopCode }"
-        >
-          <img src="~@/assets/images/img-upload.png" />
-        </el-upload>
+        <div style="overflow: auto; position: relative">
+          <canvas
+            width="400"
+            height="400"
+            id="mapEditCanvas"
+            style="border: 1px solid #ccc"
+          ></canvas>
+          <ContextMenu
+            :visible="menuVisible"
+            :position="mousePosition"
+            :set-default="handleSetDefault"
+          />
+          <DefaultCameraIcon :position="favPosition" />
+        </div>
+
+        <CameraPreview :json="cameraJSON" />
       </div>
     </div>
   </div>
@@ -126,33 +143,80 @@
   import { storeToRefs } from 'pinia';
   import { ElMessage, ElInput } from 'element-plus';
   import urlJoin from 'url-join';
-  import { onMounted, ref, toRaw } from 'vue';
-  import CameraMap, { CameraImage } from './MapBase/CameraMap';
-  // import { ElSelect, ElOption } from 'element-plus';
+  import { onMounted, ref } from 'vue';
+  import useCameraMap from './MapBase/useCameraMap';
   import { updateMinMapViewLayoutApi } from '@/api/scene/scene';
-  import { onUnmounted } from 'vue';
+  import { onUnmounted, watchEffect } from 'vue';
   import { useGlobSetting } from '@/hooks/setting';
   import { computed } from 'vue';
   import ContextMenu from './MapBase/ContextMenu.vue';
-  import { Search, ArrowLeft, Refresh } from '@element-plus/icons-vue';
+  import DefaultCameraIcon from './MapBase/DefaultCameraIcon.vue';
+  import { CameraImage } from './MapBase/types';
+  import { createSelectedPositionHash, getFavPositionByCamera } from './MapBase/utils';
+  import SelectedCameraToolbar from './components/SelectedCameraToolbar.vue';
+  import CameraPreview from './MapBase/CameraPreview.vue';
 
   const miniMap = useMiniMap();
   const globSetting = useGlobSetting();
   const { scenesTree, shopCameraList, selectedShopCode, selectedShopDetail } = storeToRefs(miniMap);
   const { getScenesTree, getShowCameras, getMapLayout } = miniMap;
+  const selectedPositionHash = ref('');
+  const allObjects = ref<CameraImage[]>([]);
+
+  const onSelectCamera = () => {
+    menuVisible.value = false;
+  };
+
+  const handleRightClick = (e) => {
+    mousePosition.value = { left: e.pointer.x, top: e.pointer.y };
+    menuVisible.value = true;
+    rightSelectedCamera.value = e.target;
+  };
+
+  const handleMoving = (e) => {
+    const target = e.transform.target;
+    selectedPositionHash.value = createSelectedPositionHash(target);
+    if (target?.cameraId === defaultCamera.value?.cameraId) {
+      favPosition.value = getFavPositionByCamera(target);
+    }
+  };
 
-  let map: CameraMap;
-  const selectedCamera = ref<CameraImage | null>(null);
+  const handleRoating = (e) => {
+    const target = e.transform.target;
+    selectedPositionHash.value = createSelectedPositionHash(target);
+    if (target.cameraId === defaultCamera.value?.cameraId) {
+      favPosition.value = getFavPositionByCamera(target);
+    }
+  };
+
+  const handleSetSelectedCameara = (target: CameraImage | null) => {
+    selectedCamera.value = target;
+  };
+
+  const handleObjectsAdded = () => {
+    allObjects.value = map.getObjects();
+  };
+
+  const map = useCameraMap({
+    onSelect: onSelectCamera,
+    onRightClick: handleRightClick,
+    onMoving: handleMoving,
+    onRotating: handleRoating,
+    setSelectedCamera: handleSetSelectedCameara,
+    onObjectsAdded: handleObjectsAdded,
+  });
+
+  const selectedCamera = ref<CameraImage | null>();
   /** 右键选中的摄像机 */
   const rightSelectedCamera = ref<CameraImage | null>(null);
-  const cameraOptions = ref<{ label: string; value: string }[]>([]);
-  const defaultCameraId = ref('');
+  const defaultCamera = ref<CameraImage | null>();
   const searchKey = ref('');
   // 是否已有背景图
   const hasBg = ref(false);
 
   const mousePosition = ref<{ left: number; top: number }>({ left: 0, top: 0 });
   const menuVisible = ref(false);
+  const favPosition = ref<{ left: number; top: number } | null>(null);
 
   const handleAvatarSuccess = (e) => {
     const imgPath = e.data;
@@ -161,24 +225,37 @@
     map.uploadBg(imgUrl);
   };
 
+  const renderMap = () => {
+    map.renderCamera();
+    favPosition.value = getFavPositionByCamera(defaultCamera.value);
+  };
+
   const changeShop = (code: string) => {
     getShopContent(code);
     hasBg.value = false;
   };
 
+  const cameraJSON = ref();
+
   const getShopContent = (code: string) => {
     getShowCameras(code);
     getMapLayout(code).then((res) => {
       if (!res) {
         map.clear();
+        defaultCamera.value = null;
         return;
       }
-      defaultCameraId.value = res.defaultCameraId;
+      cameraJSON.value = res;
+
       map.loadFromJSON(res).then(() => {
-        mapJSONToOptions();
-        if (!defaultCameraId.value) {
-          defaultCameraId.value = cameraOptions.value[0]?.value;
+        console.log('loadFromJSON', res);
+        if (res.defaultCameraId) {
+          defaultCamera.value = map.getCameraById(res.defaultCameraId);
+          console.log('defaultCamera', defaultCamera.value);
+        } else {
+          defaultCamera.value = map.getObjects()?.[0] as CameraImage;
         }
+        renderMap();
       });
     });
   };
@@ -188,19 +265,13 @@
     if (selectedShopCode.value) {
       getShopContent(selectedShopCode.value);
     }
-    map = new CameraMap({
-      canvasId: 'mapEditCanvas',
-      onSelect: onSelectCamera,
-      onRightClick: handleRightClick,
-    });
+    map.createMap('mapEditCanvas');
   });
 
   const keyupListener = (e) => {
     const keyCode = e.code;
     if (keyCode === 'Delete') {
-      if (selectedCamera.value) {
-        handleDeleteCamera();
-      }
+      handleDeleteCamera();
     }
   };
 
@@ -212,10 +283,6 @@
     document.removeEventListener('keyup', keyupListener);
   });
 
-  const selectedCameraDetail = computed(() => {
-    return shopCameraList.value.find((item) => item.code === selectedCamera.value?.cameraId);
-  });
-
   const filterShopCameraList = computed(() => {
     const k = searchKey.value.trim();
     if (!k) return shopCameraList.value;
@@ -224,7 +291,7 @@
 
   /** 摄像机是否已添加到底图 */
   const isAddedToMap = (cameraId: string): boolean => {
-    return cameraOptions.value.some((x) => x.value === cameraId);
+    return !!allObjects.value.find((item) => item.cameraId === cameraId);
   };
 
   const handleAddCamera = (cameraId: string) => {
@@ -234,38 +301,12 @@
       return;
     }
     map.addCamera(cameraId).then((cameraImg) => {
-      onSelectCamera(cameraImg);
-      mapJSONToOptions();
-      if (!defaultCameraId.value) {
-        defaultCameraId.value = cameraOptions.value[0]?.value;
+      if (!defaultCamera.value?.cameraId) {
+        defaultCamera.value = cameraImg;
       }
     });
   };
 
-  const onSelectCamera = (cameraImg: CameraImage) => {
-    console.log('onSelectCamera', cameraImg);
-    selectedCamera.value = cameraImg;
-    menuVisible.value = false;
-  };
-
-  const handleRightClick = (e) => {
-    mousePosition.value = { left: e.pointer.x, top: e.pointer.y };
-    menuVisible.value = true;
-    rightSelectedCamera.value = e.target;
-  };
-
-  const mapJSONToOptions = () => {
-    const objects = map.toJSON()?.objects || [];
-    if (map.toJSON()?.backgroundImage.src) {
-      hasBg.value = true;
-    }
-    console.log('objects', objects);
-    cameraOptions.value = objects?.map((x) => ({
-      label: x.cameraId,
-      value: x.cameraId,
-    }));
-  };
-
   const handleSave = () => {
     const json = map.toJSON();
     console.log('save json', json);
@@ -273,31 +314,40 @@
       ElMessage.error('背景图片未添加');
       return;
     }
-    const layout = JSON.stringify({ ...json, defaultCameraId: defaultCameraId.value });
-    updateMinMapViewLayoutApi({ layout, targetId: selectedShopCode.value + '' }).then((res) => {
-      console.log('updateMinMapViewLayoutApi', res);
-      ElMessage.success('保存成功');
-    });
+    const layout = JSON.stringify({ ...json, defaultCameraId: defaultCamera.value?.cameraId });
+    updateMinMapViewLayoutApi({ layout, targetId: String(selectedShopDetail.value?.id) }).then(
+      (res) => {
+        console.log('updateMinMapViewLayoutApi', res);
+        ElMessage.success('保存成功');
+      },
+    );
   };
 
   const handleDeleteCamera = () => {
     if (!selectedCamera.value) return;
-    map.removeCamera(toRaw(selectedCamera.value!));
     /** 如果删除的是默认选中的摄像头,那么先清空默认的摄像头再 */
-    mapJSONToOptions();
-    if (selectedCamera.value.cameraId === defaultCameraId.value) {
-      defaultCameraId.value = cameraOptions.value[0]?.value;
+    if (selectedCamera.value?.cameraId === defaultCamera.value?.cameraId) {
+      defaultCamera.value = map.getObjects()?.[0];
     }
-    selectedCamera.value = null;
+    map.removeActiveCamera();
   };
 
   const handleSetDefault = () => {
     const cameraId = rightSelectedCamera.value?.cameraId;
     if (!cameraId) return;
-    defaultCameraId.value = cameraId;
+    defaultCamera.value = rightSelectedCamera.value;
+
     /** 选择完成后隐藏 */
     menuVisible.value = false;
   };
+
+  watchEffect(() => {
+    if (!defaultCamera.value) {
+      favPosition.value = null;
+      return;
+    }
+    favPosition.value = getFavPositionByCamera(defaultCamera.value);
+  });
 </script>
 
 <style scoped>
@@ -396,4 +446,8 @@
   .isAdded {
     color: #409eff;
   }
+  .isActive {
+    background-color: #eee;
+  }
 </style>
+./MapBase/useCameraMap ./MapBase/CameraMapBak

+ 33 - 0
src/views/map-config/mini-map/components/EditDimension.vue

@@ -0,0 +1,33 @@
+<template>
+  <span style="padding-left: 10px">
+    <span>{{ props.label }}: </span>
+    <ElInput
+      style="width: 50px"
+      size="small"
+      @input="handleChange"
+      v-model="val"
+      :disabled="props.disabled"
+    />
+  </span>
+</template>
+<script lang="ts" setup>
+  import { ref, watch } from 'vue';
+  import { ElInput } from 'element-plus';
+  const props = defineProps<{ label: string; modelValue: number; disabled?: boolean }>();
+
+  const val = ref();
+  watch(
+    () => props.modelValue,
+    () => {
+      val.value = Math.floor(props.modelValue);
+    },
+    { immediate: true },
+  );
+
+  const emits = defineEmits<{ (e: 'update:modelValue', val: number): unknown }>();
+
+  const handleChange = (e) => {
+    emits('update:modelValue', Number(e));
+  };
+</script>
+<style scoped></style>

+ 52 - 0
src/views/map-config/mini-map/components/SelectedCameraToolbar.vue

@@ -0,0 +1,52 @@
+<template>
+  <div>
+    已选中相机<span>: {{ selectedCamera?.cameraId }}</span>
+    <span
+      ><EditDimension
+        v-model="selectedCamera.width"
+        label="width"
+        @update:model-value="renderMap"
+        disabled
+    /></span>
+    <span
+      ><EditDimension
+        v-model="selectedCamera.height"
+        label="height"
+        @update:model-value="renderMap"
+        disabled
+    /></span>
+    <span
+      ><EditDimension v-model="selectedCamera.left" label="left" @update:model-value="renderMap"
+    /></span>
+    <span
+      ><EditDimension v-model="selectedCamera.top" label="top" @update:model-value="renderMap"
+    /></span>
+    <span
+      ><EditDimension v-model="selectedCamera.angle" label="angle" @update:model-value="renderMap"
+    /></span>
+  </div>
+</template>
+<script lang="ts" setup>
+  import { ref } from 'vue';
+  import EditDimension from './EditDimension.vue';
+
+  const props = defineProps<{
+    selectedCamera: {
+      cameraId: string;
+      width: number;
+      height: number;
+      left: number;
+      top: number;
+      angle: number;
+    };
+  }>();
+
+  const selectedCamera = ref(props.selectedCamera);
+
+  const emits = defineEmits<{ (e: 'renderMap'): unknown }>();
+
+  const renderMap = () => {
+    emits('renderMap');
+  };
+</script>
+<style scoped></style>