Jelajahi Sumber

feat: 修改设计

bill 4 bulan lalu
induk
melakukan
c03972e8cc
69 mengubah file dengan 1628 tambahan dan 1347 penghapusan
  1. 2 1
      package.json
  2. 11 0
      pnpm-lock.yaml
  3. 1 1
      public/icons/A3_h.svg
  4. 1 1
      public/icons/A3_v.svg
  5. 1 1
      public/icons/A4_h.svg
  6. 1 1
      public/icons/A4_v.svg
  7. 1 1
      public/icons/a-visible.svg
  8. 1 0
      public/icons/back.svg
  9. 1 0
      public/icons/debugger.svg
  10. 1 0
      public/icons/download.svg
  11. 1 1
      public/icons/drawing.svg
  12. 1 0
      public/icons/local_i.svg
  13. 1 0
      public/icons/scene_i.svg
  14. 5 4
      src/core/components/arrow/index.ts
  15. 7 6
      src/core/components/circle/index.ts
  16. 5 4
      src/core/components/icon/index.ts
  17. 4 2
      src/core/components/image/index.ts
  18. 1 0
      src/core/components/image/temp-image.vue
  19. 1 0
      src/core/components/line/index.ts
  20. 2 1
      src/core/components/polygon/index.ts
  21. 6 5
      src/core/components/rectangle/index.ts
  22. 0 1
      src/core/components/serial/index.ts
  23. 5 5
      src/core/components/table/index.ts
  24. 5 4
      src/core/components/text/index.ts
  25. 3 2
      src/core/components/triangle/index.ts
  26. 5 6
      src/core/helper/back-grid.vue
  27. 3 3
      src/core/helper/compass.vue
  28. 3 1
      src/core/hook/use-expose.ts
  29. 1 1
      src/core/renderer/renderer.vue
  30. 108 0
      src/example/components/container/container.vue
  31. 13 0
      src/example/components/container/use-draw.ts
  32. 98 0
      src/example/components/header/actions.ts
  33. 124 0
      src/example/components/header/index.vue
  34. 1 1
      src/example/fuse/views/show-vr.vue
  35. 60 51
      src/example/fuse/views/slide/actions.ts
  36. 4 2
      src/example/fuse/views/slide/menu.ts
  37. 2 25
      src/example/fuse/views/slide/slide-item.vue
  38. 172 0
      src/example/components/slide/slide.vue
  39. 1 1
      src/example/fuse/dialog/ai/ai.vue
  40. 0 0
      src/example/dialog/ai/index.ts
  41. 0 0
      src/example/dialog/basemap/gd-map/selectAMapImage.vue
  42. 0 0
      src/example/dialog/basemap/index.ts
  43. 0 0
      src/example/dialog/dialog.vue
  44. 0 0
      src/example/dialog/vr/index.ts
  45. 1 1
      src/example/fuse/dialog/vr/test.ts
  46. 1 1
      src/example/fuse/dialog/vr/vr.vue
  47. 7 0
      src/example/env.ts
  48. 3 2
      src/example/fuse/App.vue
  49. 10 1
      src/example/fuse/main.ts
  50. 48 0
      src/example/fuse/req.ts
  51. 22 0
      src/example/fuse/router.ts
  52. 0 173
      src/example/fuse/views/header/header.vue
  53. 0 132
      src/example/fuse/views/home.vue
  54. 60 0
      src/example/fuse/views/overview/header.vue
  55. 46 0
      src/example/fuse/views/overview/index.vue
  56. 223 0
      src/example/fuse/views/overview/slide-icons.vue
  57. 31 0
      src/example/fuse/views/overview/slide.vue
  58. 0 25
      src/example/fuse/views/req.ts
  59. 0 115
      src/example/fuse/views/slide/slide.vue
  60. 37 0
      src/example/fuse/views/tabulation/header.vue
  61. 153 0
      src/example/fuse/views/tabulation/index.vue
  62. 223 0
      src/example/fuse/views/tabulation/slide-icons.vue
  63. 19 0
      src/example/fuse/views/tabulation/slide.vue
  64. 0 700
      src/example/fuse/views/test.ts
  65. 0 27
      src/example/fuse/views/use-draw.ts
  66. 1 1
      src/example/fuse/platform-draw.ts
  67. 0 0
      src/example/platform/platform-resource.ts
  68. 17 2
      src/example/fuse/styles/global.scss
  69. 63 35
      src/utils/dom.ts

+ 2 - 1
package.json

@@ -27,7 +27,8 @@
     "vite-plugin-html": "^3.2.2",
     "vite-plugin-svg-icons": "^2.0.1",
     "vue": "^3.5.13",
