Browse Source

perfect_canvas

ananzhusen 2 năm trước cách đây
mục cha
commit
2e933bd8e5

+ 4 - 0
package.json

@@ -11,7 +11,10 @@
   "dependencies": {
     "axios": "^0.26.0",
     "dayjs": "^1.11.5",
+    "fast-xml-parser": "^4.0.1",
     "file-saver": "^2.0.5",
+    "jszip": "^3.10.0",
+    "monaco-editor": "^0.28.1",
     "tdesign-vue-next": "^1.3.0",
     "vue": "^3.2.37",
     "vue-router": "^4.1.3"
@@ -28,6 +31,7 @@
     "postcss-nested": "^6.0.1",
     "typescript": "^4.7.4",
     "vite": "^4.0.3",
+    "vite-plugin-monaco-editor": "^1.0.10",
     "vue-tsc": "^1.0.5"
   }
 }

+ 95 - 1
pnpm-lock.yaml

@@ -9,13 +9,17 @@ specifiers:
   autoprefixer: ^10.4.13
   axios: ^0.26.0
   dayjs: ^1.11.5
+  fast-xml-parser: ^4.0.1
   file-saver: ^2.0.5
+  jszip: ^3.10.0
+  monaco-editor: ^0.28.1
   postcss: ^8.4.6
   postcss-import: ^14.1.0
   postcss-nested: ^6.0.1
   tdesign-vue-next: ^1.3.0
   typescript: ^4.7.4
   vite: ^4.0.3
+  vite-plugin-monaco-editor: ^1.0.10
   vue: ^3.2.37
   vue-router: ^4.1.3
   vue-tsc: ^1.0.5
@@ -23,7 +27,10 @@ specifiers:
 dependencies:
   axios: 0.26.1
   dayjs: 1.11.7
+  fast-xml-parser: 4.2.2
   file-saver: 2.0.5
+  jszip: 3.10.1
+  monaco-editor: 0.28.1
   tdesign-vue-next: 1.3.1_vue@3.2.47
   vue: 3.2.47
   vue-router: 4.1.6_vue@3.2.47
@@ -40,6 +47,7 @@ devDependencies:
   postcss-nested: 6.0.1_postcss@8.4.23
   typescript: 4.9.5
   vite: 4.3.1_@types+node@18.15.13
+  vite-plugin-monaco-editor: 1.1.0_monaco-editor@0.28.1
   vue-tsc: 1.4.1_typescript@4.9.5
 
 packages:
@@ -870,6 +878,10 @@ packages:
     resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
     dev: true
 
+  /core-util-is/1.0.3:
+    resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
+    dev: false
+
   /cssesc/3.0.0:
     resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
     engines: {node: '>=4'}
@@ -946,6 +958,13 @@ packages:
   /estree-walker/2.0.2:
     resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
 
+  /fast-xml-parser/4.2.2:
+    resolution: {integrity: sha512-DLzIPtQqmvmdq3VUKR7T6omPK/VCRNqgFlGtbESfyhcH2R4I8EzK1/K6E8PkRCK2EabWrUHK32NjYRbEFnnz0Q==}
+    hasBin: true
+    dependencies:
+      strnum: 1.0.5
+    dev: false
+
   /file-saver/2.0.5:
     resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==}
     dev: false
@@ -1008,12 +1027,24 @@ packages:
     engines: {node: '>=8'}
     dev: true
 
+  /immediate/3.0.6:
+    resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
+    dev: false
+
+  /inherits/2.0.4:
+    resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+    dev: false
+
   /is-core-module/2.12.0:
     resolution: {integrity: sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==}
     dependencies:
       has: 1.0.3
     dev: true
 
+  /isarray/1.0.0:
+    resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
+    dev: false
+
   /js-tokens/4.0.0:
     resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
     dev: true
@@ -1030,6 +1061,21 @@ packages:
     hasBin: true
     dev: true
 
+  /jszip/3.10.1:
+    resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
+    dependencies:
+      lie: 3.3.0
+      pako: 1.0.11
+      readable-stream: 2.3.8
+      setimmediate: 1.0.5
+    dev: false
+
+  /lie/3.3.0:
+    resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
+    dependencies:
+      immediate: 3.0.6
+    dev: false
+
   /lodash/4.17.21:
     resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
     dev: false
@@ -1063,6 +1109,9 @@ packages:
     resolution: {integrity: sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==}
     dev: false
 
+  /monaco-editor/0.28.1:
+    resolution: {integrity: sha512-P1vPqxB4B1ZFzTeR1ScggSp9/5NoQrLCq88fnlNUsuRAP1usEBN4TIpI2lw0AYIZNVIanHk0qwjze2uJwGOHUw==}
+
   /ms/2.1.2:
     resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
     dev: true
@@ -1085,6 +1134,10 @@ packages:
     engines: {node: '>=0.10.0'}
     dev: true
 
+  /pako/1.0.11:
+    resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
+    dev: false
+
   /path-parse/1.0.7:
     resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
     dev: true
@@ -1139,12 +1192,28 @@ packages:
       picocolors: 1.0.0
       source-map-js: 1.0.2
 
+  /process-nextick-args/2.0.1:
+    resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
+    dev: false
+
   /read-cache/1.0.0:
     resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
     dependencies:
       pify: 2.3.0
     dev: true
 
+  /readable-stream/2.3.8:
+    resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
+    dependencies:
+      core-util-is: 1.0.3
+      inherits: 2.0.4
+      isarray: 1.0.0
+      process-nextick-args: 2.0.1
+      safe-buffer: 5.1.2
+      string_decoder: 1.1.1
+      util-deprecate: 1.0.2
+    dev: false
+
   /regenerator-runtime/0.13.11:
     resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
     dev: false
@@ -1166,6 +1235,10 @@ packages:
       fsevents: 2.3.2
     dev: true
 
+  /safe-buffer/5.1.2:
+    resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
+    dev: false
+
   /semver/6.3.0:
     resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==}
     hasBin: true
@@ -1179,6 +1252,10 @@ packages:
       lru-cache: 6.0.0
     dev: true
 
+  /setimmediate/1.0.5:
+    resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
+    dev: false
+
   /sortablejs/1.15.0:
     resolution: {integrity: sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w==}
     dev: false
@@ -1195,6 +1272,16 @@ packages:
     resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
     deprecated: Please use @jridgewell/sourcemap-codec instead
 
+  /string_decoder/1.1.1:
+    resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
+    dependencies:
+      safe-buffer: 5.1.2
+    dev: false
+
+  /strnum/1.0.5:
+    resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==}
+    dev: false
+
   /supports-color/5.5.0:
     resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
     engines: {node: '>=4'}
@@ -1268,13 +1355,20 @@ packages:
 
   /util-deprecate/1.0.2:
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
-    dev: true
 
   /validator/13.9.0:
     resolution: {integrity: sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==}
     engines: {node: '>= 0.10'}
     dev: false
 
+  /vite-plugin-monaco-editor/1.1.0_monaco-editor@0.28.1:
+    resolution: {integrity: sha512-IvtUqZotrRoVqwT0PBBDIZPNraya3BxN/bfcNfnxZ5rkJiGcNtO5eAOWWSgT7zullIAEqQwxMU83yL9J5k7gww==}
+    peerDependencies:
+      monaco-editor: '>=0.33.0'
+    dependencies:
+      monaco-editor: 0.28.1
+    dev: true
+
   /vite/4.3.1_@types+node@18.15.13:
     resolution: {integrity: sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==}
     engines: {node: ^14.18.0 || >=16.0.0}

+ 2 - 0
src/services/api.ts

@@ -0,0 +1,2 @@
+//所有的接口请求
+import axios from "axios";

+ 96 - 0
src/services/defaults.ts

