Przeglądaj źródła

feat: 多选提供属性调整

bill 1 miesiąc temu
rodzic
commit
8b322fd2eb
34 zmienionych plików z 746 dodań i 397 usunięć
  1. 5 2
      src/core/components/arrow/arrow.vue
  2. 1 0
      src/core/components/arrow/temp-arrow.vue
  3. 1 0
      src/core/components/circle/temp-circle.vue
  4. 39 1
      src/core/components/group/group.vue
  5. 1 7
      src/core/components/line-icon/index.ts
  6. 29 14
      src/core/components/line/attach-server.ts
  7. 0 1
      src/core/components/line/index.ts
  8. 1 1
      src/core/components/line/single-line.vue
  9. 40 4
      src/core/components/line/use-draw.ts
  10. 1 0
      src/core/components/polygon/temp-polygon.vue
  11. 1 0
      src/core/components/sequent-line/temp-line.vue
  12. 1 0
      src/core/components/serial/temp-serial.vue
  13. 1 0
      src/core/components/table/temp-table.vue
  14. 1 0
      src/core/components/triangle/temp-triangle.vue
  15. 18 10
      src/core/helper/compass.vue
  16. 70 5
      src/core/hook/use-component.ts
  17. 6 4
      src/core/hook/use-draw.ts
  18. 0 2
      src/core/hook/use-event.ts
  19. 10 5
      src/core/hook/use-expose.ts
  20. 88 83
      src/core/hook/use-global-vars.ts
  21. 108 0
      src/core/hook/use-group.ts
  22. 23 13
      src/core/hook/use-interactive.ts
  23. 1 1
      src/core/hook/use-mouse-status.ts
  24. 14 9
      src/core/hook/use-selection.ts
  25. 2 1
      src/core/hook/use-status.ts
  26. 0 1
      src/core/html-mount/propertys/components/color.vue
  27. 2 2
      src/core/html-mount/propertys/components/input-num.vue
  28. 2 4
      src/core/html-mount/propertys/components/num.vue
  29. 189 0
      src/core/html-mount/propertys/mount-describes.vue
  30. 20 190
      src/core/html-mount/propertys/mount.vue
  31. 2 1
      src/example/components/slide/slide-icons.vue
  32. 8 12
      src/example/dialog/ai/ai.vue
  33. 18 7
      src/example/fuse/views/defStyle.ts
  34. 43 17
      src/utils/dom.ts

+ 5 - 2
src/core/components/arrow/arrow.vue

@@ -29,6 +29,7 @@ import { Pos } from "@/utils/math.ts";
 import { Group } from "konva/lib/Group";
 import { flatPositions } from "@/utils/shared.ts";
 import { themeColor } from "@/constant";
+import { watch } from "vue";
 
 const props = defineProps<{ data: ArrowData }>();
 const emit = defineEmits<{
@@ -86,8 +87,10 @@ const { shape, tData, operateMenus, describes, data } = useComponentStatus<
     // "zIndex",
   ],
 });
-describes.fill.label = "颜色";
-describes.strokeWidth.label = "粗细";
+watch(describes, describes => {
+  describes.fill.label = "颜色";
+  describes.strokeWidth.label = "粗细";
+}, {immediate: true})
 
 // const draw = useInteractiveDrawShapeAPI();
 // const store = useStore();

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

@@ -12,6 +12,7 @@
         hitStrokeWidth: data.strokeWidth + 10,
         pointerWidth: data.pointerLength,
         closed: false,
+        id: void 0,
         points: flatPositions([data.points[ndx], data.points[ndx + 1]]),
         ...eConfig,
         opacity: addMode ? 0.3 : data.opacity,

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

@@ -5,6 +5,7 @@
       :config="{
         ...data,
         ...matConfig,
+        id: void 0,
         zIndex: undefined,
         hitStrokeWidth: data.strokeWidth,
         opacity: addMode ? 0.3 : data.opacity,

+ 39 - 1
src/core/components/group/group.vue

@@ -2,16 +2,24 @@
   <template v-if="show">
     <TempGroup
       :data="{ ...tData, listening }"
-      :ref="(e: any) => {shape = e?.shape; getGroupShapes = e?.getGroupShapes}"
+      :ref="(e: any) => { shape = e?.shape; getGroupShapes = e?.getGroupShapes }"
       :autoUpdate="autoUpdate"
     />
     <Operate :target="shape" :menus="operateMenus" />
+    <PropertyUpdate
+      show
+      :name="propertyName"
+      :describes="descs"
+      @change="changePropertyHandler"
+      v-if="setPropertyDatas.length"
+    />
   </template>
 </template>
 
 <script lang="ts" setup>
 import TempGroup from "./temp-group.vue";
 import { Operate } from "../../html-mount/propertys/index.ts";