-    "vue-konva": "^3.2.0"
+    "vue-konva": "^3.2.0",
+    "vue-router": "^4.5.0"
   },
   "devDependencies": {
     "@vitejs/plugin-vue": "^5.1.4",

+ 11 - 0
pnpm-lock.yaml

@@ -24,6 +24,7 @@ specifiers:
   vite-plugin-svg-icons: ^2.0.1
   vue: ^3.5.13
   vue-konva: ^3.2.0
+  vue-router: ^4.5.0
   vue-tsc: ^2.1.6
 
 dependencies:
@@ -46,6 +47,7 @@ dependencies:
   vite-plugin-svg-icons: 2.0.1_vite@5.4.10
   vue: 3.5.13_typescript@5.6.3
   vue-konva: 3.2.0_konva@9.3.18+vue@3.5.13
+  vue-router: 4.5.0_vue@3.5.13
 
 devDependencies:
   '@vitejs/plugin-vue': 5.1.4_vite@5.4.10+vue@3.5.13
@@ -3620,6 +3622,15 @@ packages:
       vue: 3.5.13_typescript@5.6.3
     dev: false
 
+  /vue-router/4.5.0_vue@3.5.13:
+    resolution: {integrity: sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==}
+    peerDependencies:
+      vue: ^3.2.0
+    dependencies:
+      '@vue/devtools-api': 6.6.4
+      vue: 3.5.13_typescript@5.6.3
+    dev: false
+
   /vue-tsc/2.1.8_typescript@5.6.3:
     resolution: {integrity: sha512-6+vjb7JLxKIzeD/1ktoUBZGAr+148FQoEFl8Lv5EpDJLO2PrUalhp7atMEuzEkLnoooM5bg3pJqjZI+oobxIaQ==}
     hasBin: true

File diff ditekan karena terlalu besar
+ 1 - 1
public/icons/A3_h.svg


File diff ditekan karena terlalu besar
+ 1 - 1
public/icons/A3_v.svg


File diff ditekan karena terlalu besar
+ 1 - 1
public/icons/A4_h.svg


File diff ditekan karena terlalu besar
+ 1 - 1
public/icons/A4_v.svg


File diff ditekan karena terlalu besar
+ 1 - 1
public/icons/a-visible.svg


File diff ditekan karena terlalu besar
+ 1 - 0
public/icons/back.svg


File diff ditekan karena terlalu besar
+ 1 - 0
public/icons/debugger.svg


File diff ditekan karena terlalu besar
+ 1 - 0
public/icons/download.svg


File diff ditekan karena terlalu besar
+ 1 - 1
public/icons/drawing.svg


File diff ditekan karena terlalu besar
+ 1 - 0
public/icons/local_i.svg


File diff ditekan karena terlalu besar
+ 1 - 0
public/icons/scene_i.svg


+ 5 - 4
src/core/components/arrow/index.ts

@@ -1,11 +1,11 @@
 import { Pos } from "@/utils/math.ts";
 import { flatPositions, onlyId } from "@/utils/shared.ts";
 import { ArrowConfig } from "konva/lib/shapes/Arrow";
-import { themeMouseColors } from "@/constant/help-style.ts";
 import { BaseItem, generateSnapInfos, getBaseItem } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
 import { InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
 import { Transform } from "konva/lib/Util";
+import { themeColor } from "@/constant/help-style.ts";
 
 export { default as Component } from "./arrow.vue";
 export { default as TempComponent } from "./temp-arrow.vue";
@@ -17,7 +17,7 @@ export enum PointerPosition {
 }
 export const shapeName = "箭头";
 export const defaultStyle = {
-  fill: themeMouseColors.theme,
+  fill: '#000000',
   pointerPosition: PointerPosition.end,
   strokeWidth: 2,
   pointerLength: 10,
@@ -26,15 +26,16 @@ export const defaultStyle = {
 export const addMode = "dots";
 
 export const getMouseStyle = (data: ArrowData) => {
-  const strokeStatus = getMouseColors(data.fill || defaultStyle.fill);
+  const strokeStatus = getMouseColors(data.fill || themeColor);
   const strokeWidth = data.strokeWidth || defaultStyle.strokeWidth;
 
   return {
     default: {
-      fill: strokeStatus.pub,
+      fill: data.fill || defaultStyle.fill,
       strokeWidth,
     },
     hover: { fill: strokeStatus.hover },
+    focus: { fill: strokeStatus.hover },
     select: { fill: strokeStatus.select },
     press: { fill: strokeStatus.press },
   };

+ 7 - 6
src/core/components/circle/index.ts

@@ -1,5 +1,5 @@
 import { CircleConfig } from "konva/lib/shapes/Circle";
-import { themeMouseColors } from "@/constant/help-style.ts";
+import { themeColor, themeMouseColors } from "@/constant/help-style.ts";
 import {
   BaseItem,
   generateSnapInfos,
@@ -18,13 +18,13 @@ export { default as TempComponent } from "./temp-circle.vue";
 export const shapeName = "圆形";
 export const defaultStyle = {
   dash: [30, 0],
-  stroke: themeMouseColors.theme,
-  strokeWidth: 1,
+  stroke: '#000000',
+  strokeWidth: 2,
   fontSize: 16,
   align: "center",
   fill: '#ffffff',
   fontStyle: "normal",
-  fontColor: themeMouseColors.theme,
+  fontColor: '#000000',
   padding: 8
 };
 
@@ -32,12 +32,13 @@ export const addMode = "area";
 
 export const getMouseStyle = (data: CircleData) => {
   const fillStatus = data.fill && getMouseColors(data.fill);
-  const strokeStatus = getMouseColors(data.stroke || defaultStyle.stroke);
+  const strokeStatus = getMouseColors(data.stroke || themeColor);
   const strokeWidth = data.strokeWidth || defaultStyle.strokeWidth;
   return {
-    default: { fill: fillStatus && fillStatus.pub, stroke: strokeStatus.pub, strokeWidth },
+    default: { fill: fillStatus && defaultStyle.fill, stroke: data.stroke || defaultStyle.stroke, strokeWidth },
     hover: { fill: fillStatus && fillStatus.hover, stroke: strokeStatus.hover },
     select: { fill: fillStatus && fillStatus.select, stroke: strokeStatus.select  },
+    focus: { fill: fillStatus && fillStatus.hover, stroke: strokeStatus.hover },
     press: { fill: fillStatus && fillStatus.press, stroke: strokeStatus.press },
   };
 };

+ 5 - 4
src/core/components/icon/index.ts

@@ -1,4 +1,4 @@
-import { themeMouseColors } from "@/constant/help-style.ts";
+import { themeColor, themeMouseColors } from "@/constant/help-style.ts";
 import { Transform } from "konva/lib/Util";
 import {
   BaseItem,
@@ -16,7 +16,7 @@ export { default as TempComponent } from "./temp-icon.vue";
 
 export const shapeName = "图例";
 export const defaultStyle = {
-  coverFill: themeMouseColors.theme,
+  coverFill: '#000000',
   coverOpcatiy: 0,
   strokeScaleEnabled: false,
   width: 80,
@@ -38,16 +38,17 @@ export const getSnapPoints = (data: IconData) => {
 };
 
 export const getMouseStyle = (data: IconData) => {
-  const fillStatus = getMouseColors(data.coverFill || defaultStyle.coverFill);
+  const fillStatus = getMouseColors(data.coverFill || themeColor);
   const hCoverOpcaoty = data.coverOpcatiy ? data.coverOpcatiy : 0.3;
 
   return {
     default: {
-      coverFill: fillStatus.pub,
+      coverFill: data.coverFill || defaultStyle.coverFill,
       coverOpcatiy: data.coverOpcatiy || 0,
     },
     hover: { coverFill: fillStatus.hover, coverOpcatiy: hCoverOpcaoty },
     select: { coverFill: fillStatus.select, coverOpcatiy: hCoverOpcaoty  },
+    focus: { coverFill: fillStatus.select, coverOpcatiy: hCoverOpcaoty  },
     press: { coverFill: fillStatus.press, coverOpcatiy: hCoverOpcaoty },
   };
 };

+ 4 - 2
src/core/components/image/index.ts

@@ -9,6 +9,7 @@ import { getMouseColors } from "@/utils/colors.ts";
 import { imageInfo } from "@/utils/resource.ts";
 import { InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
 import { Size } from "@/utils/math.ts";
+import { themeColor } from "@/constant/help-style.ts";
 
 export { default as Component } from "./image.vue";
 export { default as TempComponent } from "./temp-image.vue";
@@ -22,11 +23,12 @@ export const defaultStyle = {
 export const addMode = "dot";
 
 export const getMouseStyle = (data: ImageData) => {
-  const strokeStatus = getMouseColors(data.stroke || defaultStyle.stroke);
+  const strokeStatus = getMouseColors(data.stroke || themeColor);
 
   return {
-    default: { stroke: strokeStatus.pub },
+    default: { stroke: data.stroke || defaultStyle.stroke },
     hover: { stroke: strokeStatus.hover },
+    focus: { stroke: strokeStatus.hover },
     select: { stroke: strokeStatus.select },
     press: { stroke: strokeStatus.press },
   };

+ 1 - 0
src/core/components/image/temp-image.vue

@@ -37,6 +37,7 @@ watch(
   async (url) => {
     image.value = null;
     image.value = await getImage(url);
+    image.value.crossOrigin = "anonymous";
   },
   { immediate: true }
 );

+ 1 - 0
src/core/components/line/index.ts

@@ -25,6 +25,7 @@ export const getMouseStyle = (data: LineData) => {
     default: { stroke: data.stroke || defaultStyle.stroke, strokeWidth },
     hover: { stroke: strokeStatus.hover },
     select: { stroke: strokeStatus.select },
+    focus: { stroke: strokeStatus.hover },
     press: { stroke: strokeStatus.press },
   };
 };

+ 2 - 1
src/core/components/polygon/index.ts

@@ -23,11 +23,12 @@ export const getMouseStyle = (data: PolygonData) => {
 
   return {
     default: {
-      fill: fillStatus && fillStatus.pub,
+      fill: data.fill || defaultStyle.fill,
       stroke: data.stroke || defaultStyle.stroke,
       strokeWidth,
     },
     hover: { fill: fillStatus && fillStatus.hover, stroke: strokeStatus.hover },
+    focus: { fill: fillStatus && fillStatus.hover, stroke: strokeStatus.hover },
     press: { fill: fillStatus && fillStatus.press, stroke: strokeStatus.press },
     select: {
       fill: fillStatus && fillStatus.select,

+ 6 - 5
src/core/components/rectangle/index.ts

@@ -1,5 +1,5 @@
 import { Pos } from "@/utils/math.ts";
-import { themeMouseColors } from "@/constant/help-style.ts";
+import { themeColor, themeMouseColors } from "@/constant/help-style.ts";
 import { getMouseColors } from "@/utils/colors.ts";
 import { onlyId } from "@/utils/shared.ts";
 import { BaseItem, generateSnapInfos, getBaseItem } from "../util.ts";
@@ -14,21 +14,22 @@ export const defaultStyle = {
   dash: [30, 0],
   strokeWidth: 1,
   fill: '#ffffff',
-  stroke: themeMouseColors.theme,
+  stroke: '#000000',
   fontSize: 16,
   align: "center",
   fontStyle: "normal",
-  fontColor: themeMouseColors.theme,
+  fontColor: '#000000',
 };
 
 export const getMouseStyle = (data: RectangleData) => {
   const fillStatus = data.fill && getMouseColors(data.fill);
-  const strokeStatus = getMouseColors(data.stroke || defaultStyle.stroke);
+  const strokeStatus = getMouseColors(data.stroke || themeColor);
   const strokeWidth = data.strokeWidth || defaultStyle.strokeWidth;
 
   return {
-    default: { fill: fillStatus && fillStatus.pub, stroke: strokeStatus.pub, strokeWidth },
+    default: { fill: data.fill || defaultStyle.fill, stroke: data.stroke || defaultStyle.stroke, strokeWidth },
     hover: { fill: fillStatus && fillStatus.hover, stroke: strokeStatus.hover },
+    focus: { fill: fillStatus && fillStatus.hover, stroke: strokeStatus.hover },
     press: { fill: fillStatus && fillStatus.press, stroke: strokeStatus.press },
     select: { fill: fillStatus && fillStatus.select, stroke: strokeStatus.select },
   };

+ 0 - 1
src/core/components/serial/index.ts

@@ -4,7 +4,6 @@ import { CircleData, defaultStyle } from "../circle";
 import { InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
 import { DrawStore } from "@/core/store/index.ts";
 import { Pos } from "@/utils/math.ts";
-import { MathUtils } from "three";
 
 export {
   defaultStyle,

+ 5 - 5
src/core/components/table/index.ts

@@ -1,5 +1,5 @@
 import { Transform } from "konva/lib/Util";
-import { themeMouseColors } from "@/constant/help-style.ts";
+import { themeColor, themeMouseColors } from "@/constant/help-style.ts";
 import {
   BaseItem,
   generateSnapInfos,
@@ -17,12 +17,12 @@ export { default as TempComponent } from "./temp-table.vue";
 
 export const shapeName = "表格";
 export const defaultStyle = {
-  stroke: themeMouseColors.theme,
+  stroke: '#000',
   strokeWidth: 1,
   fontSize: 16,
   align: "center",
   fontStyle: "normal",
-  fontColor: themeMouseColors.theme,
+  fontColor: '#000',
 };
 export const defaultCollData = {
   fontFamily: "Calibri",
@@ -51,10 +51,10 @@ export type TableData = Partial<typeof defaultStyle> &
   };
 
 export const getMouseStyle = (data: TableData) => {
-  const strokeStatus = getMouseColors(data.stroke || defaultStyle.stroke);
+  const strokeStatus = getMouseColors(data.stroke || themeColor);
 
   return {
-    default: { stroke: strokeStatus.pub },
+    default: { stroke: data.stroke || defaultStyle.stroke },
     hover: { stroke: strokeStatus.hover },
     press: { stroke: strokeStatus.press },
     select: { select: strokeStatus.select },

+ 5 - 4
src/core/components/text/index.ts

@@ -1,6 +1,6 @@
 import { Transform } from "konva/lib/Util";
 import { Text } from "konva/lib/shapes/Text";
-import { themeMouseColors } from "@/constant/help-style.ts";
+import { themeColor, themeMouseColors } from "@/constant/help-style.ts";
 import {
   BaseItem,
   generateSnapInfos,
@@ -19,7 +19,7 @@ export { default as TempComponent } from "./temp-text.vue";
 export const shapeName = "文字";
 export const defaultStyle = {
   // stroke: themeMouseColors.theme,
-  fill: themeMouseColors.theme,
+  fill: '#000000',
   // strokeWidth: 0,
   fontFamily: "Calibri",
   fontSize: 16,
@@ -38,14 +38,15 @@ export type TextData = Partial<typeof defaultStyle> &
   };
 
 export const getMouseStyle = (data: TextData) => {
-  const fillStatus = getMouseColors(data.fill || defaultStyle.fill);
+  const fillStatus = getMouseColors(data.fill || themeColor);
   // const strokeStatus = getMouseColors(data.stroke || defaultStyle.stroke);
   // const strokeWidth = data.strokeWidth || defaultStyle.strokeWidth;
 
   return {
-    default: { fill: fillStatus.pub },
+    default: { fill: data.fill || defaultStyle.fill },
     hover: { fill: fillStatus.hover },
     press: { fill: fillStatus.press },
+    focus: { fill: fillStatus.hover },
     select: { fill: fillStatus.select },
   };
 };

+ 3 - 2
src/core/components/triangle/index.ts

@@ -28,11 +28,12 @@ export const getMouseStyle = (data: TriangleData) => {
 
   return {
     default: {
-      fill: fillStatus && fillStatus.pub,
-      stroke: strokeStatus.pub,
+      fill: data.fill || defaultStyle.fill,
+      stroke: data.stroke || defaultStyle.stroke,
       strokeWidth,
     },
     hover: { fill: fillStatus && fillStatus.hover, stroke: strokeStatus.hover },
+    focus: { fill: fillStatus && fillStatus.hover, stroke: strokeStatus.hover },
     press: { fill: fillStatus && fillStatus.press, stroke: strokeStatus.press },
     select: { select: fillStatus && fillStatus.select, stroke: strokeStatus.select },
   };

+ 5 - 6
src/core/helper/back-grid.vue

@@ -5,7 +5,7 @@
       :config="{
         points: hConfig.children,
         ...style,
-        strokeWidth: style.strokeWidth * 0.33,
+        strokeWidth: style.strokeWidth * 0.5,
       }"
     />
     <v-line
@@ -13,7 +13,7 @@
       :config="{
         points: vConfig.children,
         ...style,
-        strokeWidth: style.strokeWidth * 0.33,
+        strokeWidth: style.strokeWidth * 0.5,
       }"
     />
     <v-line v-if="hConfig" :config="{ points: hConfig.dividing, ...style }" />
@@ -32,10 +32,9 @@ import { lineLen } from "@/utils/math";
 import { debounce } from "@/utils/shared";
 
 const style = {
-  stroke: "#ccc",
-  strokeWidth: 3,
-  opacity: 0.4,
-  strokeScaleEnabled: false,
+  stroke: "#EBEBEB",
+  strokeWidth: 4,
+  opacity: 1,
 };
 
 const pixelSize = useResize();

+ 3 - 3
src/core/helper/compass.vue

@@ -15,7 +15,7 @@
 import TempIcon from "../components/icon/temp-icon.vue";
 import { PropertyUpdate, mergeDescribes } from "../html-mount/propertys/index.ts";
 import { computed, ref, watch } from "vue";
-import { themeMouseColors } from "@/constant/help-style.ts";
+import { themeColor, themeMouseColors } from "@/constant/help-style.ts";
 import { useAnimationMouseStyle } from "../hook/use-mouse-status.ts";
 import { Group } from "konva/lib/Group";
 import { DC } from "@/deconstruction.js";
@@ -28,8 +28,8 @@ import { useViewerTransformConfig } from "../hook/use-viewer.ts";
 
 const config = useConfig();
 const data = ref({
-  stroke: themeMouseColors.theme,
-  fill: themeMouseColors.theme,
+  // stroke: "#000000",
+  // fill: themeColor,
   coverOpcatiy: 0,
   strokeScaleEnabled: false,
   width: 80,

+ 3 - 1
src/core/hook/use-expose.ts

@@ -25,6 +25,8 @@ import { useResourceHandler } from "./use-fetch.ts";
 import { useConfig } from "./use-config.ts";
 import { useSelectionRevise } from "./use-selection.ts";
 import { useFormalLayer, useGetFormalChildren } from "./use-layer.ts";
+import { DataGroupId } from "@/constant/index.ts";
+import { Group } from "konva/lib/Group";
 
 // 自动粘贴服务
 export const useAutoPaste = () => {
@@ -220,7 +222,7 @@ export const useExpose = installGlobalVar(() => {
 
   const formalLayer = useFormalLayer();
   const initViewport = () => {
-    const rect = formalLayer.value!.getClientRect();
+    const rect = formalLayer.value!.findOne<Group>('#' + DataGroupId)!.getClientRect();
     const invMat = viewer.transform.invert();
     const lt = invMat.point(rect);
     const rb = invMat.point({

+ 1 - 1
src/core/renderer/renderer.vue

@@ -19,7 +19,7 @@
             <!--	不可去除,去除后移动端拖拽会有溢出	-->
             <BackGrid v-if="expose.config.showGrid" />
           </v-group>
-          <v-group :id="DataGroupId">
+          <v-group :id="DataGroupId" >
             <component
               :is="GroupComponentMap[type]"
               v-for="type in types"

+ 108 - 0
src/example/components/container/container.vue

@@ -0,0 +1,108 @@
+<template>
+  <div class="layout" :class="{ full }">
+    <slot name="header" class="header" v-if="draw" :draw="draw" />
+    <div class="container">
+      <slot name="slide" class="slide" v-if="draw" :draw="draw" />
+      <div class="content" ref="drawEle">
+        <DrawBoard v-if="drawEle" :handler-resource="uploadResourse" ref="draw" />
+        <slot name="cover" v-if="draw" :draw="draw" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { onUnmounted, ref, watch } from "vue";
+import { DrawBoard } from "@/index";
+import { listener } from "@/utils/event.ts";
+import { ElMessage } from "element-plus";
+import { mergeFuns, startAnimation } from "@/utils/shared";
+import { installDraw } from "./use-draw";
+
+const props = defineProps<{
+  full: boolean;
+  uploadResourse: (file: File) => Promise<string>;
+}>();
+
+const emit = defineEmits<{ (e: "update:full", full: boolean): void }>();
+
+const drawEle = ref<HTMLDivElement | null>(null);
+const draw = installDraw(ref());
+
+watch(
+  () => props.full,
+  (_f1, _f2, onCleanup) => {
+    const hideMsg =
+      props.full &&
+      ElMessage.warning({
+        message: "按ESC键可退出全屏模式",
+        duration: 3000,
+      });
+
+    const stopAnimation = startAnimation(() => {
+      draw.value?.updateSize();
+    }, 400);
+
+    onCleanup(mergeFuns([() => hideMsg && hideMsg.close(), stopAnimation]));
+  }
+);
+
+onUnmounted(
+  listener(document.documentElement, "keyup", (ev) => {
+    if (ev.key === "Escape") {
+      emit("update:full", false);
+    }
+  })
+);
+
+defineExpose({
+  get draw() {
+    return draw.value;
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+@use '../../styles/global';
+
+.layout {
+  display: flex;
+  flex-direction: column;
+  align-items: stretch;
+  height: 100vh;
+  background: #f0f2f5;
+  --top: 0px;
+  --left: 0px;
+  overflow: hidden;
+
+  .header {
+    margin-top: var(--top);
+    transition: margin-top 0.3s ease;
+    height: global.$headerSize;
+    flex: 0 0 auto;
+  }
+
+  .container {
+    flex: 1;
+    display: flex;
+    align-items: stretch;
+  }
+
+  .slide {
+    flex: 0 0 auto;
+    width: global.$slideSize;
+    margin-left: var(--left);
+    background: #fff;
+    transition: margin-left 0.3s ease;
+  }
+
+  .content {
+    position: relative;
+    width: calc(100% - 70px - var(--left));
+  }
+  &.full {
+    --top: calc(-1 * #{global.$headerSize});
+    --left: calc(-1 * #{global.$slideSize});
+  }
+}
+</style>

+ 13 - 0
src/example/components/container/use-draw.ts

@@ -0,0 +1,13 @@
+import { DrawExpose } from "@/core/hook/use-expose";
+import { inject, provide, Ref, ShallowUnwrapRef } from "vue";
+
+const actionKey = Symbol("drawAction");
+export type Draw = ShallowUnwrapRef<DrawExpose>;
+export const installDraw = (drawRef: Ref<DrawExpose | undefined>) => {
+  provide(actionKey, drawRef);
+  return drawRef
+};
+
+export const useDraw = () => {
+  return inject<Ref<Draw>>(actionKey)?.value!;
+}

+ 98 - 0
src/example/components/header/actions.ts

@@ -0,0 +1,98 @@
+import { reactive } from "vue";
+import { Draw } from "../container/use-draw";
+import { animation } from "@/core/hook/use-animation";
+import saveAs from "@/utils/file-serve";
+
+export type Action = {
+  handler?: () => void;
+  text?: string;
+  icon: string;
+  disabled?: boolean;
+  children?: Omit<Action, "children">[];
+};
+
+export type ActionGroups = Action[][];
+
+const rotateView = (draw: Draw) => {
+  const dom = draw.stage!.container();
+  let rotated = 0;
+  animation({ rotation: 0 }, { rotation: Math.PI / 2 }, ({ rotation }) => {
+    draw.viewer.rotatePixel(
+      { x: dom.offsetWidth / 2, y: dom.offsetHeight / 2 },
+      rotation - rotated
+    );
+    rotated = rotation;
+  });
+};
+
+export const getImage = (draw: Draw, format: string) =>
+  draw.stage!.toBlob({
+    pixelRatio: 4,
+    mimeType: format,
+    quality: 1,
+  }) as Promise<Blob>;
+
+export const getHeaderActions = (draw: Draw) => {
+  return {
+    undo: reactive({
+      handler: () => draw.history.undo(),
+      text: "撤销",
+      icon: "undo",
+      disabled: !draw.history.hasUndo.value,
+    }),
+    redo: reactive({
+      handler: () => draw.history.redo(),
+      text: "重做",
+      icon: "redo",
+      disabled: !draw.history.hasRedo.value,
+    }),
+    clear: reactive({
+      handler: () => draw.store.clear(),
+      text: "清除",
+      icon: "clear",
+    }),
+    rotateView: reactive({
+      handler: () => rotateView(draw),
+      text: "旋转",
+      icon: "rotate",
+    }),
+    initViewport: reactive({
+      handler: () => draw.initViewport(),
+      text: "初始视图",
+      icon: "a_adapt",
+    }),
+    toggleShow: reactive({
+      handler: () => {},
+      text: "显示",
+      icon: "a-visible",
+    }),
+    expose: reactive({
+      handler: () => {},
+      text: "导出",
+      icon: "download",
+      children: [
+        {
+          handler: async () => {
+            saveAs(await getImage(draw, 'image/jpeg'), "canvas.png");
+          },
+          text: "jpeg",
+          icon: "a-visible",
+        },
+        {
+          handler: async () => {
+            saveAs(await getImage(draw, 'image/png'), "canvas.png");
+          },
+          text: "png",
+          icon: "a-visible",
+        },
+        {
+          handler: async () => {
+            // saveAs(await getImage(draw, 'image/jpeg'), "canvas.png");
+          },
+          text: "DXF",
+          icon: "a-visible",
+        },
+      ],
+    }),
+  };
+};

+ 124 - 0
src/example/components/header/index.vue

@@ -0,0 +1,124 @@
+<template>
+  <div class="header">
+    <div class="nav">
+      <slot name="nav" v-if="$slots.nav" />
+      <span v-else class="nav-back">
+        <span class="back operate" v-if="!noBack">
+          <Icon name="back" @click="router.back()" />
+        </span>
+        <span class="title">{{ title }}</span>
+      </span>
+    </div>
+    <div class="draw-operate">
+      <div v-for="group in actionGroups">
+        <template v-for="action in group">
+          <span
+            v-if="!action.children"
+            class="operate"
+            @click="action.handler"
+            :class="{ disabled: action.disabled }"
+          >
+            <Icon :name="action.icon" :tip="action.text" />
+          </span>
+
+          <el-dropdown class="operate-dropdown" v-else>
+            <span class="operate" :class="{ disabled: action.disabled }">
+              <Icon :name="action.icon" />
+            </span>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item
+                  v-for="cAction in action.children"
+                  @click="cAction.handler"
+                >
+                  {{ cAction.text }}
+                </el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+        </template>
+      </div>
+    </div>
+    <div class="saves">
+      <slot name="saves" />
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { router } from "@/example/fuse/router";
+import { ActionGroups } from "./actions";
+import { ElDropdown, ElDropdownItem, ElDropdownMenu } from "element-plus";
+
+defineProps<{ actionGroups: ActionGroups; title?: string; noBack?: boolean }>();
+</script>
+
+<style lang="scss" scoped>
+@use 'element-plus/theme-chalk/src/common/var';
+
+.header {
+  background-color: #fff;
+  display: flex;
+  align-items: center;
+  padding: 10px;
+  justify-content: space-between;
+  color: rgba(0, 0, 0, 0.85);
+  border-bottom: 1px solid #e6e6e6;
+}
+
+.draw-operate {
+  text-align: center;
+  display: flex;
+  align-items: center;
+  font-size: 20px;
+
+  > div:not(:last-child) {
+    padding-right: 15px;
+    margin-right: 15px;
+    position: relative;
+    &::after {
+      content: "";
+      right: 0;
+      top: 50%;
+      transform: translateY(-50%);
+      position: absolute;
+      height: 1em;
+      width: 1px;
+      background: currentColor;
+    }
+  }
+
+  i {
+    width: auto;
+  }
+
+  .operate {
+    margin: 0 11px;
+    display: inline-flex;
+    align-items: center;
+    padding: 4px;
+    flex-direction: row-reverse;
+    border-radius: 4px;
+    justify-content: center;
+  }
+}
+
+.operate-dropdown {
+  height: 100%;
+  display: flex;
+  outline: none !important;
+  font-size: inherit;
+}
+
+.nav-back {
+  display: flex;
+  align-items: center;
+  .back {
+    padding: 2px;
+  }
+  .title {
+    margin-left: 6px;
+    font-size: 16px;
+  }
+}
+</style>

+ 1 - 1
src/example/fuse/views/show-vr.vue

@@ -18,7 +18,7 @@ import { dragListener, listener } from "@/utils/event";
 import { onUnmounted, ref, watch, watchEffect } from "vue";
 import { Pos } from "@/utils/math";
 import { mergeFuns, tempStrFill } from "@/utils/shared";
-import { Scene, SceneTypeTempUrls } from "../platform-resource";
+import { Scene, SceneTypeTempUrls } from "../platform/platform-resource";
 
 defineProps<{ scene: Scene }>();
 defineEmits<{ (e: "close"): void }>();

+ 60 - 51
src/example/fuse/views/slide/actions.ts

@@ -1,9 +1,14 @@
 import { DrawItem, shapeNames, ShapeType } from "@/index";
 import { v4 as uuid } from "uuid";
-import { Draw } from "../use-draw";
 import { getAMapInfo } from "../../dialog/basemap";
 import { getImageSize } from "@/utils/shape";
-import { themeColor } from "@/constant/help-style";
+import { selectAI } from "../../dialog/ai";
+import { drawPlatformResource } from "../../platform/platform-draw";
+import { selectFile } from "@/utils/dom";
+import { getImage } from "@/utils/resource";
+import { Draw } from "../container/use-draw";
+import { MenuItem } from "./menu";
+import { uploadResourse } from "@/example/fuse/req";
 
 
 export type PresetAdd<T extends ShapeType = ShapeType> = {
@@ -21,7 +26,7 @@ const genDrawItem = <T extends ShapeType>(
   payload: { type, preset },
 });
 
-export const draw = {
+export const draw: MenuItem = {
   icon: "line_d",
   name: "绘制",
   value: uuid(),
@@ -35,51 +40,60 @@ export const draw = {
   ],
 };
 
-export const legend = {
-  icon: "legend",
-  name: "图例",
-  value: uuid(),
-  children: [
-    {
-      icon: "",
-      ...genDrawItem("icon", {
-        url: "/icons/BedsideCupboard.svg",
-        width: 100,
-        height: 100,
-        fill: themeColor,
-      }),
-      name: "vue",
-    },
-    {
-      icon: "",
-      ...genDrawItem("icon", {
-        url: "/icons/vue.svg",
-        width: 100,
-        height: 100,
-        stroke: "red",
-        strokeWidth: 1,
-        strokeScaleEnabled: false,
-      }),
-      name: "自定义",
-    },
-  ],
-};
-
-export const text = {
+export const text: MenuItem = {
   icon: "text",
   ...genDrawItem("text", { content: "文本" }),
 };
 
-export const table = {
+export const table: MenuItem = {
   icon: "table",
   ...genDrawItem("table", {}),
 };
 
-export const serial = {
+export const serial: MenuItem = {
   icon: "order_no",
   ...genDrawItem("serial", { content: "1" }),
 };
 
+export const imp: MenuItem = {
+  icon: 'import',
+  name: "导入",
+  value: uuid(),
+  type: 'sub-menu-horizontal',
+  children: [
+    {
+      value: uuid(),
+      icon: "scene_i",
+      name: "从场景导入",
+      handler: async (draw: Draw) => {
+        const aiData = await selectAI();
+        drawPlatformResource(aiData, draw);
+      }
+    },
+    {
+      value: uuid(),
+      icon: "local_i",
+      name: "从本地上传",
+      handler: async (draw: Draw) => {
+        const files = await selectFile()
+        const url = await uploadResourse(files[0])
+        const image = await getImage(url)
+        draw.addShape(
+          "image", 
+          {
+            width: image.width,
+            height: image.height,
+            url: URL.createObjectURL(files[0]),
+          },
+          { x: window.innerWidth / 2, y: window.innerHeight / 2 },
+          true
+        );
+      }
+    }
+  ]
+
+}
+
 const setPaper = (draw: Draw, p: number[], scale: number) => {
   const pad = 5 * scale;
   const size = { width: p[0] * scale, height: p[1] * scale };
@@ -99,20 +113,11 @@ const setPaper = (draw: Draw, p: number[], scale: number) => {
 export const paper = {
   icon: "drawing",
   name: "纸张",
+  type: 'sub-menu-horizontal',
+  value: uuid(),
   children: [
     {
       value: uuid(),
-      icon: "",
-      name: "满屏",
-      handler: (draw: Draw) => {
-        draw.config.size = null;
-        draw.config.back = { color: "#fff", opacity: 1 };
-        delete draw.config.margin;
-        delete draw.config.border;
-      },
-    },
-    {
-      value: uuid(),
       icon: "A4_v",
       name: "A4竖版",
       handler: (draw: Draw) => setPaper(draw, [210, 297], 2.8),
@@ -138,7 +143,8 @@ export const paper = {
   ],
 };
 
-export const dbImage = {
+export const dbImage: MenuItem = {
+  value: uuid(),
   icon: "",
   name: "底图",
   children: [
@@ -155,13 +161,14 @@ export const dbImage = {
         } else if (info.ratio > 1) {
           proportion = { scale: info.ratio, unit: "m" };
         }
+        const url = await uploadResourse(new File([info.blob], 'map.png'))
 
         draw.history.onceTrack(() => {
           draw.addShape(
             "image",
             {
               ...size,
-              url: URL.createObjectURL(info.blob),
+              url,
               zIndex: -1,
             },
             { x: window.innerWidth / 2, y: window.innerHeight / 2 },
@@ -174,9 +181,11 @@ export const dbImage = {
   ],
 };
 
-export const test = {
-  icon: "",
+export const test: MenuItem = {
+  value: uuid(),
+  icon: "debugger",
   name: "测试",
+  type: 'sub-menu-horizontal',
   children: [
     {
       value: uuid(),

+ 4 - 2
src/example/fuse/views/slide/menu.ts

@@ -1,11 +1,13 @@
 import { toRaw } from "vue";
-import { Draw } from "../use-draw";
+import { Draw } from "../container/use-draw";
 
 export type MenuItem = {
   icon: string;
   name: string;
-  value?: string;
+  value: string;
+  type?: string
   children?: MenuItem[];
+  mount?: any,
   payload?: any;
   handler?: (draw: Draw) => void;
 };

+ 2 - 25
src/example/fuse/views/slide/slide-item.vue

@@ -2,7 +2,7 @@
   <el-sub-menu :index="data.value || data.name" v-if="data.children?.length">
     <template #title>
       <div class="menu-layout">
-        <Icon :name="data.icon" size="22px" />
+        <Icon :name="data.icon" size="24px" />
         <span>{{ data.name }}</span>
       </div>
     </template>
@@ -18,33 +18,10 @@
 
 <script lang="ts" setup>
 import { ElSubMenu, ElMenuItem } from "element-plus";
-import { MenuItem } from "@/example/fuse/views/slide/menu.ts";
+import { MenuItem } from "./menu";
 defineProps<{ data: MenuItem }>();
 </script>
 
-<style scoped lang="scss">
-.menu-layout {
-  position: relative;
-  z-index: 1;
-  width: 56px;
-  height: 56px;
-  margin: auto;
-  border-radius: 4px;
-  overflow: hidden;
-  color: #000;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  line-height: 1em;
-  font-size: 22px;
-  span {
-    font-size: 14px;
-    margin-top: 5px;
-  }
-}
-</style>
-
 <style lang="scss">
 .is-active .menu-layout {
   background: var(--el-menu-hover-bg-color);

+ 172 - 0
src/example/components/slide/slide.vue

@@ -0,0 +1,172 @@
+<template>
+  <div class="slide">
+    <el-menu
+      :default-active="active"
+      class="slide-menu"
+      @select="(val) => (active = active === val ? undefined : val)"
+      collapse
+      :popper-offset="0"
+      :popper-class="childType || 'slide-menu-poper'"
+      @open="openHandler"
+    >
+      <SlideItem v-for="menu in menus" :data="menu" />
+    </el-menu>
+
+    <div class="ext">
+      <component
+        v-if="activeMenu?.mount"
+        :is="activeMenu?.mount"
+        :draw="draw"
+        @exit="active = undefined"
+      />
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ElMenu } from "element-plus";
+import { getItem, getValue, MenuItem } from "./menu.ts";
+import SlideItem from "./slide-item.vue";
+import { computed, nextTick, ref, watch } from "vue";
+import { Draw } from "../container/use-draw.ts";
+
+const props = defineProps<{ menus: MenuItem[]; draw: Draw }>();
+const active = ref<string>();
+const activeMenu = computed(() =>
+  active.value === undefined ? null : getItem(active.value, props.menus)
+);
+
+watch(activeMenu, (menu, _, onCleanup) => {
+  if (!menu || menu.mount) return;
+  if (menu.handler) {
+    menu.handler(props.draw);
+    nextTick(() => (active.value = undefined));
+  } else {
+    props.draw.enterDrawShape(menu.payload.type, menu.payload.preset);
+    onCleanup(() => props.draw.quitDrawShape());
+  }
+});
+
+watch(
+  () => props.draw.presetAdd && getValue(props.draw.presetAdd, props.menus),
+  (val) => {
+    active.value = val;
+  }
+);
+
+const childType = ref<string>();
+const openHandler = (value: string) => {
+  const item = getItem(value, props.menus);
+  childType.value = item?.type;
+};
+</script>
+
+<style lang="scss" scoped>
+@use '../../styles/global';
+
+.slide {
+  margin-left: 0;
+  &.hide {
+    transform: translateX(-100%);
+  }
+  position: relative;
+}
+
+.slide-menu {
+  padding: 20px 0;
+  width: 100%;
+  height: 100%;
+  overflow-y: auto;
+}
+
+.ext {
+  position: absolute;
+  left: 100%;
+  top: 0;
+  bottom: 0;
+  z-index: 999;
+}
+</style>
+
+<style lang="scss">
+@use '../../styles/global';
+
+.slide-popper .el-menu--popup {
+  width: global.$slideSize;
+}
+
+.slide-popper .el-menu--popup,
+.slide-menu-poper .el-menu--popup {
+  width: global.$slideSize;
+}
+
+.slide-menu,
+.slide-menu-poper .el-menu--popup {
+  --el-menu-base-level-padding: 0;
+  --el-menu-item-height: 70px;
+
+  .el-menu-item,
+  .el-sub-menu__title {
+    background: none;
+    position: relative;
+
+    &::before {
+      content: "";
+      position: absolute;
+      width: 56px;
+      height: 56px;
+      background: var(--el-menu-hover-bg-color);
+      border-radius: 4px;
+      left: 50%;
+      top: 50%;
+      transform: translate(-50%, -50%);
+      z-index: 0;
+      opacity: 0;
+      transition: opacity 0.3s ease;
+    }
+
+    &:hover::before {
+      opacity: 1;
+    }
+  }
+}
+
+.slide-menu-poper .el-menu--popup {
+  min-width: auto;
+}
+
+.slide-menu .menu-layout,
+.slide-menu-poper .menu-layout {
+  position: relative;
+  z-index: 1;
+  width: 56px;
+  height: 56px;
+  margin: auto;
+  border-radius: 4px;
+  overflow: hidden;
+  color: #000;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  line-height: 1em;
+  font-size: 22px;
+  span {
+    font-size: 14px;
+    margin-top: 5px;
+  }
+}
+
+.sub-menu-horizontal .el-menu--popup {
+  min-width: auto;
+  .menu-layout {
+    font-size: 32px;
+    display: flex;
+    align-items: center;
+    span {
+      margin-left: 5px;
+      font-size: 14px;
+    }
+  }
+}
+</style>

+ 1 - 1
src/example/fuse/dialog/ai/ai.vue

@@ -23,7 +23,7 @@ import {
   Scene,
   SCENE_TYPE,
   taggingGets,
-} from "../../platform-resource";
+} from "../../platform/platform-resource";
 import { AIExposeData } from ".";
 
 const scene = ref<Scene>();

src/example/fuse/dialog/ai/index.ts → src/example/dialog/ai/index.ts


src/example/fuse/dialog/basemap/gd-map/selectAMapImage.vue → src/example/dialog/basemap/gd-map/selectAMapImage.vue


src/example/fuse/dialog/basemap/index.ts → src/example/dialog/basemap/index.ts


src/example/fuse/dialog/dialog.vue → src/example/dialog/dialog.vue


src/example/fuse/dialog/vr/index.ts → src/example/dialog/vr/index.ts


+ 1 - 1
src/example/fuse/dialog/vr/test.ts

@@ -1,4 +1,4 @@
-import { SCENE_TYPE } from "../../platform-resource";
+import { SCENE_TYPE } from "../../platform/platform-resource";
 
 let testToken =
   "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMzYzMTI2MjkyNiIsImxvZ2luVHlwZSI6InVzZXIiLCJ1c2VyTmFtZSI6IjEzNjMxMjYyOTI2IiwiaWF0IjoxNzQ0MDc2NDg5LCJqdGkiOiI5MTczYTYzOC00ZjcyLTRlMDgtOWNmMy1lMjA5OWQ4YWU0ZjUifQ.A6wuCF7XO-RK1d-cpePt9j6z_R96_JELZUbkvMnBJ_Y";

+ 1 - 1
src/example/fuse/dialog/vr/vr.vue

@@ -26,7 +26,7 @@
 import { ElSelect, ElOption } from "element-plus";
 import { computed, ref } from "vue";
 import { asyncTimeout } from "@/utils/shared";
-import { Scene, SCENE_TYPE, SceneTypeNames } from "../../platform-resource";
+import { Scene, SCENE_TYPE, SceneTypeNames } from "../../platform/platform-resource";
 import { getMeshSceneList } from "./test";
 
 const props = defineProps<{ value?: Scene }>();

+ 7 - 0
src/example/env.ts

@@ -0,0 +1,7 @@
+const urlParams = new URLSearchParams(location.search);
+
+export const params = {} as { id: string };
+
+for (const [name, value] of urlParams.entries()) {
+  (params as any)[name] = value
+}

+ 3 - 2
src/example/fuse/App.vue

@@ -1,11 +1,12 @@
 <template>
   <ElConfigProvider :locale="zhCn">
-    <Home />
+    <router-view v-slot="{ Component }">
+      <component :is="Component" />
+    </router-view>
   </ElConfigProvider>
 </template>
 
 <script setup lang="ts">
 import { ElConfigProvider } from "element-plus";
 import zhCn from "element-plus/es/locale/lang/zh-cn.mjs";
-import Home from "./views/home.vue";
 </script>

+ 10 - 1
src/example/fuse/main.ts

@@ -1,12 +1,21 @@
 import { createApp } from 'vue'
-import './styles/global.scss'
+import '../styles/global.scss'
 import 'element-plus/theme-chalk/src/index.scss'
 import App from './App.vue'
 import 'virtual:svg-icons-register'
 import Icon from '@/components/icon/index.vue'
+import {router} from "./router";
+import { params } from '../env'
+
+if (!params.id) {
+  alert('图纸不存在!')
+  router.back()
+  throw '图纸不存在!'
+}
 
 const app = createApp(App)
 app.component('Icon', Icon)
+app.use(router)
 app.mount('#app')
 
 console.log('当前版本', window.__VERSION__)

+ 48 - 0
src/example/fuse/req.ts

@@ -0,0 +1,48 @@
+import { StoreData } from '@/core/store/store';
+
+export const getOverviewData = async () => {
+  const storeStr = localStorage.getItem("draw-data");
+  const store = (storeStr ? JSON.parse(storeStr) : {}) as StoreData
+
+  const vportStr = localStorage.getItem("view-port");
+  const vport = (vportStr ? JSON.parse(vportStr) : null) as number[] | null;
+
+  return {
+    store,
+    viewport: vport
+  }
+}
+
+export const saveOverviewData = async (data: {store: StoreData, viewport: number[] | null}) => {
+  localStorage.setItem("draw-data", JSON.stringify(data.store));
+  localStorage.setItem("view-port", JSON.stringify(data.viewport));
+}
+
+export const getTabulationData = async () => {
+  const storeStr = localStorage.getItem("tab-draw-data");
+  const store = (storeStr ? JSON.parse(storeStr) : {}) as StoreData
+
+  const vportStr = localStorage.getItem("tab-view-port");
+  const vport = (vportStr ? JSON.parse(vportStr) : null) as number[] | null;
+
+  return {
+    store,
+    cover: tabCoverUrl,
+    viewport: vport
+  }
+}
+
+export const saveTabulationData = async (data: {store: StoreData, viewport: number[] | null}) => {
+  localStorage.setItem("tab-draw-data", JSON.stringify(data.store));
+  localStorage.setItem("tab-view-port", JSON.stringify(data.viewport));
+}
+
+let tabCoverUrl: string | null = null
+export const saveTabulationCover = async (url: string) => {
+  tabCoverUrl = url
+}
+
+export const uploadResourse = async (file: File) => {
+  return URL.createObjectURL(file);
+};
+

+ 22 - 0
src/example/fuse/router.ts

@@ -0,0 +1,22 @@
+import { createRouter, createWebHashHistory } from "vue-router";
+
+import Overview from "./views/overview/index.vue";
+import Tabulation from "./views/tabulation/index.vue";
+import { watchEffect } from "vue";
+
+export const history = createWebHashHistory();
+export const router = createRouter({
+  history,
+  routes: [
+    { path: "/overview", name: "overview", component: Overview },
+    { path: "/tabulation", name: "tabulation", component: Tabulation },
+  ],
+});
+
+setTimeout(() => {
+  watchEffect(() => {
+    if (!router.currentRoute.value.name) {
+      router.replace({ name: "overview" });
+    }
+  });
+}, 1000);

+ 0 - 173
src/example/fuse/views/header/header.vue

@@ -1,173 +0,0 @@
-<template>
-  <div class="header">
-    <div class="nav">
-      <el-button type="primary" plain>返回</el-button>
-    </div>
-    <div class="draw-operate">
-      <div>
-        <span
-          class="operate"
-          @click="draw.history.undo()"
-          :class="{ disabled: !draw.history.hasUndo.value }"
-        >
-          <Icon name="undo" tip="撤销" />
-        </span>
-        <span
-          class="operate"
-          @click="draw.history.redo()"
-          :class="{ disabled: !draw.history.hasRedo.value }"
-        >
-          <Icon name="redo" tip="重做" />
-        </span>
-      </div>
-      <div>
-        <span class="operate" @click="draw.store.clear()">
-          <Icon name="clear" tip="清除" />
-        </span>
-        <span class="operate" @click="rotateView">
-          <Icon name="rotate" tip="旋转" />
-        </span>
-        <span class="operate" @click="emit('full')">
-          <Icon name="a_adapt" tip="全屏" />
-        </span>
-        <span class="operate" @click="emit('full')">
-          <Icon name="a-visible" tip="显示" />
-        </span>
-      </div>
-      <div>
-        <span class="operate" @click="selectVRHandler">
-          <Icon name="VR" tip="VR参考" />
-        </span>
-      </div>
-    </div>
-    <div class="saves">
-      <el-dropdown>
-        <el-button type="primary" plain>导出</el-button>
-        <template #dropdown>
-          <el-dropdown-menu>
-            <el-dropdown-item @click="emit('expose', 'image/jpeg')">JPG</el-dropdown-item>
-            <el-dropdown-item @click="emit('expose', 'image/png')">PNG </el-dropdown-item>
-            <el-dropdown-item @click="emit('expose', 'DXF')">DXF</el-dropdown-item>
-          </el-dropdown-menu>
-        </template>
-      </el-dropdown>
-      <el-button type="primary" @click="emit('save')" plain>保存</el-button>
-      <el-button>图纸</el-button>
-    </div>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { ElButton, ElDropdown, ElDropdownItem, ElDropdownMenu } from "element-plus";
-import { useDraw } from "../use-draw.ts";
-import { animation } from "@/core/hook/use-animation.ts";
-import { selectScene } from "@/example/fuse/dialog/vr/index.ts";
-import { selectAI } from "@/example/fuse/dialog/ai/index.ts";
-import { Scene } from "../../platform-resource.ts";
-import { drawPlatformResource } from "../../platform-draw.ts";
-
-const draw = useDraw();
-
-const emit = defineEmits<{
-  (e: "full"): void;
-  (e: "save"): any;
-  (e: "expose", format: string): void;
-  (e: "selectVR", v: Scene): void;
-}>();
-
-const setBGImage = (file: File) => {
-  draw.addShape(
-    "image",
-    {
-      width: window.innerWidth,
-      height: window.innerHeight,
-      url: URL.createObjectURL(file),
-      zIndex: -1,
-    },
-    { x: window.innerWidth / 2, y: window.innerHeight / 2 },
-    true
-  );
-};
-
-const rotateView = () => {
-  const dom = draw.stage!.container();
-  let rotated = 0;
-  animation({ rotation: 0 }, { rotation: Math.PI / 2 }, ({ rotation }) => {
-    draw.viewer.rotatePixel(
-      { x: dom.offsetWidth / 2, y: dom.offsetHeight / 2 },
-      rotation - rotated
-    );
-    rotated = rotation;
-  });
-};
-
-const selectVRHandler = async () => {
-  const scene = await selectScene();
-  emit("selectVR", scene);
-};
-
-const aiHandler = async () => {
-  const aiData = await selectAI();
-  drawPlatformResource(aiData, draw);
-};
-</script>
-
-<style lang="scss" scoped>
-@use 'element-plus/theme-chalk/src/common/var';
-
-.header {
-  background-color: var.$color-primary;
-  display: flex;
-  align-items: center;
-  padding: 10px;
-  justify-content: space-between;
-}
-
-.draw-operate {
-  text-align: center;
-  color: #fff;
-  display: flex;
-  align-items: center;
-  font-size: 20px;
-
-  > div:not(:last-child) {
-    padding-right: 15px;
-    margin-right: 15px;
-    position: relative;
-    &::after {
-      content: "";
-      right: 0;
-      top: 50%;
-      transform: translateY(-50%);
-      position: absolute;
-      height: 1em;
-      width: 1px;
-      background: currentColor;
-    }
-  }
-
-  i {
-    width: auto;
-  }
-
-  .operate {
-    margin: 0 11px;
-    display: inline-flex;
-    align-items: center;
-    padding: 4px;
-    flex-direction: row-reverse;
-    border-radius: 4px;
-    justify-content: center;
-
-    &.disabled {
-      pointer-events: none;
-      opacity: 0.7;
-    }
-  }
-}
-
-.file-input {
-  position: absolute;
-  visibility: hidden;
-}
-</style>

+ 0 - 132
src/example/fuse/views/home.vue

@@ -1,132 +0,0 @@
-<template>
-  <div class="layout" :class="{ full }">
-    <Header
-      class="header"
-      v-if="draw"
-      @full="fullHandler"
-      @expose="exposeHandler"
-      @selectVR="(scene) => (vrScene = scene)"
-      @save="saveHandler"
-    />
-    <div class="container">
-      <Slide class="slide" v-if="draw" />
-      <div class="content" ref="drawEle">
-        <DrawBoard v-if="drawEle" :handler-resource="uploadResourse" ref="draw" />
-        <ShowVR :scene="vrScene" v-if="vrScene" ref="vr" @close="vrScene = undefined" />
-      </div>
-    </div>
-  </div>
-  <Dialog />
-</template>
-
-<script lang="ts" setup>
-import Header from "./header/header.vue";
-import ShowVR from "./show-vr.vue";
-import Slide from "./slide/slide.vue";
-import Dialog from "../dialog/dialog.vue";
-import { onUnmounted, ref, watch } from "vue";
-import { DrawExpose, DrawBoard } from "@/index";
-import { installDraw } from "./use-draw.ts";
-import { listener } from "@/utils/event.ts";
-import { ElMessage } from "element-plus";
-import { startAnimation } from "@/utils/shared.ts";
-import { saveData, getData, uploadResourse, getViewport, saveViewport } from "./req.ts";
-import { Scene } from "../platform-resource.ts";
-import saveAs from "@/utils/file-serve.ts";
-
-const drawEle = ref<HTMLDivElement | null>(null);
-const draw = ref<DrawExpose>();
-installDraw(draw);
-
-const initData = async () => {
-  const data = await getData();
-  const viewport = await getViewport();
-  draw.value?.store.setStore(data);
-  viewport && draw.value?.viewer.setViewMat(viewport);
-};
-initData();
-
-const saveHandler = () => {
-  saveData(draw.value!.getData());
-  saveViewport(draw.value!.viewer.transform.m);
-};
-
-const vr = ref();
-const full = ref(false);
-watch(full, (_f1, _f2, onCleanup) => {
-  onCleanup(
-    startAnimation(() => {
-      draw.value?.updateSize();
-      vr.value?.refresh();
-    }, 400)
-  );
-});
-const fullHandler = () => {
-  full.value = true;
-  ElMessage.warning({ message: "按ESC键可退出全屏模式", duration: 3000 });
-};
-onUnmounted(
-  listener(document.documentElement, "keyup", (ev) => {
-    if (ev.key === "Escape") {
-      full.value = false;
-    }
-  })
-);
-
-const vrScene = ref<Scene>();
-
-const exposeHandler = (format: string) => {
-  const dataURL = draw.value!.stage!.toDataURL({
-    pixelRatio: 4,
-    mimeType: format,
-    quality: 1,
-  });
-  saveAs(dataURL, "canvas.png");
-};
-</script>
-
-<style lang="scss" scoped>
-@use '../styles/global';
-
-.layout {
-  display: flex;
-  flex-direction: column;
-  align-items: stretch;
-  height: 100vh;
-  background: #f0f2f5;
-  --top: 0px;
-  --left: 0px;
-  overflow: hidden;
-
-  .header {
-    margin-top: var(--top);
-    transition: margin-top 0.3s ease;
-    height: global.$headerSize;
-    flex: 0 0 auto;
-  }
-
-  .container {
-    flex: 1;
-    display: flex;
-    align-items: stretch;
-  }
-
-  .slide {
-    flex: 0 0 auto;
-    width: global.$slideSize;
-    margin-left: var(--left);
-    overflow-y: auto;
-    background: #fff;
-    transition: margin-left 0.3s ease;
-  }
-
-  .content {
-    position: relative;
-    width: calc(100% - 70px - var(--left));
-  }
-  &.full {
-    --top: calc(-1 * #{global.$headerSize});
-    --left: calc(-1 * #{global.$slideSize});
-  }
-}
-</style>

+ 60 - 0
src/example/fuse/views/overview/header.vue

@@ -0,0 +1,60 @@
+<template>
+  <Header :action-groups="actions" title="绘图" no-back>
+    <template #saves>
+      <el-button type="primary" @click="saveHandler">保存</el-button>
+      <el-button @click="gotoTabulation" color="#E6E6E6">图纸</el-button>
+    </template>
+  </Header>
+</template>
+
+<script lang="ts" setup>
+import Header from "../../../components/header/index.vue";
+import { ElButton } from "element-plus";
+import { useDraw } from "../../../components/container/use-draw.ts";
+import { selectScene } from "../../../dialog/vr/index.ts";
+import { Scene } from "../../../platform/platform-resource.ts";
+import { getHeaderActions, getImage } from "../../../components/header/actions.ts";
+import { saveOverviewData, saveTabulationCover, uploadResourse } from "../../req.ts";
+import { router } from "../../router.ts";
+import { params } from "@/example/env.ts";
+
+const draw = useDraw();
+
+const emit = defineEmits<{
+  (e: "selectVR", v: Scene): void;
+}>();
+
+const baseActions = getHeaderActions(draw);
+const actions = [
+  [baseActions.undo, baseActions.redo],
+  [
+    baseActions.clear,
+    baseActions.rotateView,
+    baseActions.initViewport,
+    baseActions.toggleShow,
+  ],
+  [
+    {
+      handler: async () => {
+        const scene = await selectScene();
+        emit("selectVR", scene);
+      },
+      text: "VR参考",
+      icon: "VR",
+    },
+  ],
+  [baseActions.expose],
+];
+
+const saveHandler = async () => {
+  await saveOverviewData({ store: draw!.getData(), viewport: draw!.viewer.transform.m });
+  const blob = await getImage(draw, "image/png");
+  const url = await uploadResourse(new File([blob], `tabulation-cover-${params.id}.png`));
+  saveTabulationCover(url);
+};
+
+const gotoTabulation = async () => {
+  await saveHandler();
+  router.push({ name: "tabulation" });
+};
+</script>

+ 46 - 0
src/example/fuse/views/overview/index.vue

@@ -0,0 +1,46 @@
+<template>
+  <Container
+    :upload-resourse="uploadResourse"
+    v-model:full="full"
+    :ref="(d: any) => (draw = d?.draw)"
+  >
+    <template #header>
+      <Header @selectVR="(scene) => (vrScene = scene)" />
+    </template>
+    <template #slide>
+      <Slide />
+    </template>
+    <template #cover>
+      <ShowVR :scene="vrScene" v-if="vrScene" ref="vr" @close="vrScene = undefined" />
+    </template>
+  </Container>
+
+  <Dialog />
+</template>
+
+<script lang="ts" setup>
+import Header from "./header.vue";
+import Slide from "./slide.vue";
+import Container from "../../../components/container/container.vue";
+import ShowVR from "../../../components/show-vr.vue";
+import { getOverviewData, uploadResourse } from "../../req";
+import { ref, watch } from "vue";
+import { Draw } from "../../../components/container/use-draw";
+import { Scene } from "../../../platform/platform-resource";
+import Dialog from "../../../dialog/dialog.vue";
+
+const full = ref(false);
+const draw = ref<Draw>();
+const vrScene = ref<Scene>();
+
+const init = async (draw: Draw) => {
+  draw.config.showCompass = true;
+  draw.config.showLabelLine = true;
+  draw.config.showComponentSize = true;
+
+  const data = await getOverviewData();
+  draw.store.setStore(data.store);
+  data.viewport && draw.viewer.setViewMat(data.viewport);
+};
+watch(draw, (draw) => draw && init(draw));
+</script>

+ 223 - 0
src/example/fuse/views/overview/slide-icons.vue

@@ -0,0 +1,223 @@
+<template>
+  <ElCollapse class="icon-layout" v-model="activeGroups">
+    <ElCollapseItem
+      v-for="group in searchGroups"
+      :name="group.name"
+      v-if="searchGroups.length"
+    >
+      <template #title>
+        <h2>{{ group.name }}</h2>
+      </template>
+
+      <div class="type-children" v-for="typeChildren in group.children">
+        <h3 v-if="typeChildren.name">{{ typeChildren.name }}</h3>
+        <div class="icon-items">
+          <div
+            v-for="item in typeChildren.children"
+            @click="drawIcon(`/icons/${item.icon}.svg`)"
+          >
+            <Icon :name="item.icon" size="32px" />
+            <span>{{ item.name }}</span>
+          </div>
+        </div>
+      </div>
+    </ElCollapseItem>
+    <el-empty description="暂无数据" v-else />
+  </ElCollapse>
+</template>
+
+<script lang="ts" setup>
+import { themeColor } from "@/constant/help-style";
+import { computed, ref } from "vue";
+import { ElCollapse, ElCollapseItem, ElEmpty } from "element-plus";
+import { getSvgContent, parseSvgContent } from "@/utils/resource";
+import { Draw } from "../../../components/container/use-draw.ts";
+
+const props = defineProps<{ draw: Draw }>();
+const emit = defineEmits<{ (e: "exit"): void }>();
+
+const drawIcon = async (url: string) => {
+  const svgContent = parseSvgContent(await getSvgContent(url));
+  const addWidth = 100;
+  const addHeight = (addWidth / svgContent.width) * svgContent.height;
+
+  props.draw.enterDrawShape("icon", {
+    url,
+    width: addWidth,
+    height: addHeight,
+    fill: themeColor,
+  });
+  emit("exit");
+};
+
+const groups = [
+  {
+    name: "常用名称",
+    children: [
+      {
+        name: "门",
+        children: [
+          { icon: "cad-men", name: "门" },
+          { icon: "cad-shuangkaimen", name: "双开门" },
+          { icon: "cad-yimen", name: "移门" },
+          { icon: "cad-yakou", name: "哑口" },
+        ],
+      },
+      {
+        name: "窗",
+        children: [
+          { icon: "cad-chuang", name: "窗" },
+          { icon: "cad-piaochuang", name: "飘窗" },
+          { icon: "cad-luodichuang", name: "落地窗" },
+        ],
+      },
+      {
+        name: "构件",
+        children: [
+          { icon: "cad-zhuzi", name: "柱子" },
+          { icon: "cad-yandao", name: "烟道" },
+          { icon: "cad-loudao", name: "楼道" },
+        ],
+      },
+    ],
+  },
+  {
+    name: "家具",
+    children: [
+      {
+        name: "客餐厅",
+        children: [
+          { icon: "TV", name: "电视柜" },
+          { icon: "CombinationSofa", name: "组合沙发" },
+          { icon: "SingleSofa", name: "单人沙发" },
+          { icon: "TeaTable", name: "茶几" },
+          { icon: "Carpet", name: "地毯" },
+          { icon: "Plant", name: "植物" },
+          { icon: "DiningTable", name: "餐桌" },
+        ],
+      },
+      {
+        name: "卧室",
+        children: [
+          { icon: "DoubleBed", name: "双人床" },
+          { icon: "SingleBed", name: "单人床" },
+          { icon: "Wardrobe", name: "衣柜" },
+          { icon: "Dresser", name: "梳妆台" },
+          { icon: "BedsideCupboard", name: "床头柜" },
+          { icon: "Pillow", name: "抱枕" },
+        ],
+      },
+      {
+        name: "厨卫",
+        children: [
+          { icon: "GasStove", name: "燃气灶" },
+          { icon: "Cupboard", name: "橱柜" },
+          { icon: "Bathtub", name: "浴缸" },
+          { icon: "Closestool", name: "马桶" },
+          { icon: "Washstand", name: "洗漱台" },
+        ],
+      },
+      {
+        name: "其他",
+        children: [
+          { icon: "Desk", name: "书桌" },
+          { icon: "BalconyChair", name: "阳台椅" },
+          { icon: "Elevator", name: "电梯" },
+        ],
+      },
+    ],
+  },
+  {
+    name: "痕迹物证",
+    children: [
+      {
+        name: "",
+        children: [
+          { icon: "zhiwen_o", name: "手印" },
+          { icon: "zuozuji_o", name: "脚印" },
+          { icon: "youzuji_o", name: "脚印" },
+          { icon: "xieyin_o", name: "鞋印" },
+          { icon: "chelunhenji_o", name: "车轮印" },
+          { icon: "dantou_o", name: "弹头" },
+          { icon: "danke_o", name: "弹壳" },
+          { icon: "shouqiang_o", name: "手枪" },
+          { icon: "buqiang_o", name: "步枪" },
+          { icon: "xuepo_o", name: "血泊" },
+          { icon: "xueji_o", name: "血迹" },
+          { icon: "shitiz_o", name: "尸体正面" },
+          { icon: "shitib_o", name: "尸体背面" },
+          { icon: "shitifuhao_o", name: "尸体" },
+        ],
+      },
+    ],
+  },
+];
+
+const activeGroups = ref(groups.map((item) => item.name));
+const keyword = ref("");
+const searchGroups = computed(() => {
+  return groups
+    .map((typeChildren) => {
+      const filterTypeChildren = typeChildren.children
+        .map((type) => {
+          const children = type.children.filter((item) =>
+            item.name.includes(keyword.value)
+          );
+          return {
+            ...type,
+            children,
+          };
+        })
+        .filter((type) => type.children.length > 0);
+      return {
+        ...typeChildren,
+        children: filterTypeChildren,
+      };
+    })
+    .filter((typeChildren) => typeChildren.children.length > 0);
+});
+</script>
+
+<style lang="scss" scoped>
+.icon-layout {
+  width: 320px;
+  height: 100%;
+  background-color: var(--el-menu-bg-color);
+  border-right: solid 1px var(--el-menu-border-color);
+  overflow-y: auto;
+  padding: 0 20px;
+  font-size: 14px;
+  color: #333;
+
+  h2,
+  h3 {
+    font-size: inherit;
+    color: inherit;
+  }
+
+  // .type-children:not(:first-child) {
+  //   margin-top: 20px;
+  // }
+  h3 {
+    margin-bottom: 20px;
+  }
+}
+
+.icon-items {
+  display: flex;
+  flex-wrap: wrap;
+
+  > div {
+    width: 25%;
+    text-align: center;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 20px;
+    span {
+      margin-top: 10px;
+    }
+  }
+}
+</style>

+ 31 - 0
src/example/fuse/views/overview/slide.vue

@@ -0,0 +1,31 @@
+<template>
+  <Slide :menus="menus" :draw="draw" />
+</template>
+
+<script lang="ts" setup>
+import Slide from "../../../components/slide/slide.vue";
+import Icons from "./slide-icons.vue";
+import { v4 as uuid } from "uuid";
+import {
+  draw as drawAction,
+  text,
+  test,
+  imp,
+} from "../../../components/slide/actions.ts";
+import { useDraw } from "../../../components/container/use-draw.ts";
+import { markRaw, reactive } from "vue";
+
+const legend = {
+  icon: "legend",
+  name: "图例",
+  value: uuid(),
+  mount: markRaw(Icons),
+};
+
+const draw = useDraw();
+const menus = reactive([imp, drawAction, text, legend]);
+
+if (import.meta.env.DEV) {
+  menus.push(test);
+}
+</script>

+ 0 - 25
src/example/fuse/views/req.ts

@@ -1,25 +0,0 @@
-import { StoreData } from '@/core/store/store';
-import { initData } from './test'
-
-export const getData = async () => {
-  const dataStr = localStorage.getItem("draw-data");
-  return (dataStr ? JSON.parse(dataStr) : initData) as StoreData;
-}
-
-export const saveData = async (data: StoreData) => {
-  localStorage.setItem("draw-data", JSON.stringify(data));
-}
-
-
-export const getViewport = async () => {
-  const dataStr = localStorage.getItem("view-port");
-  return (dataStr ? JSON.parse(dataStr) : null) as number[] | null;
-}
-
-export const saveViewport = async (viewport: number[]) => {
-  localStorage.setItem("view-port", JSON.stringify(viewport));
-}
-
-export const uploadResourse = async (file: File) => {
-  return URL.createObjectURL(file);
-};

+ 0 - 115
src/example/fuse/views/slide/slide.vue

@@ -1,115 +0,0 @@
-<template>
-  <div class="slide">
-    <el-menu
-      :default-active="active"
-      class="slide-menu"
-      @select="selectHandler"
-      collapse
-      :popper-offset="0"
-      popper-class="slide-menu-poper"
-    >
-      <SlideItem v-for="menu in menus" :data="menu" />
-    </el-menu>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { ElMenu } from "element-plus";
-import { getItem, getValue, MenuItem } from "./menu.ts";
-import { draw as drawAction, text, test, legend, table } from "./actions.ts";
-import SlideItem from "./slide-item.vue";
-import { useDraw } from "../use-draw.ts";
-import { nextTick, reactive, ref, watch } from "vue";
-
-const draw = useDraw();
-const active = ref<string>();
-const menus: MenuItem[] = reactive([drawAction, text, legend, table]);
-
-if (import.meta.env.DEV) {
-  menus.push(test);
-}
-
-watch(
-  () => draw?.presetAdd && getValue(draw?.presetAdd, menus),
-  (val) => {
-    active.value = val;
-  }
-);
-
-const selectHandler = (value: string) => {
-  const item = getItem(value, menus);
-  if (item?.handler) {
-    active.value = item.value;
-    item.handler(draw);
-    nextTick(() => (active.value = undefined));
-  } else if (item?.payload) {
-    draw.enterDrawShape(item.payload.type, item.payload.preset);
-  }
-};
-</script>
-
-<style lang="scss" scoped>
-@use '../../styles/global';
-
-.slide {
-  margin-left: 0;
-
-  &.hide {
-    transform: translateX(-100%);
-  }
-}
-
-.slide-menu {
-  width: 100%;
-  height: 100%;
-  overflow-y: auto;
-}
-</style>
-
-<style lang="scss">
-@use '../../styles/global';
-
-.slide-popper .el-menu--popup {
-  width: global.$slideSize;
-}
-
-.slide-popper .el-menu--popup,
-.slide-menu-poper .el-menu--popup {
-  width: global.$slideSize;
-}
-
-.slide-menu,
-.slide-menu-poper .el-menu--popup {
-  --el-menu-base-level-padding: 0;
-  --el-menu-item-height: 70px;
-
-  .el-menu-item,
-  .el-sub-menu__title {
-    background: none;
-    position: relative;
-
-    &::before {
-      content: "";
-      position: absolute;
-      width: 56px;
-      height: 56px;
-      background: var(--el-menu-hover-bg-color);
-      border-radius: 4px;
-      left: 50%;
-      top: 50%;
-      transform: translate(-50%, -50%);
-      z-index: 0;
-      opacity: 0;
-      transition: opacity 0.3s ease;
-    }
-
-    &:hover::before {
-      opacity: 1;
-    }
-  }
-}
-
-.slide-menu-poper .el-menu--popup {
-  min-width: auto;
-}
-</style>

+ 37 - 0
src/example/fuse/views/tabulation/header.vue

@@ -0,0 +1,37 @@
+<template>
+  <Header :action-groups="actions" title="图纸">
+    <template #saves>
+      <el-button type="primary" @click="saveHandler">保存</el-button>
+    </template>
+  </Header>
+</template>
+
+<script lang="ts" setup>
+import { ElButton } from "element-plus";
+import { useDraw } from "../../../components/container/use-draw.ts";
+import Header from "../../../components/header/index.vue";
+import { getHeaderActions } from "../../../components/header/actions.ts";
+import { saveTabulationData } from "../../req.ts";
+import { Transform } from "konva/lib/Util";
+
+const draw = useDraw();
+
+const baseActions = getHeaderActions(draw);
+const actions = [
+  [baseActions.undo, baseActions.redo],
+  [
+    baseActions.clear,
+    {
+      ...baseActions.initViewport,
+      handler: () => {
+        draw.viewer.setViewMat(new Transform());
+      },
+    },
+  ],
+  [baseActions.expose],
+];
+
+const saveHandler = () => {
+  saveTabulationData({ store: draw!.getData(), viewport: draw!.viewer.transform.m });
+};
+</script>

+ 153 - 0
src/example/fuse/views/tabulation/index.vue

@@ -0,0 +1,153 @@
+<template>
+  <Container
+    :upload-resourse="uploadResourse"
+    v-model:full="full"
+    :ref="(d: any) => (draw = d?.draw)"
+  >
+    <template #header>
+      <Header @selectVR="(scene) => (vrScene = scene)" />
+    </template>
+    <template #slide>
+      <Slide />
+    </template>
+    <template #cover>
+      <ShowVR :scene="vrScene" v-if="vrScene" ref="vr" @close="vrScene = undefined" />
+    </template>
+  </Container>
+
+  <Dialog />
+</template>
+
+<script lang="ts" setup>
+import Header from "./header.vue";
+import Slide from "./slide.vue";
+import Container from "../../../components/container/container.vue";
+import ShowVR from "../../../components/show-vr.vue";
+import { getTabulationData, uploadResourse } from "../../req";
+import { computed, ref, watch } from "vue";
+import { Draw } from "../../../components/container/use-draw";
+import { Scene } from "../../../platform/platform-resource";
+import Dialog from "../../../dialog/dialog.vue";
+import { paper } from "../../../components/slide/actions";
+import { getImage } from "@/utils/resource";
+import { StoreData } from "@/core/store/store";
+import { defaultLayer } from "@/constant";
+import { getBaseItem } from "@/core/components/util";
+import { getFixPosition } from "@/utils/bound";
+
+const full = ref(false);
+const draw = ref<Draw>();
+const vrScene = ref<Scene>();
+
+const margin = computed(() => {
+  let margin = draw.value!.config.margin!;
+  if (!Array.isArray(margin)) {
+    margin = [margin, margin, margin, margin];
+  }
+  return margin;
+});
+
+const addCover = async (url: string) => {
+  const image = await getImage(url);
+  const addWidth = 514;
+  const addHeight = (image.height / image.width) * 514;
+  draw.value!.addShape(
+    "image",
+    {
+      width: addWidth,
+      height: addHeight,
+      url,
+      lock: true,
+      zIndex: -1,
+    },
+    { x: margin.value[3] + 100 + addWidth / 2, y: margin.value[0] + 198 + addHeight / 2 },
+    true
+  );
+};
+
+const addDefaultData = () => {
+  const dom = draw.value!.stage!.container();
+  const nameColl = {
+    width: 140,
+    height: 40,
+    padding: 13,
+    content: "",
+    align: "center",
+    fontSize: 14,
+    fontColor: "#000000",
+  };
+  const valueColl = { ...nameColl, width: 200 };
+  const tableSize = {
+    width: nameColl.width + valueColl.width,
+    height: nameColl.height * 5,
+  };
+  const drawSize = {
+    width: dom.offsetWidth,
+    height: dom.offsetHeight,
+  };
+  const tablePos = getFixPosition(
+    { right: 40 + margin.value[1], bottom: 40 + margin.value[2] },
+    tableSize,
+    drawSize
+  );
+
+  const data: StoreData["layers"][0] = {
+    text: [
+      {
+        ...getBaseItem(),
+        content: "默认标题",
+        lock: true,
+        width: 300,
+        heihgt: 42,
+        fontSize: 38,
+        align: "center",
+        mat: [
+          1,
+          0,
+          0,
+          1,
+          (dom.offsetWidth - margin.value[3]) / 2 - 150 + margin.value[3],
+          40 + margin.value[0],
+        ],
+      },
+    ],
+    table: [
+      {
+        ...getBaseItem(),
+        lock: true,
+        content: [
+          [{ ...nameColl, content: "案发时间" }, valueColl],
+          [{ ...nameColl, content: "案发地点" }, valueColl],
+          [{ ...nameColl, content: "绘图单位" }, valueColl],
+          [{ ...nameColl, content: "绘图人" }, valueColl],
+          [{ ...nameColl, content: "绘图时间" }, valueColl],
+        ],
+        ...tableSize,
+        mat: [1, 0, 0, 1, tablePos.x, tablePos.y],
+      },
+    ],
+  };
+
+  return {
+    layers: { [defaultLayer]: data },
+  };
+};
+
+const init = async (draw: Draw) => {
+  paper.children[1].handler(draw);
+  draw.config.showCompass = true;
+  draw.config.showGrid = false;
+  draw.config.showLabelLine = false;
+  draw.config.showComponentSize = false;
+
+  const data = await getTabulationData();
+  if (!data.store.layers) {
+    draw.store.setStore(addDefaultData());
+  } else {
+    draw.store.setStore(data.store);
+    data.viewport && draw.viewer.setViewMat(data.viewport);
+  }
+  data.cover && addCover(data.cover);
+};
+watch(draw, (draw) => draw && init(draw));
+</script>

+ 223 - 0
src/example/fuse/views/tabulation/slide-icons.vue

@@ -0,0 +1,223 @@
+<template>
+  <ElCollapse class="icon-layout" v-model="activeGroups">
+    <ElCollapseItem
+      v-for="group in searchGroups"
+      :name="group.name"
+      v-if="searchGroups.length"
+    >
+      <template #title>
+        <h2>{{ group.name }}</h2>
+      </template>
+
+      <div class="type-children" v-for="typeChildren in group.children">
+        <h3 v-if="typeChildren.name">{{ typeChildren.name }}</h3>
+        <div class="icon-items">
+          <div
+            v-for="item in typeChildren.children"
+            @click="drawIcon(`/icons/${item.icon}.svg`)"
+          >
+            <Icon :name="item.icon" size="32px" />
+            <span>{{ item.name }}</span>
+          </div>
+        </div>
+      </div>
+    </ElCollapseItem>
+    <el-empty description="暂无数据" v-else />
+  </ElCollapse>
+</template>
+
+<script lang="ts" setup>
+import { themeColor } from "@/constant/help-style";
+import { computed, ref } from "vue";
+import { ElCollapse, ElCollapseItem, ElEmpty } from "element-plus";
+import { getSvgContent, parseSvgContent } from "@/utils/resource";
+import { Draw } from "../../../components/container/use-draw.ts";
+
+const props = defineProps<{ draw: Draw }>();
+const emit = defineEmits<{ (e: "exit"): void }>();
+
+const drawIcon = async (url: string) => {
+  const svgContent = parseSvgContent(await getSvgContent(url));
+  const addWidth = 100;
+  const addHeight = (addWidth / svgContent.width) * svgContent.height;
+
+  props.draw.enterDrawShape("icon", {
+    url,
+    width: addWidth,
+    height: addHeight,
+    fill: themeColor,
+  });
+  emit("exit");
+};
+
+const groups = [
+  {
+    name: "常用名称",
+    children: [
+      {
+        name: "门",
+        children: [
+          { icon: "cad-men", name: "门" },
+          { icon: "cad-shuangkaimen", name: "双开门" },
+          { icon: "cad-yimen", name: "移门" },
+          { icon: "cad-yakou", name: "哑口" },
+        ],
+      },
+      {
+        name: "窗",
+        children: [
+          { icon: "cad-chuang", name: "窗" },
+          { icon: "cad-piaochuang", name: "飘窗" },
+          { icon: "cad-luodichuang", name: "落地窗" },
+        ],
+      },
+      {
+        name: "构件",
+        children: [
+          { icon: "cad-zhuzi", name: "柱子" },
+          { icon: "cad-yandao", name: "烟道" },
+          { icon: "cad-loudao", name: "楼道" },
+        ],
+      },
+    ],
+  },
+  {
+    name: "家具",
+    children: [
+      {
+        name: "客餐厅",
+        children: [
+          { icon: "TV", name: "电视柜" },
+          { icon: "CombinationSofa", name: "组合沙发" },
+          { icon: "SingleSofa", name: "单人沙发" },
+          { icon: "TeaTable", name: "茶几" },
+          { icon: "Carpet", name: "地毯" },
+          { icon: "Plant", name: "植物" },
+          { icon: "DiningTable", name: "餐桌" },
+        ],
+      },
+      {
+        name: "卧室",
+        children: [
+          { icon: "DoubleBed", name: "双人床" },
+          { icon: "SingleBed", name: "单人床" },
+          { icon: "Wardrobe", name: "衣柜" },
+          { icon: "Dresser", name: "梳妆台" },
+          { icon: "BedsideCupboard", name: "床头柜" },
+          { icon: "Pillow", name: "抱枕" },
+        ],
+      },
+      {
+        name: "厨卫",
+        children: [
+          { icon: "GasStove", name: "燃气灶" },
+          { icon: "Cupboard", name: "橱柜" },
+          { icon: "Bathtub", name: "浴缸" },
+          { icon: "Closestool", name: "马桶" },
+          { icon: "Washstand", name: "洗漱台" },
+        ],
+      },
+      {
+        name: "其他",
+        children: [
+          { icon: "Desk", name: "书桌" },
+          { icon: "BalconyChair", name: "阳台椅" },
+          { icon: "Elevator", name: "电梯" },
+        ],
+      },
+    ],
+  },
+  {
+    name: "痕迹物证",
+    children: [
+      {
+        name: "",
+        children: [
+          { icon: "zhiwen_o", name: "手印" },
+          { icon: "zuozuji_o", name: "脚印" },
+          { icon: "youzuji_o", name: "脚印" },
+          { icon: "xieyin_o", name: "鞋印" },
+          { icon: "chelunhenji_o", name: "车轮印" },
+          { icon: "dantou_o", name: "弹头" },
+          { icon: "danke_o", name: "弹壳" },
+          { icon: "shouqiang_o", name: "手枪" },
+          { icon: "buqiang_o", name: "步枪" },
+          { icon: "xuepo_o", name: "血泊" },
+          { icon: "xueji_o", name: "血迹" },
+          { icon: "shitiz_o", name: "尸体正面" },
+          { icon: "shitib_o", name: "尸体背面" },
+          { icon: "shitifuhao_o", name: "尸体" },
+        ],
+      },
+    ],
+  },
+];
+
+const activeGroups = ref(groups.map((item) => item.name));
+const keyword = ref("");
+const searchGroups = computed(() => {
+  return groups
+    .map((typeChildren) => {
+      const filterTypeChildren = typeChildren.children
+        .map((type) => {
+          const children = type.children.filter((item) =>
+            item.name.includes(keyword.value)
+          );
+          return {
+            ...type,
+            children,
+          };
+        })
+        .filter((type) => type.children.length > 0);
+      return {
+        ...typeChildren,
+        children: filterTypeChildren,
+      };
+    })
+    .filter((typeChildren) => typeChildren.children.length > 0);
+});
+</script>
+
+<style lang="scss" scoped>
+.icon-layout {
+  width: 320px;
+  height: 100%;
+  background-color: var(--el-menu-bg-color);
+  border-right: solid 1px var(--el-menu-border-color);
+  overflow-y: auto;
+  padding: 0 20px;
+  font-size: 14px;
+  color: #333;
+
+  h2,
+  h3 {
+    font-size: inherit;
+    color: inherit;
+  }
+
+  // .type-children:not(:first-child) {
+  //   margin-top: 20px;
+  // }
+  h3 {
+    margin-bottom: 20px;
+  }
+}
+
+.icon-items {
+  display: flex;
+  flex-wrap: wrap;
+
+  > div {
+    width: 25%;
+    text-align: center;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 20px;
+    span {
+      margin-top: 10px;
+    }
+  }
+}
+</style>

+ 19 - 0
src/example/fuse/views/tabulation/slide.vue

@@ -0,0 +1,19 @@
+<template>
+  <Slide :menus="menus" :draw="draw" />
+</template>
+
+<script lang="ts" setup>
+import Slide from "../../../components/slide/slide.vue";
+import Icons from "./slide-icons.vue";
+import { v4 as uuid } from "uuid";
+import { paper, text, serial, table, test } from "../../../components/slide/actions.ts";
+import { useDraw } from "../../../components/container/use-draw.ts";
+import { reactive } from "vue";
+
+const draw = useDraw();
+const menus = reactive([paper, text, serial, table]);
+
+if (import.meta.env.DEV) {
+  menus.push(test);
+}
+</script>

+ 0 - 700
src/example/fuse/views/test.ts

@@ -1,700 +0,0 @@
-export const initData = {
-  layers: {
-    default: {
-      image: [
-        {
-          id: "10cc6b4dasdasd",
-          createTime: 1,
-          zIndex: -194,
-          url: "/WX20241213-205427@2x.png",
-          mat: [
-            0.11196146764392557, 0, 0, 0.11196146764392542, 115.58984375,
-            243.9711189430529,
-          ],
-        },
-        {
-          id: "10cc6b4d-e90a-47c2-ae3f-9ab3a21190e7",
-          createTime: 1736762304345,
-          zIndex: -1,
-          opacity: 1,
-          ref: false,
-          width: 1000,
-          height: 1000,
-          url: "/五楼.png",
-          mat: [1, 0, 0, 1, 591, 436],
-          lock: true,
-        },
-      ],
-      rectangle: [
-        {
-          id: "0",
-          createTime: 1,
-          zIndex: 0,
-          attitude: [
-            8.251736904074757e-17, 1.347610904600402, -1.476797504076872,
-            -1.5348915434824124e-13, 1569.5827159467738, -794.9590755343082,
-          ],
-          points: [
-            { x: 1363.3800961316804, y: 483.5732802091982 },
-            { x: 1363.380096131679, y: 711.9249144776211 },
-            { x: 1088.6068794194352, y: 711.9249144775904 },
-            { x: 1088.606879419436, y: 483.57328020916987 },
-          ],
-        },
-        {
-          id: "1",
-          createTime: 0,
-          zIndex: 0,
-          attitude: [
-            1.2148907961465611, -1.1842248053033169, 0.9286933259554236,
-            0.952742223514732, -274.21565282732183, 1472.5179431383922,
-          ],
-          fill: "red",
-          points: [
-            { x: 791.8290392640735, y: 704.1094464497272 },
-            { x: 986.4696856698326, y: 514.3818739591983 },
-            { x: 1150.8993397890408, y: 683.0695019402921 },
-            { x: 956.258693383281, y: 872.7970744308196 },
-          ],
-        },
-        {
-          id: "efe1cf7c-74ec-4c62-a23e-f68ced234c6b",
-          createTime: 1736386331805,
-          zIndex: 0,
-          opacity: 1,
-          ref: false,
-          points: [
-            { x: -399.703125, y: 420.65234375 },
-            { x: -281.390625, y: 420.65234375 },
-            { x: -281.390625, y: 544.06640625 },
-            { x: -399.703125, y: 544.06640625 },
-          ],
-          attitude: [1, 0, 0, 1, 0, 0],
-          dash: [4.32, 25.68],
-        },
-        {
-          id: "ec71e6ac-7214-484a-aebf-d0ea3bd6cd32",
-          createTime: 1736386333439,
-          zIndex: 0,
-          opacity: 1,
-          ref: false,
-          points: [
-            { x: -281.390625, y: 544.06640625 },
-            { x: -154.1640625, y: 544.06640625 },
-            { x: -154.1640625, y: 671.86328125 },
-            { x: -281.390625, y: 671.86328125 },
-          ],
-          attitude: [1, 0, 0, 1, 0, 0],
-        },
-        {
-          id: "f54f8b3b-4f2e-4811-b5c3-f90a2fd9deb2",
-          createTime: 1736386335742,
-          zIndex: 0,
-          opacity: 1,
-          ref: false,
-          points: [
-            { x: -281.390625, y: 544.06640625 },
-            { x: -399.703125, y: 544.06640625 },
-            { x: -399.703125, y: 671.86328125 },
-            { x: -281.390625, y: 671.86328125 },
-          ],
-          attitude: [1, 0, 0, 1, 0, 0],
-        },
-        {
-          id: "647b6f0d-6cf3-46ef-be3f-efd7854e66b6",
-          createTime: 1736386338885,
-          zIndex: 0,
-          opacity: 1,
-          ref: false,
-          points: [
-            { x: -281.390625, y: 544.06640625 },
-            { x: -154.1640625, y: 544.06640625 },
-            { x: -154.1640625, y: 420.65234375 },
-            { x: -281.390625, y: 420.65234375 },
-          ],
-          attitude: [1, 0, 0, 1, 0, 0],
-          dash: [8.1, 21.9],
-        },
-        {
-          id: "63d8d8d6-9bfc-4756-b4d8-28fd78d75404",
-          createTime: 1736386349032,
-          zIndex: 0,
-          opacity: 1,
-          ref: false,
-          points: [
-            { x: -154.1640625, y: 420.65234375 },
-            { x: -399.703125, y: 420.65234375 },
-            { x: -399.703125, y: 357.41015625 },
-            { x: -154.1640625, y: 357.41015625 },
-          ],
-          attitude: [1, 0, 0, 1, 0, 0],
-          dash: [19.81, 10.190000000000001],
-        },
-        {
-          id: "c147512f-b7fa-4638-83e9-b2bbd7311a4a",
-          createTime: 1736386356941,
-          zIndex: 0,
-          opacity: 1,
-          ref: false,
-          points: [
-            { x: -154.1640625, y: 671.86328125 },
-            { x: 70.11328125, y: 671.86328125 },
-            { x: 70.11328125, y: 357.41015625 },
-            { x: -154.1640625, y: 357.41015625 },
-          ],
-          attitude: [1, 0, 0, 1, 0, 0],
-        },
-        {
-          id: "27df2ef6-0d23-4620-be4c-c9ab49f6e46a",
-          createTime: 1736386360999,
-          zIndex: 0,
-          opacity: 1,
-          ref: false,
-          points: [
-            { x: -154.1640625, y: 357.41015625 },
-            { x: 70.11328125, y: 357.41015625 },
-            { x: 70.11328125, y: 544.06640625 },
-            { x: -154.1640625, y: 544.06640625 },
-          ],
-          attitude: [1, 0, 0, 1, 0, 0],
-          dash: [3.63, 26.37],
-        },
-        {
-          id: "60dbebbb-f670-4279-aa5a-7686c366b121",
-          createTime: 1736386369847,
-          zIndex: 0,
-          opacity: 1,
-          ref: false,
-          points: [
-            { x: -154.1640625, y: 544.06640625 },
-            { x: 70.11328125, y: 544.06640625 },
-            { x: 70.11328125, y: 671.86328125 },
-            { x: -154.1640625, y: 671.86328125 },
-          ],
-          attitude: [1, 0, 0, 1, 0, 0],
-          dash: [5.72, 24.28],
-        },
-        {
-          id: "15d1acd3-66cf-4e76-bd92-ba2b8432f655",
-          createTime: 1736386395905,
-          zIndex: 0,
-          opacity: 1,
-          ref: false,
-          points: [
-            { x: -399.703125, y: 357.41015625 },
-            { x: -587.9070328715221, y: 357.41015625 },
-            { x: -587.9070328715221, y: 671.86328125 },
-            { x: -399.703125, y: 671.86328125 },
-          ],
-          attitude: [1, 0, 0, 1, 0, 0],
-        },
-        {
-          id: "95ed1359-3650-4363-ac01-54c02e72e943",
-          createTime: 1737625169697,
-          zIndex: 0,
-          opacity: 1,
-          ref: false,
-          points: [
-            { x: 207.06697420440253, y: 85.1184373336713 },
-            { x: 327.6607242044025, y: 85.1184373336713 },
-            { x: 327.6607242044025, y: 182.0949998336713 },
-            { x: 207.06697420440253, y: 182.0949998336713 },
-          ],
-          attitude: [1, 0, 0, 1, -329.5931820455975, -123.4440626663287],
-        },
-      ],
-      triangle: [
-        {
-          ref: false,
-          createTime: 3,
-          zIndex: 0,
-          id: "3",
-          attitude: [1, 0, 0, 1, 0, 0],
-          points: [
-            { x: 115.58984375, y: 626.625 },
-            { x: 229.76953125, y: 495.828125 },
-            { x: 343.94921875, y: 626.625 },
-          ],
-        },
-        {
-          id: "b84daf26-66e0-40eb-a35a-ff143da7839c",
-          createTime: 1736386807568,
-          zIndex: 0,
-          opacity: 1,
-          ref: false,
-          points: [
-            { x: -597.0415203321531, y: 81.39090892655346 },
-            { x: -566.6056692947864, y: 8.839100341674886 },
-            { x: -536.1698182574196, y: 81.39090892655346 },
-          ],
-        },
-        {
-          id: "cc755da7-7cdd-44b6-b4a1-e2a9b9abb97c",
-          createTime: 1736386808809,
-          zIndex: 0,
-          opacity: 1,
-          ref: false,
-          points: [
-            { x: -505.7339672200528, y: 8.839100341674886 },
-            { x: -536.1698182574196, y: 81.39090892655346 },
-            { x: -566.6056692947864, y: 8.839100341674886 },
-          ],
-        },
-        {
-          id: "4b2505ac-4e61-4c79-9053-7ce9c8ef9f29",
-          createTime: 1736386811452,
-          zIndex: 0,
-          opacity: 1,
-          ref: false,
-          points: [
-            { x: -475.29811618268604, y: 81.39090892655346 },
-            { x: -505.7339672200528, y: 8.839100341674886 },
-            { x: -536.1698182574196, y: 81.39090892655346 },
-          ],
-        },
-        {
-          id: "8a18d563-3e1e-4b68-8436-f37a5ee932b4",
-          createTime: 1736386825508,
-          zIndex: 0,
-          opacity: 1,
-          ref: false,
-          points: [
-            { x: -444.86226514531927, y: 8.839100341674886 },
-            { x: -475.29811618268604, y: 81.39090892655346 },
-            { x: -505.7339672200528, y: 8.839100341674886 },
-          ],
-        },
-        {
-          id: "e5d5cc39-2277-4b97-9270-ab06812e33cc",
-          createTime: 1736386828089,
-          zIndex: 0,
-          opacity: 1,
-          ref: false,
-          points: [
-            { x: -414.4264141079525, y: 81.39090892655346 },
-            { x: -444.86226514531927, y: 8.839100341674886 },
-            { x: -475.29811618268604, y: 81.39090892655346 },
-          ],
-        },
-      ],
-      polygon: [
-        {
-          createTime: 3,
-          zIndex: 0,
-          id: "33",
-          attitude: [1, 0, 0, 1, -294.60546875, -13.67578125],
-          points: [
-            { x: 121.93359375, y: 341.10546875 },
-            { x: 420.87109375, y: 356.50390625 },
-            { x: 392.12890625, y: 524.4921875 },
-            { x: 105.9609375, y: 517.86328125 },
-            { x: 18.832558784586354, y: 515.8450055612782 },
-            { x: 18.832558784586354, y: 335.79467688514586 },
-          ],
-          dash: [18.42, 11.579999999999998],
-        },
-        {
-          id: "7105a114-964c-4f55-8bc4-911b37fbc768",
-          createTime: 1736845626186,
-          zIndex: 0,
-          opacity: 1,
-          ref: true,
-          points: [
-            { x: 859.33203125, y: 305.10546875 },
-            { x: 859.33203125, y: 380.2421875 },
-            { x: 859.33203125, y: 548.828125 },
-            { x: 601.39453125, y: 548.828125 },
-            { x: 351.71875, y: 548.828125 },
-            { x: 351.71875, y: 424.39453125 },
-            { x: 351.71875, y: 273.09375 },
-            { x: 351.71875, y: 195.64453125 },
-            { x: 859.33203125, y: 195.64453125 },
-          ],
-          attitude: [1, 0, 0, 1, 286.56640625, -2.3671875],
-        },
-        {
-          id: "fbdfc09e-1f81-4772-8c3a-316d50cc7da1",
-          createTime: 1737012759015,
-          zIndex: 0,
-          opacity: 1,
-          ref: false,
-          points: [
-            { x: 1705.741066896583, y: -839.713131499153 },
-            { x: 1938.3695756456666, y: -839.713131499153 },
-            { x: 1938.3695756456666, y: -619.115475249153 },
-            { x: 1705.741066896583, y: -619.115475249153 },
-          ],
-          attitude: [1, 0, 0, 1, 1054.152456203272, -1346.193323532427],
-          strokeWidth: 10,
-          stroke: "#C71585",
-          fill: "#1F93FF",
-          dash: [11.16, 18.84],
-        },
-      ],
-      line: [
-        {
-          createTime: 3,
-          zIndex: 0,
-          id: "333",
-          attitude: [1, 0, 0, 1, 264.050567067064, -209.36399936114208],
-          points: [
-            { x: 691.9908236537299, y: -256.43159647539807 },
-            { x: 834.8596014933705, y: -256.43159647539807 },
-            { x: 834.8596014933705, y: 52.60589610179676 },
-            { x: 668.8302882438311, y: 52.60589610179676 },
-            { x: 668.8302882438311, y: -84.48785389820324 },
-            { x: 569.2892889933705, y: -84.48785389820324 },
-            { x: 569.2892889933705, y: 52.60589610179676 },
-            { x: 471.5666327433705, y: 52.60589610179676 },
-            { x: 471.5666327433705, y: -122.69879139820324 },
-            { x: 569.2892889933705, y: -122.69879139820324 },
-            { x: 569.2892889933705, y: -256.43159647539807 },
-            { x: 471.5666327433705, y: -256.43159647539807 },
-            { x: 471.5666327433705, y: -385.2632664678143 },
-          ],
-          ref: false,
-          dash: [20.66, 9.34],
-          strokeWidth: 10,
-          stroke: "#047E22",
-        },
-      ],
-      arrow: [
-        {
-          id: "8",
-          createTime: 1,
-          zIndex: 0,
-          attitude: [1, 0, 0, 1, -85.9609375, 129.58203125],
-          points: [
-            { x: 879.94140625, y: 265.87109375 },
-            { x: 662.28515625, y: 381.26171875 },
-          ],
-        },
-        {
-          id: "9",
-          createTime: 1,
-          zIndex: 0,
-          attitude: [1, 0, 0, 1, 78.11328125, -42.01171875],
-          points: [
-            { x: 834.46875, y: 52.76953125 },
-            { x: 958.0546875, y: 223.859375 },
-          ],
-        },
-      ],
-      circle: [
-        {
-          id: "335dd470-a6b5-4fef-bb26-0a40010c4b96",
-          createTime: 1737621610324,
-          zIndex: 1,
-          opacity: 1,
-          ref: false,
-          mat: [1, 0, 0, 1, 645.1796875, 139.01171875],
-          radiusX: 277.6712260605102,
-          radiusY: 82.0068317036322,
-        },
-        {
-          id: "5a292b3a-ce0e-47a8-a03b-c2c44c38f97f",
-          createTime: 1738834412964,
-          lock: false,
-          zIndex: 3,
-          opacity: 1,
-          ref: false,
-          mat: [1, 0, 0, 1, -540.352979059887, -785.2716514826207],
-          radiusX: 79.5859375,
-          radiusY: 101.392578125,
-        },
-      ],
-      group: [{ id: "asdasd", ids: ["333asdt3", "3t3asd33"] }],
-      text: [
-        {
-          createTime: 3,
-          zIndex: 0,
-          id: "333asdt3",
-          fill: "#000",
-          fontFamily: "Calibri",
-          fontSize: 30,
-          content: "我爱人人人人爱我",
-          mat: [
-            -1.4930798945178672e-15, 1.0000000000000104, -1.0000000000000155,
-            -1.5971633030764742e-15, 1083.2463425727433, 95.00094657517106,
-          ],
-          width: 71.86542465603814,
-        },
-        {
-          createTime: 3,
-          zIndex: 0,
-          id: "3t3asd33",
-          fill: "#000",
-          fontFamily: "Calibri",
-          fontSize: 30,
-          width: 300,
-          content: "文字",
-          mat: [1, 0, 0, 1, 873.94921875, 659.453125],
-        },
-      ],
-      table: [
-        {
-          stroke: "#d8000a",
-          strokeWidth: 1,
-          id: "95b321a0-f36b-41e9-a102-96c04b2b4d39",
-          createTime: 1737885095135,
-          lock: false,
-          zIndex: 2,
-          opacity: 1,
-          ref: true,
-          width: 232.73828125,
-          height: 224.91796875,
-          content: [
-            [
-              {
-                content: "qwe",
-                width: 116.369140625,
-                height: 56.2294921875,
-                align: "center",
-                fontStyle: "italic",
-              },
-              {
-                content: "czx",
-                width: 116.369140625,
-                height: 56.2294921875,
-                align: "center",
-                fontStyle: "italic",
-              },
-            ],
-            [
-              {
-                content: "1231",
-                width: 116.369140625,
-                height: 56.2294921875,
-                align: "center",
-                fontStyle: "italic",
-              },
-              {
-                content: "5324",
-                width: 116.369140625,
-                height: 56.2294921875,
-                align: "center",
-                fontStyle: "italic",
-              },
-            ],
-            [
-              {
-                content: "asd",
-                width: 116.369140625,
-                height: 56.2294921875,
-                align: "center",
-                fontStyle: "italic",
-              },
-              {
-                content: "qe",
-                width: 116.369140625,
-                height: 56.2294921875,
-                align: "center",
-                fontStyle: "italic",
-              },
-            ],
-            [
-              {
-                content: "123",
-                width: 116.369140625,
-                height: 56.2294921875,
-                align: "center",
-                fontStyle: "italic",
-              },
-              {
-                content: "zxc",
-                width: 116.369140625,
-                height: 56.2294921875,
-                align: "center",
-                fontStyle: "italic",
-              },
-            ],
-          ],
-          mat: [1, 0, 0, 1, 1867.804575999732, 828.8137443432463],
-          align: "center",
-          fontStyle: "italic",
-          fill: "",
-        },
-        {
-          stroke: "#d8000a",
-          strokeWidth: 1,
-          fontSize: 16,
-          align: "center",
-          fontStyle: "normal",
-          fontColor: "#d8000a",
-          id: "de4475a7-95ef-4438-98ab-d6f0045f7f61",
-          createTime: 1738913187767,
-          lock: false,
-          zIndex: 4,
-          opacity: 1,
-          ref: false,
-          width: 425.8472387828342,
-          height: 262.0582582259674,
-          content: [
-            [
-              {
-                content: "1234",
-                width: 106.46180969570855,
-                height: 99.70181998577598,
-                padding: 8,
-                fontColor: "#d8000a",
-                fontStyle: "normal",
-                align: "center",
-              },
-              {
-                content: "请问请问",
-                width: 106.46180969570855,
-                height: 99.70181998577598,
-                padding: 8,
-                fontColor: "#d8000a",
-                fontStyle: "normal",
-                align: "center",
-              },
-              {
-                content:
-                  "按时打算自行车按时按时打算自行车按时按时打算自行车按时按时打算自行车按时",
-                width: 106.46180969570855,
-                height: 99.70181998577598,
-                padding: 8,
-                fontColor: "#d8000a",
-                fontStyle: "normal",
-                align: "center",
-              },
-              {
-                content: "亲请问请问请问",
-                width: 106.46180969570855,
-                height: 99.70181998577598,
-                padding: 8,
-                fontColor: "#d8000a",
-                fontStyle: "normal",
-                align: "center",
-              },
-            ],
-            [
-              {
-                content: "45234",
-                width: 106.46180969570855,
-                height: 54.11881274673046,
-                padding: 8,
-                fontColor: "#d8000a",
-                fontStyle: "normal",
-                align: "center",
-              },
-              {
-                content: "123asdasdasdasdasdasd按时打算大叔大婶大叔大婶的",
-                width: 106.46180969570855,
-                height: 54.11881274673046,
-                padding: 8,
-                fontColor: "#d8000a",
-                fontStyle: "normal",
-                align: "center",
-              },
-              {
-                content: "asdasd",
-                width: 106.46180969570855,
-                height: 54.11881274673046,
-                padding: 8,
-                fontColor: "#d8000a",
-                fontStyle: "normal",
-                align: "center",
-              },
-              {
-                content: "自行车这些",
-                width: 106.46180969570855,
-                height: 54.11881274673046,
-                padding: 8,
-                fontColor: "#d8000a",
-                fontStyle: "normal",
-                align: "center",
-              },
-            ],
-            [
-              {
-                content: "65345",
-                width: 106.46180969570855,
-                height: 54.11881274673046,
-                padding: 8,
-                fontColor: "#d8000a",
-                fontStyle: "normal",
-                align: "center",
-              },
-              {
-                content: "123123",
-                width: 106.46180969570855,
-                height: 54.11881274673046,
-                padding: 8,
-                fontColor: "#d8000a",
-                fontStyle: "normal",
-                align: "center",
-              },
-              {
-                content: "123123",
-                width: 106.46180969570855,
-                height: 54.11881274673046,
-                padding: 8,
-                fontColor: "#d8000a",
-                fontStyle: "normal",
-                align: "center",
-              },
-              {
-                content: "43214",
-                width: 106.46180969570855,
-                height: 54.11881274673046,
-                padding: 8,
-                fontColor: "#d8000a",
-                fontStyle: "normal",
-                align: "center",
-              },
-            ],
-            [
-              {
-                content: "5423234",
-                width: 106.46180969570855,
-                height: 54.11881274673046,
-                padding: 8,
-                fontColor: "#d8000a",
-                fontStyle: "normal",
-                align: "center",
-              },
-              {
-                content: "123123",
-                width: 106.46180969570855,
-                height: 54.11881274673046,
-                padding: 8,
-                fontColor: "#d8000a",
-                fontStyle: "normal",
-                align: "center",
-              },
-              {
-                content: "43224",
-                width: 106.46180969570855,
-                height: 54.11881274673046,
-                padding: 8,
-                fontColor: "#d8000a",
-                fontStyle: "normal",
-                align: "center",
-              },
-              {
-                content: "123123",
-                width: 106.46180969570855,
-                height: 54.11881274673046,
-                padding: 8,
-                fontColor: "#d8000a",
-                fontStyle: "normal",
-                align: "center",
-              },
-            ],
-          ],
-          mat: [1, 0, 0, 1, 416.91799465370843, 869.4103464598702],
-        },
-      ],
-    },
-    '1F': {
-
-    }
-  },
-  config: {
-    drawSize: null,
-    compass: { rotation: 0 },
-    proportion: { scale: 100, unit: "mm" },
-  },
-};

+ 0 - 27
src/example/fuse/views/use-draw.ts

@@ -1,27 +0,0 @@
-import { inject, provide, Ref, ShallowUnwrapRef, watch } from "vue";
-import { DrawExpose } from "../../../index";
-import { mergeFuns } from "@/utils/shared";
-
-const initDraw = (draw: Draw) => {
-  draw.config.showLabelLine = true;
-  draw.config.showGrid = false;
-  draw.config.showCompass = true
-
-  return mergeFuns([]);
-};
-
-const actionKey = Symbol("drawAction");
-export type Draw = ShallowUnwrapRef<DrawExpose>;
-export const installDraw = (drawRef: Ref<DrawExpose | undefined>) => {
-  watch(
-    () => drawRef?.value?.stage,
-    (_a, _b, onCleanup) => {
-      if (drawRef.value) {
-        onCleanup(initDraw(drawRef.value as unknown as Draw));
-      }
-    }
-  );
-  provide(actionKey, drawRef);
-};
-
-export const useDraw = () => inject<Ref<Draw>>(actionKey)?.value!;

+ 1 - 1
src/example/fuse/platform-draw.ts

@@ -1,6 +1,6 @@
 import { genBound } from "@/utils/shared";
 import { AIExposeData } from "./dialog/ai";
-import { Draw } from "./views/use-draw";
+import { Draw } from "../fuse/views/use-draw";
 import { SceneFloor } from "./platform-resource";
 import { getBaseItem } from "@/core/components/util";
 import { LineData } from "@/core/components/line";

src/example/fuse/platform-resource.ts → src/example/platform/platform-resource.ts


+ 17 - 2
src/example/fuse/styles/global.scss

@@ -9,16 +9,31 @@ $slideSize: 64px;
 	margin: 0;
 }
 
+body {
+	// font-family: Microsoft YaHei, Microsoft YaHei;
+	--el-font-family: PingFang SC, PingFang SC, Microsoft YaHei, Microsoft YaHei;
+	font-family: var(--el-font-family);
+}
+
 .operate {
-	transition: color .3s ease;
 	background: rgba(255,255,255,0);
 	transition: background .3s ease;
 	cursor: pointer;
+	color: rgba(0,0,0,0.85);
+	outline: none ;
+
+	.icon {
+		outline: none ;
+	}
 
 	&:hover {
-		background: rgba(255,255,255,0.3);
+		background: var(--el-color-primary-light-9);
 	}
 }
+.disabled {
+	pointer-events: none;
+	opacity: 0.7;
+}
 
 .center {
 	display: flex;

+ 63 - 35
src/utils/dom.ts

@@ -1,41 +1,69 @@
+function getCaretPosition(
+  element: HTMLInputElement | HTMLTextAreaElement,
+  x: number,
+  y: number
+) {
+  const text = element.childNodes[0];
+  const range = element.ownerDocument.createRange();
+  const start = 0;
+  const end = element.value.length;
+  let caretPosition = start;
 
-function getCaretPosition(element: HTMLInputElement | HTMLTextAreaElement, x: number, y: number) {  
-  const text = element.childNodes[0]
-  const range = element.ownerDocument.createRange();  
-  const start = 0;  
-  const end = element.value.length;  
-  let caretPosition = start;  
+  range.setStart(text, 0);
+  range.setEnd(text, end);
 
-  range.setStart(text, 0);  
-  range.setEnd(text, end);  
+  range.setStart(text, 0);
+  for (let i = start; i <= end; i++) {
+    range.setEnd(text, i);
+    console.log(range.getBoundingClientRect());
+    if (
+      range.getBoundingClientRect().left >= x &&
+      range.getBoundingClientRect().top >= y
+    ) {
+      caretPosition = i;
+      break;
+    }
+  }
 
-  range.setStart(text, 0);  
-  for (let i = start; i <= end; i++) {  
-    range.setEnd(text, i);  
-    console.log(range.getBoundingClientRect())
-    if (range.getBoundingClientRect().left >= x && range.getBoundingClientRect().top >= y) {  
-      caretPosition = i;  
-      break;  
-    }  
-  }  
+  return caretPosition;
+}
 
-  return caretPosition;  
-}  
+function setCaretPosition(element: any, position: number) {
+  element.focus();
+  if (element.setSelectionRange) {
+    element.setSelectionRange(position, position);
+  } else if (element.createTextRange) {
+    const range = element.createTextRange();
+    range.collapse(true);
+    range.moveEnd("character", position);
+    range.moveStart("character", position);
+    range.select();
+  }
+}
 
-function setCaretPosition(element: any, position: number) {  
-  element.focus();  
-  if (element.setSelectionRange) {  
-    element.setSelectionRange(position, position);  
-  } else if (element.createTextRange) {  
-    const range = element.createTextRange();  
-    range.collapse(true);  
-    range.moveEnd('character', position);  
-    range.moveStart('character', position);  
-    range.select();  
-  }  
-}  
+export const focusToMouse = (
+  element: HTMLInputElement | HTMLTextAreaElement,
+  event: MouseEvent
+) => {
+  const caretPosition = getCaretPosition(element, event.clientX, event.clientY);
+  setCaretPosition(element, caretPosition);
+};
 
-export const focusToMouse = (element: HTMLInputElement | HTMLTextAreaElement, event: MouseEvent) => {
-  const caretPosition = getCaretPosition(element, event.clientX, event.clientY);  
-  setCaretPosition(element, caretPosition);  
-}
+const $fileInput = document.createElement("input");
+$fileInput.type = "file";
+$fileInput.style = `
+  position: absolute;
+  display: none;
+`;
+export const selectFile = (multiple = false, accept = '') =>
+  new Promise<File[]>((resolve) => {
+    $fileInput.multiple = multiple
+    $fileInput.accept = accept
+    $fileInput.onchange = () => {
+      $fileInput.accept = ''
+      $fileInput.multiple = false
+      resolve(Array.from($fileInput.files!));
+      $fileInput.value = ''
+    }
+    $fileInput.click()
+  });