@@ -0,0 +1,96 @@
+export const lineDashObj = [
+  undefined,
+  [5, 5],
+  [10, 10],
+  [10, 10, 2, 10],
+  [1, 16],
+];
+
+/**
+ * 默认动画
+ */
+export const normalAnimate: any = {
+  upDown: [
+    {
+      y: -10,
+      duration: 100,
+    },
+    { y: 0, duration: 100 },
+    { y: -10, duration: 200 },
+  ],
+  leftRight: [
+    {
+      x: -10,
+      duration: 100,
+    },
+    {
+      x: 10,
+      duration: 80,
+    },
+    {
+      x: -10,
+      duration: 50,
+    },
+    {
+      x: 10,
+      duration: 30,
+    },
+    {
+      x: 0,
+      duration: 300,
+    },
+  ],
+  heart: [
+    {
+      // 通过 scale 来替代原版心跳
+      scale: 1.1,
+      duration: 100,
+    },
+    {
+      scale: 1,
+      duration: 400,
+    },
+  ],
+  success: [{ background: "#389e0d22", color: "#237804", duration: 500 }],
+  warning: [
+    {
+      color: "#fa8c16",
+      lineDash: [10, 10],
+      duration: 300,
+    },
+    {
+      color: "#fa8c16",
+      lineDash: undefined,
+      duration: 500,
+    },
+    {
+      color: "#fa8c16",
+      lineDash: [10, 10],
+      duration: 300,
+    },
+  ],
+  error: [{ color: "#cf1322", background: "#cf132222", duration: 100 }],
+  show: [
+    {
+      color: "#fa541c",
+      rotate: -10,
+      duration: 100,
+    },
+    {
+      color: "#fa541c",
+      rotate: 10,
+      duration: 100,
+    },
+    {
+      color: "#fa541c",
+      rotate: 0,
+      duration: 100,
+    },
+  ],
+  rotate: [
+    {
+      rotate: 360,
+      duration: 1000,
+    },
+  ],
+};

+ 34 - 12
src/services/file.ts

@@ -1,11 +1,17 @@
-import axios from 'axios';
+import axios from "axios";
 
-export async function upload(blob: Blob, shared = false, filename = '') {
+export async function upload(
+  blob: Blob,
+  shared = false,
+  filename = "thumb.png",
+  directory = "/2D/缩略图"
+) {
   const form = new FormData();
-  form.append('directory', filename);
-  form.append('public', shared + '');
-  form.append('file', blob);
-  const ret: any = await axios.post('/api/image', form);
+  form.append("filename", filename);
+  form.append("directory", directory);
+  form.append("public", shared + "");
+  form.append("file", blob);
+  const ret: any = await axios.post("/api/image", form);
   if (ret.error) {
     return null;
   }
@@ -14,7 +20,7 @@ export async function upload(blob: Blob, shared = false, filename = '') {
 }
 
 export async function delImage(image: string) {
-  const ret: any = await axios.delete('/api' + image);
+  const ret: any = await axios.delete("/api" + image);
   if (ret.error) {
     return false;
   }
@@ -23,20 +29,20 @@ export async function delImage(image: string) {
 }
 
 export async function addImage(image: string) {
-  const ret: any = await axios.post('/api/user/image', { image });
+  const ret: any = await axios.post("/api/user/image", { image });
   if (ret.error) {
-    return '';
+    return "";
   }
 
   return ret.id;
 }
 
 export function filename(str: string) {
-  const i = str.lastIndexOf('.');
+  const i = str.lastIndexOf(".");
   return str.substring(0, i);
 }
 
-const units = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+const units = ["KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
 export function formatBytes(size: number) {
   let l = 0;
   let n = size / 1024;
@@ -45,5 +51,21 @@ export function formatBytes(size: number) {
     n = n / 1024;
   }
 
-  return Math.round(n * 100) / 100 + ' ' + units[l];
+  return Math.round(n * 100) / 100 + " " + units[l];
+}
+
+/**
+ * 读取文件内容,返回字符串
+ * @param file 文件
+ */
+export async function readFile(file: Blob) {
+  return new Promise<string>((resolve, reject) => {
+    const reader = new FileReader();
+    reader.onload = () => {
+      resolve(reader.result as string);
+    };
+    reader.onerror = reject;
+    // readAsText 使 result 一定是字符串
+    reader.readAsText(file);
+  });
 }

+ 46 - 0
src/services/transfrom.ts

@@ -0,0 +1,46 @@
+import { lineDashObj, normalAnimate } from "./defaults";
+import { LineAnimateType } from "@meta2d/core";
+
+/*
+ * @Description:
+ * @Author: G
+ * @Date: 2021-10-18 17:17:37
+ * @LastEditTime: 2021-10-27 10:48:48
+ */
+export function transAnchorId(oldId: string | number) {
+  let id = Number(oldId);
+  if (id === 0) {
+    return 3;
+  }
+  return id - 1;
+}
+
+export function transLineDash(oldDash: number) {
+  return lineDashObj[oldDash];
+}
+
+// oldType: '', 'beads', 'dot', 'comet'
+export function transLineAnimateType(oldType: string) {
+  switch (oldType) {
+    case "beads":
+      return LineAnimateType.Beads;
+    case "dot":
+      return LineAnimateType.Dot;
+    case "comet": // TODO: 彗星在新版本中没有了,转换成默认动画
+      return LineAnimateType.Normal;
+    default:
+      return LineAnimateType.Normal;
+  }
+}
+
+export function transNodeAnimateFrame(node: any) {
+  return normalAnimate[node.animateType] || [];
+}
+
+export function transAutoPlay(el: any) {
+  if (el.hasOwnProperty("animatePlay")) {
+    return el.animatePlay;
+  } else {
+    return el.playLoop;
+  }
+}

+ 805 - 0
src/services/upgrade.ts

@@ -0,0 +1,805 @@
+import {
+  transAnchorId,
+  transLineAnimateType,
+  transLineDash,
+  transNodeAnimateFrame,
+  transAutoPlay,
+} from "./transfrom";
+import {
+  Meta2d,
+  Event,
+  EventAction,
+  calcRelativePoint,
+  isEqual,
+  Pen,
+  s8,
+  EventName,
+} from "@meta2d/core";
+import { isGif, Meta2dBackData } from "./utils";
+type Point = any;
+type Rect = any;
+
+export const baseVer = "1.0.0";
+declare const meta2d: Meta2d;
+
+enum LineSType {
+  Line = "line",
+  Polyline = "polyline",
+  Curve = "curve",
+  Mind = "mind",
+}
+/**
+ * 旧版本到新版本的格式转换函数
+ * 该方法存在定时器,随后执行的 meta2d.open 必须是同步代码
+ */
+export function upgrade(data: any, baseVer: string): Meta2dBackData {
+  const newData: Meta2dBackData = {
+    x: data.x ?? 0,
+    y: data.y ?? 0,
+    version: baseVer,
+    scale: data.scale,
+    name: data.name,
+    background: data.bkColor,
+    grid: data.grid,
+    gridColor: data.gridColor,
+    gridSize: data.gridSize,
+    locked: data.locked,
+    rule: data.rule,
+    ruleColor: data.ruleColor,
+    pens: convertPen(data.pens, data.scale),
+    folder: data.folder,
+    component: data.component,
+    class: data.class,
+    id: data.id,
+    websocket: data.websocket,
+    mqtt: data.mqttUrl,
+    mqttOptions: data.mqttOptions || {},
+    mqttTopics: data.mqttTopics,
+    shared: data.shared,
+    initJs: data.initJS,
+    origin: { x: 0, y: 0 },
+    center: { x: 0, y: 0 },
+    userId: data.userId,
+    owner: {
+      id: data.userId,
+    },
+    image: data.image,
+  };
+  return newData;
+}
+// pen格式转换
+export function convertPen(
+  pens: any[],
+  scale = 1,
+  parentEl?: any,
+  options: any = [],
+  level = 0
+) {
+  pens?.forEach((el, i) => {
+    const obj: Pen = {};
+    obj.id = el.id;
+    obj.tags = el.tags;
+    if (!el.type && el.name === "line") {
+      const { y, height } = el.rect;
+      el.rect.y = y + height / 2;
+      el.rect.height = 0;
+    }
+    if (el.type === 1 && !parentEl) {
+      // 元素为线,并且不属于组合图形
+      if (!setLineData(el, obj, scale)) {
+        // 本次数据丢弃,脏数据
+        return false;
+      }
+    } else {
+      if (!setNodeData(el, obj, parentEl, scale)) {
+        return false;
+      }
+      if (obj.name === "combine") {
+        convertPen(el.children, scale, el, options, level++);
+      }
+    }
+    obj.paddingLeft = el.paddingLeft;
+    obj.paddingRight = el.paddingRight;
+    obj.paddingTop = el.paddingTop;
+    obj.paddingBottom = el.paddingBottom;
+    obj.color = el.strokeStyle;
+    obj.lineWidth = el.lineWidth / scale;
+    obj.lineCap = el.lineCap;
+    obj.lineDash = transLineDash(el.dash);
+    obj.borderColor = el.borderColor;
+    obj.borderWidth = el.borderWidth / scale;
+    obj.rotate = el.rotate;
+    obj.visible = el.visible;
+    obj.title = el.title || el.markdown;
+    obj.background = el.fillStyle;
+    obj.lineHeight = el.lineHeight;
+
+    if (el.font) {
+      // 老版本存在 font ,把 font 抽出来
+      Object.assign(el, { ...el.font });
+    }
+
+    obj.fontSize = el.fontSize / scale;
+    obj.fontFamily = el.fontFamily;
+    obj.fontStyle = el.fontStyle;
+    obj.fontWeight = el.fontWeight;
+
+    obj.text = el.text;
+    obj.textColor = el.fontColor;
+    obj.textAlign = el.textAlign;
+    obj.textBaseline = el.textBaseline;
+    obj.textBackground = el.textBackground;
+    obj.whiteSpace = el.whiteSpace;
+
+    obj.animateSpan = el.animateSpan;
+    obj.animateColor = el.animateColor;
+    obj.animateCycle = el.animateCycle;
+    obj.animateReverse = el.animateReverse;
+    obj.nextAnimate = el.nextAnimate;
+    obj.lineAnimateType = transLineAnimateType(el.animateType);
+    obj.animateDotSize = el.animateDotSize;
+    obj.animateLineDash = el.animateLineDash;
+    (obj as any).animateType = el.animateType;
+    obj.frames = transNodeAnimateFrame(el);
+    obj.autoPlay = transAutoPlay(el);
+    obj.playLoop = el.playLoop;
+
+    obj.globalAlpha = el.globalAlpha;
+    obj.bkType = el.bkType;
+    obj.gradientFromColor = el.gradientFromColor;
+    obj.gradientToColor = el.gradientToColor;
+    obj.gradientAngle = el.gradientAngle;
+    obj.gradientRadius = el.gradientRadius;
+    obj.borderRadius = el.borderRadius;
+
+    obj.events = convertEvents(el.events || [], el.wheres || []);
+    options.push(obj);
+  });
+  return options;
+}
+
+function setNodeData(el: any, obj: any, parentEl?: any, scale = 1): boolean {
+  if (parentEl) {
+    obj.parentId = parentEl.id;
+    obj.locked = 2;
+  }
+  if (isLine(el.name) || isPen(el.name)) {
+    if (!setLineData(el, obj, scale)) {
+      return false;
+    }
+    obj.type = 0;
+    obj.close = el.closePath;
+  } else {
+    if (el.name === "combine") {
+      obj.name = "combine";
+      obj.children = el.children.map((child: any) => child.id);
+    } else {
+      obj.type = 0;
+      obj.name = el.name;
+      if (el.icon) {
+        obj.icon = el.icon;
+        obj.iconColor = el.iconColor;
+        obj.iconFamily = el.iconFamily;
+        obj.iconSize = el.iconSize / scale;
+      }
+      // 处理图片节点
+      if (el.image) {
+        obj.image = el.image;
+        obj.imageRatio = el.imageRatio;
+        obj.iconAlign = el.imageAlign;
+        obj.iconWidth = el.imageWidth / scale;
+        obj.iconHeight = el.imageHeight / scale;
+        isGif(el.image) && (obj.name = "gif");
+      }
+      //  处理echarts节点
+      if (el.name === "echarts") {
+        obj.echarts = el.data.echarts;
+      }
+      // 处理iframe节点
+      if (el.iframe) {
+        obj.name = "iframe";
+        obj.iframe = el.iframe;
+      }
+      // 处理视频节点
+      if (el.video) {
+        obj.name = "video";
+        obj.video = el.video;
+      }
+      // 处理音频节点
+      if (el.audio) {
+        obj.name = "video";
+        obj.audio = el.audio;
+      }
+    }
+  }
+  let rect = { ...el.rect };
+  if (parentEl) {
+    if (obj.name === "line") {
+      rect = calcRelativeRect(obj, parentEl.rect);
+    } else {
+      rect = calcRelativeRect(el.rect, parentEl.rect);
+    }
+  }
+
+  obj.x = rect.x;
+  obj.y = rect.y;
+  if (isEqual(rect.width, 1)) {
+    // 数据库中存在 1.0000000000000004 的值,认为是 1
+    obj.width = 1;
+  } else {
+    obj.width = rect.width;
+  }
+  obj.height = rect.height;
+  return true;
+}
+
+function setLineData(el: any, obj: any, scale = 1): boolean {
+  obj.name = "line";
+  obj.type = 1;
+  obj.fromArrow = el.fromArrow;
+  // TODO: 老版本转换新版本,箭头大小不设置,采用默认,有问题用户自行设置
+  // obj.fromArrowSize = el.fromArrowSize;
+  obj.toArrow = el.toArrow;
+  // obj.toArrowSize = el.toArrowSize;
+  let calcAnchors = [];
+  if (isPolyline(el.name)) {
+    calcAnchors = getPolylineCalcAnchors(el);
+  } else if (isCurveline(el.name)) {
+    calcAnchors = getCurvelineCalcAnchors(el);
+  } else if (isPen(el.name)) {
+    calcAnchors = getPenCalcAnchors(el);
+  }
+  if (!calcAnchors.length) {
+    // 长度为空,说明是脏数据,不转换
+    return false;
+  }
+  const rect = getLineRect(el, calcAnchors);
+  calcCenter(rect);
+  const anchors: Point = [];
+  calcAnchors.forEach((pt: Point) => {
+    anchors.push(calcRelativePoint(pt, rect));
+  });
+  obj.x = rect.x;
+  obj.y = rect.y;
+  obj.width = rect.width;
+  obj.height = rect.height;
+  obj.anchors = anchors;
+  if (el.textRect && anchors.length > 3) {
+    let textRatioX =
+      rect.width <= 1
+        ? 0
+        : ((el.fontSize / scale) * el.text.length) / rect.width;
+    let textRatioY =
+      rect.height <= 1
+        ? 0
+        : ((el.fontSize / scale) * el.lineHeight) / rect.height;
+    //老版本文字默认在最后两个控制点中心
+    let end = anchors[anchors.length - 2];
+    let start = anchors[anchors.length - 3];
+    obj.textLeft = (start.x + end.x - textRatioX) / 2;
+    if (obj.textLeft === 1) {
+      obj.textLeft = 0.99999;
+    }
+    obj.textTop = (start.y + end.y - textRatioY) / 2;
+    if (obj.textTop === 1) {
+      obj.textTop = 0.99999;
+    }
+    if (rect.width <= 1) {
+      obj.textLeft = ((-el.fontSize / scale / scale) * el.text.length) / 2;
+    }
+    if (rect.height <= 1) {
+      obj.textTop = ((-el.fontSize / scale / scale) * el.lineHeight) / 2;
+    }
+    obj.textWidth = rect.width;
+    el.textAlign = "left";
+    el.textBaseline = "top";
+  }
+  setControledLines(el, calcAnchors);
+  return true;
+}
+
+function getPolylineCalcAnchors(el: any) {
+  const { from, to } = getLineFromTo(el);
+  const calcAnchors = [
+    {
+      id: s8(),
+      connectTo: from.id,
+      penId: el.id,
+      x: from.x,
+      y: from.y,
+      anchorId: String(from.anchorIndex),
+    },
+    ...(el.controlPoints || []).map((p: Point) => ({
+      id: s8(),
+      connectTo: undefined,
+      penId: el.id,
+      x: p.x,
+      y: p.y,
+    })),
+    {
+      id: s8(),
+      connectTo: to.id,
+      penId: el.id,
+      x: to.x,
+      y: to.y,
+      anchorId: String(to.anchorIndex),
+    },
+  ];
+  return calcAnchors;
+}
+
+function getCurvelineCalcAnchors(el: any) {
+  const [nextPoint, prevPoint] = el.controlPoints;
+  const { from, to } = getLineFromTo(el);
+  const calcAnchors = [
+    {
+      id: s8(),
+      connectTo: from.id,
+      penId: el.id,
+      x: from.x,
+      y: from.y,
+      next: {
+        connectTo: from.id,
+        penId: el.id,
+        x: nextPoint.x,
+        y: nextPoint.y,
+      },
+      prev: {
+        connectTo: from.id,
+        penId: el.id,
+        x: 2 * from.x - nextPoint.x,
+        y: 2 * from.y - nextPoint.y,
+      },
+    },
+    {
+      id: s8(),
+      connectTo: to.id,
+      penId: el.id,
+      x: to.x,
+      y: to.y,
+      prev: {
+        connectTo: to.id,
+        penId: el.id,
+        x: prevPoint.x,
+        y: prevPoint.y,
+      },
+      next: {
+        connectTo: to.id,
+        penId: el.id,
+        x: 2 * to.x - prevPoint.x,
+        y: 2 * to.y - prevPoint.y,
+      },
+    },
+  ];
+  return calcAnchors;
+}
+
+function getPenCalcAnchors(el: any) {
+  if (el.children && el.children.length > 0) {
+    return el.children.reduce(
+      (arr: Point[], child: any, i: number, children: any) => {
+        if (i === 0) {
+          arr.push({
+            connectTo: undefined,
+            penId: el.id,
+            x: child.from.x,
+            y: child.from.y,
+          });
+        }
+        arr.push({
+          connectTo: undefined,
+          penId: el.id,
+          x: child.to.x,
+          y: child.to.y,
+        });
+        return arr;
+      },
+      []
+    );
+  }
+  if (el.points && el.points.length > 0) {
+    return el.points.reduce((arr: any, point: any, i: number, points: any) => {
+      let flag = false;
+      if (i > 0) {
+        const prev = points[i - 1];
+        if (point.x !== prev.x && point.y !== prev.y) {
+          flag = true;
+        }
+      } else {
+        flag = true;
+      }
+      if (flag) {
+        arr.push({
+          connectTo: undefined,
+          penId: el.id,
+          x: point.x,
+          y: point.y,
+        });
+      }
+      return arr;
+    }, []);
+  }
+  return [];
+}
+
+function getLineRect(pen: Pen, calcAnchors: any) {
+  getLineLength(pen, calcAnchors);
+  return getRectOfPoints(getLinePoints(pen, calcAnchors));
+}
+
+function getLineLength(pen: Pen, calcAnchors: any): number {
+  if (calcAnchors.length < 2) {
+    return 0;
+  }
+
+  let len = 0;
+  let from: Point;
+  calcAnchors.forEach((pt: Point) => {
+    if (from) {
+      from.lineLength = lineLen(from, from.next, pt.prev, pt);
+      len += from.lineLength;
+    }
+    from = pt;
+  });
+  if (pen.close) {
+    let to = calcAnchors[0];
+    from.lineLength = lineLen(from, from.next, to.prev, to);
+    len += from.lineLength;
+  }
+  pen.length = len;
+  return len;
+}
+
+function getLinePoints(pen: Pen, calcAnchors: any) {
+  const pts: Point[] = [];
+  let from: Point;
+  calcAnchors.forEach((pt: Point) => {
+    pts.push(pt);
+    from && pts.push(...getPoints(from, pt, pen));
+    from = pt;
+  });
+  if (pen.close && calcAnchors.length > 1) {
+    pts.push(...getPoints(from, calcAnchors[0], pen));
+  }
+  return pts;
+}
+
+function getRectOfPoints(points: Point[]): Rect {
+  let x = Infinity;
+  let y = Infinity;
+  let ex = -Infinity;
+  let ey = -Infinity;
+
+  points.forEach((item) => {
+    x = Math.min(x, item.x);
+    y = Math.min(y, item.y);
+    ex = Math.max(ex, item.x);
+    ey = Math.max(ey, item.y);
+  });
+  return { x, y, ex, ey, width: ex - x || 1, height: ey - y || 1 };
+}
+
+function getPoints(from: Point, to: Point, pen?: Pen) {
+  const pts: Point[] = [];
+  if (!to) {
+    return pts;
+  }
+
+  let step = 0.02;
+  if (from.lineLength) {
+    let r = 4;
+    if (pen && pen.lineWidth) {
+      r += pen.lineWidth / 2;
+    }
+    step = r / from.lineLength;
+  }
+  if (from.next) {
+    if (to.prev) {
+      for (let i = step; i < 1; i += step) {
+        pts.push(getBezierPoint(i, from, from.next, to.prev, to));
+      }
+    } else {
+      for (let i = step; i < 1; i += step) {
+        pts.push(getQuadraticPoint(i, from, from.next, to));
+      }
+    }
+  } else {
+    if (to.prev) {
+      for (let i = step; i < 1; i += step) {
+        pts.push(getQuadraticPoint(i, from, to.prev, to));
+      }
+    } else {
+      pts.push({ x: to.x, y: to.y });
+    }
+  }
+  if (pts.length > 1) {
+    from.curvePoints = pts;
+  }
+
+  return pts;
+}
+
+function getBezierPoint(
+  step: number,
+  from: Point,
+  cp1: Point,
+  cp2: Point,
+  to: Point
+) {
+  const { x: x1, y: y1 } = from;
+  const { x: x2, y: y2 } = to;
+  const { x: cx1, y: cy1 } = cp1;
+  const { x: cx2, y: cy2 } = cp2;
+
+  const pos = 1 - step;
+  const x =
+    x1 * pos * pos * pos +
+    3 * cx1 * step * pos * pos +
+    3 * cx2 * step * step * pos +
+    x2 * step * step * step;
+  const y =
+    y1 * pos * pos * pos +
+    3 * cy1 * step * pos * pos +
+    3 * cy2 * step * step * pos +
+    y2 * step * step * step;
+  return { x, y, step };
+}
+
+function getQuadraticPoint(step: number, from: Point, cp: Point, to: Point) {
+  const pos = 1 - step;
+  const x = pos * pos * from.x + 2 * pos * step * cp.x + step * step * to.x;
+  const y = pos * pos * from.y + 2 * pos * step * cp.y + step * step * to.y;
+  return { x, y, step };
+}
+
+function lineLen(from: Point, cp1?: Point, cp2?: Point, to?: Point): number {
+  if (!cp1 && !cp2) {
+    return (
+      Math.sqrt(
+        Math.pow(Math.abs(from.x - to.x), 2) +
+          Math.pow(Math.abs(from.y - to.y), 2)
+      ) || 0
+    );
+  }
+
+  const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
+  if (cp1 && cp2) {
+    path.setAttribute(
+      "d",
+      `M${from.x} ${from.y} C${cp1.x} ${cp1.y} ${cp2.x} ${cp2.y} ${to.x} ${to.y}`
+    );
+  } else if (cp1) {
+    path.setAttribute(
+      "d",
+      `M${from.x} ${from.y} Q${cp1.x} ${cp1.y} ${to.x} ${to.y}`
+    );
+  } else {
+    path.setAttribute(
+      "d",
+      `M${from.x} ${from.y} Q${cp2.x} ${cp2.y} ${to.x} ${to.y}`
+    );
+  }
+  return path.getTotalLength() || 0;
+}
+
+function calcRelativeRect(rect: Rect, worldRect: Rect) {
+  const relRect: Rect = {
+    x: (rect.x - worldRect.x) / worldRect.width || 0,
+    y: (rect.y - worldRect.y) / worldRect.height || 0,
+    width: rect.width / worldRect.width || 0,
+    height: rect.height / worldRect.height || 0,
+  };
+  relRect.ex = relRect.x + relRect.width;
+  relRect.ey = relRect.y + relRect.height;
+
+  return relRect;
+}
+
+function calcCenter(rect: Rect) {
+  if (!rect.center) {
+    rect.center = {} as Point;
+  }
+  rect.center.x = rect.x + rect.width / 2;
+  rect.center.y = rect.y + rect.height / 2;
+}
+
+// 锚点无需转换的 画笔类型
+const notTransformPens = ["triangle", "mindNode", "mindLine"];
+
+function setControledLines(el: any, calcAnchors: any) {
+  setTimeout(() => {
+    // 等节点被添加到数据中后,将节点与连线关联
+    const [fromNode] = meta2d.find((el.from && el.from.id) || "");
+    const [toNode] = meta2d.find((el.from && el.to.id) || "");
+    if (fromNode && fromNode.type === 0) {
+      if (!fromNode.connectedLines) {
+        fromNode.connectedLines = [];
+      }
+      fromNode.connectedLines.push({
+        anchor: `${
+          !notTransformPens.includes(fromNode.name || "")
+            ? transAnchorId(el.from.anchorIndex)
+            : el.from.anchorIndex
+        }`,
+        lineAnchor: calcAnchors[0].id,
+        lineId: el.id,
+      });
+    }
+    if (toNode && toNode.type === 0) {
+      if (!toNode.connectedLines) {
+        toNode.connectedLines = [];
+      }
+      toNode.connectedLines.push({
+        anchor: `${
+          !notTransformPens.includes(toNode.name || "")
+            ? transAnchorId(el.to.anchorIndex)
+            : el.to.anchorIndex
+        }`,
+        lineAnchor: calcAnchors[1].id,
+        lineId: el.id,
+      });
+    }
+  }, 0);
+}
+
+function isLine(name: string) {
+  return (
+    {
+      [LineSType.Curve]: true,
+      [LineSType.Mind]: true,
+      [LineSType.Line]: true,
+      [LineSType.Polyline]: true,
+    }[name] || false
+  );
+}
+
+function isPolyline(name: string) {
+  return (
+    {
+      [LineSType.Line]: true,
+      [LineSType.Polyline]: true,
+    }[name] || false
+  );
+}
+
+function isCurveline(name: string) {
+  return (
+    {
+      [LineSType.Curve]: true,
+      [LineSType.Mind]: true,
+    }[name] || false
+  );
+}
+
+function isPen(name: string) {
+  return name === "lines" || name === "graffiti";
+}
+
+// 版本比较函数
+export function compareVersion(version1: string, version2: string) {
+  const arr1 = version1.split(".");
+  const arr2 = version2.split(".");
+  const length1 = arr1.length;
+  const length2 = arr2.length;
+  const minlength = Math.min(length1, length2);
+  let i = 0;
+  for (i; i < minlength; i++) {
+    let a = parseInt(arr1[i]);
+    let b = parseInt(arr2[i]);
+    if (a > b) {
+      return 1;
+    } else if (a < b) {
+      return -1;
+    }
+  }
+  if (length1 > length2) {
+    for (let j = i; j < length1; j++) {
+      if (parseInt(arr1[j]) != 0) {
+        return 1;
+      }
+    }
+    return 0;
+  } else if (length1 < length2) {
+    for (let j = i; j < length2; j++) {
+      if (parseInt(arr2[j]) != 0) {
+        return -1;
+      }
+    }
+    return 0;
+  }
+  return 0;
+}
+
+function getLineFromTo(el: any) {
+  const rect = el.rect;
+  const from = el.from || {
+    x: rect.x,
+    y: rect.y + rect.height / 2,
+  };
+  const to = el.to || {
+    x: rect.x + rect.width,
+    y: rect.y + rect.height / 2,
+  };
+  return { from, to };
+}
+
+function convertEvents(events: any[], wheres: any[]): Event[] {
+  const newEvents = [];
+  const newEventType: EventName[] = [
+    "mousedown", // 老版本 click 对应新版本的 mousedown
+    "dblclick",
+    "valueUpdate",
+    "valueUpdate",
+    "enter",
+    "leave",
+    "click", // 老版本 mouseUp 对应新版本 click
+  ];
+  const newEventAction = [
+    EventAction.Link,
+    EventAction.StartAnimate,
+    EventAction.JS,
+    EventAction.GlobalFn,
+    undefined,
+    EventAction.PauseAnimate,
+    EventAction.StopAnimate,
+    EventAction.Emit,
+  ];
+
+  for (const event of events) {
+    const newEvent: any = {
+      name: newEventType[event.type],
+      action: newEventAction[event.action],
+      value: event.value,
+      params: event.params,
+      // TODO: 老版本 name 没有意义
+    };
+
+    newEvents.push(newEvent);
+  }
+
+  const whereActions: any = {
+    link: EventAction.Link,
+    StartAnimate: EventAction.StartAnimate,
+    PauseAnimate: EventAction.PauseAnimate,
+    StopAnimate: EventAction.StopAnimate,
+    Function: EventAction.JS,
+    GlobalFn: EventAction.GlobalFn,
+    Emit: EventAction.Emit,
+  };
+
+  for (const where of wheres) {
+    // 一个 where 可以有多个事件
+    for (const action of where.actions) {
+      const newEvent: any = {
+        name: "valueUpdate",
+        action: whereActions[action.do],
+        where: {
+          key: where.key,
+          comparison: where.comparison,
+          value: where.value,
+          fnJs: where.fn,
+        },
+      };
+      switch (newEvent.action) {
+        case EventAction.Link:
+          newEvent.value = action.url;
+          newEvent.params = action._blank;
+          break;
+        case EventAction.StartAnimate:
+        case EventAction.PauseAnimate:
+        case EventAction.StopAnimate:
+          newEvent.value = action.tag;
+          break;
+        case EventAction.JS:
+        case EventAction.GlobalFn:
+        case EventAction.Emit:
+          newEvent.value = action.fn;
+          newEvent.params = action.params;
+          break;
+      }
+
+      newEvents.push(newEvent);
+    }
+  }
+  return newEvents;
+}

+ 101 - 0
src/services/utils.ts

@@ -1,4 +1,8 @@
 import { FormItem, Pen, Meta2d, Meta2dData } from "@meta2d/core";
+import { MessagePlugin, NotifyPlugin } from "tdesign-vue-next";
+import { ref } from "vue";
+
+const market = import.meta.env.VITE_MARKET;
 
 export interface Meta2dBackData extends Meta2dData {
   id?: string;
@@ -26,3 +30,100 @@ export interface Meta2dBackData extends Meta2dData {
   editorId?: string;
   editorName?: string;
 }
+
+const notification = ref<any>(null);
+export function showNotification(title: string): Promise<boolean> {
+  return new Promise<boolean>((resolve) => {
+    if (!notification.value) {
+      notification.value = NotifyPlugin.info({
+        title: "提示",
+        content: title,
+        closeBtn: "确定",
+        onCloseBtnClick: () => {
+          //关闭按钮
+          NotifyPlugin.close(notification.value);
+          notification.value = null;
+          resolve(true);
+        },
+        onDurationEnd: () => {
+          //计时结束
+          resolve(false);
+        },
+      });
+    }
+  });
+}
+
+export async function dealwithFormatbeforeOpen(data: Meta2dBackData) {
+  if (!data) {
+    return;
+  }
+  if ((!data.https || data.https?.length == 0) && data.http) {
+    data.https = [
+      {
+        http: data.http,
+        httpTimeInterval: data.httpTimeInterval,
+        httpHeaders: data.httpHeaders,
+      },
+    ];
+    delete data.http;
+    delete data.httpHeaders;
+    delete data.httpTimeInterval;
+  }
+  //新版渐进色
+  data.pens &&
+    data.pens.forEach((pen) => {
+      if (pen.lineGradientFromColor && pen.lineGradientToColor) {
+        pen.lineGradientColors = `linear-gradient(${
+          pen.lineGradientAngle ? Number(pen.lineGradientAngle) + 90 : 0
+        }deg,${pen.lineGradientFromColor} 0%,${pen.lineGradientToColor} 100%)`;
+      }
+      if (pen.gradientFromColor && pen.gradientToColor) {
+        pen.gradientColors = `linear-gradient(${
+          pen.gradientAngle ? Number(pen.gradientAngle) + 90 : 0
+        }deg,${pen.gradientFromColor} 0%,${pen.gradientToColor} 100%)`;
+      }
+    });
+}
+
+export function gotoAccount() {
+  MessagePlugin.info({
+    content: "请开通vip,即将跳转到开通页面...",
+    duration: 3,
+  });
+  setTimeout(() => {
+    if (market) {
+      window.open("/account?unVip=true");
+    } else {
+      let arr = location.host.split(".");
+      arr[0] = "http://account";
+      let accountUrl = arr.join(".");
+      window.open(`${accountUrl}?unVip=true`);
+    }
+  }, 3000);
+}
+
+export function isGif(url: string): boolean {
+  return url.endsWith(".gif");
+}
+
+/**
+ * 正常的 assign 操作,是存在弊端的,
+ * 若源对象存在该属性,但目标对象不存在该属性(不存在并非 undefined),则会导致无法覆盖
+ * 该方法会把源对象的属性全部清空,然后再把目标对象的属性覆盖到源对象上
+ * source 可能是个监听的对象,所有最后一步再更改它的属性值
+ * @param source 原对象
+ * @param target 目标对象
+ */
+export function strictAssign(
+  source: Record<string, any>,
+  target: Record<string, any>
+) {
+  // source 的全部属性都是 undefined 的对象,而非没有这个属性
+  const undefinedSource: Record<string, any> = {};
+  Object.keys(source).forEach((key) => {
+    undefinedSource[key] = undefined;
+  });
+  Object.assign(undefinedSource, target);
+  Object.assign(source, undefinedSource);
+}

+ 88 - 9
src/views/Index.vue

@@ -15,23 +15,102 @@
 </template>
 
 <script lang="ts" setup>
-import { onBeforeMount, reactive } from 'vue';
-import axios from 'axios';
+import {
+  onBeforeMount,
+  reactive,
+  onMounted,
+  onUnmounted,
+  ref,
+  provide,
+  nextTick
+} from "vue";
+import axios from "axios";
+// import { Pen } from "@meta2d/core";
+import { strictAssign } from "@/services/utils";
 
-import Header from './components/Header.vue';
-import Graphics from './components/Graphics.vue';
-import View from './components/View.vue';
-import FileProps from './components/FileProps.vue';
-import PenProps from './components/PenProps.vue';
-import PensProps from './components/PensProps.vue';
+import Header from "./components/Header.vue";
+import Graphics from "./components/Graphics.vue";
+import View from "./components/View.vue";
+import FileProps from "./components/FileProps.vue";
+import PenProps from "./components/PenProps.vue";
+import PensProps from "./components/PensProps.vue";
 
-import { useSelection, SelectionMode } from '@/services/selections';
+import { useSelection, SelectionMode } from "@/services/selections";
 
 const { selections } = useSelection();
 
 const data = reactive<any>({});
+// const activePen= ref<Pen>({});
+//   const activePens= ref<Pen[]>([]);
+//   provide('activePen', activePen);
+//   provide('activePens', activePens);
 
 onBeforeMount(async () => {});
+
+const active = (oldPens: Pen[]) => {
+  setTimeout(() => {
+    const pens = getPenRectPens(oldPens);
+    checkPropType(pens);
+    // 永远提供一个数组值
+    // activePens.value = oldPens;
+    selections.pens = oldPens;
+  }, 1);
+};
+
+/**
+ * 根据当前传入的 pens 判断属性面板的类型
+ */
+const checkPropType = (pens: Pen[]) => {
+  if (pens.length === 1) {
+    // 选中单个画笔
+    selections.mode = SelectionMode.Pen;
+    if (Array.isArray(pens[0].frames)) {
+      (pens[0] as any).showDuration = (<any>globalThis).meta2d.calcAnimateDuration(pens[0]);
+    }
+    // strictAssign(activePen.value, pens[0]);
+    selections.pen = pens[0];
+    // changeHidden(lastActive.Pen);
+  } else if (pens.length > 1) {
+    selections.mode = SelectionMode.Pens;
+    // type.value = Type.Pens;
+    // changeHidden(lastActive.Pens);
+  } else {
+    selections.mode = SelectionMode.File;
+    // type.value = Type.Canvas;
+    // changeHidden(lastActive.Canvas);
+  }
+};
+
+function getPenRectPens(oldPens: Pen[]): Pen[] {
+  return oldPens.map((pen) => {
+    const rect = (<any>globalThis).meta2d.getPenRect(pen);
+    return {
+      ...pen,
+      ...rect,
+    };
+  });
+}
+
+const inactive = (pens: Pen[]) => {
+  setTimeout(() => {
+    active((<any>globalThis).meta2d.store.active);
+  });
+};
+
+// onMounted(() => {
+//   meta2d.on("active", active);
+//   meta2d.on("inactive", inactive);
+// });
+
+nextTick(() => {
+  (<any>globalThis).meta2d.on("active", active);
+    (<any>globalThis).meta2d.on("inactive", inactive);
+});
+
+onUnmounted(() => {
+  (<any>globalThis).meta2d.off("active", active);
+    (<any>globalThis).meta2d.off("inactive", inactive);
+});
 </script>
 
 <style lang="postcss" scoped>

+ 50 - 0
src/views/components/FileProps.vue

@@ -124,6 +124,7 @@
                   <t-button
                     variant="outline"
                     style="padding: 0 6px; margin: 2px 8px"
+                    @click="clickInit"
                   >
                     <t-icon name="ellipsis" />
                   </t-button>
@@ -133,6 +134,7 @@
                   <t-button
                     variant="outline"
                     style="padding: 0 6px; margin: 2px 8px"
+                    @click="clickFormat"
                   >
                     <t-icon name="ellipsis" />
                   </t-button>
@@ -146,6 +148,21 @@
         <ElementTree />
       </t-tab-panel>
     </t-tabs>
+    <t-dialog v-model:visible="initVisible">
+            <p>This is a dialog</p>
+          </t-dialog>
+    <MonacoModal 
+      :visible="initVisible"
+      :code="initCode"
+      title="初始化动作"
+      language="javascript"
+      @changeCode="changeInitCode"/>
+    <MonacoModal 
+      :visible="formatVisible"
+      :code="formatCode"
+      title="数据格式转换"
+      language="javascript"
+      @changeCode="changeformatCode"/>
   </div>
 </template>
 
@@ -154,6 +171,7 @@ import { onMounted, reactive, onUnmounted, ref } from "vue";
 import { getCookie } from "@/services/cookie";
 import ElementTree from "./ElementTree.vue";
 import { Meta2dData } from "@meta2d/core";
+import MonacoModal from "./common/MonacoModal.vue";
 
 const headers = {
   Authorization: "Bearer " + (localStorage.token || getCookie("token") || ""),
@@ -264,6 +282,38 @@ function openData() {
     ];
   }
 }
+
+// const modal = ref<InstanceType<typeof MyModal> | null>(null)
+const initVisible = ref(false);
+const formatVisible = ref(false);
+const initCode = ref('');
+const formatCode = ref('');
+
+const clickInit = () => {
+  initVisible.value = true;
+}
+
+const clickFormat = () => {
+  formatVisible.value = true;
+}
+const changeInitCode = (code: string) => {
+  console.log(code, initCode.value);
+  try {
+          code = JSON.parse(code);
+        } catch (error) {
+          console.log('无法转换成 js 对象', error);
+          return; // return 不修改外界值
+        }
+}
+
+const changeformatCode = (code:string) => {
+  try {
+          code = JSON.parse(code);
+        } catch (error) {
+          console.log('无法转换成 js 对象', error);
+          return; // return 不修改外界值
+        }
+}
 </script>
 <style lang="postcss" scoped>
 .props {

+ 310 - 9
src/views/components/Header.vue

@@ -13,13 +13,13 @@
     >
       <a> 文件 </a>
       <t-dropdown-menu>
-        <t-dropdown-item>
+        <t-dropdown-item @click="newFile">
           <a>新建文件</a>
         </t-dropdown-item>
-        <t-dropdown-item>
+        <t-dropdown-item @click="openFile">
           <a>打开文件</a>
         </t-dropdown-item>
-        <t-dropdown-item divider="true">
+        <t-dropdown-item divider="true" @click="loadFile">
           <a>导入文件</a>
         </t-dropdown-item>
         <t-dropdown-item>
@@ -264,28 +264,44 @@
       </t-dropdown-menu>
     </t-dropdown>
     <div class="flex middle" v-else>
-      <a class="button primary solid" style="width: 80px" :href="login()">登录</a>
+      <a class="button primary solid" style="width: 80px" :href="login()"
+        >登录</a
+      >
     </div>
   </div>
 </template>
 
 <script lang="ts" setup>
-import { reactive } from 'vue';
-import { useUser } from '@/services/user';
+import { reactive, ref } from "vue";
+import { useRouter } from "vue-router";
+import { useUser } from "@/services/user";
+import { NotifyPlugin, MessagePlugin } from "tdesign-vue-next";
+import {
+  showNotification,
+  Meta2dBackData,
+  dealwithFormatbeforeOpen,
+  gotoAccount,
+} from "@/services/utils";
+import { readFile, upload } from "@/services/file";
+import { compareVersion, baseVer, upgrade } from "@/services/upgrade";
+import { parseSvg } from "@meta2d/svg";
+import { Pen } from "@meta2d/core";
 
+const router = useRouter();
 const market = import.meta.env.VITE_MARKET;
 
-const baseUrl = import.meta.env.BASE_URL || '/';
+const baseUrl = import.meta.env.BASE_URL || "/";
 
 const { user, message, getUser, getMessage, signout } = useUser();
 
+const isNew = ref(false);
 const data = reactive({
-  name: '空白文件',
+  name: "空白文件",
 });
 
 function login() {
   //TODO 临时地址
-  return `https://account.le5le.com/?cb=${encodeURIComponent(location.href)}`
+  return `https://account.le5le.com/?cb=${encodeURIComponent(location.href)}`;
   // if (market) {
   //       return `/account/login?cb=${encodeURIComponent(location.href)}`;
   //     } else {
@@ -297,6 +313,291 @@ function login() {
   //       }?cb=${encodeURIComponent(location.href)}`;
   //     }
 }
+
+const title = "系统可能不会保存您所做的更改,是否继续?";
+const newFile = async () => {
+  if (isNew.value) {
+    if (await showNotification(title)) {
+      newfile(false);
+    }
+  } else {
+    newfile(false);
+  }
+};
+
+const pen = ref(false);
+const drawPen = () => {
+  (<any>globalThis).meta2d.inactive();
+  try {
+    if (!(<any>globalThis).meta2d.canvas.drawingLineName) {
+      // 开了钢笔,需要关掉铅笔
+      (<any>globalThis).meta2d.canvas.pencil && drawingPencil();
+      (<any>globalThis).meta2d.drawLine(
+        (<any>globalThis).meta2d.store.options.drawingLineName
+      );
+    } else {
+      (<any>globalThis).meta2d.finishDrawLine();
+      (<any>globalThis).meta2d.drawLine();
+    }
+    //钢笔
+    pen.value = !!(<any>globalThis).meta2d.canvas.drawingLineName;
+  } catch (e: any) {
+    MessagePlugin.warning(e.message);
+  }
+};
+
+const pencil = ref(false);
+const drawingPencil = () => {
+  try {
+    if (!(<any>globalThis).meta2d.canvas.pencil) {
+      // 开了铅笔需要关掉钢笔
+      (<any>globalThis).meta2d.canvas.drawingLineName && drawPen();
+      (<any>globalThis).meta2d.drawingPencil();
+    } else {
+      (<any>globalThis).meta2d.stopPencil();
+    }
+    pencil.value = (<any>globalThis).meta2d.canvas.pencil;
+  } catch (e: any) {
+    MessagePlugin.warning(e.message);
+  }
+};
+
+const magnifier = ref(false);
+const showMagnifier = () => {
+  if (!(<any>globalThis).meta2d.canvas.magnifierCanvas.magnifier) {
+    (<any>globalThis).meta2d.showMagnifier();
+  } else {
+    (<any>globalThis).meta2d.hideMagnifier();
+  }
+  magnifier.value = (<any>globalThis).meta2d.canvas.magnifierCanvas.magnifier;
+};
+
+const map = ref(false);
+const showMap = () => {
+  if (!(<any>globalThis).meta2d.map?.isShow) {
+    (<any>globalThis).meta2d.showMap();
+  } else {
+    (<any>globalThis).meta2d.hideMap();
+  }
+  map.value = (<any>globalThis).meta2d.map?.isShow;
+};
+
+async function newfile(noRouter: boolean = false) {
+  (<any>globalThis).meta2d.canvas.drawingLineName && drawPen();
+  (<any>globalThis).meta2d.canvas.pencil && drawingPencil();
+  (<any>globalThis).meta2d.canvas.magnifierCanvas.magnifier && showMagnifier();
+  (<any>globalThis).meta2d.map?.isShow && showMap();
+  isNew.value = false;
+  // await localforage.removeItem(localMeta2dDataName);
+  // 打开文件操作不跳转
+  !noRouter &&
+    router.replace({
+      path: "/",
+      query: { r: Date.now() + "" },
+    });
+}
+
+function load(newT: boolean = false) {
+  const input = document.createElement("input");
+  input.type = "file";
+  input.onchange = (event) => {
+    const elem = event.target as HTMLInputElement;
+    if (elem.files && elem.files[0]) {
+      newT && newfile(true);
+      // 路由跳转 可能在 openFile 后执行
+      if (elem.files[0].name.endsWith(".json")) {
+        openJson(elem.files[0], newT);
+      } else if (elem.files[0].name.endsWith(".svg")) {
+        MessagePlugin.info(
+          "可二次编辑但转换存在损失,若作为图片使用,请使用右侧属性面板的上传图片功能"
+        );
+        openSvg(elem.files[0]);
+      } else if (elem.files[0].name.endsWith(".zip")) {
+        openZip(elem.files[0], newT);
+      } else {
+        MessagePlugin.info("打开文件只支持 json,svg,zip 格式");
+      }
+    }
+  };
+  input.click();
+}
+
+const openJson = async (file: File, isNew: boolean = false) => {
+  const text = await readFile(file);
+  try {
+    let data: Meta2dBackData = JSON.parse(text);
+    if (!data.name) {
+      data.name = file.name.replace(".json", "");
+    }
+    if (!data.version || compareVersion(data.version, baseVer) === -1) {
+      // 如果版本号不存在或者版本号 version < 1.0.0
+      data = upgrade(data, baseVer);
+    }
+    dealwithFormatbeforeOpen(data);
+    data._id = undefined;
+    if (!isNew) {
+      delete data.owner;
+      delete data.editor;
+      delete data.username;
+      delete data.editorId;
+      delete data.editorName;
+    }
+    // if (!(window as any).meta2dFolder?.includes(data.folder)) {
+    //   delete data.folder;
+    // }
+    (<any>globalThis).meta2d.open(data);
+  } catch (e) {
+    console.log(e);
+  }
+};
+
+const openSvg = async (file: File) => {
+  const text = await readFile(file);
+  const pens: Pen[] = parseSvg(text);
+  (<any>globalThis).meta2d.canvas.addCaches = pens;
+  MessagePlugin.info("svg转换成功,请点击画布决定放置位置");
+};
+
+const openZip = async (file: File, isNew: boolean = false) => {
+  if (!(user && user.username)) {
+    MessagePlugin.warning("请先登录,否则无法保存!");
+    return;
+  }
+
+  if (!user.vipExpired) {
+    // vipVisible.value = true;
+    gotoAccount();
+    return;
+  }
+
+  const { default: JSZip } = await import("jszip");
+  const zip = new JSZip();
+  await zip.loadAsync(file);
+
+  let dataStr = "";
+  for (const key in zip.files) {
+    if (zip.files[key].dir) {
+      continue;
+    }
+    if (key.endsWith(".json")) {
+      // 认为只有一个 json 文件
+      // dataStr = await zip.file(key).async('string');
+      break;
+    }
+  }
+
+  if (!dataStr) {
+    return false;
+  }
+
+  for (const key in zip.files) {
+    if (zip.files[key].dir) {
+      continue;
+    }
+    // let _png = key.indexOf('/png');
+    // let _img = key.indexOf('/img');
+    // let _image = key.indexOf('/image');
+    // let _file = key.indexOf('/file');
+    let _keyLower = key.toLowerCase();
+    // if (!key.endsWith('.json') && (_png !== -1 || _img !== -1 || _image !== -1 || _file !== -1)) {
+    if (
+      _keyLower.endsWith(".png") ||
+      _keyLower.endsWith(".svg") ||
+      _keyLower.endsWith(".gif") ||
+      _keyLower.endsWith(".jpg") ||
+      _keyLower.endsWith(".jpeg")
+    ) {
+      let filename = key.substr(key.lastIndexOf("/") + 1);
+      const extPos = filename.lastIndexOf(".");
+      let ext = "";
+      if (extPos > 0) {
+        ext = filename.substr(extPos);
+      }
+      filename = filename.substring(0, extPos > 8 ? 8 : extPos);
+
+      // 上传文件
+      const result:any ={}
+      //   await upload(
+      //   // await zip.file(key).async('blob'),
+      //   true,
+      //   filename + ext,
+      //   "/2D/未分类"
+      // );
+      let _key = key;
+      // if (_png) {
+      //   _key = key.substring(_png);
+      // } else if (_image) {
+      //   _key = key.substring(_png);
+      // } else if (_img) {
+      //   _key = key.substring(_img);
+      // } else if (_file) {
+      //   _key = key.substring(_file);
+      // }
+      if (result) {
+        if (dataStr.replaceAll) {
+          //'le5le.meta2d'
+          dataStr = dataStr.replaceAll(_key.slice(12), result.url);
+        } else {
+          while (dataStr.includes(_key)) {
+            dataStr = dataStr.replace(_key.slice(12), result.url);
+            // 正则 gm 在特殊情况下报错,例如如下场景
+            /**
+                 *    
+      const data = '{"image":"/image/materials/IoT-Chemical(化学)/Air stripper 2(汽提塔2).svg"}';
+      const key = '/image/materials/IoT-Chemical(化学)/Air stripper 2(汽提塔2).svg';
+      data.replace(key, '123');
+      data.replaceAll(key, '123')
+      data.replace(new RegExp(key, 'gm'), '123');
+      data.replace(new RegExp(key, 'g'), '123');
+                 */
+          }
+        }
+      }
+    }
+  }
+
+  try {
+    let data: Meta2dBackData = JSON.parse(dataStr);
+    if (data) {
+      if (!data.name) {
+        data.name = file.name.replace(".zip", "");
+      }
+      if (!data.version || compareVersion(data.version, baseVer) === -1) {
+        // 如果版本号不存在或者版本号 version < 1.0.0
+        data = upgrade(data, baseVer);
+      }
+      dealwithFormatbeforeOpen(data);
+      data._id = undefined;
+      if (!isNew) {
+        delete data.owner;
+        delete data.editor;
+        delete data.username;
+        delete data.editorId;
+        delete data.editorName;
+      }
+      if (!(window as any).meta2dFolder?.includes(data.folder)) {
+        delete data.folder;
+      }
+      (<any>globalThis).meta2d.open(data);
+    }
+  } catch (e) {
+    return false;
+  }
+};
+
+async function loadFile(newT: boolean = false) {
+  if (isNew.value) {
+    if (await showNotification(title)) {
+      load(newT);
+    }
+  } else {
+    load(newT);
+  }
+}
+
+async function openFile() {
+  loadFile(true);
+}
 </script>
 <style lang="postcss" scoped>
 .app-header {

+ 109 - 0
src/views/components/common/MonacoModal.vue

@@ -0,0 +1,109 @@
+<template>
+  <div>
+  <t-dialog :header="title" className="" :visible="visible" width="900px" :on-confirm="handleOk" :on-cancel="cancel">
+    <!-- <div ref="monacoDom" class="monaco"></div> -->
+    <p>及惹</p>
+ </t-dialog>
+</div>
+</template>
+
+<script setup lang="ts">
+import {
+  defineComponent,
+  nextTick,
+  onMounted,
+  onUnmounted,
+  PropType,
+  reactive,
+  ref,
+  watch,
+} from "vue";
+import { monaco } from "./customMonaco";
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    require: true,
+  },
+  title: {
+    type: String,
+    default: () => {
+      return "JavaScript";
+    },
+  },
+  code: {
+    type: String,
+    default: () => {
+      return "";
+    },
+  },
+  language: {
+    type: String,
+    default: () => {
+      return "javascript";
+    },
+    validator: (value: string) => {
+      // 这个值必须匹配下列字符串中的一个
+      return ["javascript", "json", "markdown"].includes(value);
+    },
+  },
+});
+
+const emit = defineEmits(["update:visible", "changeCode"]);
+
+let editor: any = null;
+
+function handleOk() {
+  // 按下确认以后修改外界值
+  const code = editor.getValue();
+  emit("changeCode", code);
+  emit("update:visible", false);
+}
+
+function cancel() {
+  emit("update:visible", false);
+}
+
+const curTheme = "vs-dark"; // 暗主题
+const monacoDom: any = ref(null);
+
+watch(
+  () => props.visible,
+    (newV) => {
+    console.log("props.visible", props.visible);
+    // if (newV) {
+    //   nextTick(async () => {
+    //     if (!editor) {
+    //       editor = monaco.editor.create(monacoDom.value, {
+    //         theme: curTheme,
+    //         automaticLayout: true,
+    //         language: props.language,
+    //       });
+    //       console.log("进入")
+    //     }
+    //     // 可见状态
+    //     editor.setValue(props.code);
+    //     monaco.editor.setModelLanguage(editor.getModel(), props.language);
+    //     // 格式化
+    //     setTimeout(() => {
+    //       editor.getAction(["editor.action.formatDocument"])._run();
+    //     }, 300);
+    //   });
+    // }
+  }
+);
+
+onUnmounted(() => {
+  editor?.dispose();
+});
+</script>
+
+<style lang="postcss">
+.editorModal {
+  .ant-modal-body {
+    padding: 0;
+    .monaco {
+      height: 400px;
+    }
+  }
+}
+</style>

+ 9 - 0
src/views/components/common/customMonaco.ts

@@ -0,0 +1,9 @@
+import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js';
+import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution';
+import 'monaco-editor/esm/vs/basic-languages/markdown/markdown.contribution';
+import 'monaco-editor/esm/vs/editor/editor.all.js';
+
+import 'monaco-editor/esm/vs/language/typescript/monaco.contribution';
+import 'monaco-editor/esm/vs/language/json/monaco.contribution';
+
+export { monaco };

+ 2 - 1
vite.config.ts

@@ -2,10 +2,11 @@ import { defineConfig } from "vite";
 import vue from "@vitejs/plugin-vue";
 import vueJsx from "@vitejs/plugin-vue-jsx";
 import * as path from "path";
+import monacoEditorPlugin from "vite-plugin-monaco-editor";
 
 // https://vitejs.dev/config/
 export default defineConfig({
-  plugins: [vue(), vueJsx()],
+  plugins: [vue(), vueJsx(), monacoEditorPlugin({})],
   resolve: {
     alias: {
       "@": path.resolve(__dirname, "./src/"),