+import PropertyUpdate from "../../html-mount/propertys/mount-describes.vue";
 import {
   GroupData,
   getMouseStyle,
@@ -33,6 +41,8 @@ import { EntityShape } from "@/deconstruction.js";
 import { useForciblyShowItemIds, useStage } from "@/core/hook/use-global-vars.ts";
 import { themeColor } from "@/constant";
 import { debounce } from "@/utils/shared.ts";
+import { useComponentsDescribes } from "@/core/hook/use-group.ts";
+import { components } from "../index.ts";
 
 const props = defineProps<{ data: GroupData }>();
 const emit = defineEmits<{
@@ -59,6 +69,34 @@ const getShapeBelong = (id: string) => {
   }
 };
 
+const selectIds = computed(() => props.data.ids);
+const descs = useComponentsDescribes(selectIds);
+const setPropertyDatas = computed(() => {
+  const keys = Object.keys(descs.value);
+  return keys.length === 0 ? [] : descs.value[keys[0]].joins;
+});
+const propertyName = computed(() => {
+  const names = new Set<string>();
+  for (const id of selectIds.value) {
+    const belong = getShapeBelong(id);
+    if (belong?.type) {
+      names.add(components[belong.type].shapeName);
+    }
+  }
+  return [...names].join("、") || "多实现";
+});
+
+const changePropertyHandler = () => {
+  history.onceTrack(() => {
+    for (const item of setPropertyDatas.value) {
+      const type = store.getType(item.id);
+      if (type) {
+        store.setItem(type, { value: item, id: item.id });
+      }
+    }
+  });
+};
+
 const { shape, tData, data } = useComponentStatus<Rect, GroupData>({
   emit,
   props,

+ 1 - 7
src/core/components/line-icon/index.ts

@@ -20,7 +20,7 @@ import { LineData } from "../line/index.ts";
 import { DrawStore } from "@/core/store/index.ts";
 
 export { defaultStyle, addMode, TempComponent, Component };
-export { getMouseStyle } from "../icon/index.ts";
+export { getMouseStyle, getPredefine } from "../icon/index.ts";
 
 export const shapeName = "线段图例";
 export type LineIconData = Omit<IconData, "mat" | "width"> & {
@@ -259,11 +259,5 @@ export const interactiveFixData: InteractiveFix<"lineIcon"> = ({
 
 export const checkItemData = (item: LineIconData) => !!item.lineId
 
-export const getPredefine = (key: keyof LineIconData) => {
-  if (key === "fill" || key === "stroke") {
-    return { canun: true };
-  }
-};
-
 export const getSnapPoints = () => [];
 export const getSnapInfos = () => [];

+ 29 - 14
src/core/components/line/attach-server.ts

@@ -32,6 +32,7 @@ import { ComponentSnapInfo } from "..";
 import { useStore } from "@/core/store";
 import { computed, onUnmounted, reactive, ref, Ref, watch } from "vue";
 import {
+  useCursor,
   usePointerPos,
   useRunHook,
   useStage,
@@ -48,6 +49,7 @@ import {
   LineIconData,
 } from "../line-icon";
 import { useDrawIngData } from "@/core/hook/use-draw";
+import { useComponentDescribes } from "@/core/hook/use-component";
 
 export type NLineDataCtx = {
   del: {
@@ -535,6 +537,7 @@ export const updateLineLength = (
   }
 
   let moveVector = new Vector2(vector.x, vector.y);
+  let npoints: Pos[];
   if (flex === "both") {
     const center = lineCenter(points);
     const l1 = getVectorLine(
@@ -543,7 +546,7 @@ export const updateLineLength = (
       length / 2
     );
     const l2 = getVectorLine(moveVector, center, length / 2);
-    return [l1[1], l2[1]];
+    npoints = [l1[1], l2[1]];
   } else {
     const fNdx = flex === "a" ? 1 : 0;
     const mNdx = flex === "a" ? 0 : 1;
@@ -555,28 +558,30 @@ export const updateLineLength = (
     const nPoints: Pos[] = [];
     nPoints[fNdx] = points[fNdx];
     nPoints[mNdx] = line[1];
-    return nPoints;
+    npoints = nPoints;
   }
+  Object.assign(points[0], npoints[0]);
+  Object.assign(points[1], npoints[1]);
 };
 
 export const useLineDescribes = (line: Ref<LineData["lines"][0]>) => {
-  const d: any = mergeDescribes(line, {}, ["stroke", "strokeWidth"]);
-  d.strokeWidth.props = {
-    ...d.strokeWidth.props,
-    proportion: true,
-  };
-  d.strokeWidth.label = "粗细";
-  d.stroke.label = "颜色";
-
+  const d: any = useComponentDescribes(line, ["stroke", "strokeWidth"], {});
   const store = useStore();
   const lineData = computed(() => store.getTypeItems("line")[0]);
   const points = computed(() => [
     lineData.value.points.find((p) => p.id === line.value.a)!,
     lineData.value.points.find((p) => p.id === line.value.b)!,
   ]);
-
   let setLineVector: Vector2;
 
+  watch(d, (d) => {
+    d.strokeWidth.props = {
+      ...d.strokeWidth.props,
+      proportion: true,
+    };
+    d.strokeWidth.label = "粗细";
+    d.stroke.label = "颜色";
+
   d.length = {
     type: "inputNum",
     label: "线段长度",
@@ -585,6 +590,7 @@ export const useLineDescribes = (line: Ref<LineData["lines"][0]>) => {
       return lineLen(points.value[0], points.value[1]);
     },
     set value(val) {
+      console.log(val, d.length.isChange);
       if (!d.isChange) {
         setLineVector = lineVector(points.value);
       }
@@ -597,6 +603,7 @@ export const useLineDescribes = (line: Ref<LineData["lines"][0]>) => {
       );
     },
   };
+  }, {immediate: true});
   return d as PropertyDescribes;
 };
 
@@ -640,6 +647,7 @@ export const useDrawLinePoint = (
     store.getTypeItems("lineIcon").filter((item) => item.lineId === line.id)
   );
   const drawStore = useDrawIngData();
+  const cursor = useCursor();
   const enterDraw = () => {
     const points = [
       data.points.find((p) => p.id === line.a)!,
@@ -647,7 +655,7 @@ export const useDrawLinePoint = (
     ];
     const cdata: LineData = { ...data, points, lines: [] };
     const point = reactive({ ...lineCenter(points), id: onlyId() });
-    const cIcons = icons.value.map(icon => ({...icon, id: onlyId()}));
+    const cIcons = icons.value.map((icon) => ({ ...icon, id: onlyId() }));
     const iconInfos = icons.value.map((icon) => {
       const mat = getLineIconMat(points, icon);
       return {
@@ -689,9 +697,10 @@ export const useDrawLinePoint = (
         }
       });
     });
-
+    pos.replay();
     snapInfos.update([]);
     return mergeFuns(
+      cursor.push("./icons/m_add.png"),
       runHook<() => void>(() =>
         clickListener(stage.value!.getNode().container(), () => {
           callback({
@@ -704,7 +713,13 @@ export const useDrawLinePoint = (
           leave();
         })
       ),
-      watch(pos, (pos) => pos && afterUpdate(pos)),
+      watch(
+        pos,
+        (pos) => {
+          pos && afterUpdate(pos);
+        },
+        { immediate: true }
+      ),
       () => {
         drawProps.value = undefined;
         snapInfos.clear();

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

@@ -10,7 +10,6 @@ import { EntityShape } from "@/deconstruction.js";
 import mitt from "mitt";
 import { watch } from "vue";
 import { getInitCtx, NLineDataCtx, normalLineData } from "./attach-server.ts";
-import { IconData } from "../icon/icon.ts";
 
 export { default as Component } from "./line.vue";
 export { default as TempComponent } from "./temp-line.vue";

+ 1 - 1
src/core/components/line/single-line.vue

@@ -67,7 +67,7 @@
     :name="shapeName"
     @change="
       () => {
-        describes.length.isChange || emit('updateBefore', []);
+        emit('updateBefore', []);
         emit('updateLine', { ...line });
         emit('update');
       }

+ 40 - 4
src/core/components/line/use-draw.ts

@@ -12,20 +12,20 @@ import { useViewerTransform } from "@/core/hook/use-viewer";
 import { useOperMode } from "@/core/hook/use-status";
 import { installGlobalVar, useCursor } from "@/core/hook/use-global-vars";
 import { useInteractiveDots } from "@/core/hook/use-interactive";
-import { computed, reactive, ref, watch } from "vue";
+import { computed, nextTick, reactive, ref, watch } from "vue";
 import { copy, mergeFuns } from "@/utils/shared";
-import { Pos } from "@/utils/math";
+import { lineVector, Pos } from "@/utils/math";
 import { getSnapInfos, type LineData } from "./";
 import { useCustomSnapInfos } from "@/core/hook/use-snap";
 import { getInitCtx, normalLineData } from "./attach-server";
+import { getKeywordInput } from "@/utils/dom";
+import { useProportion } from "@/core/hook/use-proportion";
 
 type PayData = Pos;
 
 export let initData: LineData | undefined;
 export const useInitData = installGlobalVar(() => ref<LineData>());
 
-
-
 export const useDraw = () => {
   const type = "line";
   const { quitDrawShape } = useInteractiveDrawShapeAPI();
@@ -38,6 +38,7 @@ export const useDraw = () => {
   const operMode = useOperMode();
   const hInitData = useInitData();
   const customSnapInfos = useCustomSnapInfos();
+  const { invTransform } = useProportion()
 
   // 可能历史空间会撤销 重做更改到正在绘制的组件
   const currentCursor = ref("./icons/m_add.png");
@@ -236,6 +237,7 @@ export const useDraw = () => {
     }
 
     const update = () => {
+      needInputClear && keyInput.clear();
       const msg = setMessage(cur);
       drawItems[0] = obj.interactiveFixData({
         data: drawItems[0]!,
@@ -247,7 +249,40 @@ export const useDraw = () => {
       isTempDraw = true;
     };
 
+    let isInputChange = false;
+    let needInputClear = true;
+    const keyInput = getKeywordInput(
+      (inputData, prev) => {
+        const len = Number(inputData);
+        
+        if (len && drawItems[0].lines.length) {
+          const line = drawItems[0].lines[drawItems[0].lines.length - 1];
+          const points = [
+            drawItems[0].points.find((p) => p.id === line.a)!,
+            drawItems[0].points.find((p) => p.id === line.b)!,
+          ];
+          const vector = lineVector(points);
+          const position = vector.multiplyScalar(invTransform(len)).add(points[0]);
+          cur.x = position.x;
+          cur.y = position.y;
+          isInputChange = true;
+          needInputClear = false;
+          nextTick(() => {
+            needInputClear = true;
+          });
+        }
+        return len ? len.toString() : prev;
+      },
+      () => {
+        if (isInputChange) {
+          ia.singleDone.value = true;
+          keyInput.clear();
+        }
+      }
+    );
+
     stopWatch = mergeFuns(
+      keyInput.stop,
       watch(() => operMode.value.freeDraw, update),
       watch(cur, update, { immediate: true, deep: true }),
       watch(
@@ -257,6 +292,7 @@ export const useDraw = () => {
           if (messages.value.length === 0) {
             quitDrawShape();
           } else {
+            keyInput.clear();
             update();
           }
         },

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

@@ -4,6 +4,7 @@
       name="repShape"
       :config="{
         ...data,
+        id: void 0,
         closed: true,
         zIndex: undefined,
         points: flatPositions(data.points),

+ 1 - 0
src/core/components/sequent-line/temp-line.vue

@@ -5,6 +5,7 @@
       :config="{
         ...data,
         zIndex: undefined,
+        id: void 0,
         points: flatPositions(data.points),
         opacity: addMode ? 0.3 : data.opacity,
         hitFunc,

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

@@ -5,6 +5,7 @@
       :config="{
         ...data,
         ...matConfig,
+        id: void 0,
         zIndex: undefined,
         opacity: addMode ? 0.3 : data.opacity,
       }"

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

@@ -15,6 +15,7 @@
       :config="{
         points: getBorderPoints(),
         ...data,
+        id: void 0,
         closed: true,
         zIndex: undefined,
       }"

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

@@ -6,6 +6,7 @@
         ...data,
         zIndex: undefined,
         closed: true,
+        id: void 0,
         points: flatPositions(data.points),
         opacity: addMode ? 0.3 : data.opacity,
       }"

+ 18 - 10
src/core/helper/compass.vue

@@ -28,12 +28,14 @@ import {
 } from "../hook/use-viewer.ts";
 import { useStore } from "../store/index.ts";
 import { getSvgContent, parseSvgContent } from "@/utils/resource.ts";
+import { useComponentDescribes } from "../hook/use-component.ts";
 
 const config = useConfig();
 const store = useStore();
 
 const maxWidth = 60;
 const data = ref({
+  id: "__compass",
   coverOpcatiy: 0,
   strokeScaleEnabled: false,
   width: maxWidth,
@@ -81,17 +83,23 @@ const [style] = useAnimationMouseStyle({
 } as any);
 
 let currentRotation = store.config.compass.rotation;
-const describes = mergeDescribes(data, {}, ["rotate"]);
-describes.rotate = {
-  ...describes.rotate,
-  sort: 3,
-  get value() {
-    return store.config.compass.rotation;
-  },
-  set value(val) {
-    store.config.compass.rotation = val;
+const describes = useComponentDescribes(data, ["rotate"], {});
+watch(
+  describes,
+  (describes) => {
+    describes.rotate = {
+      ...describes.rotate,
+      sort: 3,
+      get value() {
+        return store.config.compass.rotation;
+      },
+      set value(val) {
+        store.config.compass.rotation = val;
+      },
+    };
   },
-};
+  { immediate: true }
+);
 const changeHandler = () => {
   if (currentRotation !== store.config.compass.rotation) {
     store.setConfig({});

+ 70 - 5
src/core/hook/use-component.ts

@@ -26,12 +26,17 @@ import { useGetShapeCopyTransform } from "./use-copy";
 import { asyncTimeout, copy, mergeFuns, onlyId } from "@/utils/shared";
 import { Shape } from "konva/lib/Shape";
 import { Transform } from "konva/lib/Util";
-import { mergeDescribes, PropertyKeys } from "../html-mount/propertys";
+import {
+  mergeDescribes,
+  PropertyDescribes,
+  PropertyKeys,
+} from "../html-mount/propertys";
 import { useStore } from "../store";
-import { globalWatch, useStage } from "./use-global-vars";
+import { globalWatch, useMountMenusFilter, useStage } from "./use-global-vars";
 import { useAlignmentShape } from "./use-alignment";
 import { useViewerTransform } from "./use-viewer";
 import { usePause } from "./use-pause";
+import { useGlobalDescribes } from "./use-group";
 
 type Emit<T> = EmitFn<{
   updateShape: (value: T) => void;
@@ -172,8 +177,8 @@ export type UseComponentStatusProps<
   alignment?: (data: T, mat: Transform) => void;
   getMouseStyle: any;
   defaultStyle: any;
-  selfData?: boolean
   propertys: PropertyKeys;
+  selfData?: boolean;
   debug?: boolean;
   noJoinZindex?: boolean;
   noOperateMenus?: boolean;
@@ -187,6 +192,59 @@ export type UseComponentStatusProps<
   copyHandler?: (transform: Transform, data: T) => T;
 };
 
+export const useComponentDescribes = <T extends { id: string }>(
+  data: Ref<T>,
+  propertys: PropertyKeys,
+  defaultStyle: any
+) => {
+  const store = useStore();
+  const id = computed(() => data.value.id);
+  const type = computed(() => id.value && store.getType(id.value));
+  const initDescs = mergeDescribes(data, defaultStyle, propertys || []);
+  const { getFilter } = useMountMenusFilter();
+  const gdescs = useGlobalDescribes();
+
+  let descs = ref(initDescs);
+  watchEffect(() => {
+    const iDescs =
+      type.value && id.value
+        ? getFilter(type.value, id.value)(initDescs)
+        : initDescs;
+    descs.value = Object.fromEntries(
+      Object.entries(iDescs).sort(
+        ([_a, a], [_b, b]) => (b.sort || -1) - (a.sort || -1)
+      )
+    ) as PropertyDescribes;
+  });
+  watchEffect((onCleanup) => {
+    gdescs.set(data.value, descs.value);
+    onCleanup(() => gdescs.del(data.value.id));
+  });
+  watch(
+    descs,
+    (descs) => {
+      for (const key in descs) {
+        const initProps = descs[key].props;
+        watchEffect(() => {
+          if (!type.value) return;
+          const getPredefine = components[type.value].getPredefine;
+          const predefine =
+            getPredefine && (getPredefine as any)(key as keyof DrawItem);
+          if (!predefine) return;
+
+          if (descs[key].props) {
+            descs[key].props = { initProps, ...predefine };
+          } else {
+            descs[key].props = predefine;
+          }
+        });
+      }
+    },
+    { immediate: true }
+  );
+  return descs;
+};
+
 export const useComponentStatus = <S extends EntityShape, T extends DrawItem>(
   args: UseComponentStatusProps<T, S>
 ) => {
@@ -199,7 +257,10 @@ export const useComponentStatus = <S extends EntityShape, T extends DrawItem>(
     },
     { flush: "sync" }
   );
-  const data = useAutomaticData(() => args.props.data, data => args.selfData ? data : copy(data));
+  const data = useAutomaticData(
+    () => args.props.data,
+    (data) => args.selfData ? data : copy(data)
+  );
   const [style, pause, resume] = useAnimationMouseStyle({
     data: data,
     shape,
@@ -256,7 +317,11 @@ export const useComponentStatus = <S extends EntityShape, T extends DrawItem>(
           args.alignment,
           args.copyHandler
         ),
-    describes: mergeDescribes(data, args.defaultStyle, args.propertys || []),
+    describes: useComponentDescribes(
+      data,
+      args.propertys || [],
+      args.defaultStyle
+    ),
   };
 };
 

+ 6 - 4
src/core/hook/use-draw.ts

@@ -86,9 +86,10 @@ export const useInteractiveDrawShapeAPI = installGlobalVar(() => {
       shapeType: T,
       preset: Partial<DrawItem<T>> = {},
       data?: PayData<T>,
-      pixel = false
+      pixel = false,
+      force = false
     ) => {
-      if (!can.drawMode) {
+      if (!force && !can.drawMode) {
         throw "当前状态不允许添加";
       }
       enter();
@@ -112,13 +113,14 @@ export const useInteractiveDrawShapeAPI = installGlobalVar(() => {
     enterDrawShape: async <T extends ShapeType>(
       shapeType: T,
       preset: InteractivePreset<T>["preset"] = {},
-      single = false
+      single = false,
+      force = false
     ) => {
       if (isEnter) {
         leave();
         await new Promise((resolve) => setTimeout(resolve, 16));
       }
-      if (!can.drawMode || mode.include(Mode.draw)) {
+      if (!force && (!can.drawMode || mode.include(Mode.draw))) {
         throw "当前状态不允许添加";
       }
       if (!preset.zIndex) {

+ 0 - 2
src/core/hook/use-event.ts

@@ -4,8 +4,6 @@ import { globalWatch, installGlobalVar, useStage } from "./use-global-vars.ts";
 import { nextTick, reactive, ref, watch, watchEffect } from "vue";
 import { KonvaEventObject } from "konva/lib/Node";
 import { debounce } from "@/utils/shared.ts";
-import { Shape } from "konva/lib/Shape";
-import { shapeTreeContain } from "@/utils/shape.ts";
 import { EntityShape } from "@/deconstruction.js";
 
 export const useListener = <

+ 10 - 5
src/core/hook/use-expose.ts

@@ -53,10 +53,11 @@ export const useAutoPaste = () => {
             "icon",
             { ...style, fill: undefined, stroke: undefined },
             pos,
+            true,
             true
           );
         } else {
-          drawAPI.addShape("text", { content: val }, pos, true);
+          drawAPI.addShape("text", { content: val }, pos, true, true);
         }
       },
       type: "string",
@@ -70,6 +71,7 @@ export const useAutoPaste = () => {
             "icon",
             { ...style, fill: undefined, stroke: undefined },
             pos,
+            true,
             true
           );
         } else {
@@ -78,6 +80,7 @@ export const useAutoPaste = () => {
             "image",
             { url, width: image.width, height: image.height },
             pos,
+            true,
             true
           );
         }
@@ -184,11 +187,13 @@ export const useAutoService = () => {
   const cursor = useCursor();
   const { set: setCursor } = cursor.push("initial");
 
-  watchEffect(() => {
-    let style: string | null = null;
+  watchEffect((onCleanup) => {
     if (operMode.value.freeView) {
-      style = "pointer";
-    } else if (mode.include(Mode.update)) {
+      onCleanup(cursor.push('pointer'))
+      return;
+    }
+    let style: string | null = null;
+    if (mode.include(Mode.update)) {
       style = "./icons/m_move.png";
     } else if (status.hovers.length) {
       style = "pointer";

+ 88 - 83
src/core/hook/use-global-vars.ts

@@ -21,14 +21,14 @@ import { Pos } from "@/utils/math.ts";
 import { listener } from "@/utils/event.ts";
 import { debounce, mergeFuns, onlyId } from "@/utils/shared.ts";
 import { StoreData } from "../store/store.ts";
-import { rendererMap, rendererName } from "@/constant/index.ts";
+import { DomMountId, rendererMap, rendererName } from "@/constant/index.ts";
 import { Shape, ShapeConfig } from "konva/lib/Shape";
 import { ElLoading } from "element-plus";
 import { PropertyDescribes } from "../html-mount/propertys/index.ts";
 import { ShapeType } from "@/index.ts";
 import { isEditableElement } from "@/utils/dom.ts";
 
-let getInstance = getCurrentInstance
+let getInstance = getCurrentInstance;
 export const useRendererInstance = () => {
   let instance = getInstance()!;
   while (instance.type.name !== rendererName) {
@@ -68,16 +68,15 @@ export const installGlobalVar = <T>(
 };
 
 export const useRunHook = installGlobalVar(() => {
-  const instance = getCurrentInstance()
+  const instance = getCurrentInstance();
   return <R, T extends () => R = () => R>(hook: T): R => {
-    const back = getInstance
-    getInstance = () => instance
-    const result = hook()
-    getInstance = back
-    return result
-  }
-})
-
+    const back = getInstance;
+    getInstance = () => instance;
+    const result = hook();
+    getInstance = back;
+    return result;
+  };
+});
 
 export type InstanceProps = {
   id?: string;
@@ -207,17 +206,23 @@ export const usePointerPos = installGlobalVar(() => {
     if (!$stage) return;
 
     const mount = $stage.container().parentElement!;
+    const domMount = document.querySelector(`#${DomMountId}`) as HTMLDivElement;
     pos.value = $stage.pointerPos;
+    const moveHandler = (ev: MouseEvent) => {
+      pos.value = $stage.pointerPos;
+      if (!replayIng) {
+        lastClient = {
+          clientX: ev.clientX,
+          clientY: ev.clientY,
+        };
+      }
+    };
+
     onCleanup(
-      listener(mount, "pointermove", (ev) => {
-        pos.value = $stage.pointerPos;
-        if (pos.value && !replayIng) {
-          lastClient = {
-            clientX: ev.clientX,
-            clientY: ev.clientY,
-          };
-        }
-      })
+      mergeFuns(
+        listener(mount, "pointermove", moveHandler),
+        listener(domMount, "pointermove", moveHandler)
+      )
     );
   });
 
@@ -275,25 +280,24 @@ export const useDownKeys = installGlobalVar(() => {
   const keyKeys = reactive(new Set<string>());
   const mouseKeys = reactive(new Set<string>());
   const evHandler = (ev: KeyboardEvent | MouseEvent, keys: Set<string>) => {
-      ev.shiftKey ? keys.add("Shift") : keys.delete("Shift");
-      ev.altKey ? keys.add("Alt") : keys.delete("Alt");
-      ev.metaKey ? keys.add("Meta") : keys.delete("Meta");
-      ev.ctrlKey ? keys.add("Ctrl") : keys.delete("Ctrl");
-
-  }
+    ev.shiftKey ? keys.add("Shift") : keys.delete("Shift");
+    ev.altKey ? keys.add("Alt") : keys.delete("Alt");
+    ev.metaKey ? keys.add("Meta") : keys.delete("Meta");
+    ev.ctrlKey ? keys.add("Ctrl") : keys.delete("Ctrl");
+  };
   const cleanup = mergeFuns(
     listener(window, "keydown", (ev) => {
       if (!isEditableElement(ev.target as HTMLElement)) {
         keyKeys.add(ev.key);
-        evHandler(ev, keyKeys)
+        evHandler(ev, keyKeys);
       }
     }),
     listener(window, "keyup", (ev) => {
       keyKeys.delete(ev.key);
-      evHandler(ev, keyKeys)
+      evHandler(ev, keyKeys);
     }),
     listener(window, "mousemove", (ev) => {
-      evHandler(ev, mouseKeys)
+      evHandler(ev, mouseKeys);
     })
   );
   const keys = reactive(new Set<string>());
@@ -361,69 +365,73 @@ export const useForciblyShowItemIds = installGlobalVar(() => {
   return set;
 }, Symbol("forciblyShowItemId"));
 
-export const useRendererDOM = installGlobalVar(() => ref<HTMLDivElement>())
+export const useRendererDOM = installGlobalVar(() => ref<HTMLDivElement>());
 
 export const useTempStatus = installGlobalVar(() => {
   const temp = ref(false);
   const enterTemp = <T>(fn: () => T): T => {
-    temp.value = !import.meta.env.DEV && true
-    const result = fn()
+    temp.value = !import.meta.env.DEV && true;
+    const result = fn();
     if (result instanceof Promise) {
-      return result.then(async (data) => {
-        temp.value = false
-        await nextTick()
-        return data;
-      }).catch(r => {
-        temp.value = false
-        throw r
-      }) as T
-    } else {
-      temp.value = false
       return result
+        .then(async (data) => {
+          temp.value = false;
+          await nextTick();
+          return data;
+        })
+        .catch((r) => {
+          temp.value = false;
+          throw r;
+        }) as T;
+    } else {
+      temp.value = false;
+      return result;
     }
-  }
-  const dom = useRendererDOM()
+  };
+  const dom = useRendererDOM();
   watch(temp, (_a, _b, onCleanup) => {
     if (temp.value && dom.value) {
-      const instance = ElLoading.service({ fullscreen: true, target: dom.value })
-      onCleanup(() => instance.close())
+      const instance = ElLoading.service({
+        fullscreen: true,
+        target: dom.value,
+      });
+      onCleanup(() => instance.close());
     }
-  })
-  
+  });
 
-  return { tempStatus: temp, enterTemp }
+  return { tempStatus: temp, enterTemp };
 });
 
 const getFilters = <T>() => {
-  type Val = (d: T) => T
-  const globalFilter = ref<{[key in ShapeType]?: Val[]}>({})
-  const shapeFilter = ref<Record<string, Val[]>>({})
+  type Val = (d: T) => T;
+  const globalFilter = ref<{ [key in ShapeType]?: Val[] }>({});
+  const shapeFilter = ref<Record<string, Val[]>>({});
   const setShapeFilter = (id: string, descs: Val) => {
     if (shapeFilter.value[id]) {
-      shapeFilter.value[id].push(descs)
+      shapeFilter.value[id].push(descs);
     } else {
-      shapeFilter.value[id] = [descs]
+      shapeFilter.value[id] = [descs];
     }
     return () => {
       if (shapeFilter.value[id]) {
-        const ndx = shapeFilter.value[id].indexOf(descs)
-        shapeFilter.value[id].splice(ndx, 1)
+        const ndx = shapeFilter.value[id].indexOf(descs);
+        shapeFilter.value[id].splice(ndx, 1);
       }
-    }
-  }
+    };
+  };
   const setFilter = (type: ShapeType, descs: Val) => {
     if (globalFilter.value[type]) {
-      globalFilter.value[type].push(descs)
+      globalFilter.value[type].push(descs);
     } else {
-      globalFilter.value[type] = [descs]
+      globalFilter.value[type] = [descs];
     }
     return () => {
       if (globalFilter.value[type]) {
-        const ndx = globalFilter.value[type].indexOf(descs)
-        globalFilter.value[type].splice(ndx, 1)
+        const ndx = globalFilter.value[type].indexOf(descs);
+        globalFilter.value[type].splice(ndx, 1);
       }
-    }
-  }
+    };
+  };
 
   return {
     setFilter,
@@ -432,38 +440,35 @@ const getFilters = <T>() => {
       return (menus: T) => {
         if (globalFilter.value[type]) {
           for (const filter of globalFilter.value[type]) {
-            menus = filter(menus)
+            menus = filter(menus);
           }
         }
         if (shapeFilter.value[id]) {
           for (const filter of shapeFilter.value[id]) {
-            menus = filter(menus)
+            menus = filter(menus);
           }
         }
-        return menus
-      }
-    }
-  }
-}
-
+        return menus;
+      };
+    },
+  };
+};
 
 export const useMountMenusFilter = installGlobalVar(() => {
-  const menusFilter = getFilters<PropertyDescribes>()
+  const menusFilter = getFilters<PropertyDescribes>();
   return {
     setMenusFilter: menusFilter.setFilter,
     setShapeMenusFilter: menusFilter.setShapeFilter,
-    getFilter: menusFilter.getFilter
-  }
-})
-
-
+    getFilter: menusFilter.getFilter,
+  };
+});
 
 export const useMouseMenusFilter = installGlobalVar(() => {
-  type Menu = { icon?: any; label?: string; handler: () => void }
-  const propsFilter = getFilters<Menu[]>()
+  type Menu = { icon?: any; label?: string; handler: () => void };
+  const propsFilter = getFilters<Menu[]>();
   return {
     setMenusFilter: propsFilter.setFilter,
     setShapeMenusFilter: propsFilter.setShapeFilter,
-    getFilter: propsFilter.getFilter
-  }
-})
+    getFilter: propsFilter.getFilter,
+  };
+});

+ 108 - 0
src/core/hook/use-group.ts

@@ -0,0 +1,108 @@
+import { computed, reactive, Ref } from "vue";
+import { PropertyDescribes } from "../html-mount/propertys";
+import { installGlobalVar } from "./use-global-vars";
+import { inRevise } from "@/utils/shared";
+
+export const useGlobalDescribes = installGlobalVar(() => {
+  const shapesDescribes: Record<string, PropertyDescribes> = reactive({});
+  const data: Record<string, { id: string }> = reactive({});
+  return {
+    set(item: { id: string }, descs: PropertyDescribes) {
+      shapesDescribes[item.id] = descs;
+      data[item.id] = item;
+    },
+    del(id: string) {
+      delete shapesDescribes[id];
+    },
+    get(id: string) {
+      return (
+        shapesDescribes[id] && { desc: shapesDescribes[id], data: data[id] }
+      );
+    },
+  };
+});
+
+export const useComponentsDescribes = (ids: Ref<string[]>) => {
+  const gdesc = useGlobalDescribes();
+  const excludeKeys = ["length", "name"];
+  const groups = computed(() => {
+    return ids.value.map((id) => gdesc.get(id)).filter((item) => !!item);
+  });
+
+  const shareDescribes = computed(() => {
+    if (groups.value.length === 0) {
+      return {};
+    }
+
+    const shareDescribes: Record<
+      string,
+      { desc: PropertyDescribes[string][]; data: { id: string }[] }
+    > = {};
+    const shareKeys: string[] = Object.keys(groups.value[0].desc);
+
+    for (const item of groups.value) {
+      const keys = Object.keys(item.desc);
+      for (const key of keys) {
+        if (shareKeys.includes(key)) {
+          if (!shareDescribes[key]) {
+            shareDescribes[key] = { desc: [], data: [] };
+          } else {
+            const temp = shareDescribes[key].desc[0];
+            if (inRevise(temp.props, item.desc[key].props)) {
+              delete shareDescribes[key];
+              shareKeys.splice(shareKeys.indexOf(key), 1);
+              continue;
+            }
+          }
+          shareDescribes[key].desc.push(item.desc[key]);
+          shareDescribes[key].data.push(item.data);
+        }
+      }
+      for (let i = 0; i < shareKeys.length; i++) {
+        if (!keys.includes(shareKeys[i])) {
+          delete shareDescribes[shareKeys[i]];
+          shareKeys.splice(i--, 1);
+        }
+      }
+    }
+    return shareDescribes;
+  });
+
+  const mergeDesc = computed(() => {
+    const mergeDesc: Record<
+      string,
+      PropertyDescribes[string] & { joins: { id: string }[] }
+    > = {};
+    for (const key in shareDescribes.value) {
+      if (excludeKeys.includes(key)) continue;
+
+      const { desc: descs, data } = shareDescribes.value[key];
+      mergeDesc[key] = {
+        ...descs[0],
+        joins: data,
+      };
+      Object.defineProperty(mergeDesc[key], "value", {
+        get() {
+          const value = descs[0].value;
+          let i = 1;
+          for (; i < descs.length; i++) {
+            if (descs[i].value !== value) {
+              break;
+            }
+          }
+          return i === descs.length ? value : undefined;
+        },
+        set(val) {
+          for (const desc of descs) {
+            desc.value = val;
+          }
+          return true;
+        },
+      });
+      delete mergeDesc[key].default;
+    }
+    return mergeDesc;
+  });
+
+  return mergeDesc;
+};

+ 23 - 13
src/core/hook/use-interactive.ts

@@ -153,7 +153,7 @@ export const useInteractiveAreas = ({
           : position;
       }
 
-      prevEv = null
+      prevEv = null;
       pushNdx = -1;
       pushed = false;
       downed = false;
@@ -161,7 +161,7 @@ export const useInteractiveAreas = ({
       singleDone.value = true;
     };
 
-    let prevEv: any
+    let prevEv: any;
     return mergeFuns(
       listener(dom, "pointerdown", (ev) => {
         if (!can.dragMode) return;
@@ -185,7 +185,7 @@ export const useInteractiveAreas = ({
         const end = getOffset(ev, dom);
         const point = beforeHandler ? beforeHandler(end) : end;
 
-        prevEv = ev
+        prevEv = ev;
         if (downed) {
           if (pushed) {
             messages.value[pushNdx]![1] = point;
@@ -234,10 +234,11 @@ export const useInteractiveDots = ({
     let pushed = false;
     const empty = { x: -9999, y: -9999 };
     const pointer = ref(empty);
+    const beforePointer = ref(empty);
     enter && enter();
     mode.add(Mode.draging);
 
-    const move = (ev: MouseEvent) => {
+    const posMove = (position: Pos) => {
       if (!can.dragMode) return;
       if (!pushed) {
         messages.value.push(pointer.value);
@@ -246,10 +247,13 @@ export const useInteractiveDots = ({
       }
 
       moveIng = true;
-      const position = getOffset(ev);
       const current = beforeHandler ? beforeHandler(position) : position;
       pointer.value.x = current.x;
       pointer.value.y = current.y;
+      beforePointer.value = { ...position };
+    };
+    const move = (ev: MouseEvent) => {
+      posMove(getOffset(ev));
     };
 
     let prevPoint: Pos = { ...empty };
@@ -257,17 +261,23 @@ export const useInteractiveDots = ({
       () => {
         mode.del(Mode.draging);
       },
-      clickListener(dom, (_, ev) => {
+      watch(singleDone, () => {
+        if (singleDone.value) {
+          prevPoint = pointer.value;
+          const prevBeforePoint = beforePointer.value
+          pointer.value = { ...empty };
+          beforePointer.value = {...empty}
+          singleDone.value = true;
+          moveIng = false;
+          pushed = false;
+          nextTick(() => posMove(prevBeforePoint));
+        }
+      }),
+      clickListener(dom, (_) => {
         if (!moveIng || !can.dragMode || eqPoint(prevPoint, pointer.value))
           return;
-        prevPoint = pointer.value;
-
-        pointer.value = { ...empty };
         singleDone.value = true;
-        moveIng = false;
-        pushed = false;
-
-        nextTick(() => move(ev));
+        // nextTick(() => move(ev));
       }),
       listener(dom, "pointermove", move)
     );

+ 1 - 1
src/core/hook/use-mouse-status.ts

@@ -302,7 +302,7 @@ export const useMouseShapesStatus = installGlobalVar(() => {
             const ndx = selects.value.findIndex(
               (item) => item.id() === target?.id()
             );
-            
+            console.log(ndx, selects.value)
             if (~ndx) {
               selects.value.splice(ndx, 1);
             } else {

+ 14 - 9
src/core/hook/use-selection.ts

@@ -32,7 +32,7 @@ import {
   useViewerInvertTransform,
   useViewerInvertTransformConfig,
 } from "./use-viewer";
-import { debounce, diffArrayChange, mergeFuns, onlyId } from "@/utils/shared";
+import { copy, debounce, diffArrayChange, mergeFuns, onlyId } from "@/utils/shared";
 import { IRect } from "konva/lib/types";
 import { useMouseShapesStatus } from "./use-mouse-status";
 import Icon from "../components/icon/temp-icon.vue";
@@ -49,11 +49,11 @@ import mitt, { Emitter } from "mitt";
 import { components, ShapeType, shapeTypes } from "../components";
 import { getFlatChildren } from "@/utils/shape";
 
-export const useExcludeSelection = installGlobalVar(() => ref<string[]>([]))
+export const useExcludeSelection = installGlobalVar(() => ref<string[]>([]));
 // 多选不包含分组, 只包含选中者
 export const useSelection = installGlobalVar(() => {
   const layer = useHelperLayer();
-  const eSelection = useExcludeSelection()
+  const eSelection = useExcludeSelection();
   const getChildren = useGetFormalChildren();
   const box = new Rect({
     stroke: themeColor,
@@ -92,7 +92,10 @@ export const useSelection = installGlobalVar(() => {
     }
   };
 
+  const store = useStore();
   const init = (dom: HTMLDivElement, layer: Layer) => {
+    store.bus.on("addItemAfter", updateInitData);
+    store.bus.on('dataChangeAfter', updateInitData);
     const stopListener = dragListener(dom, {
       down(pos) {
         layer.add(box);
@@ -112,6 +115,8 @@ export const useSelection = installGlobalVar(() => {
       },
     });
     return () => {
+      store.bus.off("addItemAfter", updateInitData);
+      store.bus.off('dataChangeAfter', updateInitData);
       stopListener();
       box.remove();
     };
@@ -298,17 +303,17 @@ export const useSelectionRevise = () => {
   useShapesIcon(computed(() => status.selects.concat(rectSelects.value || [])));
 
   const filterSelect = debounce(() => {
-    const selects: EntityShape[] = [];
+    const selects = new Set<EntityShape>();
     for (const shape of status.selects) {
-      const children = getFlatChildren(shape)
-      children.forEach(childShape => {
+      const children = getFlatChildren(shape);
+      children.forEach((childShape) => {
         const manage = getShapeSelectionManage(childShape);
         if (manage?.canSelect(childShape)) {
-          selects.push(childShape);
+          selects.add(childShape);
         }
-      })
+      });
     }
-    setSelectShapes(selects);
+    setSelectShapes([...selects]);
   }, 16);
   store.bus.on("delItemAfter", filterSelect);
   store.bus.on("clearAfter", filterSelect);

+ 2 - 1
src/core/hook/use-status.ts

@@ -87,7 +87,8 @@ export const useOperMode = installGlobalVar(() => {
 
   return computed(() => ({
     // 多选模式
-    mulSelection: keys.has('Shift') && !keys.has(' ') && !keys.has('Alt'),
+    mulSelection: keys.has('Ctrl') && !keys.has(' ') && !keys.has('Alt'),
+    // mulSelection: keys.has('Meta') && !keys.has(' ') && !keys.has('Alt'),
     // mulSelection: false,
     // 自由移动视图
     freeView: keys.has(' '),

+ 0 - 1
src/core/html-mount/propertys/components/color.vue

@@ -28,7 +28,6 @@ const emit = defineEmits<{
   (e: "update:value", val: string | null): string;
   (e: "change"): void;
 }>();
-
 const predefineColors = ref<Array<string | null>>([
   "#000000",
   "#FFFFFF",

+ 2 - 2
src/core/html-mount/propertys/components/input-num.vue

@@ -2,7 +2,7 @@
   <div>
     <el-input-number
       :controls="false"
-      :modelValue="props.proportion ? transform(value) : value"
+      :modelValue="value ? (props.proportion ? transform(value) : value) : min"
       @update:model-value="(val: any) => props.proportion ? changeHandler(invTransform(val), val) : changeHandler(val, val)"
       @change="$emit('change')"
       style="width: 98px"
@@ -24,7 +24,7 @@ import { useProportion } from "@/core/hook/use-proportion";
 import { ElInputNumber } from "element-plus";
 
 const props = defineProps<{
-  value: number;
+  value?: number;
   min?: number;
   max?: number;
   proportion?: boolean;

+ 2 - 4
src/core/html-mount/propertys/components/num.vue

@@ -3,7 +3,7 @@
     <el-slider
       class="property-num-slider"
       :class="{ proportion: props.proportion }"
-      :modelValue="props.proportion ? transform(value) : value"
+      :modelValue="value ? (props.proportion ? transform(value) : value) : min"
       @update:model-value="(val: any) => props.proportion ? changeHandler(invTransform(val)) : changeHandler(val)"
       @change="$emit('change')"
       size="small"
@@ -28,15 +28,13 @@ import { ElSlider } from "element-plus";
 
 const props = defineProps<{
   data?: Record<string, any>;
-  value: number;
+  value?: number;
   min?: number;
   max?: number;
   step?: number;
   proportion?: boolean;
 }>();
-
 const { proportion, transform, invTransform } = useProportion();
-
 const emit = defineEmits<{
   (e: "update:value", val: number): void;
   (e: "change"): void;

+ 189 - 0
src/core/html-mount/propertys/mount-describes.vue

@@ -0,0 +1,189 @@
+<template>
+  <Teleport :to="`#${DomOutMountId}`">
+    <transition name="mount-fade">
+      <div class="mount-layout" v-if="show">
+        <div v-if="name" class="title">
+          <h4>设置{{ name }}</h4>
+          <icon name="close" size="20px" class="operate" @click="emit('close')" />
+        </div>
+        <div :size="8" class="mount-controller">
+          <div
+            v-for="[key, val] in describeItems"
+            :key="key"
+            class="mount-item"
+            :class="val['layout-type'] || 'column'"
+          >
+            <span class="label">{{ val.label }}</span>
+            <component
+              :data="data"
+              v-bind="describes[key].props"
+              :value="
+                'value' in describes[key]
+                  ? describes[key].value
+                  : props.data && props.data[key]
+              "
+              @update:value="(val: any) => {
+                updateValue(key, val)
+                describes[key].isChange = true
+              }"
+              @change="
+                () => {
+                  changeHandler();
+                  describes[key].onChange && describes[key].onChange();
+                  describes[key].isChange = false;
+                }
+              "
+              :is="propertyComponents[val.type]"
+              :key="key"
+            />
+          </div>
+        </div>
+        <div class="mount-bottom" v-if="calDelete">
+          <el-button type="danger" plain @click="emit('delete')" class="del-btn">
+            删除
+          </el-button>
+        </div>
+      </div>
+    </transition>
+  </Teleport>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref, watch } from "vue";
+import { useMode } from "../../hook/use-status.ts";
+import { PropertyDescribes, propertyComponents } from "./index.ts";
+import { DomOutMountId } from "@/constant/index.ts";
+import { Mode } from "@/constant/mode.ts";
+import { debounce } from "@/utils/shared.ts";
+import { ElButton } from "element-plus";
+
+const props = defineProps<{
+  show?: boolean;
+  name?: string;
+  data?: Record<string, any>;
+  describes: PropertyDescribes;
+  calDelete?: boolean;
+}>();
+const emit = defineEmits<{
+  (e: "change"): void;
+  (e: "delete"): void;
+  (e: "close"): void;
+}>();
+
+const describeItems = computed(() => {
+  return Object.entries(props.describes).sort(
+    ([_a, a], [_b, b]) => (b.sort || -1) - (a.sort || -1)
+  );
+});
+
+const mode = useMode();
+const hidden = computed(
+  () => !props.show || mode.value.has(Mode.draw) || mode.value.has(Mode.draging)
+);
+const show = ref(false);
+watch(
+  hidden,
+  debounce(() => {
+    show.value = !hidden.value;
+  }, 32),
+  { immediate: true }
+);
+
+let isUpdate = false;
+const updateValue = (key: string, val: any) => {
+  if ("value" in props.describes[key]) {
+    props.describes[key].value = val;
+  } else {
+    props.data![key] = val;
+  }
+  isUpdate = true;
+};
+
+watch(hidden, (nHidden, oHidden) => {
+  if (nHidden && nHidden !== oHidden && isUpdate) {
+    isUpdate = false;
+    emit("change");
+    nHidden && emit("close");
+  }
+});
+
+const changeHandler = () => {
+  isUpdate = false;
+  emit("change");
+};
+</script>
+
+<style lang="scss" scoped>
+.mount-layout {
+  pointer-events: all;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  position: absolute;
+  border-left: 1px solid #e6e6e6;
+  background-color: rgba(255, 255, 255, 1);
+  overflow-y: auto;
+  padding: 6px 16px;
+  color: #333333;
+  width: 280px;
+  font-size: 14px;
+  box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12);
+  overflow: hidden;
+
+  .mount-controller {
+    border-bottom: 1px solid #f0f0f0;
+    margin-bottom: 24px;
+
+    .mount-item {
+      margin-bottom: 24px;
+
+      &.column {
+        .label {
+          display: block;
+          margin-bottom: 8px;
+        }
+      }
+
+      &.row {
+        display: flex;
+        flex-wrap: wrap;
+        align-items: center;
+        justify-content: space-between;
+      }
+    }
+  }
+}
+
+.title {
+  display: flex;
+  font-size: 16px;
+  padding: 19px 0 25px;
+  align-items: center;
+  justify-content: space-between;
+
+  h3 {
+    font-size: inherit;
+    color: #333333;
+    align-items: center;
+  }
+}
+
+.mount-fade-enter-active,
+.mount-fade-leave-active {
+  transition: transform 0.3s ease, opacity 0.3s ease;
+}
+
+.mount-fade-enter-from,
+.mount-fade-leave-to {
+  transform: translateX(100%);
+  opacity: 0;
+}
+
+.del-btn {
+  width: 100%;
+  font-size: 14px;
+  height: 34px;
+  background: none;
+  color: var(--el-color-danger);
+}
+</style>

+ 20 - 190
src/core/html-mount/propertys/mount.vue

@@ -1,79 +1,35 @@
 <template>
-  <Teleport :to="`#${DomOutMountId}`" v-if="stage">
-    <transition name="mount-fade">
-      <div class="mount-layout" v-if="show">
-        <div
-          v-if="name || data?.itemName || (type && components[type].shapeName)"
-          class="title"
-        >
-          <h4>
-            设置{{ name || data?.itemName || (type && components[type].shapeName) }}
-          </h4>
-          <icon
-            name="close"
-            size="20px"
-            class="operate"
-            @click="shapesStatus.actives = []"
-          />
-        </div>
-        <div :size="8" class="mount-controller">
-          <div
-            v-for="[key, val] in describeItems"
-            :key="key"
-            class="mount-item"
-            :class="val['layout-type'] || 'column'"
-          >
-            <span class="label">{{ val.label }}</span>
-            <component
-              :data="data"
-              v-bind="{ ...(describes[key].props || {}), ...getPredefine(key) }"
-              :value="
-                'value' in describes[key]
-                  ? describes[key].value
-                  : props.data && props.data[key]
-              "
-              @update:value="(val: any) => {
-                updateValue(key, val)
-                describes[key].isChange = true
-              }"
-              @change="
-                () => {
-                  changeHandler();
-                  describes[key].onChange && describes[key].onChange();
-                  describes[key].isChange = false;
-                }
-              "
-              :is="propertyComponents[val.type]"
-              :key="key"
-            />
-          </div>
-        </div>
-        <div class="mount-bottom" v-if="!data?.disableDelete">
-          <el-button type="danger" plain @click="emit('delete')" class="del-btn">
-            删除
-          </el-button>
-        </div>
-      </div>
-    </transition>
-  </Teleport>
+  <MountDescribes
+    :name="name || data?.itemName || (type && components[type].shapeName)"
+    :show="!hidden"
+    :describes="describes"
+    :data="data"
+    :cal-delete="!data?.disableDelete"
+    @close="
+      () => {
+        shapesStatus.actives = [];
+        emit('close');
+      }
+    "
+    @change="emit('change')"
+    @delete="emit('delete')"
+  />
 </template>
 
 <script lang="ts" setup>
-import { computed, ref, watch } from "vue";
-import { useMountMenusFilter, useStage } from "../../hook/use-global-vars.ts";
+import { computed } from "vue";
+import { useStage } from "../../hook/use-global-vars.ts";
 import { useMode } from "../../hook/use-status.ts";
-import { PropertyDescribes, propertyComponents } from "./index.ts";
+import { PropertyDescribes } from "./index.ts";
 import { DC, EntityShape } from "@/deconstruction.js";
 import {
   useMouseShapesStatus,
   useMouseShapeStatus,
 } from "../../hook/use-mouse-status.ts";
-import { DomOutMountId } from "@/constant/index.ts";
 import { Mode } from "@/constant/mode.ts";
-import { debounce } from "@/utils/shared.ts";
 import { useStore } from "@/core/store/index.ts";
-import { components, DrawItem } from "@/core/components/index.ts";
-import { ElButton } from "element-plus";
+import { components } from "@/core/components/index.ts";
+import MountDescribes from "./mount-describes.vue";
 
 const props = defineProps<{
   show?: boolean;
@@ -93,27 +49,6 @@ const id = computed(() => props.target?.getNode().id());
 const type = computed(() => id.value && store.getType(id.value));
 const data = computed(() => (id.value ? store.getItemById(id.value) : props.data));
 
-const getPredefine = (key: string) => {
-  if (!type.value) return;
-  const getPredefine = components[type.value].getPredefine;
-  const predefine = getPredefine && (getPredefine as any)(key as keyof DrawItem);
-  return predefine || {};
-};
-const { getFilter } = useMountMenusFilter();
-const describes = computed(() => {
-  if (type.value && id.value) {
-    return getFilter(type.value, id.value)(props.describes);
-  } else {
-    return props.describes;
-  }
-});
-
-const describeItems = computed(() => {
-  return Object.entries(describes.value).sort(
-    ([_a, a], [_b, b]) => (b.sort || -1) - (a.sort || -1)
-  );
-});
-
 const stage = useStage();
 const status = useMouseShapeStatus(computed(() => props.target));
 const shapesStatus = useMouseShapesStatus();
@@ -127,109 +62,4 @@ const hidden = computed(
       mode.value.has(Mode.draw) ||
       mode.value.has(Mode.draging))
 );
-const show = ref(false);
-watch(
-  hidden,
-  debounce(() => {
-    show.value = !hidden.value;
-  }, 32)
-);
-
-let isUpdate = false;
-const updateValue = (key: string, val: any) => {
-  if ("value" in describes.value[key]) {
-    describes.value[key].value = val;
-  } else {
-    props.data![key] = val;
-  }
-  isUpdate = true;
-};
-
-watch(hidden, (nHidden, oHidden) => {
-  if (nHidden && nHidden !== oHidden && isUpdate) {
-    isUpdate = false;
-    emit("change");
-  }
-});
-
-const changeHandler = () => {
-  isUpdate = false;
-  emit("change");
-  console.log("change handler");
-};
 </script>
-
-<style lang="scss" scoped>
-.mount-layout {
-  pointer-events: all;
-  right: 0;
-  top: 0;
-  bottom: 0;
-  position: absolute;
-  border-left: 1px solid #e6e6e6;
-  background-color: rgba(255, 255, 255, 1);
-  overflow-y: auto;
-  padding: 6px 16px;
-  color: #333333;
-  width: 280px;
-  font-size: 14px;
-  box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12);
-  overflow: hidden;
-
-  .mount-controller {
-    border-bottom: 1px solid #f0f0f0;
-    margin-bottom: 24px;
-
-    .mount-item {
-      margin-bottom: 24px;
-
-      &.column {
-        .label {
-          display: block;
-          margin-bottom: 8px;
-        }
-      }
-
-      &.row {
-        display: flex;
-        flex-wrap: wrap;
-        align-items: center;
-        justify-content: space-between;
-      }
-    }
-  }
-}
-
-.title {
-  display: flex;
-  font-size: 16px;
-  padding: 19px 0 25px;
-  align-items: center;
-  justify-content: space-between;
-
-  h3 {
-    font-size: inherit;
-    color: #333333;
-    align-items: center;
-  }
-}
-
-.mount-fade-enter-active,
-.mount-fade-leave-active {
-  transition: transform 0.3s ease, opacity 0.3s ease;
-}
-
-.mount-fade-enter-from,
-.mount-fade-leave-to {
-  transform: translateX(100%);
-  opacity: 0;
-}
-
-.del-btn {
-  width: 100%;
-  font-size: 14px;
-  height: 34px;
-  background: none;
-  color: var(--el-color-danger);
-}
-</style>

+ 2 - 1
src/example/components/slide/slide-icons.vue

@@ -67,7 +67,8 @@ const drawIcon = async (item: IconItem) => {
 };
 const activeName = computed(() => (props.draw.presetAdd?.preset as any)?.name);
 
-const activeGroups = computed(() => props.groups.map((item) => item.name));
+const activeGroups = ref(props.groups.map((item) => item.name));
+console.log(activeGroups.value);
 const keyword = ref("");
 const searchGroups = computed(() => {
   return props.groups

+ 8 - 12
src/example/dialog/ai/ai.vue

@@ -18,13 +18,13 @@
     </div>
     <div v-if="scene" class="tagging-layout">
       <p class="title">图例</p>
-      <el-radio-group v-model="syncTag" style="width: 200px">
-        <el-radio
+      <ElCheckboxGroup v-model="syncTags" style="width: 200px">
+        <ElCheckbox
           :label="item.label"
           :value="item.value"
           v-for="item in options[scene.type]"
         />
-      </el-radio-group>
+      </ElCheckboxGroup>
     </div>
   </div>
 </template>
@@ -32,7 +32,7 @@
 <script lang="ts" setup>
 import { computed, ref, watch } from "vue";
 import VR from "../vr/vr.vue";
-import { ElRadioGroup, ElRadio, ElMessage, ElSelect, ElOption } from "element-plus";
+import { ElCheckboxGroup, ElCheckbox, ElMessage, ElSelect, ElOption } from "element-plus";
 import { Scene, SCENE_TYPE } from "../../platform/platform-resource";
 import { getFloors } from "../../platform/resource-swkk";
 import { SelectSceneData } from ".";
@@ -46,7 +46,7 @@ const options = {
   [SCENE_TYPE.cloud]: [{ value: "hot", label: "热点" }],
   [SCENE_TYPE.fuse]: [{ value: "hot", label: "标签" }],
 };
-const syncTag = ref<string>();
+const syncTags = ref<string[]>([]);
 const syncFloor = ref<string>();
 const floors = ref<{ name: string }[]>([]);
 
@@ -57,9 +57,9 @@ watch(scene, () => {
 
   const curToken = ++token;
   if (!scene.value) {
-    syncTag.value = undefined;
+    syncTags.value = [];
   } else {
-    syncTag.value = options[scene.value.type][0].value;
+    syncTags.value = [options[scene.value.type][0].value];
     if (scene.value.type === SCENE_TYPE.mesh) {
       getFloors(scene.value).then((data) => {
         if (token === curToken) {
@@ -78,10 +78,6 @@ defineExpose({
       ElMessage.error("请选择同步场景");
       throw "请选择同步场景";
     }
-    if (!syncTag.value) {
-      ElMessage.error("请选择同步标签");
-      throw "请选择同步标签";
-    }
     if (!syncFloor.value && floors.value.length) {
       ElMessage.error("请选择楼层");
       throw "请选择楼层";
@@ -89,7 +85,7 @@ defineExpose({
     return {
       scene: scene.value,
       floorName: syncFloor.value || floors.value[0].name,
-      syncs: [syncTag.value],
+      syncs: syncTags.value,
     };
   },
 });

+ 18 - 7
src/example/fuse/views/defStyle.ts

@@ -1,4 +1,5 @@
 import { defaultStyle as iconDefStyle } from "@/core/components/icon";
+import { defaultStyle as lineIconDefStyle } from "@/core/components/line-icon";
 import { defaultStyle as rectDefStyle } from "@/core/components/rectangle";
 import { defaultStyle as circleDefStyle } from "@/core/components/circle";
 import { defaultStyle as triangleDefStyle } from "@/core/components/triangle";
@@ -16,6 +17,7 @@ import { getRealPixel } from "./tabulation/gen-tab";
 import { Draw } from "@/example/components/container/use-draw";
 import { ShapeType } from "@/index";
 import { watch } from "vue";
+import { PropertyDescribes } from "@/core/html-mount/propertys";
 
 const setDefStyle = <T extends {}>(
   sys: T,
@@ -115,14 +117,16 @@ export const tabCustomStyle = (p: PaperKey, draw: Draw) => {
 };
 
 export const overviewCustomStyle = (draw: Draw) => {
+  const filterIcon = (data: PropertyDescribes) => {
+    data.strokeWidth.props = {
+      ...data.strokeWidth.props,
+      proportion: true,
+    };
+    return data;
+  };
   const backs = [
-    draw.mountFilter.setMenusFilter("icon", (data) => {
-      data.strokeWidth.props = {
-        ...data.strokeWidth.props,
-        proportion: true,
-      };
-      return data;
-    }),
+    draw.mountFilter.setMenusFilter("icon", filterIcon),
+    draw.mountFilter.setMenusFilter("lineIcon", filterIcon),
     setDefStyle(
       iconDefStyle,
       {
@@ -130,6 +134,13 @@ export const overviewCustomStyle = (draw: Draw) => {
       } as any,
       "icon"
     ),
+    setDefStyle(
+      lineIconDefStyle,
+      {
+        strokeWidth: 1,
+      } as any,
+      "icon"
+    ),
   ];
   return mergeFuns(backs);
 };

+ 43 - 17
src/utils/dom.ts

@@ -1,3 +1,5 @@
+import { listener } from "./event";
+
 function getCaretPosition(
   element: HTMLInputElement | HTMLTextAreaElement,
   x: number,
@@ -58,16 +60,18 @@ $fileInput.style = `
 export const selectFile = (multiple = false, accept = "") =>
   new Promise<File[]>((resolve, reject) => {
     $fileInput.multiple = multiple;
-    const accepts = accept.split(',').flatMap(s => [s.trim().toLowerCase(), s.trim().toUpperCase()])
-    $fileInput.accept = accepts.join(',');
+    const accepts = accept
+      .split(",")
+      .flatMap((s) => [s.trim().toLowerCase(), s.trim().toUpperCase()]);
+    $fileInput.accept = accepts.join(",");
     $fileInput.onchange = () => {
-      console.log('0.0')
+      console.log("0.0");
       $fileInput.accept = "";
       $fileInput.multiple = false;
-      const files = [...$fileInput.files!]
+      const files = [...$fileInput.files!];
       for (const f of files) {
-        if (!accepts.some(ac => f.name.endsWith(ac))) {
-          reject(new Error(`仅支持${accept}格式文件`))
+        if (!accepts.some((ac) => f.name.endsWith(ac))) {
+          reject(new Error(`仅支持${accept}格式文件`));
           return;
         }
       }
@@ -75,15 +79,15 @@ export const selectFile = (multiple = false, accept = "") =>
       $fileInput.value = "";
     };
     $fileInput.oncancel = () => {
-      reject(`取消选择`)
-    }
+      reject(`取消选择`);
+    };
     $fileInput.click();
   });
 
 export const grayscaleImage = (
   img: HTMLImageElement,
   rgbWeight = [0.3, 0.59, 0.11],
-  format = 'image/jpeg'
+  format = "image/jpeg"
 ): Promise<Blob> => {
   const canvas = document.createElement("canvas");
   canvas.width = img.width;
@@ -121,21 +125,21 @@ export const grayscaleImage = (
 
 export const isEditableElement = (target: HTMLElement): boolean => {
   // 检查常规输入元素
-  if (['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName.toUpperCase())) {
+  if (["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName.toUpperCase())) {
     return true;
   }
-  
+
   // 检查可编辑的div或其他元素
   if (target.isContentEditable) {
     return true;
   }
-  
+
   // 检查角色属性为文本输入的元素
-  const role = target.getAttribute('role');
-  if (role === 'textbox' || role === 'textarea') {
+  const role = target.getAttribute("role");
+  if (role === "textbox" || role === "textarea") {
     return true;
   }
-  
+
   // 检查是否是富文本编辑器中的元素
   let parent = target.parentElement;
   while (parent) {
@@ -144,7 +148,29 @@ export const isEditableElement = (target: HTMLElement): boolean => {
     }
     parent = parent.parentElement;
   }
-  
+
   return false;
-}
+};
 
+export const getKeywordInput = (
+  onInput: (data: string, prev: string) => string,
+  onSubmit: (data: string) => void
+) => {
+  let inputStr = "";
+  return {
+    clear: () => (inputStr = ""),
+    stop: listener(document.body, "keydown", (ev) => {
+      if (ev.key !== "Enter") {
+        const oldStr = inputStr
+        if (ev.key === 'Backspace' || ev.key === 'Delete') {
+          inputStr = inputStr.substring(0, inputStr.length - 1)
+        } else {
+          inputStr += ev.key
+        }
+        inputStr = onInput(inputStr, oldStr);
+      } else {
+        onSubmit(inputStr);
+      }
+    }),
+  };
+};