Forráskód Böngészése

feat: 修改需求

bill 2 hónapja
szülő
commit
9ec2c7a3d1

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

@@ -63,7 +63,7 @@ const { shape, tData, operateMenus, describes, data } = useComponentStatus<
   // alignment(data, mat) {
   //   return matResponse({ mat, data, increment: true });
   // },
-  // transformType: "line",
+  transformType: "line",
   getRepShape(): Line {
     return new Line({
       fill: themeColor,

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

@@ -80,6 +80,7 @@ const dragPointIds = ref<string[]>();
 let track = false;
 let ctx: NLineDataCtx;
 const updateBeforeHandler = (ids: string[]) => {
+  console.log("before");
   dragPointIds.value = ids;
   track = true;
   ctx = getInitCtx();

+ 1 - 0
src/core/components/share/edit-point.vue

@@ -126,6 +126,7 @@ watch(offset, (offset, oldOffsert) => {
   snap.clear();
   if (!oldOffsert) {
     init = { ...position.value };
+    emit("dragstart");
     startHandler();
     dragIng.value = true;
     onUmHooks.push(() => {

+ 1 - 0
src/core/components/util.ts

@@ -15,6 +15,7 @@ export type BaseItem = {
   opacity: number
   key?: string
   layer: string
+  itemName?: string
   ref: boolean
   hide?: boolean
   listening?: boolean

+ 6 - 2
src/core/hook/use-history.ts

@@ -174,13 +174,17 @@ export class DrawHistory {
     }
   }
 
-  onceTrack(fn: () => any) {
+  onceTrack<T>(fn: () => T): T {
     this.onceFlag++
     const result = fn();
     if (result instanceof Promise) {
-      result.then(() => this.onceTrackCallback())
+      return result.then((data) => {
+        this.onceTrackCallback()
+        return data
+      }) as T
     } else {
       this.onceTrackCallback()
+      return result
     }
   }
 

+ 11 - 4
src/core/html-mount/propertys/mount.vue

@@ -2,8 +2,11 @@
   <Teleport :to="`#${DomOutMountId}`" v-if="stage">
     <transition name="mount-fade">
       <div class="mount-layout" v-if="show">
-        <div v-if="name || (type && components[type].shapeName)" class="title">
-          <h4>设置{{ name || (type && components[type].shapeName) }}</h4>
+        <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"
@@ -88,9 +91,9 @@ const getPredefine = (key: string) => {
 const { getFilter } = useMountMenusFilter();
 const describes = computed(() => {
   if (type.value && id.value) {
-    return getFilter(type.value, id.value)(props.describes)
+    return getFilter(type.value, id.value)(props.describes);
   } else {
-    return props.describes
+    return props.describes;
   }
 });
 
@@ -165,6 +168,7 @@ const changeHandler = () => {
   .mount-controller {
     border-bottom: 1px solid #f0f0f0;
     margin-bottom: 24px;
+
     .mount-item {
       margin-bottom: 24px;
 
@@ -184,12 +188,14 @@ const changeHandler = () => {
     }
   }
 }
+
 .title {
   display: flex;
   font-size: 16px;
   padding: 19px 0 25px;
   align-items: center;
   justify-content: space-between;
+
   h3 {
     font-size: inherit;
     color: #333333;
@@ -207,6 +213,7 @@ const changeHandler = () => {
   transform: translateX(100%);
   opacity: 0;
 }
+
 .del-btn {
   width: 100%;
   font-size: 14px;

+ 6 - 6
src/example/components/header/actions.ts

@@ -88,13 +88,13 @@ export const getHeaderActions = (draw: Draw) => {
       icon: "download",
       children: [
         {
-          handler: async () => {
+          handler: async (filename = 'canvas') => {
             draw.enterTemp(async () => {
               const oldShowGrid = draw.config.showGrid
               draw.config.showGrid = false
               const pop = draw.mode.push(Mode.readonly)
               await nextTick()
-              saveAs(await getImage(draw, 'image/jpeg'), "canvas.jpg");
+              saveAs(await getImage(draw, 'image/jpeg'), `${filename}.jpg`);
               pop()
               draw.config.showGrid = oldShowGrid
             })
@@ -103,7 +103,7 @@ export const getHeaderActions = (draw: Draw) => {
           icon: "a-visible",
         },
         {
-          handler: () => {
+          handler: (filename = 'canvas') => {
             draw.enterTemp(async () => {
               const oldBack = draw.config.back && {...draw.config.back}
               const oldShowGrid = draw.config.showGrid
@@ -111,7 +111,7 @@ export const getHeaderActions = (draw: Draw) => {
               draw.config.showGrid = false
               draw.config.back = undefined
               await nextTick()
-              await saveAs(await getImage(draw, 'image/png'), "canvas.png");
+              await saveAs(await getImage(draw, 'image/png'), `${filename}.png`);
               pop()
               draw.config.back = oldBack
               draw.config.showGrid = oldShowGrid
@@ -122,10 +122,10 @@ export const getHeaderActions = (draw: Draw) => {
           icon: "a-visible",
         },
         {
-          handler: async () => {
+          handler: async (filename = 'canvas') => {
             draw.enterTemp(async () => {
               const dxf = await draw.getDXF()
-              saveAs(dxf, "canvas.zip");
+              saveAs(dxf, `${filename}.zip`);
             })
           },
           text: "DXF",

+ 23 - 24
src/example/components/slide/actions.ts

@@ -1,7 +1,7 @@
 import { DrawItem, shapeNames, ShapeType } from "@/index";
+import { ImageData, defaultStyle } from "@/core/components/image/index";
 import { v4 as uuid } from "uuid";
 import { getAMapInfo } from "../../dialog/basemap";
-import { getImageSize } from "@/utils/shape";
 import { selectAI } from "../../dialog/ai";
 import { drawPlatformResource } from "../../platform/platform-draw";
 import { selectFile } from "@/utils/dom";
@@ -13,6 +13,8 @@ import { ElMessage } from "element-plus";
 import { getRealPixel } from "@/example/fuse/views/tabulation/gen-tab";
 import { nextTick } from "vue";
 import { Rect } from "konva/lib/shapes/Rect";
+import { Size } from "@/utils/math";
+import { getBaseItem } from "@/core/components/util";
 
 export type PresetAdd<T extends ShapeType = ShapeType> = {
   type: T;
@@ -85,10 +87,14 @@ export const imp: MenuItem = {
           ElMessage.error(e.message);
           return;
         }
+        if (files[0].size >= 100 * 1024 * 1024) {
+          ElMessage.error("图片大小不超过100MB");
+          return;
+        }
+
         const url = await window.platform.uploadResourse(files[0]);
         const image = await getImage(url);
         ElMessage.warning("请在画图面板中选择放置位置,鼠标右键取消");
-        console.log(image);
         draw.enterDrawShape(
           "image",
           {
@@ -169,6 +175,18 @@ export const paper = {
   ],
 };
 
+export const getMapImageItem = (url: string, size: Size) => ({
+  ...getBaseItem(),
+  ...defaultStyle,
+  cornerRadius: 0,
+  width: size.width * 100,
+  height: size.height * 100,
+  zIndex: -100,
+  url,
+  lock: true,
+  mat: [1, 0, 0, 1, 0, 0],
+});
+
 export const dbImage: MenuItem = {
   value: uuid(),
   icon: "",
@@ -180,30 +198,10 @@ export const dbImage: MenuItem = {
       name: "高德地图",
       handler: async (draw: Draw) => {
         const info = await getAMapInfo();
-        const size = await getImageSize(info.blob);
-        let proportion = { scale: info.ratio * 100, unit: "mm" };
-        if (info.ratio > 1000) {
-          proportion = { scale: info.ratio / 1000, unit: "km" };
-        } else if (info.ratio > 1) {
-          proportion = { scale: info.ratio, unit: "m" };
-        }
         const url = await window.platform.uploadResourse(
           new File([info.blob], "map.png")
         );
-
-        draw.history.onceTrack(() => {
-          draw.addShape(
-            "image",
-            {
-              ...size,
-              url,
-              zIndex: -1,
-            },
-            { x: window.innerWidth / 2, y: window.innerHeight / 2 },
-            true
-          );
-          draw.store.setConfig({ proportion });
-        });
+        draw.store.addItem('image', getMapImageItem(url, info.size))
       },
     },
   ],
@@ -215,6 +213,7 @@ export const test: MenuItem = {
   name: "测试",
   type: "sub-menu-horizontal",
   children: [
+    ...dbImage.children!,
     {
       value: uuid(),
       icon: "",
@@ -289,7 +288,7 @@ export const test: MenuItem = {
           y: (size.height - viewSize.height) / 2,
           ...viewSize,
         };
-        draw.stage?.children[2].add(new Rect({ ...rect, fill: 'red' }))
+        draw.stage?.children[2].add(new Rect({ ...rect, fill: "red" }));
       },
     },
   ],

+ 12 - 6
src/example/dialog/basemap/gd-map/selectAMapImage.vue

@@ -8,7 +8,7 @@
       clearable
     >
       <template #append>
-        <ui-icon name="search" />
+        <icon name="search" />
       </template>
     </el-input>
     <div class="rrr">
@@ -36,7 +36,7 @@
 
 <script setup lang="ts">
 import AMapLoader from "@amap/amap-jsapi-loader";
-import { ElInput, ElButton } from "element-plus";
+import { ElInput } from "element-plus";
 import { ref, watch } from "vue";
 import { analysisGPS, debounce } from "@/utils/shared";
 import { Size } from "@/utils/math";
@@ -182,7 +182,7 @@ watch([mapEl, resultEl], async (_a, _b, onCleanup) => {
   onCleanup(await init());
 });
 
-const getPixelAspectRatio = () => {
+const getBoxSize = () => {
   // 获取地图视口的经纬度范围
   const bounds = map.getBounds();
   // 计算视口的宽度和高度(单位为米)
@@ -192,8 +192,14 @@ const getPixelAspectRatio = () => {
     [southWest.lng, northEast.lat],
     [northEast.lng, northEast.lat]
   );
-  console.log(width / (mapEl.value!.offsetWidth * props.pixelRatio));
-  return width / (mapEl.value!.offsetWidth * props.pixelRatio);
+  const height = AMap.GeometryUtil.distance(
+    [northEast.lng, southWest.lat],
+    [northEast.lng, northEast.lat]
+  );
+  return {
+    width,
+    height,
+  };
 };
 
 const submit = () => {
@@ -239,7 +245,7 @@ const submit = () => {
         tempCanvas.height
       );
       tempCanvas.toBlob(
-        (blob) => resolve({ blob: blob!, info, ratio: getPixelAspectRatio() }),
+        (blob) => resolve({ blob: blob!, info, size: getBoxSize() }),
         "jpeg",
         1
       );

+ 1 - 1
src/example/dialog/basemap/index.ts

@@ -2,7 +2,7 @@ import { markRaw, reactive } from "vue";
 import selectAMapImage from "./gd-map/selectAMapImage.vue";
 import { Size } from "@/utils/math";
 
-export type BasemapInfo = { blob: Blob; info?: MarkInfo, ratio: number };
+export type BasemapInfo = { blob: Blob; info?: MarkInfo, size: Size };
 
 type Props = {
   title: string;

+ 1 - 1
src/example/dialog/expose/expose-format.vue

@@ -8,7 +8,7 @@
     >
       <el-form-item label="格式:">
         <el-radio-group v-model="data.format">
-          <el-radio value="JPEG">JPEG</el-radio>
+          <el-radio value="JPEG">JPG</el-radio>
           <el-radio value="PDF">PDF</el-radio>
         </el-radio-group>
       </el-form-item>

+ 17 - 8
src/example/fuse/enter.ts

@@ -84,7 +84,7 @@ const login = (isBack = true) => {
     return;
   }
 
-  console.log(import.meta.env.VITE_LOGIN_VIEW)
+  console.log(import.meta.env.VITE_LOGIN_VIEW);
   if (import.meta.env.VITE_LOGIN_VIEW) {
     const p: any = { ...params.value };
     delete p.token;
@@ -134,17 +134,25 @@ const getSceneList = genLoading(async (keyword: string): Promise<Scene[]> => {
   }));
 });
 
-
 const getOverviewData = genLoading(async (id: string) => {
   if (!id) {
     return {
-      store: {},
+      store: {
+        layers: {
+          default: {},
+        },
+      },
       viewport: null,
     };
   }
   const data = await get("fusion/caseOverview/info", { overviewId: id });
   return {
-    store: JSON.parse(data.store),
+    ...data,
+    store: JSON.parse(data.store) || {
+      layers: {
+        default: {},
+      },
+    },
     viewport: JSON.parse(data.viewport),
   };
 });
@@ -159,6 +167,7 @@ const saveOverviewData = genLoading(
   ) => {
     const item = await post(`fusion/caseOverview/addOrUpdate`, {
       ...params.value,
+      ...data,
       id,
       store: JSON.stringify(data.store),
       viewport: JSON.stringify(data.viewport),
@@ -186,6 +195,7 @@ const getTabulationData = genLoading(async (id: string) => {
   }
   const data = await get(`fusion/caseTabulation/info`, { tabulationId: id });
   return {
+    ...data,
     store: JSON.parse(data.store),
     viewport: JSON.parse(data.viewport),
     cover: JSON.parse(data.cover),
@@ -194,12 +204,11 @@ const getTabulationData = genLoading(async (id: string) => {
   };
 });
 
-
 if (!params.value.caseId || !token) {
-  ElMessage.error('当前项目号不存在!')
+  ElMessage.error("当前项目号不存在!");
   login(!!params.value.caseId);
 } else {
-  setTimeout(() => getSceneList(''), 500)
+  setTimeout(() => getSceneList(""), 500);
 }
 
 const saveTabulationData = genLoading(
@@ -216,6 +225,7 @@ const saveTabulationData = genLoading(
   ) => {
     const item = await post("fusion/caseTabulation/addOrUpdate", {
       ...params.value,
+      ...data,
       id,
       store: JSON.stringify(data.store),
       viewport: JSON.stringify(data.viewport),
@@ -252,6 +262,5 @@ window.platform = {
   getTabulationId,
 };
 
-
 /* @vite-ignore */
 import(import.meta.env.VITE_ENTRY_EXAMPLE);

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

@@ -9,6 +9,7 @@ export const tableCoverScaleKey = '__tableCoverScaleKey'
 export const tableTableKey = '__tableTableKey'
 export const tableTitleKey = '__tableTitleKey'
 export const tableCompassKey = '__tableCompassKey'
+export const mapImageKey = '__mapKey'
 export const tableCoverWidth = 370
 export const tableCoverHeight = 306
 
@@ -23,16 +24,24 @@ export type TabCover = {
 };
 
 export const overviewData = ref() as Ref<{
+  high?: number,
+  width?: number,
+  title?: string
+  mapUrl: string | null,
+  cover?: string
   store: StoreData;
   viewport: number[] | null;
 }>
 export const refreshOverviewData = () => {
-  return window.platform.getOverviewData(overviewId.value).then((data: any) => overviewData.value = data)
+  return window.platform.getOverviewData(overviewId.value).then((data: any) => {
+    overviewData.value = data
+  })
 }
 
 export const tabulationData = ref() as Ref<{
   store: StoreData;
   cover: TabCover | null;
+  title?: string
   paperKey: PaperKey;
   isAutoGen: boolean
   viewport: number[] | null;

+ 22 - 7
src/example/fuse/views/overview/header.vue

@@ -1,5 +1,5 @@
 <template>
-  <Header :action-groups="actions" title="绘图" no-back :draw="draw">
+  <Header :action-groups="actions" :title="title" no-back :draw="draw">
     <template #saves>
       <el-button type="primary" @click="saveHandler" :disabled="draw.drawing">
         保存
@@ -23,6 +23,7 @@ import {
   refreshTabulationData,
   tableCoverWidth,
   tableCoverHeight,
+  overviewData,
 } from "../../store.ts";
 import { nextTick, onUnmounted } from "vue";
 import { DataGroupId } from "@/constant/index.ts";
@@ -34,8 +35,8 @@ import { router } from "../../router.ts";
 import { overviewId, tabulationId } from "@/example/env.ts";
 import { listener } from "@/utils/event.ts";
 
+const props = defineProps<{ title: string }>();
 const draw = useDraw();
-
 const emit = defineEmits<{
   (e: "selectVR", v: Scene): void;
 }>();
@@ -59,7 +60,17 @@ const actions = [
       icon: "VR",
     },
   ],
-  [baseActions.expose],
+  [
+    {
+      ...baseActions.expose,
+      children: baseActions.expose.children.map((item) => ({
+        ...item,
+        handler() {
+          return item.handler(props.title);
+        },
+      })),
+    },
+  ],
 ];
 
 const setViewToTableCover = async () => {
@@ -136,15 +147,17 @@ const saveHandler = async () => {
     return [blob, scale, rect] as const;
   });
 
+  const url = await window.platform.uploadResourse(
+    new File([blob], `tabulation-cover.png`)
+  );
   overviewId.value = await window.platform.saveOverviewData(overviewId.value, {
+    ...overviewData.value,
+    listCover: url,
     store: storeData,
     viewport: draw!.viewer.transform.m,
   });
 
-  const url = await window.platform.uploadResourse(
-    new File([blob], `tabulation-cover.png`)
-  );
-
+  console.log(rect);
   const cover = {
     url,
     width: rect.width,
@@ -152,6 +165,7 @@ const saveHandler = async () => {
     proportion: { ...draw.store.config.proportion, scale },
   };
 
+  console.log(cover);
   tabulationId.value = await window.platform.getTabulationId(overviewId.value);
   await refreshTabulationData();
   const tabStore = await repTabulationStore(
@@ -164,6 +178,7 @@ const saveHandler = async () => {
   tabStore.config.compass = storeData.config.compass;
   tabulationId.value = await window.platform.saveTabulationData(tabulationId.value, {
     ...tabulationData.value,
+    title: overviewData.value.title,
     cover,
     store: tabStore,
     overviewId: overviewId.value,

+ 38 - 4
src/example/fuse/views/overview/index.vue

@@ -5,7 +5,7 @@
     :ref="(d: any) => (draw = d?.draw)"
   >
     <template #header>
-      <Header @selectVR="(scene) => (vrScene = scene)" />
+      <Header @selectVR="(scene) => (vrScene = scene)" :title="title" />
     </template>
     <template #slide>
       <Slide />
@@ -23,20 +23,50 @@ 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 { ref, watch } from "vue";
+import { computed, ref, watch, watchEffect } from "vue";
 import { Draw } from "../../../components/container/use-draw";
 import { Scene } from "../../../platform/platform-resource";
 import Dialog from "../../../dialog/dialog.vue";
-import { refreshOverviewData, overviewData } from "../../store";
+import { refreshOverviewData, overviewData, mapImageKey } from "../../store";
+import { getMapImageItem } from "@/example/components/slide/actions";
+import { defaultLayer } from "@/constant";
 
 const uploadResourse = window.platform.uploadResourse;
 const full = ref(false);
 const draw = ref<Draw>();
 const vrScene = ref<Scene>();
 
+const setMap = () => {
+  if (
+    !(overviewData.value.mapUrl && overviewData.value.high && overviewData.value.width)
+  ) {
+    return;
+  }
+
+  const mapItem = getMapImageItem(overviewData.value.mapUrl, {
+    width: overviewData.value.width,
+    height: overviewData.value.high,
+  });
+  const images = overviewData.value.store.layers[defaultLayer].image;
+  if (!images) {
+    overviewData.value.store.layers[defaultLayer].image = [mapItem];
+  } else {
+    const oldMapItem = images.find((item) => item.key === mapImageKey);
+    if (oldMapItem) {
+      oldMapItem.url = mapItem.url;
+      oldMapItem.width = mapItem.width;
+      oldMapItem.height = mapItem.height;
+    } else {
+      images.push(mapItem);
+    }
+  }
+  overviewData.value.mapUrl = null;
+};
+
 const init = async (draw: Draw) => {
   await refreshOverviewData();
-  draw.config.showCompass = import.meta.env.DEV;
+  setMap();
+  draw.config.showCompass = false;
   draw.config.showLabelLine = true;
   draw.config.showComponentSize = false;
   draw.config.back = { color: "#f0f2f5", opacity: 1 };
@@ -46,4 +76,8 @@ const init = async (draw: Draw) => {
 };
 
 watch(draw, (draw) => draw && init(draw));
+const title = computed(() => overviewData.value?.title || "绘图");
+watchEffect(() => {
+  document.title = title.value;
+});
 </script>

+ 5 - 1
src/example/fuse/views/tabulation/gen-tab.ts

@@ -52,6 +52,7 @@ export const setCoverPaperScale = (cover: ImageData, paperKey: PaperKey, scale:
 }
 
 export const genTabulationData = async (paperKey: PaperKey, compass?: number, cover?: TabCover | null) => {
+  console.error(cover)
   const { margin, size } = getPaperConfig(
     paperConfigs[paperKey].size,
     paperConfigs[paperKey].scale
@@ -108,6 +109,7 @@ export const genTabulationData = async (paperKey: PaperKey, compass?: number, co
     const tableCoverScale = tableCoverWidth / tableCoverHeight;
 
     let width: number, height: number;
+    
     if (rectScale > tableCoverScale) {
       width = transformPaper(tableCoverWidth, paperKey, "a4");
       height = width / rectScale;
@@ -132,6 +134,7 @@ export const genTabulationData = async (paperKey: PaperKey, compass?: number, co
       width,
       height,
       mat: [1, 0, 0, 1, 0, 0],
+      itemName: '比例'
     };
     const pos = getFixPosition(
       {
@@ -145,12 +148,13 @@ export const genTabulationData = async (paperKey: PaperKey, compass?: number, co
     coverData.mat[5] = pos.y;
 
     const scale = getCoverPaperScale(coverData, paperKey);
-    setCoverPaperScale(coverData, paperKey, Math.round(scale / 10) * 10)
+    setCoverPaperScale(coverData, paperKey, Math.round(scale / 10) * 10 || 10)
     return coverData;
   };
 
   const getCoverScale = (cover: ImageData) => {
     const scale = (cover.widthRaw! / cover.width) * cover.proportion!.scale;
+    console.log(cover)
     const text = {
       ...getBaseItem(),
       content: `1:${scale}`,

+ 39 - 30
src/example/fuse/views/tabulation/header.vue

@@ -1,7 +1,7 @@
 <template>
   <Header
     :action-groups="actions"
-    title="图纸"
+    :title="title"
     :draw="draw"
     @back="router.replace({ name: 'overview', query: router.currentRoute.value.query })"
   >
@@ -31,8 +31,38 @@ import { tabulationId } from "@/example/env.ts";
 import { listener } from "@/utils/event.ts";
 import { router } from "../../router.ts";
 
+const props = defineProps<{ title: string }>();
+
 const draw = useDraw();
+const getCoverImage = (format: string) => {
+  return draw.enterTemp(async () => {
+    const pop = draw.mode.push(Mode.readonly);
+    const oldMat = draw.viewer.viewMat;
+    draw.viewer.setViewMat([1, 0, 0, 1, 0, 0]);
+    await nextTick();
+
+    const viewSize = draw.viewer.viewSize!;
+    const size = {
+      width: draw.stage!.width(),
+      height: draw.stage!.height(),
+    };
+    const rect = {
+      x: (size.width - viewSize.width) / 2,
+      y: (size.height - viewSize.height) / 2,
+      ...viewSize,
+    };
 
+    let fileBlob = await (draw.stage!.toBlob({
+      pixelRatio: 4,
+      mimeType: format,
+      quality: 1,
+      ...rect,
+    }) as Promise<Blob>);
+    draw.viewer.setViewMat(oldMat);
+    pop();
+    return [rect, fileBlob] as const;
+  });
+};
 const baseActions = getHeaderActions(draw);
 const actions = [
   [baseActions.undo, baseActions.redo],
@@ -50,35 +80,8 @@ const actions = [
       handler: async () => {
         const ef = await selectExposeFormat();
         const format = "image/jpeg";
-        let [rect, fileBlob] = await draw.enterTemp(async () => {
-          const pop = draw.mode.push(Mode.readonly);
-          const oldMat = draw.viewer.viewMat;
-          draw.viewer.setViewMat([1, 0, 0, 1, 0, 0]);
-          await nextTick();
-
-          const viewSize = draw.viewer.viewSize!;
-          const size = {
-            width: draw.stage!.width(),
-            height: draw.stage!.height(),
-          };
-          const rect = {
-            x: (size.width - viewSize.width) / 2,
-            y: (size.height - viewSize.height) / 2,
-            ...viewSize,
-          };
-
-          let fileBlob = await (draw.stage!.toBlob({
-            pixelRatio: 4,
-            mimeType: format,
-            quality: 1,
-            ...rect,
-          }) as Promise<Blob>);
-          draw.viewer.setViewMat(oldMat);
-          pop();
-          return [rect, fileBlob] as const;
-        });
-
-        const filename = "canvas";
+        let [rect, fileBlob] = await getCoverImage(format);
+        const filename = props.title;
         let ext = "jpg";
 
         if (ef.color === "grayscale") {
@@ -120,7 +123,13 @@ onUnmounted(
 let isUpload = false;
 const saveHandler = async () => {
   isUpload = true;
+  const [_, fileBlob] = await getCoverImage("image/jpeg");
+  const url = await window.platform.uploadResourse(
+    new File([fileBlob], `tabulation-list-cover.png`)
+  );
   tabulationId.value = await window.platform.saveTabulationData(tabulationId.value, {
+    ...tabulationId.value,
+    listCover: url,
     store: draw!.getData(),
     viewport: draw!.viewer.transform.m,
     paperKey: tabulationData.value.paperKey,

+ 9 - 4
src/example/fuse/views/tabulation/index.vue

@@ -5,7 +5,7 @@
     :ref="(d: any) => (draw = d?.draw)"
   >
     <template #header>
-      <Header v-if="inited" />
+      <Header v-if="inited" :title="title" />
     </template>
     <template #slide>
       <Slide v-if="inited" />
@@ -19,7 +19,7 @@
 import Header from "./header.vue";
 import Slide from "./slide.vue";
 import Container from "../../../components/container/container.vue";
-import { computed, nextTick, ref, watch } from "vue";
+import { computed, nextTick, ref, watch, watchEffect } from "vue";
 import { Draw } from "../../../components/container/use-draw";
 import Dialog from "../../../dialog/dialog.vue";
 import {
@@ -36,9 +36,9 @@ import { defaultTableStyle } from "@/core/components/serial";
 import { IconData } from "@/core/components/icon";
 import { Transform } from "konva/lib/Util";
 import { MathUtils } from "three";
-import { useStage } from "@/core/hook/use-global-vars";
 import { components } from "@/core/components";
 import { ShapeType } from "@/index";
+import { round } from "@/utils/shared";
 
 const uploadResourse = window.platform.uploadResourse;
 const full = ref(false);
@@ -183,7 +183,7 @@ watch(compass, (compass, _, onCleanup) => {
       "layout-type": "column",
       sort: 3,
       get value() {
-        return (new Transform(compass.mat).decompose().rotation + 360) % 360;
+        return round((new Transform(compass.mat).decompose().rotation + 360) % 360, 1);
       },
       set value(val) {
         const config = new Transform(compass.mat).decompose();
@@ -199,4 +199,9 @@ watch(compass, (compass, _, onCleanup) => {
   }));
   onCleanup(() => mountMenus.setShapeMenusFilter(compass.id));
 });
+
+const title = computed(() => tabulationData.value?.title || "图纸");
+watchEffect(() => {
+  document.title = title.value;
+});
 </script>

+ 119 - 32
src/example/platform/platform-draw.ts

@@ -1,4 +1,4 @@
-import { genBound, onlyId } from "@/utils/shared";
+import { copy, genBound, onlyId } from "@/utils/shared";
 import { AIExposeData } from "../dialog/ai";
 import { Draw } from "../components/container/use-draw";
 import { SceneFloor } from "./platform-resource";
@@ -7,6 +7,10 @@ import { LineData, defaultStyle } from "@/core/components/line";
 import { getInitCtx, normalLineData } from "@/core/components/line/use-draw";
 import { defaultStyle as iconDefaultStyle } from "@/core/components/icon";
 import { Transform } from "konva/lib/Util";
+import { ElMessage } from "element-plus";
+import { ImageData } from "@/core/components/image";
+import { Size } from "@/utils/math";
+import { watchEffect } from "vue";
 
 const scaleResource = (info: AIExposeData, scale: number) => {
   const floors = info.floors.map((item) => ({
@@ -82,7 +86,6 @@ const getResourceLayers = (data: AIExposeData) => {
       let box: SceneFloor["box"];
       // if (true) {
       if (!floor.box || !floor.box.bound.x_max || !floor.box.bound.z_max) {
-      console.log(floor.geos)
         const xs = floor.geos.flatMap((item) => item.map((p) => p.x));
         const ys = floor.geos.flatMap((item) => item.map((p) => p.y));
         const zs = floor.geos.flatMap((item) => item.map((p) => p.z));
@@ -113,9 +116,6 @@ const getResourceLayers = (data: AIExposeData) => {
         box,
         taggings: data.taggings
           .filter((item) => {
-            console.log(item.position.z === undefined ||
-              (item.position.z >= box.bound.z_min &&
-                item.position.z <= box.bound.z_max), item, box)
             return (
               item.position.z === undefined ||
               (item.position.z >= box.bound.z_min &&
@@ -132,7 +132,33 @@ const getResourceLayers = (data: AIExposeData) => {
     .filter((floor) => floor.taggings.length > 0 || floor.geos.length > 0);
 };
 
-const drawLayerResource = (
+const getSizePlaceBoxImage = ({ width, height }: Size) => {
+  const borderWidth = 10;
+  const canvas = document.createElement("canvas");
+  canvas.width = width;
+  canvas.height = height;
+
+  const ctx = canvas.getContext("2d")!;
+
+  // 画背景白色
+  ctx.fillStyle = "#fff";
+  ctx.fillRect(0, 0, width, height);
+
+  // 画边框
+  ctx.lineWidth = borderWidth;
+  ctx.strokeStyle = "#000"; // 黑色边框
+  // 注意strokeRect位置和尺寸要配合线宽,这里我们让边框完整显示在canvas内
+  ctx.strokeRect(
+    borderWidth / 2,
+    borderWidth / 2,
+    width - borderWidth,
+    height - borderWidth
+  );
+
+  return canvas.toDataURL(); // 返回图片的Data URL字符串
+};
+
+const drawLayerResource = async (
   layerResource: ReturnType<typeof getResourceLayers>[number],
   draw: Draw
 ) => {
@@ -140,17 +166,15 @@ const drawLayerResource = (
   const images: any[] = [];
   const createTime = Date.now();
 
-  let sGeo = draw.store.getTypeItems("line")[0];
-  let geo: LineData = sGeo
-    ? sGeo
-    : {
-        ...getBaseItem(),
-        lines: [],
-        points: [],
-        polygon: [],
-        attitude: [1, 0, 0, 1, 0, 0],
-        createTime: createTime,
-      };
+  let geo: LineData = {
+    ...getBaseItem(),
+    lines: [],
+    points: [],
+    polygon: [],
+    attitude: [1, 0, 0, 1, 0, 0],
+    createTime: createTime,
+  };
+
   const geoCtx = getInitCtx();
 
   layerResource.geos.forEach((item) => {
@@ -175,15 +199,9 @@ const drawLayerResource = (
     geoCtx.add.points[b!] = p;
     geo.points.push(p);
   });
-  draw.history.onceTrack(() => {
-    geo = normalLineData(geo, geoCtx);
-    if (sGeo) {
-      draw.store.setItem("line", { id: geo.id, value: geo });
-    } else {
-      draw.store.addItem("line", geo);
-    }
-  });
+  geo = normalLineData(geo, geoCtx);
 
+  let thumb: ImageData;
   if (layerResource.thumb) {
     const box = layerResource.box;
     const width = box.bound.x_max - box.bound.x_min;
@@ -193,7 +211,7 @@ const drawLayerResource = (
       box.bound.y_min + height / 2
     );
     // .rotate(-MathUtils.degToRad(floor.box.rotate));
-    const thumb = {
+    thumb = {
       ...getBaseItem(),
       createTime: createTime - 1,
       url: layerResource.thumb,
@@ -202,14 +220,68 @@ const drawLayerResource = (
       lock: import.meta.env.DEV ? false : true,
       height,
       cornerRadius: 0,
+      zIndex: -1
     };
     images.push(thumb);
   }
 
+  let offset = { x: 0, y: 0 };
+
+  let sGeo = draw.store.getTypeItems("line")[0];
+  if (sGeo?.itemName) {
+    ElMessage.warning("请在画图面板中选择放置位置,按右键取消");
+    await new Promise<void>((resolve, reject) => {
+      const data = thumb ? thumb : { url: getSizePlaceBoxImage(bound.get()!) };
+      const key = "place-plaform";
+      draw.enterDrawShape("image", { ...data, key }, true);
+      const stopWatch = watchEffect(() => {
+        if (!draw.drawing) {
+          stopWatch();
+          const item = draw.store.items.find(
+            (item) => item.key === key
+          ) as ImageData;
+
+          if (!item) {
+            reject("用户已取消");
+          } else {
+            draw.store.delItem("image", item.id);
+            offset.x = item.mat[4];
+            offset.y = item.mat[5];
+            resolve();
+          }
+        }
+      });
+    });
+  } else if (sGeo) {
+    sGeo.itemName = "1";  
+  } else {
+    geo.itemName = "1";
+  }
+  geo.points.forEach((p) => {
+    p.x += offset.x;
+    p.y += offset.y;
+  });
+
+  if (sGeo) {
+    sGeo.points = sGeo.points.concat(geo.points)
+    sGeo.lines = sGeo.lines.concat(geo.lines)
+    sGeo.polygon = sGeo.polygon.concat(geo.polygon)
+    geo = sGeo
+    draw.store.setItem("line", { id: geo.id, value: geo });
+  } else {
+    draw.store.addItem("line", geo);
+  }
+  if (thumb!) {
+    thumb.mat[4] = offset.x
+    thumb.mat[5] = offset.y
+  }
   images.push(
     ...layerResource.taggings.map((item, ndx) => {
       bound.update(item.position);
-      const tf = new Transform().translate(item.position.x, item.position.y);
+      const tf = new Transform().translate(
+        item.position.x + offset.x,
+        item.position.y + offset.y
+      );
       if (item.rotate) {
         tf.rotate(item.rotate);
       }
@@ -245,15 +317,30 @@ const drawLayerResource = (
   //   ids: [...images, geo.id].map((item) => item.id),
   // });
 
-  return bound.get();
+  const box = bound.get()!;
+  return {
+    ...box,
+    x: box.x + offset.x,
+    y: box.y + offset.y,
+    maxX: box.maxX + offset.x,
+    maxY: box.maxY + offset.y,
+    center: {
+      x: box.center.x + offset.x,
+      y: box.center.y + offset.y,
+    }
+  }
 };
 
-export const drawPlatformResource = (data: AIExposeData, draw: Draw) => {
+export const drawPlatformResource = async (data: AIExposeData, draw: Draw) => {
   // 默认为米,为了方便绘图 一米转为100
   const layers = getResourceLayers(scaleResource(data, 100));
-  const layerBounds: ReturnType<typeof drawLayerResource>[] = [];
+  const layerBounds: ReturnType<typeof drawLayerResource> extends Promise<
+    infer T
+  >
+    ? T[]
+    : number[] = [];
 
-  draw.history.onceTrack(() => {
+  await draw.history.onceTrack(async () => {
     // draw.store.setConfig({ proportion: { scale: 10, unit: 'mm' } });
     draw.store.setConfig({ proportion: { scale: 10, unit: "mm" } });
     for (const layer of layers) {
@@ -261,7 +348,7 @@ export const drawPlatformResource = (data: AIExposeData, draw: Draw) => {
       //   draw.store.addLayer(layer.name);
       // }
       // draw.store.setCurrentLayer(layer.name);
-      layerBounds.push(drawLayerResource(layer, draw)!);
+      layerBounds.push(await drawLayerResource(layer, draw)!);
     }
     if (typeof data.compass === "number") {
       draw.store.setConfig({