Parcourir la source

Merge branch 'main' into hvac

wangcong il y a 1 semaine
Parent
commit
adab00b6ae
59 fichiers modifiés avec 6787 ajouts et 431 suppressions
  1. 1 2
      index.html
  2. 34 33
      package.json
  3. 1 4
      public/icon/v/iconfont.css
  4. 0 0
      public/icon/v/iconfont.js
  5. 126 0
      public/icon/v/iconfont.json
  6. 36 0
      public/icon/v/iconfont.svg
  7. BIN
      public/icon/v/iconfont.ttf
  8. BIN
      public/icon/v/iconfont.woff
  9. BIN
      public/icon/v/iconfont.woff2
  10. 377 0
      public/pen.d.ts
  11. 1 0
      public/view/meta2d-react/index.html
  12. 26 1
      public/view/meta2d-react/src/index.css
  13. 24 1
      public/view/meta2d-react/src/views/2d/Meta2d.tsx
  14. 1 0
      public/view/meta2d-vue2/index.html
  15. 26 0
      public/view/meta2d-vue2/src/App.vue
  16. 23 2
      public/view/meta2d-vue2/src/views/2d/Meta2d.vue
  17. 1 0
      public/view/meta2d-vue3/index.html
  18. 26 0
      public/view/meta2d-vue3/src/style.css
  19. 22 1
      public/view/meta2d-vue3/src/views/2d/Meta2d.vue
  20. 320 2
      src/i18n/lang/en.ts
  21. 316 1
      src/i18n/lang/zh_CHT.ts
  22. 4 0
      src/services/api.ts
  23. 21 6
      src/services/common.ts
  24. 150 35
      src/services/defaults.ts
  25. 92 29
      src/services/download.ts
  26. 414 0
      src/services/handle3d.ts
  27. 60 45
      src/services/load3d.ts
  28. 1721 0
      src/services/project.ts
  29. 60 1
      src/services/utils.ts
  30. 2 1
      src/views/Index.vue
  31. 45 11
      src/views/components/Actions.vue
  32. 15 11
      src/views/components/ContextMenu.vue
  33. 6 2
      src/views/components/Custom.vue
  34. 226 96
      src/views/components/DataSource.vue
  35. 83 2
      src/views/components/FileProps.vue
  36. 12 8
      src/views/components/FitProps.vue
  37. 41 9
      src/views/components/Graphics.vue
  38. 82 17
      src/views/components/Header.vue
  39. 36 23
      src/views/components/Net.vue
  40. 3 3
      src/views/components/Network.vue
  41. 467 6
      src/views/components/PenAnimates.vue
  42. 2 2
      src/views/components/PenDatas.vue
  43. 9 3
      src/views/components/PenEvents.vue
  44. 24 32
      src/views/components/PenProps.vue
  45. 4 4
      src/views/components/PensProps.vue
  46. 619 0
      src/views/components/Project.vue
  47. 8 0
      src/views/components/Structure.vue
  48. 15 6
      src/views/components/View.vue
  49. 242 0
      src/views/components/common/BezierEditor.vue
  50. 81 1
      src/views/components/common/CodeEditor.vue
  51. 5 5
      src/views/components/common/JsonModal.vue
  52. 9 6
      src/views/components/common/MoreModal.vue
  53. 3 3
      src/views/components/common/PenPropModal.vue
  54. 316 0
      src/views/components/common/ProjectCPayModal.vue
  55. 153 0
      src/views/components/common/ProjectModal.vue
  56. 282 0
      src/views/components/common/ProjectPayModal.vue
  57. 18 15
      src/views/components/common/PropModal.vue
  58. 94 0
      src/views/components/common/StepModal.vue
  59. 2 2
      vite.config.ts

+ 1 - 2
index.html

@@ -13,7 +13,7 @@
       name="description"
       content="亿维物联 - 帮助企业快速搭建可视化或企业信息系统,提高开发效率,降低开发成本和运营成本。"
     />
-
+    <script src="/js/echarts.min.js"></script>
     <style type="text/css">
       .l-icon {
         width: 1em;
@@ -53,7 +53,6 @@
     <script defer src="/js/canvas2svg.js"></script>
     <script defer src="/js/flv.min.js"></script>
     <script defer src="/js/marked.min.js"></script>
-    <script defer src="/js/echarts.min.js"></script>
     <script defer src="/js/mycharts.js"></script>
     <script defer src="/js/papaparse.min.js"></script>
     <script src="/js/rg.js"></script>

+ 34 - 33
package.json

@@ -12,52 +12,53 @@
     "icon:see": "open src/icons/fonts/index.html"
   },
   "dependencies": {
-    "@meta2d/activity-diagram": "^1.0.0",
-    "@meta2d/chart-diagram": "^1.0.6",
-    "@meta2d/class-diagram": "^1.0.1",
-    "@meta2d/core": "^1.0.72",
-    "@meta2d/flow-diagram": "^1.0.0",
-    "@meta2d/form-diagram": "^1.0.8",
+    "@meta2d/activity-diagram": "^1.0.1",
+    "@meta2d/chart-diagram": "^1.0.13",
+    "@meta2d/class-diagram": "^1.0.2",
+    "@meta2d/core": "^1.0.82",
+    "@meta2d/flow-diagram": "^1.0.2",
+    "@meta2d/form-diagram": "^1.0.27",
     "visio2meta2d":"file:../visio2meta2d",
     "dxf": "file:../dxf",
-    "@meta2d/fta-diagram": "^1.0.0",
-    "@meta2d/le5le-charts": "^1.0.3",
-    "@meta2d/sequence-diagram": "^1.0.0",
-    "@meta2d/svg": "^1.0.3",
-    "@meta2d/utils": "^1.0.0",
-    "axios": "^0.26.0",
-    "crypto-js": "^4.1.1",
-    "dayjs": "^1.11.5",
-    "exceljs": "^4.3.0",
-    "fast-xml-parser": "^4.0.1",
+    "@meta2d/fta-diagram": "^1.0.1",
+    "@meta2d/le5le-charts": "^1.0.5",
+    "@meta2d/sequence-diagram": "^1.0.1",
+    "@meta2d/svg": "^1.0.7",
+    "@meta2d/utils": "^1.0.1",
+    "axios": "^0.26.1",
+    "crypto-js": "^4.2.0",
+    "dayjs": "^1.11.13",
+    "exceljs": "^4.4.0",
+    "fast-xml-parser": "^4.5.3",
     "file-saver": "^2.0.5",
     "jszip": "^3.10.0",
     "@le5le/auth-token": "^1.0.5",
     "localforage": "^1.10.0",
     "monaco-editor": "^0.38.0",
-    "qrcode": "^1.5.3",
-    "tdesign-icons-vue-next": "^0.2.2",
-    "tdesign-vue-next": "^1.7.0",
-    "vue": "^3.3.4",
-    "vue-router": "^4.2.0",
+    "qrcode": "^1.5.4",
+    "tdesign-icons-vue-next": "^0.2.6",
+    "tdesign-vue-next": "^1.12.0",
+    "vue": "^3.5.13",
+    "vue-router": "^4.5.0",
     "vue-i18n": "^10.0.4"
   },
   "devDependencies": {
-    "@types/file-saver": "^2.0.5",
-    "@types/node": "^20.4.1",
-    "@types/offscreencanvas": "^2019.7.0",
-    "@types/qrcode": "^1.5.0",
-    "@vitejs/plugin-vue": "^4.2.3",
-    "@vitejs/plugin-vue-jsx": "^3.0.0",
-    "autoprefixer": "^10.4.13",
-    "formidable": "^2.0.1",
-    "postcss": "^8.4.6",
+    "@tsconfig/node22": "^22.0.0",
+    "@types/file-saver": "^2.0.7",
+    "@types/node": "^22.13.10",
+    "@types/offscreencanvas": "^2019.7.3",
+    "@types/qrcode": "^1.5.5",
+    "@vitejs/plugin-vue": "^5.2.2",
+    "@vitejs/plugin-vue-jsx": "^4.1.2",
+    "autoprefixer": "^10.4.21",
+    "formidable": "^2.1.2",
+    "postcss": "^8.5.3",
     "postcss-import": "^14.1.0",
-    "postcss-nested": "^6.0.1",
+    "postcss-nested": "^6.2.0",
     "svgo": "^3.3.2",
     "svgtofont": "4.2.0",
-    "typescript": "^4.7.4",
-    "vite": "^4.4.2",
+    "typescript": "~5.6.3",
+    "vite": "^6.2.2",
     "vite-plugin-compression": "^0.5.1",
     "vite-plugin-monaco-editor": "^1.1.0",
     "vue-tsc": "^1.8.3"

Fichier diff supprimé car celui-ci est trop grand
+ 1 - 4
public/icon/v/iconfont.css


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
public/icon/v/iconfont.js


+ 126 - 0
public/icon/v/iconfont.json

@@ -5,6 +5,132 @@
   "css_prefix_text": "l-",
   "description": "乐吾乐2D可视化编辑器",
   "glyphs": [
+    {
+      "icon_id": "44205190",
+      "name": "树形选择器",
+      "font_class": "shuxingxuanzeqi",
+      "unicode": "eacf",
+      "unicode_decimal": 60111
+    },
+    {
+      "icon_id": "44204366",
+      "name": "日期--周",
+      "font_class": "riqi--zhou",
+      "unicode": "eacd",
+      "unicode_decimal": 60109
+    },
+    {
+      "icon_id": "44204365",
+      "name": "日期--年",
+      "font_class": "riqi--nian",
+      "unicode": "eace",
+      "unicode_decimal": 60110
+    },
+    {
+      "icon_id": "44204367",
+      "name": "区间--周",
+      "font_class": "qujian--zhou",
+      "unicode": "eacc",
+      "unicode_decimal": 60108
+    },
+    {
+      "icon_id": "44199401",
+      "name": "树-异步",
+      "font_class": "shu-yibu",
+      "unicode": "eac3",
+      "unicode_decimal": 60099
+    },
+    {
+      "icon_id": "44199399",
+      "name": "日期--月",
+      "font_class": "riqi--yue",
+      "unicode": "eac4",
+      "unicode_decimal": 60100
+    },
+    {
+      "icon_id": "44199398",
+      "name": "区间--月",
+      "font_class": "qujian--yue",
+      "unicode": "eac5",
+      "unicode_decimal": 60101
+    },
+    {
+      "icon_id": "44199397",
+      "name": "日期--时间",
+      "font_class": "riqi--shijian",
+      "unicode": "eac6",
+      "unicode_decimal": 60102
+    },
+    {
+      "icon_id": "44199394",
+      "name": "级联--异步",
+      "font_class": "jilian--yibu",
+      "unicode": "eac7",
+      "unicode_decimal": 60103
+    },
+    {
+      "icon_id": "44199395",
+      "name": "区间--时间",
+      "font_class": "qujian--shijian",
+      "unicode": "eac8",
+      "unicode_decimal": 60104
+    },
+    {
+      "icon_id": "44199396",
+      "name": "区间--年",
+      "font_class": "qujian--nian",
+      "unicode": "eac9",
+      "unicode_decimal": 60105
+    },
+    {
+      "icon_id": "44199393",
+      "name": "区间--日",
+      "font_class": "qujian--ri",
+      "unicode": "eaca",
+      "unicode_decimal": 60106
+    },
+    {
+      "icon_id": "44199392",
+      "name": "级联选择器",
+      "font_class": "jilianxuanzeqi",
+      "unicode": "eacb",
+      "unicode_decimal": 60107
+    },
+    {
+      "icon_id": "44199400",
+      "name": "日期--日",
+      "font_class": "riqi--ri",
+      "unicode": "eac2",
+      "unicode_decimal": 60098
+    },
+    {
+      "icon_id": "44098600",
+      "name": "资源 13",
+      "font_class": "a-ziyuan13",
+      "unicode": "eac0",
+      "unicode_decimal": 60096
+    },
+    {
+      "icon_id": "44098599",
+      "name": "资源 12",
+      "font_class": "a-ziyuan12",
+      "unicode": "eac1",
+      "unicode_decimal": 60097
+    },
+    {
+      "icon_id": "44098434",
+      "name": "单边箭头 (2)",
+      "font_class": "a-danbianjiantou2",
+      "unicode": "eabe",
+      "unicode_decimal": 60094
+    },
+    {
+      "icon_id": "44098433",
+      "name": "单边箭头",
+      "font_class": "danbianjiantou",
+      "unicode": "eabf",
+      "unicode_decimal": 60095
+    },
     {
       "icon_id": "41183381",
       "name": "闪电选项卡",

+ 36 - 0
public/icon/v/iconfont.svg

@@ -14,6 +14,42 @@
     />
       <missing-glyph />
       
+      <glyph glyph-name="shuxingxuanzeqi" unicode="&#60111;" d="M744.727273 721.454545v-209.454545H69.818182V721.454545h674.909091z m-26.472728-26.181818H96.290909v-157.090909h621.963636v157.090909zM402.932364 523.636364l27.613091-0.372364L423.249455 23.272727 395.636364 23.645091zM871.435636 349.090909a80.360727 80.360727 0 0 0 69.422546-39.563636 78.242909 78.242909 0 0 0 0-79.127273 80.360727 80.360727 0 0 0-69.422546-39.563636C827.159273 190.836364 791.272727 226.269091 791.272727 269.963636s35.886545 79.127273 80.162909 79.127273z m2.583273-26.379636a53.573818 53.573818 0 0 1-46.289454-26.368 52.165818 52.165818 0 0 1 0-52.759273 53.573818 53.573818 0 0 1 46.289454-26.368c29.509818 0 53.434182 23.621818 53.434182 52.747636 0 29.125818-23.924364 52.747636-53.434182 52.747637zM673.605818 116.363636a80.360727 80.360727 0 0 0 69.434182-39.563636 78.242909 78.242909 0 0 0 0-79.127273 80.360727 80.360727 0 0 0-69.434182-39.563636C629.341091-41.890909 593.454545-6.458182 593.454545 37.236364s35.886545 79.127273 80.151273 79.127272z m0-31.034181a53.573818 53.573818 0 0 1-46.289454-26.368 52.165818 52.165818 0 0 1 0-52.759273 53.573818 53.573818 0 0 1 46.289454-26.368c29.509818 0 53.434182 23.621818 53.434182 52.747636 0 29.125818-23.924364 52.747636-53.434182 52.747637zM605.090909 46.545455v-23.272728H407.272727v23.272728zM802.909091 279.272727v-23.272727H418.909091v23.272727z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="riqi--zhou" unicode="&#60109;" d="M942.545455 523.636364a23.272727 23.272727 0 0 0 23.272727-23.272728v-232.727272a23.272727 23.272727 0 0 0-23.272727-23.272728H93.090909a23.272727 23.272727 0 0 0-23.272727 23.272728V500.363636a23.272727 23.272727 0 0 0 23.272727 23.272728h849.454546z m0-23.272728H93.090909v-232.727272h849.454546V500.363636zM826.181818 453.818182a23.272727 23.272727 0 0 0 23.272727-23.272727v-116.363637a23.272727 23.272727 0 0 0-23.272727-23.272727H686.545455a23.272727 23.272727 0 0 0-23.272728 23.272727V430.545455a23.272727 23.272727 0 0 0 23.272728 23.272727h139.636363z m0-23.272727H686.545455v-116.363637h139.636363V430.545455zM727.272727 473.216v-62.068364h-23.272727v62.068364zM808.727273 473.216v-62.068364h-23.272728v62.068364zM725.550545 325.818182h17.687273l10.146909 40.96h0.558546l9.960727-40.96h17.687273l14.801454 64.605091h-14.615272l-9.495273-47.755637h-0.558546l-9.309091 41.518546h-16.384l-9.867636-41.518546h-0.558545l-9.309091 47.755637h-14.801455z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="riqi--nian" unicode="&#60110;" d="M942.545455 523.636364a23.272727 23.272727 0 0 0 23.272727-23.272728v-232.727272a23.272727 23.272727 0 0 0-23.272727-23.272728H93.090909a23.272727 23.272727 0 0 0-23.272727 23.272728V500.363636a23.272727 23.272727 0 0 0 23.272727 23.272728h849.454546z m0-23.272728H93.090909v-232.727272h849.454546V500.363636zM826.181818 453.818182a23.272727 23.272727 0 0 0 23.272727-23.272727v-116.363637a23.272727 23.272727 0 0 0-23.272727-23.272727H686.545455a23.272727 23.272727 0 0 0-23.272728 23.272727V430.545455a23.272727 23.272727 0 0 0 23.272728 23.272727h139.636363z m0-23.272727H686.545455v-116.363637h139.636363V430.545455zM727.272727 473.216v-62.068364h-23.272727v62.068364zM808.727273 473.216v-62.068364h-23.272728v62.068364zM745.192727 325.818182h14.242909v25.879273l23.552 38.725818H767.069091l-14.336-25.786182h-0.558546l-13.870545 25.786182h-16.570182L745.192727 351.418182z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="qujian--zhou" unicode="&#60108;" d="M942.545455 523.636364a23.272727 23.272727 0 0 0 23.272727-23.272728v-232.727272a23.272727 23.272727 0 0 0-23.272727-23.272728H93.090909a23.272727 23.272727 0 0 0-23.272727 23.272728V500.363636a23.272727 23.272727 0 0 0 23.272727 23.272728h849.454546z m0-23.272728H93.090909v-232.727272h849.454546V500.363636zM849.454545 453.818182a34.909091 34.909091 0 0 0 34.909091-34.909091v-39.842909h-23.272727V418.909091a11.636364 11.636364 0 0 1-11.636364 11.636364H733.090909a11.636364 11.636364 0 0 1-11.636364-11.636364v-81.454546a11.636364 11.636364 0 0 1 11.636364-11.636363h74.973091v-23.272727H733.090909a34.909091 34.909091 0 0 0-34.909091 34.90909v81.454546a34.909091 34.909091 0 0 0 34.909091 34.909091h116.363636zM829.090909 368.465455l64.360727-30.021819-27.217454-15.709091 13.661091-23.668363-20.154182-11.636364-13.661091 23.656727-23.168-13.381818zM770.699636 349.090909h23.086546l7.726545 35.746909h0.558546L809.611636 349.090909h23.086546l14.429091 64.605091h-19.362909l-7.540364-42.356364h-0.558545l-6.516364 37.143273h-21.038546l-7.447272-37.143273h-0.558546l-7.168 42.356364h-19.456z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="shu-yibu" unicode="&#60099;" d="M733.090909 721.454545v-209.454545H58.181818V721.454545h674.909091z m-26.472727-26.181818H84.654545v-157.090909h621.963637v157.090909zM862.370909 349.090909a80.360727 80.360727 0 0 0 69.434182-39.563636 78.242909 78.242909 0 0 0 0-79.127273 80.360727 80.360727 0 0 0-69.422546-39.563636c-44.276364 0-80.174545 35.432727-80.174545 79.127272s35.898182 79.127273 80.174545 79.127273z m0-26.379636a53.573818 53.573818 0 0 1-46.277818-26.368 52.165818 52.165818 0 0 1 0-52.759273 53.573818 53.573818 0 0 1 46.289454-26.368c29.509818 0 53.434182 23.621818 53.434182 52.747636 0 29.125818-23.924364 52.747636-53.434182 52.747637zM661.981091 111.709091a80.360727 80.360727 0 0 0 69.434182-39.563636 78.242909 78.242909 0 0 0 0-79.127273 80.360727 80.360727 0 0 0-69.434182-39.563637C617.704727-46.545455 581.818182-11.112727 581.818182 32.581818s35.886545 79.127273 80.151273 79.127273z m0-26.379636a53.573818 53.573818 0 0 1-46.289455-26.368 52.165818 52.165818 0 0 1 0-52.759273 53.573818 53.573818 0 0 1 46.289455-26.368c29.509818 0 53.434182 23.621818 53.434182 52.747636 0 29.125818-23.924364 52.747636-53.434182 52.747637zM389.294545 523.636364v-81.454546h23.272728v81.454546h-23.272728z m0-209.454546v-81.454545h23.272728v81.454545h-23.272728z m0-209.454545v-81.454546h23.272728v81.454546h-23.272728zM802.909091 290.909091h-81.454546v-23.272727h81.454546v23.272727z m-174.545455 0h-81.454545v-23.272727h81.454545v23.272727z m-174.545454 0h-41.250909v-23.272727H453.818182v23.272727zM593.454545 46.545455h-69.818181v-23.272728h69.818181v23.272728z m-128 0h-58.181818v-23.272728h58.181818v23.272728z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="riqi--yue" unicode="&#60100;" d="M942.545455 523.636364a23.272727 23.272727 0 0 0 23.272727-23.272728v-232.727272a23.272727 23.272727 0 0 0-23.272727-23.272728H93.090909a23.272727 23.272727 0 0 0-23.272727 23.272728V500.363636a23.272727 23.272727 0 0 0 23.272727 23.272728h849.454546z m0-23.272728H93.090909v-232.727272h849.454546V500.363636zM826.181818 453.818182a23.272727 23.272727 0 0 0 23.272727-23.272727v-116.363637a23.272727 23.272727 0 0 0-23.272727-23.272727H686.545455a23.272727 23.272727 0 0 0-23.272728 23.272727V430.545455a23.272727 23.272727 0 0 0 23.272728 23.272727h139.636363z m0-23.272727H686.545455v-116.363637h139.636363V430.545455zM727.272727 473.216v-62.068364h-23.272727v62.068364zM808.727273 473.216v-62.068364h-23.272728v62.068364zM728.622545 325.818182H742.4v50.827636h0.558545l12.288-38.632727h14.149819l12.474181 38.632727h0.558546V325.818182h13.963636v64.605091h-22.341818l-11.170909-36.398546h-0.558545l-10.705455 36.398546h-22.993455z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="qujian--yue" unicode="&#60101;" d="M942.545455 523.636364a23.272727 23.272727 0 0 0 23.272727-23.272728v-232.727272a23.272727 23.272727 0 0 0-23.272727-23.272728H93.090909a23.272727 23.272727 0 0 0-23.272727 23.272728V500.363636a23.272727 23.272727 0 0 0 23.272727 23.272728h849.454546z m0-23.272728H93.090909v-232.727272h849.454546V500.363636zM849.454545 453.818182a34.909091 34.909091 0 0 0 34.909091-34.909091v-39.842909h-23.272727V418.909091a11.636364 11.636364 0 0 1-11.636364 11.636364H733.090909a11.636364 11.636364 0 0 1-11.636364-11.636364v-81.454546a11.636364 11.636364 0 0 1 11.636364-11.636363h74.973091v-23.272727H733.090909a34.909091 34.909091 0 0 0-34.909091 34.90909v81.454546a34.909091 34.909091 0 0 0 34.909091 34.909091h116.363636zM829.090909 368.465455l64.360727-30.021819-27.217454-15.709091 13.661091-23.668363-20.154182-11.636364-13.661091 23.656727-23.168-13.381818zM762.507636 349.090909h17.966546v46.824727h0.558545l8.936728-36.491636h18.059636l9.216 36.491636h0.558545V349.090909h18.152728v64.605091h-29.137455l-7.168-32.395636h-0.558545l-6.330182 32.395636h-30.254546z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="riqi--shijian" unicode="&#60102;" d="M942.545455 523.636364a23.272727 23.272727 0 0 0 23.272727-23.272728v-232.727272a23.272727 23.272727 0 0 0-23.272727-23.272728H93.090909a23.272727 23.272727 0 0 0-23.272727 23.272728V500.363636a23.272727 23.272727 0 0 0 23.272727 23.272728h849.454546z m0-23.272728H93.090909v-232.727272h849.454546V500.363636zM826.181818 453.818182a23.272727 23.272727 0 0 0 23.272727-23.272727v-116.363637a23.272727 23.272727 0 0 0-23.272727-23.272727H686.545455a23.272727 23.272727 0 0 0-23.272728 23.272727V430.545455a23.272727 23.272727 0 0 0 23.272728 23.272727h139.636363z m0-23.272727H686.545455v-116.363637h139.636363V430.545455zM727.272727 473.216v-62.068364h-23.272727v62.068364zM808.727273 473.216v-62.068364h-23.272728v62.068364zM804.852364 358.4v-23.272727h-62.068364v23.272727zM766.056727 395.636364v-60.509091h-23.272727V395.636364z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="jilian--yibu" unicode="&#60103;" d="M733.090909 721.454545v-209.454545H58.181818V721.454545h674.909091z m-26.472727-26.181818H84.654545v-157.090909h621.963637v157.090909zM414.568727 523.636364l27.613091-0.325819L434.885818 81.454545 407.272727 81.780364zM661.864727 108.218182V81.454545H407.272727v26.763637zM930.909091 174.545455v-151.272728H651.636364V174.545455h279.272727z m-23.272727-23.272728H674.909091v-104.727272h232.727273v104.727272z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="qujian--shijian" unicode="&#60104;" d="M942.545455 523.636364a23.272727 23.272727 0 0 0 23.272727-23.272728v-232.727272a23.272727 23.272727 0 0 0-23.272727-23.272728H93.090909a23.272727 23.272727 0 0 0-23.272727 23.272728V500.363636a23.272727 23.272727 0 0 0 23.272727 23.272728h849.454546z m0-23.272728H93.090909v-232.727272h849.454546V500.363636zM849.454545 453.818182a34.909091 34.909091 0 0 0 34.909091-34.909091v-39.842909h-23.272727V418.909091a11.636364 11.636364 0 0 1-11.636364 11.636364H733.090909a11.636364 11.636364 0 0 1-11.636364-11.636364v-81.454546a11.636364 11.636364 0 0 1 11.636364-11.636363h74.973091v-23.272727H733.090909a34.909091 34.909091 0 0 0-34.909091 34.90909v81.454546a34.909091 34.909091 0 0 0 34.909091 34.909091h116.363636zM818.420364 370.036364v-23.272728H756.363636v23.272728zM779.636364 407.272727v-60.509091h-23.272728V407.272727zM829.090909 368.465455l64.360727-30.021819-27.217454-15.709091 13.661091-23.668363-20.154182-11.636364-13.661091 23.656727-23.168-13.381818z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="qujian--nian" unicode="&#60105;" d="M942.545455 523.636364a23.272727 23.272727 0 0 0 23.272727-23.272728v-232.727272a23.272727 23.272727 0 0 0-23.272727-23.272728H93.090909a23.272727 23.272727 0 0 0-23.272727 23.272728V500.363636a23.272727 23.272727 0 0 0 23.272727 23.272728h849.454546z m0-23.272728H93.090909v-232.727272h849.454546V500.363636zM849.454545 453.818182a34.909091 34.909091 0 0 0 34.909091-34.909091v-39.842909h-23.272727V418.909091a11.636364 11.636364 0 0 1-11.636364 11.636364H733.090909a11.636364 11.636364 0 0 1-11.636364-11.636364v-81.454546a11.636364 11.636364 0 0 1 11.636364-11.636363h74.973091v-23.272727H733.090909a34.909091 34.909091 0 0 0-34.909091 34.90909v81.454546a34.909091 34.909091 0 0 0 34.909091 34.909091h116.363636zM829.090909 368.465455l64.360727-30.021819-27.217454-15.709091 13.661091-23.668363-20.154182-11.636364-13.661091 23.656727-23.168-13.381818zM780.288 349.090909h18.711273v24.482909l24.017454 40.122182H801.978182l-12.008727-23.086545h-0.558546l-11.077818 23.086545h-21.876364l23.831273-40.680727z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="qujian--ri" unicode="&#60106;" d="M942.545455 523.636364a23.272727 23.272727 0 0 0 23.272727-23.272728v-232.727272a23.272727 23.272727 0 0 0-23.272727-23.272728H93.090909a23.272727 23.272727 0 0 0-23.272727 23.272728V500.363636a23.272727 23.272727 0 0 0 23.272727 23.272728h849.454546z m0-23.272728H93.090909v-232.727272h849.454546V500.363636zM849.454545 453.818182a34.909091 34.909091 0 0 0 34.909091-34.909091v-39.842909h-23.272727V418.909091a11.636364 11.636364 0 0 1-11.636364 11.636364H733.090909a11.636364 11.636364 0 0 1-11.636364-11.636364v-81.454546a11.636364 11.636364 0 0 1 11.636364-11.636363h74.973091v-23.272727H733.090909a34.909091 34.909091 0 0 0-34.909091 34.90909v81.454546a34.909091 34.909091 0 0 0 34.909091 34.909091h116.363636zM829.090909 368.465455l64.360727-30.021819-27.217454-15.709091 13.661091-23.668363-20.154182-11.636364-13.661091 23.656727-23.168-13.381818 6.178909 70.760728z m39.179636-31.127273l-29.032727 13.556363-2.792727-31.930181 31.825454 18.385454zM762.507636 413.696V349.090909h24.762182c19.362909 0 31.744 7.354182 31.744 32.302546 0 26.903273-12.381091 32.302545-31.744 32.302545h-24.762182z m18.525091-48.779636v33.326545h5.12c10.426182 0 13.591273-3.351273 13.591273-16.849454 0-12.939636-2.792727-16.477091-13.591273-16.477091h-5.12z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="jilianxuanzeqi" unicode="&#60107;" d="M733.090909 721.454545v-209.454545H58.181818V721.454545h674.909091z m-26.472727-26.181818H84.654545v-157.090909h621.963637v157.090909zM930.909091 174.545455v-151.272728H651.636364V174.545455h279.272727z m-23.272727-23.272728H674.909091v-104.727272h232.727273v104.727272zM406.504727 523.636364v-58.181819h23.272728v58.181819h-23.272728z m0-186.181819v-58.181818h23.272728v58.181818h-23.272728z m0-186.181818v-58.181818h23.272728v58.181818h-23.272728zM651.636364 116.363636h-58.181819v-23.272727h58.181819v23.272727z m-186.181819 0h-47.313454v-23.272727H465.454545v23.272727z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="riqi--ri" unicode="&#60098;" d="M942.545455 523.636364a23.272727 23.272727 0 0 0 23.272727-23.272728v-232.727272a23.272727 23.272727 0 0 0-23.272727-23.272728H93.090909a23.272727 23.272727 0 0 0-23.272727 23.272728V500.363636a23.272727 23.272727 0 0 0 23.272727 23.272728h849.454546z m0-23.272728H93.090909v-232.727272h849.454546V500.363636zM826.181818 453.818182a23.272727 23.272727 0 0 0 23.272727-23.272727v-116.363637a23.272727 23.272727 0 0 0-23.272727-23.272727H686.545455a23.272727 23.272727 0 0 0-23.272728 23.272727V430.545455a23.272727 23.272727 0 0 0 23.272728 23.272727h139.636363z m0-23.272727H686.545455v-116.363637h139.636363V430.545455zM727.272727 473.216v-62.068364h-23.272727v62.068364zM808.727273 473.216v-62.068364h-23.272728v62.068364zM727.598545 390.423273V325.818182h24.762182c19.362909 0 31.744 7.354182 31.744 32.302545 0 26.903273-12.381091 32.302545-31.744 32.302546h-24.762182z m18.525091-48.779637v33.326546h5.12c10.426182 0 13.591273-3.351273 13.591273-16.849455 0-12.939636-2.792727-16.477091-13.591273-16.477091h-5.12z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="a-ziyuan13" unicode="&#60096;" d="M3324.164883 834h-19.79786l-701.843044-350.832342H311.858105C272.262386 483.167658 232.666667 443.571938 232.666667 384s39.59572-99.167658 79.191438-99.167658h2298.870393c2.140309 0 4.458977 0.535077 6.599286 0.891796L3304.367023-57.795481c19.79786-19.79786 59.393579 0 79.191439 19.79786V774.606421c0 19.79786-19.79786 59.393579-59.393579 59.393579z"  horiz-adv-x="3585" />
+      
+      <glyph glyph-name="a-ziyuan12" unicode="&#60097;" d="M3304.367023 483.167658H1005.496631c-2.140309 0-4.458977-0.535077-6.599286-0.891796L311.858105 825.795481C292.060245 845.593341 252.464527 825.795481 232.666667 805.997621v-812.604042c0-19.79786 19.79786-59.393579 59.393578-59.393579h19.79786l701.843044 350.832342h2290.665874c39.59572 0 79.191439 39.59572 79.191439 99.167658s-39.59572 99.167658-79.191439 99.167658z"  horiz-adv-x="3585" />
+      
+      <glyph glyph-name="a-danbianjiantou2" unicode="&#60094;" d="M2589.395165 483.167658H290.524772C250.929053 483.167658 211.333333 443.393579 211.333333 384s39.59572-99.167658 79.191439-99.167658h2298.870393c39.59572 0 79.191439 39.59572 79.191438 99.167658s-39.59572 99.167658-79.191438 99.167658zM2490.405866 338.518431L3283.03369-57.795481c19.79786-19.79786 59.393579 0 79.191439 19.79786V774.606421c0.178359 19.79786-19.619501 59.393579-59.393579 59.393579h-19.79786l-792.627824-396.313912c-19.79786-19.79786-19.79786-59.393579-19.79786-79.191438l19.79786-19.79786z"  horiz-adv-x="3585" />
+      
+      <glyph glyph-name="danbianjiantou" unicode="&#60095;" d="M999.807742 284.832342h2298.870393c39.59572 0 79.191439 39.59572 79.191438 99.167658s-39.59572 99.167658-79.191438 99.167658H999.807742c-39.59572 0-79.191439-39.59572-79.191439-99.167658s39.59572-99.167658 79.191439-99.167658zM1098.975399 429.481569L306.169216 825.795481C286.371356 845.593341 246.775638 825.795481 226.977778 805.997621v-812.604042c0-19.79786 19.79786-59.393579 59.393578-59.393579h19.79786l792.627824 396.313912c19.79786 19.79786 19.79786 59.393579 19.79786 79.191438l-19.79786 19.79786z"  horiz-adv-x="3585" />
+      
       <glyph glyph-name="shandianxuanxiangka" unicode="&#60093;" d="M913.699149 753.563217a32.135932 32.135932 0 0 0 32.135931-32.135933v-803.398296a32.135932 32.135932 0 0 0-32.135931-32.135933H110.300851a32.135932 32.135932 0 0 0-32.135931 32.135933V721.427284a32.135932 32.135932 0 0 0 32.135931 32.135933h803.398298z m0-32.135933H110.300851v-803.398296h803.398298V721.427284zM536.101948 890.140928v-32.135933h-449.903045v32.135933zM469.74125 280.184873h-72.723614a13.529227 13.529227 0 0 0-12.661558 18.31748l72.257644 189.280639a13.545296 13.545296 0 0 0 12.661556 8.692769h121.939794a13.51316 13.51316 0 0 0 12.549082-18.622772l-42.082004-103.043866h79.215073a13.51316 13.51316 0 0 0 10.187091-22.43088L456.886877 131.572255c-9.447964-10.733401-26.945979-1.446117-23.298551 12.372333l36.152924 136.224216z"  horiz-adv-x="1024" />
       
       <glyph glyph-name="dianchi" unicode="&#60092;" d="M780.32247801 835.91154164a42.36670718 42.36670718 0 0 0 42.36670717-42.36670718v-875.57861224a42.36670718 42.36670718 0 0 0-42.36670717-42.36670718H243.67752199a42.36670718 42.36670718 0 0 0-42.36670717 42.36670718V793.54483446a42.36670718 42.36670718 0 0 0 42.36670717 42.36670718h536.64495602z m0-28.24447104H243.67752199a14.12223613 14.12223613 0 0 1-14.12223612-14.12223614v-875.57861224a14.12223613 14.12223613 0 0 1 14.12223612-14.12223613h536.64495602a14.12223613 14.12223613 0 0 1 14.12223612 14.12223613V793.54483446a14.12223613 14.12223613 0 0 1-14.12223612 14.12223614zM413.14435072 892.40048496m14.12223491 0l169.46682874 0q14.12223613 0 14.12223491-14.12223614l0-56.48894209q0-14.12223613-14.12223491-14.12223613l-169.46682874 0q-14.12223613 0-14.12223491 14.12223613l0 56.48894209q0 14.12223613 14.12223491 14.12223614ZM596.73341437 892.40048496a14.12223613 14.12223613 0 0 0 14.12223491-14.12223614v-56.48894209a14.12223613 14.12223613 0 0 0-14.12223491-14.12223613H427.26658563a14.12223613 14.12223613 0 0 0-14.12223491 14.12223613V878.27824882a14.12223613 14.12223613 0 0 0 14.12223491 14.12223614h169.46682874z m-14.12223614-28.24447105H441.38882177v-28.24447227h141.22235646v28.24447227zM502.69344624 590.94724213l26.02728137-10.97297767-90.05749711-213.59881483 166.09161307-0.01412179-90.43879648-251.70060659-26.57804834 9.54663155 76.85320674 213.92362579-168.47827157-0.01412179z"  horiz-adv-x="1024" />

BIN
public/icon/v/iconfont.ttf


BIN
public/icon/v/iconfont.woff


BIN
public/icon/v/iconfont.woff2


+ 377 - 0
public/pen.d.ts

@@ -0,0 +1,377 @@
+declare interface Pen {
+    id?: string;
+    tags?: string[];
+    parentId?: string;
+    name?: string;
+    lineName?: string;
+    borderRadius?: number;
+    visible?: boolean;
+    close?: boolean;
+    length?: number;
+    title?: string;
+    titleFnJs?: string;
+    titleFn?: (pen: Pen) => string;
+    lineWidth?: number;
+    borderWidth?: number;
+    borderColor?: string;
+    globalAlpha?: number;
+    lineDash?: number[];
+    lineDashOffset?: number;
+    color?: string;
+    background?: string;
+    anchorColor?: string;
+    hoverAnchorColor?: string;
+    hoverColor?: string;
+    hoverBackground?: string;
+    activeColor?: string;
+    activeBackground?: string;
+    mouseDownValid?: boolean;
+    mouseDownColor?: string;
+    mouseDownBackground?: string;
+    /**
+     * @deprecated 改用 gradientColors
+     */
+    gradientFromColor?: string;
+    /**
+     * @deprecated 改用 gradientColors
+     */
+    gradientToColor?: string;
+    /**
+     * @deprecated 改用 gradientColors
+     */
+    gradientAngle?: number;
+    gradientRadius?: number;
+    /**
+     * @deprecated 改用 lineGradientColors
+     */
+    lineGradientFromColor?: string;
+    /**
+     * @deprecated 改用 lineGradientColors
+     */
+    lineGradientToColor?: string;
+    /**
+     * @deprecated 改用 lineGradientColors
+     */
+    lineGradientAngle?: number;
+    gradientColors?: string;
+    switch?: boolean;
+    checked?: boolean;
+    onBackground?: string;
+    onGradientColors?: string;
+    lineGradientColors?: string;
+    shadowColor?: string;
+    shadowBlur?: number;
+    shadowOffsetX?: number;
+    shadowOffsetY?: number;
+    textHasShadow?: boolean;
+    text?: string;
+    textWidth?: number;
+    textHeight?: number;
+    textLeft?: number;
+    textTop?: number;
+    textColor?: string;
+    hoverTextColor?: string;
+    activeTextColor?: string;
+    fontFamily?: string;
+    fontSize?: number;
+    lineHeight?: number;
+    fontStyle?: string;
+    fontWeight?: string;
+
+    textBackground?: string;
+    ellipsis?: boolean;
+    image?: string;
+    icon?: string;
+    iconRotate?: number;
+    iconWidth?: number;
+    iconHeight?: number;
+    iconTop?: number;
+    iconLeft?: number;
+    iconColor?: string;
+    iconFamily?: string;
+    iconWeight?: string;
+    iconSize?: number;
+    iconAlign?: 'top' | 'bottom' | 'left' | 'right' | 'left-top' | 'right-top' | 'left-bottom' | 'right-bottom' | 'center';
+    imageRatio?: boolean;
+    disableInput?: boolean;
+    disableRotate?: boolean;
+    disableSize?: boolean;
+    disableAnchor?: boolean;
+    paddingTop?: number;
+    paddingBottom?: number;
+    paddingLeft?: number;
+    paddingRight?: number;
+    backgroundImage?: string;
+    strokeImage?: string;
+    children?: string[];
+    followers?: string[];
+    anchorRadius?: number;
+    anchorBackground?: string;
+    pathId?: string;
+    path?: string;
+    fromArrow?: string;
+    toArrow?: string;
+    fromArrowSize?: number;
+    toArrowSize?: number;
+    fromArrowColor?: string;
+    toArrowColor?: string;
+    autoFrom?: boolean;
+    autoTo?: boolean;
+    animateCycle?: number;
+    nextAnimate?: string;
+    autoPlay?: boolean;
+    playLoop?: boolean;
+    duration?: number;
+    linear?: boolean;
+    scale?: number;
+    animateSpan?: number;
+    animateColor?: string;
+    animateLineDash?: number[];
+    animateReverse?: boolean;
+    keepAnimateState?: boolean;
+    animateName?: string;
+    frames?: Pen[];
+    animateList?: Pen[][];
+    animateInterval?: number;
+    animateShadow?: boolean;
+    animateShadowColor?: string;
+    animateShadowBlur?: number;
+    input?: boolean;
+    autofocus?: boolean;
+    dropdownBackground?: string;
+    dropdownColor?: string;
+    dropdownHoverColor?: string;
+    dropdownHoverBackground?: string;
+    iframe?: string;
+    video?: string;
+    audio?: string;
+    progress?: number;
+    progressColor?: string;
+    verticalProgress?: boolean;
+    reverseProgress?: boolean;
+    progressGradientColors?: string;
+    externElement?: boolean;
+    autoPolyline?: boolean;
+    affectByTheme?: boolean;
+    flipX?: boolean;
+    flipY?: boolean;
+    fillTexts?: string[];
+    hiddenText?: boolean;
+    keepDecimal?: number;
+    showChild?: number;
+    animateDotSize?: number;
+    isRuleLine?: boolean;
+    /**
+     * @deprecated 改用 canvasLayer
+     */
+    isBottom?: boolean;
+
+    ratio?: boolean;
+    animateLineWidth?: number;
+    lineSmooth?: number;
+    gradientSmooth?: number;
+    scrolling?: string;
+    animations?: any[];
+    currentAnimation?: number;
+
+    crossOrigin?: string;
+    imageRadius?: number;
+    textFlip?: boolean;
+    textRotate?: boolean;
+    textAutoAdjust?: boolean;
+    dbInput?: boolean;
+    operationalRect?: Rect;
+    blur?: number;
+    blurBackground?: string;
+    /**
+     * @deprecated 改用 canvasLayer
+     */
+    template?: boolean;
+    thumbImg?: string;
+    apiUrl?: string;
+    apiMethod?: string;
+    apiHeaders?: any;
+    apiBody?: any;
+    apiEnable?: boolean;
+    sql?: string;
+    container?: boolean;
+    disabled?: boolean;
+    disabledColor?: string;
+    disabledBackground?: string;
+    disabledTextColor?: string;
+    inputType?: string;
+    productId?: string;
+    deviceId?: string;
+    noOnBinds?: boolean;
+    interaction?: boolean;
+    childHover?: boolean;
+    childActive?: boolean;
+    draw?: boolean;
+    copyIndex?: number;
+    formId?: string;
+    formData?: any;
+    formKey?: string;
+    formValue?: string;
+    formType?: string;
+    className?: string;
+    styles?: any;
+    calculative?: {
+        x?: number;
+        y?: number;
+        width?: number;
+        height?: number;
+        borderRadius?: number;
+        progress?: number;
+        progressColor?: string;
+        progressGradientColors?: string;
+        verticalProgress?: boolean;
+        childrenVisible?: any;
+        rotate?: number;
+        lineWidth?: number;
+        borderWidth?: number;
+        borderColor?: string;
+        globalAlpha?: number;
+        lineDash?: number[];
+        lineDashOffset?: number;
+        color?: string;
+        background?: string;
+        bkType?: number;
+        /**
+         * @deprecated 改用 gradientColors
+         */
+        gradientFromColor?: string;
+        /**
+         * @deprecated 改用 gradientColors
+         */
+        gradientToColor?: string;
+        /**
+         * @deprecated 改用 gradientColors
+         */
+        gradientAngle?: number;
+        gradientRadius?: number;
+        /**
+         * @deprecated 改用 lineGradientColors
+         */
+        lineGradientFromColor?: string;
+        /**
+         * @deprecated 改用 lineGradientColors
+         */
+        lineGradientToColor?: string;
+        /**
+         * @deprecated 改用 lineGradientColors
+         */
+        lineGradientAngle?: number;
+        shadowColor?: string;
+        shadowBlur?: number;
+        shadowOffsetX?: number;
+        shadowOffsetY?: number;
+        textHasShadow?: boolean;
+        tempText?: string;
+        text?: string;
+        textWidth?: number;
+        textHeight?: number;
+        textLeft?: number;
+        textTop?: number;
+        textColor?: string;
+        textGradientColors?: string;
+        fontFamily?: string;
+        fontSize?: number;
+        lineHeight?: number;
+        fontStyle?: string;
+        fontWeight?: string;
+        textBackground?: string;
+        iconSize?: number;
+        icon?: string;
+        iconRotate?: number;
+        iconWidth?: number;
+        iconHeight?: number;
+        iconTop?: number;
+        iconLeft?: number;
+        iconColor?: string;
+        iconFamily?: string;
+        iconWeight?: string;
+        paddingTop?: number;
+        paddingBottom?: number;
+        paddingLeft?: number;
+        paddingRight?: number;
+        textLines?: string[];
+        textLineWidths?: number[];
+        image?: string;
+        imgNaturalWidth?: number;
+        imgNaturalHeight?: number;
+        backgroundImage?: string;
+        strokeImage?: string;
+
+        active?: boolean;
+        focus?: boolean;
+        hover?: boolean;
+        mouseDown?: boolean;
+        containerHover?: boolean;
+        isDock?: boolean;
+        pencil?: boolean;
+        activeAnchor?: Point;
+        patchFlags?: boolean;
+        visible?: boolean;
+        inView?: boolean;
+        drawlineH?: boolean;
+        hasImage?: boolean;
+        imageDrawed?: boolean;
+        /**
+         * @deprecated 改用 canvasLayer
+         */
+        isBottom?: boolean;
+        scale?: number;
+        start?: number;
+        duration?: number;
+        end?: number;
+        frameIndex?: number;
+        frameStart?: number;
+        frameEnd?: number;
+        frameDuration?: number;
+        animatePos?: number;
+        cycleIndex?: number;
+        pause?: number;
+        layer?: number;
+        iframe?: string;
+        video?: string;
+        audio?: string;
+        flipX?: boolean;
+        flipY?: boolean;
+        h?: boolean;
+        hiddenText?: boolean;
+        keepDecimal?: number;
+        showChild?: number;
+        animateDotSize?: number;
+        zIndex?: number;
+        onended?: (pen: Pen) => void;
+        singleton?: any;
+        gradientColors?: string;
+        onBackground?: string;
+        onGradientColors?: string;
+        checked?: boolean;
+        lineGradientColors?: string;
+
+        gradientTimer?: any;
+        animateLineWidth?: number;
+        lineSmooth?: number;
+        gradientSmooth?: number;
+        cssDisplay?: string;
+        animations?: any[];
+        imageRadius?: number;
+        disabled?: boolean;
+        disabledColor?: string;
+        disabledBackground?: string;
+        disabledTextColor?: string;
+    };
+    lastConnected?: any;
+    textDecoration?: string;
+    textDecorationDash?: number[];
+    textDecorationColor?: string;
+    textStrickoutColor?: string;
+    textStrickoutDash?: number[];
+    textStrickout?: boolean;
+    prevFrame?: Pen;
+    onAdd?: (pen: Pen) => void;
+    onValue?: (pen: Pen) => void;
+
+}

+ 1 - 0
public/view/meta2d-react/index.html

@@ -14,6 +14,7 @@
     <div id="app"></div>
     <script src="js/1.js"></script>
     <script src="js/r.js"></script>
+    <script src="js/2d-components.js"></script>
     <script src="js/lcjs.iife.js"></script>
     <script src="js/marked.min.js"></script>
     <script crossorigin="anonymous" integrity="sha512-ppWbHq6q2f7HAwS481w6qikuC0XEeBnmkRg6KWnWg3zSIbJwWWBgsCDMAxzSB7SVqXzWwSYQ2s8TSJKjnaikMg==" src="https://lib.baomitu.com/echarts/5.1.2/echarts.min.js"></script>

+ 26 - 1
public/view/meta2d-react/src/index.css

@@ -31,4 +31,29 @@ body {
   height: 100%;
   touch-action: none;
   overflow: hidden;
-}
+}
+
+/*定义滚动条轨道 内阴影+圆角*/
+::-webkit-scrollbar {
+  width: 3px;
+  height: 3px;
+  background: transparent;
+}
+
+::-webkit-scrollbar-corner {
+  background-color: transparent;
+}
+
+/*滚动条里面小方块*/
+::-webkit-scrollbar-thumb {
+  background-color: var(--color-scrollbar) !important;
+  border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+  background-color: var(--color-scrollbar-hover);
+}
+
+::-webkit-scrollbar-track {
+  background-color: transparent !important;
+}

+ 24 - 1
public/view/meta2d-react/src/views/2d/Meta2d.tsx

@@ -51,6 +51,21 @@ const registerBasicDiagram = () => {
   registerCanvasDraw(ftaPensbyCtx());
   registerAnchors(ftaAnchors());
 };
+
+//注册所有主题
+const registerTheme = () => {
+  fetch('/theme/dark.json')
+    .then((r) => r.json())
+    .then((theme) => {
+      (globalThis as any).echarts.registerTheme('le-dark', theme);
+    });
+  fetch('/theme/light.json')
+    .then((r) => r.json())
+    .then((theme) => {
+      (globalThis as any).echarts.registerTheme('le-light', theme);
+    });
+}
+
 const open = (id: string) => {
   let url = '/json/v.json';
   if (id) {
@@ -105,8 +120,16 @@ const initMeta2d = (id: any) => {
   meta2d = new Meta2d('meta2d', options);
   //注册基本图形库
   registerBasicDiagram();
+  const themetimer = setInterval(() => {
+    if ( (globalThis as any).echarts) {
+      registerTheme();
+      clearInterval(themetimer);
+    }
+  }, 200);
   meta2d.on('opened', opened);
-  open(id);
+  setTimeout(()=>{
+    open(id);
+  },1000);
   setTimeout(async () => {
     (globalThis as any).userId = meta2d.store.data.userId;
     (globalThis as any).registerIot &&

+ 1 - 0
public/view/meta2d-vue2/index.html

@@ -15,6 +15,7 @@
     <!-- built files will be auto injected -->
     <script src="js/1.js"></script>
     <script src="js/r.js"></script>
+    <script src="js/2d-components.js"></script>
     <script src="js/lcjs.iife.js"></script>
     <script src="js/marked.min.js"></script>
     <script crossorigin="anonymous" integrity="sha512-ppWbHq6q2f7HAwS481w6qikuC0XEeBnmkRg6KWnWg3zSIbJwWWBgsCDMAxzSB7SVqXzWwSYQ2s8TSJKjnaikMg==" src="https://lib.baomitu.com/echarts/5.1.2/echarts.min.js"></script>

+ 26 - 0
public/view/meta2d-vue2/src/App.vue

@@ -22,4 +22,30 @@ body {
   margin: 0;
   padding: 0;
 }
+
+/*定义滚动条轨道 内阴影+圆角*/
+::-webkit-scrollbar {
+  width: 3px;
+  height: 3px;
+  background: transparent;
+}
+
+::-webkit-scrollbar-corner {
+  background-color: transparent;
+}
+
+/*滚动条里面小方块*/
+::-webkit-scrollbar-thumb {
+  background-color: var(--color-scrollbar) !important;
+  border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+  background-color: var(--color-scrollbar-hover);
+}
+
+::-webkit-scrollbar-track {
+  background-color: transparent !important;
+}
+
 </style>

+ 23 - 2
public/view/meta2d-vue2/src/views/2d/Meta2d.vue

@@ -62,9 +62,16 @@ export default {
       meta2d = new Meta2d('meta2d', options);
       //注册基础图形库 参考文档:https://doc.le5le.com/document/119754049
       this.registerBasicDiagram();
+      const themetimer = setInterval(() => {
+        if (globalThis.echarts) {
+          this.registerTheme();
+          clearInterval(themetimer);
+        }
+      }, 200);
       meta2d.on('opened',this.opened);
-      
-      this.open();
+      setTimeout(() => {
+        this.open();
+      }, 1000);
 
       //注册企业图形库
       setTimeout(async()=>{
@@ -134,6 +141,20 @@ export default {
       registerCanvasDraw(ftaPensbyCtx());
       registerAnchors(ftaAnchors());
     },
+
+     //注册所有主题
+     registerTheme() {
+      fetch('/theme/dark.json')
+        .then((r) => r.json())
+        .then((theme) => {
+          globalThis.echarts.registerTheme('le-dark', theme);
+        });
+      fetch('/theme/light.json')
+        .then((r) => r.json())
+        .then((theme) => {
+          globalThis.echarts.registerTheme('le-light', theme);
+        });
+    }
   },
 };
 </script>

+ 1 - 0
public/view/meta2d-vue3/index.html

@@ -14,6 +14,7 @@
     <div id="app"></div>
     <script src="js/1.js"></script>
     <script src="js/r.js"></script>
+    <script src="js/2d-components.js"></script>
     <script src="js/lcjs.iife.js"></script>
     <script src="js/marked.min.js"></script>
     <script crossorigin="anonymous" integrity="sha512-ppWbHq6q2f7HAwS481w6qikuC0XEeBnmkRg6KWnWg3zSIbJwWWBgsCDMAxzSB7SVqXzWwSYQ2s8TSJKjnaikMg==" src="https://lib.baomitu.com/echarts/5.1.2/echarts.min.js"></script>

+ 26 - 0
public/view/meta2d-vue3/src/style.css

@@ -46,3 +46,29 @@ body {
     background-color: #f9f9f9;
   }
 }
+
+/*定义滚动条轨道 内阴影+圆角*/
+::-webkit-scrollbar {
+  width: 3px;
+  height: 3px;
+  background: transparent;
+}
+
+::-webkit-scrollbar-corner {
+  background-color: transparent;
+}
+
+/*滚动条里面小方块*/
+::-webkit-scrollbar-thumb {
+  background-color: var(--color-scrollbar) !important;
+  border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+  background-color: var(--color-scrollbar-hover);
+}
+
+::-webkit-scrollbar-track {
+  background-color: transparent !important;
+}
+

+ 22 - 1
public/view/meta2d-vue3/src/views/2d/Meta2d.vue

@@ -35,8 +35,16 @@ onMounted(async () => {
   meta2d = new Meta2d('meta2d',options);
   //注册基本图形库
   registerBasicDiagram();
+  const themetimer = setInterval(() => {
+    if ( (globalThis as any).echarts) {
+      registerTheme();
+      clearInterval(themetimer);
+    }
+  }, 200);
   meta2d.on('opened',opened);
-  open();
+  setTimeout(() => {
+    open();
+  }, 1000);
 
   //注册企业图形库
   setTimeout(async()=>{
@@ -121,6 +129,19 @@ function registerBasicDiagram() {
   registerAnchors(ftaAnchors());
 }
 
+//注册所有主题
+function registerTheme() {
+  fetch('/theme/dark.json')
+    .then((r) => r.json())
+    .then((theme) => {
+      (globalThis as any).echarts.registerTheme('le-dark', theme);
+    });
+  fetch('/theme/light.json')
+    .then((r) => r.json())
+    .then((theme) => {
+      (globalThis as any).echarts.registerTheme('le-light', theme);
+    });
+}
 
 onUnmounted(() => {
   meta2d?.off('opened',opened);

+ 320 - 2
src/i18n/lang/en.ts

@@ -259,7 +259,7 @@ const a = {
   "['方案', '模板'].includes(activedGroup)": "[' scheme ', 'template '].includes(activedGroup)",
   "subMenu in moveGroups[activedGroup==='方案'?'模板':'方案']": "subMenu in moveGroups[activedGroup===' Scheme '?' template ':' scheme ']",
   "图片": "picture",
-  "编辑": "EDITOR",
+  "编辑": "Edit",
   "检测到该图纸正在被编辑,": "Detects that the drawing is being edited,",
   "画布将被清空,": "The canvas will be emptied,",
   "{current?$t('检测到该图纸正在被编辑,'):''}}确定删除该数据吗?": "{current? $t(' Detected that the drawing is being edited, '):''}} Are you sure to delete this data?",
@@ -1381,5 +1381,323 @@ const a = {
   "此功能仅对dxf文件解析结果":"This function only parses the results of dxf files",
   "dxf缩放倍率":"dxf scaling",
   "导入visio(vsdx)文件":"Import visio (vsdx) files",
-  "导入CAD(dxf)文件":"Import CAD (dxf) file"
+  "导入CAD(dxf)文件":"Import CAD (dxf) file",
+  "数据列表": "Data list",
+  "批量导入数据图元到画布": "Batch import data primitives to the canvas",
+  "需配合乐吾乐物联网平台使用此功能": "This function needs to be used in conjunction with the Lewu Le Internet of Things platform",
+  "搜索我的数据列表": "Search my data list",
+  "请选择数据源": "Please select the data source.",
+  "轮询间隔": "Polling interval",
+  "不填,仅初始执行一次": "Leave it blank. It will only be executed initially once",
+  "查询方式": "Query method",
+  "单条": "Single entry",
+  "语句": "Statement",
+  "第几页": "Which page?",
+  "每页数量": "Quantity per page",
+  "默认20": "Default 20",
+  "关联属性名": "Associated attribute name",
+  "连接测试": "Connection test",
+  "搜索设备属性": "Search for device attributes",
+  "JSON值": "JSON value",
+  "简单模式": "Simple mode",
+  "可自定义输入": "Customizable input",
+  "JSON模式": "JSON mode",
+  "更多属性": "More attributes",
+  "搜索属性": "Search attribute",
+  "关联图元属性": "Associate graphic element attributes",
+  "搜索图元": "Search for graphic elements",
+  "新建页面": "Create a new page",
+  "工程": "project",
+  "新建工程":"New Project",
+  "导入工程":"Import project",
+  "导出工程": "Export the project",
+  "导出工程离线部署包": "Export the offline deployment package of the project",
+  "导出工程Vue3组件包":"Export project Vue3 component package",
+  "导出工程Vue2组件包":"Export project Vue2 component package",
+  "导出工程react组件包":"Export project react component package",
+  "从模版创建": "Create from the template",
+  "引用页面": "Reference page",
+  "复制页面": "Copy the page",
+  "粘贴页面": "Paste the page",
+  "仅删除与工程的关联关系,不删除图纸": "Only delete the association with the project, not the drawings",
+  "移除页面": "Remove the page",
+  "删除文件夹": "Delete the folder",
+  "设备属性": "Equipment attributes",
+  "自定义数据": "Custom data",
+  "下载进度": "Download progress",
+  "重新下载": "Download again",
+  "确认取消吗?": "Is the cancellation confirmed?",
+  "取消任务": "Cancel the task",
+  "总览": "Overview",
+  "我的项目": "My project",
+  "我的消息": "My Message",
+  "未读消息": "Unread message",
+  "全部消息": "All messages",
+  "账号管理": "Account Management",
+  "添加物联网平台": "Add an Internet of Things platform",
+  "添加sql数据源": "Add sql data source",
+  "编辑sql数据源": "Edit the sql data source",
+  "请填写sql语句": "Please fill in the sql statement",
+  "关联属性名必填": "The name of the associated attribute is required",
+  "连接错误:": "Connection error:",
+  "连接成功:[": "Connection successful:",
+  "连接成功:": "Connection successful:",
+  "编辑${networkDialog.network.protocol}数据源": "Edit ${networkDialog.net work. Protocol} the data source",
+  "添加${protocol}数据源": "Add the ${protocol} data source",
+  "具体配置参考": "For specific configuration reference",
+  "距离下一年还有:${day}天${hours}时${minutes}分${seconds}秒": "There are still: ${day} days, ${hours} hours, ${minutes} minutes, ${seconds} seconds until the next year",
+  "合并单元格": "Merge cells",
+  "表头": "Header",
+  "当前页背景颜色": "Background color of the current page",
+  "边框颜色": "Border color",
+  "当前页边框颜色": "The border color of the current page",
+  "当前页文字颜色": "The text color of the current page",
+  "状态选项卡": "Status TAB",
+  "字体": "Font",
+  "状态图标大小": "Status icon size",
+  "日期-日": "Date - Day",
+  "日期选择-日": "Date selection - Day",
+  "是否自动平铺": "Whether it is automatically tiled",
+  "类型选择": "Type selection",
+  "日": "day",
+  "周": "weeks",
+  "月": "month",
+  "年": "years",
+  "是否多选": "Whether to make multiple choices or not",
+  "自定义样式": "Custom style",
+  "日期-周": "Date - Week",
+  "日期选择-周": "Date selection - Week",
+  "日期-月": "Date - Month",
+  "日期选择-月": "Date selection - Month",
+  "日期-年": "Date - Year",
+  "日期选择-年": "Date selection - year",
+  "日期-时间": "Date - Time",
+  "带时间的日期选择": "Date selection with time",
+  "区间-日": "Interval - day",
+  "日期区间-日": "Date range - day",
+  "区间-周": "Interval - week",
+  "日期区间-周": "Date range - week",
+  "区间-月": "Interval - month",
+  "日期区间-月": "Date range - month",
+  "区间-年": "Interval - year",
+  "日期区间-年": "Date range - year",
+  "区间-时间": "Interval - time",
+  "日期区间-时间": "Date interval - time",
+  "树形选择器": "Tree selector",
+  "湖北省": "Hubei Province",
+  "黄冈市": "Huanggang City",
+  "武穴市": "Wuxue City",
+  "武汉市": "Wuhan City",
+  "武昌区": "Wuchang District",
+  "东湖风景区": "East Lake Scenic Area",
+  "听涛景区": "Tingtao Scenic Area",
+  "汉阳区": "Hanyang District",
+  "汉阳大道": "Hanyang Avenue",
+  "汉阳火车站": "Hanyang Railway Station",
+  "湖南省": "Hunan Province",
+  "长沙市": "Changsha City",
+  "橘子洲头": "Orange Isle Head",
+  "常德市": "Changde City",
+  "桃源县": "Taoyuan County",
+  "展开所有": "Expand all",
+  "只能选择叶子节点": "Only leaf nodes can be selected",
+  "父子是否不关联": "Whether father and son are not related",
+  "过滤搜索": "Filter search",
+  "手风琴模式": "Accordion mode",
+  "默认文本": "Default text",
+  "树形-异步": "Tree shape - Asynchronous",
+  "树形筛选器-动态加载": "Tree Filter - Dynamic loading",
+  "异步加载接口(只支持GET请求)": "Asynchronous loading interface (only supports GET requests)",
+  "接口header": "Interface header",
+  "级联选择器": "Cascade selector",
+  "选项1.1": "Option 1.1",
+  "选项1.1.1": "Option 1.1.1",
+  "选项1.1.1.1": "Option 1.1.1.1",
+  "选项1.1.1.2": "Option 1.1.1.2",
+  "选项1.1.2": "Option 1.1.2",
+  "选项1.1.2.1": "Option 1.1.2.1",
+  "选项1.1.2.2": "Option 1.1.2.2",
+  "选项1.2": "Option 1.2",
+  "选项1.2.1": "Option 1.2.1",
+  "选项1.2.1.1": "Option 1.2.1.1",
+  "选项1.2.1.2": "Option 1.2.1.2",
+  "选项1.2.2": "Option 1.2.2",
+  "选项1.2.2.1": "Option 1.2.2.1",
+  "选项1.2.2.2": "Option 1.2.2.2",
+  "选项2.1": "Option 2.1",
+  "选项2.2": "Option 2.2",
+  "选项3.1": "Option 3.1",
+  "选项3.1.1": "Option 3.1.1",
+  "选项3.2.1": "Option 3.2.1",
+  "选项3.2": "Option 3.2",
+  "级联-异步": "Cascading - Asynchronous",
+  "级联筛选器-动态加载": "Cascade Filter - Dynamic loading",
+  "唯一ID": "Unique ID",
+  "父图元id": "Parent graphic element id",
+  "X坐标": "X coordinate",
+  "Y坐标": "Y-coordinate",
+  "宽度": "Width",
+  "高度": "Height",
+  "连线类型名": "Connection type name",
+  "连线封闭": "Connection closure",
+  "锚点数组。": "Anchor point group.",
+  "锁定宽高比": "Lock the aspect ratio",
+  "旋转角度": "Rotation Angle",
+  "文字旋转": "Text rotation",
+  "连线长度": "Connection length",
+  "提示脚本": "Prompt script",
+  "线宽": "Line width",
+  "虚线模式": "Dotted line mode",
+  "虚线偏移": "Dotted line offset",
+  "背景色": "Background color",
+  "移入颜色": "Move into the color",
+  "移入背景": "Move into the background",
+  "锚点颜色": "Anchor point color",
+  "禁用文本颜色": "Disable text color",
+  "文本自动调整": "Automatic text adjustment",
+  "绘制类型": "Draw type",
+  "渐变背景色": "Gradient background color",
+  "连线渐变色": "Gradient color of the connection lines",
+  "线端点样式": "Line endpoint style",
+  "线连接样式": "Line connection style",
+  "阴影模糊度": "Shadow blur degree",
+  "阴影X偏移量": "Shadow X offset",
+  "阴影Y偏移量": "Shadow Y offset",
+  "文本宽度": "Text width",
+  "文本高度": "Text height",
+  "文本左偏移": "Text left offset",
+  "文本上偏移": "Offset on the text",
+  "文本颜色类型": "Text color type",
+  "文本渐变颜色": "Text gradient color",
+  "移入文本颜色": "Move into the text color",
+  "选中文本颜色": "Select the text color",
+  "文本大小": "Text size",
+  "字体样式": "Font style",
+  "字体粗细": "Font thickness",
+  "文字对齐": "Text alignment",
+  "文本基线": "Text baseline",
+  "文本背景": "Text background",
+  "文本换行方式": "Text line break mode",
+  "文本省略": "Text omission",
+  "image跨源": "image Cross-source",
+  "图片保持比例": "Keep the picture in proportion.",
+  "图片圆角": "Rounded corners of the picture",
+  "图标旋转": "Icon rotation",
+  "图片/图标宽度": "Picture/icon width",
+  "图片/图标高度": "Picture/icon height",
+  "图标上偏移": "Offset on the icon",
+  "图标左偏移": "Left offset of the icon",
+  "图标字体": "Icon font",
+  "图标颜色": "Icon color",
+  "字体对齐": "Font alignment",
+  "图标加粗": "Bold the icon",
+  "禁止输入": "Prohibited input",
+  "禁止改变大小": "Do not change the size.",
+  "顶部填充": "Top filling",
+  "线条填充图片": "Line filling picture",
+  "子画笔": "Sub-brush",
+  "锚点大小": "Anchor point size",
+  "锚点背景": "Anchor point background",
+  "svg路径": "svg path",
+  "起始箭头": "Starting arrow",
+  "起始箭头大小": "Starting arrow size",
+  "终点箭头大小": "The size of the end arrow",
+  "起始箭头颜色": "Starting arrow color",
+  "终点箭头颜色": "Endpoint arrow color",
+  "连线关系": "Connection relationship",
+  "动画播放次数": "The number of animation plays",
+  "下一个动画": "The next animation",
+  "循环播放": "Loop play",
+  "动画时长": "Duration of the animation",
+  "动画速度": "Animation speed",
+  "动画虚线样式": "Animation dotted line style",
+  "动画圆点大小": "The size of the animation dots",
+  "动画反向": "Animation reverse",
+  "动画保持初始状态": "The animation remains in its initial state",
+  "连线动画类型": "Connection animation type",
+  "动画帧": "Animation frame",
+  "单击输入": "Click input",
+  "外部元素": "External element",
+  "自动折线": "Automatic folding line",
+  "起点自动关联": "Automatic association of the starting point",
+  "终点自动关联": "Automatic association of the endpoint",
+  "文字镜像": "Text mirror image",
+  "层": "layer",
+  "连线边框": "Connection border",
+  "边框圆角": "Rounded corners of the border",
+  "隐藏文本": "Hidden text",
+  "小数点": "Decimal point",
+  "最后帧": "The last frame",
+  "元素层级": "Element hierarchy",
+  "类名": "Class name",
+  "跟随图元": "Follow the graphic element",
+  "旋转中心": "Rotation center",
+  "key名": "key name",
+  "页面": "Page",
+  "文件夹": "Folder",
+  "请先删除文件夹下的页面": "Please delete the pages under the folder first",
+  "请先保存当前画布": "Please save the current canvas first",
+  "该页面已被引用,是否拷贝一份?": "This page has been referenced. Do you want to make a copy?",
+  "sql数据源": "sql data source",
+  "请选择一项设备属性!": "Please select a device attribute!",
+  "属性名必填!": "Attribute name required!",
+  "发送到":"Send to",
+  "参数名":"Parameter name",
+  "参数值":"Parameter value",
+  "URL地址":"URL address",
+  "背景网格":"Background grid",
+  "网格颜色":"Grid color",
+  "开启网格":"Open grid",
+  "网格大小":"Grid size",
+  "数据源名称":"Datasource name",
+  "名称:":"Name:",
+  "地址:":"Address:",
+  "支持设置动态参数,例如:":"Supports setting dynamic parameters, for example:",
+  "参考文档:":"Reference document:",
+  "SQL数据源":"SQL data source",
+  "消息类型":"Message type",
+  "消息内容":"Message content",
+  "普通消息":"Normal message",
+  "成功":"Success",
+  "警告":"Warning",
+  "错误":"Error",
+  "问题":"Problem",
+  "二次确认":"Second confirmation",
+  "确认文本":"Confirmation text",
+  "免费示例下载体验":"Free sample download experience",
+  "快速入门":"Quick Start",
+  "组态教程":"Configuration Tutorial",
+  "部署试用":"Deployment Trial",
+  "企业版":"Enterprise Edition",
+  "解决方案":"Solution",
+  "技术交流群":"Technology Exchange Group",
+  "日期":"Date",
+  "常用图标":"Common icons",
+  "智慧楼宇":"Smart buildings",
+  "装饰":"Decoration",
+  "关闭自动生成":"Turn off auto-generation",
+  "跨域凭据":"Cross-domain credentials",
+  "确认":"Confirm",
+  "取消":"Cancel",
+  "边框宽度":"Border width",
+  "网格自动对齐":"Grid auto-alignment",
+  "时间曲线":"Time curve",
+  "需同时配置动画时长,并且运动速度配置将失效":"Animation duration must be configured at the same time, and the motion speed configuration will be invalid",
+  "自定义轨迹动画":"Custom trajectory animation",
+  "不可编辑列":"Non-editable columns",
+  "SQL数据库":"SQL database",
+  "更多":"More",
+  "设置":"Settings",
+  "连线动画反向":"Reverse line animation",
+  "图形类型":"Graphic type",
+  "元素宽度":"Element width",
+  "元素高度":"Element height",
+  "元素数量":"Number of elements",
+  "时间函数":"Time function",
+  "代码":"Code",
+  "请选择你的图形类型":"Please select your graphic type",
+  "单位像素,默认10px":"Unit pixel, default 10px",
+  "新增选项":"New option",
+  "请输入间隔,例如 10,20":"Please enter the interval, for example 10,20",
+  "编辑动画元素":"Edit animation element",
 };export default a

+ 316 - 1
src/i18n/lang/zh_CHT.ts

@@ -1382,5 +1382,320 @@ const a = {
   "此功能仅对dxf文件解析结果":"此功能僅對dxf檔案解析結果",
   "dxf缩放倍率":"dxf縮放倍率",
   "导入visio(vsdx)文件":"導入visio(vsdx)檔案",
-  "导入CAD(dxf)文件":"導入CAD(dxf)檔案"
+  "导入CAD(dxf)文件":"導入CAD(dxf)檔案",
+  "数据列表": "數據列表",
+  "批量导入数据图元到画布": "批量導入數據圖元到畫布",
+  "需配合乐吾乐物联网平台使用此功能": "需配合樂吾樂物聯網平臺使用此功能",
+  "搜索我的数据列表": "搜索我的數據列表",
+  "请选择数据源": "請選擇數據源",
+  "轮询间隔": "輪詢間隔",
+  "不填,仅初始执行一次": "不填,僅初始執行一次",
+  "查询方式": "查詢方式",
+  "单条": "單條",
+  "语句": "語句",
+  "第几页": "第幾頁",
+  "每页数量": "每頁數量",
+  "默认20": "默認20",
+  "关联属性名": "關聯屬性名",
+  "连接测试": "連接測試",
+  "搜索设备属性": "搜索設備屬性",
+  "JSON值": "JSON值",
+  "简单模式": "簡單模式",
+  "可自定义输入": "可自定義輸入",
+  "JSON模式": "JSON模式",
+  "更多属性": "更多屬性",
+  "搜索属性": "搜索屬性",
+  "关联图元属性": "關聯圖元屬性",
+  "搜索图元": "搜索圖元",
+  "新建页面": "新建頁面",
+  "新建工程":"新建工程",
+  "导入工程":"導入工程",
+  "导出工程": "導出工程",
+  "导出工程离线部署包": "導出工程離線部署包",
+  "导出工程Vue3组件包":"導出工程Vue3元件包",
+  "导出工程Vue2组件包":"導出工程Vue2元件包",
+  "导出工程react组件包":"導出工程react元件包",
+  "从模版创建": "從模版創建",
+  "引用页面": "引用頁面",
+  "复制页面": "複製頁面",
+  "粘贴页面": "粘貼頁面",
+  "仅删除与工程的关联关系,不删除图纸": "僅刪除與工程的關聯關係,不刪除圖紙",
+  "移除页面": "移除頁面",
+  "删除文件夹": "刪除文件夾",
+  "设备属性": "設備屬性",
+  "自定义数据": "自定義數據",
+  "下载进度": "下載進度",
+  "重新下载": "重新下載",
+  "确认取消吗?": "確認取消嗎?",
+  "取消任务": "取消任務",
+  "总览": "總覽",
+  "我的项目": "我的項目",
+  "我的消息": "我的消息",
+  "未读消息": "未讀消息",
+  "全部消息": "全部消息",
+  "账号管理": "賬號管理",
+  "添加物联网平台": "添加物聯網平臺",
+  "添加sql数据源": "添加sql數據源",
+  "编辑sql数据源": "編輯sql數據源",
+  "请填写sql语句": "請填寫sql語句",
+  "关联属性名必填": "關聯屬性名必填",
+  "连接错误:": "連接錯誤:",
+  "连接成功:[": "連接成功:[",
+  "连接成功:": "連接成功:",
+  "编辑${networkDialog.network.protocol}数据源": "編輯${networkDialog.network.protocol}數據源",
+  "添加${protocol}数据源": "添加${protocol}數據源",
+  "合并单元格": "合併單元格",
+  "表头": "表頭",
+  "当前页背景颜色": "當前頁背景顏色",
+  "边框颜色": "邊框顏色",
+  "当前页边框颜色": "當前頁邊框顏色",
+  "当前页文字颜色": "當前頁文字顏色",
+  "状态选项卡": "狀態選項卡",
+  "字体": "字體",
+  "状态图标大小": "狀態圖標大小",
+  "日期-日": "日期-日",
+  "日期选择-日": "日期選擇-日",
+  "是否自动平铺": "是否自動平鋪",
+  "类型选择": "類型選擇",
+  "日": "日",
+  "周": "周",
+  "月": "月",
+  "年": "年",
+  "是否多选": "是否多選",
+  "自定义样式": "自定義樣式",
+  "日期-周": "日期-周",
+  "日期选择-周": "日期選擇-周",
+  "日期-月": "日期-月",
+  "日期选择-月": "日期選擇-月",
+  "日期-年": "日期-年",
+  "日期选择-年": "日期選擇-年",
+  "日期-时间": "日期-時間",
+  "带时间的日期选择": "帶時間的日期選擇",
+  "区间-日": "區間-日",
+  "日期区间-日": "日期區間-日",
+  "区间-周": "區間-周",
+  "日期区间-周": "日期區間-周",
+  "区间-月": "區間-月",
+  "日期区间-月": "日期區間-月",
+  "区间-年": "區間-年",
+  "日期区间-年": "日期區間-年",
+  "区间-时间": "區間-時間",
+  "日期区间-时间": "日期區間-時間",
+  "树形选择器": "樹形選擇器",
+  "湖北省": "湖北省",
+  "黄冈市": "黃岡市",
+  "武穴市": "武穴市",
+  "武汉市": "武漢市",
+  "武昌区": "武昌區",
+  "东湖风景区": "東湖風景區",
+  "听涛景区": "聽濤景區",
+  "汉阳区": "漢陽區",
+  "汉阳大道": "漢陽大道",
+  "汉阳火车站": "漢陽火車站",
+  "湖南省": "湖南省",
+  "长沙市": "長沙市",
+  "橘子洲头": "橘子洲頭",
+  "常德市": "常德市",
+  "桃源县": "桃源縣",
+  "展开所有": "展開所有",
+  "只能选择叶子节点": "只能選擇葉子節點",
+  "父子是否不关联": "父子是否不關聯",
+  "过滤搜索": "過濾搜索",
+  "手风琴模式": "手風琴模式",
+  "默认文本": "默認文本",
+  "树形-异步": "樹形-異步",
+  "树形筛选器-动态加载": "樹形篩選器-動態加載",
+  "异步加载接口(只支持GET请求)": "異步加載接口(只支持GET請求)",
+  "接口header": "接口header",
+  "级联选择器": "級聯選擇器",
+  "选项1.1": "選項1.1",
+  "选项1.1.1": "選項1.1.1",
+  "选项1.1.1.1": "選項1.1.1.1",
+  "选项1.1.1.2": "選項1.1.1.2",
+  "选项1.1.2": "選項1.1.2",
+  "选项1.1.2.1": "選項1.1.2.1",
+  "选项1.1.2.2": "選項1.1.2.2",
+  "选项1.2": "選項1.2",
+  "选项1.2.1": "選項1.2.1",
+  "选项1.2.1.1": "選項1.2.1.1",
+  "选项1.2.1.2": "選項1.2.1.2",
+  "选项1.2.2": "選項1.2.2",
+  "选项1.2.2.1": "選項1.2.2.1",
+  "选项1.2.2.2": "選項1.2.2.2",
+  "选项2.1": "選項2.1",
+  "选项2.2": "選項2.2",
+  "选项3.1": "選項3.1",
+  "选项3.1.1": "選項3.1.1",
+  "选项3.2.1": "選項3.2.1",
+  "选项3.2": "選項3.2",
+  "级联-异步": "級聯-異步",
+  "级联筛选器-动态加载": "級聯篩選器-動態加載",
+  "唯一ID": "唯一ID",
+  "父图元id": "父圖元id",
+  "X坐标": "X座標",
+  "Y坐标": "Y座標",
+  "宽度": "寬度",
+  "高度": "高度",
+  "连线类型名": "連線類型名",
+  "连线封闭": "連線封閉",
+  "锚点数组。": "錨點數組。",
+  "锁定宽高比": "鎖定寬高比",
+  "旋转角度": "旋轉角度",
+  "文字旋转": "文字旋轉",
+  "连线长度": "連線長度",
+  "提示脚本": "提示腳本",
+  "线宽": "線寬",
+  "虚线模式": "虛線模式",
+  "虚线偏移": "虛線偏移",
+  "背景色": "背景色",
+  "移入颜色": "移入顏色",
+  "移入背景": "移入背景",
+  "锚点颜色": "錨點顏色",
+  "禁用文本颜色": "禁用文本顏色",
+  "文本自动调整": "文本自動調整",
+  "绘制类型": "繪製類型",
+  "渐变背景色": "漸變背景色",
+  "连线渐变色": "連線漸變色",
+  "线端点样式": "線端點樣式",
+  "线连接样式": "線連接樣式",
+  "阴影模糊度": "陰影模糊度",
+  "阴影X偏移量": "陰影X偏移量",
+  "阴影Y偏移量": "陰影Y偏移量",
+  "文本宽度": "文本寬度",
+  "文本高度": "文本高度",
+  "文本左偏移": "文本左偏移",
+  "文本上偏移": "文本上偏移",
+  "文本颜色类型": "文本顏色類型",
+  "文本渐变颜色": "文本漸變顏色",
+  "移入文本颜色": "移入文本顏色",
+  "选中文本颜色": "選中文本顏色",
+  "文本大小": "文本大小",
+  "字体样式": "字體樣式",
+  "字体粗细": "字體粗細",
+  "文字对齐": "文字對齊",
+  "文本基线": "文本基線",
+  "文本背景": "文本背景",
+  "文本换行方式": "文本換行方式",
+  "文本省略": "文本省略",
+  "image跨源": "image跨源",
+  "图片保持比例": "圖片保持比例",
+  "图片圆角": "圖片圓角",
+  "图标旋转": "圖標旋轉",
+  "图片/图标宽度": "圖片/圖標寬度",
+  "图片/图标高度": "圖片/圖標高度",
+  "图标上偏移": "圖標上偏移",
+  "图标左偏移": "圖標左偏移",
+  "图标字体": "圖標字體",
+  "图标颜色": "圖標顏色",
+  "字体对齐": "字體對齊",
+  "图标加粗": "圖標加粗",
+  "禁止输入": "禁止輸入",
+  "禁止改变大小": "禁止改變大小",
+  "顶部填充": "頂部填充",
+  "线条填充图片": "線條填充圖片",
+  "子画笔": "子畫筆",
+  "锚点大小": "錨點大小",
+  "锚点背景": "錨點背景",
+  "svg路径": "svg路徑",
+  "起始箭头": "起始箭頭",
+  "起始箭头大小": "起始箭頭大小",
+  "终点箭头大小": "終點箭頭大小",
+  "起始箭头颜色": "起始箭頭顏色",
+  "终点箭头颜色": "終點箭頭顏色",
+  "连线关系": "連線關係",
+  "动画播放次数": "動畫播放次數",
+  "下一个动画": "下一個動畫",
+  "循环播放": "循環播放",
+  "动画时长": "動畫時長",
+  "动画速度": "動畫速度",
+  "动画虚线样式": "動畫虛線樣式",
+  "动画圆点大小": "動畫圓點大小",
+  "动画反向": "動畫反向",
+  "动画保持初始状态": "動畫保持初始狀態",
+  "连线动画类型": "連線動畫類型",
+  "动画帧": "動畫幀",
+  "单击输入": "單擊輸入",
+  "外部元素": "外部元素",
+  "自动折线": "自動折線",
+  "起点自动关联": "起點自動關聯",
+  "终点自动关联": "終點自動關聯",
+  "文字镜像": "文字鏡像",
+  "层": "層",
+  "连线边框": "連線邊框",
+  "边框圆角": "邊框圓角",
+  "隐藏文本": "隱藏文本",
+  "小数点": "小數點",
+  "最后帧": "最後幀",
+  "元素层级": "元素層級",
+  "类名": "類名",
+  "跟随图元": "跟隨圖元",
+  "旋转中心": "旋轉中心",
+  "key名": "key名",
+  "页面": "頁面",
+  "文件夹": "文件夾",
+  "请先删除文件夹下的页面": "請先刪除文件夾下的頁面",
+  "请先保存当前画布": "請先保存當前畫布",
+  "该页面已被引用,是否拷贝一份?": "該頁面已被引用,是否拷貝一份?",
+  "sql数据源": "sql數據源",
+  "请选择一项设备属性!": "請選擇一項設備屬性!",
+  "属性名必填!": "屬性名必填!",
+  "发送到":"發送到",
+  "参数名":"參數名稱",
+  "参数值":"參數值",
+  "背景网格":"背景網格",
+  "网格颜色":"網格顏色",
+  "开启网格":"開啟網格",
+  "网格大小":"網格大小",
+  "数据源名称":"資料來源名稱",
+  "名称:":"名稱",
+  "地址:":"地址:",
+  "支持设置动态参数,例如:":"支援設定動態參數,例如:",
+  "参考文档:":"參考文件:",
+  "SQL数据源":"SQL資料來源",
+  "消息类型":"訊息類型",
+  "消息内容":"訊息內容",
+  "普通消息":"普通訊息",
+  "成功":"成功",
+  "警告":"警告",
+  "错误":"錯誤",
+  "问题":"問題",
+  "二次确认":"二次確認",
+  "确认文本":"確認文字",
+  "免费示例下载体验":"免費範例下載體驗",
+  "快速入门":"快速入門",
+  "组态教程":"組態教學",
+  "部署试用":"部署試用",
+  "企业版":"企業版",
+  "解决方案":"解決方案",
+  "技术交流群":"科技交流群",
+  "日期":"日期",
+  "常用图标":"常用圖標",
+  "智慧楼宇":"智慧樓宇",
+  "装饰":"裝飾",
+  "关闭自动生成":"關閉自動生成",
+  "跨域凭据":"跨域憑證",
+  "确认":"確認",
+  "取消":"取消",
+  "边框宽度":"邊框寬度",
+  "网格自动对齐":"網格自動對齊",
+  "时间曲线":"時間曲線",
+  "需同时配置动画时长,并且运动速度配置将失效":"需同時配置動畫時長,且運動速度配置將失效",
+  "自定义轨迹动画":"自訂軌跡動畫",
+  "不可编辑列":"不可編輯列",
+  "SQL数据库":"SQL資料庫",
+  "更多":"更多",
+  "设置":"設定",
+  "连线动画反向":"連線動畫反向",
+  "图形类型":"圖形類型",
+  "元素宽度":"元素寬度",
+  "元素高度":"元素高度",
+  "元素数量":"元素數量",
+  "时间函数":"時間函數",
+  "代码":"程式碼",
+  "请选择你的图形类型":"請選擇你的圖形類型",
+  "单位像素,默认10px":"單位像素,預設10px",
+  "新增选项":"新增選項",
+  "请输入间隔,例如 10,20":"請輸入間隔,例如 10,20",
+  "编辑动画元素":"編輯動畫元素",
+  "添加动画元素":"新增動畫元素"
 };export default a

+ 4 - 0
src/services/api.ts

@@ -47,6 +47,10 @@ export async function updateCollection(collection: string, data: any) {
   return await axios.post(`/api/data/${collection}/update`, data);
 }
 
+export async function getCollection(collection: string, id: string, projection?: any) {
+  return await axios.post(`/api/data/${collection}/get`, {id,projection});
+}
+
 export async function getCollectionList(
   collection: string,
   data: any,

+ 21 - 6
src/services/common.ts

@@ -15,10 +15,14 @@ import { deepClone, isDomShapes } from '@meta2d/core';
 import { useSelection } from '@/services/selections';
 import { rootDomain } from './defaults';
 import { updateObject } from './object';
+import { useProject } from './project';
+
 import i18n from '../i18n';
 import { HvacDevicePen, IframeMsg } from '@/types';
 import { getVisual2DMsgType, Visual2DMsgType } from '@/views';
 const $t = i18n.global.t;
+
+const {saveProject, clearProject } = useProject();
 const { select } = useSelection();
 
 const assets = reactive({
@@ -174,10 +178,10 @@ export const save = async (
   }, 2000);
   // 区分是模板还是方案
   meta2d.stopAnimate();
-  if( vType === 'v.component'){
-    // @ts-ignore
-   meta2d.store.data.component = true
-  }
+  // if( vType === 'v.component'){
+  //   // @ts-ignore
+  //  meta2d.store.data.component = true
+  // }
   const data: Meta2dBackData = meta2d.data();
   if(!data.pens.length){
     MessagePlugin.warning($t('画布为空,无法保存!'));
@@ -271,6 +275,7 @@ export const save = async (
       delete data.id;
       delete data._id;
       delete data.folder;
+      data.component = true;
     }else if(!vType && (data.isTemplate&&data.component)){
       //方案 -> 模版/组件
       delete data.id;
@@ -391,7 +396,7 @@ export const save = async (
       }
     }
   });
-  
+  let projectId = null;
   // data.childIds = childIds;
   const body:any = {
     data,
@@ -428,7 +433,13 @@ export const save = async (
     //   delete data.folder;
     //   ret = await updateCollection(collection, data);
     // } else
-    
+    if(collection === 'v'){
+      projectId = await saveProject();
+      if(projectId){
+        body.otherData = {projectId};
+      }
+    }
+
     if (data.id || data._id) {
       body.ownerId = data.ownerId;
       body.ownerName = data.ownerName;
@@ -440,6 +451,9 @@ export const save = async (
   if (ret.error) {
     return;
   }
+  if(ret.id){
+    await saveProject(ret.id);
+  }
   //  保存图纸之后的钩子函数
   globalThis.afterSaveMeta2d && (await globalThis.afterSaveMeta2d(ret));
   if (
@@ -663,6 +677,7 @@ export const newFile = () => {
       r: Date.now() + '',
     },
   });
+  clearProject();
   // setTimeout(() => {
   //   autoSave(true);
   // }, 300);

+ 150 - 35
src/services/defaults.ts

@@ -23,6 +23,7 @@ export const fromArrows = [
   { icon: '#l-from-diamondSolid', value: 'diamondSolid' },
   { icon: '#l-from-circleSolid', value: 'circleSolid' },
   { icon: '#l-from-line', value: 'line' },
+  { icon: '#l-danbianjiantou', value: 'reTriangleSolid' },
 ];
 
 export const toArrows = [
@@ -36,6 +37,7 @@ export const toArrows = [
   { icon: '#l-to-diamondSolid', value: 'diamondSolid' },
   { icon: '#l-to-circleSolid', value: 'circleSolid' },
   { icon: '#l-to-line', value: 'line' },
+  { icon: '#l-a-danbianjiantou2', value: 'reTriangleSolid' },
 ];
 
 export const lineDashObj = [
@@ -3946,6 +3948,119 @@ context.meta2d.translate(
           },
         },
       },
+      {
+        name: '状态选项卡',
+        icon: 'l-xuanxiangka',
+        data: {
+          name: 'tab',
+          width: 440,
+          height: 48,
+          direction: 'horizontal',
+          selectedKey: '1',
+          btnHeight: 32,
+          btnWidth: 100,
+          gap:8,
+          disableInput: true,
+          iconSize:16,
+          data: [
+            { text: '场景一', key: '0', isForbidden: true },
+            { text: '场景二', key: '1' },
+            { text: '场景三', key: '2' },
+            { text: '场景四', key: '3' },
+          ],
+          props: {
+            custom: [
+              {
+                key: 'data',
+                label: '数据',
+                type: 'code',
+              },
+              {
+                key: 'direction',
+                label: '方向',
+                type: 'select',
+                options: [
+                  { label: '水平', value: 'horizontal' },
+                  { label: '垂直', value: 'vertical' },
+                ],
+              },
+              {
+                key: 'gap',
+                label: '间隔',
+                type: 'number',
+                min:1
+              },
+              {
+                key: 'btnWidth',
+                label: '按钮宽度',
+                type: 'number',
+                min:1
+              },
+              {
+                key: 'btnHeight',
+                label: '按钮高度',
+                type: 'number',
+                min:1
+              },
+              {
+                key: 'activeBackground',
+                label: '选中背景',
+                type: 'color',
+              },
+              {
+                key: 'activeColor',
+                label: '选中边框',
+                type: 'color',
+              },
+              {
+                key: 'activeTextColor',
+                label: '选中文字',
+                type: 'color',
+              }, 
+              {
+                key: 'activeBgImage',
+                label: '选中背景图片',
+                type: 'string',
+              },
+              {
+                key: 'background',
+                label: '(未选中)背景',
+                type: 'color',
+              },
+              {
+                key: 'color',
+                label: '(未选中)边框',
+                type: 'color',
+              },
+              {
+                key: 'textColor',
+                label: '(未选中)文字',
+                type: 'color',
+              },
+              {
+                key: 'bgImage',
+                label: '(未选中)背景图片',
+                type: 'string',
+              },
+              {
+                key: 'iconFamily',
+                label: '字体',
+                type: 'string',
+              },
+              {
+                key:'statuses',
+                label: '状态',
+                type:'code',
+              },
+              {
+                key:'iconSize',
+                label: '状态图标大小',
+                type:'number',
+              }
+            ],
+          },
+        },
+      },
     ],
   },
   {
@@ -4414,7 +4529,7 @@ context.meta2d.translate(
             text:true
           } 
         },
-      },/*
+      },
        {
         name: '日期-日',
         icon: 'l-riqi--ri',
@@ -4428,7 +4543,7 @@ context.meta2d.translate(
           autoDropdown: true, // false,平铺,true,自动展开
           mode: 'date',
           pickerTimes: ['2024-11-11'],
-          styles: styleMap['datePickerDom'],
+          extraStyles: styleMap['datePickerDom'],
           props: {
             custom: [
               {
@@ -4454,7 +4569,7 @@ context.meta2d.translate(
                 type: "switch"
               },
               {
-                key: 'styles',
+                key: 'extraStyles',
                 label: '自定义样式',
                 type: 'code',
                 language: 'css',
@@ -4471,7 +4586,7 @@ context.meta2d.translate(
           height: 32,
           title: '日期选择-周',
           name: 'datePickerDom',
-          styles: styleMap['datePickerDom'],
+          extraStyles: styleMap['datePickerDom'],
           disableInput: true,
           multiple: true,
           autoDropdown: true, // false,平铺,true,自动展开
@@ -4502,7 +4617,7 @@ context.meta2d.translate(
                 type: "switch"
               },
               {
-                key: 'styles',
+                key: 'extraStyles',
                 label: '自定义样式',
                 type: 'code',
                 language: 'css',
@@ -4519,7 +4634,7 @@ context.meta2d.translate(
           height: 32,
           title: '日期选择-月',
           name: 'datePickerDom',
-          styles: styleMap['datePickerDom'],
+          extraStyles: styleMap['datePickerDom'],
           disableInput: true,
           multiple: false,
           autoDropdown: true, // false,平铺,true,自动展开
@@ -4550,7 +4665,7 @@ context.meta2d.translate(
                 type: "switch"
               },
               {
-                key: 'styles',
+                key: 'extraStyles',
                 label: '自定义样式',
                 type: 'code',
                 language: 'css',
@@ -4567,7 +4682,7 @@ context.meta2d.translate(
           height: 32,
           title: '日期选择-年',
           name: 'datePickerDom',
-          styles: styleMap['datePickerDom'],
+          extraStyles: styleMap['datePickerDom'],
           disableInput: true,
           multiple: true,
           autoDropdown: true, // false,平铺,true,自动展开
@@ -4598,7 +4713,7 @@ context.meta2d.translate(
                 type: "switch"
               },
               {
-                key: 'styles',
+                key: 'extraStyles',
                 label: '自定义样式',
                 type: 'code',
                 language: 'css',
@@ -4614,7 +4729,7 @@ context.meta2d.translate(
           width: 240,
           height: 32,
           name: 'datePickerDom',
-          styles: styleMap['datePickerDom'],
+          extraStyles: styleMap['datePickerDom'],
           title: '带时间的日期选择',
           disableInput: true,
           mode: 'time',
@@ -4646,7 +4761,7 @@ context.meta2d.translate(
                 type: "switch"
               },
               {
-                key: 'styles',
+                key: 'extraStyles',
                 label: '自定义样式',
                 type: 'code',
                 language: 'css',
@@ -4664,7 +4779,7 @@ context.meta2d.translate(
           title: '日期区间-日',
           name: 'dateRangePickerDom',
           mode: 'date',
-          styles: styleMap['dateRangePickerDom'],
+          extraStyles: styleMap['dateRangePickerDom'],
           // styles: ['.l-button {width: 24px;padding: 0;background-color: red !important;border-color: red !important;'],
           disableInput: true,
           autoDropdown: true, // false,平铺,true,自动展开
@@ -4689,7 +4804,7 @@ context.meta2d.translate(
                 ],
               },
               {
-                key: 'styles',
+                key: 'extraStyles',
                 label: '自定义样式',
                 type: 'code',
                 language: 'css',
@@ -4707,7 +4822,7 @@ context.meta2d.translate(
           title: '日期区间-周',
           mode: 'week',
           name: 'dateRangePickerDom',
-          styles: styleMap['dateRangePickerDom'],
+          extraStyles: styleMap['dateRangePickerDom'],
           disableInput: true,
           autoDropdown: true, // false,平铺,true,自动展开
           pickerTimes: ['2024-11', '2025-03'],
@@ -4731,7 +4846,7 @@ context.meta2d.translate(
                 ],
               },
               {
-                key: 'styles',
+                key: 'extraStyles',
                 label: '自定义样式',
                 type: 'code',
                 language: 'css',
@@ -4749,7 +4864,7 @@ context.meta2d.translate(
           title: '日期区间-月',
           mode: 'month',
           name: 'dateRangePickerDom',
-          styles: styleMap['dateRangePickerDom'],
+          extraStyles: styleMap['dateRangePickerDom'],
           disableInput: true,
           autoDropdown: true, // false,平铺,true,自动展开
           pickerTimes: ['2024-11', '2027-03'],
@@ -4773,7 +4888,7 @@ context.meta2d.translate(
                 ],
               },
               {
-                key: 'styles',
+                key: 'extraStyles',
                 label: '自定义样式',
                 type: 'code',
                 language: 'css',
@@ -4791,7 +4906,7 @@ context.meta2d.translate(
           title: '日期区间-年',
           mode: 'year',
           name: 'dateRangePickerDom',
-          styles: styleMap['dateRangePickerDom'],
+          extraStyles: styleMap['dateRangePickerDom'],
           disableInput: true,
           autoDropdown: true, // false,平铺,true,自动展开
           pickerTimes: ['2024', '2037'],
@@ -4815,7 +4930,7 @@ context.meta2d.translate(
                 ],
               },
               {
-                key: 'styles',
+                key: 'extraStyles',
                 label: '自定义样式',
                 type: 'code',
                 language: 'css',
@@ -4833,7 +4948,7 @@ context.meta2d.translate(
           title: '日期区间-时间',
           mode: 'time',
           name: 'dateRangePickerDom',
-          styles: styleMap['dateRangePickerDom'],
+          extraStyles: styleMap['dateRangePickerDom'],
           disableInput: true,
           autoDropdown: true, // false,平铺,true,自动展开
           pickerTimes: [],
@@ -4857,7 +4972,7 @@ context.meta2d.translate(
                 ],
               },
               {
-                key: 'styles',
+                key: 'extraStyles',
                 label: '自定义样式',
                 type: 'code',
                 language: 'css',
@@ -4868,16 +4983,16 @@ context.meta2d.translate(
       },
       {
         name: '树形选择器',
-        icon: 'l-shu-copy',
+        icon: 'l-shuxingxuanzeqi',
         data: {
           width: 300,
           height: 64,
           name: 'treeFilterDom',
-          styles: styleMap['treeFilterDom'],
+          extraStyles: styleMap['treeFilterDom'],
           defaultText: '请选择',
           checked: [],
           expanded: [],
-          autoDropdown: false, // false,平铺,true,自动展开
+          autoDropdown: true, // false,平铺,true,自动展开
           expandAll: false,
           multiple: false,
           onlyLeafCheck: false,
@@ -5016,7 +5131,7 @@ context.meta2d.translate(
                 language: 'json',
               },
               {
-                key: 'styles',
+                key: 'extraStyles',
                 label: '自定义样式',
                 type: 'code',
                 language: 'css',
@@ -5032,11 +5147,11 @@ context.meta2d.translate(
           width: 300,
           height: 64,
           name: 'treeFilterDom',
-          styles: styleMap['treeFilterDom'],
+          extraStyles: styleMap['treeFilterDom'],
           checked: [], //所有被选中的节点id
           expanded: [], //所有展开的节点id
           title: '树形筛选器-动态加载', //鼠标的title
-          autoDropdown: false, // false,平铺,true,自动展开
+          autoDropdown: true, // false,平铺,true,自动展开
           expandAll: false, // true,展开全部,false,不展开
           multiple: false, // true,多选,false,单选
           disableInput: true, // true,禁用自带输入框,false,启用输入框
@@ -5105,7 +5220,7 @@ context.meta2d.translate(
                 language: 'json',
               },
               {
-                key: 'styles',
+                key: 'extraStyles',
                 label: '自定义样式',
                 type: 'code',
                 language: 'css',
@@ -5132,11 +5247,11 @@ context.meta2d.translate(
           width: 300,
           height: 64,
           name: 'cascadeFilterDom',
-          styles: styleMap['cascadeFilterDom'],
+          extraStyles: styleMap['cascadeFilterDom'],
           defaultText: '请选择', //默认文本
           multiple: false, // true,多选,false,单选
           onlyLeafCheck: false, //只能选择叶子节点,true,只能选择叶子节点,false,可以选择任意节点
-          autoDropdown: false, // false,平铺,true,自动展开
+          autoDropdown: true, // false,平铺,true,自动展开
           disableInput: true, // true,禁用自带输入框,false,启用输入框
           flowPath: [], //流程路径,不对外暴露此属性
           checkStrictly: false, //true,父子不关联,false,父子关联
@@ -5295,7 +5410,7 @@ context.meta2d.translate(
                 language: 'json',
               },
               {
-                key: 'styles',
+                key: 'extraStyles',
                 label: '自定义样式',
                 type: 'code',
                 language: 'css',
@@ -5311,9 +5426,9 @@ context.meta2d.translate(
           width: 300,
           height: 64,
           name: 'cascadeFilterDom',
-          styles: styleMap['cascadeFilterDom'],
+          extraStyles: styleMap['cascadeFilterDom'],
           multiple: false,
-          autoDropdown: false, // false,平铺,true,自动展开
+          autoDropdown: true, // false,平铺,true,自动展开
           disableInput: true,
           flowPath: [],
           title: '级联筛选器-动态加载',
@@ -5372,7 +5487,7 @@ context.meta2d.translate(
                 language: 'json',
               },
               {
-                key: 'styles',
+                key: 'extraStyles',
                 label: '自定义样式',
                 type: 'code',
                 language: 'css',
@@ -5391,7 +5506,7 @@ context.meta2d.translate(
             ]
           }
         },
-      },*/
+      },
     ],
   },
   {

+ 92 - 29
src/services/download.ts

@@ -58,11 +58,12 @@ const components = [
   'cascadeFilterDom',
 ];
 
-export const getDownloadList = (meta2dData: any, path: string = 'v') => {
+export const getDownloadList = (meta2dData: any, path: string = 'v', flag_3d :boolean = true) => {
   const lists = new Set();
   //TODO 加一个type区分是数据/还是接口
   //背景图片
   // const meta2dData = meta2d.data();
+  if(meta2dData){
   let img = meta2dData.bkImage;
   if (img) {
     if (
@@ -70,7 +71,7 @@ export const getDownloadList = (meta2dData: any, path: string = 'v') => {
       img.startsWith(img_cdn) ||
       img.startsWith(img_upCdn)
     ) {
-      let _img = img.replace(img_cdn, '').replace(img_upCdn, '');
+      let _img = img.replace(img_cdn, '').replace(img_upCdn, '').split('?')[0];
       if (_img.startsWith('/v/')) {
         _img = _img.slice(2);
       }
@@ -82,9 +83,9 @@ export const getDownloadList = (meta2dData: any, path: string = 'v') => {
       meta2dData.bkImage = `projects/assets` + _img;
     }
   }
-
+  
   //图片图元(image strokeImage backgroundImage)
-  const imageKeys = ['image', 'strokeImage', 'backgroundImage'];
+  const imageKeys = ['image', 'strokeImage', 'backgroundImage','activeBgImage','bgImage'];
   const images: string[] = [];
   for (const pen of meta2dData.pens) {
     for (const i of imageKeys) {
@@ -96,7 +97,7 @@ export const getDownloadList = (meta2dData: any, path: string = 'v') => {
           image.startsWith(img_upCdn)
         ) {
           // 只考虑相对路径下的 image ,绝对路径图片无需下载
-          let _img = image.replace(img_cdn, '').replace(img_upCdn, '');
+          let _img = image.replace(img_cdn, '').replace(img_upCdn, '').split('?')[0];
           if (_img.startsWith('/v/')) {
             _img = _img.slice(2);
           }
@@ -125,7 +126,7 @@ export const getDownloadList = (meta2dData: any, path: string = 'v') => {
                 image.startsWith(img_upCdn)
               ) {
                 // 只考虑相对路径下的 image ,绝对路径图片无需下载
-                let _img = image.replace(img_cdn, '').replace(img_upCdn, '');
+                let _img = image.replace(img_cdn, '').replace(img_upCdn, '').split('?')[0];
                 if (_img.startsWith('/v/')) {
                   _img = _img.slice(2);
                 }
@@ -157,7 +158,7 @@ export const getDownloadList = (meta2dData: any, path: string = 'v') => {
                   image.startsWith(img_upCdn)
                 ) {
                   // 只考虑相对路径下的 image ,绝对路径图片无需下载
-                  let _img = image.replace(img_cdn, '').replace(img_upCdn, '');
+                  let _img = image.replace(img_cdn, '').replace(img_upCdn, '').split('?')[0];
                   if (_img.startsWith('/v/')) {
                     _img = _img.slice(2);
                   }
@@ -176,6 +177,37 @@ export const getDownloadList = (meta2dData: any, path: string = 'v') => {
         }
       });
     });
+
+    if(pen.name === 'echarts'){
+      if(pen.echarts?.geoUrl){
+        let geoUrl = pen.echarts.geoUrl.replace(img_cdn, '').replace(img_upCdn, '').split('?')[0];
+        if (geoUrl.startsWith('/v/')) {
+          geoUrl = geoUrl.slice(2);
+        }
+        lists.add({
+          url: pen.echarts.geoUrl,
+          path: `/view/projects/assets` + geoUrl
+        });
+        pen.echarts.geoUrl = `projects/assets` + geoUrl;
+      }
+      if(pen.echarts?.geoName){
+        pen.echarts.option.series?.forEach((item:any) => {
+          if(item.symbol&&item.symbol.startsWith('image://')){
+            let _img = item.symbol.replace('image://','').replace(img_cdn, '').replace(img_upCdn, '');
+            if (_img.startsWith('/v/')) {
+              _img = _img.slice(2);
+            }
+            _img = decodeURIComponent(_img);
+            lists.add({
+              url: item.symbol.replace('image://',''),
+              path: `/view/projects/assets` + _img,
+            });
+            item.symbol = `image://projects/assets` + _img;
+          }
+        });
+      }
+    }
+  }
   }
   if(path === 'v'){ //iframe嵌入的页面无需再次下载
     //其他文件
@@ -211,6 +243,7 @@ export const getDownloadList = (meta2dData: any, path: string = 'v') => {
 
       '/view/js/r.js',
       '/view/index.html',
+      '/view/config.js', //3D配置文件
       '/view/favicon.ico',
       '/view/view.conf',
       '/view/离线部署包使用说明.pdf',
@@ -236,12 +269,28 @@ export const getDownloadList = (meta2dData: any, path: string = 'v') => {
       '/view/theme/light.json',
     ];
     files.forEach((file) => {
-      lists.add({
-        url: (cdn ? cdn : '') + file,
-        path: file,
-      });
+      // lists.add({
+      //   url: (cdn ? cdn : '') + file,
+      //   path: file,
+      // });
+      let _file = file;
+      // if(file === '/view/assets/index.js' && !flag_3d){
+      //   _file = '/v/view/index.js';
+      // }
+      if(_file.startsWith('/view/css/v')){
+        lists.add({
+          url: (cdn ? cdn + '/v' : import.meta.env.BASE_URL.slice(0, -1)) + _file,
+          path: file,
+        });
+      }else{
+        lists.add({
+          url: (cdn ? cdn : '') + _file,
+          path: file,
+        });
+      }
     });
   }
+  if(meta2dData){
   //数据
   // const data: any = meta2d.data();
   if (meta2dData._id) delete meta2dData._id;
@@ -254,7 +303,7 @@ export const getDownloadList = (meta2dData: any, path: string = 'v') => {
       .replaceAll(img_upCdn, ''),
     path: `/view/projects/${path}`,
   });
-
+  }
   return lists;
 };
 
@@ -357,9 +406,11 @@ export const getComponentPurchased = async (list: any) => {
   }
   const res: any = await axios.post('/api/paid/2d/component?pageSize=1000', {
     list: _list,
+    // collection:'v',
+    // id:meta2d.store.data.id
   });
 
-  if (res.error) {
+  if (res?.error) {
     return [];
   }
 
@@ -374,7 +425,7 @@ export const getComponentPurchased = async (list: any) => {
   //   ([...list.iotPens].length - purchasedIot.length) * 70+
   //   ([...list.svgPens].length - purchasedSvg.length) * 10;
 
-  return res.list;
+  return res?.list||[];
 };
 
 export const get2dComponentJs = async (names: string[] = components) => {
@@ -389,6 +440,8 @@ export const get2dComponentJs = async (names: string[] = components) => {
     '/api/2d-component.js',
     {
       // list,
+      // collection:'v',
+      // id:meta2d.store.data.id
     },
     {
       responseType: 'blob',
@@ -400,6 +453,8 @@ export const get2dComponentJs = async (names: string[] = components) => {
 export const getTemPngs = async (names: string[]) => {
   const res: any = await axios.post('/api/file/presign', {
     names,
+    // collection:'v',
+    // id:meta2d.store.data.id
   });
   return res;
 };
@@ -657,6 +712,8 @@ export async function zipImages(zip: JSZip, pens: Pen[]) {
     },
     { string: 'strokeImage' },
     { string: 'backgroundImage' },
+    { string: 'activeBgImage' },
+    { string: 'bgImage' },
   ] as const;
   const images: string[] = [];
   for (const pen of pens) {
@@ -746,6 +803,7 @@ export async function zipImages(zip: JSZip, pens: Pen[]) {
 
 export const getFrameDownloadList =(meta2dData: any, path: string = 'v',type:Frame, flag_3d=false) => {
   const lists = new Set();
+  if(meta2dData){
   let img = meta2dData.bkImage;
   if (img) {
     if (
@@ -753,7 +811,7 @@ export const getFrameDownloadList =(meta2dData: any, path: string = 'v',type:Fra
       img.startsWith(img_cdn) ||
       img.startsWith(img_upCdn)
     ) {
-      let _img = img.replace(img_cdn, '').replace(img_upCdn, '');
+      let _img = img.replace(img_cdn, '').replace(img_upCdn, '').split('?')[0];
       if (_img.startsWith('/v/')) {
         _img = _img.slice(2);
       }
@@ -773,7 +831,7 @@ export const getFrameDownloadList =(meta2dData: any, path: string = 'v',type:Fra
   }
 
   //图片图元(image strokeImage backgroundImage)
-  const imageKeys = ['image', 'strokeImage', 'backgroundImage'];
+  const imageKeys = ['image', 'strokeImage', 'backgroundImage','activeBgImage','bgImage'];
   const images: string[] = [];
   for (const pen of meta2dData.pens) {
     for (const i of imageKeys) {
@@ -785,7 +843,7 @@ export const getFrameDownloadList =(meta2dData: any, path: string = 'v',type:Fra
           image.startsWith(img_upCdn)
         ) {
           // 只考虑相对路径下的 image ,绝对路径图片无需下载
-          let _img = image.replace(img_cdn, '').replace(img_upCdn, '');
+          let _img = image.replace(img_cdn, '').replace(img_upCdn, '').split('?')[0];
           if (_img.startsWith('/v/')) {
             _img = _img.slice(2);
           }
@@ -821,7 +879,7 @@ export const getFrameDownloadList =(meta2dData: any, path: string = 'v',type:Fra
                 image.startsWith(img_upCdn)
               ) {
                 // 只考虑相对路径下的 image ,绝对路径图片无需下载
-                let _img = image.replace(img_cdn, '').replace(img_upCdn, '');
+                let _img = image.replace(img_cdn, '').replace(img_upCdn, '').split('?')[0];
                 if (_img.startsWith('/v/')) {
                   _img = _img.slice(2);
                 }
@@ -861,7 +919,7 @@ export const getFrameDownloadList =(meta2dData: any, path: string = 'v',type:Fra
                   image.startsWith(img_upCdn)
                 ) {
                   // 只考虑相对路径下的 image ,绝对路径图片无需下载
-                  let _img = image.replace(img_cdn, '').replace(img_upCdn, '');
+                  let _img = image.replace(img_cdn, '').replace(img_upCdn, '').split('?')[0];
                   if (_img.startsWith('/v/')) {
                     _img = _img.slice(2);
                   }
@@ -889,7 +947,7 @@ export const getFrameDownloadList =(meta2dData: any, path: string = 'v',type:Fra
       });
     });
   }
-
+  }
   let folderName =type===Frame.vue3?'meta2d-vue3':type===Frame.vue2?'meta2d-vue2': 'meta2d-react';
   if(path === 'v'){ //iframe嵌入的页面无需再次下载
     //其他文件
@@ -913,6 +971,7 @@ export const getFrameDownloadList =(meta2dData: any, path: string = 'v',type:Fra
       '/view/css/fonts/斗鱼追光体.ttf',
 
       '/view/assets/index.js',
+      '/view/config.js', //3D配置文件
       '/view/assets/index.css',
       '/view/assets/cloudy.env',
       '/view/assets/default.env',
@@ -984,7 +1043,7 @@ export const getFrameDownloadList =(meta2dData: any, path: string = 'v',type:Fra
           //   ]);
           //   files.push( '/view/meta2d-vue2/src/views/3d/Meta3d.vue',)
           // }
-          // break;
+          break;
       case Frame.react:
         files =[
           '/view/meta2d-react/src/router/index.tsx',
@@ -1011,7 +1070,7 @@ export const getFrameDownloadList =(meta2dData: any, path: string = 'v',type:Fra
         //   ]);
         //   files.push('/view/meta2d-react/src/views/3d/Meta3d.tsx',)
         // }
-        // break;
+        break;
       default:
         break;
     }
@@ -1088,6 +1147,7 @@ export const getFrameDownloadList =(meta2dData: any, path: string = 'v',type:Fra
       });
     }); 
   }
+  if(meta2dData){
   //图纸数据
   if (meta2dData._id) delete meta2dData._id;
   if (meta2dData.id) delete meta2dData.id;
@@ -1098,7 +1158,7 @@ export const getFrameDownloadList =(meta2dData: any, path: string = 'v',type:Fra
       .replaceAll(img_upCdn, ''),
     path: `/${folderName}/public/json/${path}.json`,
   });
-
+  }
   return lists;
 };
 
@@ -1110,19 +1170,20 @@ export const getDownloadZipList = (meta2dData: any, zips:any) => {
   let _fileName =
     (meta2dData.name && meta2dData.name.replace(/\//g, '_').replace(/:/g, '_')) ||
     'unimat.iot';
+  getDataPngs(meta2dData).forEach(item => lists.add(item));
   lists.add({
     data:JSON.stringify(meta2dData).replaceAll(img_cdn, '').replaceAll(img_upCdn, ''),
     path: `${_fileName}.json`
   });
   for (let key in zips) {
+    // zips[key].pngs.forEach(item => pngs.add(item));
+    getDataPngs(zips[key].data).forEach(item => lists.add(item));
     lists.add({
       data:JSON.stringify(zips[key].data)?.replaceAll(img_cdn, '').replaceAll(img_upCdn, ''),
       path: `${key}/${zips[key].data.name?.replace(/\//g, '_')?.replace(/:/g, '_')}.json`
     });
-    // zips[key].pngs.forEach(item => pngs.add(item));
-    getDataPngs(zips[key].data).forEach(item => lists.add(item));
   }
-  getDataPngs(meta2dData).forEach(item => lists.add(item));
+ 
   return lists;
 }
 
@@ -1135,11 +1196,12 @@ export const getDataPngs = (meta2dData: any)=>{
       img.startsWith(img_cdn) ||
       img.startsWith(img_upCdn)
     ) {
-      let _img = img.replace(img_cdn, '').replace(img_upCdn, '');
+      let _img = img.replace(img_cdn, '').replace(img_upCdn, '').split('?')[0];
       _img = decodeURIComponent(_img);
       // if (_img.startsWith('/v/')) {
       //   _img = _img.slice(2);
       // }
+      meta2dData.bkImage = _img;
       lists.add({
         url: img,
         path: _img.split('?')[0],
@@ -1148,7 +1210,7 @@ export const getDataPngs = (meta2dData: any)=>{
   }
 
   //图片图元(image strokeImage backgroundImage)
-  const imageKeys = ['image', 'strokeImage', 'backgroundImage'];
+  const imageKeys = ['image', 'strokeImage', 'backgroundImage','activeBgImage','bgImage'];
   const images: string[] = [];
   for (const pen of meta2dData.pens) {
     for (const i of imageKeys) {
@@ -1160,7 +1222,8 @@ export const getDataPngs = (meta2dData: any)=>{
           image.startsWith(img_upCdn)
         ) {
           // 只考虑相对路径下的 image ,绝对路径图片无需下载
-          let _img = image.replace(img_cdn, '').replace(img_upCdn, '');
+          let _img = image.replace(img_cdn, '').replace(img_upCdn, '').split('?')[0];
+          pen[i] = _img;
           if (!images.includes(image)) {
             // let _img = image.replace(cdn, '').replace(upCdn, '');
             lists.add({

+ 414 - 0
src/services/handle3d.ts

@@ -0,0 +1,414 @@
+/** 3D数据加密解密 */
+import CryptoJS from "crypto-js";
+
+/** 部署时获取资源路径以及数据 */
+export const getResource = (data: any, path: string, type: 'deploy' | 'toVue2' | 'toVue3' | 'toReact'|'deployAsZip') => {
+  const _data = JSON.parse(
+    JSON.stringify(parseData(data)).replace(
+      /\?versionId=([^\"]+)/g,
+      ""
+    )
+  );
+  let resultData: any[] = [];
+  if (type === "deploy") {
+    const result = deploy(_data, "/view");
+    resultData = [
+      ...result.resources,
+      { path: `/view/projects/${path}`, data: result.data },
+    ];
+  } else if (
+    type === "toVue3" ||
+    type === "toVue2" ||
+    type === "toReact"
+  ) {
+    const storePath = {
+      toVue3: "/vue3/public/view/",
+      toVue2: "/vue2/public/view/",
+      toReact: "/react/public/view/",
+    }[type];
+    const result = deploy(_data, storePath);
+    resultData = [
+      ...result.resources,
+      {
+        path: joinUrl(`${storePath}projects/${path}`),
+        data: result.data,
+      },
+    ];
+  } else if (type === 'deployAsZip') {
+    const result = deployAsZip(_data);
+    resultData = [
+      ...result.resources,
+      { path: `${path}`, data: result.data },
+    ];
+  }
+  return resultData;
+};
+
+const parseData = (data: any) => {
+  if (typeof data === "string") {
+    data = data.startsWith("{") ? data : decrypto(data);
+    return JSON.parse(data);
+  }
+  return data;
+};
+
+/** 加密,data为未加密的json数据 */
+const crypto = (data: string) => {
+  return c(data);
+};
+
+/** 解密,data为加密后的json数据 */
+const decrypto = (data: string) => {
+  return d(data);
+};
+
+function s16() {
+  let chars = [
+    "0",
+    "1",
+    "2",
+    "3",
+    "4",
+    "5",
+    "6",
+    "7",
+    "8",
+    "9",
+    "A",
+    "B",
+    "C",
+    "D",
+    "E",
+    "F",
+    "G",
+    "H",
+    "I",
+    "J",
+    "K",
+    "L",
+    "M",
+    "N",
+    "O",
+    "P",
+    "Q",
+    "R",
+    "S",
+    "T",
+    "U",
+    "V",
+    "W",
+    "X",
+    "Y",
+    "Z",
+    "a",
+    "b",
+    "c",
+    "d",
+    "e",
+    "f",
+    "g",
+    "h",
+    "i",
+    "j",
+    "k",
+    "l",
+    "m",
+    "n",
+    "o",
+    "p",
+    "q",
+    "r",
+    "s",
+    "t",
+    "u",
+    "v",
+    "w",
+    "x",
+    "y",
+    "z",
+  ];
+  let strs = "";
+  for (let i = 0; i < 16; i++) {
+    let id = Math.ceil(Math.random() * 61);
+    strs += chars[id];
+  }
+  return strs;
+}
+
+const a = (word: string, k: string, i: string) => {
+  const srcs = CryptoJS.enc.Utf8.parse(word);
+  const encrypted = CryptoJS.AES.encrypt(srcs, CryptoJS.enc.Utf8.parse(k), {
+    iv: CryptoJS.enc.Utf8.parse(i),
+    mode: CryptoJS.mode.CBC,
+    padding: CryptoJS.pad.Pkcs7,
+  });
+  return encrypted.ciphertext.toString().toUpperCase();
+};
+
+const b = (word: string, k: string, i: string) => {
+  const encryptedHexStr = CryptoJS.enc.Hex.parse(word);
+  const srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr);
+  const decrypt = CryptoJS.AES.decrypt(srcs, CryptoJS.enc.Utf8.parse(k), {
+    iv: CryptoJS.enc.Utf8.parse(i),
+    mode: CryptoJS.mode.CBC,
+    padding: CryptoJS.pad.Pkcs7,
+  });
+  const decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
+  return decryptedStr.toString();
+};
+
+const c = (word: string): string => {
+  const k = s16().toUpperCase();
+  const i = s16().toUpperCase();
+  const d = a(word, k, i);
+  return k + d + i;
+};
+
+const d = (word: string): string => {
+  const k = word.substring(0, 16);
+  const i = word.substring(word.length - 16);
+  return b(word.substring(16, word.length - 16), k, i);
+};
+
+const deploy = (data: any, storePath = "") => {
+  const { scenes, resourceUrls } = data;
+  Object.keys(resourceUrls).forEach((key) => {
+    const url = resourceUrls[key];
+    if (
+      typeof url === "string" &&
+      ["http:", "https:", "/"].some((host) => url.startsWith(host))
+    ) {
+      const index = url.lastIndexOf("/assets/");
+      if (index !== -1) {
+        // 从/assets开始截取
+        resourceUrls[key] = url.substring(index + 1);
+      }
+    }
+  });
+  // 加载场景用到的所有资源
+  const urlCacheMap = new Map<string, string>();
+  // 使用相对路径
+  const filePath = "projects/assets/files/";
+  const transUrl = (url: string) => {
+    if (urlCacheMap.has(url)) {
+      return urlCacheMap.get(url)!;
+    }
+    if (
+      typeof url === "string" &&
+      ["http:", "https:", "/"].some((host) => url.startsWith(host))
+    ) {
+      const suffix = filesuffix(url, true);
+      const fullpath = joinUrl(filePath, createId() + suffix);
+      urlCacheMap.set(url, fullpath);
+      return fullpath;
+    }
+    return url;
+  };
+
+  for (const sceneData of scenes) {
+    if (!sceneData) {
+      continue;
+    }
+    const {
+      nodes = [],
+      scene = {},
+      glbMap = {},
+      textures = {},
+      materials = {},
+      DOMDatas = [],
+    } = sceneData;
+    Object.keys(glbMap).forEach((glbId) => {
+      const { url, name } = glbMap[glbId];
+      const newUrl = transUrl(url + name);
+      glbMap[glbId].url = filepath(newUrl);
+      glbMap[glbId].name = filename(newUrl, true);
+    });
+
+    for (const node of [
+      scene,
+      ...nodes,
+      ...DOMDatas,
+      ...Object.keys(materials).map((id) => materials[id]),
+      ...Object.keys(textures).map((id) => textures[id]),
+    ]) {
+      convertResourceAddress(node, transUrl);
+    }
+  }
+  const _data = JSON.stringify(data);
+  return {
+    resources: [...urlCacheMap].map(([url, path]) => ({
+      url,
+      path: joinUrl(storePath, path),
+    })),
+    data: crypto(_data),
+  };
+};
+
+const deployAsZip = (
+  data: any,
+  rootPath: string = ""
+) => {
+  delete data.resourceUrls;
+  const filePath = joinUrl(rootPath, "/files/");
+  const fileUrlMap: { [url: string]: string } = {};
+  // 加载场景用到的所有资源
+  const transUrl = (url: string) => {
+    if (!url || fileUrlMap[url]) {
+      return;
+    }
+    if (
+      typeof url === "string" &&
+      ["http:", "https:", "/", "projects/assets/"].some((host) =>
+        url.startsWith(host)
+      )
+    ) {
+      fileUrlMap[url] = createId();
+    }
+  };
+  const { scenes = [] } = data;
+  for (const sceneData of scenes) {
+    if (!sceneData) {
+      continue;
+    }
+    const {
+      nodes = [],
+      scene = {},
+      glbMap = {},
+      textures = {},
+      materials = {},
+      DOMDatas = [],
+    } = sceneData;
+    Object.keys(glbMap).forEach((glbId) => {
+      const { url, name } = glbMap[glbId];
+      transUrl(url + name);
+    });
+    for (const node of [
+      scene,
+      ...nodes,
+      ...DOMDatas,
+      ...Object.keys(materials).map((id) => materials[id]),
+      ...Object.keys(textures).map((id) => textures[id]),
+    ]) {
+      convertResourceAddress(node, transUrl, false);
+    }
+  }
+  return {
+    resources: Object.keys(fileUrlMap).map((url) => ({
+      url,
+      path: joinUrl(filePath, fileUrlMap[url]),
+    })),
+    data: JSON.stringify({
+      data: crypto(JSON.stringify(data)),
+      map: fileUrlMap,
+    }),
+  };
+};
+
+const filesuffix = (str: string, withDecimalPoint = false) => {
+  const paths = getUrlNoParam(str).split("/");
+  const name = paths[paths.length - 1];
+  const i = name.lastIndexOf(".");
+  if (i === -1) {
+    return "";
+  }
+  return name.substring(withDecimalPoint ? i : i + 1).toLocaleLowerCase();
+};
+
+const getUrlNoParam = (url: string) => {
+  return url.split("?")[0];
+};
+
+const joinUrl = (...urls: string[]) => {
+  if (urls.length < 2) {
+    return urls[0] || "";
+  }
+  return urls
+    .map((url, index) => {
+      if (index === 0) {
+        return url.replace(/\/$/, "");
+      }
+      if (index === urls.length - 1) {
+        return url.replace(/^\//, "");
+      }
+      return url.replace(/^\//, "").replace(/\/$/, "");
+    })
+    .join("/");
+};
+
+const createId = (prefix?: string) => {
+  return `${prefix || ""}${RandomGUID()}`;
+};
+
+const RandomGUID = () => {
+  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
+    const r = (Math.random() * 16) | 0,
+      v = c === "x" ? r : (r & 0x3) | 0x8;
+    return v.toString(16);
+  });
+};
+
+const filepath = (str: string, trimDelimiter = false) => {
+  const paths = str.split("/");
+  paths[paths.length - 1] = "";
+  if (trimDelimiter) {
+    if (paths[0] === "") {
+      paths.shift();
+    }
+    if (paths[paths.length - 1] === "") {
+      paths.pop();
+    }
+  }
+  return paths.join("/");
+};
+
+const filename = (str: string, withSuffix = false) => {
+  const paths = getUrlNoParam(str).split("/");
+  const name = paths[paths.length - 1];
+  if (withSuffix) {
+    return name;
+  }
+  const i = name.lastIndexOf(".");
+  if (i === -1) {
+    return name;
+  }
+  return name.substring(0, i);
+};
+
+const urlProps = [
+  "url",
+  "imageSource",
+  "skyboxUrl",
+  "hdrUrl",
+  "colorGradingTexture",
+  "source",
+  "backgroundImage",
+];
+const convertResourceAddress = (
+  data: any,
+  transFn: (url: string) => any,
+  reset = true
+) => {
+  for (const urlProp of urlProps) {
+    if (urlProp in data === false) {
+      continue;
+    }
+    const url = data[urlProp] || "";
+    const newUrl = transFn(url);
+    if (reset) {
+      data[urlProp] = newUrl;
+    }
+  }
+  if (data.contents) {
+    data.contents.forEach((content: any) =>
+      convertResourceAddress(content, transFn, reset)
+    );
+  }
+  if (data.__initOption) {
+    convertResourceAddress(data.__initOption, transFn, reset);
+  }
+  if (data.children) {
+    data.children.forEach((child: any) =>
+      convertResourceAddress(child, transFn, reset)
+    );
+  }
+};

+ 60 - 45
src/services/load3d.ts

@@ -47,7 +47,7 @@ export async function load3d(zip: JSZip, key: string) {
             tags = '我的HDR';
           }
           if (directory) {
-            const res = await uploadFile({
+            const res:any = await uploadFile({
               file: newFile,
               directory,
               type,
@@ -222,7 +222,7 @@ export const uploadFile = async (info: {
   )) as ResponseResult<FileData>;
   if (res.error) {
   }
-  return res.data;
+  return res;
 };
 
 export type UploadFileType =
@@ -295,65 +295,80 @@ const meta3dReplaceUrl = (
   const _data = parseData(data);
   const { scenes } = _data;
   const transUrl = (url: string): string => {
-    if (url.startsWith('data:')) {
-      const str = url.substring(5);
-      const [image, ...arr] = str.split('#').reverse();
-      url = arr.reverse().join('#');
-      const newUrl = transUrl(url);
-      return 'data:' + newUrl + '#' + image;
-    }
     if (url in urlMap) {
       return urlMap[url];
     }
     return url;
   };
-  const loadFile = (data: any, glbMap: any) => {
-    if (data.__glbUUID && glbMap) {
-      const glbInfo = glbMap[data.__glbUUID];
-      if (glbInfo) {
-        const { url, name } = glbInfo;
-        const fullUrl = url + name;
-        if (fullUrl in urlMap) {
-          glbInfo.url = filepath(urlMap[fullUrl]);
-          glbInfo.name = filename(urlMap[fullUrl], true);
-        }
-      }
-    }
-    const urlProps = [
-      'url',
-      'imageSource',
-      'skyboxUrl',
-      'hdrUrl',
-      'colorGradingTexture',
-    ];
-    for (const urlProp of urlProps) {
-      if (urlProp in data === false) {
-        continue;
-      }
-      const url = data[urlProp] || '';
-      data[urlProp] = transUrl(url);
-    }
-    if (data.images?.length) {
-      for (const image of data.images) {
-        const { source } = image;
-        image.source = transUrl(source);
-      }
-    }
-  };
-
   for (const sceneData of scenes) {
     if (!sceneData) {
       continue;
     }
-    const { nodes = [], scene = {}, glbMap = {}, textures = {} } = sceneData;
+    const {
+      nodes = [],
+      scene = {},
+      glbMap = {},
+      textures = {},
+      materials = {},
+      DOMDatas = [],
+    } = sceneData;
+    Object.keys(glbMap).forEach((glbId) => {
+      const { url, name } = glbMap[glbId];
+      const fullUrl = url + name;
+      if (fullUrl in urlMap) {
+        glbMap[glbId].url = filepath(urlMap[fullUrl]);
+        glbMap[glbId].name = filename(urlMap[fullUrl], true);
+      }
+    });
     for (const node of [
       scene,
       ...nodes,
+      ...DOMDatas,
+      ...Object.keys(materials).map((id) => materials[id]),
       ...Object.keys(textures).map((id) => textures[id]),
     ]) {
-      loadFile(node, glbMap);
+      convertResourceAddress(node, transUrl);
     }
   }
 
   return _data;
 };
+
+const urlProps = [
+  'url',
+  'imageSource',
+  'skyboxUrl',
+  'hdrUrl',
+  'colorGradingTexture',
+  'source',
+  'backgroundImage',
+];
+function convertResourceAddress(
+  data: any,
+  transFn: (url: string) => any,
+  reset = true
+) {
+  for (const urlProp of urlProps) {
+    if (urlProp in data === false) {
+      continue;
+    }
+    const url = data[urlProp] || '';
+    const newUrl = transFn(url);
+    if (reset) {
+      data[urlProp] = newUrl;
+    }
+  }
+  if (data.contents) {
+    data.contents.forEach((content: any) =>
+      convertResourceAddress(content, transFn, reset)
+    );
+  }
+  if (data.__initOption) {
+    convertResourceAddress(data.__initOption, transFn, reset);
+  }
+  if (data.children) {
+    data.children.forEach((child: any) =>
+      convertResourceAddress(child, transFn, reset)
+    );
+  }
+}

+ 1721 - 0
src/services/project.ts

@@ -0,0 +1,1721 @@
+import { reactive, ref } from 'vue';
+import { addCollection, updateCollection, getCollection, cdn } from './api';
+import {
+  getPayList,
+  getDownloadList,
+  Frame,
+  get2dComponentJs,
+  getTemPngs,
+  // getDeployPngs,
+  img_cdn,
+  img_upCdn,
+  getFrameDownloadList,
+  getComponentPurchased,
+  getGoods,
+  // getDeployAccess,
+} from './download';
+import { rootDomain } from './defaults';
+import { newFile, queryURLParams } from './common';
+import axios from 'axios';
+import { MessagePlugin } from 'tdesign-vue-next';
+import { useUser } from '@/services/user';
+import { getResource } from './handle3d';
+import { s8 } from './random';
+import { load3d } from './load3d';
+import router from '@/router';
+import { noLoginTip } from './utils';
+import localforage from 'localforage';
+
+export const activedGroup = ref('');
+export const activeAssets = ref('system');
+
+const { user } = useUser();
+
+const isVip = () => {
+  if (!(user && user.id)) {
+    MessagePlugin.warning(noLoginTip);
+    return;
+  }
+
+  if (!user.vip) {
+    MessagePlugin.info('需要开通会员~');
+    return;
+  }
+  return true;
+};
+
+const taskDialog = reactive({
+  visible: false,
+  tasks: [],
+  cancel: false,
+  reload: false,
+  downloadList: [],
+});
+
+export const useTask = () => {
+  const setTask = (key, value) => {
+    taskDialog[key] = value;
+  };
+
+  return {
+    taskDialog,
+    setTask,
+  };
+};
+
+export const reDownload = async () => {
+  taskDialog.tasks[1].status = 'process';
+  taskDialog.reload = false;
+  let result = await saveZip(taskDialog.downloadList);
+  if (result === 'error') {
+    taskDialog.tasks[1].status = 'error';
+    taskDialog.reload = true;
+    return;
+  }
+  taskDialog.tasks[1].status = 'success';
+
+  MessagePlugin.closeAll();
+  taskDialog.tasks[2].status = 'success';
+  MessagePlugin.success('下载成功,请在浏览器下载列表中查看');
+  taskDialog.visible = false;
+};
+
+const payDialog = reactive({
+  visible: false,
+  data: {
+    price: 0,
+    list: [],
+    hasJs: false,
+    checked: true,
+  },
+  token: false,
+});
+
+export const usePay = () => {
+  const setPay = (key, value) => {
+    payDialog[key] = value;
+  };
+
+  return {
+    payDialog,
+    setPay,
+  };
+};
+
+const prePay = async () => {
+  if (user.vipData?.deployment) {
+    //可视化会员
+    if ((user.vipData.deployment & 128) === 128) {
+      //直接下载
+      payDialog.token = true;
+      doDownloadProjectSource();
+      return;
+    }
+  }
+  //TODO 付费
+  let purchased = await getComponentPurchased({
+    pngs: [...prePayList.pngs],
+    jsPens: [],
+    iotPens: [],
+    svgPens: [],
+  });
+  if (
+    [...prePayList.jsPens].length ||
+    [...prePayList.iotPens].length ||
+    [...prePayList.svgPens].length
+  ) {
+    payDialog.data.hasJs = true;
+  } else {
+    payDialog.data.hasJs = false;
+  }
+  let names = purchased.map((item) => item.name);
+  let goods = await getGoods();
+  let list = [...prePayList.pngs].filter((item) => !names.includes(item));
+  if (!list.length) {
+    //直接下载
+    doDownloadProjectSource();
+  } else {
+    let unitPrice = goods.find((item) => item.type === '图片图元').unitPrice;
+    payDialog.data.price = unitPrice * list.length;
+    payDialog.data.checked = true;
+    payDialog.visible = true;
+    payDialog.data.list = list;
+  }
+};
+
+export const payProjectDialog = reactive({
+  visible: false,
+  data: {
+    price: 0,
+    payAll: false,
+    list: [],
+    title: '',
+    checked: true,
+    payList: [],
+  },
+  token: '',
+});
+
+/*
+const prePayProject = async () => {
+  if (user.vipData?.deployment) {
+    //可视化会员
+    if ((user.vipData.deployment & 128) === 128) {
+      //直接下载
+      payDialog.token = true;
+      doDownloadProject();
+      return;
+    }
+  }
+
+  const result: {
+    token?: string;
+    vip?: boolean;
+    error?: string;
+    deployment?: any;
+  } = await getDeployAccess(
+    Object.keys(page3dDatas).length ? '3d,v' : 'v',
+    prePayList
+  );
+  if (result.vip) {
+    if (result.token) {
+      payProjectDialog.token = result.token;
+      doDownloadProject();
+      return;
+    }
+  }
+
+  //图形库
+  let _goods = await getGoods();
+  let purchasedList = await getComponentPurchased(prePayList);
+  payProjectDialog.data.list = [];
+  // return;
+  payProjectDialog.data.payAll = false;
+  // _goods.forEach((goods)=>{
+
+  for (let i = 0; i < _goods.length; i++) {
+    let goods = _goods[i];
+    let purchased = purchasedList?.filter((_item) => _item.type === goods.type);
+    let names = purchased.map((item) => item.name);
+    let list = [];
+    if (goods.type === '图片图元') {
+      list = [...prePayList.pngs];
+    } else if (goods.type === 'JS线性图元') {
+      list = [...prePayList.jsPens];
+    } else if (goods.type === 'SVG线性图元') {
+      list = [...prePayList.svgPens];
+    } else if (goods.type === '控件') {
+      list = [...prePayList.iotPens];
+    }
+    let num = 0;
+    if (goods.type === 'SVG线性图元' && [...prePayList.svgPens].includes('*')) {
+      //需要购买全部
+      num = goods.count - names.length;
+      payProjectDialog.data.payAll = true;
+    } else {
+      // list.forEach((item)=>{
+      for (let j = 0; j < list.length; j++) {
+        let item = list[j];
+        if (!names.includes(item)) {
+          if (goods.type === '控件') {
+            payProjectDialog.data.list.push({
+              name: item,
+            });
+          } else if (goods.type === 'JS线性图元') {
+            payProjectDialog.data.list.push({
+              svg: globalThis.jsPensMap ? globalThis.jsPensMap[item] : item,
+            });
+          } else {
+            payProjectDialog.data.list.push({
+              img: item,
+            });
+          }
+          num += 1;
+        }
+      }
+    }
+    goods.num = num;
+  }
+  let price = 0;
+  for (let i = 0; i < _goods.length; i++) {
+    price += _goods[i].num * _goods[i].unitPrice;
+  }
+  payProjectDialog.data.price = price;
+
+  let payList = [];
+  let names = purchasedList.map((item) => item.name);
+  prePayList.pngs.forEach((item) => {
+    if (!names.includes(item)) {
+      payList.push({
+        type: '图片图元',
+        name: item,
+      });
+    }
+  });
+  prePayList.jsPens.forEach((item) => {
+    if (!names.includes(item)) {
+      payList.push({
+        type: 'JS线性图元',
+        name: item,
+      });
+    }
+  });
+
+  prePayList.iotPens.forEach((item) => {
+    if (!names.includes(item)) {
+      payList.push({
+        type: '控件',
+        name: item,
+      });
+    }
+  });
+  if ([...prePayList.svgPens].includes('*')) {
+    payList.push({
+      type: 'SVG线性图元',
+    });
+  } else {
+    prePayList.svgPens.forEach((item) => {
+      if (!names.includes(item)) {
+        payList.push({
+          type: 'SVG线性图元',
+          name: item,
+        });
+      }
+    });
+  }
+  payProjectDialog.data.checked = true;
+  payProjectDialog.data.payList = payList;
+
+  switch (downloadType) {
+    case Frame.html:
+      payProjectDialog.data.title = '下载离线部署包';
+      break;
+    case Frame.vue2:
+      payProjectDialog.data.title = '下载vue2组件包';
+      break;
+    case Frame.vue3:
+      payProjectDialog.data.title = '下载vue3组件包';
+      break;
+    case Frame.react:
+      payProjectDialog.data.title = '下载react组件包';
+      break;
+  }
+  if (payProjectDialog.data.price === 0) {
+    doDownloadProject();
+  } else {
+    payProjectDialog.visible = true;
+  }
+};*/
+
+const data = reactive({
+  id: undefined,
+  tree: [
+    // {
+    //   value: '1',
+    //   label: '文件夹1',
+    //   children: [
+    //     {
+    //       value: '11',
+    //       label: '页面1',
+    //       type: 'page',
+    //       pageId: '1',
+    //       pageType: '', //v/2d/3d
+    //       tag: '首页',
+    //     },
+    //   ],
+    // },
+    // {
+    //   value: '2',
+    //   label: '文件夹2',
+    //   children: [
+    //     {
+    //       value: '21',
+    //       label: '页面1',
+    //       type: 'page',
+    //     },
+    //     {
+    //       value: '22',
+    //       label: '页面1',
+    //       type: 'page',
+    //     },
+    //   ],
+    // },
+  ],
+  actived: [],
+});
+
+export const graphicsRef = ref(null);
+const tree = ref();
+const activeNode = ref(null);
+let activePage = null;
+
+//新建工程
+export const newProject = () => {
+  //新建页面
+  newFile();
+  //新建工程
+  data.tree = [
+    {
+      value: s8(),
+      label: '文件夹',
+      children: [
+        {
+          value: s8(),
+          label: '页面',
+          type: 'page',
+          pageType: '', //v/2d/3d
+        },
+      ],
+    },
+  ];
+  data.id = undefined;
+
+  //展示工程
+  activeAssets.value = 'structure';
+  graphicsRef.value.assetsChange(activeAssets.value);
+  activedGroup.value = '工程';
+};
+
+const getHomePage = (treeData) => {
+  for (let i = 0; i < treeData.length; i++) {
+    if (treeData[i].type === 'page' && treeData[i].tag === '首页') {
+      return treeData[i];
+    }
+    if (treeData[i].children) {
+      const page = getHomePage(treeData[i].children);
+      if (page) {
+        return page;
+      }
+    }
+  }
+};
+
+export const loadProject = () => {
+  if (!isVip()) {
+    return;
+  }
+  const input = document.createElement('input');
+  input.type = 'file';
+  input.onchange = async (event) => {
+    const elem = event.target as HTMLInputElement;
+    if (elem.files && elem.files[0]) {
+      // 路由跳转 可能在 openFile 后执行
+      if (elem.files[0].name.endsWith('.zip')) {
+        let projectData = await uploadProjectSource(elem.files[0]);
+        if (projectData) {
+          let home = getHomePage(projectData.data);
+          if (!home) {
+            home = projectData.data[0].children?.length
+              ? projectData.data[0].children[0]
+              : projectData.data[0];
+          }
+          router.push({
+            path: '/',
+            query: {
+              r: Date.now() + '',
+              id: home.pageId,
+            },
+          });
+        }
+
+        //获取首页
+        if (activedGroup.value !== '工程') {
+          activeAssets.value = 'structure';
+          graphicsRef.value.assetsChange(activeAssets.value);
+          activedGroup.value = '工程';
+        }
+      } else {
+        MessagePlugin.info('打开工程文件只支持 zip 格式');
+      }
+    }
+  };
+  input.click();
+};
+
+export const useProject = () => {
+  const getProject = async (id: string) => {
+    if (id === data.id) {
+      setActive(meta2d.store.data.id);
+      return;
+    }
+    const ret: any = await getCollection('web', id);
+    data.tree = ret.data;
+    data.id = ret.id;
+    setActive(meta2d.store.data.id);
+  };
+
+  const setActive = (pageId) => {
+    const find = (treeData) => {
+      for (let i = 0; i < treeData.length; i++) {
+        if (treeData[i].pageId === pageId) {
+          data.actived = [treeData[i].value];
+          return;
+        }
+        if (treeData[i].children) {
+          find(treeData[i].children);
+        }
+      }
+    };
+    find(data.tree);
+  };
+
+  const setProject = async (data) => {};
+
+  const saveProject = async (id?: string, over?: boolean) => {
+    if (!data.tree.length) {
+      //无数据
+      return;
+    }
+    if (!activeNode.value && !over) {
+      return;
+    }
+    if (id) {
+      if (!tree.value) {
+        // let activePage = getLeaf(activeNode.value.data.value,data.tree);
+        setData(data.tree, activeNode.value.data.value, 'pageId', id);
+        // activePage.pageId = id;
+      } else {
+        await activeNode.value.setData({ pageId: id });
+      }
+      setData(data.tree, activeNode.value.data.value, 'pageId', id);
+    }
+    // return
+
+    const treeData = (await tree?.value?.getTreeData?.()) || data.tree;
+    if (!(treeData && treeData.length > 0)) {
+      return;
+    }
+
+    const _data = {
+      id: data.id,
+      data: treeData,
+      name: 'test',
+      image: '',
+    };
+    // if(data.id&&!id){
+    //   // 已经有工程 没有 不需要更新数据
+    //   return data.id;
+    // }
+    if (data.id) {
+      await updateCollection('web', _data);
+      return data.id;
+    } else {
+      const ret: any = await addCollection('web', _data);
+      data.id = ret.id;
+      return ret.id;
+    }
+  };
+
+  const clearProject = () => {
+    data.id = undefined;
+    data.tree = [];
+  };
+
+  const setActivedNode = (node) => {
+    activeNode.value = node;
+    // if(!data.tree){
+
+    // }
+    // activePage = getLeaf(node.data.value,data.tree);
+  };
+  return {
+    data,
+    tree,
+    activePage,
+    activeNode,
+    saveProject,
+    getProject,
+    setProject,
+    clearProject,
+    setActivedNode,
+  };
+};
+
+const getLeaf = (id, arr: any) => {
+  arr.forEach((item) => {
+    if (item.children) {
+      getLeaf(id, item.children);
+    } else {
+      if (item.value === id) {
+        return item;
+      }
+    }
+  });
+};
+
+const setData = (arr: any, id, key, value) => {
+  arr.forEach((item) => {
+    if (item.children) {
+      setData(item.children, id, key, value);
+    } else {
+      if (item.value === id) {
+        item[key] = value;
+      }
+    }
+  });
+};
+
+let downloadType: Frame = Frame.html;
+let pageDatas = {};
+let page3dDatas = {};
+let page2dDatas = {};
+
+const setDownloadType = (type: Frame) => {
+  downloadType = type;
+};
+
+const prePayList = reactive({
+  pngs: new Set<string>(),
+  jsPens: new Set<string>(),
+  iotPens: new Set<string>(),
+  svgPens: new Set<string>(),
+});
+
+let downloadList = [];
+
+const isV = (url) => {
+  return (
+    url.indexOf(`v.le5le.com`) !== -1 ||
+    url.indexOf(`view.le5le.com/v`) !== -1 ||
+    url.indexOf(`/view/v`) !== -1 ||
+    url.indexOf(`/preview`) !== -1
+  );
+};
+
+const is3D = (url) => {
+  return (
+    url.indexOf(`3d.le5le.com`) !== -1 ||
+    url.indexOf(`view.le5le.com/3d`) !== -1 ||
+    url.indexOf(`/view/3d`) !== -1
+  );
+};
+
+const is2D = (url) => {
+  return (
+    url.indexOf(`2d.le5le.com`) !== -1 ||
+    url.indexOf(`view.le5le.com/2d`) !== -1 ||
+    url.indexOf(`/view/2d`) !== -1
+  );
+};
+
+const getPageDataByUrl = async (url) => {
+  // let id = queryURLParams(url.split('?')[1])?.id;
+  let params = queryURLParams(url.split('?')[1]);
+  let id = params?.id;
+  delete params.id;
+  let search = '';
+  for(let key in params){
+    search += `&${key}=${params[key]}`
+  }
+
+  if (isV(url)) {
+    if (!pageDatas[id]) {
+      let data = await getCollection('v', id);
+      pageDatas[id] = data.data;
+    }
+    //TODO 更改图纸中的iframe地址
+    if (downloadType === Frame.html) {
+      return `/view?data=${id}${search}`; //`/view/v?data=${id}`
+    } else {
+      return `/2d?id=${id}${search}`;
+    }
+  } else if (is3D(url)) {
+    if (!page3dDatas[id]) {
+      let data = await getCollection('3d', id);
+      page3dDatas[id] = data.data;
+    }
+    if (downloadType === Frame.html) {
+      return `/view?data=${id}${search}`; //`/view/v?data=${id}`
+    } else {
+      return `/view/index.html?data=${id}${search}`;
+    }
+  } else if (is2D(url)) {
+    if (!page2dDatas[id]) {
+      let data = await getCollection('2d', id);
+      page2dDatas[id] = data.data;
+    }
+    if (downloadType === Frame.html) {
+      return `/view?data=${id}${search}`; //`/view/v?data=${id}`
+    } else {
+      return `/2d?id=${id}${search}`;
+    }
+  }
+};
+
+//获取工程包含的所有数据内容
+/**
+ *
+ * @param replace 是否替换地址
+ */
+const getDatas = async (replace = true) => {
+  const list = treeToList(data.tree).filter((item) => item.type && item.pageId);
+  pageDatas = {};
+  page3dDatas = {};
+  page2dDatas = {};
+
+  await Promise.all(
+    list.map(async (item: any) => {
+      let v = await getCollection('v', item.pageId);
+      pageDatas[item.pageId] = v.data;
+      if (v.data?.pens?.length) {
+        // 内嵌iframe网页
+        const pens = v.data.pens.filter(
+          (pen) =>
+            pen.name === 'iframe' &&
+            (isV(pen.iframe) || is3D(pen.iframe) || is2D(pen.iframe))
+        );
+        for (let i = 0; i < pens.length; i++) {
+          let pen = pens[i];
+          let iframe = await getPageDataByUrl(pen.iframe);
+          if (replace) {
+            pen.iframe = iframe;
+          }
+        }
+
+        for (let i = 0; i < v.data.pens.length; i++) {
+          let pen = v.data.pens[i];
+          if (pen.events?.length) {
+            for (let j = 0; j < pen.events.length; j++) {
+              //打开弹框
+              const actions = pen.events[j].actions; //.filter((action)=>action.action === 14);
+              for (let k = 0; k < actions.length; k++) {
+                let action = actions[k];
+                if (action.action === 14) {
+                  //打开弹框
+                  let iframe = await getPageDataByUrl(action.params);
+                  if (replace) {
+                    action.params = iframe;
+                  }
+                } else if (action.action === 1) {
+                  //更改iframe属性
+                  if (action.value?.iframe) {
+                    let iframe = await getPageDataByUrl(action.value.iframe);
+                    if (replace) {
+                      action.value.iframe = iframe;
+                    }
+                  }
+                }
+              }
+            }
+          }
+          if (pen.triggers?.length) {
+            for (let j = 0; j < pen.triggers.length; j++) {
+              const trigger = pen.triggers[j];
+              for (let k = 0; k < trigger.status.length; k++) {
+                let status = trigger.status[k];
+                for (let l = 0; l < status.actions.length; l++) {
+                  let action = status.actions[l];
+                  if (action.action === 14) {
+                    //打开弹框
+                    // action.params = await getPageDataByUrl(action.params);
+                    let iframe = await getPageDataByUrl(action.params);
+                    if (replace) {
+                      action.params = iframe;
+                    }
+                  } else if (action.action === 1) {
+                    //更改iframe属性
+                    if (action.value?.iframe) {
+                      // action.value.iframe = await getPageDataByUrl(action.value.iframe);
+                      let iframe = await getPageDataByUrl(action.value.iframe);
+                      if (replace) {
+                        action.value.iframe = iframe;
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    })
+  );
+};
+
+const getEnterprisePens = async () => {
+  let pngs = [],
+    jsPens = [],
+    iotPens = [],
+    svgPens = [];
+  for (let key in pageDatas) {
+    const pageData = pageDatas[key];
+    const payList = getPayList(pageData);
+    pngs.push(...payList.pngs);
+    jsPens.push(...payList.jsPens);
+    iotPens.push(...payList.iotPens);
+    svgPens.push(...payList.svgPens);
+  }
+
+  for (let key in page2dDatas) {
+    const pageData = page2dDatas[key];
+    const payList = getPayList(pageData);
+    pngs.push(...payList.pngs);
+    jsPens.push(...payList.jsPens);
+    iotPens.push(...payList.iotPens);
+    svgPens.push(...payList.svgPens);
+  }
+
+  prePayList.pngs = new Set(pngs);
+  prePayList.jsPens = new Set(jsPens);
+  prePayList.iotPens = new Set(iotPens);
+  prePayList.svgPens = new Set(svgPens);
+};
+
+// 下载工程
+export const downloadProject = async (type: Frame) => {
+  // if (!isVip()) {
+  //   return;
+  // }
+  downloadType = type;
+  //通过工程获取所有的页面数据 ,包括2d 3d v
+  await getDatas();
+  // if (isDownload) {
+    //安装包
+  await doDownloadProject();
+  return;
+  // }
+  //获取 2d 大屏企业图形库
+  // await getEnterprisePens();
+  // await prePayProject();
+};
+
+export const doDownloadProject = async () => {
+  //TODO 验证付费图形库
+  taskDialog.visible = true;
+  taskDialog.tasks = [
+    {
+      status: 'prepare',
+      title: '获取工程数据资源',
+    },
+    {
+      status: 'prepare',
+      title: '下载工程资源',
+    },
+    {
+      status: 'prepare',
+      title: '完成',
+    },
+  ];
+  taskDialog.tasks[0].status = 'process';
+
+  let dList = [];
+  let flag_3d = false;
+  if (downloadType === Frame.html) {
+    for (let key in pageDatas) {
+      // pageDatas[key].userId = user.id;
+      dList.push(...(await getDownloadList(pageDatas[key], key, false)));
+    }
+    for (let key in page2dDatas) {
+      // page2dDatas[key].userId = user.id;
+      dList.push(...(await getDownloadList(page2dDatas[key], key, false)));
+    }
+
+    for (let key in page3dDatas) {
+      flag_3d = true;
+      let source3dList = await getResource(
+        page3dDatas[key].data,
+        key,
+        'deploy'
+      );
+      dList.push(...source3dList);
+    }
+
+    //下载运行环境文件
+    dList.push(...getDownloadList(undefined, 'v', flag_3d));
+  } else {
+    for (let key in pageDatas) {
+      // pageDatas[key].userId = user.id;
+      dList.push(
+        ...(await getFrameDownloadList(
+          pageDatas[key],
+          key,
+          downloadType,
+          false
+        ))
+      );
+    }
+    for (let key in page2dDatas) {
+      // page2dDatas[key].userId = user.id;
+      dList.push(
+        ...(await getFrameDownloadList(
+          page2dDatas[key],
+          key,
+          downloadType,
+          false
+        ))
+      );
+    }
+    const map = {
+      [Frame.vue2]: 'toVue2',
+      [Frame.vue3]: 'toVue3',
+      [Frame.react]: 'toReact',
+    };
+    for (let key in page3dDatas) {
+      flag_3d = true;
+      let source3dList = await getResource(
+        page3dDatas[key].data,
+        key,
+        map[downloadType]
+      );
+      let folderName =downloadType===Frame.vue3?'meta2d-vue3':downloadType===Frame.vue2?'meta2d-vue2': 'meta2d-react';
+      source3dList.forEach((item) => {
+        if(item.path.startsWith('/vue3/')||item.path.startsWith('/vue2/')||item.path.startsWith('/react/')){
+          item.path = item.path.replace('vue3',folderName).replace('vue2',folderName).replace('react',folderName);
+        }
+      });
+      dList.push(...source3dList);
+    }
+
+    //下载运行环境文件
+    dList.push(...getFrameDownloadList(undefined, 'v', downloadType, flag_3d));
+  }
+  taskDialog.tasks[0].status = 'success';
+  taskDialog.tasks[1].status = 'process';
+  downloadList = uniqueObjArrayFast(dList, 'path');
+
+  //下载列表
+  let result = await saveDownload(downloadList);
+  if (!result) {
+    return;
+  }
+  taskDialog.tasks[1].status = 'success';
+  taskDialog.tasks[2].status = 'process';
+  taskDialog.visible = false;
+};
+
+const saveDownload = async (downloadList) => {
+  const list = [...downloadList];
+  //控件
+  let jsPath = '';
+  let jsPensPath = '';
+  switch (downloadType) {
+    case Frame.html:
+      jsPath = '/view/js/2d-components.js';
+      jsPensPath = `/view/js/1.js`;
+      break;
+    case Frame.vue2:
+      jsPath = '/meta2d-vue2/public/js/2d-components.js';
+      jsPensPath = `/meta2d-vue2/public/js/1.js`;
+      break;
+    case Frame.vue3:
+      jsPath = '/meta2d-vue3/public/js/2d-components.js';
+      jsPensPath = `/meta2d-vue3/public/js/1.js`;
+      break;
+    case Frame.react:
+      jsPath = '/meta2d-react/public/js/2d-components.js';
+      jsPensPath = `/meta2d-react/public/js/1.js`;
+      break;
+  }
+  
+  // if (isDownload) {
+    // 安装包
+    list.push({
+      url: '/view/js/2d-components.js', //需要购买
+      path: jsPath,
+    });
+    /*
+  } else {
+    const js = await get2dComponentJs([...prePayList.iotPens]);
+    list.push({
+      data: js,
+      path: jsPath,
+    });
+
+    ///png图形库
+    let pngs: any = {};
+    //TODO token
+    let token = '';
+    if (token) {
+      pngs = await getDeployPngs([...prePayList.pngs], token);
+    } else {
+      pngs = await getTemPngs([...prePayList.pngs]);
+    }
+    list.forEach((item) => {
+      if (item.url) {
+        let url = item.url.replace(img_cdn, '').replace(img_upCdn, '');
+        if (pngs[url]) {
+          item.url = pngs[url];
+        }
+      }
+    });
+    //js线性图元
+    let arr = [];
+    if (token) {
+      arr = [...prePayList.jsPens];
+    } else {
+      const res: any = await axios.post(
+        '/api/paid/2d/component?pageSize=1000',
+        {
+          type: 'JS线性图元',
+          collection: 'v',
+          id: meta2d.store.data.id,
+        }
+      );
+      let purchased = [];
+      if (res?.list && res.list.length) {
+        purchased = res.list.map((item) => item.name);
+      }
+      [...prePayList.jsPens].forEach((item) => {
+        if (purchased.includes(item)) {
+          arr.push(item);
+        }
+      });
+    }
+    const res_list: any = await axios.post('/api/2d/tools', {
+      list: arr.map((item) => {
+        return {
+          type: 'JS线性图元',
+          name: item,
+        };
+      }),
+    });
+    const json =
+      `!window.meta2dToolId&&(window.meta2dToolId="${res_list?.id}");var tmpTools=` +
+      JSON.stringify(res_list?.list) +
+      `;!window.meta2dTools&&(window.meta2dTools=[]);window.meta2dTools.push.apply(window.meta2dTools,tmpTools);`;
+
+    list.push({
+      data: json,
+      path: jsPensPath,
+    });
+
+    //SVG线性图元
+    if ([...prePayList.svgPens].length) {
+      // let purchased = data.purchasedList?.filter(
+      //   (_item) => _item.type === 'SVG线性图元'
+      // );
+      let purchased = [];
+      if (!token) {
+        const res: any = await axios.post(
+          '/api/paid/2d/component?pageSize=1000',
+          {
+            type: 'SVG线性图元',
+            collection: 'v',
+            id: meta2d.store.data.id,
+          }
+        );
+        purchased = res?.list || [];
+      }
+      //TODO
+      let count = 0; // data.goods.find((item) => item.type === 'SVG线性图元')?.count;
+      if (purchased.length === count || token) {
+        // if (purchased.length === 1 && !purchased[0].name) {
+        //已经购买全部
+        list.forEach((item) => {
+          if (
+            item.data &&
+            (item.path.indexOf('/projects/2d') !== -1 ||
+              item.path.indexOf('/projects/v') !== -1 ||
+              item.path.indexOf('/public/json') !== -1) &&
+            item.path.indexOf('/projects/v/png/') === -1 &&
+            item.path.indexOf('/projects/2d/png/') === -1
+          ) {
+            //清空所有svgpath
+            let meta2dData = JSON.parse(item.data);
+            for (let key of Object.keys(meta2dData.paths)) {
+              let path = meta2dData.paths[key];
+              if (
+                path.indexOf('-1.18Zm4-1') !== -1 ||
+                path.indexOf('-1.19Zm4-1') !== -1 ||
+                path.indexOf('2.85ZM') !== -1 ||
+                path.indexOf('-1-2.39.3') !== -1
+              ) {
+                meta2dData.paths[key] = '';
+              }
+            }
+            item.data = JSON.stringify(meta2dData);
+          }
+        });
+      } else {
+        let svgnames = purchased.map((i) => i.name);
+        list.forEach((item) => {
+          if (
+            item.data &&
+            (item.path.indexOf('/projects/2d') !== -1 ||
+              item.path.indexOf('/projects/v') !== -1 ||
+              item.path.indexOf('/public/json') !== -1) &&
+            item.path.indexOf('/projects/v/png/') === -1 &&
+            item.path.indexOf('/projects/2d/png/') === -1
+          ) {
+            //2d 图纸数据
+            let meta2dData = JSON.parse(item.data);
+            meta2dData.pens.forEach((pen) => {
+              if (pen.name === 'svgPath' && pen.svgUrl) {
+                if (svgnames.includes(pen.svgUrl.replace(img_cdn, ''))) {
+                  pen.pathId = null;
+                }
+              }
+            });
+            item.data = JSON.stringify(meta2dData);
+          }
+        });
+      }
+    }
+  }*/
+  //开始下载list
+  let result = await saveZip(list);
+  if (result === 'error') {
+    taskDialog.tasks[1].status = 'error';
+    taskDialog.reload = true;
+    taskDialog.downloadList = list;
+    return false;
+  }
+  MessagePlugin.closeAll();
+  MessagePlugin.success('下载成功,请在浏览器下载列表中查看');
+  return true;
+};
+
+const saveZip = async (list: any[], fileName: string = '工程下载') => {
+  //开始下载list
+  const [{ default: JSZip }, { saveAs }] = await Promise.all([
+    import('jszip'),
+    import('file-saver'),
+  ]);
+  const zip = new JSZip();
+  const _zip = zip.folder(`${fileName}`);
+
+  const results = await Promise.all(
+    list.map(async (item: any) => {
+      if (item.url) {
+        //接口请求
+        try {
+          let url = item.url.startsWith('/') ? cdn + item.url : item.url;
+          if (url.indexOf('/view/index.html') !== -1) {
+            url = url.replace('/view/index.html', '/v/view/index.html'); //线上不包含cdn的inde.html
+          }
+          if (url.includes('?')) {
+            url = url + `&r=${Date.now()}`;
+          } else {
+            url = url + `?r=${Date.now()}`;
+          }
+          let res: Blob = null;
+          let localBlob: any = await localforage.getItem(item.path);
+          if (localBlob) {
+            res = localBlob;
+          } else {
+            res = await axios.get(url, {
+              responseType: 'blob',
+            });
+          }
+          if (!res) {
+            throw new Error('请求失败');
+          }
+          localforage.setItem(item.path, res); //缓存
+          let path = item.path.split('?')[0];
+          if (path.startsWith('/')) {
+            path = path.slice(1);
+          }
+          _zip.file(path, res, { createFolders: true });
+        } catch (error) {
+          return { error: error.message,url: item.url}; // 返回错误信息
+        }
+      } else if (item.data) {
+        //直接写数据
+        let path = item.path;
+        if (path.startsWith('/')) {
+          path = path.slice(1);
+        }
+        _zip.file(path, item.data, { createFolders: true });
+      }
+    })
+  );
+  console.log("results",results);
+  let errorLen = results.filter((item) => item && item.error);
+  if (errorLen.length) {
+    MessagePlugin.error('部分文件下载失败,请确保网络畅通');
+    return 'error';
+  }
+  //清除本地存储
+  list.forEach((item) => {
+    localforage.removeItem(item.path);
+  });
+  const blob = await zip.generateAsync({ type: 'blob' });
+  saveAs(blob, `${fileName}.zip`);
+};
+
+//遍历树
+const treeToList = (tree) => {
+  const list = [];
+  for (let i = 0; i < tree.length; i++) {
+    const item = tree[i];
+    list.push(item);
+    if (item.children) {
+      list.push(...treeToList(item.children));
+    }
+  }
+  return list;
+};
+
+//下载zip资源
+export const downloadProjectSource = async () => {
+  // if (!isVip()) {
+  //   return; //非vip用户不能下载
+  // }
+  await getDatas(false);
+  // if (isDownload) {
+    //安装包
+  await doDownloadProjectSource();
+    // return;
+  // }
+  // await getEnterprisePens(); //获取企业图库
+  // await prePay();
+};
+
+export const doDownloadProjectSource = async () => {
+  // prePayList.pngs.forEach
+  taskDialog.reload = false;
+  taskDialog.visible = true;
+  taskDialog.tasks = [
+    {
+      status: 'prepare',
+      title: '获取工程数据资源',
+    },
+    {
+      status: 'prepare',
+      title: '下载资源',
+    },
+    {
+      status: 'prepare',
+      title: '完成',
+    },
+  ];
+
+  taskDialog.tasks[0].status = 'process';
+  let dList = [];
+  let imgList = [];
+  for (let key in pageDatas) {
+    imgList.push(...(await getImgList(pageDatas[key])));
+  }
+  for (let key in page2dDatas) {
+    imgList.push(...(await getImgList(page2dDatas[key])));
+  }
+  imgList = uniqueObjArrayFast(imgList, 'path');
+
+  //获取去水印企业图库
+  /*if ([...prePayList.pngs].length && !isDownload) {
+    let pngs: any = {};
+    if (payDialog.token) {
+      pngs = await getDeployPngs(
+        [...prePayList.pngs],
+        undefined
+      );
+    } else {
+      pngs = await getTemPngs([...prePayList.pngs]);
+    }
+    if (!pngs) {
+      pngs = {};
+    }
+    imgList.forEach((item) => {
+      if (item.url) {
+        let url = item.url.replace(img_cdn, '').replace(img_upCdn, '');
+        if (pngs[url]) {
+          item.url = pngs[url];
+        }
+      }
+    });
+  }*/
+
+  dList.push(...imgList);
+  for (let key in pageDatas) {
+    dList.push({
+      data: JSON.stringify(pageDatas[key]),
+      path: `${key}.json`,
+    });
+  }
+  for (let key in page2dDatas) {
+    dList.push({
+      data: JSON.stringify(page2dDatas[key]),
+      path: `${key}.json`,
+    });
+  }
+  //TODO 获取zip包的内容
+  for (let key in page3dDatas) {
+    let source3dList = await getResource(
+      page3dDatas[key].data,
+      '3d-' + key,
+      'deployAsZip'
+    );
+    dList.push(...source3dList);
+  }
+  dList.push({
+    data: JSON.stringify(data.tree),
+    path: 'project.json',
+  });
+  dList = uniqueObjArrayFast(dList, 'path'); //去重
+  taskDialog.tasks[0].status = 'success';
+  taskDialog.tasks[1].status = 'process';
+
+  //下载
+  //开始下载list
+  let result = await saveZip(dList);
+  if (result === 'error') {
+    taskDialog.tasks[1].status = 'error';
+    taskDialog.reload = true;
+    taskDialog.downloadList = dList;
+    return;
+  }
+  taskDialog.tasks[1].status = 'success';
+
+  MessagePlugin.closeAll();
+  taskDialog.tasks[2].status = 'success';
+  MessagePlugin.success('下载成功,请在浏览器下载列表中查看');
+  taskDialog.visible = false;
+};
+
+//TODO 任务取消 是否需要删除已经上传的内容
+export const uploadProjectSource = async (file) => {
+  taskDialog.visible = true;
+  taskDialog.tasks = [
+    {
+      status: 'prepare',
+      title: '解析ZIP文件',
+    },
+    {
+      status: 'prepare',
+      title: '上传资源文件',
+    },
+    {
+      status: 'prepare',
+      title: '生成项目',
+    },
+    {
+      status: 'prepare',
+      title: '完成',
+    },
+  ];
+  // return;
+  //TODO 上传zip包
+  const { default: JSZip } = await import('jszip');
+  const zip = new JSZip();
+  await zip.loadAsync(file, { base64: true });
+
+  taskDialog.tasks[0].status = 'success';
+  taskDialog.tasks[1].status = 'process';
+
+  let fileName = file.name.slice(0, -4);
+  let treeData = '';
+  for (const key in zip.files) {
+    if (zip.files[key].dir) {
+      fileName = key.split('/')[0]; // 取第一个文件夹名称
+      continue;
+    }
+    if (key.endsWith('project.json')) {
+      // 工程json 文件
+      treeData = await zip.file(key).async('string');
+      break;
+    }
+  }
+
+  if (!treeData) {
+    MessagePlugin.error('这不是一个工程包,请重新选择');
+    taskDialog.visible = false;
+    return false;
+  }
+
+  const imgMap = {}; //新老图纸映射
+  const _2dDataMap = {}; //2d数据映射
+  const _3dIDMap = {}; //3d id映射
+  const _2dIDMap = {}; //2d id映射
+  //上传所有图片资源
+  for (const key in zip.files) {
+    if (taskDialog.cancel) {
+      return;
+    }
+    if (zip.files[key].dir) {
+      continue;
+    }
+    let _keyLower = key.toLowerCase();
+    if (
+      _keyLower.endsWith('.png') ||
+      _keyLower.endsWith('.svg') ||
+      _keyLower.endsWith('.gif') ||
+      _keyLower.endsWith('.jpg') ||
+      _keyLower.endsWith('.jpeg')
+    ) {
+      let _filename = key.substr(key.lastIndexOf('/') + 1);
+      const form = new FormData();
+      form.append('name', _filename);
+      form.append('directory', '/大屏/图片/默认');
+      form.append('shared', true + '');
+      form.append('file', await zip.file(key).async('blob'));
+      form.append('conflict', 'new');
+      const result: any = await axios.post('/api/image/upload', form);
+      let arr = key.split('/');
+      // arr.shift();
+
+      if (arr[0] === fileName) {
+        arr.shift();
+      }
+      if (arr[0] === fileName) {
+        arr.shift();
+      }
+      let _key = '/' + arr.join('/');
+
+      if (result) {
+        imgMap[_key] = result.url;
+      }
+    } else if (_keyLower.endsWith('.json')) {
+      let arr = key.split('/');
+      if (arr[0] === fileName) {
+        arr.shift();
+      }
+      if (arr[0] === fileName) {
+        arr.shift();
+      }
+      let data = await zip.file(key).async('string');
+      let id = arr[0].split('.')[0];
+      // if(!arr[0].endsWith('.json')){
+      if (!key.endsWith('project.json')) {
+        _2dDataMap[id] = data;
+      }
+      // }
+    } else {
+      //TODO 多个3d场景问题
+      if (key.indexOf('3d') !== -1 && key.indexOf('files/') === -1) {
+        let id_3d = await load3d(zip, key);
+        if (id_3d) {
+          let originId = key.split('3d-')[1];
+          _3dIDMap[originId] = id_3d;
+        }
+      }
+    }
+  }
+
+  taskDialog.tasks[1].status = 'success';
+  taskDialog.tasks[2].status = 'process';
+  if (taskDialog.cancel) {
+    return;
+  }
+  //将 图纸里面的地址替换成最新的地址
+  for (let old in imgMap) {
+    let newImg = imgMap[old];
+    for (let key in _2dDataMap) {
+      _2dDataMap[key] = _2dDataMap[key].replaceAll(old, newImg);
+    }
+  }
+  //上传所有图纸
+  //生成 老图纸和新图纸对应的map
+  let arr = Object.keys(_2dDataMap);
+  // for(let key in _2dDataMap){
+  for (let i = 0; i < arr.length; i++) {
+    if (taskDialog.cancel) {
+      return;
+    }
+    let idata = JSON.parse(_2dDataMap[arr[i]]);
+    const ret: any = await addCollection('v', {
+      data: idata,
+      image: idata.image || 'xxx',
+      name: idata.name || '新建项目',
+      folder: idata.folder,
+      system: false,
+      case: idata.case,
+    });
+    _2dIDMap[arr[i]] = ret.id;
+  }
+  treeData = JSON.parse(treeData);
+  arr = treeToList(treeData)
+    .filter((item) => item.pageId)
+    .map((item) => item.pageId);
+  deepUpdateID(treeData, _2dIDMap);
+  //上传 生成的新的工程树
+  const projectData = {
+    id: data.id,
+    data: treeData,
+    name: 'test',
+    image: '',
+  };
+  const ret: any = await addCollection('web', projectData);
+  const projectId = ret.id;
+
+  //更新图纸里面工程树id内容
+  // for(let key in _2dDataMap){
+  for (let i = 0; i < arr.length; i++) {
+    if (taskDialog.cancel) {
+      return;
+    }
+    let idata = await update2dData(_2dDataMap[arr[i]], _2dIDMap, _3dIDMap);
+    const ret: any = await updateCollection('v', {
+      data: idata,
+      id: _2dIDMap[arr[i]],
+      otherData: { projectId },
+    });
+    _2dIDMap[arr[i]] = ret.id;
+  }
+  taskDialog.tasks[2].status = 'success';
+  taskDialog.tasks[3].status = 'process';
+
+  taskDialog.tasks[3].status = 'success';
+  taskDialog.visible = false;
+  return ret;
+};
+
+//将工程树里面的图纸id替换成新的id
+const deepUpdateID = (treeData, _2dIDMap) => {
+  treeData.forEach((item) => {
+    if (item.children) {
+      deepUpdateID(item.children, _2dIDMap);
+    } else {
+      if (item.type === 'page') {
+        item.pageId = _2dIDMap[item.pageId];
+      }
+    }
+  });
+};
+
+function getSearch(url){
+  let params = queryURLParams(url.split('?')[1]);
+  let id = params?.id;
+  delete params.id;
+  let search = '';
+  for(let key in params){
+    search += `&${key}=${params[key]}`
+  }
+  return {
+    id,
+    search
+  }
+}
+
+const update2dData = async (_data, _2dIDMap, _3dIDMap) => {
+  let domain = 'https://view.le5le.com';
+  // if (isDownload || location.origin.indexOf('le5le.com') === -1) {
+    domain = location.origin + '/view';
+  // }
+  let data = JSON.parse(_data);
+  if (data.pens?.length) {
+    // 内嵌iframe网页
+    const pens = data.pens.filter(
+      (pen) =>
+        pen.name === 'iframe' &&
+        (isV(pen.iframe) || is3D(pen.iframe) || is2D(pen.iframe))
+    );
+    for (let i = 0; i < pens.length; i++) {
+      let pen = pens[i];
+      let { id, search } = getSearch(pen.iframe);
+      if (is3D(pen.iframe)) {
+        pen.iframe = `${domain}/3d/?id=${_3dIDMap[id]}${search}`;
+      } else {
+        pen.iframe = `${domain}/v/?id=${_2dIDMap[id]}${search}`;
+      }
+    }
+
+    for (let i = 0; i < data.pens.length; i++) {
+      let pen = data.pens[i];
+      if (pen.events?.length) {
+        for (let j = 0; j < pen.events.length; j++) {
+          //打开弹框
+          const actions = pen.events[j].actions;
+          for (let k = 0; k < actions.length; k++) {
+            let action = actions[k];
+            if (action.action === 14) {
+              //打开弹框
+              if (
+                is2D(action.params) ||
+                is3D(action.params) ||
+                isV(action.params)
+              ) {
+                // let id = queryURLParams(action.params.split('?')[1])?.id;
+                let { id, search } = getSearch(action.params);
+                action.params = `${domain}/v/?id=${_2dIDMap[id]}${search}`;
+              }
+            } else if (action.action === 1) {
+              //更改iframe属性
+              if (action.value?.iframe) {
+                if (is2D(action.value.iframe) || isV(action.value.iframe)) {
+                  // let id = queryURLParams(
+                  //   action.value.iframe.split('?')[1]
+                  // )?.id;
+                  let { id, search } = getSearch(action.value.iframe);
+                  action.value.iframe = `${domain}/v/?id=${_2dIDMap[id]}${search}`;
+                } else if (is3D(action.value.iframe)) {
+                  // let id = queryURLParams(
+                  //   action.value.iframe.split('?')[1]
+                  // )?.id;
+                  let { id, search } = getSearch(action.value.iframe);
+                  action.value.iframe = `${domain}/3d/?id=${_3dIDMap[id]}${search}`;
+                }
+              }
+            } else if (action.action === 13) {
+              //打开视图
+              action.value = _2dIDMap[action.value];
+            }
+          }
+        }
+      }
+      if (pen.triggers?.length) {
+        for (let j = 0; j < pen.triggers.length; j++) {
+          const trigger = pen.triggers[j];
+          for (let k = 0; k < trigger.status.length; k++) {
+            let status = trigger.status[k];
+            for (let l = 0; l < status.actions.length; l++) {
+              let action = status.actions[l];
+              if (action.action === 14) {
+                //打开弹框
+                if (
+                  is2D(action.params) ||
+                  is3D(action.params) ||
+                  isV(action.params)
+                ) {
+                  // let id = queryURLParams(action.params.split('?')[1])?.id;
+                  let { id, search } = getSearch(action.params);
+                  action.params = `${domain}/v/?id=${_2dIDMap[id]}${search}`;
+                }
+              } else if (action.action === 1) {
+                //更改iframe属性
+                if (action.value?.iframe) {
+                  if (is2D(action.value.iframe) || isV(action.value.iframe)) {
+                    // let id = queryURLParams(
+                    //   action.value.iframe.split('?')[1]
+                    // )?.id;
+                    let { id, search } = getSearch(action.value.iframe);
+                    action.value.iframe = `${domain}/v/?id=${_2dIDMap[id]}${search}`;
+                  } else if (is3D(action.value.iframe)) {
+                    // let id = queryURLParams(
+                    //   action.value.iframe.split('?')[1]
+                    // )?.id;
+                    let { id, search } = getSearch(action.value.iframe);
+                    action.value.iframe = `${domain}/3d/?id=${_3dIDMap[id]}${search}`;
+                  }
+                }
+              } else if (action.action === 13) {
+                //打开视图
+                action.value = _2dIDMap[action.value];
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+  return data;
+};
+
+//去重
+function uniqueObjArrayFast(arr, key) {
+  const seen = {};
+  return arr.filter((item) => {
+    const val = item[key];
+    return seen.hasOwnProperty(val) ? false : (seen[val] = true);
+  });
+}
+
+const uploadProject = async (file) => {};
+
+const getImgList = async (meta2dData) => {
+  let lists = [];
+  let img = meta2dData.bkImage;
+  if (img) {
+    if (
+      img.startsWith('/') ||
+      img.startsWith(img_cdn) ||
+      img.startsWith(img_upCdn)
+    ) {
+      let _img = img.replace(img_cdn, '').replace(img_upCdn, '');
+      if (_img.startsWith('/v/')) {
+        _img = _img.slice(2);
+      }
+      _img = decodeURIComponent(_img);
+      lists.push({
+        url: img,
+        path: _img,
+      });
+      meta2dData.bkImage = _img;
+    }
+  }
+  //图片图元(image strokeImage backgroundImage)
+  const imageKeys = ['image', 'strokeImage', 'backgroundImage','activeBgImage','bgImage'];
+  const images: string[] = [];
+  for (const pen of meta2dData.pens) {
+    for (const i of imageKeys) {
+      const image = pen[i];
+      if (image) {
+        if (
+          image.startsWith('/') ||
+          image.startsWith(img_cdn) ||
+          image.startsWith(img_upCdn)
+        ) {
+          // 只考虑相对路径下的 image ,绝对路径图片无需下载
+          let _img = image.replace(img_cdn, '').replace(img_upCdn, '').split('?')[0];
+          if (_img.startsWith('/v/')) {
+            _img = _img.slice(2);
+          }
+          _img = decodeURIComponent(_img);
+          if (!images.includes(image)) {
+            // let _img = image.replace(cdn, '').replace(upCdn, '');
+            lists.push({
+              url: image,
+              path: _img,
+            });
+          }
+          pen[i] = _img;
+        }
+      }
+    }
+    pen.events?.forEach((event) => {
+      if (event.actions?.length) {
+        event.actions.forEach((action) => {
+          if (action.action === 1) {
+            //更改属性
+            if (action.value?.image) {
+              let image = action.value.image;
+              if (
+                image.startsWith('/') ||
+                image.startsWith(img_cdn) ||
+                image.startsWith(img_upCdn)
+              ) {
+                // 只考虑相对路径下的 image ,绝对路径图片无需下载
+                let _img = image.replace(img_cdn, '').replace(img_upCdn, '').split('?')[0];
+                if (_img.startsWith('/v/')) {
+                  _img = _img.slice(2);
+                }
+                _img = decodeURIComponent(_img);
+                if (!images.includes(image)) {
+                  lists.push({
+                    url: image,
+                    path: _img,
+                  });
+                }
+                action.value.image = _img;
+              }
+            }
+          }
+        });
+      }
+    });
+    pen.triggers?.forEach((trigger) => {
+      trigger?.status?.forEach((state) => {
+        if (state.actions?.length) {
+          state.actions.forEach((action) => {
+            if (action.action === 1) {
+              //更改属性
+              if (action.value?.image) {
+                let image = action.value.image;
+                if (
+                  image.startsWith('/') ||
+                  image.startsWith(img_cdn) ||
+                  image.startsWith(img_upCdn)
+                ) {
+                  // 只考虑相对路径下的 image ,绝对路径图片无需下载
+                  let _img = image.replace(img_cdn, '').replace(img_upCdn, '').split('?')[0];
+                  if (_img.startsWith('/v/')) {
+                    _img = _img.slice(2);
+                  }
+                  _img = decodeURIComponent(_img);
+                  if (!images.includes(image)) {
+                    lists.push({
+                      url: image,
+                      path: _img,
+                    });
+                  }
+                  action.value.image = _img;
+                }
+              }
+            }
+          });
+        }
+      });
+    });
+  }
+
+  return lists;
+};

+ 60 - 1
src/services/utils.ts

@@ -301,4 +301,63 @@ export const getImgUrl = (url: string) => {
   }
 
   return import.meta.env.VITE_IMG_API + '/' + url
-}
+}
+
+// monaco代码提示类型管理
+export class MonacoTypeManager {
+  private monaco: any;
+  constructor(monacoInstance) {
+    this.monaco = monacoInstance;
+  }
+
+   loadTypeFile(path: string,callback) {
+    fetch(path)
+        .then(response => response.text())
+        .then(data => {
+          this.registerTypeFile(data)
+          callback()
+        })
+        .catch(error => {
+          console.error('Error fetching type file:', error);
+        });
+   }
+
+  /**
+   * 加载并注册 .d.ts 文件内容
+   * @param {string} typeContent - 类型声明字符串内容
+   * @param {string} filePath - 唯一虚拟文件路径
+   */
+  registerTypeFile(typeContent) {
+    this.monaco.languages.typescript.javascriptDefaults.addExtraLib(
+        typeContent,
+    );
+  }
+
+  clearTypeFiles() {
+    this.monaco.languages.typescript.javascriptDefaults.setExtraLibs([]);
+  }
+
+  /**
+   * 为指定变量生成类型声明并注入
+   * @param {string} varName - 变量名,例如 ctx
+   * @param {string} typeName - 类型名,例如 CanvasRenderingContext2D
+   */
+  injectVariableType(varName, typeName) {
+    // const filePath = `file:///injected-types/${varName}.d.ts`;
+    const typeContent = `
+      /** @type {${typeName}} */
+      var ${varName};
+    `;
+    this.registerTypeFile(typeContent);
+  }
+
+  /**
+   * 批量注册变量类型
+   * @param {Array<{ varName: string, typeName: string }>} typeList
+   */
+  injectMultipleVariables(typeList) {
+    typeList.forEach(({ varName, typeName }) => {
+      this.injectVariableType(varName, typeName);
+    });
+  }
+}

+ 2 - 1
src/views/Index.vue

@@ -3,7 +3,7 @@
     <Header />
 
     <div class="design-body">
-      <Graphics />
+      <Graphics ref="graphicsRef"/>
       <View />
       <div style="border-left: 1px solid var(--color-border);">
         <FitProps v-if="selections.mode === SelectionMode.Fit" />
@@ -29,6 +29,7 @@ import { onMounted, onUnmounted, ref } from "vue";
 import { DeviceItem, IframeMsg } from '@/types';
 import { getVisual2DMsgType, Visual2DMsgType } from '.';
 import { useModuleType } from '@/services/module-type';
+import { graphicsRef } from '@/services/project';
 import { useRoute } from 'vue-router';
 import { saveModuleAndClose } from '@/utils';
 

+ 45 - 11
src/views/components/Actions.vue

@@ -15,7 +15,7 @@
         <div class="form-item mt-4">
           <label>{{$t('动作类型')}}</label>
           <t-select v-model="a.action" @change="onChangeAction(a)" :placeholder="$t('请选择')">
-            <t-option v-for="option in actionOptions" :key="option.value" :value="option.value" :label="option.label"></t-option>
+            <t-option v-for="option in actionOptions" :key="option.value" :value="option.value" :label="$t(option.label)"></t-option>
           </t-select>
         </div>
         <template v-if="a.action == 0">
@@ -34,7 +34,15 @@
         <template v-else-if="a.action == 13">
           <div class="form-item mt-8">
             <label>{{$t('视图')}}</label>
-            <t-input v-model="a.value" placeholder="ID"></t-input>
+            <t-tree-select
+              v-if="projectData.id"
+              v-model="a.value"
+              :data="projectData.tree"
+              :keys="{value:'pageId'}"
+              filterable
+              placeholder="ID"
+            />
+            <t-input v-else v-model="a.value" placeholder="ID" />
           </div>
         </template>
         <template v-else-if="a.action == 2 || a.action == 3 || a.action == 4">
@@ -157,6 +165,7 @@
                       @change="valueChange($event,a.value,key)"
                       :placeholder="$t('值')"
                     />
+                    <LinkIcon class="hover actions-bind" :class="{ primary: a.value[key].key }" @click="bindPenAProp(a.value,key)" />
                   </div>
                   <div class="right px-8" style="line-height: 20px">
                     <!-- <t-icon
@@ -207,7 +216,7 @@
           <Network v-model="a.network" mode="1" />
           <div class="flex middle mt-16">
             <t-tooltip content="json格式">
-              <div class="flex middle mr-8" style="font-size:12px;color:var(--color)">数据</div>
+              <div class="flex middle mr-8" style="font-size:12px;color:var(--color)">{{$t('数据')}}</div>
             </t-tooltip>
             <!-- <add-circle-icon @click="addSendData(a)" class="hover"/> -->
           </div>
@@ -219,7 +228,7 @@
                   <template v-if="d.prop">{{d._label||d.label}}<br>{{ d.prop }}</template>
                 </template>
                 <!-- <t-input class="actions-prop" style="width: 70px;" v-model="d.prop" /> -->
-                <div class="actions-prop"> {{ d.label||'参数名'  }}</div>
+                <div class="actions-prop"> {{ d.label||$t('参数名')  }}</div>
                 </t-tooltip>
                 <Edit1Icon class="hover" @click="selectDeviceProps(d)"/>
               <!-- <t-input style="width: 100px;" v-model="item.value" /> -->
@@ -234,7 +243,7 @@
                 >
                   <ellipsis-icon slot="icon" />
                 </t-button>
-                <t-input v-else :placeholder="d.key&&d.id?((d.idLabel||d.id)+'.'+(d.keyLabel||d.key)):('参数值')" :readonly="!!(d.key&&d.id)" class="full" v-model="d.value" />
+                <t-input v-else :placeholder="d.key&&d.id?((d.idLabel||d.id)+'.'+(d.keyLabel||d.key)):($t('参数值'))" :readonly="!!(d.key&&d.id)" class="full" v-model="d.value" />
                 <LinkIcon class="hover actions-bind" :class="{ primary: d.key }" @click="bindPenProp(d)" />
               </div>
               <!-- <t-tree-select
@@ -597,22 +606,22 @@
         </template>
         <template v-else-if="a.action == 18">
           <div class="form-item mt-8">
-            <label>消息类型</label>
+            <label>{{$t('消息类型')}}</label>
             <t-select
               v-model="a.params"
-              placeholder="请选择"
+              :placeholder="$t('请选择')"
             >
               <t-option
                 v-for="option in themeOptions"
                 :key="option.value"
                 :value="option.value"
-                :label="option.label"
+                :label="$t(option.label)"
               />
             </t-select>
           </div>
           <div class="form-item mt-8">
-            <label>消息内容</label>
-            <t-input v-model="a.value" placeholder="消息内容" />
+            <label>{{$t('消息内容')}}</label>
+            <t-input v-model="a.value" :placeholder="$t('消息内容')" />
           </div>
         </template> 
       </div>
@@ -648,10 +657,12 @@ import Network from './Network.vue';
 import { CloseIcon, AddCircleIcon, DeleteIcon, CheckIcon, Edit1Icon, LinkIcon, EllipsisIcon, MinusCircleIcon} from 'tdesign-icons-vue-next';
 import { getPenAnimations, getPenTree ,changeType, getIframeTree} from '@/services/common';
 import { deepClone } from '@meta2d/core';
+import { useProject } from '@/services/project';
 
 const { proxy } = getCurrentInstance();
 const $t = proxy.$t
 
+const { data:projectData } = useProject();
 const { data } = defineProps<{
   data: any;
 }>();
@@ -722,7 +733,7 @@ const actionOptions = [
     value: 17,
   },
   {
-    label: '全局消息',
+    label: $t('全局消息'),
     value: 18,
   },
 ];
@@ -1112,6 +1123,14 @@ const bindPenProp = (d)=>{
   penProp.value.data = d;
 }
 
+const bindPenAProp = (value,key)=>{
+  penProp.value.visible = true;
+  if(!(Object.prototype.toString.call(value[key]) === '[object Object]')){
+    value[key] = {}
+  }
+  penProp.value.data = value[key];
+}
+
 const more = ref({
   visible:false,
   data:null,
@@ -1272,5 +1291,20 @@ const themeOptions = [
       width: 82px;
     } */
   }
+
+  .value-input{
+    position: relative;
+    .actions-bind{
+      display: none;
+      position: absolute;
+      right: 2px;
+      top:0px;
+    }
+    &:hover{
+      .actions-bind{
+          display: inline-block;
+        }
+      }
+  }
 }
 </style>

+ 15 - 11
src/views/components/ContextMenu.vue

@@ -35,39 +35,39 @@
         {{$t('下一个图层')}}
       </t-menu-item>
       <template v-if="!isEnvAreaUsed && selections.mode === SelectionMode.Pens">
-        <t-submenu value="1-1-10" title="画布层">
+        <t-submenu value="1-1-10" :title="$t('画布层')">
           <t-menu-item :disabled="!allImg()" value="4">
-            上层图片层
+            {{$t('上层图片层')}}
           </t-menu-item>
           <t-menu-item :disabled="hasDom()" value="3">
-            主画布层
+            {{$t('主画布层')}}
           </t-menu-item>
           <t-menu-item :disabled="!allImg()" value="2">
-            下层图片层
+            {{$t('下层图片层')}}
           </t-menu-item>
           <t-menu-item :disabled="hasDom()" value="1">
-            模板层
+            {{$t('模板层')}}
           </t-menu-item>
         </t-submenu>
       </template>
       <template  v-if="!isEnvAreaUsed && selections.mode === SelectionMode.Pen">
-        <t-submenu value="1-1-10" title="画布层">
+        <t-submenu value="1-1-10" :title="$t('画布层')">
           <t-menu-item :disabled="!isImg()" value="4">
-            上层图片层
+            {{$t('上层图片层')}}
           </t-menu-item>
           <t-menu-item value="3">
-            主画布层
+            {{$t('主画布层')}}
           </t-menu-item>
           <t-menu-item :disabled="!isImg()" value="2">
-            下层图片层
+            {{$t('下层图片层')}}
           </t-menu-item>
           <t-menu-item value="1">
-            模板层
+            {{$t('模板层')}}
           </t-menu-item>
         </t-submenu>
       </template>
       <t-divider />
-      <template v-if="!isEnvAreaUsed && selections.mode === SelectionMode.Pens">
+      <template v-if="!isEnvAreaUsed && hasPens">
         <t-menu-item value="group"> {{$t('组合')}} </t-menu-item>
         <t-menu-item value="states"> {{$t('组合为状态')}} </t-menu-item>
       </template>
@@ -277,6 +277,10 @@ const onMenu = (val: string) => {
   emit('changeVisible', false);
 };
 
+const hasPens = () => {
+  return meta2d?.store.active.length > 1;
+};
+
 const hasC =() => {
   return meta2d?.store.data.pens?.some((pen: Pen) => pen.name === "combine");
 }

+ 6 - 2
src/views/components/Custom.vue

@@ -340,8 +340,12 @@ const onConfirmDrawer = () => {
     MessagePlugin.error($t('数据不满足json格式'));
     return;
   }
-  props.pen[drawer.key] = drawer.value;
-  updatePen(toRaw(props.pen), drawer.key);
+  let pen = {
+    id: props.pen.id,
+    [drawer.key]:drawer.value,
+  }
+  // props.pen[drawer.key] = drawer.value;
+  updatePen(pen, drawer.key);
   // 选择器需要设置option,更新控件
   if(filterShapes.includes(props.pen.name)){
     props.pen.onSetOption?.(props.pen,drawer.key);

+ 226 - 96
src/views/components/DataSource.vue

@@ -9,16 +9,16 @@
         class="flex mb-16"
         style="justify-content: space-between; padding-right: 8px"
       >
-        <div style="line-height: 32px">数据列表</div>
+        <div style="line-height: 32px">{{$t('数据列表')}}</div>
         <div class="flex">
-          <t-tooltip content="批量导入数据图元到画布" placement="top">
+          <t-tooltip :content="$t('批量导入数据图元到画布')" placement="top">
             <div @click="onCheckAllChange" class="icon-box">
               <DragDropIcon  :style="{
                   color: data.checkAll ? 'var(--color-primary)' : '',
                 }"/>
             </div>
           </t-tooltip>
-          <t-tooltip content="开启全局数据模拟" placement="top">
+          <t-tooltip :content="$t('开启全局数据模拟')" placement="top">
             <div  @click="onChangeMock" class="icon-box">
               <RouterWaveIcon
                 :style="{
@@ -37,8 +37,20 @@
               <AddIcon />
             </div>
             <t-dropdown-menu>
-              <t-dropdown-item @click="onShowIot"> 物联网平台 </t-dropdown-item>
-              <t-dropdown-item @click="addSql"> SQL数据源 </t-dropdown-item>
+              <t-dropdown-item @click="onShowIot"> 
+                <div style="width:100%"> {{$t('物联网平台')}}   
+                  <t-tooltip :content="$t('需配合乐吾乐物联网平台使用此功能')" placement="right"> 
+                    <HelpCircleIcon style="font-size: 14px" class="ml-4 hover"/>
+                  </t-tooltip> 
+                </div>
+              </t-dropdown-item>
+              <t-dropdown-item @click="addSql">
+                  <div style="width:100%">{{$t('SQL数据源')}}
+                    <t-tooltip :content="$t('需配合乐吾乐物联网平台使用此功能')" placement="right"> 
+                      <HelpCircleIcon style="font-size: 14px" class="ml-4 hover"/>
+                    </t-tooltip> 
+                  </div>
+              </t-dropdown-item>
               <t-dropdown-item @click="addNetwork('mqtt')"> MQTT </t-dropdown-item>
               <t-dropdown-item @click="addNetwork('websocket')"> Websocket </t-dropdown-item>
               <t-dropdown-item @click="addNetwork('http')"> HTTP </t-dropdown-item>
@@ -66,7 +78,7 @@
             v-model="dataSearch"
             @change="onSearchData"
             @enter="onSearchData"
-            placeholder="搜索我的数据列表"
+            :placeholder="$t('搜索我的数据列表')"
           />
         </div>
       </div>
@@ -75,7 +87,7 @@
       <div class="flex mt-16 between" style="height: 32px; line-height: 32px">
         <!-- <div class="flex"> -->
           <!-- <ApplicationIcon class="tree-icon mt-8" /> -->
-          <div class="datasource-title">物联网平台</div>
+          <div class="datasource-title">{{$t('物联网平台')}}</div>
         <!-- </div> -->
         <!-- <div>
           <Edit2Icon  class="mr-12 hover" style="width: 14px;height: 14px;" @click="onShowIot" />
@@ -120,7 +132,7 @@
     <div v-if="data.sqls?.length">
       <div class="flex mt-16" style="height: 32px">
         <!-- <DataIcon class="tree-icon" /> -->
-        <div class="datasource-title">SQL数据源</div>
+        <div class="datasource-title">{{$t('SQL数据源')}}</div>
       </div>
       <div
         :draggable="data.checkAll ? true : false"
@@ -319,10 +331,10 @@
                   :divider="true"
                   @click="showAddData(node.data)"
                 >
-                  新建属性
+                  {{$t('新建属性')}}
                 </t-dropdown-item>
                 <t-dropdown-item :value="3" :divider="true">
-                  在线接口
+                  {{$t('在线接口')}}
 
                   <t-dropdown-menu
                     class="menu-item-input"
@@ -337,9 +349,9 @@
                         style="max-width: 216px !important"
                       >
                         <t-input
-                          label="地址:"
+                          :label="$t('地址:')"
                           style="width: 216px"
-                          placeholder="请输入地址"
+                          :placeholder="$t('请输入地址')"
                           v-model="data.dataset.url"
                           @blur="getDatas(node.data)"
                           @enter="getDatas(node.data)"
@@ -356,7 +368,7 @@
                   </t-dropdown-menu>
                 </t-dropdown-item>
                 <t-dropdown-item :value="2" @click="importDataset(node.data)">
-                  从Excel导入
+                  {{$t('从Excel导入')}}
                 </t-dropdown-item>
                 <t-dropdown-item :value="4">
                   <a
@@ -365,7 +377,7 @@
                     style="color: var(--td-text-color-primary)"
                     @click.stop
                   >
-                    下载Excel示例
+                    {{$t('下载Excel示例')}}
                   </a>
                 </t-dropdown-item>
               </t-dropdown-menu>
@@ -451,10 +463,10 @@
                   :divider="true"
                   @click="showAddData(node.data)"
                 >
-                  新建属性
+                  {{$t('新建属性')}}
                 </t-dropdown-item>
                 <t-dropdown-item :value="3" :divider="true">
-                  在线接口
+                  {{$t('在线接口')}}
 
                   <t-dropdown-menu
                     class="menu-item-input"
@@ -469,9 +481,9 @@
                         style="max-width: 216px !important"
                       >
                         <t-input
-                          label="地址:"
+                          :label="$t('地址:')"
                           style="width: 216px"
-                          placeholder="请输入地址"
+                          :placeholder="$t('请输入地址')"
                           v-model="data.dataset.url"
                           @blur="getDatas(node.data)"
                           @enter="getDatas(node.data)"
@@ -488,7 +500,7 @@
                   </t-dropdown-menu>
                 </t-dropdown-item>
                 <t-dropdown-item :value="2" @click="importDataset(node.data)">
-                  从Excel导入
+                    {{$t('从Excel导入')}}
                 </t-dropdown-item>
                 <t-dropdown-item :value="4">
                   <a
@@ -496,7 +508,7 @@
                     style="color: var(--td-text-color-primary)"
                     @click.stop
                   >
-                    下载Excel示例
+                    {{$t('下载Excel示例')}}
                   </a>
                 </t-dropdown-item>
               </t-dropdown-menu>
@@ -583,10 +595,10 @@
                   :divider="true"
                   @click="showAddData(node.data)"
                 >
-                  新建属性
+                  {{$t('新建属性')}}
                 </t-dropdown-item>
                 <t-dropdown-item :value="3" :divider="true">
-                  在线接口
+                  {{$t('在线接口')}}
 
                   <t-dropdown-menu
                     class="menu-item-input"
@@ -601,9 +613,9 @@
                         style="max-width: 216px !important"
                       >
                         <t-input
-                          label="地址:"
+                          :label=" $t('地址:')"
                           style="width: 216px"
-                          placeholder="请输入地址"
+                          :placeholder="$t('请输入地址')"
                           v-model="data.dataset.url"
                           @blur="getDatas(node.data)"
                           @enter="getDatas(node.data)"
@@ -620,7 +632,7 @@
                   </t-dropdown-menu>
                 </t-dropdown-item>
                 <t-dropdown-item :value="2" @click="importDataset(node.data)">
-                  从Excel导入
+                  {{$t('从Excel导入')}}
                 </t-dropdown-item>
                 <t-dropdown-item :value="4">
                   <a
@@ -628,7 +640,7 @@
                     style="color: var(--td-text-color-primary)"
                     @click.stop
                   >
-                    下载Excel示例
+                  {{$t('下载Excel示例')}}
                   </a>
                 </t-dropdown-item>
               </t-dropdown-menu>
@@ -714,10 +726,10 @@
                   :divider="true"
                   @click="showAddData(node.data)"
                 >
-                  新建属性
+                {{$t('新建属性')}}
                 </t-dropdown-item>
                 <t-dropdown-item :value="3" :divider="true">
-                  在线接口
+                  {{$t('在线接口')}}
 
                   <t-dropdown-menu
                     class="menu-item-input"
@@ -732,9 +744,9 @@
                         style="max-width: 216px !important"
                       >
                         <t-input
-                          label="地址:"
+                          :label="$t('地址:')"
                           style="width: 216px"
-                          placeholder="请输入地址"
+                          :placeholder="$t('请输入地址')"
                           v-model="data.dataset.url"
                           @blur="getDatas(node.data)"
                           @enter="getDatas(node.data)"
@@ -751,7 +763,7 @@
                   </t-dropdown-menu>
                 </t-dropdown-item>
                 <t-dropdown-item :value="2" @click="importDataset(node.data)">
-                  从Excel导入
+                  {{$t('从Excel导入')}}
                 </t-dropdown-item>
                 <t-dropdown-item :value="4">
                   <a
@@ -759,7 +771,7 @@
                     style="color: var(--td-text-color-primary)"
                     @click.stop
                   >
-                    下载Excel示例
+                  {{$t('下载Excel示例')}}
                   </a>
                 </t-dropdown-item>
               </t-dropdown-menu>
@@ -801,7 +813,7 @@
       v-if="!data.iotTree.length && !data.sqls.length && !data.networks.length"
     >
       <img src="/img/no-data.png" />
-      <div class="gray center">暂无数据</div>
+      <div class="gray center"> {{$t('暂无数据')}}</div>
       <!-- <div class="mt-20">
         <t-button theme="primary" @click="addNetwork()">
           添加数据源
@@ -811,7 +823,7 @@
   </div>
   <div class="content" v-if="group === '解析'">
     <div class="flex between">
-      <div class="title">数据{{ group }}</div>
+      <div class="title"> 数据{{ group }}</div>
     </div>
     <div class="mt-8">
       <CodeEditor
@@ -825,12 +837,12 @@
       </div>
     </div>
     <div class="mt-16">
-      参考文档:
+      {{$t('参考文档:')}}
       <a
         target="_blank"
-        href="https://doc.le5le.com/document/136233394#%E8%A7%A3%E6%9E%90%E8%87%AA%E5%AE%9A%E4%B9%89%E6%95%B0%E6%8D%AE%E6%A0%BC%E5%BC%8F"
+        href="https://doc.le5le.com/document/75"
       >
-        解析自定义格式数据</a
+        {{$t('解析自定义格式数据')}}</a
       >
     </div>
   </div>
@@ -839,7 +851,7 @@
     v-if="addDataDialog.show"
     :visible="true"
     class="data-dialog"
-    :header="addDataDialog.header"
+    :header="$t(addDataDialog.header)"
     @close="addDataDialog.show = false"
     @confirm="onOkAddData"
   >
@@ -848,28 +860,28 @@
         <t-input v-model="addDataDialog.data.device" placeholder="设备名称" />
       </div> -->
     <div class="form-item mt-16">
-      <label>显示名称</label>
+      <label>{{$t('显示名称')}}</label>
       <t-input
         @change="changeDataLabel($event)"
         :value="addDataDialog.data.label"
-        placeholder="属性简短描述"
+        :placeholder="$t('属性简短描述')"
       />
     </div>
     <div class="form-item mt-16">
-      <label>属性名</label>
+      <label>{{$t('属性名')}}</label>
       <t-input
         @change="changeDataID($event)"
         :value="addDataDialog.data.id"
-        placeholder="属性名"
+        :placeholder="$t('属性名')"
       />
     </div>
     <div class="form-item mt-16">
-      <label>类型</label>
+      <label>{{$t('类型')}}</label>
       <t-select
         class="w-full"
         :options="typeOptions"
         v-model="addDataDialog.data.type"
-        placeholder="字符串"
+        :placeholder="$t('字符串')"
         @change="addDataDialog.data.value = null"
       />
     </div>
@@ -910,7 +922,7 @@
     :visible="true"
     width="800px"
     class="data-dialog"
-    :header="networkDialog.header"
+    :header="$t(networkDialog.header)"
     @close="networkDialog.show = false"
     @confirm="onOkNetwork"
   >
@@ -919,7 +931,7 @@
         <!-- <t-checkbox v-model="networkDialog.save" class="mr-12">
           同时保存到我的数据源
         </t-checkbox> -->
-        <t-button @click="onOkNetwork">确定</t-button>
+        <t-button @click="onOkNetwork">{{$t('确定')}}</t-button>
       </div>
     </template>
     <div style="max-height: 450px; padding: 8px; overflow-y: auto">
@@ -929,19 +941,19 @@
   <t-dialog
     v-if="dataTransformationDialog.show"
     :visible="true"
-    header="数据监听"
+    :header="$t('数据监听')"
     @confirm="onOkDataTransformation"
     @close="dataTransformationDialog.show = false"
     :width="800"
   >
     <CodeEditor v-model="dataTransformationDialog.data" style="height: 300px" />
     <div class="mt-8">
-      参考文档:
+      {{$t('参考文档:')}}
       <a
         target="_blank"
-        href="https://doc.le5le.com/document/136233394#%E8%A7%A3%E6%9E%90%E8%87%AA%E5%AE%9A%E4%B9%89%E6%95%B0%E6%8D%AE%E6%A0%BC%E5%BC%8F"
+        href="https://doc.le5le.com/document/75"
       >
-        解析自定义格式数据</a
+      {{$t('解析自定义格式数据')}}</a
       >
     </div>
   </t-dialog>
@@ -950,14 +962,16 @@
     :visible="true"
     width="800px"
     class="data-dialog"
-    :header="sqlDialog.header"
+    :header="$t(sqlDialog.header)"
     @close="sqlDialog.show = false"
     @confirm="onOkSql"
+    :confirmBtn="$t('确认')"
+    :cancelBtn="$t('取消')"
   >
     <div style="max-height: 450px; padding: 8px">
       <div class="form-item mt-8">
-        <label>sql数据源</label>
-        <t-select v-model="sqlDialog.sql.dbid" placeholder="请选择数据源">
+        <label>sql {{$t('数据源')}}</label>
+        <t-select v-model="sqlDialog.sql.dbid" :placeholder="$t('请选择数据源')">
           <t-option
             v-for="sql in sqlList"
             @click="sqlChange(sql)"
@@ -968,22 +982,22 @@
         </t-select>
       </div>
       <div class="form-item mt-8">
-        <label>sql轮询间隔</label>
+        <label>sql {{$t('轮询间隔')}}</label>
         <t-input-number
           theme="column"
           v-model="sqlDialog.sql.interval"
-          placeholder="不填,仅初始执行一次"
+          :placeholder="$t('不填,仅初始执行一次')"
         />
       </div>
       <div class="form-item mt-8">
-        <label>查询方式</label>
+        <label>{{$t('查询方式')}}</label>
         <t-select v-model="sqlDialog.sql.method">
-          <t-option key="get" value="get" label="单条" />
-          <t-option key="list" value="list" label="列表" />
+          <t-option key="get" value="get" :label="$t('单条')" />
+          <t-option key="list" value="list" :label="$t('列表')" />
         </t-select>
       </div>
       <div class="form-item mt-8">
-        <label>sql语句</label>
+        <label>sql {{$t('语句')}}</label>
         <CodeEditor
           :json="false"
           :language="'sql'"
@@ -993,7 +1007,7 @@
         />
       </div>
       <div v-if="sqlDialog.sql.method === 'list'" class="form-item mt-8">
-        <label>第几页</label>
+        <label>{{$t('第几页')}}</label>
         <t-input-number
           v-model="sqlDialog.sql.current"
           theme="normal"
@@ -1002,21 +1016,21 @@
         />
       </div>
       <div v-if="sqlDialog.sql.method === 'list'" class="form-item mt-8">
-        <label>每页数量</label>
+        <label>{{$t('每页数量')}}</label>
         <t-input-number
           v-model="sqlDialog.sql.pageSize"
           theme="normal"
-          placeholder="默认20"
+          :placeholder="$t('默认20')"
           :min="1"
         />
       </div>
       <div class="form-item mt-8">
-        <label>关联属性名</label>
-        <t-input v-model="sqlDialog.sql.bindId" placeholder="关联属性名" />
+        <label>{{$t('关联属性名')}}</label>
+        <t-input v-model="sqlDialog.sql.bindId" :placeholder="$t('关联属性名')" />
       </div>
       <div class="flex mt-8">
         <!-- <label> -->
-        <t-button style="width: 75px" @click="sqlTest">连接测试</t-button>
+        <t-button style="width: 75px" @click="sqlTest">{{$t('连接测试')}}</t-button>
         <!-- </label> -->
         <p class="ml-8" style="width: 700px">{{ sqlDialog.result }}</p>
       </div>
@@ -1027,9 +1041,11 @@
     :visible="true"
     width="472px"
     dialogClassName="iot-dialog"
-    :header="iotDialog.header"
+    :header="$t(iotDialog.header)"
     @close="iotDialog.show = false"
     @confirm="onOkIot"
+    :confirmBtn="$t('确认')"
+    :cancelBtn="$t('取消')"
   >
     <!-- <t-input
       v-model="iotSearch"
@@ -1038,19 +1054,21 @@
       placeholder="设备属性搜索"
     /> -->
     <div class="input-search" style="padding: 0px 4px">
-      <div class="btn" style="left: 14px">
+      <div class="btn" style="left: 14px; top: 4px">
         <img src="/img/icon_search_gray.svg" />
       </div>
       <t-input
         style="height: 32px"
         v-model="iotSearch"
+        @focus="iotInputFocus"
         @change="onSearchIot"
         @enter="onSearchIot"
-        placeholder="搜索设备属性"
+        :placeholder="$t('搜索设备属性')"
       />
     </div>
     <div style="height: 320px; margin-top: 8px; overflow-y: scroll">
       <t-tree
+        ref="iotTreeRef"
         style="overflow-y: hidden"
         activeMultiple
         v-model="checkedIots"
@@ -1060,6 +1078,7 @@
         :checkStrictly="false"
         allow-fold-node-on-filter
         :filter="iotFilter"
+        :load="load" 
         :scroll="{
           // rowHeight: 34,
           bufferSize: 50,
@@ -1072,7 +1091,7 @@
 </template>
 
 <script lang="ts" setup>
-import { reactive, defineComponent, ref, onMounted, toRaw, watch } from 'vue';
+import { reactive, defineComponent, ref, onMounted, toRaw, watch, getCurrentInstance } from 'vue';
 import {
   FileImportIcon,
   FileExportIcon,
@@ -1118,6 +1137,9 @@ import {
   doSqlCode,
 } from '@/services/iot';
 
+const { proxy } = getCurrentInstance();
+const $t = proxy.$t;
+
 const props = defineProps<{
   group: string;
 }>();
@@ -1237,7 +1259,8 @@ const iotDialog = ref({
 
 const onOkIot = () => {
   let _iots = [];
-  iots.value.forEach((item) => {
+  let iotsValue = iotTreeRef.value.getTreeData();
+  iotsValue.forEach((item) => {
     if (checkedIots.value.includes(item.value)) {
       _iots.push(deepClone(item));
     } else {
@@ -1258,6 +1281,9 @@ const onOkIot = () => {
     }
   });
   _iots.forEach((item)=>{item.checkable = false});
+  if(data.iotTree?.length){
+    _iots = simpleMergeTwoLevelTrees(data.iotTree,_iots);
+  }
   data.iotTree = _iots;
   if (!meta2d.store.data.iot) {
     meta2d.store.data.iot = {};
@@ -1266,11 +1292,30 @@ const onOkIot = () => {
   iotDialog.value.show = false;
 };
 
+function simpleMergeTwoLevelTrees(tree1, tree2) {
+  const merged = [...tree1];
+  const idMap = new Map(tree1.map(node => [node.value, true]));
+  
+  tree2.forEach(node => {
+    if (idMap.has(node.value)) {
+      // 覆盖相同ID的父节点
+      const index = merged.findIndex(n => n.value === node.value);
+      merged[index] = { ...merged[index], ...node };
+    } else {
+      // 添加新父节点
+      merged.push(node);
+    }
+  });
+  return merged;
+}
+
 const onShowIot = async () => {
   LoadingPlugin(true);
 
   try {
-    await getIotTree();
+    // await getIotTree();
+    checkedIots.value = [];
+    await getRootTree();
     getCheckedIots();
     iotDialog.value.show = true;
   } catch (err) {
@@ -1285,6 +1330,91 @@ const onShowIot = async () => {
 };
 
 const iots = ref([]);
+const iotTreeRef = ref();
+
+const getRootTree = async () => {
+  if (iots.value.length) {
+    return;
+  }
+  let ret = await getDevices();
+  const type = ret.type;
+  const list = ret.list;
+  for (let i = 0; i < list.length; i++) {
+    const item = list[i];
+    item.label = item.name;
+    item.value = item.id;
+
+    item.deviceId = item.id; //item.id
+    item.token = item.token;
+    item._type = type;
+    item.children = true;
+    // item.checkable = false;
+    // let properties = await getDeviceProperties(item.id);
+    // item.children = properties.map((prop: any) => {
+    //   return {
+    //     label: prop.name,
+    //     value: type?prop.key:item.deviceId + '#' + prop.key,
+    //     _label: item.name + '#' + prop.name,
+    //     token: item.token,
+    //     class: 'iot',
+    //     type: prop.type
+    //   };
+    // });
+    if (!item.children?.length) {
+      item.checkable = false;
+    }
+  }
+  // setTimeout(()=>{
+  iots.value = deepClone(list);
+}
+
+const load = async (node) => {
+  let properties = await getDeviceProperties(node.data.id);
+  setTimeout(()=>{
+    getCheckedIots();
+  },100);
+  let children =  properties.map((prop: any) => {
+    return {
+      label: prop.name,
+      value: node.data._type?prop.key:node.data.deviceId + '#' + prop.key,
+      _label: node.data.name + '#' + prop.name,
+      token: node.data.token,
+      class: 'iot',
+      type: prop.type
+    };
+  });
+  iots.value.forEach((item)=>{
+    if(item.id===node.data.id){
+      item.children = children;
+    }
+  });
+  // iots.value = deepClone(iots.value);
+  return children;
+}
+
+const iotInputFocus = async ()=>{
+  let flag = false;
+  for (let i = 0; i < iots.value.length; i++) {
+    const item = iots.value[i];
+    if(!item.children?.length){
+      let properties = await getDeviceProperties(item.id);
+      item.children = properties.map((prop: any) => {
+        return {
+          label: prop.name,
+          value: item._type?prop.key:item.deviceId + '#' + prop.key,
+          _label: item.name + '#' + prop.name,
+          token: item.token,
+          class: 'iot',
+          type: prop.type
+        };
+      }); 
+      flag = true;
+    }
+  }
+  if(flag){
+    iots.value = deepClone(iots.value);
+  }
+}
 
 const getIotTree = async () => {
   if (iots.value.length) {
@@ -1327,7 +1457,7 @@ const getCheckedIots = ()=>{
   meta2d.store.data?.iot?.tree.forEach((item)=>{
     arr.push(...item.children?.map((_item)=>_item.value));
   });
-  checkedIots.value = arr;
+  checkedIots.value = [...new Set([...checkedIots.value, ...arr])];
 }
 
 const iotSearch = ref('');
@@ -1396,7 +1526,7 @@ const selectIot = (protocol: any) => {
 
 const addSql = async () => {
   sqlList.value = await getSqlSourceList();
-  sqlDialog.header = '添加sql数据源';
+  sqlDialog.header = $t('添加sql数据源');
   sqlDialog.show = true;
   sqlDialog.edit = false;
   sqlDialog.sql = {
@@ -1423,18 +1553,18 @@ const deleteSql = (index: number) => {
 const sqlTreeKey = ref(s8());
 const onOkSql = async () => {
   if (!sqlDialog.sql.dbid) {
-    MessagePlugin.error('请选择数据源');
+    MessagePlugin.error($t('请选择数据源'));
     return;
   }
   if (!sqlDialog.sql.sql) {
-    MessagePlugin.error('请填写sql语句');
+    MessagePlugin.error($t('请填写sql语句'));
     return;
   }
   if (!sqlDialog.sql.interval) {
     sqlDialog.sql.interval = undefined;
   }
   if (!sqlDialog.sql.bindId) {
-    MessagePlugin.error('关联属性名必填');
+    MessagePlugin.error($t('关联属性名必填'));
     return;
   }
   sqlDialog.sql.label = sqlDialog.sql.bindId;
@@ -1462,7 +1592,7 @@ const sqlDialog = reactive<any>({
   show: false,
   edit: false,
   index: -1,
-  header: '添加sql数据源',
+  header: $t('添加sql数据源'),
   sql: {
     interval: undefined,
     sql: '',
@@ -1479,11 +1609,11 @@ const sqlChange = (sql: any) => {
 const sqlTest = async () => {
   let ret: any = await doSqlCode(sqlDialog.sql);
   if (ret.error) {
-    MessagePlugin.error('连接错误:' + ret.error);
-    sqlDialog.result = '连接错误:' + ret.error;
+    MessagePlugin.error($t('连接错误:') + ret.error);
+    sqlDialog.result = $t('连接错误:') + ret.error;
   } else {
     if (sqlDialog.sql.method === 'list') {
-      sqlDialog.result = '连接成功:[' + JSON.stringify(ret[0]) + ',...]';
+      sqlDialog.result = $t('连接成功:[') + JSON.stringify(ret[0]) + ',...]';
       // sqlDialog.sql.columns = ret[0];
       const columnsKeys = Object.keys(ret[0]);
       const children = new Array(ret.length).fill(0).map((item, index) => {
@@ -1505,7 +1635,7 @@ const sqlTest = async () => {
       sqlDialog.sql.class = 'sql';
       sqlDialog.sql.children = children;
     } else {
-      sqlDialog.result = '连接成功:' + JSON.stringify(ret);
+      sqlDialog.result = $t('连接成功:') + JSON.stringify(ret);
       // sqlDialog.sql.columns = ret;
       const children = [];
       for (let key in ret) {
@@ -1560,12 +1690,12 @@ const clearData = () => {
 
 const changeDataLabel = (value) => {
   if (!value) {
-    MessagePlugin.error('显示名称不能为空!');
+    MessagePlugin.error($t('显示名称不能为空!'));
     return;
   }
   let item = data.dataset.devices?.filter((item) => item.label === value);
   if (item && item.length) {
-    MessagePlugin.error('显示名称重复!');
+    MessagePlugin.error($t('显示名称重复!'));
     return;
   }
   addDataDialog.data.label = value;
@@ -1573,12 +1703,12 @@ const changeDataLabel = (value) => {
 
 const changeDataID = (value) => {
   if (!value) {
-    MessagePlugin.error('属性名不能为空!');
+    MessagePlugin.error($t('属性名不能为空!'));
     return;
   }
   let item = data.dataset.devices?.filter((item) => item.id === value);
   if (item && item.length) {
-    MessagePlugin.error('属性名重复!');
+    MessagePlugin.error($t('属性名重复!'));
     return;
   }
   addDataDialog.data.id = value;
@@ -1586,11 +1716,11 @@ const changeDataID = (value) => {
 
 const onOkAddData = () => {
   if (!addDataDialog.data.label) {
-    MessagePlugin.error('请填写名称');
+    MessagePlugin.error($t('请填写名称'));
     return;
   }
   if (!addDataDialog.data.id) {
-    MessagePlugin.error('请填写数据ID');
+    MessagePlugin.error($t('请填写数据ID'));
     return;
   }
   if (!addDataDialog.network.children) {
@@ -1697,7 +1827,7 @@ const importDataset = async (network) => {
 
 const downloadAsExcel = () => {
   if (!(data.dataset.devices && data.dataset.devices.length)) {
-    MessagePlugin.error('属性列表不能为空!');
+    MessagePlugin.error($t('属性列表不能为空!'));
     return;
   }
   const name = meta2d.store.data.name;
@@ -1724,7 +1854,7 @@ const downloadAsExcel = () => {
 
 const downloadAsJson = () => {
   if (!(data.dataset.devices && data.dataset.devices.length)) {
-    MessagePlugin.error('属性列表不能为空!');
+    MessagePlugin.error($t('属性列表不能为空!'));
     return;
   }
   import('file-saver').then(({ saveAs }) => {
@@ -1743,7 +1873,7 @@ const onOkDataset = async (saveas = false) => {
   //   return;
   // }
   if (!(data.dataset.devices && data.dataset.devices.length)) {
-    MessagePlugin.error('属性列表不能为空');
+    MessagePlugin.error($t('属性列表不能为空'));
     return;
   }
   const dataset = JSON.parse(JSON.stringify(data.dataset));
@@ -1912,7 +2042,7 @@ const onSelDataset = async (datasetId = false) => {
 // 请求我的数据模型
 const getDatasets = async (name?: string) => {
   if (!user.id) {
-    MessagePlugin.error('请先登录');
+    MessagePlugin.error($t('请先登录'));
     return;
   }
   const body: any = {
@@ -2064,7 +2194,7 @@ const editNetwork = (network: any, index: number) => {
   networkDialog.network = JSON.parse(JSON.stringify(data.networks[index]));
   networkDialog.editNetwork = 2;
   networkDialog.editNetworkIndex = index;
-  networkDialog.header = `编辑${networkDialog.network.protocol}数据源`;
+  networkDialog.header = `${$t('编辑')} ${networkDialog.network.protocol}${$t('数据源')}`;
   networkDialog.show = true;
 };
 
@@ -2092,7 +2222,7 @@ const addNetwork = (protocol:string) => {
     networkDialog.network.options = {protocols:''}
   }
   networkDialog.editNetwork = 1;
-  networkDialog.header = `添加${protocol}数据源`;
+  networkDialog.header = `${$t('添加')} ${protocol} ${$t('数据源')}`;
   networkDialog.show = true;
 };
 
@@ -2107,11 +2237,11 @@ const onOkNetwork = async () => {
       ['mqtt', 'websocket', 'http'].includes(networkDialog.network.protocol) &&
       !networkDialog.network.url
     ) {
-      MessagePlugin.error('URL地址不能为空!');
+      MessagePlugin.error($t('URL地址不能为空!'));
       return;
     }
     if (!networkDialog.network.name) {
-      MessagePlugin.error('名称不能为空!');
+      MessagePlugin.error($t('名称不能为空!'));
       return;
     }
     // if (networkDialog.save) {
@@ -2252,7 +2382,7 @@ const onAddShape = (e, _data,type) => {
     }
     // const checked = _data.filter((item) => item.checked);
     if (!checked.length) {
-      MessagePlugin.error('请先选择数据');
+      MessagePlugin.error($t('请先选择数据'));
       return;
     }
     data = [];

+ 83 - 2
src/views/components/FileProps.vue

@@ -47,13 +47,75 @@
             <label>{{$t('背景图片')}}</label>
             <t-upload class="ml-8" v-model="data.background" :action="`${axios.defaults.baseURL}/api/image/upload`" theme="image" accept="image/*" :headers="headers" :data="updataData" :auto-upload="true" :before-upload="beforeUpload" :upload-all-files-in-one-request="false" @success="fileSuccessed" @remove="fileRemoved"></t-upload>
           </div>
+          <div class="form-item">
+            <label>{{$t('背景网格')}}</label>
+            <div class="flex ml-8" style="align-items:center">
+              <t-tooltip
+                :content="$t('开启网格')"
+                placement="top"
+              >
+                <t-checkbox
+                  v-model="data.grid"
+                  @change="changeGrid($event, 'grid')"
+                  style="width: 22px"
+                />
+              </t-tooltip>
+              <t-tooltip
+               v-if="data.grid"
+                :content="$t('网格自动对齐')"
+                placement="top"
+              >
+                <t-checkbox
+                  v-model="data.autoAlignGrid"
+                  @change="changeGrid($event, 'autoAlignGrid')"
+                  style="width: 22px"
+                />
+              </t-tooltip>
+              <t-tooltip
+                v-if="data.grid"
+                :content="$t('网格颜色')"
+                placement="top"
+              >
+                <t-color-picker
+                  class="simple ml-8"
+                  format="CSS"
+                  :recent-colors="null"
+                  :enable-alpha="true"
+                  :swatch-colors="defaultPureColor"
+                  :color-modes="['monochrome']"
+                  :show-primary-color-preview="false"
+                  v-model="data.gridColor"
+                  @change="changeGrid($event, 'gridColor')"
+                />
+              </t-tooltip>
+              <t-tooltip
+                v-if="data.grid"
+                :content="$t('网格大小')"
+                placement="top"
+              >
+                <t-input-number
+                  theme="normal"
+                  v-model="data.gridSize"
+                  :min="5"
+                  :decimalPlaces="0"
+                  @change="changeGrid($event,'gridSize')"
+                  class="ml-8"
+                  style="width: 100px"
+                />
+            </t-tooltip>
+            </div>
+          </div>
           <div class="form-item">
             <label>{{$t('主题')}}</label>
             <t-select v-model="data.theme" :options="themeOptions" @change="changeTheme" clearable class="shrink-0" :placeholder="$t('暗黑')"></t-select>
           </div>
         </t-space>
-        <t-space direction="vertical" size="small" class="mt-8">
-          <t-collapse :defaultValue="['1', '2','3']" expandIconPlacement="right" :borderless="true">
+        <t-space direction="vertical" size="small" class="mt-8" style="width: 100%">
+          <t-collapse
+            :defaultValue="['1', '2','3']"
+            expandIconPlacement="right"
+            :borderless="true"
+          >
             <t-collapse-panel value="1" :header="$t('预览设置')">
               <t-space direction="vertical" size="small">
                 <div class="form-item">
@@ -422,6 +484,19 @@ const changeValue = (e: any, key: string) => {
   openData();
 };
 
+const changeGrid = (e: any, key: string)=>{
+  let opt = {[key]: e}
+  if(key === 'gridSize') {
+    if(e <= 5) return;
+  }
+  if(key==='grid'&&e===true){
+    opt.gridColor = '#303746';
+    data.gridColor = '#303746';
+  }
+  meta2d.setOptions(opt);
+  meta2d.render();
+}
+
 const changeTheme = (e)=>{
   meta2d.setTheme(e);
   data.meta2dData.background = meta2d.store.data.background;
@@ -481,6 +556,12 @@ function openData() {
       },
     ];
   }
+  if(meta2d?.store?.options?.grid){
+    data.grid = meta2d.store.options.grid;
+    data.gridSize = meta2d.store.options?.gridSize;
+    data.gridColor = meta2d.store.options?.gridColor;
+    data.autoAlignGrid = meta2d.store.options?.autoAlignGrid;
+  }
 }
 function finishLine(e) {
   setTimeout(()=>{

+ 12 - 8
src/views/components/FitProps.vue

@@ -166,10 +166,12 @@ const fitPenVisible = (e) => {
   if (e) {
     meta2d.store.data.pens.forEach((pen) => {
       if (!selections.fit.children?.includes(pen.id)) {
-        meta2d.setValue(
-          { id: pen.id, visible: false },
-          { render: false, doEvent: false, history: false }
-        );
+        if(!pen.parentId){
+          meta2d.setValue(
+            { id: pen.id, visible: false },
+            { render: false, doEvent: false, history: false }
+          );
+        }
       }
     });
     meta2d.canvas.hideFit();
@@ -180,10 +182,12 @@ const fitPenVisible = (e) => {
   } else {
     meta2d.store.data.pens.forEach((pen) => {
       if (!selections.fit.children?.includes(pen.id)) {
-        meta2d.setValue(
-          { id: pen.id, visible: true },
-          { render: false, doEvent: false, history: false }
-        );
+        if(!pen.parentId){
+          meta2d.setValue(
+            { id: pen.id, visible: true },
+            { render: false, doEvent: false, history: false }
+          );
+        }
       }
     });
     meta2d.canvas.showFit();

+ 41 - 9
src/views/components/Graphics.vue

@@ -166,7 +166,7 @@
 
               <template v-for="elem in item.list">
                 <div
-                  v-show="elem.visible !== false"
+                  v-if="elem.visible !== false"
                   class="graphic"
                   :draggable="true"
                   @dragstart="dragStart($event, elem)"
@@ -188,7 +188,7 @@
                   <svg v-else class="l-icon" aria-hidden="true">
                     <use :xlink:href="'#' + elem.icon"></use>
                   </svg>
-                  <p :title="elem.name">{{ elem.name }}</p>
+                  <p :title="elem.name">{{ $t(elem.name) }}</p>
                   <div class="price" v-if="elem.price > 0">
                     ¥{{ elem.price }}
                   </div>
@@ -451,15 +451,17 @@ import DataSource from './DataSource.vue';
 import Structure from './Structure.vue';
 import { useDot } from '@/services/common';
 import { getToken  } from '@le5le/auth-token';
+import { activedGroup, activeAssets, useProject } from '@/services/project';
 
+const { data:projectData } = useProject();
 const { dot } = useDot();
 const { user } = useUser();
 const { setFolder, getFolder } = useFolder();
 const router = useRouter();
 const route = useRoute();
 const { select } = useSelection();
-const activedGroup = ref('');
-const activeAssets = ref('system');
+// const activedGroup = ref('');
+// const activeAssets = ref('system');
 let groups = reactive([]);
 let moveGroups = reactive<any>({
   '方案':[],
@@ -658,7 +660,15 @@ const assetsChange = (value) => {
       activedGroup.value = '数据';
     }
   }else if(value === 'structure'){
-    groups = structureGroups;
+    groups = deepClone(structureGroups);
+    if(projectData.tree?.length){
+      groups.unshift(
+        {
+        icon: 'layers',
+        name: '工程',
+        key: '',
+      });
+    }
     activedGroup.value = '图层';
   }
   groupChange(activedGroup.value);
@@ -846,6 +856,13 @@ const groupChange = async (name: string) => {
 
   searchGraphics();
 };
+
+defineExpose({
+  assetsChange,
+  groupChange
+})
+
+
 // TODO 获取方案文件
 //获取方案文件夹
 const getCollectionImageList = async (name?: string, collection?: string, system?:boolean, template?:boolean) => {
@@ -1272,10 +1289,12 @@ const dragStart = async (event: DragEvent | MouseEvent|TouchEvent, item: any) =>
     item.draggable = false;
     data = item.data || item;
     dropped =true;
+    open(item);
   } else if (item.draggable === false) {
     //方案
     data = item.data || item;
     dropped =true;
+    open(item);
   } else if (item['3d']) {
     const res: any = await getLe5le3d(item.id || item._id, "id, name, image");
     let iframe = (location.host.indexOf('.le5le')!==-1? 'https://view.le5le.com/3d?id=' :(location.origin+'/view/3d/?id=')) + (item.id || item._id)
@@ -1364,8 +1383,8 @@ const dragStart = async (event: DragEvent | MouseEvent|TouchEvent, item: any) =>
 
     data = {
       name,
-      width,
-      height,
+      width: 100,
+      height: 100 * (height / width),
       isBottom:true,
       image: getImgUrl(item.image),
       imageRatio: true,
@@ -2200,6 +2219,10 @@ const searchGraphics = async () => {
       0,
       activedPanels[activedGroup.value].length
     );
+  }else{
+    if(activedGroup.value === '设备'){
+      activedPanels[activedGroup.value] = ['2.5D-光伏系统'];
+    }
   }
 
   for (const group of subGroups.value) {
@@ -2227,6 +2250,15 @@ const searchGraphics = async () => {
     }else{
       group.visible = true;
     }
+    if(search.value&&flag){
+      //如果该组没有搜索到任何内容,则判断是否显示该组
+      if(group.name.includes(search.value)){
+        group.visible = true;
+        group.list.forEach((item: any) => {
+          item.visible = true;
+        });
+      }
+    }
     if (search.value) {
       activedPanels[activedGroup.value].push(group.name);
     }
@@ -2454,7 +2486,7 @@ onMounted(() => {
   document.addEventListener('dragend', dragend, false);
 
   setTimeout(() => {
-    meta2d.on('drop', drop);
+    // meta2d.on('drop', drop);
     meta2d.on('logout', reloadCurrent);
     meta2d.on('business-save', updateAfterSave);
     meta2d.on('business-assets', changeAssets);
@@ -2504,7 +2536,7 @@ onUnmounted(() => {
   document.removeEventListener('dragstart', dragstart);
   document.removeEventListener('dragend', dragend);
 
-  meta2d.off('drop', drop);
+  // meta2d.off('drop', drop);
   meta2d.off('logout', reloadCurrent);
   meta2d.off('business-save', updateAfterSave);
   meta2d.off('business-assets', changeAssets);

+ 82 - 17
src/views/components/Header.vue

@@ -1,12 +1,22 @@
 <template>
   <div class="app-header">
     <!-- <a class="logo" :href="enterprise.home" target="_blank">
-      <img src="/favicon.ico">
+      <img src="/img/logo.png" />
       <span> {{ $t(enterprise.name) }}</span>
     </a> -->
     <t-dropdown :minColumnWidth="200" :maxHeight="560" :delay2="[10, 150]" overlayClassName="header-dropdown" trigger1="click">
       <a> {{$t('文件')}} </a>
       <t-dropdown-menu>
+        <!-- <t-dropdown-item @click="newProject">
+          <a>{{$t('新建工程')}}</a>
+        </t-dropdown-item>
+        <t-dropdown-item @click="loadProject" divider="true">
+          <a>
+            <div class="flex">
+              {{$t('导入工程')}} <span class="flex-grow"></span>
+            </div>
+          </a>
+        </t-dropdown-item> -->
         <t-dropdown-item v-if="isBigScreen" @click="newFile">
           <a>{{$t('新建文件')}}</a>
         </t-dropdown-item>
@@ -228,7 +238,7 @@
           v-for="item in enterprise.helps_v"
           :divider="item.divider"
         >
-          <a :href="item.url" target="_blank">{{ item.name }}</a>
+          <a :href="item.url" target="_blank">{{ $t(item.name) }}</a>
         </t-dropdown-item>
       </t-dropdown-menu>
     </t-dropdown>
@@ -496,6 +506,31 @@
   <t-dialog v-model:visible="newFileDialog.show" @close="newFileDialog.show = false" @confirm="saveNewfile" @cancel="nosaveNewfile" :header="$t('提示')" :confirm-btn="$t('保存后新建')" :cancel-btn="$t('不保存直接新建')">
     {{$t('当前画布数据未保存')}},{{$t('是否保存后新建')}}?
   </t-dialog>
+  <t-dialog
+     v-model:visible="progress.show"
+     header="下载进度"
+     :footer="null"
+   >
+     <div class="flex" style="height: 40px;align-items: center;">
+       <t-progress 
+         style="width:360px"  
+         theme="line"
+         :color="{ from: '#0052D9', to: '#00A870' }"
+         :percentage="(progress.current/progress.total*100).toFixed(2)" 
+         :status ="progress.status"
+         />
+       <div class="progress-text ml-8">
+         {{ progress.current }}/{{ progress.total }}
+       </div>  
+     </div>
+   </t-dialog>
+  <StepModal
+    v-model:visible="taskDialog.visible"
+    :tasks="taskDialog.tasks"
+    :reload="taskDialog.reload"
+    @cancel="setTask('cancel', true)"
+    @reDownload="reDownload"
+  />
 </template>
 
 <script lang="ts" setup>
@@ -582,6 +617,8 @@ import {getNetJsDiagram} from '@/services/material';
 import { useMeta2dData, useDAnchor } from '@/services/common';
 import { upload } from '@/services/file';
 import { load3d } from '@/services/load3d';
+import { newProject, useTask, loadProject, reDownload} from '@/services/project';
+import StepModal from './common/StepModal.vue';
 
 const {anchorShow,setDAnchor } = useDAnchor();
 const { enterprise } = useEnterprise();
@@ -958,13 +995,17 @@ const openSvg = async (file: File) => {
   const text = await readFile(file);
   const pens: Pen[] = parseSvg(text);
   if(pens.length){
-    if(pens[0].width>100||pens[0].height>100){
-      if(pens[0].width>pens[0].height){
-        pens[0].height = pens[0].height/pens[0].width*100;
-        pens[0].width = 100;
-      }else{
-        pens[0].width = pens[0].width/pens[0].height*100;
-        pens[0].height = 100;
+    if(pens.length>100){
+      //认为是大图
+    }else{
+      if(pens[0].width>100||pens[0].height>100){
+        if(pens[0].width>pens[0].height){
+          pens[0].height = pens[0].height/pens[0].width*100;
+          pens[0].width = 100;
+        }else{
+          pens[0].width = pens[0].width/pens[0].height*100;
+          pens[0].height = 100;
+        }
       }
     }
   }
@@ -998,6 +1039,7 @@ const openZip = async (file: File) => {
       continue;
     }
     if (key.endsWith('.json')) {
+      fileName = key.split('/')[0];
       // 认为只有一个 json 文件
       dataStr = await zip.file(key).async('string');
       break;
@@ -1080,7 +1122,7 @@ const openZip = async (file: File) => {
       if(pen.name==='iframe'){
         let url = pen.iframe.split('?')[1]
         let params = queryURLParams(url);
-        if(pen.iframe.indexOf('/2d/')!==-1 || pen.iframe.indexOf('/v/')!==-1 || pen.iframe.indexOf('/2d?')!==-1 ||pen.iframe.indexOf('/v?')!==-1){
+        if(pen.iframe.indexOf('/2d/')!==-1 || pen.iframe.indexOf('/v/')!==-1 || pen.iframe.indexOf('/2d?')!==-1 ||pen.iframe.indexOf('/v?')!==-1||pen.iframe.indexOf('/preview')!==-1){
           const idata = JSON.parse(_2dDataMap[params.id]);
           const ret:any = await addCollection('v',{
             data:idata,
@@ -1845,7 +1887,7 @@ const preDownload =() => {
   iframeNum = 0;
   compareNum = 0;
   const meta2dData:any = meta2d.data();
-  meta2dData.userId = user.id;
+  // meta2dData.userId = user.id;
   const pen_3d = meta2dData.pens.filter(
     (pen) =>
       pen.name === 'iframe' &&
@@ -1982,7 +2024,7 @@ const preFrameDownload = async () => {
   iframeNum = 0;
   compareNum = 0;
   const meta2dData:any = meta2d.data();
-  meta2dData.userId = user.id;
+  // meta2dData.userId = user.id;
   const pen_3d = meta2dData.pens.filter(
     (pen) =>
       pen.name === 'iframe' &&
@@ -2117,8 +2159,20 @@ const preFrameDownload = async () => {
   }
 }
 
+const progress = reactive({
+   show: false,
+   total: 0,
+   current:0,
+   status:''
+ });
+
 const saveDownload = async () => {
   const list = [...downloadList];
+
+  progress.total = list.length+1;
+  progress.current = 0;
+  progress.show = true;
+  progress.status = 'active';
   //控件
   let jsPath= '';
   // let jsPensPath = '';
@@ -2140,10 +2194,10 @@ const saveDownload = async () => {
       // jsPensPath = `/meta2d-react/public/js/1.js`;
       break;
   }
-  // list.push({
-  //   url: '/view/js/2d-components.js', //需要购买
-  //   path: jsPath,
-  // });
+  list.push({
+    url: '/view/js/2d-components.js', //需要购买
+    path: jsPath,
+  });
   // const js = await get2dComponentJs([...prePayList.iotPens]);
   // list.push({
   //   data: js,
@@ -2246,7 +2300,8 @@ const saveDownload = async () => {
   //     });
   //   }
   // }
-
+  progress.total = list.length;
+  progress.current = 0;
   //开始下载list
   const [{ default: JSZip }, { saveAs }] = await Promise.all([
     import('jszip'),
@@ -2278,16 +2333,23 @@ const saveDownload = async () => {
         }
         zip.file(path, item.data, { createFolders: true });
       }
+      progress.current += 1;
     })
   );
+  // progress.status = 'success';
   let _fileName =
     (meta2d.store.data.name &&
       meta2d.store.data.name.replace(/\//g, '_').replace(/:/g, '_')) ||
     'unimat.iot';
   const blob = await zip.generateAsync({ type: 'blob' });
   saveAs(blob, `${_fileName}.zip`);
+  progress.current += 1;
+  progress.status = 'success';
   MessagePlugin.closeAll();
   MessagePlugin.success($t('下载成功,请在浏览器下载列表中查看'));
+  setTimeout(()=>{
+     progress.show = false;
+  },1000);
 };
 
 // const _downloadHtml = async () => {
@@ -3050,6 +3112,9 @@ const nosaveNewfile = ()=>{
 //   }
 // };
 
+const { taskDialog, setTask } = useTask();
+
+
 </script>
 <style lang="postcss" scoped>
 .app-header {

+ 36 - 23
src/views/components/Net.vue

@@ -2,13 +2,13 @@
   <div class="network-component">
     <div class="form-item mt-8">
       <label>
-        数据源名称
+        {{$t('数据源名称')}}
       </label>
       <t-select-input
         v-if="mode"
         v-model:inputValue="modelValue.name"
         :value="modelValue.name"
-        placeholder="我的数据发送"
+        :placeholder="$t('我的数据发送')"
         allow-input
         clearable
         v-model:popup-visible="popupVisible"
@@ -29,8 +29,8 @@
               :key="item.url"
               @click="() => onSelect(item)"
             >
-              名称: {{ item.name }}
-              <div class="desc">地址: {{ item.url }}</div>
+            {{$t('名称:')}} {{ item.name }}
+              <div class="desc">{{$t('地址:')}}' {{ item.url }}</div>
 
               <span class="del" @click.stop="onDelNetWork(item, i)">
                 <delete-icon />
@@ -49,12 +49,12 @@
               style="line-height: 1.5; padding: 8px; border-radius: 2px"
               :key="-1"
             >
-              <div class="desc">暂无数据</div>
+              <div class="desc">{{$t('暂无数据')}}</div>
             </li>
           </ul>
         </template>
       </t-select-input>
-      <t-input v-else v-model="modelValue.name" placeholder="名称" />
+      <t-input v-else v-model="modelValue.name" :placeholder="$t('名称')" />
     </div>
 
     <!-- <div class="form-item mt-8">
@@ -70,35 +70,39 @@
       </t-select>
     </div> -->
     <div class="form-item mt-8">
-      <label>URL地址</label>
+      <label>{{$t('URL地址')}}</label>
       <t-input
         :format="urlFormat"
         :placeholder="
           (modelValue.protocol !== 'http'&&modelValue.protocol !== 'SSE')
             ? isSafeProtocol()
-              ? '必须是wss协议'
-              : '必须是ws协议'
-            : '请输入'
+              ? $t('必须是wss协议')
+              : $t('必须是ws协议')
+            : $t('请输入')
         "
         v-model="modelValue.url"
       />
     </div>
     <template v-if="modelValue.protocol === 'websocket'">
       <div class="form-item mt-8">
-        <label>protocols</label>
+        <label>protocols
+          <t-tooltip content="一个协议字符串或者一个包含协议字符串的数组。这些字符串用于指定子协议,这样单个服务器可以实现多个 WebSocket 子协议(例如,你可能希望一台服务器能够根据指定的协议(protocol)处理不同类型的交互)。如果不指定协议字符串,则假定为空字符串。" placement="top">
+              <HelpCircleIcon style="font-size: 12px" class="ml-2"/>
+          </t-tooltip>
+        </label>
         <t-input v-model="modelValue.options.protocols" />
       </div>
     </template>
     <template v-else-if="modelValue.protocol === 'http'">
       <div class="form-item mt-8">
-        <label>请求方式</label>
+        <label>{{$t('请求方式')}}</label>
         <t-select v-model="modelValue.method" @change="httpMethodChange">
           <t-option key="GET" value="GET" label="GET" />
           <t-option key="POST" value="POST" label="POST" />
         </t-select>
       </div>
       <div v-if="modelValue.type === 'subscribe'" class="form-item mt-8">
-        <label>请求间隔</label>
+        <label>{{$t('请求间隔')}}</label>
         <t-input-number
           theme="column"
           v-model="modelValue.interval"
@@ -106,7 +110,7 @@
         />
       </div>
       <div class="form-item mt-8">
-        <label>请求头</label>
+        <label>{{$t('请求头')}}</label>
         <!-- <t-textarea
           v-model="modelValue.headers"
           :autosize="{ minRows: 3, maxRows: 5 }"
@@ -122,10 +126,10 @@
       </div>
       <div class="form-item mt-8 desc">
         <label></label>
-        支持设置动态参数,例如:{"Authorization": "Bearer ${token}"}
+        {{$t('支持设置动态参数,例如:')}}{"Authorization": "Bearer ${token}"}
       </div>
       <div v-if="!mode && modelValue.method === 'POST'" class="form-item mt-8">
-        <label>请求体</label>
+        <label>{{$t('请求体')}}</label>
         <!-- <t-textarea
           v-model="modelValue.body"
           :autosize="{ minRows: 3, maxRows: 5 }"
@@ -144,12 +148,12 @@
         class="form-item mt-8 desc"
       >
         <label></label>
-        支持设置动态参数,例如:{"value": "${value}"}
+        {{$t('支持设置动态参数,例如:')}}{"value": "${value}"}
       </div>
     </template>
     <template v-else-if="modelValue.protocol === 'SSE'">
       <div class="form-item mt-8">
-        <label>跨域凭据</label>
+        <label>{{$t('跨域凭据')}}</label>
         <t-switch
           class="mt-8 ml-8"
           v-model="modelValue.withCredentials"
@@ -163,7 +167,11 @@
         <t-input v-model="modelValue.options.clientId" />
       </div>
       <div class="form-item mt-8">
-        <label>自动生成</label>
+        <label>{{$t('关闭自动生成')}}
+          <t-tooltip  :content="$t('是否关闭Client Id自动生成')" placement="top">
+              <HelpCircleIcon style="font-size: 12px" class="ml-2"/>
+          </t-tooltip>
+        </label>
         <t-switch
           class="mt-8 ml-8"
           v-model="modelValue.options.customClientId"
@@ -171,11 +179,11 @@
         />
       </div>
       <div class="form-item mt-8">
-        <label>用户名</label>
+        <label>{{$t('用户名')}}</label>
         <t-input v-model="modelValue.options.username" />
       </div>
       <div class="form-item mt-8">
-        <label>密码</label>
+        <label>{{$t('密码')}}</label>
         <t-input v-model="modelValue.options.password" />
       </div>
       <div class="form-item mt-8">
@@ -186,7 +194,7 @@
     <div class="form-item mt-8" v-if="mode">
       <label> </label>
       <div>
-        <t-button @click="onSave">保存到我的数据发送</t-button>
+        <t-button @click="onSave">{{$t('保存到我的数据发送')}}</t-button>
       </div>
     </div>
   </div>
@@ -198,7 +206,7 @@ import axios from 'axios';
 import { debounce } from '@/services/debouce';
 import { MessagePlugin } from 'tdesign-vue-next';
 import CodeEditor from '@/views/components/common/CodeEditor.vue';
-import { DeleteIcon } from 'tdesign-icons-vue-next';
+import { DeleteIcon, HelpCircleIcon } from 'tdesign-icons-vue-next';
 import { transformData } from '@/services/utils';
 
 const { modelValue, mode } = defineProps<{
@@ -327,6 +335,11 @@ const urlFormat = (val) => {
 };
 </script>
 <style lang="postcss" scoped>
+.form-item{
+  label{
+    width: 100px;
+  }
+}
 .network-component {
 }
 </style>

+ 3 - 3
src/views/components/Network.vue

@@ -77,7 +77,7 @@
       </t-select>
     </div> -->
     <div v-if="!modelValue.unmodifiable"  class="form-item mt-8">
-      <label>URL地址</label>
+      <label>{{$t('URL地址')}}</label>
       <t-input
         :format="urlFormat"
         :placeholder="
@@ -96,7 +96,7 @@
     </template>
     <template v-else-if="modelValue.protocol === 'http'">
       <div class="form-item mt-8">
-        <label>请求方式</label>
+        <label>{{$t('请求方式')}}</label>
         <t-select :disabled="modelValue.unmodifiable" v-model="modelValue.method" @change="httpMethodChange">
           <t-option key="GET" value="GET" label="GET" />
           <t-option key="POST" value="POST" label="POST" />
@@ -276,7 +276,7 @@ const hasIot = ref(false);
 // 请求我的数据源接口
 const getNetworks = async () => {
   let arr = [];
-  meta2d.store.data.networks.forEach((network)=>{
+  meta2d.store.data.networks?.forEach((network)=>{
      if(['mqtt','websocket','http'].includes(network.protocol)&&network.method!=="POST"){
         let net:any = deepClone(network);
         if(net.protocol === 'http'){

+ 467 - 6
src/views/components/PenAnimates.vue

@@ -72,22 +72,216 @@
                 <t-option :key="0" :value="0" :label="$t('水流')"></t-option>
                 <t-option :key="1" :value="1" :label="$t('水珠')"></t-option>
                 <t-option :key="2" :value="2" :label="$t('圆点')"></t-option>
+                <t-option :key="5" :value="5" :label="$t('自定义轨迹动画')"></t-option>
                 <t-option v-if="['polyline','line'].includes(props.pen.lineName)" :key="3" :value="3" :label="$t('箭头')"></t-option>
                 <t-option v-if="['polyline','line'].includes(props.pen.lineName)" :key="4" :value="4" :label="$t('水滴')"></t-option>
               </t-select>
             </div>
+            <section v-if="item.lineAnimateType===5">
+              <div class="form-item mt-8">
+                <label>{{$t('图形类型')}}</label>
+                <t-tabs v-model="item.lineAnimateTargetType">
+                  <t-tab-panel :value="0" :label="$t('图片')">
+                    <div class="mt-8">
+                      <t-upload
+                          class="ml-8"
+                          :showImageFileName="false"
+                          action="/api/image/upload"
+                          theme="image"
+                          accept="image/*"
+                          :headers="headers"
+                          :data="updataData"
+                          @success="fileSuccessed($event,item,i)"
+                          @remove="fileRemoved(item,i)"
+                          v-model="imagesRef"
+                          :auto-upload="true"
+                          :upload-all-files-in-one-request="false"
+                      />
+                    </div>
 
+                  </t-tab-panel>
+                  <t-tab-panel :value="1" :label="$t('图标')">
+                    <div class="mt-8">
+                    <t-color-picker
+                        class="w-full"
+                        format="CSS"
+                        :enable-alpha="true"
+                        :recent-colors="null"
+                        :color-modes="['monochrome']"
+                        :show-primary-color-preview="false"
+                        :clearable="true"
+                        v-model="item.iconColor"
+                        @change="changeValue(i)"
+                    />
+                    <i
+                        class="ml-8"
+                        :class="pen.iconFamily"
+                        style="line-height: 30px; height: 30px; color: var(--color)"
+                    >
+                      {{ pen.lineAnimateIcon }}
+                    </i>
+                    <a class="ml-12 mt-4" @click="showIconDrawer = true">
+                      {{$t('选择')}}
+                    </a>
+                    <t-drawer
+                        v-model:visible="showIconDrawer"
+                        :header="$t('选择图标')"
+                        :footer="null"
+                    >
+                      <Iconfonts :urls="data.iconUrls" @change="onChangeIcon" />
+                    </t-drawer>
+                    </div>
+                  </t-tab-panel>
+                  <t-tab-panel :value="2" :label="$t('图元')">
+                    <div
+                        class="form-item mt-8 mb-8"
+                    >
+                      <t-radio-group class="ml-8" v-model="item.temType" @change="item.nextAnimate = ''">
+                        <t-radio value="id">{{$t('图元')}}</t-radio>
+                        <t-radio value="tag">{{$t('组')}}</t-radio>
+                      </t-radio-group>
+                    </div>
+                    <t-tree-select
+                        v-if="item.temType === 'id'"
+                        v-model="item.lineAnimatePens"
+                        :data="penTree"
+                        filterable
+                        :placeholder="$t('无')"
+                    />
+                    <t-select
+                        v-else
+                        v-model="item.lineAnimatePens"
+                        :options="groups"
+                        :placeholder="$t('组')"
+                    />
+                  </t-tab-panel>
+                  <t-tab-panel :value="3" :label="$t('代码')">
+                    <div class="mt-8">
+                      <t-select v-model="item.lineAnimateElement" :placeholder="$t('请选择你的图形类型')">
+                        <t-option v-for="item in drawFuncDialog.animateList" :key="item.label" :value="item.value">
+                          <div style="display: flex;justify-content: space-between;align-items: center;">
+                            <span>{{ item.label }}</span>
+                            <div style="display: flex;gap: 6px">
+                              <edit-icon @click="editeLineAnimateDraw($event,item)"></edit-icon>
+                              <DeleteIcon @click="deleteLineAnimateDraw($event,item)"/>
+                            </div>
+                          </div>
+                        </t-option>
+                        <template #panelBottomContent>
+                          <div class="select-panel-footer">
+                            <t-button theme="primary" variant="text" block @click="editeLineAnimateDraw($event)"
+                            >{{$t('新增选项')}}</t-button
+                            >
+                          </div>
+                        </template>
+                      </t-select>
+                    </div>
+                  </t-tab-panel>
+                </t-tabs>
+              </div>
+
+              <div class="form-item mt-8">
+                <label>{{$t('元素宽度')}}</label>
+                <t-input-number
+                    theme="column"
+                    :min="1"
+                    :placeholder="$t('单位像素,默认10px')"
+                    v-model="item.lineAnimateElementWidth"
+                    @change="changeValue(i)"
+                ></t-input-number>
+              </div>
+
+              <div class="form-item mt-8">
+                <label>{{$t('元素高度')}}</label>
+                <t-input-number
+                    theme="column"
+                    :min="1"
+                    :placeholder="$t('单位像素,默认10px')"
+                    v-model="item.lineAnimateElementHeight"
+                    @change="changeValue(i)"
+                ></t-input-number>
+              </div>
+
+
+              <div class="form-item mt-8">
+                <label>{{$t('间隔')}}</label>
+                <t-input
+                    v-model="item.lineAnimateDash"
+                    :placeholder="$t('请输入间隔,例如 10,20')"
+                >
+                </t-input>
+
+              </div>
+              <div class="form-item mt-8">
+                <label>{{$t('元素数量')}}</label>
+                <t-input-number
+                    v-model="item.lineAnimateElementCount"
+                    theme="column"
+                    :min="1"
+                    :placeholder="$t('无限')"
+                    @change="changeValue(i)"
+                />
+              </div>
+
+            </section>
             <div class="form-item mt-8">
+              <label>{{$t('时间函数')}}</label>
+              <t-switch
+                class="ml-8 mt-8"
+                size="small"
+                v-model="item.curveAnimate"
+                @change="changeValue(i)"
+              ></t-switch>
+            </div>
+            <div class="form-item mt-8" v-if="!item.curveAnimate">
               <label>{{$t('运动速度')}}</label>
-              <t-slider class="ml-12" v-model="item.animateSpan" :show-tooltip="true" :min="1" :max="10" @change="changeValue(i)"></t-slider>
+              <t-slider
+                class="ml-12"
+                v-model="item.animateSpan"
+                :show-tooltip="true"
+                :min="1"
+                :max="10"
+                @change="changeValue(i)"
+              />
             </div>
+            <section v-else>
+              <div class="form-item mt-8">
+                <label>{{$t('动画时长')}}</label>
+                <t-input-number
+                    theme="column"
+                    :min="1"
+                    :placeholder="$t('单位秒,默认5秒')"
+                    v-model="item.duration"
+                    @change="changeValue(i)"
+                ></t-input-number>
+              </div>
+
+              <div class="form-item mt-8">
+                <label>{{$t('时间曲线')}}</label>
+
+                <BezierEditor
+                    style="width: 200px"
+                    v-model="item.animateTimingFunction"
+                    @change="changeValue(i)"
+                    :preset="bezierPreset"
+                ></BezierEditor>
+              </div>
+            </section>
+
             <div class="form-item mt-8">
               <label>{{$t('动画颜色')}}</label>
               <t-color-picker class="w-full" format="CSS" :enable-alpha="true" :recent-colors="null" :swatch-colors="defaultPureColor" :color-modes="['monochrome']" :show-primary-color-preview="false" :clearable="true" v-model="item.animateColor" @change="changeValue(i)"></t-color-picker>
             </div>
+
+
             <div class="form-item mt-8">
               <label>{{$t('发光效果')}}</label>
-              <t-switch class="ml-8 mt-8" size="small" v-model="item.animateShadow" @change="changeValue(i)"></t-switch>
+              <t-switch
+                class="ml-8 mt-8"
+                size="small"
+                v-model="item.animateShadow"
+                @change="changeValue(i)"
+              />
             </div>
             <div v-if="item.animateShadow" class="form-item mt-8">
               <label>{{$t('发光颜色')}}</label>
@@ -105,6 +299,7 @@
               <label>{{$t('轨迹间隔')}}</label>
               <t-input-number v-model="item.animateInterval" theme="column" :min="1" @change="changeValue(i)" :placeholder="$t('默认100')"></t-input-number>
             </div>
+
             <div class="form-item mt-8">
               <label>{{$t('反向流动')}}</label>
               <t-switch class="ml-8 mt-8" size="small" v-model="item.animateReverse" @change="changeValue(i)"></t-switch>
@@ -189,6 +384,43 @@
       </div>
     </div>
   </div>
+
+  <t-dialog
+    v-model:visible="drawFuncDialog.show"
+    lazy
+    :header="drawFuncDialog.edit ? '编辑动画元素' : '添加动画元素'"
+    :width="800"
+    :closeOnOverlayClick="false"
+    :destroy-on-close="true"
+    @confirm="confirmLineDarwFunc"
+    @close="drawFuncDialog.show = false"
+  >
+    <div class="form-item mt-8">
+      <label>名称</label>
+      <t-input
+        v-model="drawFuncDialog.name"
+      ></t-input>
+    </div>
+
+    <div class="form-item mt-8">
+      <label>代码
+        <a style="color: inherit;" target="_blank" href="https://doc.le5le.com/document/123#QZKWt">
+          <HelpCircleIcon style="font-size: 14px" class="ml-4 hover"/>
+        </a>
+      </label>
+      <div style="display: flex;flex-direction: column;width: 100%">
+        <div>function (ctx,pen,state,index) {</div>
+        <CodeEditor
+            v-model="drawFuncDialog.code"
+            :hints="userHints"
+            style="height: 400px;width: 100%"
+        ></CodeEditor>
+        <div>}</div>
+      </div>
+
+
+    </div>
+  </t-dialog>
   <AnimateFrames
     v-if="animate"
     :animate="animate"
@@ -197,17 +429,26 @@
 </template>
 
 <script lang="ts" setup>
-import { onBeforeMount, ref, watch, onUnmounted,onBeforeUnmount,getCurrentInstance } from 'vue';
+import { onBeforeMount, ref, watch, onUnmounted,onBeforeUnmount,getCurrentInstance,computed, reactive } from 'vue';
 
-import { getPenTree } from '@/services/common';
+import {autoSave, getPenTree} from '@/services/common';
 import { deepClone } from '@meta2d/core';
 
+import { getToken  } from '@le5le/auth-token';
+
 import AnimateFrames from './AnimateFrames.vue';
 import { defaultPureColor } from '@/services/defaults';
 import { MessagePlugin } from 'tdesign-vue-next';
-import {StopCircleIcon, PlayCircleIcon, EditIcon, DeleteIcon} from 'tdesign-icons-vue-next';
+import {StopCircleIcon, PlayCircleIcon, EditIcon, DeleteIcon, HelpCircleIcon} from 'tdesign-icons-vue-next';
+import CodeEditor from "@/views/components/common/CodeEditor.vue";
+import {cdn} from "@/services/api";
+import BezierEditor from "@/views/components/common/BezierEditor.vue";
+import Iconfonts from "@/views/components/common/Iconfonts.vue";
+import {s8} from "@/services/random";
+
 const { proxy } = getCurrentInstance();
 const $t = proxy.$t
+
 const props = defineProps<{
   pen: any;
 }>();
@@ -233,6 +474,55 @@ function changeAnimateAutoPlay(value, item) {
     }
   }
 }
+
+const updataData = { directory: '/大屏/图片/默认' };
+const headers = {
+  Authorization: 'Bearer ' + (getToken() || ''),
+};
+
+const bezierPreset = [
+    '0.25,0.25,0.75,0.75',
+    '0.25,0.1,0.25,1',
+    '0.42,0,0.58,1',
+    '0,0,0.58,1',
+  ]
+
+const imagesRef = ref(
+    props.pen.lineAnimateImages?.map((i) => ({ name: i, url: i })) || []
+)
+
+const showIconDrawer = ref(false)
+
+const data = reactive<any>({
+  iconUrls:[]
+});
+
+const onChangeIcon = (params: any) => {
+
+  // Object.assign(props.pen, params);
+  meta2d.setValue({
+    id: props.pen.id,
+    lineAnimateIcon: params.icon,
+    iconFamily: params.iconFamily,
+  });
+  autoSave(true);
+  showIconDrawer.value = false;
+};
+
+watch(imagesRef, (val) => {
+  meta2d.setValue({
+    id: props.pen.id,
+    lineAnimateImages: val.map(i => i.url)
+  })
+})
+
+function fileSuccessed(content,item,i) {
+  changeValue(i)
+}
+
+function fileRemoved(item,i){
+  changeValue(i)
+}
 const checkAnimateName = (item:any) => {
   if(!item.name) {
     MessagePlugin.warning($t('动画名不能为空!'));
@@ -253,6 +543,144 @@ const changeAnimateName = (event, item) => {
     item.name = event;
   }
 };
+const drawFuncDialog = ref({
+  show:false,
+  code:'',
+  name:'',
+  _name:'', // 原始name,用于寻找原始数据
+  animateList:[],
+  edit:false
+});
+
+const userHints = ref([])
+function codeHints() {
+  userHints.value = []
+
+  userHints.value.push({
+    obj:"pen",
+    type:'Pen',
+    typeFilePath: (cdn? cdn +'/v' :'') + '/pen.d.ts', // 外部引入类型 从public目录加载的类型文件
+  })
+
+  userHints.value.push({
+    obj:"ctx",
+    type:'CanvasRenderingContext2D', // js DOM 内置类型
+  })
+
+  userHints.value.push({
+    obj:'state',
+    properties:[{
+      label: 'x', // 显示的文本
+      kind: 9, // 图标类型
+      insertText: "x", // 插入的文本
+      detail: "当前动画帧的x坐标"
+    },
+    {
+      label:'y', // 显示的文本
+      kind: 9, // 图标类型
+      insertText: "y", // 插入的文本
+      detail: "当前动画帧的y坐标"
+    },
+      {
+        label: 'rotate', // 显示的文本
+        kind: 9, // 图标类型
+        insertText: "rotate", // 插入的文本
+        detail: "当前动画帧的该点的切线斜率"
+      },
+      {
+        label: 'progress', // 显示的文本
+        kind: 9, // 图标类型
+        insertText: "progress", // 插入的文本
+        detail: "当前元素的进度"
+      }],
+    trigger:'.'
+  })
+
+  userHints.value.push({
+    obj:"c",
+    trigger:'/',
+    replace:true,
+    properties:[{
+      label: '补全代码模板', // 显示的文本
+      kind: 9, // 图标类型
+      insertText: `const scale = pen.calculative.canvas.store.data.scale;
+
+const width = (pen.lineAnimateElementWidth || 10);
+const height = (pen.lineAnimateElementHeight || 10) ;
+
+ctx.translate(state.x + (width/2 * scale), state.y + (height/2 * scale));  // 平移到中心点
+ctx.rotate((state.rotate * Math.PI) / 180); // 配置旋转角度
+ctx.scale(scale, scale); // 配置缩放
+ctx.translate(-(width/2 * scale), -(height/2 * scale));  // 平移回左上角
+
+ctx.beginPath();
+//在此处编写你的图形定义代码...
+`, // 插入的文本
+      detail: "补全代码模板"
+    }],
+  })
+
+  userHints.value.push({
+    obj:"index",
+    type:'number', // js DOM 内置类型
+  })
+}
+
+
+function editeLineAnimateDraw(e,draw?) {
+  e.e?
+    e.e.stopPropagation():
+    e.stopPropagation()
+
+  drawFuncDialog.value.edit = !!draw;
+  drawFuncDialog.value.show = true;
+  drawFuncDialog.value.name = draw?.label;
+  drawFuncDialog.value._name = draw?.label;
+  drawFuncDialog.value.code = draw?.code;
+}
+
+function deleteLineAnimateDraw(e,draw?) {
+  e.e?
+      e.e.stopPropagation():
+      e.stopPropagation()
+  meta2d.updateLineAnimateDraws(draw.label,-1)
+  syncLineAnimateDraws()
+}
+
+const confirmLineDarwFunc = ()=>{
+  if(drawFuncDialog.value.edit) {
+    const code = drawFuncDialog.value.code
+    meta2d.updateLineAnimateDraws(drawFuncDialog.value._name,{
+      name:drawFuncDialog.value.name,
+      code
+    });
+  }else{
+    if(!addLineDrawFunc()) return
+  }
+  syncLineAnimateDraws()
+  drawFuncDialog.value.show = false;
+}
+const syncLineAnimateDraws = ()=>{
+  // 获取所有动画类型
+  drawFuncDialog.value.animateList = [];
+  Object.entries(meta2d.store.data.lineAnimateDraws).forEach(([name, func]) => {
+  drawFuncDialog.value.animateList.push({
+    label: name,
+    value: name,
+    code: func
+  })
+  })
+}
+
+const addLineDrawFunc = ()=>{
+  if(!drawFuncDialog.value.name) {
+    MessagePlugin.warning('请输入名称!');
+    return false;
+  }
+  const code = drawFuncDialog.value.code
+  meta2d.registerLineAnimateDraws(drawFuncDialog.value.name,code);
+  return true
+}
 
 const penTree: any = ref([]);
 const groups: any = ref([]);
@@ -401,7 +829,13 @@ onBeforeMount(() => {
   if (!props.pen.animations) {
     props.pen.animations = [];
   }
+  syncLineAnimateDraws()
+  codeHints()
 
+  if(props.pen.type && props.pen.lineAnimateType === 6){
+    // 若为自定义类型
+    drawFuncDialog.value.name = props.pen.lineAnimateElement;
+  }
   const p = meta2d.findOne(props.pen.id);
   if (p?.calculative?.start) {
     // @ts-ignore
@@ -420,6 +854,10 @@ onBeforeMount(() => {
       groups.value.push({ label: item, value: item });
     }
   }
+  if (!d.iconUrls) {
+    d.iconUrls = [];
+  }
+  data.iconUrls = d.iconUrls;
 });
 onBeforeUnmount(() => {
   meta2d.off('animateEnd', cancleAnimatePlayState);
@@ -446,7 +884,8 @@ const addAnimate = () => {
   openedCollapses.value.push(props.pen.animations.length);
   props.pen.animations.push({
     name: $t('动画') + (props.pen.animations.length + 1),
-    temType: 'id'
+    temType: 'id',
+    lineAnimateTargetType:0
   });
 };
 
@@ -536,4 +975,26 @@ onUnmounted(() => {
     }
   }
 }
+
+.props .t-tab-panel{
+  height: auto;
+}
+:deep(.t-tabs__nav-item.t-size-m){
+  height: auto;
+}
+:deep(.t-tabs){
+  width: 100%;
+}
+:deep(.t-tabs__nav-item-wrapper){
+  padding: 0 0;
+}
+:deep(.t-tabs__nav-wrap){
+  justify-content: space-around;
+}
+:deep(.t-tabs__nav-scroll){
+  display: block;
+}
+:deep(.t-tabs__bar){
+  display: none;
+}
 </style>

+ 2 - 2
src/views/components/PenDatas.vue

@@ -228,7 +228,7 @@
         </div>
         </div>
       </t-collapse-panel>
-      <t-collapse-panel value="3" header="数据">
+      <t-collapse-panel value="3" :header="$t('数据')">
         <!-- <div class="t-space-item">
           <div class="form-item  py-12">
               <label style="width: 76px">{{$t('关联设备')}}</label>
@@ -380,7 +380,7 @@
               @click="addRealTime"
               :minColumnWidth="150"
             >
-              <t-button style="height: 30px"> 添加动态数据 </t-button>
+              <t-button style="height: 30px"> {{$t('添加动态数据')}} </t-button>
             </t-dropdown>
           </div>
         </div>

+ 9 - 3
src/views/components/PenEvents.vue

@@ -36,11 +36,11 @@
 
           <Conditions :data="props.pen.events[i]" />
           <div class="form-item mt-16" v-if="['click','dblclick'].includes(item.name)||item.confirm">
-            <label>二次确认</label>
+            <label> {{$t('二次确认')}}</label>
             <t-switch size="small" class="mt-8 ml-8" v-model="item.confirm" />
           </div>
           <div v-if="item.confirm" class="form-item mt-8 mb-16">
-            <label>确认文本</label>
+            <label>{{$t('确认文本')}}</label>
             <t-input  v-model="item.confirmTitle" />
           </div>
           <Actions :data="props.pen.events[i]" />
@@ -166,7 +166,13 @@ const options: any = computed(() => {
       content: $t('输入'),
       divider: true,
     });
-  } else if (props.pen.input) {
+  } else if(['datePickerDom','dateRangePickerDom','treeFilterDom','cascadeFilterDom'].includes(props.pen.name)){
+    options.push({
+      value: 'change',
+      content: '选择完成',
+      divider: true,
+    });
+  }else if (props.pen.input) {
     options.push(
       ...[
         {

+ 24 - 32
src/views/components/PenProps.vue

@@ -181,8 +181,8 @@
               <div class="form-item px-16" style="margin-top: -12px">
                 <label>{{$t('状态')}}</label>
                 <t-select v-model="data.pen.showChild" @change="changeValue('showChild')" style="width: 150px" :placeholder="$t('状态')">
-                  <t-option v-for="(a, index) in data.pen.children" :key="index" :value="index">
-{{$t('                    状态')}}{{ index + 1 }}</t-option>
+                  <t-option v-for="(a, index) in data.pen.children" :key="index" :value="index" :label="$t('状态')+(index + 1)">
+                  {{$t('状态')}}{{ index + 1 }}</t-option>
                 </t-select>
               </div>
               <div v-if="data.childPen.image" class="px-16 py-8">
@@ -260,7 +260,7 @@
                         </g>
                       </svg>
                     </template>
-                    <t-option :key="-1" :value="-1">
+                    <t-option :key="-1" :value="-1" label="A">
                       <div class="flex" style=" align-items:center;">
                         <svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="12" height="12">
                           <path d="M113.777778 0h796.444444a113.777778 113.777778 0 0 1 113.777778 113.777778v796.444444a113.777778 113.777778 0 0 1-113.777778 113.777778H113.777778a113.777778 113.777778 0 0 1-113.777778-113.777778V113.777778a113.777778 113.777778 0 0 1 113.777778-113.777778z m0 85.333333a28.444444 28.444444 0 0 0-28.444445 28.444445v796.444444a28.444444 28.444444 0 0 0 28.444445 28.444445h796.444444a28.444444 28.444444 0 0 0 28.444445-28.444445V113.777778a28.444444 28.444444 0 0 0-28.444445-28.444445H113.777778z" fill="#e3e8f4" p-id="37532"></path>
@@ -269,7 +269,7 @@
                         <p style="margin-left:8px;">{{$t('无')}}</p>
                       </div>
                     </t-option>
-                    <t-option :key="0" :value="0">
+                    <t-option :key="0" :value="0" label="B">
                       <svg
                         xmlns="http://www.w3.org/2000/svg"
                         version="1.1"
@@ -280,7 +280,7 @@
                         </g>
                       </svg>
                     </t-option>
-                    <t-option :key="1" :value="1">
+                    <t-option :key="1" :value="1" label="C">
                       <svg
                         xmlns="http://www.w3.org/2000/svg"
                         version="1.1"
@@ -291,7 +291,7 @@
                         </g>
                       </svg>
                     </t-option>
-                    <t-option :key="2" :value="2">
+                    <t-option :key="2" :value="2" label="D">
                       <svg
                         xmlns="http://www.w3.org/2000/svg"
                         version="1.1"
@@ -302,7 +302,7 @@
                         </g>
                       </svg>
                     </t-option>
-                    <t-option :key="3" :value="3">
+                    <t-option :key="3" :value="3" label="E">
                       <svg
                         xmlns="http://www.w3.org/2000/svg"
                         version="1.1"
@@ -402,7 +402,7 @@
                         <use :xlink:href="fromArrows.find(item=>item.value===value).icon"></use>
                       </svg>
                     </template>
-                    <t-option v-for="item in fromArrows" :key="item.value" :value="item.value">
+                    <t-option v-for="item in fromArrows" :key="item.value" :label="item.icon"  :value="item.value">
                       <svg class="l-icon" aria-hidden="true">
                         <use :xlink:href="item.icon"></use>
                       </svg>
@@ -415,7 +415,7 @@
                         <use :xlink:href="toArrows.find(item=>item.value===value).icon"></use>
                       </svg>
                     </template>
-                    <t-option v-for="item in toArrows" :key="item.value" :value="item.value">
+                    <t-option v-for="item in toArrows" :key="item.value" :label="item.icon" :value="item.value">
                       <svg class="l-icon" aria-hidden="true">
                         <use :xlink:href="item.icon"></use>
                       </svg>
@@ -423,7 +423,7 @@
                   </t-select>
                 </div>
                 <div class="form-item">
-                  <label style="width: 76px">起点箭头大小 </label>
+                  <label style="width: 76px">{{$t('起始箭头大小')}} </label>
                   <t-input-number
                     style="width:54px"
                     theme="column"
@@ -431,7 +431,7 @@
                     :min="0"
                     @change="changeValue('fromArrowSize')"
                   />
-                  <label style="width: 76px"  class="ml-4">终点箭头大小 </label>
+                  <label style="width: 76px"  class="ml-4">{{$t('终点箭头大小')}} </label>
                   <t-input-number
                     style="width:54px"
                     theme="column"
@@ -441,7 +441,7 @@
                   />
                 </div>
                 <div class="form-item">
-                  <label style="width: 76px">起点箭头颜色 </label>
+                  <label style="width: 76px">{{$t('起始箭头颜色')}} </label>
                   <t-color-picker
                     class="simple mt-8 ml-8"
                     format="CSS"
@@ -454,7 +454,7 @@
                     v-model="data.pen.fromArrowColor"
                     @change="changeValue('fromArrowColor')"
                   />
-                  <label style="width: 76px;margin-left:36px">终点箭头颜色 </label>
+                  <label style="width: 76px;margin-left:36px">{{$t('终点箭头颜色')}} </label>
                   <t-color-picker
                     class="simple mt-8 ml-8"
                     format="CSS"
@@ -532,7 +532,7 @@
                     <div class="mt-4" style="font-size: 12px">{{$t('连接样式')}}</div>
                   </div>
                 </div>
-                <div class="form-item" v-if="!data.pen.type">
+                <div class="form-item" v-if="!data.pen.type||(data.pen.type&&data.pen.close)">
                   <label style="width: 32px">{{$t('背景')}}</label>
                   <div class="ml-8">
                     <t-radio-group size="small" v-model="data.pen.bkType" :default-value="0" @change="changeValue('bkType')">
@@ -717,6 +717,16 @@
                     <t-input class="ml-4" placeholder="0" v-model.number="data.pen.shadowBlur" style="width: 64px" @change="changeValue('shadowBlur')" :label="$t('模糊')" :title="$t('模糊大小')"></t-input>
                   </div>
                 </div>
+
+                <div class="form-item">
+                  <label style="width: 52px">滤镜 </label>
+                  <t-input
+                      theme="normal"
+                      placeholder="css滤镜参数"
+                      v-model="data.pen.filter"
+                      @change="changeValue('filter')"
+                  />
+                </div>
               </t-space>
             </t-collapse-panel>
 
@@ -1515,24 +1525,6 @@ const getRect = () => {
 };
 const detailResizePens = () => {
   getRect();
-  updateTabBtnData();
-}
-const updateTabBtnData = () => {//更新tab按钮的宽高
-  if(data.pen.name === 'tab') {
-    const len = data.pen.data.length;
-    const gap = data.pen.gap;
-    const {width: w, height: h} = data.rect;
-    let btnWidth = 0, btnHeight = 0;
-    if (data.pen.direction == 'horizontal') {
-      btnWidth = (w - (len + 1) * gap) / len;
-      btnHeight = h - gap * 2;
-    } else  {
-      btnWidth =  w - gap * 2;
-      btnHeight =  (h - (len + 1) * gap) / len;
-    }
-    data.pen.btnWidth = btnWidth;
-    data.pen.btnHeight = btnHeight;
-  }
 }
 const decimalPlaces = (val: number) => {
   if (!val) {

+ 4 - 4
src/views/components/PensProps.vue

@@ -219,7 +219,7 @@
                         </g>
                       </svg>
                     </template>
-                    <t-option :key="0" :value="0">
+                    <t-option :key="0" :value="0" label="A">
                       <svg
                         xmlns="http://www.w3.org/2000/svg"
                         version="1.1"
@@ -230,7 +230,7 @@
                         </g>
                       </svg>
                     </t-option>
-                    <t-option :key="1" :value="1">
+                    <t-option :key="1" :value="1" label="B">
                       <svg
                         xmlns="http://www.w3.org/2000/svg"
                         version="1.1"
@@ -241,7 +241,7 @@
                         </g>
                       </svg>
                     </t-option>
-                    <t-option :key="2" :value="2">
+                    <t-option :key="2" :value="2" label="C">
                       <svg
                         xmlns="http://www.w3.org/2000/svg"
                         version="1.1"
@@ -252,7 +252,7 @@
                         </g>
                       </svg>
                     </t-option>
-                    <t-option :key="3" :value="3">
+                    <t-option :key="3" :value="3" label="D">
                       <svg
                         xmlns="http://www.w3.org/2000/svg"
                         version="1.1"

+ 619 - 0
src/views/components/Project.vue

@@ -0,0 +1,619 @@
+<template>
+  <div class="content" @click="closeMenu">
+    <div class="flex between">
+      <div></div>
+      <div>
+        <t-dropdown :minColumnWidth="200" :hide-after-item-click="false">
+          <t-tooltip :content="$t('导入属性列表')" placement="top">
+            <div class="icon-box">
+              <AddIcon />
+            </div>
+          </t-tooltip>
+          <t-dropdown-menu>
+            <t-dropdown-item @click="append(undefined, true)">
+              <a>{{$t('新建文件夹')}}</a>
+            </t-dropdown-item>
+            <t-dropdown-item @click="append(undefined)" divider="true">
+              <a>{{$t('新建页面')}}</a>
+            </t-dropdown-item>
+            <t-dropdown-item @click="downloadProjectSource">
+              <a>
+                <div class="flex">
+                  {{$t('导出工程')}}<span class="flex-grow"></span>
+                </div>
+              </a>
+            </t-dropdown-item>
+            <t-dropdown-item @click="downloadProject(Frame.html)">
+              <a>
+                <div class="flex">
+                  {{$t('导出工程离线部署包')}} <span class="flex-grow"></span>
+                </div>
+              </a>
+            </t-dropdown-item>
+            <t-dropdown-item @click="downloadProject(Frame.vue3)">
+              <a>
+                <div class="flex">
+                  {{$t('导出工程Vue3组件包')}} <span class="flex-grow"></span>
+                </div>
+              </a>
+            </t-dropdown-item>
+            <t-dropdown-item @click="downloadProject(Frame.vue2)">
+              <a>
+                <div class="flex">
+                  {{$t('导出工程Vue2组件包')}} <span class="flex-grow"></span>
+                </div>
+              </a>
+            </t-dropdown-item>
+            <t-dropdown-item @click="downloadProject(Frame.react)">
+              <a>
+                <div class="flex">
+                  {{$t('导出工程react组件包')}} <span class="flex-grow"></span>
+                </div>
+              </a>
+            </t-dropdown-item>
+          </t-dropdown-menu>
+        </t-dropdown>
+      </div>
+    </div>
+    <t-tree
+      :key="treeKey"
+      class="flex-grow"
+      ref="tree"
+      :data="data.tree"
+      expandAll
+      :actived="data.actived"
+      activable
+      style="padding: 0 4px 8px 8px"
+    >
+      <template #label="{ node }: any">
+        <div class="flex middle" :class="{ gray: node.data.visible === false }">
+          <FileIcon v-if="node.data.type === 'page'" />
+          <template v-else>
+            <folder-open-icon class="mr-8" v-if="node.expanded" />
+            <folder-icon class="mr-8" v-else />
+          </template>
+          <t-input
+            v-if="node.data.edited"
+            v-model="node.data.label"
+            :autofocus="true"
+            style="width: 100px"
+            @blur="onDescription(node)"
+            @enter="onDescription(node)"
+          />
+          <span
+            v-else
+            style="width: 100px"
+            @dblclick="ondblclick(node)"
+            @contextmenu="oncontextmenu($event, node)"
+            @click="onClick(node)"
+          >
+            {{ node.label }}
+          </span>
+        </div>
+      </template>
+      <!-- <template #operations="{ node }: any">
+        <div
+          class="flex middle operations"
+          :class="{
+            gray: node.data.visible === false,
+            show: node.data.visible === false || node.data.locked,
+          }"
+          style="width: 46px; height: 16px"
+        ></div>
+      </template> -->
+    </t-tree>
+    <t-menu
+      class="context-menu"
+      v-if="contextmenu.visible"
+      :style="contextmenu.style"
+      @change="onMenu"
+      expandType="popup"
+    >
+      <t-menu-item v-for="item in operations" :value="item.key">
+        <div class="flex">{{$t(item.label) }}</div>
+      </t-menu-item>
+      <t-menu-item
+        v-if="current.data.type === 'page'"
+        value="createFromTemplate"
+      >
+        <div class="flex">{{$t('从模版创建')}}</div>
+      </t-menu-item>
+      <t-menu-item v-if="current.data.type === 'page'" value="quotePage">
+        <div class="flex">{{$t('引用页面')}}</div>
+      </t-menu-item>
+      <t-menu-item
+        v-if="current.data.type === 'page' && current.data.pageId"
+        value="copyPage"
+      >
+        <div class="flex">{{$t('复制页面')}}</div>
+      </t-menu-item>
+      <t-menu-item :disabled="!copyData" value="pastePage">
+        <div class="flex">{{$t('粘贴页面')}}</div>
+      </t-menu-item>
+      <t-menu-item v-if="current.data.type === 'page'" value="delete">
+        <t-tooltip
+          :content="$t('仅删除与工程的关联关系,不删除图纸')"
+          placement="right"
+        >
+          <div class="flex">{{$t('移除页面')}}</div>
+        </t-tooltip>
+      </t-menu-item>
+      <t-menu-item v-else value="delete">
+        <div class="flex">{{$t('删除文件夹')}}</div>
+      </t-menu-item>
+    </t-menu>
+  </div>
+  <ProjectModal
+    v-model:visible="projectDialog.visible"
+    v-model:type="projectDialog.type"
+    @change="getScene"
+  />
+  <!-- <StepModal
+    v-model:visible="taskDialog.visible"
+    :tasks="taskDialog.tasks"
+    :reload="taskDialog.reload"
+    @cancel="setTask('cancel', true)"
+    @reDownload="reDownload"
+  /> -->
+  <!-- <ProjectPayModal
+    v-model:visible="payDialog.visible"
+    v-model:data="payDialog.data"
+    @change="doDownload"
+  />
+  <ProjectCPayModal
+    v-model:visible="payProjectDialog.visible"
+    v-model:data="payProjectDialog.data"
+    @change="doCDownload"
+  /> -->
+</template>
+
+<script lang="ts" setup>
+import { reactive, ref, onMounted, onUnmounted, onBeforeUnmount, getCurrentInstance } from 'vue';
+import {
+  FolderOpenIcon,
+  FolderIcon,
+  AddIcon,
+  FileIcon,
+} from 'tdesign-icons-vue-next';
+import { s8 } from '@/services/random';
+import { blank, useDot } from '@/services/common';
+import { MessagePlugin } from 'tdesign-vue-next';
+import { useRouter } from 'vue-router';
+import {
+  useProject,
+  useTask,
+  loadProject,
+  downloadProject,
+  downloadProjectSource,
+  doDownloadProjectSource,
+  usePay,
+  payProjectDialog,
+  doDownloadProject,
+  reDownload,
+} from '@/services/project';
+import ProjectModal from './common/ProjectModal.vue';
+import { addCollection, updateCollection, getCollection } from '@/services/api';
+import Data from './Data.vue';
+import { deepClone } from '@meta2d/core';
+import { DialogPlugin } from 'tdesign-vue-next';
+// import StepModal from './common/StepModal.vue';
+import { Frame } from '@/services/download';
+// import ProjectPayModal from './common/ProjectPayModal.vue';
+// import ProjectCPayModal from './common/ProjectCPayModal.vue';
+// import { isDownload } from '@/services/defaults';
+
+const { proxy } = getCurrentInstance();
+const $t = proxy.$t;
+
+const { taskDialog, setTask } = useTask();
+
+const { payDialog } = usePay();
+
+const treeKey = ref(s8());
+
+onBeforeUnmount(() => {
+  data.tree = tree.value.getTreeData();
+});
+
+const { data, tree, activeNode, activePage, setActivedNode, saveProject } =
+  useProject();
+const { dot } = useDot();
+const router = useRouter();
+
+const operations: any[] = [
+  {
+    label: '新建文件夹',
+    key: 'newFolder',
+  },
+  {
+    label: '新建页面',
+    key: 'newFile',
+  },
+];
+
+const onDescription = (node: any) => {
+  node.data.edited = false;
+  node.setData({ label: node.data.label });
+  dot.value = true;
+};
+
+const ondblclick = (node: any) => {
+  if (node.data.type==='page' && node.data.value !== data.actived[0]) {
+    MessagePlugin.warning($t('请先选中页面'));
+    return;
+  }
+  node.data.edited = true;
+};
+
+const treeAppend = (data,key,node)=>{
+  if(!key){
+    data.push(node);
+  }else{
+    deepAppend(data,key,node);
+  }
+}
+
+const deepAppend = (data,key,node)=>{
+  data.forEach((item,index)=>{
+    if(item.value===key){
+      if(item.type==='page'){
+        data.splice(index+1,0,node);
+      }else{
+        if(!item.children){
+          item.children = [];
+        }
+        item.children.push(node);
+      }
+    }
+    if(item.children){
+      deepAppend(item.children,key,node);
+    }
+  })
+}
+
+const append = async (node, folder?: boolean) => {
+  const item: any = {
+    value: s8(),
+    type: 'page',
+    label: $t('页面'),
+  };
+  if (folder) {
+    item.label = $t('文件夹');
+    item.type = 'folder';
+    item.children = [];
+  }
+  if (node && node.data.type === 'page') {
+    insertAfter(node, folder);
+    treeKey.value = s8();
+    return;
+  }
+  if (!node) {
+    // tree.value.appendTo('', item);
+    treeAppend(data.tree,'',item);
+  } else {
+    // tree.value.appendTo(node.value, item);
+    treeAppend(data.tree,node.value,item);
+  }
+  // tree.value?.refresh();
+  treeKey.value = s8();
+};
+
+const pastePage = async (node) => {
+  let copyPageId = copyData.value.pageId;
+  const item = copyData.value;
+  item.value = s8();
+  item.pageId = '';
+  if (node) {
+    if (node.data.type === 'page') {
+      tree.value.insertAfter(node.value, item);
+      treeAppend(data.tree,node.value,item);
+    } else {
+      tree.value.appendTo(node.value, item);
+      treeAppend(data.tree,node.value,item);
+    }
+    treeKey.value = s8();
+  }
+  data.actived = [item.value];
+  let acNode = tree.value.getItem(item.value);
+
+  setActivedNode(acNode);
+  // saveProject(undefined);
+  // return;
+  if (copyPageId) {
+    const ret: any = await getCollection('v', copyPageId);
+    const body: any = {
+      data: ret.data,
+      image: '',
+      name: ret.name,
+      folder: ret.folder,
+      template: ret.template,
+      case: ret.case,
+      otherData: {
+        projectId: data.id,
+      },
+    };
+    const ret1: any = await addCollection('v', body); // 新增
+    if (ret1.id) {
+      saveProject(ret1.id);
+      item.pageId = ret1.id;
+      router.push({
+        path: '/',
+        query: {
+          r: Date.now() + '',
+          id: ret1.id,
+        },
+      });
+      copyData.value = null;
+    }
+  }
+};
+
+const deletePage = async (node) => {
+  if (node.data.type !== 'page') {
+    if (node.data.children?.length) {
+      MessagePlugin.info($t('请先删除文件夹下的页面'));
+      return;
+    }
+  }
+  //仅删除图纸与工程的关联关系,不会删除图纸
+  await tree.value.remove(node.value);
+  if (node.data.type === 'page') {
+    // setActivedNode(current);
+    await saveProject(undefined, true);
+    if (node.data.pageId) {
+      await updateCollection('v', {
+        id: node.data.pageId,
+        otherData: { projectId: '' },
+      });
+    }
+  }
+};
+
+const insertAfter = async (node, folder?: boolean) => {
+  const item: any = {
+    value: s8(),
+    type: 'page',
+    label: $t('页面'),
+  };
+  if (folder) {
+    item.label = $t('文件夹');
+    item.type = 'folder';
+    item.children = [];
+  }
+  if (item) {
+    treeAppend(data.tree,node.value,item);
+    // await tree.value.insertAfter(node.value, item);
+    // setLabel(item.value);
+  }
+};
+
+const contextmenu = reactive<any>({
+  visible: false,
+  type: '',
+  style: {},
+});
+
+let current = null;
+
+const oncontextmenu = (e, node: any) => {
+  if (node.data.type === 'page' && node.data.value !== data.actived[0]) {
+    return;
+  }
+  contextmenu.style = {
+    left: e.clientX + 'px',
+    top: e.clientY + 'px',
+  };
+  contextmenu.visible = true;
+  current = node;
+};
+
+let copyData = ref(null);
+
+const onMenu = (val: string) => {
+  switch (val) {
+    case 'newFolder':
+      append(current, true);
+      break;
+    case 'newFile':
+      append(current);
+      break;
+    case 'delete':
+      deletePage(current);
+      break;
+    case 'createFromTemplate':
+      projectDialog.value.visible = true;
+      projectDialog.value.type = 'template';
+      break;
+    case 'copyPage':
+      copyData.value = deepClone(current.data);
+      break;
+    case 'pastePage':
+      if (copyData.value) {
+        pastePage(current);
+        // append(current);
+        // current.children[current.children.length - 1].data = copyData.value;
+      }
+      break;
+    case 'quotePage':
+      projectDialog.value.visible = true;
+      projectDialog.value.type = 'v';
+      break;
+  }
+  contextmenu.visible = false;
+};
+
+const closeMenu = () => {
+  contextmenu.visible = false;
+};
+
+let clickTimer = null;
+
+const onClick = (node) => {
+  if (clickTimer) {
+    clearTimeout(clickTimer); // 如果在双击时间内,再次点击则清除定时器
+    clickTimer = null;
+  } else {
+    clickTimer = setTimeout(() => {
+      click(node);
+      clickTimer = null;
+    }, 300);
+  }
+};
+
+const click = (node) => {
+  if (node.data.type !== 'page') {
+    // data.actived = [];
+    return;
+  }
+  if (meta2d.store.data.pens.length && dot.value) {
+    MessagePlugin.info($t('请先保存当前画布'));
+    return;
+  }
+  data.actived = [node.value];
+  setActivedNode(node);
+  if (node.data.pageId) {
+    // 是否需要自动保存
+    open(node.data.pageId);
+  } else {
+    blank();
+    router.push({
+      path: '/',
+      query: {
+        r: Date.now() + '',
+      },
+    });
+  }
+};
+
+const open = async (id: string) => {
+  if (meta2d.store.data.pens.length && dot.value) {
+    MessagePlugin.info($t('请先保存当前画布'));
+    return;
+  }
+  router.push({
+    path: '/',
+    query: {
+      r: Date.now() + '',
+      id,
+    },
+  });
+};
+
+const getData = () => {
+  const data = tree.value.getTreeData();
+};
+
+const projectDialog = ref({
+  visible: false,
+  type: 'template',
+  data: {},
+});
+
+const getScene = async (e) => {
+  if (projectDialog.value.type === 'template') {
+    if (meta2d.store.data.id) {
+      //先删除引用关系
+      await updateCollection('v', {
+        id: meta2d.store.data.id,
+        otherData: { projectId: '' },
+      });
+    }
+
+    const ret: any = await getCollection('v', e.id);
+    ret.data.id = undefined; //meta2d.store.data.id;
+    meta2d.open(ret.data);
+    history.replaceState({}, document.title, window.location.pathname);
+    dot.value = true;
+  } else if (projectDialog.value.type === 'v') {
+    //TODO 一个页面不能被多个项目引用。
+    const ret: any = await getCollection('v', e.id, 'id,other_data');
+    if (ret.otherData?.projectId) {
+      const confirmDia = DialogPlugin({
+        header: $t('提示'),
+        body:  $t('该页面已被引用,是否拷贝一份?'),
+        confirmBtn:  $t('是'),
+        cancelBtn:  $t('否'),
+        onConfirm: async () => {
+          //如果当前已经存在id,则删除关系
+          if (meta2d.store.data.id) {
+            await updateCollection('v', {
+              id: meta2d.store.data.id,
+              otherData: { projectId: '' },
+            });
+          }
+          const ret: any = await getCollection('v', e.id);
+          ret.data.id = undefined;
+          // ret.data.id = meta2d.store.data.id;
+          meta2d.open(ret.data);
+          history.replaceState({}, document.title, window.location.pathname);
+          dot.value = true;
+          confirmDia.hide();
+          projectDialog.value.visible = false;
+        },
+        onClose: ({ e, trigger }) => {
+          confirmDia.hide();
+          projectDialog.value.visible = false;
+        },
+      });
+      return;
+    }
+    // 引用页面
+    if (meta2d.store.data.id) {
+      await updateCollection('v', {
+        id: meta2d.store.data.id,
+        otherData: { projectId: '' },
+      });
+    }
+
+    setActivedNode(current);
+    await saveProject(e.id);
+    let _data = {
+      id: e.id,
+      otherData: { projectId: data.id },
+    };
+    await updateCollection('v', _data);
+    router.push({
+      path: '/',
+      query: {
+        r: Date.now() + '',
+        id: e.id,
+      },
+    });
+  }
+  projectDialog.value.visible = false;
+};
+
+const doDownload = async () => {
+  await doDownloadProjectSource();
+};
+
+const doCDownload = async () => {
+  await doDownloadProject();
+};
+</script>
+<style lang="postcss" scoped>
+.content {
+  height: 100%;
+  .icon-box {
+    width: 24px;
+    height: 24px;
+    margin: 4px;
+    text-align: center;
+    line-height: 24px;
+    border-radius: 4px;
+
+    &:hover {
+      background: var(--td-brand-color-light);
+    }
+
+    .t-icon {
+      width: 14px;
+      height: 14px;
+    }
+  }
+  .context-menu {
+    width: 120px !important;
+    box-shadow: var(--shadow-popup);
+  }
+}
+</style>

+ 8 - 0
src/views/components/Structure.vue

@@ -1,4 +1,7 @@
 <template>
+  <div class="elements props" v-if="projectData.tree?.length" v-show="group === '工程'">
+    <Project></Project>
+  </div>
   <div class="elements props" v-show="group === '图层'">
     <div class="flex mt-16 mb-16" style="justify-content: end; padding-right: 8px">
       <t-tooltip placement="top" :content="$t('置顶')">
@@ -197,7 +200,12 @@ import {
   MenuUnfoldIcon,
   MenuFoldIcon,
 } from 'tdesign-icons-vue-next';
+import Project from '@/views/components/Project.vue';
+
+import { useProject  } from '@/services/project';
 
+const { data:projectData } = useProject();
+ 
 const props = defineProps<{
   group: string;
 }>();

+ 15 - 6
src/views/components/View.vue

@@ -909,8 +909,10 @@ import { upload } from '@/services/file';
 import { getCollectionList } from '@/services/api';
 import { parseSearchToken } from '@le5le/auth-token';
 import { useModuleType } from '@/services/module-type';
+import { useProject } from '@/services/project';
 
 const { isBigScreen } = useModuleType();
+const { getProject } = useProject();
 const {anchorShow } = useDAnchor();
 const router = useRouter();
 const route = useRoute();
@@ -1095,6 +1097,9 @@ const open = async (flag: boolean = false) => {
       if(!ret.data.width){ret.data.width= 1920};
       if(!ret.data.height){ret.data.height= 1080};
       meta2d.open(ret.data);
+      if(ret.otherData?.projectId){
+        getProject(ret.otherData.projectId);
+      }
       if (!route.query.c) {
         shared.value = ret.shared;
 
@@ -1427,7 +1432,9 @@ const onPencil = () => {
 }
 const penAdd = (pen:Pen)=>{
   // 画布新增一个pen,就需要根据当前主题调用setTheme
-  pen[0].setTheme && pen[0].setTheme(pen[0],meta2d.store.styles);
+  if(meta2d.store.data.theme){
+    pen[0].setTheme && pen[0].setTheme(pen[0],meta2d.store.styles);
+  }
 }
 const lineAdd = (pens: Pen[]) => {
   if (pens.length === 1 && pens[0].name === 'line') {
@@ -2420,11 +2427,13 @@ const changeFit = () => {
     meta2d.canvas.showFit();
   }else{
     meta2d.store.data.pens.forEach((pen) => {
-      if (!selections.fit.children?.includes(pen.id)) {
-        meta2d.setValue(
-          { id: pen.id, visible: true },
-          { render: false, doEvent: false, history: false }
-        );
+      if (selections.fit&&!selections.fit.children?.includes(pen.id)) {
+        if(!pen.parentId){
+          meta2d.setValue(
+            { id: pen.id, visible: true },
+            { render: false, doEvent: false, history: false }
+          );
+        }
       }
     });
     meta2d.canvas.hideFit();

+ 242 - 0
src/views/components/common/BezierEditor.vue

@@ -0,0 +1,242 @@
+<template>
+  <div class="bezier-editor">
+
+    <div class="main">
+      <svg
+          ref="svg"
+          class="editor"
+          viewBox="0 0 1 1"
+          preserveAspectRatio="none"
+          @mousedown="onSvgMouseDown"
+      >
+
+        <line x1="0" y1="1" x2="1" y2="0" stroke="#88888893" stroke-width="0.01" />
+
+        <line :x1="0" :y1="1" :x2="p1.x" :y2="p1.y" stroke="#4583FF" stroke-dasharray="2,2" stroke-width="0.015" />
+        <line :x1="p2.x" :y1="p2.y" :x2="1" :y2="0" stroke="#4583FF" stroke-dasharray="2,2" stroke-width="0.015" />
+
+        <path :d="curvePath" fill="none" stroke="#E3E8F4" stroke-width="0.02" />
+
+        <circle
+            class="point"
+            r="0.03"
+            :cx="p1.x"
+            :cy="p1.y"
+            @mousedown.prevent="startDrag('p1')"
+        />
+        <circle
+            class="point"
+            r="0.03"
+            :cx="p2.x"
+            :cy="p2.y"
+            @mousedown.prevent="startDrag('p2')"
+        />
+      </svg>
+
+      <div class="preset">
+        <div v-for="(item, index) in preset" :key="index" class="presetItem" @click="onPresetClick(item)">
+          <svg
+              :class="{presetItemSvg:true,active:modelValue===item}"
+              viewBox="0 0 1 1"
+              preserveAspectRatio="none"
+          >
+            <path :d="'M 0 1 C ' + parsePresetStr(item)+ ',1 0'" fill="none" stroke="#E3E8F4" stroke-width="0.04" />
+          </svg>
+        </div>
+      </div>
+    </div>
+
+    <div class="controls">
+      <RefreshIcon class="resetIcon hover" @click="reset"/>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, watch, computed, onMounted, onBeforeUnmount } from 'vue'
+import {RefreshIcon} from "tdesign-icons-vue-next";
+
+const props = defineProps({
+  modelValue: String,
+  preset:{
+    type: Array,
+    default:()=>{
+      return []
+    }
+  }
+})
+const emit = defineEmits(['update:modelValue', 'change','reset'])
+
+const svg = ref(null)
+const p1 = ref({ x: 0.25, y: 0.75 })
+const p2 = ref({ x: 0.75, y: 0.25 })
+const dragging = ref(null)
+
+function formatVal() {
+  const x1 = Number(p1.value.x.toFixed(3))
+  const y1 = Number((1 - p1.value.y).toFixed(3))
+  const x2 = Number(p2.value.x.toFixed(3))
+  const y2 = Number((1 - p2.value.y).toFixed(3))
+  return `${x1},${y1},${x2},${y2}`
+}
+
+function onPresetClick(item) {
+  parseRaw(item)
+  emit('update:modelValue', formatVal())
+  emit('change', formatVal())
+}
+
+const parseRaw = (str) => {
+  if(!str) return
+  const nums = str.split(',').map(Number)
+  if (nums.length === 4 && nums.every(n => !isNaN(n))) {
+    p1.value = { x: nums[0], y: 1 - nums[1] }
+    p2.value = { x: nums[2], y: 1 - nums[3] }
+  }
+}
+
+watch(() => props.modelValue, (val) => {
+  if (val) parseRaw(val)
+}, { immediate: true })
+
+const curvePath = computed(() => {
+  return `M 0 1 C ${p1.value.x} ${p1.value.y}, ${p2.value.x} ${p2.value.y}, 1 0`
+})
+
+function parsePresetStr(str) {
+  if(!str) return
+  const nums = str.split(',').map(Number)
+  if (nums.length === 4 && nums.every(n => !isNaN(n))) {
+    return `${nums[0]} ${1 - nums[1]}, ${nums[2]} ${1 - nums[3]}`
+  }
+}
+function startDrag(name) {
+  dragging.value = name
+}
+let rafId = null
+
+function onMouseMove(e) {
+  if (!dragging.value || !svg.value) return
+  if (rafId) cancelAnimationFrame(rafId)
+
+  rafId = requestAnimationFrame(() => {
+    const rect = svg.value.getBoundingClientRect()
+    const x = (e.clientX - rect.left) / rect.width
+    const y = (e.clientY - rect.top) / rect.height
+    const unbounded = {
+      x,
+      y,
+    }
+    if (dragging.value === 'p1') p1.value = unbounded
+    if (dragging.value === 'p2') p2.value = unbounded
+    emit('update:modelValue', formatVal())
+    emit('change', formatVal())
+  })
+}
+function onMouseUp() {
+  dragging.value = null
+}
+
+const reset = () => {
+  p1.value = { x: 0.25, y: 0.75 }
+  p2.value = { x: 0.75, y: 0.25 }
+
+  const updatedValue = formatVal();
+
+  emit('update:modelValue', updatedValue);  // 更新v-model值
+  emit('reset', updatedValue);              // 触发重置事件(如果有其他监听器)
+  emit('change', updatedValue)
+}
+function onSvgMouseDown(e) {
+  if (!svg.value) return
+  const rect = svg.value.getBoundingClientRect()
+  const x = (e.clientX - rect.left) / rect.width
+  const y = (e.clientY - rect.top) / rect.height
+  const point = { x, y }
+
+  const dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y)
+  const d1 = dist(point, p1.value)
+  const d2 = dist(point, p2.value)
+
+  if (d1 < d2) {
+    p1.value = point
+  } else {
+    p2.value = point
+  }
+
+  emit('update:modelValue', formatVal())
+  emit('change', formatVal())
+}
+
+
+onMounted(() => {
+  window.addEventListener('mousemove', onMouseMove)
+  window.addEventListener('mouseup', onMouseUp)
+})
+
+onBeforeUnmount(() => {
+  window.removeEventListener('mousemove', onMouseMove)
+  window.removeEventListener('mouseup', onMouseUp)
+})
+</script>
+
+<style scoped>
+.bezier-editor {
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  user-select: none;
+}
+.main {
+  width: 100%;
+  aspect-ratio:1;
+  border: 1px solid #303746;
+  background: #303746;
+  border-radius: 4px;
+}
+.editor {
+  padding: 20px;
+}
+.point {
+  fill: #4583FF;
+  cursor: pointer;
+}
+.label {
+  font-family: monospace;
+  font-size: 14px;
+}
+.controls {
+  position: absolute;
+  top: 3px;
+  left: 10px;
+  display: flex;
+  width: 100%;
+  justify-content: space-between;
+  align-items: center;
+}
+.preset{
+  width: 100%;
+  display: flex;
+  justify-content: space-around;
+  align-items: center;
+  margin-bottom: 5px;
+}
+
+.presetItem{
+  width: 34px;
+  height: 34px;
+  background-color: #1e2430;
+  border-radius: 4px;
+}
+.presetItem:hover{
+  cursor: pointer;
+}
+
+.presetItemSvg{
+  padding: 4px;
+}
+.active{
+  background-color: #262D3A;
+}
+</style>

+ 81 - 1
src/views/components/common/CodeEditor.vue

@@ -47,6 +47,7 @@ import 'monaco-editor/esm/vs/editor/contrib/unusualLineTerminators/browser/unusu
 import 'monaco-editor/esm/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.js';
 import 'monaco-editor/esm/vs/editor/contrib/wordOperations/browser/wordOperations.js';
 import 'monaco-editor/esm/vs/editor/contrib/wordPartOperations/browser/wordPartOperations.js';
+import 'monaco-editor/esm/vs/language/typescript/monaco.contribution'
 
 // import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
 // import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
@@ -56,13 +57,30 @@ import 'monaco-editor/esm/vs/language/json/monaco.contribution';
 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/basic-languages/sql/sql.contribution';
+import {MonacoTypeManager} from "@/services/utils";
 
 const dom = ref<any>();
 
-const { modelValue, json, language, options } = defineProps<{
+const { modelValue, json, language, options, hints } = defineProps<{
   modelValue: any;
   json?: boolean;
   language?: string;
+  hints?: any; /** 代码提示,传入参考:[{
+   obj:'data',
+   properties:[{
+     label: 'xxxx', // 显示的文本
+     kind: 9, // 图标类型
+     insertText: 'insertXXX', // 插入的文本
+     detail: 'detail'
+   }],
+   trigger:'[' 触发条件
+ },{
+   obj:"ctx", // 对象
+   type:'Pen', // 类型
+   typeFilePath:'pen.d.ts', //从public目录加载的类型文件
+   }]
+   */
+
   options?: any;
 }>();
 
@@ -82,6 +100,9 @@ const emit = defineEmits(['update:modelValue', 'change']);
 
 let editor: monaco.editor.IStandaloneCodeEditor;
 
+const typeManager = new MonacoTypeManager(monaco);
+typeManager.clearTypeFiles()
+
 onMounted(() => {
   let text = '';
   if (json) {
@@ -122,6 +143,64 @@ onMounted(() => {
   }, 300);
 });
 
+let completionDisposables: monaco.IDisposable[] = [];
+watch(() => hints, (newValue) => {
+  if (!hints) return;
+
+  completionDisposables.forEach(disposable => disposable.dispose());
+
+  newValue.forEach((hint: any) => {
+    if (hint.type && !hint.typeFilePath) {
+      typeManager.injectVariableType(hint.obj,hint.type)
+    }else if(hint.type && hint.typeFilePath){
+      typeManager.loadTypeFile(hint.typeFilePath,()=>{
+        typeManager.injectVariableType(hint.obj,hint.type)
+      })
+
+    }
+
+    const instance = monaco.languages.registerCompletionItemProvider('javascript', {
+      triggerCharacters: [hint.trigger],
+      provideCompletionItems: (model, position) => {
+        const textUntilPosition = model.getValueInRange({
+          startLineNumber: position.lineNumber,
+          startColumn: 1,
+          endLineNumber: position.lineNumber,
+          endColumn: position.column
+        });
+
+        const match = textUntilPosition.match(new RegExp(`(\\b(${hint.obj})\\${hint.trigger})$`));
+        if (!match) return { suggestions: [] };
+
+        const properties = hint.properties;
+        if (properties) {
+          return {
+            suggestions: properties.map(prop => ({
+              label: String(prop.label),
+              kind: prop.kind,
+              insertText: prop.insertText,
+              detail: prop.detail,
+            })),
+          dispose() {
+            if(!hint.replace)return
+             const line = position.lineNumber
+             const column = position.column
+             editor.executeEdits("", [
+               {
+                 range: new monaco.Range(line, column - hint.obj.length - 1, line, column),
+                 text: null
+               }
+             ])
+          }
+          };
+        }
+      }
+    });
+
+    completionDisposables.push(instance);
+  });
+}, { immediate: true });
+
 // watch(
 //   () => modelValue,
 //   (newValue) => {
@@ -151,6 +230,7 @@ watch(
 );
 
 onUnmounted(() => {
+  completionDisposables.forEach(disposable=>{disposable.dispose()})
   editor?.dispose();
 });
 </script>

+ 5 - 5
src/views/components/common/JsonModal.vue

@@ -1,19 +1,19 @@
 <template>
   <t-dialog
     v-model:visible="props.visible"
-    header="JSON值"
+    :header="$t('JSON值')"
     :width="470"
     @close="close"
     @confirm="confirm"
   >
     <t-tabs v-model="tabValue" @change="tabChange">
-      <t-tab-panel :value="1" label="简单模式">
+      <t-tab-panel :value="1" :label="$t('简单模式')">
         <div class="mt-8">
           <div class="flex mt-4 one-data" v-for="(d, i) in simpleData">
             <t-select-input
               class="input-key"
               v-if="props.options?.length"
-              placeholder="可自定义输入"
+              :placeholder="$t('可自定义输入')"
               v-model:inputValue="d.key"
               :value="d.keyLabel"
               v-model:popupVisible="d.keyPopupVisible"
@@ -49,9 +49,9 @@
           </div>
         </div>
       </t-tab-panel>
-      <t-tab-panel :value="2" label="JSON模式">
+      <t-tab-panel :value="2" :label="$t('JSON模式')">
         <template #label>
-          JSON模式
+          {{$t('JSON模式')}}
           <t-tooltip v-if="props.tips" :content="props.tips" placement="top">
             <HelpCircleIcon class="ml-4" />
           </t-tooltip>

+ 9 - 6
src/views/components/common/MoreModal.vue

@@ -1,7 +1,7 @@
 <template>
   <t-dialog
     v-model:visible="props.visible"
-    header="更多属性"
+    :header="$t('更多属性')"
     :width="470"
     @close="close"
     @confirm="confirm"
@@ -14,7 +14,7 @@
         v-model="search"
         @change="onSearch"
         @enter="onSearch"
-        placeholder="搜索属性"
+        :placeholder="$t('搜索属性')"
       />
     </div>
     <div class="props mt-8">
@@ -53,9 +53,12 @@
 
 <script lang="ts" setup>
 import { SearchIcon } from 'tdesign-icons-vue-next';
-import { ref, toRaw } from 'vue';
+import { ref, toRaw, getCurrentInstance } from 'vue';
 import { penProps } from '@/services/defaults';
  
+const { proxy } = getCurrentInstance();
+const $t = proxy.$t
+
 const props = defineProps<{
   visible: boolean;
 }>();
@@ -125,9 +128,9 @@ function getList(){
 
 const columns = [
     { colKey: 'row-select', type: 'single',width:50 },
-    { colKey: 'label', title: '名称' },
-    { colKey: 'value', title: 'key名' ,width:150},
-    { colKey: 'type', title: '类型' ,width:80},
+    { colKey: 'label', title: $t('名称') },
+    { colKey: 'value', title: $t('key名') ,width:150},
+    { colKey: 'type', title: $t('类型') ,width:80},
 ];
 
 let data = ref(penProps);

+ 3 - 3
src/views/components/common/PenPropModal.vue

@@ -1,7 +1,7 @@
 <template>
   <t-dialog
     v-model:visible="props.visible"
-    header="关联图元属性"
+    :header="$t('关联图元属性')"
     :width="580"
     @close="close"
     @confirm="confirm"
@@ -16,7 +16,7 @@
             v-model="search"
             @change="onSearch"
             @enter="onSearch"
-            placeholder="搜索图元"
+            :placeholder="$t('搜索图元')"
           />
         </div>
         <div class="props-tree mt-8">
@@ -48,7 +48,7 @@
             v-model="propSearch"
             @change="onPropSearch"
             @enter="onPropSearch"
-            placeholder="搜索属性"
+            :placeholder="$t('搜索属性')"
           />
         </div>
         <div class="props mt-8">

+ 316 - 0
src/views/components/common/ProjectCPayModal.vue

@@ -0,0 +1,316 @@
+<template>
+  <t-dialog
+    v-if="props.visible"
+    v-model:visible="props.visible"
+    class="deploy-dialog"
+    :close-on-overlay-click="false"
+    :top="95"
+    :width="700"
+    :cancel-btn="null"
+    :confirm-btn="{
+      content: !props.data.checked ? '直接下载' : '去支付',
+      style: {
+        width: '110px',
+        fontWeight: 400,
+      },
+    }"
+    @cancel="close"
+    @close="close"
+    @confirm="prePay"
+  >
+    <template #header>
+      <div>
+        <span style="vertical-align: middle; font-size: 16px">{{
+          props.data.title
+        }}</span>
+        <!-- <a
+            :href="'#'"
+            target="_blank"
+            class="hover"
+          >
+            <span class="deploy-tip">如何部署集成?></span>
+          </a> -->
+      </div>
+    </template>
+    <!-- <div v-if="!data.publish&&!data.vip" class="pay-box flex">
+     <div class="pay-title">{{ payListDialog.title }}</div>
+     <div class="pay-title pay-tip" v-html="payListDialog.tip"></div>
+     <div class="pay-price" :style="{
+        color:  data.deploy.price <=0?'#6e7b9199':''
+     }">
+       <p>¥</p>
+       <p>{{ data.deploy.price }}</p>
+     </div>
+   </div> -->
+    <div class="pay-box pay-diagram">
+      <div class="pay-up flex">
+        <div class="pay-title">
+          <t-checkbox v-model="props.data.checked"> 企业图形库 </t-checkbox>
+          <ChevronDownIcon
+            @click="props.data.expend = false"
+            v-if="props.data.expend"
+          />
+          <ChevronRightIcon @click="props.data.expend = true" v-else />
+        </div>
+        <div class="pay-title pay-tip">
+          {{
+            props.data.checked
+              ? '图库去水印,一次购买,永久授权'
+              : '图库含水印,或显示为空,无商业授权'
+          }}
+        </div>
+        <div
+          class="pay-price"
+          :style="{
+            color: props.data.checked ? '' : '#6e7b9199',
+          }"
+        >
+          <p>¥</p>
+          <p>{{ props.data.checked ? props.data.price : 0 }}</p>
+        </div>
+      </div>
+      <div v-if="props.data.expend" class="pay-down">
+        <div v-for="item in props.data.list">
+          <img v-if="item.img" :src="item.img" />
+          <div v-else-if="item.svg" v-html="item.svg"></div>
+          <svg v-else class="l-icon" aria-hidden="true">
+            <use :xlink:href="'#' + iotPensMap[item.name]?.icon"></use>
+          </svg>
+        </div>
+        <p v-if="props.data.payAll">
+          【说明】检查到当前项目为老版本格式,需要购买整套SVG线性图元。
+        </p>
+      </div>
+    </div>
+    <div class="pay-footer">
+      合计
+      <div
+        :style="{
+          color: !props.data.checked ? '#6e7b9199' : '',
+        }"
+      >
+        <p>¥</p>
+        <p>{{ props.data.checked ? props.data.price : 0 }}</p>
+      </div>
+    </div>
+  </t-dialog>
+  <t-dialog
+    v-if="wechatPayDialog.show"
+    v-model:visible="wechatPayDialog.show"
+    class="pay-dialog"
+    header="乐吾乐收银台"
+    :close-on-overlay-click="false"
+    :top="95"
+    :width="700"
+    confirm-btn="支付完成"
+    :cancel-btn="null"
+    @close="wechatPayDialog.show = false"
+    :footer="false"
+  >
+    <Pay
+      :order="wechatPayDialog.order"
+      :alipay-url="wechatPayDialog.order.alipayUrl"
+      :code-url="wechatPayDialog.order.codeUrl"
+      @success="onSuccess"
+    />
+  </t-dialog>
+</template>
+
+<script lang="ts" setup>
+import { formComponents } from '@/services/defaults';
+import { reactive, watch } from 'vue';
+import Pay from '../Pay.vue';
+import axios from 'axios';
+import { ChevronRightIcon, ChevronDownIcon } from 'tdesign-icons-vue-next';
+import { MessagePlugin } from 'tdesign-vue-next';
+
+const emit = defineEmits(['update:visible', 'change', 'success']);
+
+function close() {
+  emit('update:visible', false);
+}
+const props = defineProps<{
+  visible: boolean;
+  data: {
+    price: number;
+    payAll: boolean;
+    list: any[];
+    title: string;
+    checked: boolean;
+    expend?: boolean;
+    // purchasedList: any[];
+    payList?: any[];
+  };
+  token?: '';
+}>();
+
+watch(
+  () => props.visible,
+  (val) => {
+    if (val) {
+      getIotPensMap();
+    }
+  }
+);
+
+const wechatPayDialog = reactive({
+  show: false,
+  order: null,
+});
+
+const iotPensMap = {};
+
+const getIotPensMap = () => {
+  formComponents.forEach((item) => {
+    item.list.forEach((_item) => {
+      iotPensMap[_item.data.name] = { name: _item.name, icon: _item.icon };
+    });
+  });
+};
+
+const prePay = async () => {
+  if (props.data.checked) {
+    const res: any = await axios.post('/api/order/resource/submit', {
+      goods: {},
+      '2ds': props.data.payList,
+      collection: 'v',
+      // id:meta2d.store.data.id
+    });
+    wechatPayDialog.show = true;
+    wechatPayDialog.order = res;
+  } else {
+    emit('update:visible', false);
+    emit('change', true);
+  }
+};
+
+const onSuccess = (success: boolean) => {
+  finishPay();
+  emit('success', success);
+};
+
+const finishPay = async () => {
+  MessagePlugin.success('支付成功');
+  wechatPayDialog.show = false;
+  emit('update:visible', false);
+  emit('change', true);
+};
+</script>
+<style lang="postcss" scoped>
+.pay-box {
+  background: rgba(175, 202, 255, 0.04);
+  border-radius: 4px;
+  margin-bottom: 24px;
+  padding: 20px 24px;
+  position: relative;
+  display: flex;
+  .pay-up {
+    display: flex;
+  }
+  :deep(.t-checkbox__label) {
+    font-size: 16px;
+    color: #6e7b91;
+    margin-left: 16px;
+  }
+
+  .pay-title {
+    font-size: 16px;
+    color: #6e7b91;
+    .t-icon {
+      margin-top: -5px;
+      margin-left: 12px;
+    }
+    :deep(.t-checkbox__input) {
+      background: #fff0;
+    }
+    :deep(.t-is-checked) {
+      .t-checkbox__input {
+        background: var(--color-primary);
+      }
+    }
+  }
+  .pay-tip {
+    flex: 1;
+    text-align: right;
+    margin-right: 100px;
+    color: #fa541c;
+  }
+  .pay-price {
+    position: absolute;
+    right: 24px;
+    color: #4480f9;
+    display: flex;
+    p:nth-child(2) {
+      font-size: 20px;
+    }
+  }
+
+  .pay-down {
+    margin-top: 16px;
+    display: flex;
+    flex-wrap: wrap;
+    /* justify-content:space-between; */
+    /* padding-top:24px; */
+    max-height: 200px;
+    /* overflow-y: scroll; */
+    & > div {
+      width: 48px;
+      height: 48px;
+      background: #1c283b;
+      border-radius: 4px;
+      margin-right: 2.5px;
+      margin-bottom: 2.5px;
+      &:nth-child(12n) {
+        margin-right: 0px;
+      }
+      img {
+        margin: 4px;
+        width: 40px;
+        height: 40px;
+      }
+      .l-icon {
+        /* width: 80px; */
+        /* height: 80px; */
+        /* margin:8px; */
+        font-size: 24px;
+        margin: 12px;
+      }
+      & > div {
+        margin: 8px 12px;
+      }
+
+      :deep(svg) {
+        path {
+          stroke: var(--color) !important;
+        }
+      }
+    }
+    & > p {
+      display: block;
+      margin-top: 8px;
+      width: 100%;
+    }
+  }
+}
+.pay-footer {
+  display: flex;
+  margin-bottom: -48px;
+  margin-left: 24px;
+  font-size: 14px;
+  & > div {
+    color: #4480f9;
+    display: flex;
+    p:nth-child(2) {
+      font-size: 24px;
+      font-weight: 800;
+      margin-top: -3px;
+    }
+    p:nth-child(1) {
+      margin: 0px 4px;
+    }
+  }
+}
+.pay-diagram {
+  flex-direction: column;
+}
+</style>

+ 153 - 0
src/views/components/common/ProjectModal.vue

@@ -0,0 +1,153 @@
+<template>
+   <t-dialog
+      v-model:visible="props.visible"
+      :header="title[props.type]"
+      class="project-dialog"
+      :width="700"
+      @close="close"
+      @confirm="confirm"
+    >
+      <div class="flex box-list"  style="flex-wrap:wrap;justify-content: flex-start;">
+          <template v-if="replaceDialog.list?.length">
+            <div class="box" :class="{active:replaceDialog.select?.id===_item.id}" @click="selectScene(_item)" v-for="_item in replaceDialog.list">
+              <div class="box-img">
+                <img :src="_item.image"></img>
+              </div>
+              <div class="item-title" :title="_item.name">{{_item.name}}</div>
+            </div>
+          </template>
+          <template v-else >
+             <div class="item-title" style="height:50px;">
+               {{$t('暂无数据')}}
+             </div>
+          </template>
+        </div>
+      <t-pagination :pageSizeOptions="[15,30,60]" :total="replaceDialog.total"  v-model="replaceDialog.current" :pageSize="replaceDialog.pageSize" @change="pageChange" />
+    </t-dialog>
+</template>
+
+<script lang='ts' setup>
+import { reactive,defineComponent,ref, onMounted, watch } from 'vue';
+import { getCollectionList } from '@/services/api';
+
+const props = defineProps<{
+  visible: boolean;
+  type: string;
+}>();
+const emit = defineEmits(['update:visible', 'change']);
+
+function close() {
+  emit('update:visible', false);
+}
+
+const replaceDialog = reactive<any>({
+  list:[],
+  current:1,
+  pageSize:15,
+  total:0,
+  collection:'v',
+  select:{
+
+  }
+});
+
+const title = {
+  template:'模版列表',
+  c:'选择组件'
+}
+
+watch(()=>props.visible,(val)=>{
+  if(val){
+    getList(props.type);
+  }
+})
+
+const getList = async(e?:string) => {
+   let collection = e;
+   if(!collection){
+     collection = replaceDialog.collection;
+   }else{
+    replaceDialog.collection = e;
+    replaceDialog.current = 1;
+   }
+   const data = {
+      template:false,
+      projection: 'name,image,id',
+   };
+
+   if(collection === 'template'){
+     collection = 'v';
+     data.template = true;
+   }
+  
+  const config = {
+    params: {
+      current: replaceDialog.current,
+      pageSize: replaceDialog.pageSize,
+    },
+  };
+  //2.请求所有图纸/组件数据
+  const res: any = await getCollectionList(collection, data, config);
+  replaceDialog.list = res.list;
+  replaceDialog.total = res.total;
+}
+
+const pageChange = (e) => {
+  replaceDialog.current = e.current;
+  replaceDialog.pageSize = e.pageSize;
+  getList();
+}
+
+const confirm = () => {
+  emit('change',replaceDialog.select);
+}
+
+const selectScene = (_item) => {
+  replaceDialog.select = _item;
+}
+</script>
+<style lang='postcss' scoped>
+  .box-list{
+       flex-wrap: wrap;
+       padding:8px;
+       margin-bottom:12px;
+       background-color: var(--color-background-active);
+       overflow:scroll;
+       max-height:375px;
+       min-height: 350px;
+      .box{
+        width:125px;
+        height:110px;
+        padding:8px;
+        .box-img{
+          width:100%;
+          height:70px;
+        }
+        /* .title{
+          text-align:center; 
+           white-space: nowrap;
+           overflow: hidden;
+           text-overflow:ellipsis;
+           font-size:12px;
+           color: var(--td-text-color-secondary);
+        } */
+        &:hover{
+          border-radius: 4px;
+          border:1px solid var(--color-primary);
+        }
+      }
+
+      .item-title{
+        text-align:center; 
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow:ellipsis;
+        font-size:12px;
+        color: var(--td-text-color-secondary);
+      }
+      .active{
+        border-radius: 4px;
+        border:1px solid var(--color-primary);
+      }
+    }
+</style>

+ 282 - 0
src/views/components/common/ProjectPayModal.vue

@@ -0,0 +1,282 @@
+<template>
+  <t-dialog
+    v-if="props.visible"
+    v-model:visible="props.visible"
+    class="zip-dialog"
+    :close-on-overlay-click="false"
+    :top="95"
+    :width="700"
+    :cancel-btn="null"
+    :confirm-btn="{
+          content: props.data.checked?'去支付':'直接下载',
+          style: {
+            width: '110px',
+          } }"
+    @close="close"
+    @confirm="prePay"
+  >
+  <template #header>
+        <div>
+          <span style="vertical-align: middle">导出工程Zip包</span>
+            <a
+            :href="``"
+            target="_blank"
+            class="hover"
+            >
+              <span class="deploy-tip"> 说明?></span>
+            </a>
+        </div>
+   </template>
+   <div class="pay-box pay-diagram" style="font-size: 14px;
+    line-height: 30px;
+    color: #6E7B91;">
+    Zip包仅包含数据文件和图片文件,不包含js等依赖库。
+    <div >- 注意</div>Zip包用于平台(已包含js等依赖库)的快捷导入导出工程,无法作为独立部署包使用。
+    <p v-if="props.data.hasJs">
+      <p>- 当前图纸包含js企业图形依赖库。</p>
+      项目中打开如果没有js企业图形依赖库,对应的图元将无法显示。
+      推荐下载为离线部署包(包含js企业图形依赖库)。
+  </p>
+   </div>
+   <div class="pay-box pay-diagram">
+    <div class="pay-up">
+     <div class="pay-title">
+      <t-checkbox v-model="props.data.checked"> 企业图形库  
+      
+    </t-checkbox>
+    <ChevronDownIcon @click="data.expend=false" v-if="data.expend" />
+      <ChevronRightIcon @click="data.expend=true" v-else />
+     </div>
+     <div class="pay-title pay-tip" >
+      {{ props.data.checked?'图库去水印,一次购买,永久授权':'图库含水印,或显示为空,无商业授权' }}
+     </div>
+     <div class="pay-price" :style="{
+       color:props.data.checked?'':'#6e7b9199'
+     }">
+       <p>¥</p>
+       <p>{{ props.data.checked?props.data.price:0 }}</p>
+     </div>
+    </div>
+    <div v-if="data.expend" class="pay-down">
+        <div v-for="item in props.data.list">
+          <img :src="item" />
+        </div>
+    </div>
+   </div>
+   <div class="pay-footer">
+    合计
+    <div  :style="{
+       color:props.data.checked?'':'#6e7b9199'
+     }">
+       <p>¥</p>
+       <p>{{props.data.checked? props.data.price:0 }}</p>
+     </div>
+   </div>
+</t-dialog>
+<t-dialog
+    v-if="wechatPayDialog.show"
+    v-model:visible="wechatPayDialog.show"
+    class="pay-dialog"
+    header="乐吾乐收银台"
+    :close-on-overlay-click="false"
+    :top="95"
+    :width="700"
+    confirm-btn="支付完成"
+    :cancel-btn="null"
+    @close="wechatPayDialog.show = false"
+    :footer="false"
+  >
+    <Pay
+      :order="wechatPayDialog.order"
+      :alipay-url="wechatPayDialog.order.alipayUrl"
+      :code-url="wechatPayDialog.order.codeUrl"
+      @success="onSuccess"
+    />
+  </t-dialog>
+</template>
+
+<script lang='ts' setup>
+import { reactive,defineComponent,ref } from 'vue';
+import { ChevronRightIcon, ChevronDownIcon} from 'tdesign-icons-vue-next';
+import axios from 'axios';
+import Pay from '../Pay.vue';
+import { MessagePlugin } from 'tdesign-vue-next';
+
+const data = reactive({
+  // checked:true,
+  // price:0,
+  expend:false,
+  // hasJs:false
+});
+
+const props = defineProps<{
+  visible: boolean;
+  data:{
+    hasJs:boolean;
+    price:number;
+    list:string[];
+    checked:boolean;
+  }
+}>();
+
+const wechatPayDialog = reactive({
+  show: false,
+  order:null
+});
+
+const emit = defineEmits(['update:visible', 'change','success']);
+
+function close() {
+  emit('update:visible', false);
+}
+
+
+const prePay = async ()=>{
+  if(props.data.checked){
+    const res: any = await axios.post('/api/order/resource/submit', {
+     goods:{},
+    '2ds':props.data.list.map((item)=>{return { type: '图片图元', name: item}}),
+     collection:'v',
+     id:meta2d.store.data.id
+    });
+    wechatPayDialog.show = true;
+    wechatPayDialog.order = res;
+  }else {
+    //直接下载
+    emit('update:visible', false);
+    emit('change',true);
+  }
+}
+
+const onSuccess = (success: boolean) => {
+  finishPay();
+  emit('success', success);
+};
+
+const finishPay = async () => {
+    MessagePlugin.success('支付成功');
+    wechatPayDialog.show = false;
+    emit('update:visible', false);
+    emit('change',true);
+}
+</script>
+<style lang='postcss' scoped>
+.pay-box{
+  background: rgba(175,202,255,0.04);
+  border-radius: 4px;
+  margin-bottom:24px;
+  padding:20px 24px;
+  position:relative;
+  display:flex;
+  .pay-up{
+    display:flex;
+  }
+ :deep(.t-checkbox__label){
+  font-size: 16px;
+    color: #6e7b91;
+    margin-left:16px;
+ }
+
+  .pay-title{
+    font-size: 16px;
+    color: #6e7b91;
+    .t-icon{
+      margin-top:-5px;
+      margin-left:12px;
+    }
+    :deep(.t-checkbox__input){
+      background:#fff0;
+    }
+    :deep(.t-is-checked){
+      .t-checkbox__input{
+        background:var(--color-primary);
+      }
+    }
+  }
+  .pay-tip{
+    flex:1;
+    text-align:right;
+    margin-right: 100px;
+    color: #fa541c;
+  }
+  .pay-price{
+    position:absolute;
+    right:24px;
+    color:#4480F9;
+    display:flex;
+    p:nth-child(2){
+      font-size: 20px;
+    }
+  }
+
+  .pay-down{
+    margin-top:16px;
+    display:flex;
+    flex-wrap:wrap;
+    /* justify-content:space-between; */
+    /* padding-top:24px; */
+    max-height: 200px;
+    /* overflow-y: scroll; */
+    &>div{
+      width: 48px;
+      height: 48px;
+      background: #1c283b;
+      border-radius: 4px;
+      margin-right:2.5px;
+      margin-bottom:2.5px;
+      &:nth-child(12n){
+        margin-right: 0px;
+      }
+      img{
+        margin:4px;
+        width:40px;
+        height:40px;
+      }
+      .l-icon{
+        /* width: 80px; */
+        /* height: 80px; */
+        /* margin:8px; */
+        font-size:24px;
+        margin:12px;
+      }
+      &>div{
+        margin: 8px 12px;
+      }
+
+      :deep(svg){
+        path{
+          stroke: var(--color) !important;
+        }
+      }
+    }
+    &>p{
+      display: block;
+      margin-top:8px;
+      width:100%;
+    }
+  }
+
+}
+.pay-footer{
+  display:flex;
+  margin-bottom: -48px;
+  margin-left:24px;
+  font-size: 14px;
+  &>div{
+    color:#4480F9;
+    display:flex;
+    p:nth-child(2){
+      font-size: 24px;
+      font-weight: 800;
+      margin-top:-3px;
+    }
+    p:nth-child(1){
+      margin:0px 4px;
+    }
+  }
+}
+.pay-diagram{
+    flex-direction:column;
+}
+
+</style>

+ 18 - 15
src/views/components/common/PropModal.vue

@@ -1,13 +1,13 @@
 <template>
   <t-dialog
     v-model:visible="props.visible"
-    header="设备属性"
+    :header="$t('设备属性')"
     :width="470"
     @close="close"
     @confirm="confirm"
   >
   <t-tabs v-model="tabValue" @change="tabChange">
-    <t-tab-panel :value="1" label="数据列表">
+    <t-tab-panel :value="1" :label="$t('数据列表')">
     <div class="input-search mt-8">
       <div class="btn">
         <search-icon class="hover" />
@@ -16,7 +16,7 @@
         v-model="search"
         @change="onSearch"
         @enter="onSearch"
-        placeholder="搜索属性"
+        :placeholder="$t('搜索属性')"
       />
     </div>
     <div class="props mt-8">
@@ -31,28 +31,28 @@
       />
     </div>
     </t-tab-panel>
-    <t-tab-panel :value="2" label="自定义数据">
+    <t-tab-panel :value="2" :label="$t('自定义数据')">
       <div class="form-item mt-16">
-        <label>显示名称</label>
+        <label>{{$t('显示名称')}}</label>
         <t-input
             v-model:value="activeObj.label"
-            placeholder="属性简短描述"
+            :placeholder="$t('属性简短描述')"
           />
       </div>
       <div class="form-item mt-16">
-      <label>属性名</label>
+      <label>{{$t('属性名')}}</label>
       <t-input
         v-model:value="activeObj.value"
-        placeholder="属性名"
+        :placeholder="$t('属性名')"
       />
     </div>
     <div class="form-item mt-16">
-      <label>类型</label>
+      <label>{{$t('类型')}}</label>
       <t-select
         class="w-full"
         :options="typeOptions"
         v-model="activeObj.type"
-        placeholder="字符串"
+        :placeholder="$t('字符串')"
       />
     </div>
     </t-tab-panel>
@@ -62,10 +62,13 @@
 
 <script lang="ts" setup>
 import { SearchIcon } from 'tdesign-icons-vue-next';
-import { ref, toRaw, onMounted } from 'vue';
+import { ref, toRaw, onMounted, getCurrentInstance } from 'vue';
 import { typeOptions } from '@/services/common';
 import { MessagePlugin } from 'tdesign-vue-next';
 
+const { proxy } = getCurrentInstance();
+const $t = proxy.$t
+
 const props = defineProps<{
   visible: boolean;
 }>();
@@ -90,7 +93,7 @@ const getBindTreeData = () => {
       item.checkable = false;
     });
     data.push({
-      label: '物联网平台',
+      label: $t('物联网平台'),
       value: 'iot',
       checkable: false,
       children: iotTree,
@@ -99,7 +102,7 @@ const getBindTreeData = () => {
   const sqlTree = meta2d.store.data.sqls || [];
   if (sqlTree.length) {
     data.push({
-      label: 'sql数据源',
+      label: $t('sql数据源'),
       value: 'sql',
       checkable: false,
       children: sqlTree,
@@ -197,9 +200,9 @@ const tabValue = ref(1);
 const confirm = () => {
   if(!activeObj.value.value){
     if(tabValue.value===1){
-      MessagePlugin.info("请选择一项设备属性!");
+      MessagePlugin.info($t("请选择一项设备属性!"));
     }else{
-      MessagePlugin.info("属性名必填!");
+      MessagePlugin.info($t("属性名必填!"));
     }
     return;
   }

+ 94 - 0
src/views/components/common/StepModal.vue

@@ -0,0 +1,94 @@
+<template>
+  <t-dialog
+    v-model:visible="props.visible"
+    :header="$t('下载进度')"
+    class="project-dialog"
+    :width="700"
+    :closeBtn="false"
+  >
+    <div class="task-steps-container">
+      <t-steps class="task-steps" :current="3">
+        <t-step-item
+          v-for="step in props.tasks"
+          :title="$t(step.title)"
+          :status="step.status"
+        >
+          <template #icon>
+            <StopIcon v-if="step.status === 'prepare'" />
+            <LoadingIcon
+              v-else-if="step.status === 'process'"
+              class="text-color-primary"
+            />
+            <CheckCircleIcon
+              v-else-if="step.status === 'success'"
+              class="text-color-success"
+            />
+            <ErrorCircleIcon
+              v-else-if="step.status === 'warn'"
+              class="text-color-warning"
+            />
+            <CloseCircleIcon
+              v-else-if="step.status === 'error'"
+              class="text-color-error"
+            />
+          </template>
+        </t-step-item>
+      </t-steps>
+    </div>
+    <template #footer>
+      <t-button v-if="props.reload" theme="primary" @click="reDownload">{{$t('重新下载')}} </t-button>
+      <t-popconfirm theme="default" :content="$t('确认取消吗?')" @confirm="close">
+        <t-button theme="default">{{$t('取消任务')}} </t-button>
+      </t-popconfirm>
+    </template>
+  </t-dialog>
+</template>
+
+<script lang="ts" setup>
+import {
+  CheckCircleIcon,
+  CloseCircleIcon,
+  ErrorCircleIcon,
+  LoadingIcon,
+  StopIcon,
+} from 'tdesign-icons-vue-next';
+
+const emit = defineEmits(['update:visible', 'cancel','reDownload']);
+
+function close() {
+  emit('update:visible', false);
+  emit('cancel');
+}
+
+const props = defineProps<{
+  visible: boolean;
+  tasks: any[];
+  reload:boolean;
+}>();
+
+function reDownload() {
+  emit('reDownload',true); 
+}
+
+</script>
+<style lang="postcss" scoped>
+.task-steps-container {
+  padding-top: 30px;
+  height: 100px;
+}
+.text-color-success {
+  color: var(--color-success);
+}
+.task-steps {
+  :deep(.t-steps-item--success) {
+    .t-steps-item__title {
+      color: var(--color-success);
+    }
+  }
+  :deep(.t-steps-item) {
+    .t-steps-item__title {
+      font-size: 14px;
+    }
+  }
+}
+</style>

+ 2 - 2
vite.config.ts

@@ -51,12 +51,12 @@ export default defineConfig({
       // '/image': 'https://v.le5le.com/',
       // '/file': 'https://v.le5le.com/',
       // '/api': 'http://192.168.110.141:7000/',
-      // '/api': 'http://192.168.1.137:9900/api-dashboard',
+      // '/file': 'http://192.168.110.141:7000/',
       // '/api': 'https://v.le5le.com/',
       // '/v/material': 'https://v.le5le.com/',
       // '/png': 'https://v.le5le.com/',
       // '/svg': 'https://v.le5le.com/',
-      // '/view': 'https://v.le5le.com/',
+      // // '/view': 'https://v.le5le.com/',
       // '/api/tools':'https://v.le5le.com/'
     },
   },

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff