Quellcode durchsuchen

log: add home config

sunhongyao341504 vor 2 Jahren
Ursprung
Commit
cba082f647

+ 1 - 1
.env.development

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

+ 1 - 1
mock/login/routers.ts

@@ -118,7 +118,7 @@ const list = [
       {
         path: 'config',
         name: 'layout-config',
-        component: '/page-config/ConfigEidt',
+        component: '/page-config/ConfigEdit',
         redirect: null,
         meta: {
           icon: '',

+ 3 - 1
package.json

@@ -48,6 +48,7 @@
     "element-plus": "2.3.6",
     "element-resize-detector": "1.2.4",
     "fabric": "5.3.0",
+    "html2canvas": "1.4.1",
     "konva": "9.3.0",
     "lodash-es": "4.17.21",
     "mockjs": "1.1.0",
@@ -116,6 +117,7 @@
     "vite-plugin-html": "3.2.0",
     "vite-plugin-mock": "2.9.6",
     "vite-plugin-style-import": "2.0.0",
+    "vite-plugin-svg-icons": "2.0.1",
     "vite-plugin-vue-setup-extend": "0.4.0",
     "vue-eslint-parser": "9.0.3",
     "vue-tsc": "0.35.2"
@@ -149,4 +151,4 @@
       ]
     }
   }
-}
+}

Datei-Diff unterdrückt, da er zu groß ist
+ 949 - 2
pnpm-lock.yaml


+ 23 - 0
src/assets/icons/map-activation.svg

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="17px" height="17px" viewBox="0 0 17 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>切片</title>
+    <g id="地图" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="地图-list-日期选择、搜索完毕、气泡提示" transform="translate(-1367.000000, -565.000000)">
+            <g id="编组-10" transform="translate(218.000000, 239.000000)">
+                <g id="编组-3" transform="translate(-0.000000, 0.000000)">
+                    <g id="编组-24" transform="translate(919.000000, 0.000000)">
+                        <g id="数据展示/Table表格/浅色/列备份-39" transform="translate(72.036820, 310.000000)">
+                            <g id="编组-15" transform="translate(26.000000, 16.000000)">
+                                <g id="删除-icon备份-6" transform="translate(132.000000, -0.000000)">
+                                    <ellipse id="椭圆形" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" cx="11" cy="11.0367954" rx="4" ry="4.01338015"></ellipse>
+                                    <line x1="7.43533751" y1="7.5697343" x2="1.8179871" y2="1.97122367" id="路径-3" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></line>
+                                    <line x1="4.62666231" y1="4.15613005" x2="6.7532782" y2="1.97122367" id="路径-4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></line>
+                                </g>
+                            </g>
+                        </g>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 19 - 0
src/assets/icons/posPoint.svg

@@ -0,0 +1,19 @@
+<svg width="26.762" 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%" />
+        </linearGradient>
+        <!-- <linearGradient x1="50%" y1="0%" x2="50%" y2="98.062%" id="prefix__b">
+            <stop stop-color="#D3FFFA" offset="0%" />
+            <stop stop-color="#C1EDFF" offset="51.021%" />
+            <stop stop-color="#90E0FF" offset="100%" />
+        </linearGradient> -->
+    </defs>
+    <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)" />
+        <ellipse fill="#FFFFFF" fill-rule="nonzero" cx="13.634" cy="13.619" rx="4.545" ry="4.54" />
+    </g>
+</svg>

Datei-Diff unterdrückt, da er zu groß ist
+ 24 - 0
src/assets/icons/posRect.svg


+ 34 - 0
src/assets/icons/workboard-layout.svg

@@ -0,0 +1,34 @@
+<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+  <defs>
+    <filter x="-146.2%" y="-123.8%" width="392.5%" height="392.5%" filterUnits="objectBoundingBox" id="prefix__a">
+      <feMorphology radius="4" operator="dilate" in="SourceAlpha" result="shadowSpreadOuter1" />
+      <feOffset dy="9" in="shadowSpreadOuter1" result="shadowOffsetOuter1" />
+      <feGaussianBlur stdDeviation="14" in="shadowOffsetOuter1" result="shadowBlurOuter1" />
+      <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowBlurOuter1"
+        result="shadowMatrixOuter1" />
+      <feOffset dy="6" in="SourceAlpha" result="shadowOffsetOuter2" />
+      <feGaussianBlur stdDeviation="8" in="shadowOffsetOuter2" result="shadowBlurOuter2" />
+      <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0" in="shadowBlurOuter2"
+        result="shadowMatrixOuter2" />
+      <feMorphology radius="2" in="SourceAlpha" result="shadowSpreadOuter3" />
+      <feOffset dy="3" in="shadowSpreadOuter3" result="shadowOffsetOuter3" />
+      <feGaussianBlur stdDeviation="3" in="shadowOffsetOuter3" result="shadowBlurOuter3" />
+      <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" in="shadowBlurOuter3"
+        result="shadowMatrixOuter3" />
+      <feMerge>
+        <feMergeNode in="shadowMatrixOuter1" />
+        <feMergeNode in="shadowMatrixOuter2" />
+        <feMergeNode in="shadowMatrixOuter3" />
+      </feMerge>
+    </filter>
+    <circle id="prefix__b" cx="20" cy="20" r="20" />
+  </defs>
+  <g fill="none" fill-rule="evenodd">
+    <g>
+      <use fill="#FFF" xlink:href="#prefix__b" />
+    </g>
+    <path
+      d="M25.35 12h-10.7A2.652 2.652 0 0012 14.65v10.7A2.652 2.652 0 0014.65 28h10.7A2.652 2.652 0 0028 25.35v-10.7A2.652 2.652 0 0025.35 12zm-10.7 1.067h10.7c.873 0 1.583.71 1.583 1.583v1.582H13.067V14.65c0-.873.71-1.583 1.583-1.583zM13.067 25.35v-8.053h4.266v9.635H14.65a1.582 1.582 0 01-1.583-1.582h0zm12.283 1.583H18.4v-9.636h8.533v8.051c0 .875-.71 1.585-1.583 1.585z"
+      fill-rule="nonzero" stroke="#000" stroke-width=".2" />
+  </g>
+</svg>

+ 59 - 0
src/components/SvgIcon/SvgIcon.vue

@@ -0,0 +1,59 @@
+<template>
+  <svg class="svg-icon" aria-hidden="true" :style="{ color: color }">
+    <use :xlink:href="svgName" />
+  </svg>
+</template>
+
+<script>
+  import { defineComponent, computed } from 'vue';
+
+  // const importAll = (requireContext) => requireContext.keys().forEach(requireContext);
+  // try {
+  //     importAll(import.meta.glob('@/assets/icons/*.svg'))
+  // } catch (error) {
+  //     console.log(error)
+  // }
+
+  export default defineComponent({
+    name: 'SvgIcon',
+    props: {
+      iconName: {
+        type: String,
+        required: true,
+      },
+      color: {
+        type: String,
+        required: false,
+      },
+    },
+    setup(props) {
+      const svgName = computed(() => `#icon-${props.iconName}`);
+      return {
+        svgName,
+      };
+    },
+  });
+</script>
+
+<style lang="scss" scoped>
+  .svg-icon {
+    position: relative;
+    width: 1em;
+    height: 1em;
+    overflow: hidden;
+    vertical-align: -0.15em;
+    fill: currentColor;
+
+    &::after {
+      position: absolute;
+      top: 0;
+      right: 0;
+      bottom: 0;
+      left: 0;
+      z-index: 1;
+      pointer-events: none;
+      cursor: pointer;
+      content: '';
+    }
+  }
+</style>

+ 1 - 0
src/components/SvgIcon/index.ts

@@ -0,0 +1 @@
+export { default as SvgIcon } from './SvgIcon.vue';

+ 1 - 0
src/main.ts

@@ -9,6 +9,7 @@ import { createApp } from 'vue';
 import App from './App.vue';
 import router, { setupRouter } from './router';
 import { setupStore } from '@/store';
+import 'virtual:svg-icons-register';
 import { setupElement, setupDirectives, setupCustomComponents } from '@/plugins';
 
 async function bootstrap() {

+ 2 - 0
src/plugins/customComponents.ts

@@ -1,4 +1,5 @@
 import { PageWrapper, PageFooter } from '@/components/Page';
+import { SvgIcon } from '@/components/SvgIcon';
 
 /**
  * 全局注册自定义组件 待完善
@@ -7,4 +8,5 @@ import { PageWrapper, PageFooter } from '@/components/Page';
 export function setupCustomComponents(app) {
   app.component('PageWrapper', PageWrapper);
   app.component('PageFooter', PageFooter);
+  app.component('SvgIcon', SvgIcon);
 }

+ 2 - 0
src/router/router-icons.ts

@@ -22,6 +22,7 @@ import {
   DesktopOutline,
   DocumentTextOutline,
   DiamondOutline,
+  FileTrayFullOutline,
 } from '@vicons/ionicons5';
 
 //前端路由图标映射表
@@ -46,4 +47,5 @@ export const constantRouterIcon = {
   PictureOutlined: renderIcon(PictureOutlined),
   CameraOutlined: renderIcon(CameraOutlined),
   ApartmentOutlined: renderIcon(ApartmentOutlined),
+  FileTrayFullOutline: renderIcon(FileTrayFullOutline),
 };

+ 19 - 0
src/utils/index.ts

@@ -272,3 +272,22 @@ export const downloadFile = (res, filename?) => {
     window.URL.revokeObjectURL(objectUrl); // 释放掉blob对象。
   }
 };
+
+/**
+ * 将rgb或rgba颜色转化为[hex,透明度?]
+ */
+export const colorRGB2Hex = (color: string): string[] => {
+  console.log(color);
+
+  if (!color) return ['#ffffff', '100%'];
+  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]);
+  const b = Number(array![3]);
+  const hex = '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
+
+  return [hex, Number(array![4]) * 100 + '%'];
+};

