Browse Source

feat: 主页配置功能主体完成

sunhongyao341504 2 years ago
parent
commit
36e45703ab

+ 1 - 1
.env.development

@@ -5,7 +5,7 @@ VITE_PORT = 8092
 VITE_PUBLIC_PATH = /
 
 # 是否开启mock
-VITE_USE_MOCK = true
+VITE_USE_MOCK = false
 
 # 网站前缀
 VITE_BASE_URL = /

+ 1 - 0
package.json

@@ -42,6 +42,7 @@
     "@wangeditor/editor-for-vue": "5.1.12",
     "axios": "0.27.2",
     "blueimp-md5": "2.19.0",
+    "canvg": "^4.0.1",
     "cropperjs": "1.5.12",
     "dayjs": "1.11.4",
     "echarts": "5.3.3",

+ 48 - 0
pnpm-lock.yaml

@@ -38,6 +38,9 @@ dependencies:
   blueimp-md5:
     specifier: 2.19.0
     version: 2.19.0
+  canvg:
+    specifier: ^4.0.1
+    version: 4.0.1
   cropperjs:
     specifier: 1.5.12
     version: 1.5.12
@@ -1402,6 +1405,10 @@ packages:
     resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
     dev: true
 
+  /@types/offscreencanvas@2019.7.3:
+    resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==}
+    dev: false
+
   /@types/parse-json@4.0.0:
     resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
     dev: true
@@ -1412,6 +1419,10 @@ packages:
       '@types/node': 17.0.45
     dev: true
 
+  /@types/raf@3.4.3:
+    resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==}
+    dev: false
+
   /@types/resolve@1.17.1:
     resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
     dependencies:
@@ -2567,6 +2578,18 @@ packages:
     dev: false
     optional: true
 
+  /canvg@4.0.1:
+    resolution: {integrity: sha512-5gD/d6SiCCT7baLnVr0hokYe93DfcHW2rSqdKOuOQD84YMlyfttnZ8iQsThTdX6koYam+PROz/FuQTo500zqGw==}
+    engines: {node: '>=12.0.0'}
+    dependencies:
+      '@types/offscreencanvas': 2019.7.3
+      '@types/raf': 3.4.3
+      raf: 3.4.1
+      rgbcolor: 1.0.1
+      stackblur-canvas: 2.6.0
+      svg-pathdata: 6.0.3
+    dev: false
+
   /capital-case@1.0.4:
     resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==}
     dependencies:
@@ -6291,6 +6314,10 @@ packages:
     resolution: {integrity: sha512-dzalfutyP3e/FOpdlhVryN4AJ5XDVauVWxybSkLZmakFE2sS3y3pc4JnSprw8tGmHvkaG5Edr5T7LBTZ+WWU2g==}
     dev: false
 
+  /performance-now@2.1.0:
+    resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
+    dev: false
+
   /picocolors@1.0.0:
     resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
 
@@ -6676,6 +6703,12 @@ packages:
       quill-delta: 3.6.3
     dev: false
 
+  /raf@3.4.1:
+    resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
+    dependencies:
+      performance-now: 2.1.0
+    dev: false
+
   /read-cache@1.0.0:
     resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
     dependencies:
@@ -6845,6 +6878,11 @@ packages:
     resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==}
     dev: true
 
+  /rgbcolor@1.0.1:
+    resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==}
+    engines: {node: '>= 0.8.15'}
+    dev: false
+
   /rimraf@3.0.2:
     resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
     hasBin: true
@@ -7225,6 +7263,11 @@ packages:
     deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility'
     dev: true
 
+  /stackblur-canvas@2.6.0:
+    resolution: {integrity: sha512-8S1aIA+UoF6erJYnglGPug6MaHYGo1Ot7h5fuXx4fUPvcvQfcdw2o/ppCse63+eZf8PPidSu4v1JnmEVtEDnpg==}
+    engines: {node: '>=0.1.14'}
+    dev: false
+
   /static-extend@0.1.2:
     resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==}
     engines: {node: '>=0.10.0'}
@@ -7505,6 +7548,11 @@ packages:
       - supports-color
     dev: true
 
+  /svg-pathdata@6.0.3:
+    resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==}
+    engines: {node: '>=12.0.0'}
+    dev: false
+
   /svg-tags@1.0.0:
     resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==}
     dev: true

+ 45 - 1
src/api/scene/scene.ts

@@ -53,7 +53,7 @@ export type WorkShopInfoItem = {
   /** 所属公司id */
   companyId: number;
   /** 1-生产安全 2-安全环保 */
-  type: number;
+  // type: number;
   /** 工厂名称 */
   name: string;
   /** 工厂code */
@@ -194,3 +194,47 @@ export const getCamerasByWorkSpace = (params: { workshopId: number }) => {
     headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
   });
 };
+
+/** 公司主页配置 */
+interface UpdateCompanyLayoutParam extends Omit<UpdateViewLayoutParam, 'targetId'> {
+  /** 标签id */
+  labelId: number;
+  targetId: number;
+  /** 更新时需要上传layout布局id */
+  id?: number;
+}
+
+/** 新增公司主页配置 */
+export const uploadCompanyLayoutApi = (data: UpdateCompanyLayoutParam) => {
+  return http.request({
+    url: '/homepageConfig/saveCompanyLayout',
+    method: 'post',
+    data,
+  });
+};
+
+export const uploadCompanyLayout = (param: Omit<UpdateCompanyLayoutParam, 'viewType'>) => {
+  return uploadCompanyLayoutApi({ ...param, viewType: ViewType.safety });
+};
+
+/** 更新公司主页配置 */
+export const updateCompanyLayoutApi = (data: UpdateCompanyLayoutParam) => {
+  return http.request({
+    url: '/homepageConfig/updateCompanyLayout',
+    method: 'put',
+    data,
+  });
+};
+
+export const updateCompanyLayout = (param: Omit<UpdateCompanyLayoutParam, 'viewType'>) => {
+  return updateCompanyLayoutApi({ ...param, viewType: ViewType.safety });
+};
+
+/** 查询公司主页配置 */
+export const getCompanyLayoutApi = (params: { companyId: number; labelId: number }) => {
+  return http.request({
+    url: '/homepageConfig/getCompanyLayoutByCompanyLabel',
+    method: 'get',
+    params,
+  });
+};

+ 5 - 4
src/assets/icons/posPoint.svg

@@ -1,8 +1,8 @@
-<svg width="26.762" height="32.282" xmlns="http://www.w3.org/2000/svg">
+<svg width="26.762" fill="currentColor" height="32.282" xmlns="http://www.w3.org/2000/svg">
     <defs>
         <linearGradient x1="84.362%" y1="100%" x2="15.638%" y2="0%" id="prefix__a">
-            <stop stop-color="#ef684d" offset="0%" />
-            <stop stop-color="#ef684d" stop-opacity=".5" offset="100%" />
+            <stop stop-color="currentColor" offset="0%" />
+            <stop stop-color="currentColor" stop-opacity=".5" offset="100%" />
         </linearGradient>
         <!-- <linearGradient x1="50%" y1="0%" x2="50%" y2="98.062%" id="prefix__b">
             <stop stop-color="#D3FFFA" offset="0%" />
@@ -13,7 +13,8 @@
     <g fill="none" fill-rule="evenodd">
         <path
             d="M13.38.25c3.361 0 6.722 1.281 9.286 3.844a13.101 13.101 0 013.846 9.279c0 3.36-1.287 6.585-3.586 9.012l-.26.266-9.285 9.278-9.286-9.28A13.101 13.101 0 01.25 13.372c0-3.484 1.384-6.824 3.845-9.278A13.094 13.094 0 0113.381.25zm0 8.916a4.24 4.24 0 00-3.015 1.256 4.282 4.282 0 00-1.249 3.03 4.28 4.28 0 001.249 3.029 4.24 4.24 0 003.016 1.255 4.24 4.24 0 003.016-1.255 4.282 4.282 0 001.248-3.03 4.282 4.282 0 00-1.248-3.03 4.24 4.24 0 00-3.016-1.255z"
-            stroke-opacity=".498" stroke="#B0E8FF" stroke-width=".5" fill-opacity=".9" fill="url(#prefix__a)" />
+            stroke-opacity=".498" stroke="#B0E8FF" stroke-width=".5" fill-opacity=".9" fill="currentColor"
+            xlink:href="url(#prefix__a)" />
         <ellipse fill="#FFFFFF" fill-rule="nonzero" cx="13.634" cy="13.619" rx="4.545" ry="4.54" />
     </g>
 </svg>

File diff suppressed because it is too large
+ 15 - 13
src/assets/icons/posRect.svg


BIN
src/assets/images/camera/hover-b16.jpg


+ 0 - 1
src/hooks/useSceneInfos.ts

@@ -54,7 +54,6 @@ export function useSceneInfos() {
     getShopSpaceList().then((res) => {
       scenesInfos.value = res;
       scenesTree.value = calculateTreeData(res, treeProps, 1);
-      console.log(flattendWorkspaces.value);
     });
   };
 

+ 1 - 4
src/utils/index.ts

@@ -277,12 +277,9 @@ export const downloadFile = (res, filename?) => {
  * 将rgb或rgba颜色转化为[hex,透明度?]
  */
 export const colorRGB2Hex = (color: string): string[] => {
-  console.log(color);
-
-  if (!color) return ['#ffffff', '100%'];
+  if (!color) return ['', ''];
   const pattern = /\((\d+),\s*(\d+),\s*(\d+),\s*(\d+(\.\d+)?)\)$/;
   const array = color.match(pattern);
-  console.log(array);
 
   const r = Number(array![1]);
   const g = Number(array![2]);

+ 6 - 7
src/views/map-config/mini-map/MiniMapConfig.vue

@@ -17,15 +17,15 @@
           />
         </div>
         <div class="flex">
-          <el-button @click="mapEditor.toJson">tojson</el-button>
+          <!-- <el-button @click="mapEditor.toJson">tojson</el-button> -->
           <el-upload
             class="avatar-uploader flex justify-center items-center"
-            action="/layout/uploadPicture"
+            action="/api/layout/uploadPicture"
             :show-file-list="false"
             :on-success="handleAvatarSuccess"
             :with-credentials="true"
             name="file"
-            :data="{ workshopId: selectedShopCode }"
+            :data="{ workshopId: selectedShopDetail?.id }"
           >
             <el-button style="font-size: 12px" :icon="Refresh" :disabled="!hasBg">
               替换照片
@@ -146,20 +146,19 @@
   };
 
   const changeShop = (code: string) => {
+    mapEditor.resetMap();
     getShopContent(code);
     hasBg.value = false;
   };
 
-  const mapJSON = ref();
-
   const getShopContent = (code: string) => {
     getShowCameras(code);
     getMapLayout(code).then((res) => {
       if (!res) {
         return;
       }
-      mapJSON.value = res;
-      console.log(mapJSON);
+      hasBg.value = true;
+      mapEditor.createMap(res);
     });
   };
 

+ 25 - 11
src/views/map-config/mini-map/hooks/useMapEditor.ts

@@ -10,8 +10,8 @@ import { useGlobSetting } from '@/hooks/setting';
 import urlJoin from 'url-join';
 
 export function useMapEditor() {
-  let initWidth; // 默认宽度
-  let initHeight; // 默认高度
+  // let initWidth; // 默认宽度
+  // let initHeight; // 默认高度
   let stage: Konva.Stage | null = null;
   let layer: Konva.Layer | null = null;
   let defaultIcon: Konva.Image | null = null; // 默认相机的图标shape
@@ -28,8 +28,8 @@ export function useMapEditor() {
 
   /** 容器初始化 */
   const initContainer = (opt: Konva.StageConfig) => {
-    initWidth = opt.width || 0;
-    initHeight = opt.height || 0;
+    // initWidth = opt.width || 0;
+    // initHeight = opt.height || 0;
     stage = new Konva.Stage(opt);
     stage.on('click tap', handleStageClick);
     layer = new Konva.Layer();
@@ -60,10 +60,12 @@ export function useMapEditor() {
 
   /** 更换背景图时根据图片大小重置容器宽高 */
   const resizeContainer = (width, height) => {
-    const newWidth = width > initWidth ? width : initWidth;
-    const newHeight = height > initHeight ? height : initHeight;
-    stage?.width(newWidth);
-    stage?.height(newHeight);
+    // const newWidth = width > initWidth ? width : initWidth;
+    // const newHeight = height > initHeight ? height : initHeight;
+    // stage?.width(newWidth);
+    // stage?.height(newHeight);
+    stage?.width(width);
+    stage?.height(height);
   };
 
   /** 添加背景 */
@@ -97,7 +99,9 @@ export function useMapEditor() {
 
   /** 变更需要激活transform的相机 */
   const attachTransformer = (group: Konva.Group): Konva.Transformer => {
+    activeGroup.value?.draggable(false);
     activeGroup.value = group;
+    group.draggable(true);
     stage!.find('Transformer')[0]?.destroy(); // 清除现有transformer
     const id = group.id();
     const tr = new Konva.Transformer({
@@ -339,8 +343,8 @@ export function useMapEditor() {
   };
 
   /** 导入布局json */
-  const createMap = (json) => {
-    const layout = JSON.parse(json);
+  const createMap = (layout) => {
+    // const layout = JSON.parse(json);
     bgImgUrl.value = layout.bgImg;
     addBg();
     layout.cameraList.forEach((camera) => {
@@ -351,7 +355,7 @@ export function useMapEditor() {
         rotation: camera.rotation,
         scaleX: camera.scaleX,
         scaleY: camera.scaleY,
-        draggable: true,
+        draggable: false,
         name: 'group',
       });
       const camImg = new Image();
@@ -379,6 +383,15 @@ export function useMapEditor() {
     });
   };
 
+  const resetMap = () => {
+    layer?.clear();
+    addedCameras.value = [];
+    activeGroup.value = null;
+    defaultCameraId.value = '';
+    isTransform = false;
+    bgImgUrl.value = '';
+  };
+
   onMounted(() => {
     window.addEventListener('keydown', handleKeyDown);
   });
@@ -398,6 +411,7 @@ export function useMapEditor() {
     destoryOptBlock,
     toJson,
     createMap,
+    resetMap,
   };
 }
 

+ 128 - 86
src/views/page-config/ConfigEdit.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="page" @click="handleWholeClick">
+  <div class="page">
     <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">
@@ -23,7 +23,12 @@
         <div v-if="selectedCompany" style="display: flex; margin-top: 8px">
           <div class="label-workshop">选择标签:</div>
           <div>
-            <el-radio-group v-model="label" :border="true" style="display: flex">
+            <el-radio-group
+              v-model="label"
+              :border="true"
+              style="display: flex"
+              @change="changeShop"
+            >
               <el-radio-button
                 v-for="item in labelList"
                 :key="item.id"
@@ -35,7 +40,7 @@
           >
         </div>
         <div class="flex">
-          <el-button @click="mapEditor.toJson">tojson</el-button>
+          <!-- <el-button @click="toJson">tojson</el-button> -->
           <el-upload
             class="avatar-uploader flex justify-center items-center"
             action="/api/homepageConfig/updateCompanyPicture"
@@ -43,6 +48,7 @@
             :on-success="handleAvatarSuccess"
             :with-credentials="true"
             name="file"
+            :data="{ companyId: selectedCompany, labelId: label, deleteFileName: bgImg }"
           >
             <el-button style="font-size: 12px" :icon="Refresh" :disabled="!hasBg">
               替换照片
@@ -80,17 +86,19 @@
             :key="item.code"
             class="camera-item flex justify-start items-center"
             :class="{
-              isAdded: isAddedCamera(item.id),
-              isActive: item.code === activeShopId,
+              isAdded: isAddedShop(item.id),
+              isActive: item.id === activeShop.id,
             }"
-            @click="handleAddCamera(item)"
+            @click="handleAddShop(item)"
           >
             <span class="camera-id">{{ item.name }}</span>
           </div>
         </el-scrollbar>
       </div>
       <div ref="drawContainer" id="drawContainer" class="draw-container">
-        <div id="editContainer" v-moveable:1></div>
+        <div id="editContainer" v-moveable:1>
+          <MapContainer />
+        </div>
         <el-upload
           v-if="!hasBg"
           class="upload-icon flex justify-center items-center"
@@ -100,6 +108,7 @@
           :on-success="handleAvatarSuccess"
           :with-credentials="true"
           name="file"
+          :data="{ companyId: selectedCompany, labelId: label }"
         >
           <img src="~@/assets/images/img-upload.png" />
         </el-upload>
@@ -111,7 +120,6 @@
       content="显示侧边栏"
       :offset="12"
       placement="left"
-      @click="showEditConfig"
     >
       <div
         v-if="leftShow"
@@ -128,13 +136,8 @@
       <!-- <img src="~@/assets/icons/slide.png" alt="" class="dialog-btn" /> -->
     </el-tooltip>
 
-    <ConfigDialog
-      ref="configDrawer"
-      :shop="configShop"
-      @on-close="onClose"
-      @save-config="saveConfig"
-      class="drawer-position"
-    />
+    <ConfigDialog ref="configDrawer" @on-close="onClose" class="drawer-position" />
+
     <ConfigFinish
       :visible="visibleResult"
       :status="configStatus"
@@ -149,20 +152,25 @@
   import ConfigFinish from './component/ConfigFinish.vue';
   import { storeToRefs } from 'pinia';
   import { ElMessage, ElInput } from 'element-plus';
-  import { onMounted, ref } from 'vue';
-  import { updateMinMapViewLayoutApi, WorkShopInfoItem } from '@/api/scene/scene';
+  import { onBeforeUnmount, onMounted, ref } from 'vue';
+  import { WorkShopInfoItem } from '@/api/scene/scene';
   import { computed } from 'vue';
   import { Search, Refresh } from '@element-plus/icons-vue';
-  import useMapEditor from './hooks/useMapEditor';
   import usePageConfig from './usePageConfig';
-  import tempBg from '@/assets/images/camera/video-live.png';
-  import { title } from 'process';
+  import MapContainer from './component/mapContainer/MapContainer.vue';
+  import useMapEditor, { LabelPositionEnum } from './stores/useMapEditor';
+  import { uploadCompanyLayout, updateCompanyLayout, getCompanyLayoutApi } from '@/api/scene/scene';
+  import safeParse from '@/utils/safeParse';
+  import { useRouter } from 'vue-router';
 
-  const mapEditor = useMapEditor({ onShopStyle });
-  const { addedShops, bgImgUrl, activeShopId } = mapEditor;
+  const mapEditor = useMapEditor();
+  const { bgImg, addedShops, activeShop } = storeToRefs(mapEditor);
+  const { addShop, addBg, toJson, resetMap, createMap, deleteShop } = mapEditor;
+
+  const router = useRouter();
 
   const pageConfig = usePageConfig();
-  const { selectedCompany, scenesInfos, label, labelList, shopList } = pageConfig;
+  const { selectedCompany, scenesInfos, label, labelList, shopList, layoutId } = pageConfig;
 
   const drawContainer = ref<HTMLDivElement>();
 
@@ -171,23 +179,20 @@
   const hasBg = ref(false);
 
   const handleBeforeUpload = () => {
-    if (!selectedShopCode.value) {
+    if (!selectedCompany.value || !label.value) {
       ElMessage.error({
-        message: '请先选择车间',
+        message: '请先选择公司和标签',
       });
       return false;
     }
   };
 
   /** 判断相机是否已经添加 */
-  const isAddedCamera = (shopId: number) => {
-    const index = addedShops.value.findIndex((item) => item === shopId);
+  const isAddedShop = (shopId: number) => {
+    const index = addedShops.value.findIndex((item) => item.id === shopId);
     return index >= 0;
   };
 
-  /** -------- */
-  const showEditConfig = () => {};
-
   //左边的浮动按钮
   const leftShow = ref(true);
   const onClose = (val) => {
@@ -207,13 +212,6 @@
     configDrawer.value.openDialog();
   };
 
-  //需要从子组件中获得当前保存的车间数据
-  const saveConfig = (val) => {
-    console.log(val);
-  };
-
-  const configShop = ref<WorkShopInfoItem>({} as WorkShopInfoItem);
-
   //总体的保存,将整个数据传过去
   const visibleResult = ref(false);
   const configStatus = ref(true);
@@ -223,88 +221,120 @@
   /** ------------- */
 
   const handleAvatarSuccess = (e) => {
-    bgImgUrl.value = e.data;
-    mapEditor.addBg();
+    bgImg.value = e.data;
+    addBg();
     hasBg.value = true;
   };
 
-  const changeShop = (code: string) => {
-    getShopContent(code);
+  const changeShop = () => {
+    resetMap();
+    getShopContent();
     hasBg.value = false;
   };
 
-  const mapJSON = ref();
-
-  const getShopContent = (code: string) => {
-    getShowCameras(code);
-    getMapLayout(code).then((res) => {
-      if (!res) {
-        return;
-      }
-      mapJSON.value = res;
-      console.log(mapJSON);
-    });
+  const getShopContent = () => {
+    getCompanyLayoutApi({ companyId: selectedCompany.value || 2, labelId: label.value || 1 }).then(
+      (res) => {
+        if (!res) {
+          return;
+        }
+        layoutId.value = res.id;
+        const layoutJSON = res.layout ? safeParse(res.layout) : null;
+        if (!layoutJSON) {
+          return;
+        }
+        hasBg.value = true;
+        createMap(layoutJSON);
+      },
+    );
   };
 
-  const handleWholeClick = (e) => {
-    if (e.button === 0) {
-      mapEditor.destoryOptBlock();
-    }
+  const changeCompany = () => {
+    label.value = undefined;
+    resetMap();
+    hasBg.value = false;
   };
 
-  const changeCompany = () => {};
+  const handleKeyDown = (e) => {
+    // 删除键
+    if (e.keyCode === 46 || e.code === 'Delete') {
+      deleteShop();
+    }
+  };
 
   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);
-    // }
-
-    bgImgUrl.value = tempBg;
-    mapEditor.addBg();
-    hasBg.value = true;
+    const routerParams = router.currentRoute.value.query;
+    if (routerParams.companyId) {
+      selectedCompany.value = Number(routerParams.companyId);
+      console.log(selectedCompany.value);
+    }
+
+    window.addEventListener('keydown', handleKeyDown);
+
+    if (selectedCompany.value && label.value) {
+      getShopContent();
+    }
   });
 
   const filterShopList = computed(() => {
     const k = searchKey.value.trim();
     if (!k) return shopList.value;
-    return shopList.value.filter((x) => x.name?.includes(k));
+    return shopList.value?.filter((x) => x.name?.includes(k));
   });
 
-  const handleAddCamera = (shop: WorkShopInfoItem) => {
+  const handleAddShop = (shop: WorkShopInfoItem) => {
     if (!hasBg.value) {
       ElMessage.warning({
         message: '请先添加背景图片',
       });
       return;
     }
-    mapEditor.addShop(shop);
+    const shopNode = {
+      ...shop,
+      x: 20,
+      y: 20,
+      scale: 1,
+      bgColor: 'rgba(58, 170, 209, 1)',
+      fontSize: 14,
+      fontColor: '#ffffff',
+      posType: LabelPositionEnum.LEFT,
+    };
+    addShop(shopNode);
+    dialogReopen();
   };
 
   const handleSave = () => {
-    const layout = mapEditor.toJson();
-    updateMinMapViewLayoutApi({ layout, targetId: String(selectedShopDetail.value?.id) }).then(
-      (res) => {
-        console.log('updateMinMapViewLayoutApi', res);
+    const layout = toJson();
+    const param = {
+      layout,
+      targetId: selectedCompany.value || 2,
+      labelId: label.value || 1,
+    };
+    if (!layoutId.value) {
+      uploadCompanyLayout(param).then((res) => {
+        console.log('uploadCompanyLayout', res);
+        layoutId.value = res;
         ElMessage.success('保存成功');
-      },
-    );
+      });
+    } else {
+      updateCompanyLayout({ ...param, id: layoutId.value }).then((res) => {
+        console.log('updateCompanyLayout', res);
+        layoutId.value = res;
+        ElMessage.success('更新成功');
+      });
+    }
   };
 
-  function onShopStyle(shop: WorkShopInfoItem) {
-    configShop.value = shop;
-    dialogReopen();
-  }
+  onBeforeUnmount(() => {
+    window.removeEventListener('keydown', handleKeyDown);
+    resetMap();
+  });
 </script>
 
 <style scoped lang="scss">
   .page {
   }
+
   .page-head {
     height: 54px;
     padding-left: 15px;
@@ -318,12 +348,18 @@
   }
 
   .avatar-uploader {
-    /* width: 120px; */
-    /* height: 30px; */
-    /* border: 1px solid #eee; */
     border-radius: 4px;
     margin-left: 30px;
   }
+
+  .label-workshop {
+    font-size: 14px;
+    font-weight: 400;
+    margin-left: 36px;
+    margin-top: 6px;
+    margin-right: 16px;
+  }
+
   .upload-icon {
     position: absolute;
     top: 0;
@@ -456,5 +492,11 @@
     text-align: center;
     font-weight: 400;
   }
+  ::v-deep.el-radio-button {
+    margin-right: 8px;
+    box-shadow: none;
+    border-radius: 4px !important;
+    border: 1px solid #d9d9d9 !important;
+  }
 </style>
 ./MapBase/useCameraMap ./MapBase/CameraMapBak ./usePageConfig1

+ 58 - 64
src/views/page-config/component/ConfigDrawer.vue

@@ -7,10 +7,11 @@
       draggable
       :append-to-body="false"
       :destroy-on-close="true"
+      @close="closeDialog"
     >
       <template #header>
         <div style="position: relative">
-          <div class="dialog-header">{{ props.shop.name }}</div>
+          <div class="dialog-header">{{ editShop.name }}</div>
           <img
             src="~@/assets/icons/slide-right.png"
             alt=""
@@ -25,12 +26,22 @@
           <el-upload
             class="pic-uploader"
             list-type="picture-card"
-            :auto-upload="false"
+            :action="
+              editShop.thumbnail
+                ? '/api/homepageConfig/updateWorkshopPicture'
+                : '/api/homepageConfig/uploadWorkshopPicture'
+            "
             :show-file-list="false"
-            :on-change="onSelectfile"
+            :with-credentials="true"
             :before-upload="beforeAvatarUpload"
+            :on-success="handleAvatarSuccess"
+            :data="
+              editShop.thumbnail
+                ? { workshopId: editShop.id, deleteFileName: editShop.thumbnail }
+                : { workshopId: editShop.id }
+            "
           >
-            <img v-if="imageUrl" :src="imageUrl" class="avatar" />
+            <img v-if="editShop.thumbnail" :src="editShop.thumbnail" class="avatar" />
             <!-- <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon> -->
             <div v-else>
               <el-icon class="avatar-upload-icon" size="24px"><Plus /></el-icon>
@@ -52,10 +63,12 @@
             <el-icon class="refresh-right"><RefreshRight /></el-icon
           ></div>
           <div class="color-select"
-            ><el-color-picker v-model="color" show-alpha size="small" color-format="rgb" /><div
-              class="color-content"
-              >{{ showColor[0] }}&emsp;{{ showColor[1] }}</div
-            ></div
+            ><el-color-picker
+              v-model="editShop.bgColor"
+              show-alpha
+              size="small"
+              color-format="rgb"
+            /><div class="color-content">{{ showColor[0] }}&emsp;{{ showColor[1] }}</div></div
           >
         </div>
         <hr />
@@ -65,7 +78,12 @@
             <el-icon class="refresh-right"><RefreshRight /></el-icon
           ></div>
           <div style="display: flex">
-            <el-select v-model="fontSize" class="fontsize-select" style="width: 50px" size="small">
+            <el-select
+              v-model="editShop.fontSize"
+              class="fontsize-select"
+              style="width: 50px"
+              size="small"
+            >
               <el-option
                 v-for="(item, index) in fontSizeList"
                 :key="index"
@@ -76,7 +94,7 @@
             <div class="color-fontsize-select"
               ><el-color-picker v-model="fontSizeColor" size="small" /><div
                 class="color-fontSize-content"
-                >{{ showFontColor[0] }}&emsp;{{ showFontColor[1] }}</div
+                >{{ editShop.fontColor }}</div
               ></div
             >
           </div>
@@ -97,67 +115,59 @@
 </template>
 <script lang="ts" setup>
   import { computed, ref } from 'vue';
-  import { Close, Plus, RefreshRight } from '@element-plus/icons-vue';
+  import { Plus, RefreshRight } from '@element-plus/icons-vue';
   import { ElMessage } from 'element-plus';
-  import { WorkShopInfoItem } from '@/api/scene/scene';
   import { colorRGB2Hex } from '@/utils';
+  import useMapEditor, { MapWorkShopInfoItem } from '../stores/useMapEditor';
+  import { storeToRefs } from 'pinia';
+  import { cloneDeep } from 'lodash-es';
+  import { useGlobSetting } from '@/hooks/setting';
+  import urlJoin from 'url-join';
 
-  const props = defineProps<{
-    // visible: boolean;
-    shop: WorkShopInfoItem;
-    // workshopTemplateList: WorkshopModuleType[];
-  }>();
+  const mapEditor = useMapEditor();
+  const { addedShops, showShops, activeShop } = storeToRefs(mapEditor);
 
-  const emit = defineEmits(['onClose', 'saveConfig']);
+  const globSetting = useGlobSetting();
 
-  // const emit = defineEmits<{
-  //   // (e: 'update:modelValue'): unknown;
-  //   (e: 'onClose'): unknown;
-  // }>();
+  const editShop = computed(
+    () =>
+      showShops.value.find((item) => item.id === activeShop.value.id) ||
+      ({} as MapWorkShopInfoItem),
+  );
 
-  // const closeDrawer = () => {
-  //   emit('onClose');
-  // };
-  const color = ref('');
-  const showColor = computed(() => colorRGB2Hex(color.value));
-  const dialogTableVisible = ref(false);
-  const imageUrl = ref('');
+  const emit = defineEmits(['onClose', 'saveConfig']);
 
-  const onSelectfile = (uploadFile) => {
-    imageUrl.value = URL.createObjectURL(uploadFile.raw!);
-  };
-  // const handleAvatarSuccess = () => {};
+  const showColor = computed(() => colorRGB2Hex(editShop.value!.bgColor));
+  const dialogTableVisible = ref(false);
 
   const beforeAvatarUpload = (rawFile) => {
     if (rawFile.type !== 'image/jpeg') {
-      ElMessage.error('Avatar picture must be JPG format!');
+      ElMessage.error('请上传jpg格式的图片!');
       return false;
     }
-    // else if (rawFile.size / 1024 / 1024 > 2) {
-    //   ElMessage.error('Avatar picture size can not exceed 2MB!');
-    //   return false;
-    // }
     return true;
   };
 
+  const handleAvatarSuccess = (e) => {
+    editShop.value.thumbnail = urlJoin(globSetting.imgUrl!, e.data);
+  };
+
   const openDialog = () => {
     dialogTableVisible.value = !dialogTableVisible.value;
   };
 
   const closeDialog = () => {
     dialogTableVisible.value = false;
+    showShops.value = cloneDeep(addedShops.value);
     emit('onClose', true);
   };
 
-  const fontSize = ref<number>(14);
   const fontSizeList = Array.from({ length: 11 }, (_, index) => index + 10);
 
   const fontSizeColor = ref('');
-  const showFontColor = computed(() => colorRGB2Hex(fontSizeColor.value));
 
   const saveWorkshopConfig = () => {
-    //这里需要传入数据
-    emit('saveConfig', {});
+    addedShops.value = cloneDeep(showShops.value);
   };
 
   defineExpose({ openDialog });
@@ -170,7 +180,6 @@
     position: absolute;
     right: 0px;
     top: -10px;
-    // height: 660px !important;
   }
 
   .dialog-header {
@@ -186,9 +195,6 @@
   :deep(.el-dialog__headerbtn .el-dialog__close) {
     display: none;
   }
-  // ::v-deep.el-dialog__headerbtn .el-dialog__close {
-  //   display: none;
-  // }
 
   .drawer-box {
     width: 251px;
@@ -206,22 +212,23 @@
     width: 104px;
     height: 104px;
     cursor: pointer;
-    // margin-top: 24px;
     margin-left: 0px;
     position: relative;
     overflow: hidden;
     transition: var(--el-transition-duration-fast);
-    // text-align: center;
   }
 
-  //   .avatar-uploader .el-upload:hover {
-  //     border-color: var(--el-color-primary);
-  //   }
   .uploader-title {
     font-size: 12px;
     margin-bottom: 8px;
   }
 
+  .avatar {
+    width: 104px;
+    height: 104px;
+    object-fit: contain;
+  }
+
   .el-icon.avatar-upload-icon {
     font-size: 18px;
     color: black;
@@ -238,16 +245,12 @@
     font-size: 14px;
     font-weight: 400;
     color: rgba(0, 0, 0, 0.88);
-    // font-weight: 350;
-    // color: #3d3d3d;
-    // opacity: 0.4;
   }
   .upload-config-tip {
     text-align: center;
     font-size: 14px;
     font-weight: 400;
     color: rgba(0, 0, 0, 0.45);
-    // line-height: 22px;
     margin-left: -20px;
     margin-top: 8px;
     margin-bottom: 8px;
@@ -259,13 +262,7 @@
     color: rgba(0, 0, 0, 0.45);
     margin-left: 4px;
     margin-bottom: 16px;
-    // text-align: center;
-    // margin-top: 10px;
   }
-  // .upload-tips {
-  //   font-size: 16px;
-  //   text-align: center;
-  // }
 
   .content-title {
     display: flex;
@@ -288,7 +285,6 @@
     height: 24px;
     line-height: 24px;
     padding-left: 10px;
-    // margin-top: 8px;
     background: rgba(0, 0, 0, 0.04);
   }
 
@@ -300,13 +296,11 @@
     height: 24px;
     line-height: 24px;
     padding-left: 10px;
-    // margin-top: 8px;
     background: rgba(0, 0, 0, 0.04);
   }
 
   .color-fontsize-select {
     display: flex;
-    // margin-left: 34px;
     margin-bottom: 16px;
   }
   .layout-set {

+ 0 - 8
src/views/page-config/component/ConfigFinish.vue

@@ -30,14 +30,6 @@
 
   const emit = defineEmits(['onClose', 'saveConfig']);
 
-  // const emit = defineEmits<{
-  //   // (e: 'update:modelValue'): unknown;
-  //   (e: 'onClose'): unknown;
-  // }>();
-
-  // const closeDrawer = () => {
-  //   emit('onClose');
-  // };
   const closeResult = () => {
     emit('onClose');
   };

+ 0 - 25
src/views/page-config/component/LabelItem.vue

@@ -1,25 +0,0 @@
-<template>
-  <div>
-    <SvgIcon icon-name="map-activation" color="#ef684d" class="test-icon" />
-    <SvgIcon icon-name="posPoint" color="#ef684d" class="test-icon" />
-    <SvgIcon icon-name="posRect" color="#ef684d" class="test-icon1" />
-  </div>
-</template>
-
-<script setup lang="ts">
-  import { ref, reactive } from 'vue';
-  import { SvgIcon } from '@/components/SvgIcon';
-</script>
-
-<style scoped>
-  .label-img {
-    width: 26.762px;
-    /* filter: drop-shadow(80px 0 0 #cd4646);
-    transform: translate(-80px); */
-  }
-
-  .test-icon {
-    width: 26.762px;
-    height: 32px;
-  }
-</style>

+ 71 - 0
src/views/page-config/component/mapContainer/LabelItem.vue

@@ -0,0 +1,71 @@
+<template>
+  <div class="flex">
+    <SvgIcon icon-name="posPoint" :color="props.shop.bgColor" class="test-icon" />
+    <img
+      v-if="showThumbnail && props.shop.thumbnail"
+      class="hover-img"
+      :src="props.shop.thumbnail"
+      :alt="props.shop.name"
+    />
+    <div class="pos-rect">
+      <SvgIcon icon-name="posRect" :color="props.shop.bgColor" class="test-icon1" />
+      <span
+        class="label-name"
+        :style="{ fontSize: `${props.shop.fontSize}px`, color: props.shop.fontColor }"
+        >{{ props.shop.name }}</span
+      >
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { computed } from 'vue';
+  import { SvgIcon } from '@/components/SvgIcon';
+  import { MapWorkShopInfoItem } from '../../stores/useMapEditor';
+
+  const props = defineProps<{ shop: MapWorkShopInfoItem; show: boolean }>();
+
+  const showThumbnail = computed(() => props.show);
+</script>
+
+<style scoped>
+  .label-name {
+    position: absolute;
+    font-family: TRENDS;
+    margin-bottom: 10px;
+  }
+
+  .test-icon {
+    width: 26.762px;
+    height: 32px;
+    margin-right: 10px;
+  }
+
+  .hover-img {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    width: 81px;
+    height: 98px;
+    z-index: 10;
+    transform: translateX(-40%);
+  }
+
+  .pos-rect {
+    position: relative;
+    width: 161px;
+    height: 37px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+
+  .test-icon1 {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    object-fit: fill;
+  }
+</style>

+ 29 - 0
src/views/page-config/component/mapContainer/MapContainer.vue

@@ -0,0 +1,29 @@
+<template>
+  <div
+    :style="{
+      position: 'relative',
+      width: `${mapWidth}px`,
+      height: `${mapHeight}px`,
+      background: `url(${getRealImgUrl()})`,
+    }"
+  >
+    <Transformer v-for="item in showShops" :key="item.id" :shop="item" />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { storeToRefs } from 'pinia';
+  import Transformer from './Transformer.vue';
+  import useMapEditor from '../../stores/useMapEditor';
+  import { useGlobSetting } from '@/hooks/setting';
+  import urlJoin from 'url-join';
+
+  const mapEditor = useMapEditor();
+  const { mapWidth, mapHeight, bgImg, showShops } = storeToRefs(mapEditor);
+
+  const globSetting = useGlobSetting();
+
+  const getRealImgUrl = () => urlJoin(globSetting.imgUrl!, bgImg.value);
+</script>
+
+<style scoped></style>

+ 261 - 0
src/views/page-config/component/mapContainer/Transformer.vue

@@ -0,0 +1,261 @@
+<template>
+  <div
+    class="transformer-box flex"
+    :style="{
+      left: `${transformShop.x - 2}px`,
+      top: `${transformShop.y - 2}px`,
+    }"
+    @click="activeShop = transformShop"
+  >
+    <LabelItem
+      :shop="transformShop"
+      :show="hasHover"
+      :style="{
+        transform: `scale(${transformShop.scale})`,
+        transformOrigin: transformOrigin,
+      }"
+    />
+    <div
+      ref="baffleRef"
+      class="baffle"
+      :class="{ hide: props.shop.id !== activeShop.id }"
+      @mouseover="hasHover = true"
+      @mouseout="hasHover = false"
+    ></div>
+    <div
+      ref="cornerLTRef"
+      class="corner left-top"
+      :class="{ hide: props.shop.id !== activeShop.id }"
+    ></div>
+    <div
+      ref="cornerRTRef"
+      class="corner right-top"
+      :class="{ hide: props.shop.id !== activeShop.id }"
+    ></div>
+    <div
+      ref="cornerLBRef"
+      class="corner left-bottom"
+      :class="{ hide: props.shop.id !== activeShop.id }"
+    ></div>
+    <div
+      ref="cornerRBRef"
+      class="corner right-bottom"
+      :class="{ hide: props.shop.id !== activeShop.id }"
+    ></div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, onMounted, nextTick, computed, onBeforeUnmount, watch } from 'vue';
+  import { MapWorkShopInfoItem } from '../../stores/useMapEditor';
+  import LabelItem from './LabelItem.vue';
+  import useMapEditor from '../../stores/useMapEditor';
+  import { storeToRefs } from 'pinia';
+  import { TransformType } from './type';
+  import { cloneDeep } from 'lodash-es';
+
+  const mapEditor = useMapEditor();
+  const { mapWidth, mapHeight, showShops, addedShops, activeShop } = storeToRefs(mapEditor);
+
+  const props = defineProps<{ shop: MapWorkShopInfoItem }>();
+
+  const transformShop = computed(() => showShops.value.find((item) => item.id === props.shop.id)!);
+  const hasHover = ref(false);
+
+  let transform: TransformType = TransformType.STATIC;
+  let parentEl: HTMLDivElement;
+
+  const baffleRef = ref<HTMLDivElement>();
+  const cornerLTRef = ref<HTMLDivElement>();
+  const cornerRTRef = ref<HTMLDivElement>();
+  const cornerLBRef = ref<HTMLDivElement>();
+  const cornerRBRef = ref<HTMLDivElement>();
+
+  const originPos = {
+    x: 0,
+    y: 0,
+  };
+
+  const originRect = {
+    w: 0,
+    h: 0,
+  };
+
+  const transformOrigin = ref('center');
+
+  const resizeRect = () => {
+    originRect.w = baffleRef.value?.clientWidth || 0;
+    originRect.h = baffleRef.value?.clientHeight || 0;
+  };
+
+  const getNumByBound = (num: number, min: number, max: number): number => {
+    let ret = num;
+    if (num > max) {
+      ret = max;
+    } else if (num < min) {
+      ret = min;
+    }
+    return ret;
+  };
+
+  const handleMouseDown = (e) => {
+    console.log(e);
+
+    console.log(transformShop.value.name);
+    console.log(activeShop.value.name);
+
+    if (transformShop.value.id !== activeShop.value.id) {
+      console.log('0-0-0-0');
+
+      return;
+    }
+
+    if (e.target === baffleRef.value) {
+      console.log('move start');
+
+      transform = TransformType.MOVE;
+      originPos.x = e.clientX;
+      originPos.y = e.clientY;
+    } else if (e.target === cornerLTRef.value) {
+      transform = TransformType.SCALE;
+      transformOrigin.value = 'right bottom';
+    } else if (e.target === cornerRTRef.value) {
+      transform = TransformType.SCALE;
+      transformOrigin.value = 'left bottom';
+    } else if (e.target === cornerLBRef.value) {
+      transform = TransformType.SCALE;
+      transformOrigin.value = 'right top';
+    } else if (e.target === cornerRBRef.value) {
+      transform = TransformType.SCALE;
+      transformOrigin.value = 'left top';
+    } else {
+      activeShop.value = {} as MapWorkShopInfoItem;
+    }
+  };
+
+  const handleMouseMove = (e) => {
+    const dx = e.clientX - originPos.x;
+    const dy = e.clientY - originPos.y;
+    originPos.x = e.clientX;
+    originPos.y = e.clientY;
+    const shop = showShops.value.find((item) => item.id === props.shop.id)!;
+    if (e.target === baffleRef.value && transform === TransformType.MOVE) {
+      shop.x = getNumByBound(shop.x + dx, 0, mapWidth.value - originRect.w);
+      shop.y = getNumByBound(shop.y + dy, 0, mapHeight.value - originRect.h);
+    } else if (transform === TransformType.SCALE) {
+      return;
+      console.log('0-0-00-');
+      console.log(originRect.w, originRect.h);
+      console.log(dx, dy);
+
+      const newW =
+        originRect.w + dx + shop.x > mapWidth.value ? mapWidth.value - shop.x : originRect.w + dx;
+      const newH =
+        originRect.h + dy + shop.y > mapHeight.value ? mapHeight.value - shop.y : originRect.h + dy;
+      shop.scale = shop.scale * Math.min(newW / originRect.w, newH / originRect.h);
+      console.log(newW, newH, shop.scale);
+
+      nextTick(() => {
+        resizeRect();
+      });
+    }
+  };
+
+  const handleMouseUp = () => {
+    transform = TransformType.STATIC;
+    originPos.x = 0;
+    originPos.y = 0;
+    addedShops.value = cloneDeep(showShops.value);
+  };
+
+  watch(
+    activeShop,
+    (val) => {
+      if (val.id === props.shop.id) {
+        bindListener();
+      } else {
+        unbindListener();
+      }
+    },
+    {
+      deep: true,
+    },
+  );
+
+  onMounted(() => {
+    parentEl = document.getElementById('drawContainer') as HTMLDivElement;
+    resizeRect();
+    bindListener();
+  });
+
+  const bindListener = () => {
+    parentEl.addEventListener('mousedown', handleMouseDown);
+    parentEl.addEventListener('mousemove', handleMouseMove);
+    parentEl.addEventListener('mouseup', handleMouseUp);
+  };
+
+  const unbindListener = () => {
+    parentEl.removeEventListener('mousedown', handleMouseDown);
+    parentEl.removeEventListener('mousemove', handleMouseMove);
+    parentEl.removeEventListener('mouseup', handleMouseUp);
+  };
+
+  onBeforeUnmount(() => {
+    unbindListener();
+  });
+</script>
+
+<style scoped lang="scss">
+  .transformer-box {
+    position: absolute;
+    padding: 2px;
+  }
+
+  .baffle {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    border: 1px solid black;
+    top: 0;
+    left: 0;
+    cursor: move;
+    transform: none;
+  }
+
+  .corner {
+    position: absolute;
+    width: 8px;
+    height: 8px;
+    background-color: #ffffff;
+    border: 1px solid black;
+    cursor: crosshair;
+
+    &.left-top {
+      left: 0;
+      top: 0;
+      transform: translate(-50%, -50%);
+    }
+
+    &.right-top {
+      right: 0;
+      top: 0;
+      transform: translate(50%, -50%);
+    }
+
+    &.left-bottom {
+      left: 0;
+      bottom: 0;
+      transform: translate(-50%, 50%);
+    }
+
+    &.right-bottom {
+      right: 0;
+      bottom: 0;
+      transform: translate(50%, 50%);
+    }
+  }
+
+  .hide {
+    opacity: 0;
+  }
+</style>

+ 5 - 0
src/views/page-config/component/mapContainer/type.ts

@@ -0,0 +1,5 @@
+export enum TransformType {
+  STATIC = 'static',
+  MOVE = 'move',
+  SCALE = 'scale',
+}

+ 72 - 7
src/views/page-config/hooks/useMapEditor.ts

@@ -8,7 +8,9 @@ import { useGlobSetting } from '@/hooks/setting';
 import urlJoin from 'url-join';
 import { WorkShopInfoItem } from '@/api/scene/scene';
 import LabelItem from '../component/LabelItem.vue';
+import { SvgIcon } from '@/components/SvgIcon';
 import html2canvas from 'html2canvas';
+import { Canvg } from 'canvg';
 
 interface MapEditorOption {
   onShopStyle?: (shop: WorkShopInfoItem) => void;
@@ -123,16 +125,79 @@ export function useMapEditor(opt: MapEditorOption) {
     const dv = document.createElement('div');
     dv.setAttribute('id', 'labelItem');
     dv.setAttribute('style', `position: absolute; left: 0px; top: 0px;`);
-    const label = h(LabelItem);
+    const label = h(SvgIcon, {
+      iconName: 'posPoint',
+      color: '#ef684d',
+      style: { width: '27px', height: '32px' },
+    });
     render(label, dv);
-    const parentEl = document.getElementById('drawContainer') as HTMLDivElement;
-    parentEl.append(dv);
-    html2canvas(dv, { backgroundColor: 'transparent' }).then((canvas) => {
-      console.log(canvas);
-
-      testImg.image(canvas);
+    const labelSvg = dv.querySelector('svg');
+    const labelSvgStr = new XMLSerializer().serializeToString(labelSvg!);
+    const canvas = document.createElement('canvas') as HTMLCanvasElement;
+    const ctx = canvas.getContext('2d');
+    Canvg.from(ctx!, labelSvgStr).then(() => {
+      console.log('0-0-0-0-0-');
+
+      const ahref = document.createElement('a');
+      ahref.href = canvas.toDataURL('image/png');
+      ahref.download = 'exportPng';
+      ahref.click();
+
+      const parentEl = document.getElementById('drawContainer') as HTMLDivElement;
+      parentEl.append(canvas);
+      const test = new Konva.Image({
+        x: 10,
+        y: 10,
+        width: 27,
+        height: 32,
+        image: canvas,
+        name: 'image',
+      });
+      layer?.add(test);
+      test.moveToTop();
+      layer?.draw();
     });
 
+    //     const test = new konva.Image({
+    //       x: 10,
+    //       y: 10,
+    //       width: 27,
+    //       height: 32,
+    //       image: canvas,
+    //       name: 'image',
+    //     });
+    //     layer.add(test);
+    //     test.moveToTop();
+    //     layer.draw();
+    //   },
+    // });
+
+    // const labelImg = new Image();
+    // const svgUrl = new Blob([labelSvgStr], { type: 'image/svg+xml;charset=utf-8' });
+    // const DOMURL = self.URL || self.webkitURL || self;
+    // const labelUrl = DOMURL.createObjectURL(svgUrl);
+    // console.log(labelSvgStr);
+
+    // labelImg.onload = () => {
+    //   const labelPoint = new Konva.Image({
+    //     width: 27,
+    //     height: 32,
+    //     image: labelImg,
+    //     name: 'image',
+    //   });
+    //   layer?.add(labelPoint);
+    //   layer?.batchDraw();
+    // };
+    // labelImg.src = labelUrl;
+
+    // parentEl.append(dv);
+    // html2canvas(dv, { backgroundColor: 'transparent' }).then((canvas) => {
+    //   console.log(canvas);
+
+    //   testImg.image(canvas);
+    //   layer?.batchDraw();
+    // });
+
     const shopName = new Konva.Text({
       text: shop.name,
       fontSize: 14,

+ 111 - 0
src/views/page-config/stores/useMapEditor.ts

@@ -0,0 +1,111 @@
+import { ref } from 'vue';
+import { defineStore } from 'pinia';
+import { WorkShopInfoItem } from '@/api/scene/scene';
+import { cloneDeep } from 'lodash-es';
+import { useGlobSetting } from '@/hooks/setting';
+import urlJoin from 'url-join';
+
+export enum LabelPositionEnum {
+  LEFT = 'left',
+  RIGHT = 'right',
+  TOP = 'top',
+}
+
+export type EditStyle = {
+  x: number;
+  y: number;
+  scale: number;
+  thumbnail?: string;
+  bgColor: string;
+  fontSize: number;
+  fontColor: string;
+  posType: LabelPositionEnum;
+};
+
+export type MapWorkShopInfoItem = WorkShopInfoItem & EditStyle;
+
+export const useMapEditor = defineStore('home-map-ediotr', () => {
+  const globSetting = useGlobSetting();
+
+  const bgImg = ref('');
+  const mapWidth = ref(0);
+  const mapHeight = ref(0);
+  const addedShops = ref<MapWorkShopInfoItem[]>([]);
+  const showShops = ref<MapWorkShopInfoItem[]>([]);
+  const activeShop = ref<MapWorkShopInfoItem>({} as MapWorkShopInfoItem);
+
+  const addBg = () => {
+    const imgUrl = urlJoin(globSetting.imgUrl!, bgImg.value);
+    console.log(imgUrl);
+
+    const img = new Image();
+    img.onload = () => {
+      mapWidth.value = img.width;
+      mapHeight.value = img.height;
+    };
+    img.src = imgUrl;
+  };
+
+  const addShop = (shop: MapWorkShopInfoItem) => {
+    activeShop.value = shop;
+    addedShops.value.push(cloneDeep(shop));
+    showShops.value.push(cloneDeep(shop));
+  };
+
+  const toJson = () => {
+    const layout = {
+      bgInfo: {
+        width: mapWidth.value,
+        height: mapHeight.value,
+        img: bgImg.value,
+      },
+      shopList: addedShops.value.map((shop) => {
+        const temp = cloneDeep(shop) as any;
+        delete temp.children;
+        return temp;
+      }),
+    };
+
+    return JSON.stringify(layout);
+  };
+
+  const createMap = (layout) => {
+    bgImg.value = layout.bgInfo.img;
+    mapWidth.value = layout.bgInfo.width;
+    mapHeight.value = layout.bgInfo.height;
+    addedShops.value = cloneDeep(layout.shopList);
+    showShops.value = cloneDeep(layout.shopList);
+  };
+
+  const deleteShop = () => {
+    addedShops.value = addedShops.value.filter((item) => item.id !== activeShop.value.id);
+    showShops.value = showShops.value.filter((item) => item.id !== activeShop.value.id);
+    activeShop.value = {} as MapWorkShopInfoItem;
+  };
+
+  const resetMap = () => {
+    bgImg.value = '';
+    mapWidth.value = 0;
+    mapHeight.value = 0;
+    addedShops.value = [];
+    showShops.value = [];
+    activeShop.value = {} as MapWorkShopInfoItem;
+  };
+
+  return {
+    bgImg,
+    mapWidth,
+    mapHeight,
+    addedShops,
+    showShops,
+    activeShop,
+    addShop,
+    addBg,
+    toJson,
+    createMap,
+    deleteShop,
+    resetMap,
+  };
+});
+
+export default useMapEditor;

+ 135 - 124
src/views/page-config/usePageConfig.ts

@@ -1,6 +1,5 @@
-import { computed, ref, toRefs } from 'vue';
+import { computed, onMounted, ref, toRefs } from 'vue';
 import useSceneInfos from '@/hooks/useSceneInfos';
-import { defineStore } from 'pinia';
 
 export const usePageConfig = () => {
   const sceneInfos = useSceneInfos();
@@ -8,133 +7,145 @@ export const usePageConfig = () => {
   const { getScenesTree } = sceneInfos;
 
   const selectedCompany = ref<number | undefined>();
-  const label = ref('');
-  const shopList = ref([
-    {
-      id: 1,
-      companyId: 2,
-      sceneLabelId: 1,
-      name: 'ARJ21部装车间',
-      code: 'C12',
-      remark: '',
-      status: 0,
-      createdAt: '2023-10-13 09:48:58',
-      updatedAt: '2024-01-16 15:00:46',
-      isDeleted: 0,
-      serial: 0,
-      labelName: '生产安全',
-      workshopModule: {
-        id: 2,
-        name: '厂房',
-        code: '2',
-        remark: '',
-        status: 0,
-        createdAt: '2023-12-22 11:44:06',
-        updatedAt: '2023-12-22 11:44:06',
-        isDeleted: 0,
-      },
-      children: [
-        {
-          id: 2,
-          workshopId: 1,
-          name: '西侧200室内气密试验区',
-          code: 'C12-W200test',
-          remark: '',
-          principal: '',
-          status: 0,
-          createdAt: '2023-12-27 14:07:15',
-          updatedAt: '2024-01-05 09:05:52',
-          isDeleted: 0,
-          serial: 0,
-        },
-        {
-          id: 1,
-          workshopId: 1,
-          name: '东侧200室内气密试验区',
-          code: 'C12-E200test',
-          remark: '',
-          principal: '',
-          status: 0,
-          createdAt: '2023-12-27 14:06:46',
-          updatedAt: '2024-01-05 09:05:52',
-          isDeleted: 0,
-          serial: 1,
-        },
-      ],
-    },
-    {
-      id: 3,
-      companyId: 2,
-      sceneLabelId: 1,
-      name: 'C919部装车间',
-      code: 'C02',
-      remark: '',
-      status: 0,
-      createdAt: '2023-10-13 09:50:31',
-      updatedAt: '2024-01-16 15:00:52',
-      isDeleted: 0,
-      serial: 0,
-      labelName: '生产安全',
-      workshopModule: {
-        id: 2,
-        name: '厂房',
-        code: '2',
-        remark: '',
-        status: 0,
-        createdAt: '2023-12-22 11:44:06',
-        updatedAt: '2023-12-22 11:44:06',
-        isDeleted: 0,
-      },
-      children: [],
-    },
-    {
-      id: 6,
-      companyId: 2,
-      sceneLabelId: 1,
-      name: '胶接车间',
-      code: 'B16',
-      remark: '',
-      status: 0,
-      createdAt: '2023-10-13 09:51:42',
-      updatedAt: '2024-01-16 15:00:57',
-      isDeleted: 0,
-      serial: 0,
-      labelName: '生产安全',
-      workshopModule: {
-        id: 2,
-        name: '厂房',
-        code: '2',
-        remark: '',
-        status: 0,
-        createdAt: '2023-12-22 11:44:06',
-        updatedAt: '2023-12-22 11:44:06',
-        isDeleted: 0,
-      },
-      children: [],
-    },
-    {
-      id: 7,
-      companyId: 2,
-      sceneLabelId: 1,
-      name: '复材车间',
-      code: 'B01',
-      remark: '',
-      status: 0,
-      createdAt: '2023-10-13 09:52:02',
-      updatedAt: '2024-01-16 15:00:59',
-      isDeleted: 0,
-      serial: 0,
-      labelName: '生产安全',
-      workshopModule: null,
-      children: [],
-    },
-  ]);
+  const label = ref<number | undefined>();
+  const layoutId = ref<number | undefined>();
+
+  // const shopList = ref([
+  //   {
+  //     id: 1,
+  //     companyId: 2,
+  //     sceneLabelId: 1,
+  //     name: 'ARJ21部装车间',
+  //     code: 'C12',
+  //     remark: '',
+  //     status: 0,
+  //     createdAt: '2023-10-13 09:48:58',
+  //     updatedAt: '2024-01-16 15:00:46',
+  //     isDeleted: 0,
+  //     serial: 0,
+  //     labelName: '生产安全',
+  //     workshopModule: {
+  //       id: 2,
+  //       name: '厂房',
+  //       code: '2',
+  //       remark: '',
+  //       status: 0,
+  //       createdAt: '2023-12-22 11:44:06',
+  //       updatedAt: '2023-12-22 11:44:06',
+  //       isDeleted: 0,
+  //     },
+  //     children: [
+  //       {
+  //         id: 2,
+  //         workshopId: 1,
+  //         name: '西侧200室内气密试验区',
+  //         code: 'C12-W200test',
+  //         remark: '',
+  //         principal: '',
+  //         status: 0,
+  //         createdAt: '2023-12-27 14:07:15',
+  //         updatedAt: '2024-01-05 09:05:52',
+  //         isDeleted: 0,
+  //         serial: 0,
+  //       },
+  //       {
+  //         id: 1,
+  //         workshopId: 1,
+  //         name: '东侧200室内气密试验区',
+  //         code: 'C12-E200test',
+  //         remark: '',
+  //         principal: '',
+  //         status: 0,
+  //         createdAt: '2023-12-27 14:06:46',
+  //         updatedAt: '2024-01-05 09:05:52',
+  //         isDeleted: 0,
+  //         serial: 1,
+  //       },
+  //     ],
+  //   },
+  //   {
+  //     id: 3,
+  //     companyId: 2,
+  //     sceneLabelId: 1,
+  //     name: 'C919部装车间',
+  //     code: 'C02',
+  //     remark: '',
+  //     status: 0,
+  //     createdAt: '2023-10-13 09:50:31',
+  //     updatedAt: '2024-01-16 15:00:52',
+  //     isDeleted: 0,
+  //     serial: 0,
+  //     labelName: '生产安全',
+  //     workshopModule: {
+  //       id: 2,
+  //       name: '厂房',
+  //       code: '2',
+  //       remark: '',
+  //       status: 0,
+  //       createdAt: '2023-12-22 11:44:06',
+  //       updatedAt: '2023-12-22 11:44:06',
+  //       isDeleted: 0,
+  //     },
+  //     children: [],
+  //   },
+  //   {
+  //     id: 6,
+  //     companyId: 2,
+  //     sceneLabelId: 1,
+  //     name: '胶接车间',
+  //     code: 'B16',
+  //     remark: '',
+  //     status: 0,
+  //     createdAt: '2023-10-13 09:51:42',
+  //     updatedAt: '2024-01-16 15:00:57',
+  //     isDeleted: 0,
+  //     serial: 0,
+  //     labelName: '生产安全',
+  //     workshopModule: {
+  //       id: 2,
+  //       name: '厂房',
+  //       code: '2',
+  //       remark: '',
+  //       status: 0,
+  //       createdAt: '2023-12-22 11:44:06',
+  //       updatedAt: '2023-12-22 11:44:06',
+  //       isDeleted: 0,
+  //     },
+  //     children: [],
+  //   },
+  //   {
+  //     id: 7,
+  //     companyId: 2,
+  //     sceneLabelId: 1,
+  //     name: '复材车间',
+  //     code: 'B01',
+  //     remark: '',
+  //     status: 0,
+  //     createdAt: '2023-10-13 09:52:02',
+  //     updatedAt: '2024-01-16 15:00:59',
+  //     isDeleted: 0,
+  //     serial: 0,
+  //     labelName: '生产安全',
+  //     workshopModule: null,
+  //     children: [],
+  //   },
+  // ]);
 
   const labelList = computed(
     () => scenesInfos.value.find((item) => item.id === selectedCompany.value)?.labelList,
   );
+  const shopList = computed(
+    () =>
+      scenesInfos.value
+        .find((item) => item.id === selectedCompany.value)
+        ?.children.filter((shop) => shop.sceneLabelId === label.value) || [],
+  );
+
+  onMounted(() => {
+    getScenesTree({ level: 2, valueKey: 'code', labelKey: 'name', disabled: true });
+  });
 
-  return { selectedCompany, scenesInfos, label, labelList, shopList };
+  return { selectedCompany, scenesInfos, label, labelList, shopList, layoutId };
 };
 
 export default usePageConfig;

+ 7 - 1
src/views/system-config/scene-manage/SceneManage.vue

@@ -102,6 +102,9 @@
   } from '@/api/scene/sceneOperate';
   import useScene from './use-scene';
   import useSceneTemplete from './use-sence-templete';
+  import { useRouter } from 'vue-router';
+
+  const router = useRouter();
 
   const useSceneList = useScene();
   const { tableData, getSceneDetail } = useSceneList;
@@ -205,7 +208,10 @@
 
   //页面设置函数
   const handleConfig = (row) => {
-    console.log(row);
+    router.push({
+      path: '/page-config/config',
+      query: { companyId: row.id },
+    });
   };
 
   const handleAdd = (row) => {