+ 2 - 1
src/views/map-config/mini-map/MiniMapConfig.vue

@@ -20,7 +20,7 @@
           <el-button @click="mapEditor.toJson">tojson</el-button>
           <el-upload
             class="avatar-uploader flex justify-center items-center"
-            action="/temp/api/layout/uploadPicture"
+            action="/layout/uploadPicture"
             :show-file-list="false"
             :on-success="handleAvatarSuccess"
             :with-credentials="true"
@@ -271,6 +271,7 @@
   }
   .isAdded {
     color: #1890ff;
+    cursor: not-allowed;
   }
   .isActive {
     background-color: #e6f7ff;

+ 460 - 0
src/views/page-config/ConfigEdit.vue

@@ -0,0 +1,460 @@
+<template>
+  <div class="page" @click="handleWholeClick">
+    <div class="page-head flex items-center">
+      <el-icon size="20"><ArrowLeft /></el-icon>
+      <div class="head-opt flex-1 flex justify-between items-center">
+        <div>
+          <span>场景:</span>
+          <el-select
+            v-model="selectedCompany"
+            class="m-2"
+            placeholder="请选择相关公司"
+            style="width: 216px"
+            @change="changeCompany"
+          >
+            <el-option
+              v-for="item in scenesInfos"
+              :key="item.id"
+              :label="item.name"
+              :value="item.id"
+            />
+          </el-select>
+        </div>
+        <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-button
+                v-for="item in labelList"
+                :key="item.id"
+                :label="item.id!"
+                class="label-select"
+                >{{ item.name }}</el-radio-button
+              >
+            </el-radio-group></div
+          >
+        </div>
+        <div class="flex">
+          <el-button @click="mapEditor.toJson">tojson</el-button>
+          <el-upload
+            class="avatar-uploader flex justify-center items-center"
+            action="/api/homepageConfig/updateCompanyPicture"
+            :show-file-list="false"
+            :on-success="handleAvatarSuccess"
+            :with-credentials="true"
+            name="file"
+          >
+            <el-button style="font-size: 12px" :icon="Refresh" :disabled="!hasBg">
+              替换照片
+            </el-button>
+          </el-upload>
+
+          <el-button
+            @click="handleSave"
+            style="margin-left: 40px"
+            type="primary"
+            :disabled="!selectedCompany && !label"
+            >保存为布局
+          </el-button>
+        </div>
+      </div>
+    </div>
+    <div class="paint-tool flex">
+      <div class="camera-list">
+        <div>
+          <span class="label-text flex">车间列表:</span>
+          <ElInput
+            class="search-put"
+            style="margin: 10px 0; width: 230px"
+            placeholder="请输入搜索内容"
+            v-model="searchKey"
+            :suffix-icon="Search"
+          />
+        </div>
+        <span v-if="filterShopList.length == 0" class="ml-1" style="color: #3f3f3f">
+          提示:请先选择相应公司和图片
+        </span>
+        <el-scrollbar v-else style="position: relative; height: calc(100% - 90px)">
+          <div
+            v-for="item in filterShopList"
+            :key="item.code"
+            class="camera-item flex justify-start items-center"
+            :class="{
+              isAdded: isAddedCamera(item.id),
+              isActive: item.code === activeShopId,
+            }"
+            @click="handleAddCamera(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>
+        <el-upload
+          v-if="!hasBg"
+          class="upload-icon flex justify-center items-center"
+          action="/api/homepageConfig/uploadCompanyPicture"
+          :show-file-list="false"
+          :before-upload="handleBeforeUpload"
+          :on-success="handleAvatarSuccess"
+          :with-credentials="true"
+          name="file"
+        >
+          <img src="~@/assets/images/img-upload.png" />
+        </el-upload>
+      </div>
+    </div>
+    <el-tooltip
+      class="box-item position-tooltip"
+      effect="dark"
+      content="显示侧边栏"
+      :offset="12"
+      placement="left"
+      @click="showEditConfig"
+    >
+      <div
+        v-if="leftShow"
+        class="circle-rectangle"
+        :class="{ 'shape-shadow': shadow }"
+        @mouseover="shadowAdd"
+        @mouseout="shadowRemove"
+        @click="dialogReopen"
+      >
+        <el-icon class="left-icon" size="16px"><ArrowLeftBold /></el-icon>
+        <el-icon style="margin-top: 7px" size="16px"><ArrowLeftBold /></el-icon>
+        <!-- <el-icon class="left-icon"><DArrowLeft /></el-icon> -->
+      </div>
+      <!-- <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"
+    />
+    <ConfigFinish
+      :visible="visibleResult"
+      :status="configStatus"
+      @on-close="closeResult"
+      class="feedback-position"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import ConfigDialog from './component/ConfigDrawer.vue';
+  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 { 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';
+
+  const mapEditor = useMapEditor({ onShopStyle });
+  const { addedShops, bgImgUrl, activeShopId } = mapEditor;
+
+  const pageConfig = usePageConfig();
+  const { selectedCompany, scenesInfos, label, labelList, shopList } = pageConfig;
+
+  const drawContainer = ref<HTMLDivElement>();
+
+  const searchKey = ref('');
+  // 是否已有背景图
+  const hasBg = ref(false);
+
+  const handleBeforeUpload = () => {
+    if (!selectedShopCode.value) {
+      ElMessage.error({
+        message: '请先选择车间',
+      });
+      return false;
+    }
+  };
+
+  /** 判断相机是否已经添加 */
+  const isAddedCamera = (shopId: number) => {
+    const index = addedShops.value.findIndex((item) => item === shopId);
+    return index >= 0;
+  };
+
+  /** -------- */
+  const showEditConfig = () => {};
+
+  //左边的浮动按钮
+  const leftShow = ref(true);
+  const onClose = (val) => {
+    leftShow.value = val;
+  };
+
+  const shadow = ref(false);
+  const shadowAdd = () => {
+    shadow.value = true;
+  };
+  const shadowRemove = () => {
+    shadow.value = false;
+  };
+
+  const configDrawer = ref();
+  const dialogReopen = () => {
+    configDrawer.value.openDialog();
+  };
+
+  //需要从子组件中获得当前保存的车间数据
+  const saveConfig = (val) => {
+    console.log(val);
+  };
+
+  const configShop = ref<WorkShopInfoItem>({} as WorkShopInfoItem);
+
+  //总体的保存,将整个数据传过去
+  const visibleResult = ref(false);
+  const configStatus = ref(true);
+  const closeResult = () => {
+    visibleResult.value = false;
+  };
+  /** ------------- */
+
+  const handleAvatarSuccess = (e) => {
+    bgImgUrl.value = e.data;
+    mapEditor.addBg();
+    hasBg.value = true;
+  };
+
+  const changeShop = (code: string) => {
+    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);
+    });
+  };
+
+  const handleWholeClick = (e) => {
+    if (e.button === 0) {
+      mapEditor.destoryOptBlock();
+    }
+  };
+
+  const changeCompany = () => {};
+
+  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 filterShopList = computed(() => {
+    const k = searchKey.value.trim();
+    if (!k) return shopList.value;
+    return shopList.value.filter((x) => x.name?.includes(k));
+  });
+
+  const handleAddCamera = (shop: WorkShopInfoItem) => {
+    if (!hasBg.value) {
+      ElMessage.warning({
+        message: '请先添加背景图片',
+      });
+      return;
+    }
+    mapEditor.addShop(shop);
+  };
+
+  const handleSave = () => {
+    const layout = mapEditor.toJson();
+    updateMinMapViewLayoutApi({ layout, targetId: String(selectedShopDetail.value?.id) }).then(
+      (res) => {
+        console.log('updateMinMapViewLayoutApi', res);
+        ElMessage.success('保存成功');
+      },
+    );
+  };
+
+  function onShopStyle(shop: WorkShopInfoItem) {
+    configShop.value = shop;
+    dialogReopen();
+  }
+</script>
+
+<style scoped lang="scss">
+  .page {
+  }
+  .page-head {
+    height: 54px;
+    padding-left: 15px;
+    background-color: #ffffff;
+  }
+  .head-opt {
+    margin-left: 20px;
+    padding-right: 15px;
+    font-size: 14px;
+    color: #3f3f3f;
+  }
+
+  .avatar-uploader {
+    /* width: 120px; */
+    /* height: 30px; */
+    /* border: 1px solid #eee; */
+    border-radius: 4px;
+    margin-left: 30px;
+  }
+  .upload-icon {
+    position: absolute;
+    top: 0;
+    right: 0;
+    left: 0;
+    bottom: 0;
+    margin: auto;
+  }
+
+  .paint-tool {
+    position: relative;
+    height: calc(100vh - 138px);
+    margin-top: 2px;
+  }
+
+  .camera-list {
+    width: 250px;
+    padding: 0 10px;
+    background-color: #ffffff;
+  }
+  .label-text {
+    font-size: 14px;
+    font-weight: 600;
+    margin: 10px 0 5px 10px;
+  }
+  .camera-item {
+    height: 32px;
+    font-size: 14px;
+    padding-left: 8px;
+    font-weight: 400;
+    color: #404040;
+    line-height: 14px;
+    cursor: pointer;
+
+    &:hover {
+      background-color: #e6f7ff;
+      color: #1890ff;
+    }
+  }
+  .isAdded {
+    color: #1890ff;
+    cursor: not-allowed;
+  }
+  .isActive {
+    background-color: #e6f7ff;
+    color: #1890ff;
+  }
+  .camera-item-disabled {
+    color: #c6c6c6;
+  }
+  .camera-id {
+    width: 110px;
+  }
+  .space-name {
+    width: 120px;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+
+  .draw-container {
+    position: relative;
+    width: calc(100% - 300px);
+    margin: 20px;
+    overflow: hidden;
+  }
+
+  .drawer-position {
+    position: absolute;
+    right: 0px;
+    // left: 1150px;
+    top: 74px;
+    z-index: 99;
+    opacity: 1;
+    background: #ffffff;
+  }
+
+  .dialog-btn {
+    position: absolute;
+    right: 0px;
+    top: 66px;
+  }
+
+  .position-tooltip {
+    margin-right: -10px;
+  }
+
+  .circle-rectangle {
+    width: 40px;
+    height: 30px;
+    border-radius: 50% 0% 0% 50%;
+    position: absolute;
+    right: 0px;
+    top: 66px;
+    background-color: white;
+    display: flex;
+  }
+
+  .shape-shadow {
+    filter: drop-shadow(5px 5px 10px rgb(102, 100, 100));
+  }
+  .left-icon {
+    margin-top: 7px;
+    margin-left: 8px;
+    margin-right: -7px;
+  }
+
+  .feedback-position {
+    position: absolute;
+    left: 460px;
+    top: 150px;
+    z-index: 9999;
+  }
+
+  :deep(.search-put .el-input__wrapper) {
+    background-color: #f0f2f5;
+  }
+  :deep(.el-popper__arrow) {
+    display: none;
+  }
+  :deep(.el-tree-node__content:hover) {
+    background: #e6f4ff;
+  }
+  :deep(.el-button--primary) {
+    --el-button-disabled-bg-color: #bfbfbf;
+  }
+  :deep(.el-popover) {
+    width: unset !important;
+    min-width: 110px;
+    text-align: center;
+    font-weight: 400;
+  }
+</style>
+./MapBase/useCameraMap ./MapBase/CameraMapBak ./usePageConfig1

+ 26 - 8
src/views/page-config/PageConfig.vue

@@ -1,14 +1,32 @@
 <template>
-  <el-card :bordered="false" class="proCard" style="position: relative">
-    <PageMain />
-    <!-- <ConfigEidt :pageShow="pageShow" /> -->
-  </el-card>
+  <div class="flex flex-col page-config-content">
+    <el-card shadow="never">
+      <div style="display: flex">
+        <el-input v-model="searchCom" class="search-btn w-50" placeholder="搜索公司主页">
+          <template #suffix>
+            <el-icon @click="searchPageConfig" @keyup.enter="searchPageConfig"><Search /></el-icon>
+          </template>
+        </el-input>
+      </div>
+    </el-card>
+    <el-card shadow="never" class="flex-1" style="margin-top: 8px">
+      <PageMain />
+    </el-card>
+  </div>
 </template>
 <script lang="ts" setup>
-  // import { ref } from 'vue';
+  import { ref } from 'vue';
+  import { Search } from '@element-plus/icons-vue';
   import PageMain from './component/PageMain.vue';
-  // import ConfigEidt from './component/ConfigEidt.vue';
-  // const pageShow = ref<boolean>(true);
+
+  const searchCom = ref('');
+
+  const searchPageConfig = () => {};
 </script>
 
-<style lang="scss" sctep></style>
+<style lang="scss" sctep>
+  .page-config-content {
+    width: 100%;
+    height: calc(100vh - 64px - 12px);
+  }
+</style>

+ 12 - 7
src/views/page-config/component/ConfigDrawer.vue

@@ -10,7 +10,7 @@
     >
       <template #header>
         <div style="position: relative">
-          <div class="dialog-header">{{ props.title }}</div>
+          <div class="dialog-header">{{ props.shop.name }}</div>
           <img
             src="~@/assets/icons/slide-right.png"
             alt=""
@@ -52,9 +52,10 @@
             <el-icon class="refresh-right"><RefreshRight /></el-icon
           ></div>
           <div class="color-select"
-            ><el-color-picker v-model="color" size="small" /><div class="color-content">{{
-              color
-            }}</div></div
+            ><el-color-picker v-model="color" show-alpha size="small" color-format="rgb" /><div
+              class="color-content"
+              >{{ showColor[0] }}&emsp;{{ showColor[1] }}</div
+            ></div
           >
         </div>
         <hr />
@@ -75,7 +76,7 @@
             <div class="color-fontsize-select"
               ><el-color-picker v-model="fontSizeColor" size="small" /><div
                 class="color-fontSize-content"
-                >{{ fontSizeColor }}</div
+                >{{ showFontColor[0] }}&emsp;{{ showFontColor[1] }}</div
               ></div
             >
           </div>
@@ -95,13 +96,15 @@
   </div>
 </template>
 <script lang="ts" setup>
-  import { ref } from 'vue';
+  import { computed, ref } from 'vue';
   import { Close, Plus, RefreshRight } from '@element-plus/icons-vue';
   import { ElMessage } from 'element-plus';
+  import { WorkShopInfoItem } from '@/api/scene/scene';
+  import { colorRGB2Hex } from '@/utils';
 
   const props = defineProps<{
     // visible: boolean;
-    title: string;
+    shop: WorkShopInfoItem;
     // workshopTemplateList: WorkshopModuleType[];
   }>();
 
@@ -116,6 +119,7 @@
   //   emit('onClose');
   // };
   const color = ref('');
+  const showColor = computed(() => colorRGB2Hex(color.value));
   const dialogTableVisible = ref(false);
   const imageUrl = ref('');
 
@@ -149,6 +153,7 @@
   const fontSizeList = Array.from({ length: 11 }, (_, index) => index + 10);
 
   const fontSizeColor = ref('');
+  const showFontColor = computed(() => colorRGB2Hex(fontSizeColor.value));
 
   const saveWorkshopConfig = () => {
     //这里需要传入数据

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

@@ -0,0 +1,25 @@
+<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>

+ 10 - 20
src/views/page-config/component/PageMain.vue

@@ -1,21 +1,14 @@
 <template>
-  <div
-    ><div style="display: flex">
-      <el-input
-        v-model="searchCom"
-        class="search-btn w-50 m-2"
-        placeholder="搜索公司主页"
-        :suffix-icon="Search"
-      />
-    </div>
+  <div>
     <div class="add-page-box">
       <div class="add-config">
-        <div class="add-box" @click="handleAddPage"
-          ><div
-            ><el-icon class="add-icon" size="24px"><Plus /></el-icon></div
-          ><div class="add-content">新建主页</div></div
-        ></div
-      >
+        <div class="add-box" @click="handleAddPage">
+          <div>
+            <el-icon class="add-icon" size="24px"><Plus /></el-icon>
+          </div>
+          <div class="add-content">新建主页</div>
+        </div>
+      </div>
 
       <div
         v-for="item in picList"
@@ -46,12 +39,11 @@
 </template>
 <script lang="ts" setup>
   import { ref } from 'vue';
-  import { Search, Plus, Delete } from '@element-plus/icons-vue';
   import { picList } from '../constant';
   import { useRouter } from 'vue-router';
+  import { Plus } from '@element-plus/icons-vue';
   const router = useRouter();
 
-  const searchCom = ref('');
   // const pageAdd = ref<boolean>(true);
   const handleAddPage = () => {
     router.push('/page-config/config');
@@ -83,11 +75,9 @@
   }
 
   .add-page-box {
-    padding-top: 2px;
-    padding-left: 8px;
     display: flex;
-    height: 770px;
     flex-wrap: wrap;
+    margin-top: 10px;
     margin-bottom: 24px;
     align-content: flex-start;
   }

+ 295 - 0
src/views/page-config/hooks/useMapEditor.ts

@@ -0,0 +1,295 @@
+import { computed, h, onBeforeUnmount, onMounted, ref, render } from 'vue';
+import Konva from 'konva';
+import cameraImg from '@/assets/camera/cameraImg.png';
+import OptBar from '../components/CameraOptBar.vue';
+import DefaultTip from '../components/DefaultTip.vue';
+import { ElMessage } from 'element-plus';
+import { useGlobSetting } from '@/hooks/setting';
+import urlJoin from 'url-join';
+import { WorkShopInfoItem } from '@/api/scene/scene';
+import LabelItem from '../component/LabelItem.vue';
+import html2canvas from 'html2canvas';
+
+interface MapEditorOption {
+  onShopStyle?: (shop: WorkShopInfoItem) => void;
+}
+
+export function useMapEditor(opt: MapEditorOption) {
+  let initWidth; // 默认宽度
+  let initHeight; // 默认高度
+  let stage: Konva.Stage | null = null;
+  let layer: Konva.Layer | null = null;
+  const addedShops = ref<number[]>([]); // 已添加车间列表
+  const activeGroup = ref<Konva.Group | null>(null); // transformer激活的车间
+  let optBlock: HTMLDivElement | null = null; // 鼠标右击弹出的选项组
+  let isTransform = false; // 是否在变换中
+  const activeShopId = computed(() => activeGroup.value?.id()); // 当前选中车间ID
+  const bgImgUrl = ref<string>('');
+
+  const globSetting = useGlobSetting();
+
+  /** 容器初始化 */
+  const initContainer = (opt: Konva.StageConfig) => {
+    initWidth = opt.width || 0;
+    initHeight = opt.height || 0;
+    stage = new Konva.Stage(opt);
+    stage.on('click tap', handleStageClick);
+    layer = new Konva.Layer();
+    stage.add(layer);
+  };
+
+  /** 更换背景图时根据图片大小重置容器宽高 */
+  const resizeContainer = (width, height) => {
+    const newWidth = width > initWidth ? width : initWidth;
+    const newHeight = height > initHeight ? height : initHeight;
+    stage?.width(newWidth);
+    stage?.height(newHeight);
+  };
+
+  /** 添加背景 */
+  const addBg = () => {
+    // const imgUrl = urlJoin(globSetting.imgUrl!, bgImgUrl.value);
+    const imgUrl = bgImgUrl.value;
+    const bgNode = layer?.find('#bgImg')[0] as Konva.Image;
+    const bgImg = new Image();
+    bgImg.onload = () => {
+      // 判断是否已有背景
+      if (!bgNode) {
+        const mapBg = new Konva.Image({
+          x: 0,
+          y: 0,
+          image: bgImg,
+          width: bgImg.width,
+          height: bgImg.height,
+          id: 'bgImg',
+        });
+        layer?.add(mapBg);
+        mapBg.moveToBottom();
+      } else {
+        bgNode.width(bgImg.width);
+        bgNode.height(bgImg.height);
+        bgNode.image(bgImg);
+      }
+      resizeContainer(bgImg.width, bgImg.height);
+      layer?.batchDraw();
+    };
+    bgImg.src = imgUrl;
+  };
+
+  /** 变更需要激活transform的车间 */
+  const attachTransformer = (group: Konva.Group): Konva.Transformer => {
+    activeGroup.value = group;
+    stage!.find('Transformer')[0]?.destroy(); // 清除现有transformer
+    const id = group.id();
+    const tr = new Konva.Transformer({
+      keepRatio: true,
+      rotateAnchorOffset: 30,
+      rotateEnabled: false,
+      enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
+      id: 'tr_' + id,
+    });
+    tr.nodes([group]);
+    layer?.add(tr);
+    layer?.draw();
+
+    group.on('dragstart', handleDragStart);
+    group.on('dragstart', handleDragEnd);
+
+    return tr;
+  };
+
+  /** 添加车间 */
+  const addShop = (shop: WorkShopInfoItem) => {
+    const group = new Konva.Group({
+      x: 100,
+      y: 100,
+      id: shop.id + '',
+      draggable: true,
+      name: 'group',
+    });
+    // Konva.Image.fromURL(point, (imageNode) => {
+    //   group.add(imageNode),
+    //     imageNode.setAttrs({
+    //       fill: 'red',
+    //     });
+    // });
+
+    const testImg = new Konva.Image({
+      width: 27,
+      height: 32,
+      image: undefined,
+    });
+    group?.add(testImg);
+    const dv = document.createElement('div');
+    dv.setAttribute('id', 'labelItem');
+    dv.setAttribute('style', `position: absolute; left: 0px; top: 0px;`);
+    const label = h(LabelItem);
+    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 shopName = new Konva.Text({
+      text: shop.name,
+      fontSize: 14,
+      fontFamily: 'TRENDS',
+      fill: '#FFFFFF',
+    });
+    group.add(shopName);
+    const w = shopName.width();
+    const th = shopName.height();
+    const markCir = new Konva.Circle({
+      x: th / 2 + 3,
+      y: th / 2 + 3,
+      radius: th / 2 + 3,
+      fill: 'blue',
+    });
+    group.add(markCir);
+    shopName.moveUp();
+    const markRect = new Konva.Rect({
+      x: th + 20,
+      y: 0,
+      width: w + 10,
+      height: th + 6,
+      fill: 'blue',
+    });
+    group.add(markRect);
+    shopName.x(th + 25);
+    shopName.y(5);
+    shopName.moveUp();
+
+    testImg.moveToTop();
+
+    layer?.add(group);
+    attachTransformer(group); // 添加的车间默认激活transformer
+
+    opt.onShopStyle && opt.onShopStyle(shop);
+    addedShops.value.push(shop.id);
+  };
+
+  /** 创建右键选项组 */
+  const createOptBlock = (node: Konva.Group, x: number, y: number) => {};
+
+  /** 删除右键选项组 */
+  const destoryOptBlock = () => {
+    optBlock?.remove();
+    optBlock = null;
+  };
+
+  /** 删除车间 */
+  const deleteShop = () => {
+    const index = addedShops.value.findIndex((item) => item === activeGroup.value?.id());
+    index >= 0 && addedShops.value.splice(index, 1);
+    activeGroup.value?.destroy();
+    stage!.find('Transformer')[0]?.destroy();
+    layer?.draw();
+  };
+
+  /** 鼠标悬浮事件 */
+  const handleMouseOver = (e) => {
+    // 禁用浏览器默认鼠标事件
+    document.oncontextmenu = () => {
+      return false;
+    };
+  };
+
+  /** 鼠标离开事件 */
+  const handleMouseLeave = () => {
+    // 恢复浏览器默认事件
+    document.oncontextmenu = () => {
+      return true;
+    };
+  };
+
+  /** 开始拖拽事件 */
+  const handleDragStart = () => {
+    isTransform = true;
+    destoryOptBlock();
+  };
+
+  /** 结束拖拽事件 */
+  const handleDragEnd = () => {
+    isTransform = false;
+  };
+
+  /** 全局点击事件 */
+  const handleStageClick = (e) => {
+    // 点击舞台取消现有激活的transformer
+    if (e.target === stage) {
+      stage!.find('Transformer')[0].destroy();
+      layer!.draw();
+      return;
+    }
+
+    // 判断点击对象是否为车间
+    if (!e.target.hasName('image')) {
+      return;
+    }
+    const parent = e.target.parent;
+    if (!parent.hasName('group')) {
+      return;
+    }
+    const group = e.target.parent;
+    attachTransformer(group);
+    // 判断是否为右键点击
+    if (e.evt.button === 2) {
+      createOptBlock(group, e.evt.offsetX + 20, e.evt.offsetY);
+    }
+  };
+
+  /** 键盘点击事件 */
+  const handleKeyDown = (e) => {
+    // 删除键
+    if (e.keyCode === 46 || e.code === 'Delete') {
+      deleteShop();
+    }
+  };
+
+  // 基础监听事件绑定
+  const bindBaseEvt = (node: Konva.Node) => {
+    // node.on('transform', handleDragStart);
+    // node.on('transformend', handleDragEnd);
+    node.on('mouseover', handleMouseOver);
+    node.on('mouseleave', handleMouseLeave);
+  };
+
+  /** 输出布局json */
+  const toJson = () => {
+    const json = stage!.toJSON();
+    const layout = {
+      bgImg: bgImgUrl.value,
+      shopLis: [],
+    };
+
+    return JSON.stringify(layout);
+  };
+
+  /** 导入布局json */
+  const createMap = (json) => {};
+
+  onMounted(() => {
+    window.addEventListener('keydown', handleKeyDown);
+  });
+
+  onBeforeUnmount(() => {
+    window.removeEventListener('keydown', handleKeyDown);
+  });
+
+  return {
+    activeShopId,
+    addedShops,
+    bgImgUrl,
+    initContainer,
+    addBg,
+    addShop,
+    destoryOptBlock,
+    toJson,
+    createMap,
+  };
+}
+
+export default useMapEditor;

+ 272 - 0
src/views/page-config/hooks/useMapEditor1.ts

@@ -0,0 +1,272 @@
+import { computed, h, onBeforeUnmount, onMounted, ref, render } from 'vue';
+import Konva from 'konva';
+import cameraImg from '@/assets/camera/cameraImg.png';
+import OptBar from '../components/CameraOptBar.vue';
+import DefaultTip from '../components/DefaultTip.vue';
+import { ElMessage } from 'element-plus';
+import { useGlobSetting } from '@/hooks/setting';
+import urlJoin from 'url-join';
+import { WorkShopInfoItem } from '@/api/scene/scene';
+import point from '@/assets/icons/posPoint.svg';
+
+interface MapEditorOption {
+  onShopStyle?: (shop: WorkShopInfoItem) => void;
+}
+
+export function useMapEditor(opt: MapEditorOption) {
+  let initWidth; // 默认宽度
+  let initHeight; // 默认高度
+  let stage: Konva.Stage | null = null;
+  let layer: Konva.Layer | null = null;
+  const addedShops = ref<number[]>([]); // 已添加车间列表
+  const activeGroup = ref<Konva.Group | null>(null); // transformer激活的车间
+  let optBlock: HTMLDivElement | null = null; // 鼠标右击弹出的选项组
+  let isTransform = false; // 是否在变换中
+  const activeShopId = computed(() => activeGroup.value?.id()); // 当前选中车间ID
+  const bgImgUrl = ref<string>('');
+
+  const globSetting = useGlobSetting();
+
+  /** 容器初始化 */
+  const initContainer = (opt: Konva.StageConfig) => {
+    initWidth = opt.width || 0;
+    initHeight = opt.height || 0;
+    stage = new Konva.Stage(opt);
+    stage.on('click tap', handleStageClick);
+    layer = new Konva.Layer();
+    stage.add(layer);
+  };
+
+  /** 更换背景图时根据图片大小重置容器宽高 */
+  const resizeContainer = (width, height) => {
+    const newWidth = width > initWidth ? width : initWidth;
+    const newHeight = height > initHeight ? height : initHeight;
+    stage?.width(newWidth);
+    stage?.height(newHeight);
+  };
+
+  /** 添加背景 */
+  const addBg = () => {
+    // const imgUrl = urlJoin(globSetting.imgUrl!, bgImgUrl.value);
+    const imgUrl = bgImgUrl.value;
+    const bgNode = layer?.find('#bgImg')[0] as Konva.Image;
+    const bgImg = new Image();
+    bgImg.onload = () => {
+      // 判断是否已有背景
+      if (!bgNode) {
+        const mapBg = new Konva.Image({
+          x: 0,
+          y: 0,
+          image: bgImg,
+          width: bgImg.width,
+          height: bgImg.height,
+          id: 'bgImg',
+        });
+        layer?.add(mapBg);
+        mapBg.moveToBottom();
+      } else {
+        bgNode.width(bgImg.width);
+        bgNode.height(bgImg.height);
+        bgNode.image(bgImg);
+      }
+      resizeContainer(bgImg.width, bgImg.height);
+      layer?.batchDraw();
+    };
+    bgImg.src = imgUrl;
+  };
+
+  /** 变更需要激活transform的车间 */
+  const attachTransformer = (group: Konva.Group): Konva.Transformer => {
+    activeGroup.value = group;
+    stage!.find('Transformer')[0]?.destroy(); // 清除现有transformer
+    const id = group.id();
+    const tr = new Konva.Transformer({
+      keepRatio: true,
+      rotateAnchorOffset: 30,
+      rotateEnabled: false,
+      enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
+      id: 'tr_' + id,
+    });
+    tr.nodes([group]);
+    layer?.add(tr);
+    layer?.draw();
+
+    group.on('dragstart', handleDragStart);
+    group.on('dragstart', handleDragEnd);
+
+    return tr;
+  };
+
+  /** 添加车间 */
+  const addShop = (shop: WorkShopInfoItem) => {
+    const group = new Konva.Group({
+      x: 100,
+      y: 100,
+      id: shop.id + '',
+      draggable: true,
+      name: 'group',
+    });
+    // Konva.Image.fromURL(point, (imageNode) => {
+    //   group.add(imageNode),
+    //     imageNode.setAttrs({
+    //       fill: 'red',
+    //     });
+    // });
+
+    const shopName = new Konva.Text({
+      text: shop.name,
+      fontSize: 14,
+      fontFamily: 'TRENDS',
+      fill: '#FFFFFF',
+    });
+    group.add(shopName);
+    const w = shopName.width();
+    const h = shopName.height();
+    const markCir = new Konva.Circle({
+      x: h / 2 + 3,
+      y: h / 2 + 3,
+      radius: h / 2 + 3,
+      fill: 'blue',
+    });
+    group.add(markCir);
+    shopName.moveUp();
+    const markRect = new Konva.Rect({
+      x: h + 20,
+      y: 0,
+      width: w + 10,
+      height: h + 6,
+      fill: 'blue',
+    });
+    group.add(markRect);
+    shopName.x(h + 25);
+    shopName.y(5);
+    shopName.moveUp();
+    layer?.add(group);
+    attachTransformer(group); // 添加的车间默认激活transformer
+
+    opt.onShopStyle && opt.onShopStyle(shop);
+    addedShops.value.push(shop.id);
+  };
+
+  /** 创建右键选项组 */
+  const createOptBlock = (node: Konva.Group, x: number, y: number) => {};
+
+  /** 删除右键选项组 */
+  const destoryOptBlock = () => {
+    optBlock?.remove();
+    optBlock = null;
+  };
+
+  /** 删除车间 */
+  const deleteShop = () => {
+    const index = addedShops.value.findIndex((item) => item === activeGroup.value?.id());
+    index >= 0 && addedShops.value.splice(index, 1);
+    activeGroup.value?.destroy();
+    stage!.find('Transformer')[0]?.destroy();
+    layer?.draw();
+  };
+
+  /** 鼠标悬浮事件 */
+  const handleMouseOver = (e) => {
+    // 禁用浏览器默认鼠标事件
+    document.oncontextmenu = () => {
+      return false;
+    };
+  };
+
+  /** 鼠标离开事件 */
+  const handleMouseLeave = () => {
+    // 恢复浏览器默认事件
+    document.oncontextmenu = () => {
+      return true;
+    };
+  };
+
+  /** 开始拖拽事件 */
+  const handleDragStart = () => {
+    isTransform = true;
+    destoryOptBlock();
+  };
+
+  /** 结束拖拽事件 */
+  const handleDragEnd = () => {
+    isTransform = false;
+  };
+
+  /** 全局点击事件 */
+  const handleStageClick = (e) => {
+    // 点击舞台取消现有激活的transformer
+    if (e.target === stage) {
+      stage!.find('Transformer')[0].destroy();
+      layer!.draw();
+      return;
+    }
+
+    // 判断点击对象是否为车间
+    if (!e.target.hasName('image')) {
+      return;
+    }
+    const parent = e.target.parent;
+    if (!parent.hasName('group')) {
+      return;
+    }
+    const group = e.target.parent;
+    attachTransformer(group);
+    // 判断是否为右键点击
+    if (e.evt.button === 2) {
+      createOptBlock(group, e.evt.offsetX + 20, e.evt.offsetY);
+    }
+  };
+
+  /** 键盘点击事件 */
+  const handleKeyDown = (e) => {
+    // 删除键
+    if (e.keyCode === 46 || e.code === 'Delete') {
+      deleteShop();
+    }
+  };
+
+  // 基础监听事件绑定
+  const bindBaseEvt = (node: Konva.Node) => {
+    // node.on('transform', handleDragStart);
+    // node.on('transformend', handleDragEnd);
+    node.on('mouseover', handleMouseOver);
+    node.on('mouseleave', handleMouseLeave);
+  };
+
+  /** 输出布局json */
+  const toJson = () => {
+    const json = stage!.toJSON();
+    const layout = {
+      bgImg: bgImgUrl.value,
+      shopLis: [],
+    };
+
+    return JSON.stringify(layout);
+  };
+
+  /** 导入布局json */
+  const createMap = (json) => {};
+
+  onMounted(() => {
+    window.addEventListener('keydown', handleKeyDown);
+  });
+
+  onBeforeUnmount(() => {
+    window.removeEventListener('keydown', handleKeyDown);
+  });
+
+  return {
+    activeShopId,
+    addedShops,
+    bgImgUrl,
+    initContainer,
+    addBg,
+    addShop,
+    destoryOptBlock,
+    toJson,
+    createMap,
+  };
+}
+
+export default useMapEditor;

+ 534 - 0
src/views/page-config/shops.json

@@ -0,0 +1,534 @@
+[
+  {
+    "id": 16,
+    "companyId": 6,
+    "sceneLabelId": 2,
+    "name": "硝盐槽",
+    "code": "nitrate tank",
+    "remark": "",
+    "status": 0,
+    "createdAt": "2023-12-11 12:04:17",
+    "updatedAt": "2023-12-11 14:00:40",
+    "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": 17,
+    "companyId": 6,
+    "sceneLabelId": 2,
+    "name": "化学品库房",
+    "code": "cw",
+    "remark": "",
+    "status": 0,
+    "createdAt": "2023-12-11 12:04:42",
+    "updatedAt": "2023-12-11 12:04:42",
+    "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": 18,
+    "companyId": 3,
+    "sceneLabelId": 2,
+    "name": "燃油实验室",
+    "code": "sfy-FL",
+    "remark": "",
+    "status": 0,
+    "createdAt": "2023-12-11 13:41:37",
+    "updatedAt": "2023-12-11 13:41:37",
+    "isDeleted": 0,
+    "serial": 0,
+    "labelName": "安全管控",
+    "workshopModule": {
+      "id": 1,
+      "name": "危险点",
+      "code": "1",
+      "remark": "",
+      "status": 0,
+      "createdAt": "2023-12-22 11:43:54",
+      "updatedAt": "2023-12-22 11:43:54",
+      "isDeleted": 0
+    },
+    "children": []
+  },
+  {
+    "id": 19,
+    "companyId": 5,
+    "sceneLabelId": 2,
+    "name": "配电站",
+    "code": "kf-substation",
+    "remark": "",
+    "status": 0,
+    "createdAt": "2023-12-26 11:09:22",
+    "updatedAt": "2023-12-26 11:09:22",
+    "isDeleted": 0,
+    "serial": 0,
+    "labelName": "安全管控",
+    "workshopModule": {
+      "id": 1,
+      "name": "危险点",
+      "code": "1",
+      "remark": "",
+      "status": 0,
+      "createdAt": "2023-12-22 11:43:54",
+      "updatedAt": "2023-12-22 11:43:54",
+      "isDeleted": 0
+    },
+    "children": []
+  },
+  {
+    "id": 20,
+    "companyId": 5,
+    "sceneLabelId": 2,
+    "name": "气瓶间",
+    "code": "kf-gcr",
+    "remark": "",
+    "status": 0,
+    "createdAt": "2023-12-26 11:09:59",
+    "updatedAt": "2023-12-26 11:09:59",
+    "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": 21,
+    "companyId": 5,
+    "sceneLabelId": 2,
+    "name": "柴油发电机",
+    "code": "kf-dg",
+    "remark": "",
+    "status": 0,
+    "createdAt": "2023-12-26 11:10:19",
+    "updatedAt": "2023-12-26 11:10:19",
+    "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": 22,
+    "companyId": 5,
+    "sceneLabelId": 2,
+    "name": "运行支持指挥中心",
+    "code": "kf-qrh",
+    "remark": "",
+    "status": 0,
+    "createdAt": "2023-12-26 11:10:40",
+    "updatedAt": "2024-01-16 15:01:28",
+    "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": 23,
+    "companyId": 5,
+    "sceneLabelId": 2,
+    "name": "C919飞行模拟机",
+    "code": "kf-sl",
+    "remark": "",
+    "status": 0,
+    "createdAt": "2023-12-26 11:11:00",
+    "updatedAt": "2024-01-17 08:51:47",
+    "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": 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": []
+  },
+  {
+    "id": 8,
+    "companyId": 2,
+    "sceneLabelId": 1,
+    "name": "工装中心",
+    "code": "B04",
+    "remark": "",
+    "status": 0,
+    "createdAt": "2023-10-13 09:53:01",
+    "updatedAt": "2023-10-13 09:53:01",
+    "isDeleted": 0,
+    "serial": 0,
+    "labelName": "生产安全",
+    "workshopModule": null,
+    "children": []
+  },
+  {
+    "id": 9,
+    "companyId": 2,
+    "sceneLabelId": 2,
+    "name": "B16热压罐",
+    "code": "B16autoclave",
+    "remark": "",
+    "status": 0,
+    "createdAt": "2023-10-13 10:22:20",
+    "updatedAt": "2023-10-19 19:12:27",
+    "isDeleted": 0,
+    "serial": 0,
+    "labelName": "安全管控",
+    "workshopModule": null,
+    "children": []
+  },
+  {
+    "id": 10,
+    "companyId": 2,
+    "sceneLabelId": 2,
+    "name": "B01热压罐",
+    "code": "B01autoclave",
+    "remark": "",
+    "status": 0,
+    "createdAt": "2023-10-13 10:22:39",
+    "updatedAt": "2023-10-13 10:22:39",
+    "isDeleted": 0,
+    "serial": 0,
+    "labelName": "安全管控",
+    "workshopModule": null,
+    "children": []
+  },
+  {
+    "id": 11,
+    "companyId": 2,
+    "sceneLabelId": 2,
+    "name": "110KV变电站",
+    "code": "110KVsubstation",
+    "remark": "",
+    "status": 0,
+    "createdAt": "2023-10-13 10:23:02",
+    "updatedAt": "2023-10-13 10:23:02",
+    "isDeleted": 0,
+    "serial": 0,
+    "labelName": "安全管控",
+    "workshopModule": null,
+    "children": []
+  },
+  {
+    "id": 12,
+    "companyId": 2,
+    "sceneLabelId": 2,
+    "name": "2011锅炉房",
+    "code": "2011boiler",
+    "remark": "",
+    "status": 0,
+    "createdAt": "2023-10-13 10:23:20",
+    "updatedAt": "2023-10-13 10:23:20",
+    "isDeleted": 0,
+    "serial": 0,
+    "labelName": "安全管控",
+    "workshopModule": {
+      "id": 1,
+      "name": "危险点",
+      "code": "1",
+      "remark": "",
+      "status": 0,
+      "createdAt": "2023-12-22 11:43:54",
+      "updatedAt": "2023-12-22 11:43:54",
+      "isDeleted": 0
+    },
+    "children": []
+  },
+  {
+    "id": 13,
+    "companyId": 2,
+    "sceneLabelId": 2,
+    "name": "2001a锅炉房",
+    "code": "2001aboiler",
+    "remark": "",
+    "status": 0,
+    "createdAt": "2023-10-13 10:23:36",
+    "updatedAt": "2023-10-13 10:23:36",
+    "isDeleted": 0,
+    "serial": 0,
+    "labelName": "安全管控",
+    "workshopModule": {
+      "id": 1,
+      "name": "危险点",
+      "code": "1",
+      "remark": "",
+      "status": 0,
+      "createdAt": "2023-12-22 11:43:54",
+      "updatedAt": "2023-12-22 11:43:54",
+      "isDeleted": 0
+    },
+    "children": []
+  },
+  {
+    "id": 15,
+    "companyId": 2,
+    "sceneLabelId": 2,
+    "name": "增材实验室",
+    "code": "sf-additive_lab",
+    "remark": "",
+    "status": 0,
+    "createdAt": "2023-12-11 13:40:33",
+    "updatedAt": "2023-12-11 13:40:33",
+    "isDeleted": 0,
+    "serial": 0,
+    "labelName": "安全管控",
+    "workshopModule": {
+      "id": 1,
+      "name": "危险点",
+      "code": "1",
+      "remark": "",
+      "status": 0,
+      "createdAt": "2023-12-22 11:43:54",
+      "updatedAt": "2023-12-22 11:43:54",
+      "isDeleted": 0
+    },
+    "children": []
+  },
+  {
+    "id": 2,
+    "companyId": 2,
+    "sceneLabelId": 1,
+    "name": "ARJ21总装车间",
+    "code": "C11",
+    "remark": "",
+    "status": 0,
+    "createdAt": "2023-10-13 09:49:31",
+    "updatedAt": "2024-01-16 15:00:48",
+    "isDeleted": 0,
+    "serial": 1,
+    "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": 4,
+    "companyId": 2,
+    "sceneLabelId": 1,
+    "name": "C919总装车间",
+    "code": "C01",
+    "remark": "",
+    "status": 0,
+    "createdAt": "2023-10-13 09:50:53",
+    "updatedAt": "2024-01-16 15:00:54",
+    "isDeleted": 0,
+    "serial": 1,
+    "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": 5,
+    "companyId": 2,
+    "sceneLabelId": 1,
+    "name": "维修交付中心",
+    "code": "repair",
+    "remark": "",
+    "status": 0,
+    "createdAt": "2023-10-13 09:51:22",
+    "updatedAt": "2024-01-16 15:00:55",
+    "isDeleted": 0,
+    "serial": 2,
+    "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": []
+  }
+]

+ 692 - 0
src/views/page-config/tree.json

@@ -0,0 +1,692 @@
+[
+  {
+    "id": 1,
+    "parentId": 0,
+    "name": "商飞公司",
+    "code": "COMAC",
+    "remark": "",
+    "status": 0,
+    "createdAt": "2023-10-10 19:53:58",
+    "updatedAt": "2024-01-02 15:25:01",
+    "serial": 0,
+    "isDeleted": 0,
+    "children": [],
+    "labelList": [
+      {
+        "id": 2,
+        "name": "安全管控",
+        "code": "2",
+        "remark": "",
+        "status": 0,
+        "createdAt": "2023-12-27 13:49:28",
+        "updatedAt": "2024-01-16 15:00:28",
+        "isDeleted": 0
+      }
+    ],
+    "moduleList": [
+      {
+        "id": 1,
+        "name": "生产安全模板",
+        "code": "12",
+        "remark": "32437857",
+        "status": 0,
+        "createdAt": "2023-12-22 11:43:05",
+        "updatedAt": "2024-01-04 14:37:40",
+        "isDeleted": 0
+      }
+    ]
+  },
+  {
+    "id": 6,
+    "parentId": 1,
+    "name": "上飞公司大场基地",
+    "code": "shangfeidachang",
+    "remark": "",
+    "status": 0,
+    "createdAt": "2023-12-11 09:07:53",
+    "updatedAt": "2023-12-11 09:07:53",
+    "serial": 0,
+    "isDeleted": 0,
+    "children": [
+      {
+        "id": 16,
+        "companyId": 6,
+        "sceneLabelId": 2,
+        "name": "硝盐槽",
+        "code": "nitrate tank",
+        "remark": "",
+        "status": 0,
+        "createdAt": "2023-12-11 12:04:17",
+        "updatedAt": "2023-12-11 14:00:40",
+        "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": 17,
+        "companyId": 6,
+        "sceneLabelId": 2,
+        "name": "化学品库房",
+        "code": "cw",
+        "remark": "",
+        "status": 0,
+        "createdAt": "2023-12-11 12:04:42",
+        "updatedAt": "2023-12-11 12:04:42",
+        "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": []
+      }
+    ],
+    "labelList": [],
+    "moduleList": []
+  },
+  {
+    "id": 3,
+    "parentId": 1,
+    "name": "上飞院",
+    "code": "shangfeiyuan",
+    "remark": "",
+    "status": 0,
+    "createdAt": "2023-10-13 09:42:50",
+    "updatedAt": "2024-01-02 15:25:01",
+    "serial": 1,
+    "isDeleted": 0,
+    "children": [
+      {
+        "id": 18,
+        "companyId": 3,
+        "sceneLabelId": 2,
+        "name": "燃油实验室",
+        "code": "sfy-FL",
+        "remark": "",
+        "status": 0,
+        "createdAt": "2023-12-11 13:41:37",
+        "updatedAt": "2023-12-11 13:41:37",
+        "isDeleted": 0,
+        "serial": 0,
+        "labelName": "安全管控",
+        "workshopModule": {
+          "id": 1,
+          "name": "危险点",
+          "code": "1",
+          "remark": "",
+          "status": 0,
+          "createdAt": "2023-12-22 11:43:54",
+          "updatedAt": "2023-12-22 11:43:54",
+          "isDeleted": 0
+        },
+        "children": []
+      }
+    ],
+    "labelList": [],
+    "moduleList": []
+  },
+  {
+    "id": 4,
+    "parentId": 1,
+    "name": "北研中心",
+    "code": "beiyan",
+    "remark": "",
+    "status": 0,
+    "createdAt": "2023-10-13 09:43:09",
+    "updatedAt": "2024-01-02 15:25:01",
+    "serial": 2,
+    "isDeleted": 0,
+    "children": [],
+    "labelList": [],
+    "moduleList": []
+  },
+  {
+    "id": 5,
+    "parentId": 1,
+    "name": "客服中心",
+    "code": "kefu",
+    "remark": "",
+    "status": 1,
+    "createdAt": "2023-10-17 16:56:33",
+    "updatedAt": "2024-01-02 19:22:12",
+    "serial": 3,
+    "isDeleted": 0,
+    "children": [
+      {
+        "id": 19,
+        "companyId": 5,
+        "sceneLabelId": 2,
+        "name": "配电站",
+        "code": "kf-substation",
+        "remark": "",
+        "status": 0,
+        "createdAt": "2023-12-26 11:09:22",
+        "updatedAt": "2023-12-26 11:09:22",
+        "isDeleted": 0,
+        "serial": 0,
+        "labelName": "安全管控",
+        "workshopModule": {
+          "id": 1,
+          "name": "危险点",
+          "code": "1",
+          "remark": "",
+          "status": 0,
+          "createdAt": "2023-12-22 11:43:54",
+          "updatedAt": "2023-12-22 11:43:54",
+          "isDeleted": 0
+        },
+        "children": []
+      },
+      {
+        "id": 20,
+        "companyId": 5,
+        "sceneLabelId": 2,
+        "name": "气瓶间",
+        "code": "kf-gcr",
+        "remark": "",
+        "status": 0,
+        "createdAt": "2023-12-26 11:09:59",
+        "updatedAt": "2023-12-26 11:09:59",
+        "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": 21,
+        "companyId": 5,
+        "sceneLabelId": 2,
+        "name": "柴油发电机",
+        "code": "kf-dg",
+        "remark": "",
+        "status": 0,
+        "createdAt": "2023-12-26 11:10:19",
+        "updatedAt": "2023-12-26 11:10:19",
+        "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": 22,
+        "companyId": 5,
+        "sceneLabelId": 2,
+        "name": "运行支持指挥中心",
+        "code": "kf-qrh",
+        "remark": "",
+        "status": 0,
+        "createdAt": "2023-12-26 11:10:40",
+        "updatedAt": "2024-01-16 15:01:28",
+        "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": 23,
+        "companyId": 5,
+        "sceneLabelId": 2,
+        "name": "C919飞行模拟机",
+        "code": "kf-sl",
+        "remark": "",
+        "status": 0,
+        "createdAt": "2023-12-26 11:11:00",
+        "updatedAt": "2024-01-17 08:51:47",
+        "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": []
+      }
+    ],
+    "labelList": [],
+    "moduleList": []
+  },
+  {
+    "id": 2,
+    "parentId": 1,
+    "name": "上飞厂",
+    "code": "shangfei",
+    "remark": "",
+    "status": 0,
+    "createdAt": "2023-10-13 09:42:21",
+    "updatedAt": "2024-01-02 19:22:12",
+    "serial": 4,
+    "isDeleted": 0,
+    "children": [
+      {
+        "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": []
+      },
+      {
+        "id": 8,
+        "companyId": 2,
+        "sceneLabelId": 1,
+        "name": "工装中心",
+        "code": "B04",
+        "remark": "",
+        "status": 0,
+        "createdAt": "2023-10-13 09:53:01",
+        "updatedAt": "2023-10-13 09:53:01",
+        "isDeleted": 0,
+        "serial": 0,
+        "labelName": "生产安全",
+        "workshopModule": null,
+        "children": []
+      },
+      {
+        "id": 9,
+        "companyId": 2,
+        "sceneLabelId": 2,
+        "name": "B16热压罐",
+        "code": "B16autoclave",
+        "remark": "",
+        "status": 0,
+        "createdAt": "2023-10-13 10:22:20",
+        "updatedAt": "2023-10-19 19:12:27",
+        "isDeleted": 0,
+        "serial": 0,
+        "labelName": "安全管控",
+        "workshopModule": null,
+        "children": []
+      },
+      {
+        "id": 10,
+        "companyId": 2,
+        "sceneLabelId": 2,
+        "name": "B01热压罐",
+        "code": "B01autoclave",
+        "remark": "",
+        "status": 0,
+        "createdAt": "2023-10-13 10:22:39",
+        "updatedAt": "2023-10-13 10:22:39",
+        "isDeleted": 0,
+        "serial": 0,
+        "labelName": "安全管控",
+        "workshopModule": null,
+        "children": []
+      },
+      {
+        "id": 11,
+        "companyId": 2,
+        "sceneLabelId": 2,
+        "name": "110KV变电站",
+        "code": "110KVsubstation",
+        "remark": "",
+        "status": 0,
+        "createdAt": "2023-10-13 10:23:02",
+        "updatedAt": "2023-10-13 10:23:02",
+        "isDeleted": 0,
+        "serial": 0,
+        "labelName": "安全管控",
+        "workshopModule": null,
+        "children": []
+      },
+      {
+        "id": 12,
+        "companyId": 2,
+        "sceneLabelId": 2,
+        "name": "2011锅炉房",
+        "code": "2011boiler",
+        "remark": "",
+        "status": 0,
+        "createdAt": "2023-10-13 10:23:20",
+        "updatedAt": "2023-10-13 10:23:20",
+        "isDeleted": 0,
+        "serial": 0,
+        "labelName": "安全管控",
+        "workshopModule": {
+          "id": 1,
+          "name": "危险点",
+          "code": "1",
+          "remark": "",
+          "status": 0,
+          "createdAt": "2023-12-22 11:43:54",
+          "updatedAt": "2023-12-22 11:43:54",
+          "isDeleted": 0
+        },
+        "children": []
+      },
+      {
+        "id": 13,
+        "companyId": 2,
+        "sceneLabelId": 2,
+        "name": "2001a锅炉房",
+        "code": "2001aboiler",
+        "remark": "",
+        "status": 0,
+        "createdAt": "2023-10-13 10:23:36",
+        "updatedAt": "2023-10-13 10:23:36",
+        "isDeleted": 0,
+        "serial": 0,
+        "labelName": "安全管控",
+        "workshopModule": {
+          "id": 1,
+          "name": "危险点",
+          "code": "1",
+          "remark": "",
+          "status": 0,
+          "createdAt": "2023-12-22 11:43:54",
+          "updatedAt": "2023-12-22 11:43:54",
+          "isDeleted": 0
+        },
+        "children": []
+      },
+      {
+        "id": 15,
+        "companyId": 2,
+        "sceneLabelId": 2,
+        "name": "增材实验室",
+        "code": "sf-additive_lab",
+        "remark": "",
+        "status": 0,
+        "createdAt": "2023-12-11 13:40:33",
+        "updatedAt": "2023-12-11 13:40:33",
+        "isDeleted": 0,
+        "serial": 0,
+        "labelName": "安全管控",
+        "workshopModule": {
+          "id": 1,
+          "name": "危险点",
+          "code": "1",
+          "remark": "",
+          "status": 0,
+          "createdAt": "2023-12-22 11:43:54",
+          "updatedAt": "2023-12-22 11:43:54",
+          "isDeleted": 0
+        },
+        "children": []
+      },
+      {
+        "id": 2,
+        "companyId": 2,
+        "sceneLabelId": 1,
+        "name": "ARJ21总装车间",
+        "code": "C11",
+        "remark": "",
+        "status": 0,
+        "createdAt": "2023-10-13 09:49:31",
+        "updatedAt": "2024-01-16 15:00:48",
+        "isDeleted": 0,
+        "serial": 1,
+        "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": 4,
+        "companyId": 2,
+        "sceneLabelId": 1,
+        "name": "C919总装车间",
+        "code": "C01",
+        "remark": "",
+        "status": 0,
+        "createdAt": "2023-10-13 09:50:53",
+        "updatedAt": "2024-01-16 15:00:54",
+        "isDeleted": 0,
+        "serial": 1,
+        "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": 5,
+        "companyId": 2,
+        "sceneLabelId": 1,
+        "name": "维修交付中心",
+        "code": "repair",
+        "remark": "",
+        "status": 0,
+        "createdAt": "2023-10-13 09:51:22",
+        "updatedAt": "2024-01-16 15:00:55",
+        "isDeleted": 0,
+        "serial": 2,
+        "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": []
+      }
+    ],
+    "labelList": [
+      {
+        "id": 1,
+        "name": "生产安全",
+        "code": "1",
+        "remark": "11",
+        "status": 0,
+        "createdAt": "2023-12-22 11:42:52",
+        "updatedAt": "2024-01-16 15:00:11",
+        "isDeleted": 0
+      },
+      {
+        "id": 2,
+        "name": "安全管控",
+        "code": "2",
+        "remark": "",
+        "status": 0,
+        "createdAt": "2023-12-27 13:49:28",
+        "updatedAt": "2024-01-16 15:00:28",
+        "isDeleted": 0
+      }
+    ],
+    "moduleList": [
+      {
+        "id": 1,
+        "name": "生产安全模板",
+        "code": "12",
+        "remark": "32437857",
+        "status": 0,
+        "createdAt": "2023-12-22 11:43:05",
+        "updatedAt": "2024-01-04 14:37:40",
+        "isDeleted": 0
+      },
+      {
+        "id": 1,
+        "name": "生产安全模板",
+        "code": "12",
+        "remark": "32437857",
+        "status": 0,
+        "createdAt": "2023-12-22 11:43:05",
+        "updatedAt": "2024-01-04 14:37:40",
+        "isDeleted": 0
+      }
+    ]
+  }
+]

+ 140 - 0
src/views/page-config/usePageConfig.ts

@@ -0,0 +1,140 @@
+import { computed, ref, toRefs } from 'vue';
+import useSceneInfos from '@/hooks/useSceneInfos';
+import { defineStore } from 'pinia';
+
+export const usePageConfig = () => {
+  const sceneInfos = useSceneInfos();
+  const { scenesInfos } = toRefs(sceneInfos);
+  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 labelList = computed(
+    () => scenesInfos.value.find((item) => item.id === selectedCompany.value)?.labelList,
+  );
+
+  return { selectedCompany, scenesInfos, label, labelList, shopList };
+};
+
+export default usePageConfig;

+ 0 - 271
src/views/system-config/scene-layout/SceneLayout.vue

@@ -1,271 +0,0 @@
-<template>
-  <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">
-        <el-space :size="20">
-          <div>
-            <span>公司:</span>
-            <el-select v-model="curCompany">
-              <el-option
-                v-for="item in workshopInfos"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
-              />
-            </el-select>
-          </div>
-          <div>
-            <span>标签:</span>
-            <el-select v-model="curScene">
-              <el-option
-                v-for="item in workshopInfos.find((item) => item.id === curCompany)?.labelList"
-                :key="item.id"
-                :value="item.id"
-                :label="item.name"
-              />
-            </el-select>
-          </div>
-        </el-space>
-        <div class="flex">
-          <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"
-          >
-            <el-button style="font-size: 12px" :icon="Refresh" :disabled="!hasBg">
-              替换照片
-            </el-button>
-          </el-upload>
-
-          <el-button @click="handleSave" style="margin-left: 40px" type="primary"
-            >保存为布局
-          </el-button>
-        </div>
-      </div>
-    </div>
-    <div class="paint-tool flex">
-      <div class="camera-list">
-        <div>
-          <span class="label-text flex">车间列表:</span>
-          <ElInput
-            class="search-put"
-            style="margin: 10px 0; width: 230px"
-            placeholder="请输入搜索内容"
-            v-model="searchKey"
-            :suffix-icon="Search"
-          />
-        </div>
-        <span class="ml-1" style="color: #3f3f3f"> 提示:请先选择相应公司和场景 </span>
-        <el-scrollbar style="position: relative; height: calc(100% - 90px)">
-          <div></div>
-        </el-scrollbar>
-      </div>
-      <div class="draw-container">
-        <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"
-        >
-          <img src="~@/assets/images/img-upload.png" />
-        </el-upload>
-        <div class="tag-option">
-          <div>
-            <span>选项</span>
-          </div>
-          <div class="flex flex-col">
-            <div class="flex">
-              <span>背景色:</span>
-              <el-radio-group v-model="tagOption.backgroundColor" size="small">
-                <el-radio label="orange">
-                  <div
-                    style="width: 24px; height: 12px; background-color: orange; margin-right: 10px"
-                  ></div>
-                </el-radio>
-                <el-radio label="blue">
-                  <div
-                    style="width: 24px; height: 12px; background-color: aqua; margin-right: 10px"
-                  ></div>
-                </el-radio>
-                <el-radio label="yellow">
-                  <div
-                    style="width: 24px; height: 12px; background-color: yellow; margin-right: 10px"
-                  ></div>
-                </el-radio>
-              </el-radio-group>
-            </div>
-            <div class="flex">
-              <span>字体:</span>
-              <el-input v-model="tagOption.fontSize" style="width: 150px" />
-            </div>
-            <div class="flex">
-              <span>文字位置:</span>
-              <el-radio-group v-model="tagOption.textPosition" size="small">
-                <el-radio-button label="up">上</el-radio-button>
-                <el-radio-button label="bottom">下</el-radio-button>
-                <el-radio-button label="left">左</el-radio-button>
-                <el-radio-button label="right">右</el-radio-button>
-              </el-radio-group>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-  import { ref } from 'vue';
-  import { workshopInfos } from './tempConstant';
-
-  const curCompany = ref<number>();
-  const curScene = ref<string>('');
-
-  const tagOption = ref({
-    backgroundColor: 'orange',
-    fontSize: 14,
-    textPosition: 'right',
-  });
-
-  import { ElInput } from 'element-plus';
-  import urlJoin from 'url-join';
-  import { useGlobSetting } from '@/hooks/setting';
-  import { Search, ArrowLeft, Refresh } from '@element-plus/icons-vue';
-
-  const globSetting = useGlobSetting();
-
-  const searchKey = ref('');
-  // 是否已有背景图
-  const hasBg = ref(false);
-
-  const handleAvatarSuccess = (e) => {
-    const imgPath = e.data;
-    const imgUrl = urlJoin(globSetting.imgUrl!, imgPath);
-    hasBg.value = true;
-  };
-
-  const handleSave = () => {};
-</script>
-
-<style scoped>
-  .page {
-  }
-  .page-head {
-    height: 54px;
-    padding-left: 15px;
-    background-color: #ffffff;
-  }
-  .head-opt {
-    margin-left: 20px;
-    padding-right: 15px;
-    font-size: 14px;
-    color: #3f3f3f;
-  }
-
-  .avatar-uploader {
-    /* width: 120px; */
-    /* height: 30px; */
-    /* border: 1px solid #eee; */
-    border-radius: 4px;
-    margin-left: 30px;
-  }
-  .upload-icon {
-    position: absolute;
-    top: 0;
-    right: 0;
-    left: 0;
-    bottom: 0;
-    margin: auto;
-  }
-
-  .paint-tool {
-    position: relative;
-    height: calc(100vh - 138px);
-    margin-top: 2px;
-  }
-
-  .camera-list {
-    width: 250px;
-    padding: 0 10px;
-    background-color: #ffffff;
-  }
-  .label-text {
-    font-size: 14px;
-    font-weight: 600;
-    margin: 10px 0 5px 10px;
-  }
-  .camera-item {
-    height: 32px;
-    font-size: 14px;
-    font-weight: 400;
-    color: #404040;
-    line-height: 14px;
-    cursor: pointer;
-  }
-  .camera-item-disabled {
-    color: #c6c6c6;
-  }
-  .camera-id {
-    width: 110px;
-  }
-  .space-name {
-    width: 120px;
-    white-space: nowrap;
-    overflow: hidden;
-    text-overflow: ellipsis;
-  }
-
-  .draw-container {
-    position: relative;
-    width: calc(100% - 300px);
-    margin: 20px;
-  }
-
-  .tag-option {
-    position: absolute;
-    width: 264px;
-    height: 167px;
-    top: 24px;
-    right: 0px;
-    border-radius: 10px;
-    padding: 12px;
-    background-color: #ffffff;
-  }
-
-  :deep(.search-put .el-input__wrapper) {
-    background-color: #f0f2f5;
-  }
-  :deep(.el-popper__arrow) {
-    display: none;
-  }
-  :deep(.el-tree-node__content:hover) {
-    background: #e6f4ff;
-  }
-  :deep(.el-button--primary) {
-    --el-button-disabled-bg-color: #bfbfbf;
-  }
-  :deep(.el-popover) {
-    width: unset !important;
-    min-width: 110px;
-    text-align: center;
-    font-weight: 400;
-  }
-
-  :deep(.el-radio) {
-    margin-right: 0;
-  }
-
-  .isAdded {
-    color: #409eff;
-  }
-</style>

+ 0 - 27
src/views/system-config/scene-layout/tempConstant.ts

@@ -1,27 +0,0 @@
-export interface LabelItem {
-  code: string;
-  id: number;
-  name: string;
-}
-
-export interface WorkshopInfo {
-  code: string;
-  name: string;
-  id: number;
-  labelList: LabelItem[];
-}
-
-export const workshopInfos: WorkshopInfo[] = [
-  {
-    code: 'shangfei',
-    name: '上飞公司',
-    id: 2,
-    labelList: [
-      {
-        code: 'safe-production',
-        id: 0,
-        name: '生产安全-上飞公司安全模板',
-      },
-    ],
-  },
-];

+ 11 - 2
vite.config.ts

@@ -1,12 +1,21 @@
 import type { UserConfig, ConfigEnv } from 'vite';
 import { loadEnv } from 'vite';
-import { resolve } from 'path';
+import path, { resolve } from 'path';
 import { wrapperEnv } from './build/utils';
 import { createVitePlugins } from './build/vite/plugin';
 import { OUTPUT_DIR } from './build/constant';
 import { createProxy } from './build/vite/proxy';
 import pkg from './package.json';
 import { formatToDateTime } from './src/utils/dateUtil';
+import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
+
+const svg = createSvgIconsPlugin({
+  // 要缓存的图标文件夹
+  iconDirs: [path.resolve(__dirname, 'src/assets/icons')],
+  // 执行 icon name 的格式
+  symbolId: 'icon-[name]',
+});
+
 const { dependencies, devDependencies, name, version } = pkg;
 
 const __APP_INFO__ = {
@@ -41,7 +50,7 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
       ],
       dedupe: ['vue'],
     },
-    plugins: createVitePlugins(viteEnv, isBuild, prodMock),
+    plugins: [createVitePlugins(viteEnv, isBuild, prodMock), svg],
     define: {
       __APP_INFO__: JSON.stringify(__APP_INFO__),
     },