Jelajahi Sumber

relase: v1.1.0

gemercheung 1 tahun lalu
induk
melakukan
3d24656ff4
100 mengubah file dengan 24548 tambahan dan 1401 penghapusan
  1. 4 0
      .env
  2. 3 0
      .env.development
  3. 3 0
      .env.production
  4. 3 0
      .env.uat
  5. 4 0
      .gitmodules
  6. 416 0
      demo.json
  7. 4 0
      package.json
  8. 373 236
      pnpm-lock.yaml
  9. 3 3
      public/android-download/app-download.html
  10. TEMPAT SAMPAH
      public/image/logo_sp.png
  11. 4 0
      public/map/location/location_b.svg
  12. 5 0
      public/map/location/location_n.svg
  13. 5 0
      public/map/location/location_o.svg
  14. 6 0
      public/map/location/location_y.svg
  15. 51 0
      public/test.html
  16. 0 1
      src/App.vue
  17. TEMPAT SAMPAH
      src/assets/avatar.png
  18. TEMPAT SAMPAH
      src/assets/empty__no_rights.png
  19. 8 0
      src/assets/frame.svg
  20. 5 0
      src/assets/location.svg
  21. 7 0
      src/assets/location_n.svg
  22. 7 0
      src/assets/location_o.svg
  23. 5 0
      src/assets/panorama.svg
  24. 8 0
      src/assets/pic_edit.svg
  25. 5 0
      src/assets/pic_pen.svg
  26. 11 0
      src/assets/state_gps.svg
  27. 5 0
      src/assets/state_gps_d.svg
  28. 5 0
      src/assets/vector.svg
  29. 19 21
      src/components/single-input.vue
  30. 1318 0
      src/lib/board/4dmap.d.ts
  31. 14019 0
      src/lib/board/4dmap.js
  32. 27 0
      src/lib/board/4dmap.umd.cjs
  33. 36 0
      src/request/URL.ts
  34. 40 0
      src/request/drawing.ts
  35. 43 12
      src/request/index.ts
  36. 80 0
      src/request/organization.ts
  37. 34 0
      src/request/type.ts
  38. 103 0
      src/request/users.ts
  39. 84 3
      src/router.ts
  40. 15 0
      src/store/organization.ts
  41. 41 0
      src/store/polygons.ts
  42. 17 3
      src/store/relics.ts
  43. 114 29
      src/store/scene.ts
  44. 9 6
      src/store/user.ts
  45. 16 12
      src/style.scss
  46. 27 0
      src/util/index.ts
  47. 2 0
      src/util/pc4xlsl.ts
  48. 2 0
      src/util/regex.ts
  49. 17 0
      src/util/tree.ts
  50. 29 6
      src/view/device.vue
  51. 141 15
      src/view/layout/nav.vue
  52. 30 2
      src/view/layout/slide/index.vue
  53. 156 71
      src/view/login.vue
  54. 433 0
      src/view/map/coord.vue
  55. 104 0
      src/view/map/install.ts
  56. 325 0
      src/view/map/layout.vue
  57. 0 67
      src/view/map/manage.ts
  58. 0 283
      src/view/map/map-right.vue
  59. 0 340
      src/view/map/map.vue
  60. 0 0
      src/view/map/openlayer/hot.ts
  61. 3 1
      src/view/map/index.ts
  62. 115 0
      src/view/map/openlayer/manage.ts
  63. 1 0
      src/view/map/tile.ts
  64. 3 1
      src/view/map/pc4Helper.ts
  65. 317 0
      src/view/map/polygons.vue
  66. 23 0
      src/view/no-persession.vue
  67. 186 0
      src/view/organization-add.vue
  68. 89 0
      src/view/organization-edit.vue
  69. 202 0
      src/view/organization.vue
  70. 90 21
      src/view/pano/pano.vue
  71. 28 0
      src/view/quisk.ts
  72. 364 0
      src/view/register/register.vue
  73. 322 0
      src/view/register/reset.vue
  74. 4 2
      src/view/relics-edit.vue
  75. 6 3
      src/view/relics.vue
  76. 29 24
      src/view/scene-select.vue
  77. 25 57
      src/view/scene.vue
  78. 176 0
      src/view/step-tree-v2/StepTree.vue
  79. 689 0
      src/view/step-tree-v2/example/data/1.json
  80. 161 0
      src/view/step-tree-v2/example/data/2.json
  81. 689 0
      src/view/step-tree-v2/example/data/3.json
  82. 65 0
      src/view/step-tree-v2/example/data/4.json
  83. 811 0
      src/view/step-tree-v2/example/data/5.json
  84. 191 0
      src/view/step-tree-v2/example/data/6.json
  85. 1 0
      src/view/step-tree-v2/example/data/7.json
  86. 1 0
      src/view/step-tree-v2/example/data/8.json
  87. 380 0
      src/view/step-tree-v2/example/data/9.json
  88. 109 0
      src/view/step-tree-v2/example/example.vue
  89. TEMPAT SAMPAH
      src/view/step-tree-v2/example/image/c.png
  90. TEMPAT SAMPAH
      src/view/step-tree-v2/example/image/g.png
  91. TEMPAT SAMPAH
      src/view/step-tree-v2/example/image/p.png
  92. TEMPAT SAMPAH
      src/view/step-tree-v2/example/image/x.png
  93. 126 0
      src/view/step-tree-v2/example/step.vue
  94. 264 0
      src/view/step-tree-v2/helper-v2.ts
  95. 355 0
      src/view/step-tree-v2/tree-helper.ts
  96. 11 0
      src/view/step-tree-v2/type.ts
  97. 205 0
      src/view/step-tree/StepTree.vue
  98. 120 163
      src/view/step-tree/example/data.ts
  99. 156 19
      src/view/step-tree/example/example.vue
  100. 0 0
      src/view/step-tree/helper.ts

+ 4 - 0
.env

@@ -0,0 +1,4 @@
+VITE_QJ_URL=https://test.4dkankan.com/panorama
+VITE_LASER_URL=https://uat-laser.4dkankan.com/4pc
+VITE_API=https://uat-sp.4dkankan.com/
+

+ 3 - 0
.env.development

@@ -0,0 +1,3 @@
+VITE_QJ_URL=https://test.4dkankan.com/panorama
+VITE_LASER_URL=https://uat-laser.4dkankan.com/4pc
+VITE_API=https://uat-sp.4dkankan.com/

+ 3 - 0
.env.production

@@ -0,0 +1,3 @@
+VITE_QJ_URL=https://test.4dkankan.com/panorama
+VITE_LASER_URL=https://laser.4dkankan.com/4pc
+VITE_API=https://uat-sp.4dkankan.com/

+ 3 - 0
.env.uat

@@ -0,0 +1,3 @@
+VITE_QJ_URL=https://test.4dkankan.com/panorama
+VITE_LASER_URL=https://uat-laser.4dkankan.com/4pc
+VITE_API=https://uat-sp.4dkankan.com/

+ 4 - 0
.gitmodules

@@ -0,0 +1,4 @@
+[submodule "src/submodule"]
+	path = src/submodule
+	url = http://face3d.4dage.com:7005/bill/drawing-board
+	ignore = dirty

+ 416 - 0
demo.json

@@ -0,0 +1,416 @@
+{
+    "relicsId": "233",
+    "data": {
+        "id": "233",
+        "points": [
+            {
+                "x": 121.544638604172,
+                "y": 29.8801039733684,
+                "title": "00000",
+                "id": "3359",
+                "rtk": true
+            },
+            {
+                "x": 121.544669875671,
+                "y": 29.8801484103304,
+                "title": "00001",
+                "id": "3360",
+                "rtk": true
+            },
+            {
+                "x": 121.54466875188,
+                "y": 29.8801826075332,
+                "title": "00002",
+                "id": "3361",
+                "rtk": true
+            },
+            {
+                "x": 121.544700240395,
+                "y": 29.8801648536107,
+                "title": "00003",
+                "id": "3362",
+                "rtk": true
+            },
+            {
+                "x": 121.544714361162,
+                "y": 29.8801529436098,
+                "title": "00004",
+                "id": "3363",
+                "rtk": true
+            },
+            {
+                "x": 121.544691282811,
+                "y": 29.8801754096823,
+                "title": "00005",
+                "id": "3364",
+                "rtk": true
+            },
+            {
+                "x": 121.544701482461,
+                "y": 29.8801944381949,
+                "title": "00006",
+                "id": "3365",
+                "rtk": true
+            },
+            {
+                "x": 121.544719605391,
+                "y": 29.8802215789063,
+                "title": "00007",
+                "id": "3366",
+                "rtk": true
+            },
+            {
+                "x": 121.544672386404,
+                "y": 29.8802430633049,
+                "title": "00008",
+                "id": "3367",
+                "rtk": true
+            },
+            {
+                "x": 121.5447617094,
+                "y": 29.8801935655845,
+                "title": "00009",
+                "id": "3368",
+                "rtk": true
+            },
+            {
+                "x": 121.544739106645,
+                "y": 29.8802478574773,
+                "title": "00010",
+                "id": "3369",
+                "rtk": true
+            },
+            {
+                "x": 121.544605489902,
+                "y": 29.8803451222352,
+                "title": "00037",
+                "id": "3393",
+                "rtk": true
+            },
+            {
+                "x": 121.544605489902,
+                "y": 29.8803451222352,
+                "title": "00038",
+                "id": "3394",
+                "rtk": true
+            },
+            {
+                "x": 121.544556914301,
+                "y": 29.8804757370409,
+                "title": "00039",
+                "id": "3395",
+                "rtk": true
+            },
+            {
+                "x": 121.544508589777,
+                "y": 29.8805015188362,
+                "title": "00040",
+                "id": "3396",
+                "rtk": true
+            },
+            {
+                "x": 121.54446899986,
+                "y": 29.8804290911679,
+                "title": "00041",
+                "id": "3397",
+                "rtk": true
+            },
+            {
+                "x": 121.544493520076,
+                "y": 29.8804052136078,
+                "title": "00042",
+                "id": "3398",
+                "rtk": true
+            },
+            {
+                "x": 121.544473501909,
+                "y": 29.8803784640223,
+                "title": "00043",
+                "id": "3399",
+                "rtk": true
+            },
+            {
+                "x": 121.544592516274,
+                "y": 29.8803681739517,
+                "title": "00045",
+                "id": "3401",
+                "rtk": true
+            },
+            {
+                "x": 121.544580170847,
+                "y": 29.8803233525532,
+                "title": "00046",
+                "id": "3402",
+                "rtk": true
+            },
+            {
+                "x": 121.544536850631,
+                "y": 29.8802916459494,
+                "title": "00047",
+                "id": "3403",
+                "rtk": true
+            },
+            {
+                "x": 121.54451169236,
+                "y": 29.8802616988889,
+                "title": "00048",
+                "id": "3404",
+                "rtk": true
+            },
+            {
+                "x": 121.544345220904,
+                "y": 29.8803154057134,
+                "title": "00052",
+                "id": "3408",
+                "rtk": true
+            },
+            {
+                "x": 121.544400433972,
+                "y": 29.8802802705269,
+                "title": "00053",
+                "id": "3409",
+                "rtk": true
+            },
+            {
+                "x": 121.544456594742,
+                "y": 29.8802371379489,
+                "title": "00054",
+                "id": "3410",
+                "rtk": true
+            },
+            {
+                "x": 121.544868174413,
+                "y": 29.8801198122661,
+                "title": "00068",
+                "id": "3419",
+                "rtk": true
+            },
+            {
+                "x": 121.544963265795,
+                "y": 29.8802543265332,
+                "title": "00069",
+                "id": "3420",
+                "rtk": true
+            },
+            {
+                "x": 121.54480695656,
+                "y": 29.8803615855196,
+                "title": "00078",
+                "id": "3426",
+                "rtk": true
+            },
+            {
+                "rtk": false,
+                "title": "",
+                "x": 121.54407074310784,
+                "y": 29.87918145802906,
+                "id": "3427"
+            },
+            {
+                "rtk": false,
+                "title": "",
+                "x": 121.54566397526268,
+                "y": 29.880023671659735,
+                "id": "3428"
+            },
+            {
+                "rtk": false,
+                "title": "",
+                "x": 121.54547622063164,
+                "y": 29.878832770857123,
+                "id": "3429"
+            },
+            {
+                "rtk": false,
+                "title": "",
+                "x": 121.54384543770915,
+                "y": 29.879862739118842,
+                "id": "3430"
+            },
+            {
+                "rtk": false,
+                "title": "",
+                "x": 121.54541721219188,
+                "y": 29.88012023118427,
+                "id": "3431"
+            },
+            {
+                "rtk": false,
+                "title": "",
+                "x": 121.54502024525767,
+                "y": 29.88111264851978,
+                "id": "3432"
+            },
+            {
+                "rtk": false,
+                "title": "",
+                "x": 121.5441243874467,
+                "y": 29.881482793363837,
+                "id": "3433"
+            },
+            {
+                "rtk": false,
+                "title": "",
+                "x": 121.54557814430986,
+                "y": 29.881708098790984,
+                "id": "3434"
+            },
+            {
+                "rtk": false,
+                "title": "",
+                "x": 121.54551913470709,
+                "y": 29.88038845195565,
+                "id": "3435"
+            },
+            {
+                "rtk": false,
+                "title": "",
+                "x": 121.54380788535559,
+                "y": 29.88063521518502,
+                "id": "3436"
+            },
+            {
+                "rtk": false,
+                "title": "",
+                "x": 121.54397954673254,
+                "y": 29.88136477603707,
+                "id": "3437"
+            },
+            {
+                "rtk": false,
+                "title": "",
+                "x": 121.54412438601935,
+                "y": 29.881493522069785,
+                "id": "3438"
+            },
+            {
+                "rtk": false,
+                "title": "",
+                "x": 121.54412438601935,
+                "y": 29.881493522069785,
+                "id": "3439"
+            },
+            {
+                "rtk": false,
+                "title": "",
+                "x": 121.54468764991248,
+                "y": 29.881820751569602,
+                "id": "3440"
+            }
+        ],
+        "lines": [
+            {
+                "id": "2",
+                "pointIds": [
+                    "3427",
+                    "3428"
+                ]
+            },
+            {
+                "id": "3",
+                "pointIds": [
+                    "3428",
+                    "3429"
+                ]
+            },
+            {
+                "id": "4",
+                "pointIds": [
+                    "3429",
+                    "3430"
+                ]
+            },
+            {
+                "id": "5",
+                "pointIds": [
+                    "3430",
+                    "3431"
+                ]
+            },
+            {
+                "id": "6",
+                "pointIds": [
+                    "3431",
+                    "3432"
+                ]
+            },
+            {
+                "id": "7",
+                "pointIds": [
+                    "3432",
+                    "3433"
+                ]
+            },
+            {
+                "id": "8",
+                "pointIds": [
+                    "3433",
+                    "3434"
+                ]
+            },
+            {
+                "id": "9",
+                "pointIds": [
+                    "3434",
+                    "3435"
+                ]
+            },
+            {
+                "id": "10",
+                "pointIds": [
+                    "3435",
+                    "3436"
+                ]
+            },
+            {
+                "id": "11",
+                "pointIds": [
+                    "3436",
+                    "3437"
+                ]
+            },
+            {
+                "id": "12",
+                "pointIds": [
+                    "3437",
+                    "3438"
+                ]
+            },
+            {
+                "id": "13",
+                "pointIds": [
+                    "3438",
+                    "3439"
+                ]
+            },
+            {
+                "id": "14",
+                "pointIds": [
+                    "3439",
+                    "3440"
+                ]
+            }
+        ],
+        "polygons": [
+            {
+                "id": "2",
+                "name": "xxxkxkxkxk",
+                "lineIds": [
+                    "2",
+                    "3",
+                    "4",
+                    "5",
+                    "6",
+                    "7",
+                    "8",
+                    "9",
+                    "10",
+                    "11",
+                    "12",
+                    "13",
+                    "14"
+                ]
+            }
+        ]
+    }
+}

+ 4 - 0
package.json

@@ -6,6 +6,7 @@
   "scripts": {
     "dev": "vite",
     "build": "vue-tsc && vite build",
+    "build-uat": "vue-tsc && vite build --mode uat",
     "preview": "vite preview"
   },
   "dependencies": {
@@ -15,10 +16,13 @@
     "gl-matrix": "^3.4.3",
     "js-base64": "^3.7.7",
     "jszip": "^3.10.1",
+    "konva": "9.3.6",
     "mitt": "^3.0.1",
     "ol": "^9.1.0",
+    "pinia": "^2.1.7",
     "proj4": "^2.11.0",
     "qrcode": "^1.5.3",
+    "vite-svg-loader": "^5.1.0",
     "vue": "^3.4.21",
     "vue-router": "^4.3.0",
     "xlsx": "^0.18.5"

File diff ditekan karena terlalu besar
+ 373 - 236
pnpm-lock.yaml


+ 3 - 3
public/android-download/app-download.html

@@ -8,7 +8,7 @@
   <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
   <meta http-equiv="expires" content="0">
   <meta content="telephone=no" name="format-detection">
-  <meta name="description" content="世界上首款消费级3D相机—四维看看(4DKanKan)。技术核心三要素:易操作;自动化;高精度。主要应用领域为数字文博、数字地产、数字电商、数字餐饮、数字家居等。">
+  <meta name="description" content="文保1号与文保2号,是四维时代研发的高端数字化设备,专为不可移动文化遗产的调查与保护而设计。">
   <link rel="shortcut icon" href="//4dkk.4dage.com/FDKKIMG/icon/kankan_icon.ico">
   <link rel="icon" type="image/png" href="//4dkk.4dage.com/FDKKIMG/icon/kankan_icon192.png" sizes="192x192">
   <link rel="apple-touch-icon" sizes="180x180" href="//4dkk.4dage.com/FDKKIMG/icon/kankan_icon180.png">
@@ -90,14 +90,14 @@
     }
     var ua = versions()
     var domicon = document.querySelector('.icon-warp i')
-    const version = `1.1.0`
+    const version = `1.2.0`
 
     document.getElementById('btn').addEventListener('click', function (e) {
       if (ua.weixin) {
         alert('微信/QQ内无法下载应用,请点击右上角,选择"浏览器中打开"')
       }
       else {
-        location.href = `http://4dkankan.oss-cn-shenzhen.aliyuncs.com/apps/customApp/wenbaono1/android/app/4dkk_webbaono1_v${version}_arm64.apk`
+        location.href = `https://4dkankan.oss-cn-shenzhen.aliyuncs.com/apps/customApp/wenbaono1/android/app/4dkk_webbaono1_v${version}_arm64.apk`
       }
     })
     document.querySelector('span[itemprop=softwareVersion]').innerHTML = version

TEMPAT SAMPAH
public/image/logo_sp.png


+ 4 - 0
public/map/location/location_b.svg

@@ -0,0 +1,4 @@
+<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M22 44C32.6667 33.891 38 25.891 38 20C38 11.1634 30.8366 4 22 4C13.1634 4 6 11.1634 6 20C6 25.891 11.3333 33.891 22 44Z" fill="#409EFF"/>
+<path d="M22 30C27.5228 30 32 25.5228 32 20C32 14.4772 27.5228 10 22 10C16.4772 10 12 14.4772 12 20C12 25.5228 16.4772 30 22 30Z" fill="white"/>
+</svg>

+ 5 - 0
public/map/location/location_n.svg

@@ -0,0 +1,5 @@
+<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M22 44C32.6667 33.891 38 25.891 38 20C38 11.1634 30.8366 4 22 4C13.1634 4 6 11.1634 6 20C6 25.891 11.3333 33.891 22 44Z" fill="#728191"/>
+<path d="M33.1679 29.8636C35.7879 25.9337 37 22.6592 37 20C37 11.7157 30.2843 5 22 5C13.7157 5 7 11.7157 7 20C7 22.6592 8.21213 25.9337 10.8321 29.8636C13.3275 33.6067 17.0414 37.8575 22 42.6181C26.9586 37.8575 30.6726 33.6067 33.1679 29.8636ZM22 44C11.3333 33.891 6 25.891 6 20C6 11.1634 13.1634 4 22 4C30.8366 4 38 11.1634 38 20C38 25.891 32.6667 33.891 22 44Z" fill="white"/>
+<path d="M22 30C27.5228 30 32 25.5228 32 20C32 14.4772 27.5228 10 22 10C16.4772 10 12 14.4772 12 20C12 25.5228 16.4772 30 22 30Z" fill="white"/>
+</svg>

+ 5 - 0
public/map/location/location_o.svg

@@ -0,0 +1,5 @@
+<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M22 44C32.6667 33.891 38 25.891 38 20C38 11.1634 30.8366 4 22 4C13.1634 4 6 11.1634 6 20C6 25.891 11.3333 33.891 22 44Z" fill="#E6A23C"/>
+<path d="M33.1679 29.8636C30.6726 33.6067 26.9586 37.8575 22 42.6181C17.0414 37.8575 13.3275 33.6067 10.8321 29.8636C8.21213 25.9337 7 22.6592 7 20C7 11.7157 13.7157 5 22 5C30.2843 5 37 11.7157 37 20C37 22.6592 35.7879 25.9337 33.1679 29.8636ZM22 44C32.6667 33.891 38 25.891 38 20C38 11.1634 30.8366 4 22 4C13.1634 4 6 11.1634 6 20C6 25.891 11.3333 33.891 22 44Z" fill="white"/>
+<path d="M22 30C27.5228 30 32 25.5228 32 20C32 14.4772 27.5228 10 22 10C16.4772 10 12 14.4772 12 20C12 25.5228 16.4772 30 22 30Z" fill="white"/>
+</svg>

+ 6 - 0
public/map/location/location_y.svg

@@ -0,0 +1,6 @@
+<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M22 44C32.6667 33.891 38 25.891 38 20C38 11.1634 30.8366 4 22 4C13.1634 4 6 11.1634 6 20C6 25.891 11.3333 33.891 22 44Z" fill="#409EFF"/>
+<path d="M33.1679 29.8636C30.6726 33.6067 26.9586 37.8575 22 42.6181C17.0414 37.8575 13.3275 33.6067 10.8321 29.8636C8.21213 25.9337 7 22.6592 7 20C7 11.7157 13.7157 5 22 5C30.2843 5 37 11.7157 37 20C37 22.6592 35.7879 25.9337 33.1679 29.8636ZM22 44C32.6667 33.891 38 25.891 38 20C38 11.1634 30.8366 4 22 4C13.1634 4 6 11.1634 6 20C6 25.891 11.3333 33.891 22 44Z" fill="white"/>
+<path d="M22 30C27.5228 30 32 25.5228 32 20C32 14.4772 27.5228 10 22 10C16.4772 10 12 14.4772 12 20C12 25.5228 16.4772 30 22 30Z" fill="white"/>
+<path d="M28.2071 16.7929C28.5976 17.1834 28.5976 17.8166 28.2071 18.2071L21 25.4142L16.2929 20.7071C15.9024 20.3166 15.9024 19.6834 16.2929 19.2929C16.6834 18.9024 17.3166 18.9024 17.7071 19.2929L21 22.5858L26.7929 16.7929C27.1834 16.4024 27.8166 16.4024 28.2071 16.7929Z" fill="#409EFF"/>
+</svg>

+ 51 - 0
public/test.html

@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>M3U8 视频播放示例</title>
+    <link href="https://vjs.zencdn.net/7.20.3/video-js.css" rel="stylesheet" />
+    <script src="https://vjs.zencdn.net/7.20.3/video.min.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/videojs-contrib-hls/5.15.0/videojs-contrib-hls.min.js"></script>
+</head>
+
+<body>
+    <h1>M3U8 视频播放示例</h1>
+    <div>
+        <video id="hlsVideo" class="video-js vjs-default-skin vjs-big-play-centered" controls preload="auto"
+            width="100%" height="100%">
+            <source id="source" src="/gear1/prog_index.m3u8" type="application/x-mpegURL" />
+        </video>
+    </div>
+
+    <script>
+        document.addEventListener("DOMContentLoaded", function () {
+            var videoElement = document.getElementById('hlsVideo');
+            var player = videojs(videoElement, {
+                bigPlayButton: true,
+                textTrackDisplay: false,
+                posterImage: false,
+                errorDisplay: false,
+                autoplay: true
+            });
+
+            player.src({
+                src: videoElement.querySelector('#source').getAttribute('src'),
+                type: 'application/x-mpegURL'
+            });
+
+            // 将播放器实例存储在全局变量中以便将来可以访问它
+            window.hlsPlayer = player;
+        });
+
+        // 清理工作,例如卸载播放器
+        window.onunload = function () {
+            if (window.hlsPlayer) {
+                window.hlsPlayer.dispose();
+            }
+        };
+    </script>
+</body>
+
+</html>

+ 0 - 1
src/App.vue

@@ -37,7 +37,6 @@ lifeHook.push({
     if (exixts) {
       clearTimeout(timeout);
       timeout = setTimeout(() => {
-        console.log("close");
         loading!.close();
         loading = null;
         exixts = false;

TEMPAT SAMPAH
src/assets/avatar.png


TEMPAT SAMPAH
src/assets/empty__no_rights.png


+ 8 - 0
src/assets/frame.svg

@@ -0,0 +1,8 @@
+<svg fill="currentColor" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+	<g id="frame">
+		<g id="Union">
+			<path d="M3.5 1C2.11929 1 1 2.11929 1 3.5V5C1 5.27614 1.22386 5.5 1.5 5.5C1.77614 5.5 2 5.27614 2 5V3.5C2 2.67157 2.67157 2 3.5 2H12.5C13.3284 2 14 2.67157 14 3.5V12.5C14 13.3284 13.3284 14 12.5 14H3.5C2.67157 14 2 13.3284 2 12.5V11C2 10.7239 1.77614 10.5 1.5 10.5C1.22386 10.5 1 10.7239 1 11V12.5C1 13.8807 2.11929 15 3.5 15H12.5C13.8807 15 15 13.8807 15 12.5V3.5C15 2.11929 13.8807 1 12.5 1H3.5Z"  />
+			<path d="M7.14645 4.14645C7.34171 3.95118 7.65829 3.95118 7.85355 4.14645L10.8536 7.14645C11.0488 7.34171 11.0488 7.65829 10.8536 7.85355L7.85355 10.8536C7.65829 11.0488 7.34171 11.0488 7.14645 10.8536C6.95118 10.6583 6.95118 10.3417 7.14645 10.1464L9.29289 8H1.5C1.22386 8 1 7.77614 1 7.5C1 7.22386 1.22386 7 1.5 7H9.29289L7.14645 4.85355C6.95118 4.65829 6.95118 4.34171 7.14645 4.14645Z" />
+		</g>
+	</g>
+</svg>

+ 5 - 0
src/assets/location.svg

@@ -0,0 +1,5 @@
+<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
+	<g id="location">
+		<path id="Subtract" fill-rule="evenodd" clip-rule="evenodd" d="M12 23C12 23 21 17.0417 21 10.1667C21 5.10406 16.9706 1 12 1C7.02945 1 3 5.10406 3 10.1667C3 17.0417 12 23 12 23ZM12 14.7499C14.4852 14.7499 16.5 12.6979 16.5 10.1666C16.5 7.63528 14.4852 5.58325 12 5.58325C9.51467 5.58325 7.49995 7.63528 7.49995 10.1666C7.49995 12.6979 9.51467 14.7499 12 14.7499Z" />
+	</g>
+</svg>

File diff ditekan karena terlalu besar
+ 7 - 0
src/assets/location_n.svg


File diff ditekan karena terlalu besar
+ 7 - 0
src/assets/location_o.svg


File diff ditekan karena terlalu besar
+ 5 - 0
src/assets/panorama.svg


+ 8 - 0
src/assets/pic_edit.svg

@@ -0,0 +1,8 @@
+<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="pic_edit">
+<g id="Union">
+<path d="M21.9723 5.99838C21.1912 5.21734 19.9249 5.21733 19.1438 5.99838L8.65834 16.4839C8.30656 16.8357 8.09851 17.3059 8.07482 17.8029L7.93323 20.7729C7.87705 21.9513 8.8478 22.922 10.0262 22.8659L12.9962 22.7243C13.4931 22.7006 13.9634 22.4925 14.3152 22.1407L24.8007 11.6552C25.5817 10.8742 25.5817 9.60786 24.8007 8.82681L21.9723 5.99838Z" fill="#409EFF"/>
+<path d="M6 25C5.44772 25 5 25.4477 5 26C5 26.5523 5.44772 27 6 27H26C26.5523 27 27 26.5523 27 26C27 25.4477 26.5523 25 26 25H6Z" fill="#409EFF"/>
+</g>
+</g>
+</svg>

File diff ditekan karena terlalu besar
+ 5 - 0
src/assets/pic_pen.svg


+ 11 - 0
src/assets/state_gps.svg

@@ -0,0 +1,11 @@
+<svg  viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
+	<g id="state_gps">
+		<g id="Union">
+			<path d="M4.11091 2.61088C4.30617 2.41562 4.30617 2.09903 4.11091 1.90377C3.91565 1.70851 3.59907 1.70851 3.40381 1.90377C0.865398 4.44218 0.865398 8.55775 3.40381 11.0962C3.59907 11.2914 3.91565 11.2914 4.11091 11.0962C4.30617 10.9009 4.30617 10.5843 4.11091 10.3891C1.96303 8.24117 1.96303 4.75876 4.11091 2.61088Z"  />
+			<path d="M12.5962 11.0962C15.1346 8.55775 15.1346 4.44218 12.5962 1.90377C12.4009 1.70851 12.0843 1.70851 11.8891 1.90377C11.6938 2.09903 11.6938 2.41562 11.8891 2.61088C14.037 4.75876 14.037 8.24117 11.8891 10.3891C11.6938 10.5843 11.6938 10.9009 11.8891 11.0962C12.0843 11.2914 12.4009 11.2914 12.5962 11.0962Z"  />
+			<path d="M5.52513 3.31796C5.72039 3.51322 5.72039 3.8298 5.52513 4.02506C4.15829 5.3919 4.15829 7.60797 5.52513 8.97481C5.72039 9.17007 5.72039 9.48665 5.52513 9.68192C5.32986 9.87718 5.01328 9.87718 4.81802 9.68192C3.06066 7.92456 3.06066 5.07531 4.81802 3.31796C5.01328 3.12269 5.32986 3.12269 5.52513 3.31796Z" />
+			<path d="M11.182 3.31796C12.9393 5.07531 12.9393 7.92456 11.182 9.68192C10.9867 9.87718 10.6701 9.87718 10.4749 9.68192C10.2796 9.48665 10.2796 9.17007 10.4749 8.97481C11.8417 7.60797 11.8417 5.3919 10.4749 4.02506C10.2796 3.8298 10.2796 3.51322 10.4749 3.31796C10.6701 3.12269 10.9867 3.12269 11.182 3.31796Z"  />
+			<path d="M8 4.49994C9.10457 4.49994 10 5.39537 10 6.49994C10 7.58489 9.1361 8.46806 8.05866 8.4991L11.3091 14.9999H4.69104L7.94146 8.4991C6.86396 8.46813 6 7.58493 6 6.49994C6 5.39537 6.89543 4.49994 8 4.49994ZM8 5.49994C7.44772 5.49994 7 5.94765 7 6.49994C7 7.05222 7.44772 7.49994 8 7.49994C8.55228 7.49994 9 7.05222 9 6.49994C9 5.94765 8.55228 5.49994 8 5.49994ZM7.99978 10.6166L6.30878 13.9996H9.69078L7.99978 10.6166Z"/>
+		</g>
+	</g>
+</svg>

+ 5 - 0
src/assets/state_gps_d.svg

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="state_gps_d">
+<path id="Union" d="M8 4.50244C9.10457 4.50244 10 5.39787 10 6.50244C10 7.58739 9.1361 8.47057 8.05866 8.5016L11.3091 15.0024H4.69104L7.94146 8.5016C6.86396 8.47063 6 7.58743 6 6.50244C6 5.39787 6.89543 4.50244 8 4.50244ZM8 5.50244C7.44772 5.50244 7 5.95016 7 6.50244C7 7.05473 7.44772 7.50244 8 7.50244C8.55228 7.50244 9 7.05473 9 6.50244C9 5.95016 8.55228 5.50244 8 5.50244ZM7.99978 10.6192L6.30878 14.0022H9.69078L7.99978 10.6192Z" fill="#B3B3B3"/>
+</g>
+</svg>

+ 5 - 0
src/assets/vector.svg

@@ -0,0 +1,5 @@
+<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
+	<g id="vector">
+		<path id="Union" fill-rule="evenodd" clip-rule="evenodd" d="M2 22V2H22V22H2ZM17.1226 4.85714H14.0203L4.85714 14.0203V17.1226L17.1226 4.85714ZM4.85714 4.85714H9.97971L4.85714 9.97971V4.85714ZM14.0203 19.1429H19.1429V14.0203L14.0203 19.1429ZM9.97971 19.1429H6.87747L19.1429 6.87747V9.97971L9.97971 19.1429Z" />
+	</g>
+</svg>

+ 19 - 21
src/components/single-input.vue

@@ -1,17 +1,7 @@
 <template>
-  <el-dialog
-    :model-value="visible"
-    @update:model-value="(val) => emit('update:visible', val)"
-    :title="title"
-    width="500"
-  >
-    <el-input
-      v-model.trim="ivalue"
-      :maxlength="100"
-      show-word-limit
-      type="textarea"
-      placeholder="请输入"
-    />
+  <el-dialog :model-value="visible" @update:model-value="(val) => emit('update:visible', val)" :title="title"
+    width="500">
+    <el-input v-model.trim="ivalue" :maxlength="100" show-word-limit type="textarea" :placeholder="placeholder" />
     <template #footer>
       <div class="dialog-footer">
         <el-button @click="emit('update:visible', false)">取消</el-button>
@@ -25,12 +15,20 @@
 import { ElMessage } from "element-plus";
 import { ref, watchEffect } from "vue";
 
-const props = defineProps<{
-  visible: boolean;
-  value: string;
-  title: string;
-  updateValue: (value: string) => void;
-}>();
+const props = withDefaults(
+  defineProps<{
+    visible: boolean;
+    value: string;
+    title: string;
+    name?: string;
+    placeholder: string;
+    isAllowEmpty?: boolean;
+    updateValue: (value: string) => void;
+  }>(),
+  {
+    placeholder: "请输入",
+  }
+);
 const emit = defineEmits<{
   (e: "update:visible", visible: boolean): void;
 }>();
@@ -41,8 +39,8 @@ watchEffect(() => {
 });
 
 const submit = async () => {
-  if (ivalue.value.length === 0) {
-    return ElMessage.error("点位名称不能为空!");
+  if (ivalue.value.length === 0 && !props.isAllowEmpty) {
+    return ElMessage.error(`${props.name || "点位"}名称不能为空!`);
   }
   await props.updateValue(ivalue.value);
   emit("update:visible", false);

File diff ditekan karena terlalu besar
+ 1318 - 0
src/lib/board/4dmap.d.ts


File diff ditekan karena terlalu besar
+ 14019 - 0
src/lib/board/4dmap.js


File diff ditekan karena terlalu besar
+ 27 - 0
src/lib/board/4dmap.umd.cjs


+ 36 - 0
src/request/URL.ts

@@ -23,3 +23,39 @@ export const exportVectorData = `/relics/excel/vectorData`;
 export const getDevicePage = `/relics/camera/page`;
 export const delDevice = `/relics/camera/del/:deviceId`;
 export const addDevice = `/relics/camera/add`;
+
+// organization
+// export const organizationPage = `/relics/relics/org/page`;
+export const organizationPageList = `/relics/org/treeList`;
+export const organizationPage = `/relics/org/treePage`;
+export const addOrganization = `/relics/org/add`;
+export const delOrganization = `/relics/org/del`;
+export const getOrganizationDetail = `/relics/org/info/:orgId`;
+export const alterOrganization = `/relics/org/update`;
+
+export const registerOrganization = `/relics/org/register`;
+
+// users
+export const addUser = `/relics/user/addUser`;
+export const changeUserStatus = `/relics/user/changeStatus`;
+export const delUser = `/relics/user/del/:userId`;
+export const alterUser = `/relics/user/edit`;
+export const getUserSceneInfo = `/relics/user/getUserInfo`;
+export const getUserInfoById = `/relics/user/info/:id`;
+export const userScenepage = `/relics/user/page`;
+
+export const getMsgAuthCode = `/relics/user/getMsgAuthCode`;
+export const changePassword = `/relics/user/changePassword`;
+
+
+
+///drawing
+
+export const addOrUpdateDrawing = `/relics/relics/drawing/saveOrUpdate`;
+export const delDrawing = `/relics/relics/drawing/del`;
+export const getDrawingDetail = `/relics/relics/drawing/info/:drawingId`;
+export const getDrawingInfoByRelicsId = `/relics/relics/drawing/infoByRelicsId/:drawingId`;
+export const updateDrawing = `/relics/relics/drawing/update`;
+
+//token
+export const getFdTokenByNum = `/relics/scene/getFdTokenByNum?num=`;

+ 40 - 0
src/request/drawing.ts

@@ -0,0 +1,40 @@
+import { sendFetch, PageProps } from './index'
+import * as URL from "./URL";
+import {
+
+    PolygonsAttrib,
+} from "./type";
+
+// 
+export type PolyDataType = {
+    id: string
+    lineIds: string[]
+    name?: string
+
+}
+
+export interface DrawingDataType extends PolygonsAttrib {
+    id?: string;
+    polygons: PolyDataType[],
+}
+
+export type DrawingParamsType = {
+    data: DrawingDataType,
+    relicsId: string
+    drawingId?: string
+}
+
+export const addOrUpdateDrawingFetch = (params: Partial<DrawingParamsType>) =>
+    sendFetch<PageProps<DrawingDataType>>(URL.addOrUpdateDrawing, {
+        method: "post",
+        body: JSON.stringify(params),
+    });
+
+
+
+export const getDrawingDetailFetch = (drawingId: string) =>
+    sendFetch<PageProps<DrawingParamsType>>(
+        URL.getDrawingInfoByRelicsId,
+        { method: "post", body: JSON.stringify({}) },
+        { paths: { drawingId: drawingId } }
+    );

+ 43 - 12
src/request/index.ts

@@ -10,11 +10,20 @@ import {
   Param,
 } from "./state";
 import { ElMessage } from "element-plus";
-import { Relics, Scene, ScenePoint, ResPage, UserInfo, Device } from "./type";
+import {
+  Relics,
+  Scene,
+  ScenePoint,
+  ResPage,
+  UserInfo,
+  Device,
+  // PolygonsAttrib,
+} from "./type";
+// import { getFdTokenByNum } from "./URL";
 
 const error = throttle((msg: string) => ElMessage.error(msg), 2000);
 
-type Other = { params?: Param; paths?: Param };
+type Other = { params?: Param; paths?: Param; useResult?: boolean, noToken?: boolean };
 export const sendFetch = <T>(
   url: string,
   init: RequestInit,
@@ -32,6 +41,9 @@ export const sendFetch = <T>(
       sendUrl =
         sendUrl + "?" + new URLSearchParams({ ...gParams, ...other.params });
     }
+    if (other.noToken) {
+      delete gHeaders['relics-token']
+    }
   }
   lifeHook.forEach(({ start }) => start());
 
@@ -39,9 +51,9 @@ export const sendFetch = <T>(
     ...init,
     headers: headers
       ? {
-          ...headers,
-          ...gHeaders,
-        }
+        ...headers,
+        ...gHeaders,
+      }
       : gHeaders,
   })
     .then((res) => {
@@ -54,14 +66,18 @@ export const sendFetch = <T>(
       }
     })
     .then((data) => {
-      if (data.code !== 0) {
-        error(data.message);
-        errorHook.map((err) => {
-          err(data.code, data.msg);
-        });
-        throw data.message;
+      if (other && other.useResult) {
+        return data
       } else {
-        return data.data;
+        if (data.code !== 0) {
+          error(data.message);
+          errorHook.map((err) => {
+            err(data.code, data.msg);
+          });
+          throw data.message;
+        } else {
+          return data.data;
+        }
       }
     });
 };
@@ -76,6 +92,13 @@ export const loginFetch = (props: LoginProps) =>
     body: JSON.stringify(props),
   });
 
+  export const loginOutFetch = () =>
+    sendFetch<{ user: UserInfo; token: string }>(URL.logout, {
+      method: "post",
+      body: JSON.stringify({}),
+    });
+  
+
 export const userInfoFetch = () =>
   sendFetch<UserInfo>(URL.getUserInfo, { method: "post" });
 
@@ -242,3 +265,11 @@ export const addDeviceFetch = (sn: string) =>
     method: "post",
     body: JSON.stringify({ cameraSn: sn }),
   });
+
+export const getTokenFetch = (num: string) =>
+  sendFetch(URL.getFdTokenByNum + num, {
+    method: "get",
+  });
+
+export * from "./organization";
+export * from "./users";

+ 80 - 0
src/request/organization.ts

@@ -0,0 +1,80 @@
+import { sendFetch, PageProps } from './index'
+import { ResPage, ResResult } from './type'
+import { organizationTypeEnum } from '@/store/organization'
+import * as URL from "./URL";
+import { ElMessage } from "element-plus";
+import { throttle, encodePwd } from "@/util";
+
+const error = throttle((msg: string) => ElMessage.error(msg), 2000);
+const success = throttle((msg: string) => ElMessage.success(msg), 2000);
+
+export type OrganizationType = {
+    ancestors: string
+    contact: string
+    orderNum: number
+    orgId: number
+    parentId: number
+    orgName: string
+    password: string
+    type: organizationTypeEnum | null
+    userName: string
+    confirmPwd?: string,
+    msgAuthCode?: string
+
+}
+
+export const addOrgFetch = async (params: Partial<OrganizationType>) => {
+    const api = await sendFetch<ResResult>(URL.addOrganization, {
+        method: "post",
+        body: JSON.stringify(params),
+    }, {
+        useResult: true
+    });
+    if (api.code === 0) {
+        success('添加成功')
+    } else {
+        if (api.code === 2008) {
+            success(api.message)
+        } else {
+            error(api.message)
+            throw (api.message)
+        }
+    }
+
+}
+
+
+export const alterOrgFetch = (params: Partial<OrganizationType>) =>
+    sendFetch<PageProps<OrganizationType>>(URL.alterOrganization, {
+        method: "post",
+        body: JSON.stringify(params),
+    });
+export const delOrgFetch = (params: Partial<OrganizationType>) =>
+    sendFetch<PageProps<OrganizationType>>(URL.delOrganization, {
+        method: "post",
+        body: JSON.stringify(params),
+    });
+export const getOrgListFetch = (params: PageProps<Partial<OrganizationType>>) =>
+    sendFetch<ResPage<PageProps<OrganizationType>>>(URL.organizationPage, {
+        method: "post",
+        body: JSON.stringify(params),
+    });
+
+
+export const getOrgListFetchList = () =>
+    sendFetch<ResPage<PageProps<OrganizationType>>>(URL.organizationPageList, {
+        method: "post",
+        body: JSON.stringify({}),
+    });
+
+
+export const registerOrganization = (params: any) => {
+    const password = encodePwd(params.password)
+    return sendFetch<ResResult>(URL.registerOrganization, {
+        method: "post",
+        body: JSON.stringify({ ...params, password, confirmPwd: password }),
+    }, {
+        noToken: true
+    });
+
+}

+ 34 - 0
src/request/type.ts

@@ -5,11 +5,25 @@ import {
   creationMethodDesc,
 } from "@/store/relics";
 import { SceneStatus } from "@/store/scene";
+import {
+  WholeLineLineAttrib,
+  WholeLinePointAttrib,
+  WholeLinePolygonAttrib,
+} from "drawing-board";
 
+type UserInfoRoles = {
+  roleId: number;
+  roleKey: string;
+  roleName: string;
+};
 export type UserInfo = {
   head: string;
   nickName: string;
   userName: string;
+  roles: UserInfoRoles[];
+  orgId: string;
+  orgName?: string;
+  userId?: number;
 };
 
 export type Relics = {
@@ -31,11 +45,19 @@ export type ResPage<T> = {
   records: T[];
 };
 
+export type ResResult = {
+  code: number;
+  data: any;
+  message: any;
+  success: boolean;
+  timestamp: string;
+};
 export type ScenePoint = {
   tbStatus: number;
   createTime: string;
   updateTime: string;
   cameraType: DeviceType;
+  index: number;
   id: number;
   uuid: number;
   name: string;
@@ -132,3 +154,15 @@ export type Device = {
   userId: 2;
   userName: string;
 };
+
+export type PolygonsPointAttrib = WholeLinePointAttrib & {
+  rtk: boolean;
+  title: string;
+};
+export type PolygonsLineAttrib = WholeLineLineAttrib;
+
+export type PolygonsAttrib = {
+  lines: PolygonsLineAttrib[];
+  polygons: WholeLinePolygonAttrib[];
+  points: PolygonsPointAttrib[];
+};

+ 103 - 0
src/request/users.ts

@@ -0,0 +1,103 @@
+import { sendFetch, PageProps } from './index'
+import { ResPage, ResResult } from './type'
+import { encodePwd } from "@/util";
+import * as URL from "./URL";
+import { ElMessage } from "element-plus";
+
+export type UserType = {
+    createBy: string
+    createTime: string
+    fdkkId: number
+    head: string
+    nickName: string
+    orgId: number
+    orgName: string
+    status: number
+    tbStatus: number
+    updateBy: string
+    updateTime: string
+    userId: number
+    userName: string
+    roleNames: string
+    confirmPwd?: string
+    msgAuthCode?: string
+    password?: string
+    phoneNum?: string
+}
+
+
+export const getUserpageFetch = (params: any) =>
+    sendFetch<ResPage<PageProps<UserType>>>(URL.userScenepage, {
+        method: "post",
+        body: JSON.stringify(params),
+    });
+
+
+export const addUserFetch = (params: any) =>
+    sendFetch<ResPage<PageProps<UserType>>>(URL.addUser, {
+        method: "post",
+        body: JSON.stringify(params),
+    });
+export const editUserFetch = (params: Partial<UserType>) =>
+    sendFetch<ResPage<PageProps<UserType>>>(URL.alterUser, {
+        method: "post",
+        body: JSON.stringify(params),
+    });
+
+export const delUserFetch = (params: Pick<UserType, 'userId'>) =>
+    sendFetch<ResPage<PageProps<UserType>>>(
+        URL.delUser,
+        { method: "post", body: JSON.stringify({}) },
+        { paths: { userId: params.userId } }
+    );
+export const updateUserStatusFetch = (params: Pick<UserType, 'userId' | 'status'>) =>
+    sendFetch<ResPage<PageProps<UserType>>>(URL.changeUserStatus, {
+        method: "post",
+        body: JSON.stringify(params),
+    });
+
+export type ChangePasswordParam = Pick<UserType, 'confirmPwd' | 'msgAuthCode' | 'password' | 'phoneNum'>
+export const changePassword = async (params: ChangePasswordParam) => {
+
+    const ent = encodePwd(params.password)
+    if (params.password !== params.confirmPwd) {
+        ElMessage.error("当前密码与密码确认不一致!");
+        return Promise.reject()
+    } else {
+        const api = await sendFetch<ResResult>(URL.changePassword, {
+            method: "post",
+            body: JSON.stringify({
+                confirmPwd: ent,
+                password: ent,
+                msgAuthCode: params.msgAuthCode,
+                phoneNum: params.phoneNum
+            }),
+        }, {
+            useResult: true
+        });
+        if (api.code === 0) {
+            ElMessage.success("编辑成功!");
+        } else {
+            ElMessage.error(api.message);
+            throw (api.message)
+        }
+    }
+
+}
+
+export const getMsgAuthCode = (areaNum: string,
+    phoneNum: string) =>
+    sendFetch<ResResult>(URL.getMsgAuthCode, {
+        method: "post",
+        body: JSON.stringify({
+            areaNum,
+            phoneNum
+        }),
+    }, {
+        useResult: true
+    });
+
+
+
+
+

+ 84 - 3
src/router.ts

@@ -2,11 +2,24 @@ import { RouteRecordRaw, createRouter, createWebHashHistory } from "vue-router";
 import { UserStatus, logintAuth, userStatus } from "./store/user";
 import { watch, watchEffect } from "vue";
 
+export const COORD_NAME = "map-coord";
+export const POYS_NAME = "map-poy";
+export const QUERY_COORD_NAME = "query-map-coord";
+export const QUERY_POYS_NAME = "query-map-poy";
+
 const history = createWebHashHistory();
+
 const routes: RouteRecordRaw[] = [
   {
+    path: "/no-persession",
+    name: "no-persession",
+    meta: { title: "无权限", hidden: true },
+    component: () => import("@/view/no-persession.vue"),
+  },
+  {
     path: "/down-vision",
     name: "down-vision",
+    meta: { title: "" },
     component: () => import("@/view/down-vision.vue"),
   },
   {
@@ -16,6 +29,12 @@ const routes: RouteRecordRaw[] = [
     component: () => import("@/view/login.vue"),
   },
   {
+    path: "/tree2",
+    name: "query-tree-2",
+    meta: { title: "登录" },
+    component: () => import("@/view/step-tree-v2/example/example.vue"),
+  },
+  {
     path: "/tree",
     name: "query-tree",
     meta: { title: "登录" },
@@ -35,11 +54,31 @@ const routes: RouteRecordRaw[] = [
       {
         path: "relics/:relicsId",
         children: [
+          // {
+          //   path: "",
+          //   name: "map",
+          //   meta: { title: "文物", navClass: "map" },
+          //   component: () => import("@/view/map/map-board.vue"),
+          // },
           {
-            path: "",
+            path: "map",
             name: "map",
             meta: { title: "文物", navClass: "map" },
-            component: () => import("@/view/map/map.vue"),
+            component: () => import("@/view/map/layout.vue"),
+            children: [
+              {
+                path: "coord",
+                name: COORD_NAME,
+                meta: { title: "文物", navClass: "map" },
+                component: () => import("@/view/map/coord.vue"),
+              },
+              {
+                path: "polygons",
+                name: POYS_NAME,
+                meta: { title: "文物", navClass: "map" },
+                component: () => import("@/view/map/polygons.vue"),
+              },
+            ],
           },
           {
             path: "pano/:pid",
@@ -61,6 +100,18 @@ const routes: RouteRecordRaw[] = [
         meta: { title: "设备管理" },
         component: () => import("@/view/device.vue"),
       },
+      {
+        path: "organization",
+        name: "organization",
+        meta: { title: "单位管理" },
+        component: () => import("@/view/organization.vue"),
+      },
+      {
+        path: "users",
+        name: "users",
+        meta: { title: "用户管理" },
+        component: () => import("@/view/users.vue"),
+      },
     ],
   },
   {
@@ -75,7 +126,21 @@ const routes: RouteRecordRaw[] = [
             path: "",
             name: "query-map",
             meta: { title: "文物", navClass: "map" },
-            component: () => import("@/view/map/map.vue"),
+            component: () => import("@/view/map/layout.vue"),
+            children: [
+              {
+                path: "query-coord",
+                name: QUERY_COORD_NAME,
+                meta: { title: "文物", navClass: "map" },
+                component: () => import("@/view/map/coord.vue"),
+              },
+              {
+                path: "query-polygons",
+                name: QUERY_POYS_NAME,
+                meta: { title: "文物", navClass: "map" },
+                component: () => import("@/view/map/polygons.vue"),
+              },
+            ],
           },
           {
             path: "pano/:pid",
@@ -87,6 +152,8 @@ const routes: RouteRecordRaw[] = [
       },
     ],
   },
+  { path: '/:pathMatch(.*)*', component: import("@/view/layout/nav.vue") },
+
 ];
 
 export const findRoute = (
@@ -150,6 +217,20 @@ router.beforeEach((to, _, next) => {
     }
     return;
   }
+  // organization
+  // if (to.name === "organization") {
+  //   console.log('isSuper-organization', isSuper.value)
+  //   if (!isSuper.value) {
+  //     router.replace({ name: "scene" });
+  //     return
+  //   }
+  // }
+
+  if (to.name === "map") {
+    router.replace({ name: COORD_NAME, params: to.params });
+  } else if (to.name === "query-map") {
+    router.replace({ name: QUERY_COORD_NAME, params: to.params });
+  }
 
   if (to.meta?.title) {
     setDocTitle(to.meta.title as string);

+ 15 - 0
src/store/organization.ts

@@ -0,0 +1,15 @@
+
+export enum organizationTypeEnum {
+  Province = 1,
+  City = 2,
+  Country = 3,
+  Supplier = 4
+}
+
+export const OrganizationTypeDesc: { [key in organizationTypeEnum]: string } = {
+  [organizationTypeEnum.Province]: "省(自治区、直辖市)",
+  [organizationTypeEnum.City]: "市(地区、州、盟)",
+  [organizationTypeEnum.Country]: "县(区、市、旗)",
+  [organizationTypeEnum.Supplier]: "服务商",
+
+};

+ 41 - 0
src/store/polygons.ts

@@ -0,0 +1,41 @@
+import {
+  WholeLineLineAttrib,
+  WholeLinePointAttrib,
+  WholeLinePolygonAttrib,
+} from "drawing-board";
+import { ref } from "vue";
+
+export type Polygons = {
+  id: string;
+  lines: WholeLineLineAttrib[];
+  polygons: WholeLinePolygonAttrib[];
+  points: (WholeLinePointAttrib & { rtk: boolean })[];
+};
+
+export const polygons = ref<Polygons>({
+  id: "0",
+  lines: [],
+  polygons: [],
+  points: [],
+});
+
+setTimeout(() => {
+  polygons.value = {
+    id: "0",
+    lines: [
+      { id: "1", pointIds: ["2666", "2667"] },
+      { id: "2", pointIds: ["2667", "2669"] },
+    ],
+    polygons: [{ id: "1", lineIds: ["1", "2"] }],
+    points: [
+      { rtk: false, x: 115.949835199646, y: 30.0971239995873, id: "2666" },
+      { rtk: false, x: 115.949706558269, y: 30.0975243383135, id: "2667" },
+      { rtk: false, x: 115.950002555619, y: 30.0977552558535, id: "2668" },
+      { rtk: false, x: 115.949968744193, y: 30.097862045865, id: "2669" },
+      { rtk: true, x: 115.950063977564, y: 30.0978879318173, id: "2670" },
+      { rtk: true, x: 115.949964417593, y: 30.0978650571868, id: "2671" },
+      { rtk: true, x: 115.950300839723, y: 30.0976756336231, id: "2672" },
+      { rtk: true, x: 115.950437426448, y: 30.097269657442, id: "2673" },
+    ],
+  };
+}, 2000);

+ 17 - 3
src/store/relics.ts

@@ -1,24 +1,38 @@
 import {
   relicsInfoFetch,
+  // relicsPolyginsFetch,
   relicsSelfCheckFetch,
   updateRelicsFetch,
 } from "@/request";
+import { errorHook } from "@/request/state";
 import { ref } from "vue";
 import { Relics } from "@/request/type";
 import { refreshScenes } from "./scene";
+import { router } from '../router'
 
 export type { Relics } from "@/request/type";
 export const relics = ref<Relics>();
 
+errorHook.push((code) => {
+  if (code === 4002) {
+    // (window as any).router = router
+    setTimeout(() => {
+      router.replace({ name: "no-persession" });
+    }, 500)
+    return
+    // debugger
+  }
+});
+
 export const initRelics = async (relicsId: number) => {
   relics.value = await relicsInfoFetch(relicsId);
   if (relics.value) {
     await refreshScenes();
   }
 };
-export const initSelfRelics = async (relicsId: number) => {
-  await relicsSelfCheckFetch(relicsId);
-
+export const initSelfRelics = async (relicsId: number, isEdit = false) => {
+  console.log('isEditMode', isEdit)
+  isEdit && await relicsSelfCheckFetch(relicsId);
   relics.value = await relicsInfoFetch(relicsId);
   if (relics.value) {
     await refreshScenes();

+ 114 - 29
src/store/scene.ts

@@ -1,10 +1,16 @@
 import { relicsScenesFetch, updateRelicsScenePosNameFetch } from "@/request";
-import { computed, ref } from "vue";
+import { computed, ref, watch } from "vue";
 import { Scene, ScenePoint } from "@/request/type";
-import { gHeaders } from "@/request/state";
 import { relics } from "./relics";
 import { DeviceType, DeviceType as SceneType } from "./device";
 import { conversionFactory } from "@/helper/coord-transform";
+import { getTokenFetch } from "@/request";
+import {
+  PolygonsPointAttrib,
+  getWholeLineLinesByPointId,
+  PolygonsAttrib,
+} from "drawing-board";
+import { getDrawingDetailFetch } from "@/request/drawing";
 
 export type { Scene, ScenePoint };
 
@@ -20,6 +26,7 @@ export const scenePoints = computed(() =>
     return t;
   }, [] as ScenePoint[])
 );
+
 export const relicsId = computed(() => relics.value!.relicsId);
 
 // https://4dkankan.oss-cn-shenzhen.aliyuncs.com/scene_view_data/KJ-t-OgSx9XIrvNQ/images/panoramas/22.jpg?x-oss-process=image/resize,m_fixed,w_6144&171342528615
@@ -37,6 +44,7 @@ export const getPointPano = (point: ScenePoint, tile = false) => {
     return `https://4dkk.4dage.com/scene_view_data/${point.sceneCode}/images/pan/high/${point.uuid}.jpg`;
   }
 };
+
 export const refreshScenes = async () => {
   const sscenes = await relicsScenesFetch(relicsId.value);
   scenes.value = sscenes.map((scene) => {
@@ -69,28 +77,32 @@ export const refreshScenes = async () => {
 
     return {
       ...scene,
-      scenePos: scene.scenePos.map((pos) => {
-        let coord =
-          scene.calcStatus !== SceneStatus.SUCCESS ? ([] as any) : pos.pos;
-        if (conversion && scene.calcStatus === SceneStatus.SUCCESS) {
-          let center = scenesTransform[pos.sceneCode]?.translate || [0, 0, 0];
-          let rotate = scenesTransform[pos.sceneCode]?.rotate || 0;
-          let [x, y, z] = pos.location;
-          console.log(pos.location);
-          const cos = Math.cos(rotate);
-          const sin = Math.sin(rotate);
-          x = x * cos - y * sin + center[0];
-          y = x * sin + y * cos + center[1];
-
-          coord = conversion.toWGS84([x, y, z]);
-        }
-        return {
-          ...pos,
-          pos: coord,
-        };
-      }),
+      scenePos: scene.scenePos
+        .sort((a, b) => a.index - b.index)
+        .map((pos) => {
+          let coord =
+            scene.calcStatus !== SceneStatus.SUCCESS ? ([] as any) : pos.pos;
+          if (conversion && scene.calcStatus === SceneStatus.SUCCESS) {
+            let center = scenesTransform[pos.sceneCode]?.translate || [0, 0, 0];
+            let rotate = scenesTransform[pos.sceneCode]?.rotate || 0;
+            let [x, y, z] = pos.location;
+            console.log(pos.location);
+            const cos = Math.cos(rotate);
+            const sin = Math.sin(rotate);
+            x = x * cos - y * sin + center[0];
+            y = x * sin + y * cos + center[1];
+
+            coord = conversion.toWGS84([x, y, z]);
+          }
+          return {
+            ...pos,
+            pos: coord,
+          };
+        }),
     };
   });
+
+  await refreshBoardData();
 };
 
 export const updateScenePointName = async (
@@ -101,21 +113,25 @@ export const updateScenePointName = async (
   relicsId.value && (await refreshScenes());
 };
 
-export const gotoScene = (scene: Scene, edit = false) => {
+export const gotoScene = async (scene: Scene, edit = false) => {
   const params = new URLSearchParams();
   if (edit) {
-    params.set("token", gHeaders.token);
+    try {
+      const res = await getTokenFetch(scene.sceneCode);
+      params.set("token", (res as any).token);
+    } catch {
+      edit = false;
+    }
   }
   params.set("lang", "zh");
   if (scene.sceneCode.startsWith("KJ")) {
+    const qjURL = import.meta.env.VITE_QJ_URL;
     params.set("id", scene.sceneCode);
-    window.open(
-      `https://www.4dkankan.com/panorama/${edit ? "edit" : "show"}.html?` +
-        params.toString()
-    );
+    // console.log('')
+    window.open(`${qjURL}/${edit ? "edit" : "show"}.html?` + params.toString());
   } else {
     params.set("m", scene.sceneCode);
-    window.open(`https://laser.4dkankan.com/4pc/?` + params.toString());
+    window.open(`${import.meta.env.VITE_LASER_URL}/?` + params.toString());
   }
 };
 
@@ -143,3 +159,72 @@ export const SceneStatusDesc: { [key in SceneStatus]: string } = {
   [SceneStatus.ERR]: "计算失败",
   [SceneStatus.SUCCESS]: "计算成功",
 };
+
+export const boardData = ref<PolygonsAttrib & { id: string }>();
+export const refreshBoardData = async () => {
+  const res = await getDrawingDetailFetch(String(relicsId.value));
+  const data = (res?.data || {
+    points: [],
+    polygons: [],
+    lines: [],
+  }) as PolygonsAttrib;
+
+  boardData.value = {
+    ...data,
+    id: relicsId.value.toString(),
+  };
+};
+
+const scenePosTransform = (scenes: Scene[]) => {
+  const points: PolygonsPointAttrib[] = [];
+  scenes.forEach((scene) => {
+    if (scene.calcStatus !== SceneStatus.SUCCESS) {
+      return;
+    }
+    scene.scenePos.forEach((pos) => {
+      if (!pos.pos || pos.pos.length === 0) {
+        return;
+      }
+      points.push({
+        x: pos.pos[0],
+        y: pos.pos[1],
+        title: pos.index
+          ? pos.index + (pos.name ? "-" + pos.name : "")
+          : pos.name,
+        id: pos.id.toString(),
+        rtk: true,
+      });
+    });
+  });
+  return points;
+};
+
+watch(
+  () => ({ scenes: scenes.value, poyData: boardData.value }),
+  ({ scenes, poyData }) => {
+    if (!poyData) return;
+
+    const points = scenePosTransform(scenes);
+    const canDelPoint = (id: string) =>
+      getWholeLineLinesByPointId(poyData, id).length === 0 &&
+      !points.some(({ id: rtkId }) => id === rtkId);
+
+    // 查看是否有多余的点,有则删除,出现原因是删除了场景
+    for (let i = 0; i < poyData.points.length; i++) {
+      if (canDelPoint(poyData.points[i].id)) {
+        poyData.points.splice(i--, 1);
+      }
+    }
+
+    // 将rtk点加入
+    for (let i = 0; i < points.length; i++) {
+      const ndx = poyData.points.findIndex(({ id }) => id === points[i].id);
+      if (!~ndx) {
+        poyData.points.push(points[i]);
+      } else {
+        poyData.points[ndx] = { ...points[i] };
+      }
+    }
+  },
+  { immediate: true, flush: "sync" }
+);

+ 9 - 6
src/store/user.ts

@@ -1,8 +1,8 @@
-import { LoginProps, loginFetch, userInfoFetch } from "@/request";
+import { LoginProps, loginFetch, userInfoFetch, loginOutFetch } from "@/request";
 import { errorHook, gHeaders } from "@/request/state";
 import { UserInfo } from "@/request/type";
 import { encodePwd } from "@/util";
-import { ref } from "vue";
+import { ref, computed } from "vue";
 
 export const user = ref<UserInfo>();
 export enum UserStatus {
@@ -12,19 +12,22 @@ export enum UserStatus {
 }
 export const userStatus = ref<UserStatus>(UserStatus.UNKNOWN);
 
+export const isSuper = computed(() => user.value ? user.value.roles.filter(item => item.roleKey === "super_admin").length > 0 : false)
+
 export const login = async (props: LoginProps) => {
   const data = await loginFetch({
     ...props,
     password: encodePwd(props.password),
   });
   user.value = data.user;
-  gHeaders.token = data.token;
+  gHeaders['relics-token'] = data.token;
   localStorage.setItem("token", data.token);
   await getUserInfo();
 };
 
-export const logout = () => {
-  localStorage.removeItem("token");
+export const logout = async (isLogin = false) => {
+  isLogin && await loginOutFetch();
+  isLogin && localStorage.setItem("token", "");
   userStatus.value = UserStatus.NOT_LOGIN;
 };
 
@@ -43,7 +46,7 @@ export const logintAuth = getUserInfo;
 
 const token = localStorage.getItem("token");
 if (token) {
-  gHeaders.token = token;
+  gHeaders['relics-token'] = token;
 } else {
   userStatus.value = UserStatus.NOT_LOGIN;
 }

+ 16 - 12
src/style.scss

@@ -2,7 +2,7 @@ html,
 body,
 #app {
   margin: 0;
-  width : 100vw;
+  width: 100vw;
   height: 100vh;
 }
 
@@ -15,30 +15,30 @@ body,
 }
 
 .disable {
-  opacity       : 0.7;
+  opacity: 0.7;
   pointer-events: none;
 }
 
 .relics-layout {
-  height        : 100%;
-  display       : flex;
+  height: 100%;
+  display: flex;
   flex-direction: column;
-  overflow      : hidden !important;
+  overflow: hidden !important;
 
   .relics-header {
     flex: none;
   }
 
   .relics-content {
-    flex    : 1;
+    flex: 1;
     position: relative;
 
     .el-table {
       position: absolute;
-      left    : 0;
-      top     : 0;
-      width   : 100%;
-      height  : 100%;
+      left: 0;
+      top: 0;
+      width: 100%;
+      height: 100%;
     }
   }
 
@@ -48,6 +48,10 @@ body,
 }
 
 .link {
-  color : var(--el-color-primary);
+  color: var(--el-color-primary);
   cursor: pointer;
-}
+}
+:root {
+  --font14: 14px;
+  --font16: 16px;
+}

+ 27 - 0
src/util/index.ts

@@ -253,3 +253,30 @@ export const dateFormat = (date: Date, fmt: string) => {
   }
   return fmt;
 };
+
+let canvas: HTMLCanvasElement;
+let ctx: CanvasRenderingContext2D;
+export const getTextBound = (
+  text: string,
+  font: string,
+  padding: number[] = [0, 0],
+  margin: number[] = [0, 0]
+) => {
+  if (!canvas) {
+    canvas = document.createElement("canvas");
+    ctx = canvas.getContext("2d")!;
+  }
+
+  ctx.font = font;
+  const textMetrics = ctx.measureText(text);
+
+  const width = textMetrics.width + (padding[1] + margin[1]) * 2;
+  const fontHeight =
+    textMetrics.fontBoundingBoxAscent + textMetrics.fontBoundingBoxDescent;
+  console.log(fontHeight);
+  const height = fontHeight + (padding[0] + margin[0]) * 2;
+
+  return { width, height };
+};
+
+export * from "./tree";

+ 2 - 0
src/util/pc4xlsl.ts

@@ -85,3 +85,5 @@ export const downloadPointsXLSL = async (
   downloadPointsXLSL1(points, desc, name);
   downloadPointsXLSL2(points, desc, name + "本体边界坐标");
 };
+
+

+ 2 - 0
src/util/regex.ts

@@ -0,0 +1,2 @@
+//  /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,16}$/
+export const globalPasswordRex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d]{8,16}$/

+ 17 - 0
src/util/tree.ts

@@ -0,0 +1,17 @@
+const traverse: any = (arr: any[], parentId: number, idName: string) =>
+    arr.filter(node => node.parentId === parentId)
+        .reduce((result, current) => [
+            ...result,
+            {
+                ...current,
+                children: traverse(arr, current[idName])
+            }
+        ], [])
+
+export const parseTree = (arr: any[], idName: string = "id") =>
+    arr.sort(({ order }) => order)
+        .filter(({ parentId }) => !parentId)
+        .map(node => ({
+            ...node,
+            children: traverse(arr, node[idName], idName)
+        }))

+ 29 - 6
src/view/device.vue

@@ -12,7 +12,11 @@
             />
           </el-form-item>
           <el-form-item label="设备类型:">
-            <el-select style="width: 250px" v-model="pageProps.cameraType" clearable>
+            <el-select
+              style="width: 250px"
+              v-model="pageProps.cameraType"
+              clearable
+            >
               <el-option
                 :value="Number(key)"
                 :label="type"
@@ -23,7 +27,11 @@
 
           <el-form-item>
             <el-button type="primary" @click="refresh">查询</el-button>
-            <el-button type="primary" plain @click="pageProps = { ...initProps }">
+            <el-button
+              type="primary"
+              plain
+              @click="pageProps = { ...initProps }"
+            >
               重置
             </el-button>
             <el-button type="primary" @click="addHandler"> 添加设备 </el-button>
@@ -43,16 +51,31 @@
         >
           {{ DeviceTypeDesc[row.cameraType] }}
         </el-table-column>
-        <!-- <el-table-column label="所属单位" prop="deptId"></el-table-column> -->
-        <!-- <el-table-column label="绑定账号" prop="userName"> </el-table-column> -->
+        <el-table-column label="所属单位" prop="orgName"></el-table-column>
+        <el-table-column
+          label="绑定账号"
+          prop="bindName"
+          v-slot:default="{ row }"
+        >
+          {{ row.bindName || "-" }}
+        </el-table-column>
         <el-table-column label="创建人" prop="createBy"> </el-table-column>
-        <el-table-column label="创建时间" prop="createTime" v-slot:default="{ row }">
+        <el-table-column
+          label="创建时间"
+          prop="createTime"
+          v-slot:default="{ row }"
+        >
           {{ row.createTime && row.createTime.substr(0, 16) }}
         </el-table-column>
 
         <el-table-column label="操作" width="100px" fixed="right">
           <template #default="{ row }">
-            <el-button link type="danger" @click="delHandler(row.cameraId)" size="small">
+            <el-button
+              link
+              type="danger"
+              @click="delHandler(row.cameraId)"
+              size="small"
+            >
               删除
             </el-button>
           </template>

+ 141 - 15
src/view/layout/nav.vue

@@ -9,18 +9,43 @@
           @click="router.back()"
           v-if="showBack"
         />
-        <span v-if="!name">不可移动文物管理平台</span>
-      </span>
-      <el-dropdown class="avatar" v-if="user">
-        <span>
-          <el-avatar :src="user.head" />
+
+        <span v-if="!name" class="logo-wrapper">
+          <img class="sp" src="/image/logo_sp.png" alt="logo" />
+          <span>不可移动文物管理平台</span>
         </span>
-        <template #dropdown>
-          <el-dropdown-menu>
-            <el-dropdown-item @click="logoutHandler">退出登录</el-dropdown-item>
-          </el-dropdown-menu>
-        </template>
-      </el-dropdown>
+      </span>
+      <div class="right-panel" v-if="user && !['pano', 'map'].includes(name)">
+        <a
+          target="_blank"
+          class="helper"
+          href="https://showdoc.4dage.com/web/#/179?page_id=1007"
+        >
+          <el-icon :size="16">
+            <QuestionFilled />
+          </el-icon>
+          帮助
+        </a>
+        <el-dropdown placement="bottom-start" class="avatar" v-if="user">
+          <span class="avatar-left-label">
+            <el-avatar class="avatar" :src="user.head || avatarDefault" />
+          </span>
+
+          <template #dropdown>
+            <el-dropdown-menu>
+              <el-dropdown-item @click="passwordHandler"
+                >修改密码</el-dropdown-item
+              >
+              <el-dropdown-item @click="logoutHandler"
+                >退出登录</el-dropdown-item
+              >
+            </el-dropdown-menu>
+          </template>
+        </el-dropdown>
+
+        <span class="name" v-if="user"> {{ user.nickName }}</span>
+        <span class="org" v-if="user"> {{ user.orgName }}</span>
+      </div>
     </div>
     <div class="content">
       <ly-slide class="slide" v-if="user && !['pano', 'map'].includes(name)" />
@@ -36,19 +61,55 @@
 </template>
 
 <script setup lang="ts">
-import { Back } from "@element-plus/icons-vue";
+import { Back, QuestionFilled } from "@element-plus/icons-vue";
 import { router } from "@/router";
-import { computed } from "vue";
+import { computed, reactive } from "vue";
 import { user, logout } from "@/store/user";
 import { errorHook } from "@/request/state";
 import lySlide from "./slide/index.vue";
-
+import { usersPasswordEdit } from "@/view/quisk";
+import avatarDefault from "@/assets/avatar.png";
+import { UserType, changePassword } from "@/request";
+import { UserStatus, userStatus } from "@/store/user";
 const name = computed(() => router.currentRoute.value.meta?.navClass as string);
 const routeName = computed(() => router.currentRoute.value.name as string);
+let isLogout = false;
 const logoutHandler = () => {
-  logout();
+  if (!isLogout) {
+    logout(true);
+    isLogout = true;
+  }
+  userStatus.value = UserStatus.NOT_LOGIN;
   router.replace({ name: "login" });
 };
+const passwordHandler = async () => {
+  const userObj = reactive<UserType>({
+    orgName: "",
+    createBy: "",
+    createTime: "",
+    fdkkId: 0,
+    head: "",
+    nickName: "",
+    orgId: 0,
+    status: 0,
+    tbStatus: 0,
+    updateBy: "",
+    updateTime: "",
+    userId: 0,
+    userName: "",
+    roleNames: "",
+  });
+  const userinfo = {
+    ...userObj,
+    ...user.value,
+    phoneNum: user.value.userName,
+  } as any as UserType;
+  console.log("passwordHandler", userinfo);
+  await usersPasswordEdit({
+    user: userinfo,
+    submit: changePassword,
+  });
+};
 errorHook.push((code) => {
   if (code === 4008) {
     router.replace({ name: "login" });
@@ -71,16 +132,47 @@ const showBack = computed(() => {
   flex-direction: column;
   height: 100%;
 }
+
 .header {
   display: flex;
   align-items: center;
   justify-content: space-between;
+  min-height: 60px;
   padding: 4px 10px;
 
+  .right-panel {
+    display: inline-flex;
+    align-items: center;
+    padding-right: 14px;
+    .avatar {
+      margin-right: 16px;
+      width: 32px;
+      height: 32px;
+      &:hover {
+        cursor: pointer;
+      }
+    }
+    .name,
+    .org {
+      font-size: 14px;
+      color: #606266;
+    }
+    .name {
+      padding-right: 8px;
+    }
+  }
+
+  .avatar-left-label {
+    display: inline-flex;
+    align-items: center;
+    outline: none;
+  }
+
   &:not(.pano, .map) {
     border-bottom: 1px solid var(--border-color);
     flex: 0 0 auto;
   }
+
   &.pano,
   &.map {
     pointer-events: none;
@@ -91,6 +183,7 @@ const showBack = computed(() => {
     .avatar {
       display: none;
     }
+
     * {
       pointer-events: all;
     }
@@ -137,8 +230,41 @@ const showBack = computed(() => {
 .title-span {
   display: flex;
   align-items: center;
+
   > span {
     margin-left: 10px;
   }
 }
+.logo-wrapper {
+  display: inline-flex;
+  font-family: Microsoft YaHei, Microsoft YaHei;
+  font-weight: bold;
+  font-size: 20px;
+  color: #303133;
+  line-height: 23px;
+  letter-spacing: 2px;
+  text-align: left;
+  font-style: normal;
+  justify-content: center;
+  align-items: center;
+  .sp {
+    height: 23px;
+    width: auto;
+    margin-right: 16px;
+  }
+}
+.helper {
+  font-size: 14px;
+  color: #177ddc;
+  margin-right: 32px;
+  display: inline-flex;
+  align-items: center;
+  text-decoration: none;
+  i {
+    margin-right: 4px;
+  }
+  &:hover {
+    color: #0b467d;
+  }
+}
 </style>

+ 30 - 2
src/view/layout/slide/index.vue

@@ -3,10 +3,12 @@
     <el-menu
       :default-active="(router.currentRoute.value.name as string)"
       @select="(name: string) => router.push({ name })"
+      style="border-right: none"
     >
       <sub-menu
         v-for="route in routes"
         :meta="route.meta"
+        v-show="!route.meta.hidden"
         :name="(route.name as string)"
         :key="route.name"
       />
@@ -15,11 +17,37 @@
 </template>
 
 <script setup lang="ts">
+import { computed } from "vue";
 import subMenu from "./submenu.vue";
+import { user } from "@/store/user";
+
 import { router, findRoute } from "@/router";
+//@TODO
+const isSuper = computed(
+  () =>
+    user.value.roles.filter((item) => item.roleKey === "super_admin").length > 0
+);
+const normal_name = [
+  "scene",
+  "relics",
+  "device",
+  "organization",
+  "users",
+  "no-persession",
+];
+const super_names = [
+  "scene",
+  "relics",
+  "device",
+  "organization",
+  "users",
+  "no-persession",
+];
+console.log("isSuper", isSuper.value);
 
-const names = ["scene", "relics", "device"];
-const routes = names.map((name) => findRoute(name)!);
+const routes = isSuper.value
+  ? super_names.map((name) => findRoute(name)!)
+  : normal_name.map((name) => findRoute(name)!);
 </script>
 
 <style lang="scss" scoped>

+ 156 - 71
src/view/login.vue

@@ -30,72 +30,96 @@
               <img class="code" src="/image/pic_camera@2x.png" />
             </div>
           </div>
-          <el-form class="panel login" :model="form" @submit.stop>
-            <h2>欢迎登录</h2>
-            <el-form-item class="panel-form-item">
-              <p class="err-info">{{ verification.phone }}</p>
-              <el-input
-                :maxlength="11"
-                v-model.trim="form.phone"
-                placeholder="手机号"
-                @keydown.enter="submitClick"
-              ></el-input>
-            </el-form-item>
-            <el-form-item class="panel-form-item">
-              <p class="err-info">{{ verification.psw }}</p>
-              <el-input
-                v-model="form.psw"
-                :maxlength="16"
-                placeholder="密码"
-                :type="flag ? 'text' : 'password'"
-                @keydown.enter="submitClick"
-              >
-                <template v-slot:suffix>
-                  <el-icon :size="20" @click="flag = !flag" class="icon-style">
-                    <View v-if="flag" />
-                    <Hide v-else />
-                  </el-icon>
-                </template>
-              </el-input>
-            </el-form-item>
-
-            <el-form-item class="panel-form-item" style="user-select: none">
-              <DragVerify
-                ref="verify"
-                :class="{ passing: isPassing2 }"
-                :isPassing="isPassing2"
-                @passcallback="isPassing2 = true"
-                handlerIcon="el-icon-d-arrow-right"
-                background="#D9D9D9"
-                textColor="#333333"
-                successIcon="el-icon-circle-check"
-                :text="isPassing2 ? '已通过验证' : '登录需要拖拽验证'"
-                successText="验证通过"
-                :width="400"
-              >
-                <template v-slot:handlerIcon>
-                  <el-icon
-                    :size="20"
-                    style="
-                      width: 20px;
-                      display: inline-block;
-                      line-height: 20px;
-                      margin-top: 8px;
-                    "
+          <div class="right-panel">
+            <!-- login right panel -->
+            <template v-if="currentStatus(0)">
+              <el-form class="panel login" :model="form" @submit.stop>
+                <h2>欢迎登录</h2>
+                <el-form-item class="panel-form-item">
+                  <p class="err-info">{{ verification.phone }}</p>
+                  <el-input
+                    :maxlength="11"
+                    v-model.trim="form.phone"
+                    placeholder="手机号"
+                    @keydown.enter="submitClick"
+                  ></el-input>
+                </el-form-item>
+                <el-form-item class="panel-form-item">
+                  <p class="err-info">{{ verification.psw }}</p>
+                  <el-input
+                    v-model="form.psw"
+                    :maxlength="16"
+                    placeholder="密码"
+                    :type="flag ? 'text' : 'password'"
+                    @keydown.enter="submitClick"
                   >
-                    <DArrowRight v-if="!isPassing2" />
-                    <SuccessFilled v-else />
-                  </el-icon>
-                </template>
-              </DragVerify>
-            </el-form-item>
-
-            <el-form-item class="panel-form-item">
-              <el-button type="primary" class="fill submit" @click="submitClick"
-                >登录</el-button
-              >
-            </el-form-item>
-          </el-form>
+                    <template v-slot:suffix>
+                      <el-icon
+                        :size="20"
+                        @click="flag = !flag"
+                        class="icon-style"
+                      >
+                        <View v-if="flag" />
+                        <Hide v-else />
+                      </el-icon>
+                    </template>
+                  </el-input>
+                </el-form-item>
+
+                <el-form-item class="panel-form-item" style="user-select: none">
+                  <DragVerify
+                    ref="verify"
+                    :class="{ passing: isPassing2 }"
+                    :isPassing="isPassing2"
+                    @passcallback="isPassing2 = true"
+                    handlerIcon="el-icon-d-arrow-right"
+                    background="#D9D9D9"
+                    textColor="#333333"
+                    successIcon="el-icon-circle-check"
+                    :text="isPassing2 ? '已通过验证' : '登录需要拖拽验证'"
+                    successText="验证通过"
+                    :width="400"
+                  >
+                    <template v-slot:handlerIcon>
+                      <el-icon
+                        :size="20"
+                        style="
+                          width: 20px;
+                          display: inline-block;
+                          line-height: 20px;
+                          margin-top: 8px;
+                        "
+                      >
+                        <DArrowRight v-if="!isPassing2" />
+                        <SuccessFilled v-else />
+                      </el-icon>
+                    </template>
+                  </DragVerify>
+                </el-form-item>
+
+                <el-form-item class="panel-form-item">
+                  <el-button
+                    type="primary"
+                    class="fill submit"
+                    @click="submitClick"
+                    >登录</el-button
+                  >
+                </el-form-item>
+
+                <div class="register">
+                  <span @click="handleForgetPassword"> 忘记密码</span> |
+                  <span @click="handleRegister"> 单位注册</span>
+                </div>
+              </el-form>
+            </template>
+
+            <template v-if="currentStatus(1)">
+              <register @done="goTologin"></register>
+            </template>
+            <template v-if="currentStatus(2)">
+              <reset @done="goTologin"></reset>
+            </template>
+          </div>
         </div>
       </div>
     </div>
@@ -103,14 +127,20 @@
 </template>
 
 <script lang="ts" setup>
-import { reactive, watch, ref } from "vue";
-import { View, Hide, DArrowRight, SuccessFilled } from "@element-plus/icons-vue";
+import { reactive, watch, ref, computed } from "vue";
+import {
+  View,
+  Hide,
+  DArrowRight,
+  SuccessFilled,
+} from "@element-plus/icons-vue";
 import { login } from "@/store/user";
 import { ElMessage } from "element-plus";
 import { router } from "@/router";
 import qrCode from "qrcode";
 import DragVerify from "@/components/drag-verify.vue";
-
+import register from "@/view/register/register.vue";
+import reset from "@/view/register/reset.vue";
 const PHONE = {
   REG: /^1(3|4|5|6|7|8|9)\d{9}$/,
   // REG: /^((13[0-9]|14[01456879]|15[0-3,5-9]|16[2567]|17[0-8]|18[0-9]|19[0-3,5-9])\d{8})|(8){11}$/,
@@ -120,10 +150,15 @@ const PHONE = {
 const flag = ref(false);
 const verify = ref<any>();
 const isPassing2 = ref(false);
+
+const registerStatus = ref(0);
+const currentStatus = computed(
+  () => (status: number) => status === registerStatus.value
+);
 // 表单
 const form = reactive({
-  phone: import.meta.env.DEV ? "99999999999" : "",
-  psw: import.meta.env.DEV ? "4Dage168" : "",
+  phone: import.meta.env.DEV ? "13800000001" : "",
+  psw: import.meta.env.DEV ? "88888888Sw" : "",
 });
 const verification = reactive({ phone: "", psw: "" });
 
@@ -187,6 +222,19 @@ const submitClick = async () => {
   verify.value.reset();
   isPassing2.value = false;
 };
+
+// 忘记密码
+const handleForgetPassword = () => {
+  registerStatus.value = 2;
+};
+// 注册
+const handleRegister = () => {
+  registerStatus.value = 1;
+};
+// 业务回到login
+const goTologin = () => {
+  registerStatus.value = 0;
+};
 </script>
 
 <style lang="scss" scoped>
@@ -194,6 +242,7 @@ const submitClick = async () => {
   width: 100%;
   height: 100%;
 }
+
 .content {
   display: flex;
   justify-content: center;
@@ -202,8 +251,10 @@ const submitClick = async () => {
   box-sizing: border-box;
   max-width: 1620px;
   height: 100vh;
+  margin: 0 auto;
   padding: 0 50px 0 50px;
 }
+
 .info {
   color: #000;
   flex: none;
@@ -218,10 +269,12 @@ const submitClick = async () => {
 
   .top {
     margin-top: 50px;
+
     img {
       width: 142px;
     }
   }
+
   .bottom {
     height: 470px;
     display: flex;
@@ -257,13 +310,16 @@ const submitClick = async () => {
           background-size: 100% 100%;
         }
       }
+
       .e-code {
         width: 128px;
         margin-top: 13px;
         position: relative;
+
         > img {
           width: 100%;
         }
+
         .e-logo {
           position: absolute;
           top: 50%;
@@ -274,6 +330,7 @@ const submitClick = async () => {
           padding: 7px;
           border-radius: 4px;
           text-align: center;
+
           img {
             height: 100%;
             width: 100%;
@@ -281,6 +338,7 @@ const submitClick = async () => {
           }
         }
       }
+
       p:last-child {
         font-weight: 400;
         font-size: 14px;
@@ -291,15 +349,18 @@ const submitClick = async () => {
 
   .center {
     text-align: center;
+
     h1 {
       color: #781c0b;
       font-size: 48px;
       line-height: 3.7rem;
       margin-bottom: 0.7rem;
     }
+
     p {
       width: 100%;
       margin-top: 40px;
+
       img {
         width: 320px;
       }
@@ -312,14 +373,17 @@ const submitClick = async () => {
   pointer-events: none;
   height: 153px;
   min-width: 1200px;
+
   img {
     position: absolute;
     right: 0;
   }
 }
+
 .fill {
   width: 100%;
 }
+
 .login {
   width: 400px;
   // padding: 40px 40px 30px;
@@ -344,6 +408,7 @@ const submitClick = async () => {
   .panel-form-item {
     padding-left: 0;
     padding-right: 0;
+
     .icon-style {
       margin-right: 14px;
       font-size: 20px;
@@ -379,6 +444,7 @@ const submitClick = async () => {
   background: no-repeat left bottom;
   background-size: auto 100%;
 }
+
 .l-content {
   display: flex;
   width: 100%;
@@ -386,6 +452,17 @@ const submitClick = async () => {
   justify-content: center;
   align-items: flex-start;
 }
+
+.register {
+  display: flex;
+  flex-direction: row;
+  flex: 1;
+  justify-content: center;
+
+  span {
+    padding: 0 10px;
+  }
+}
 </style>
 
 <style>
@@ -406,10 +483,12 @@ const submitClick = async () => {
 .login .code-form-item .el-input__inner {
   flex: 1;
 }
+
 .login .code-form-item .el-input-group__append,
 .login .code-form-item .el-input__inner {
   border-radius: 4px;
 }
+
 input[type="password"]::-ms-reveal {
   display: none;
 }
@@ -476,7 +555,7 @@ input[type="password"]::-ms-reveal {
 .panel-form-item .el-button,
 .panel-form-item .el-input__inner {
   height: 40px;
-  font-size: 1.14rem;
+  font-size: 16px;
 }
 
 .panel-form-item .el-button {
@@ -488,6 +567,7 @@ input[type="password"]::-ms-reveal {
 .panel-form-item .el-form-item__label {
   line-height: 50px;
 }
+
 .e-code img {
   width: 100%;
 }
@@ -503,4 +583,9 @@ input[type="password"]::-ms-reveal {
 .drag_verify {
   border: 1px solid #dcdfe6;
 }
+
+.register span {
+  cursor: pointer;
+}
+
 </style>

+ 433 - 0
src/view/map/coord.vue

@@ -0,0 +1,433 @@
+<template>
+  <div class="right-layout" @click="board.polygon.status.lightPointId = null">
+    <div class="right-content">
+      <el-form :inline="false" v-if="!queryMode">
+        <el-form-item>
+          <el-button type="primary" :icon="Plus" style="width: 100%" @click="addHandler">
+            添加场景
+          </el-button>
+        </el-form-item>
+      </el-form>
+      <div class="tree-layout">
+        <p class="sub-title">全部数据</p>
+        <el-tree
+          style="max-width: 600px"
+          :data="treeNode"
+          node-key="id"
+          ref="treeRef"
+          :show-checkbox="!queryMode"
+          default-expand-all
+          :expand-on-click-node="false"
+        >
+          <template #default="{ node, data }">
+            <div
+              class="tree-item"
+              :class="{
+                active: board.polygon.status.lightPointId === data.raw.id.toString(),
+              }"
+              @click.stop="
+                !data.disable &&
+                  (data.type === 'scene' ? flyScene(data) : flyPos(data.raw))
+              "
+            >
+              <el-tooltip
+                v-if="data.type === 'scene'"
+                class="box-item"
+                effect="dark"
+                :content="data.raw.sceneName + ' ' + node.label"
+                placement="top"
+              >
+                <span :class="{ disable: data.disable }" class="title">
+                  <el-icon>
+                    <Grid />
+                  </el-icon>
+                  <span>
+                    <p>{{ data.raw.sceneName }}</p>
+                  </span>
+                  <span class="tree-scene-name">
+                    <p>{{ node.label }}</p>
+                  </span>
+                </span>
+              </el-tooltip>
+              <el-tooltip
+                v-else
+                class="box-item"
+                effect="dark"
+                :content="data.raw.name || node.label.toString()"
+                placement="top"
+              >
+                <div class="title-box">
+                  <span :class="{ disable: data.disable }" class="title">
+                    <el-icon>
+                      <StateGpsIcon v-if="!data.disable" />
+                      <StateGpsDIcon v-else />
+                    </el-icon>
+                    {{ node.label }}
+                  </span>
+                  <span :class="{ disable: data.disable }" class="name">
+                    {{ data.raw.name }}
+                  </span>
+                </div>
+              </el-tooltip>
+              <span class="oper" @click.stop>
+                <template v-if="!queryMode">
+                  <template v-if="data.type === 'scene'">
+                    <el-icon color="#409efc" v-if="data.raw.creationMethod !== 2">
+                      <Delete @click.stop="delSceneHandler([data.raw])" />
+                    </el-icon>
+                  </template>
+                  <el-icon v-else color="#409efc">
+                    <Edit @click.stop="inputPoint = data.raw" />
+                  </el-icon>
+                </template>
+                <el-icon color="#409efc" style="margin-left: 8px">
+                  <!-- root -->
+                  <template v-if="data.raw.scenePos">
+                    <FrameIcon
+                      v-if="!data.run"
+                      @click.stop="
+                        data.type === 'scene'
+                          ? gotoScene(data.raw, false)
+                          : gotoPointPage(data.raw)
+                      "
+                    />
+                  </template>
+                  <template v-else>
+                    <PanoramaIcon
+                      v-if="!data.run"
+                      @click.stop="
+                        data.type === 'scene'
+                          ? gotoScene(data.raw)
+                          : gotoPointPage(data.raw)
+                      "
+                    />
+                  </template>
+                </el-icon>
+              </span>
+            </div>
+          </template>
+        </el-tree>
+      </div>
+    </div>
+
+    <template v-if="!queryMode">
+      <el-button
+        type="primary"
+        :icon="Download"
+        style="width: 100%"
+        @click="exportFile(getSelectPoints(), 2, relics?.name)"
+      >
+        导出本体边界坐标
+      </el-button>
+
+      <el-button
+        type="primary"
+        :icon="Download"
+        style="width: 100%; margin-top: 20px; margin-left: 0"
+        @click="exportImage(getSelectPoints(), relics?.name)"
+      >
+        下载全景图
+        {{ inputPoint?.name }}
+      </el-button>
+    </template>
+  </div>
+
+  <SingleInput
+    :key="inputPoint?.id"
+    :visible="!!inputPoint"
+    @update:visible="inputPoint = null"
+    :value="inputPoint?.name || ''"
+    :update-value="updatePointName"
+    is-allow-empty
+    title="测点说明"
+    placeholder="请填写测点说明"
+  />
+</template>
+
+<script setup lang="ts">
+import { boardDataChange, noValidPoint, queryMode, validScene } from "./install";
+import {
+  Plus,
+  Delete,
+  Grid,
+  Download,
+  // DeleteLocation,
+  Edit,
+} from "@element-plus/icons-vue";
+import { computed, onBeforeUnmount, ref, watchEffect } from "vue";
+import {
+  Scene,
+  scenes,
+  ScenePoint,
+  updateScenePointName,
+  gotoScene,
+  relicsId,
+  refreshScenes,
+  boardData,
+  scenePoints,
+} from "@/store/scene";
+import { relics } from "@/store/relics";
+import SingleInput from "@/components/single-input.vue";
+import { selectScenes } from "../quisk";
+import { addRelicsScenesFetch, delRelicsScenesFetch } from "@/request";
+import { exportFile, exportImage } from "./pc4Helper";
+import { SceneStatus } from "@/store/scene";
+import StateGpsIcon from "@/assets/state_gps.svg";
+import StateGpsDIcon from "@/assets/state_gps_d.svg";
+import PanoramaIcon from "@/assets/panorama.svg";
+import FrameIcon from "@/assets/frame.svg";
+import { alert, confirm } from "@/helper/message";
+import {
+  PolygonsPointAttrib,
+  getWholeLineLinesByPointId,
+  getWholeLinePoint,
+} from "drawing-board";
+import { flyScene, gotoPointPage, mapManage } from "./install";
+import { board } from "./install";
+
+const inputPoint = ref<ScenePoint | null>(null);
+const updatePointName = async (title: string) => {
+  const point = getWholeLinePoint(
+    boardData.value,
+    inputPoint.value.id.toString()
+  ) as PolygonsPointAttrib;
+  await Promise.all([
+    boardDataChange(() => {
+      if (point) {
+        point.title = inputPoint.value.index
+          ? inputPoint.value.index + "-" + title
+          : title;
+      }
+    }),
+    updateScenePointName(inputPoint.value!, title),
+  ]);
+};
+
+const flyPos = (point: ScenePoint) => {
+  mapManage.map.getView().setCenter(point.pos);
+  board.polygon.status.lightPointId = point.id.toString();
+};
+
+const relicsName = ref("");
+watchEffect(() => (relicsName.value = relics.value?.name || ""));
+
+const treeRef = ref<any>();
+const treeNode = computed(() =>
+  scenes.value.map((scene) => ({
+    label: scene.sceneCode,
+    id: scene.id,
+    type: "scene",
+    run: scene.calcStatus !== SceneStatus.SUCCESS,
+    disable: !validScene(scene),
+    raw: scene,
+    children: scene.scenePos.map((pos) => ({
+      label: pos.index || pos.uuid,
+      run: scene.calcStatus !== SceneStatus.SUCCESS,
+      disable: noValidPoint(pos),
+      id: pos.id,
+      type: "point",
+      raw: { ...pos, name: pos.name, cameraType: scene.cameraType },
+    })),
+  }))
+);
+
+const getSelectPoints = () =>
+  treeRef
+    .value!.getCheckedNodes(false, false)
+    .filter((option: any) => option.type === "point")
+    .map((option: any) => option.raw) as ScenePoint[];
+
+watchEffect(() => {
+  if (treeRef.value) {
+    board.polygon.status.selectPoiIds = getSelectPoints().map((point) =>
+      point.id.toString()
+    );
+  }
+});
+
+const delScenesBeforeCheck = async (scenes: Scene[]) => {
+  if (scenes.length === 0 || !(await confirm("确定要删除场景吗?"))) return true;
+  for (const scene of scenes) {
+    const que = scene.scenePos.some((pos) => {
+      const id = pos.id.toString();
+      return getWholeLineLinesByPointId(boardData.value, id).length !== 0;
+    });
+    if (que) {
+      await alert("已存在矢量图数据,不可删除。");
+      return false;
+    }
+    return true;
+  }
+};
+
+const addHandler = async () => {
+  const sceneCodes = scenes.value.map((scene) => scene.sceneCode);
+  await selectScenes({
+    scenes: scenes.value,
+    selfScenes: scenes.value.filter((scene) => scene.creationMethod === 2),
+    submit: async (nScene) => {
+      const requests: Promise<any>[] = [];
+      const delScenes = sceneCodes
+        .filter((sceneCode) => !nScene.some((scene) => scene.sceneCode === sceneCode))
+        .map((sceneCode) => scenes.value.find((scene) => scene.sceneCode === sceneCode)!);
+
+      if (!(await delScenesBeforeCheck(delScenes))) {
+        throw "不可删除";
+      }
+
+      delScenes.length &&
+        requests.push(
+          delRelicsScenesFetch(
+            relicsId.value,
+            delScenes.map((item) => ({ sceneCode: item.sceneCode, id: item.sceneId }))
+          )
+        );
+      const addScenes = nScene.filter(({ sceneCode }) => !sceneCodes.includes(sceneCode));
+      addScenes.length &&
+        requests.push(
+          addRelicsScenesFetch(
+            relicsId.value!,
+            addScenes.map((item) => ({ sceneCode: item.sceneCode, id: item.sceneId }))
+          )
+        );
+
+      await Promise.all(requests);
+      requests.length && (await refreshScenes());
+    },
+  });
+};
+
+const delSceneHandler = async (scenes: Scene[]) => {
+  if (!(await delScenesBeforeCheck(scenes))) {
+    return;
+  }
+  await delRelicsScenesFetch(
+    relicsId.value,
+    scenes.map((item) => ({ sceneCode: item.sceneCode, id: item.sceneId }))
+  );
+  await refreshScenes();
+};
+
+const pointClickHandler = ({ id }: { id: any }) => {
+  const point = scenePoints.value.find((point) => point.id.toString() === id);
+  point && gotoPointPage(point);
+};
+
+board.polygon.container.stage.on("click.checkPointSelect", (ev) => {
+  if (ev.target === board.polygon.container.stage) {
+    board.polygon.status.lightPointId = null;
+  }
+});
+board.polygon.bus.on("clickPoint", pointClickHandler);
+onBeforeUnmount(() => {
+  board.polygon.bus.off("clickPoint", pointClickHandler);
+  board.polygon.container.stage.off("click.checkPointSelect");
+  board.polygon.status.lightPointId = null;
+});
+</script>
+
+<style lang="scss" scoped>
+:deep(.el-tree-node__content) {
+  --el-tree-node-content-height: 26px;
+  line-height: 26px;
+  user-select: none;
+  margin-bottom: 8px;
+}
+
+:deep(.el-tree-node__children .el-tree-node__content) {
+  --el-tree-node-content-height: 52px;
+
+  & > label.el-checkbox {
+    padding-top: 6px;
+    align-items: flex-start;
+  }
+}
+
+.tree-item {
+  display: flex;
+  width: calc(100% - 50px);
+  align-items: flex-start;
+  justify-content: space-between;
+  font-size: var(--font14);
+
+  &.active {
+    color: rgba(64, 158, 255, 1);
+  }
+
+  .title {
+    flex: 1;
+    overflow: hidden;
+    display: inline-flex;
+    align-items: center;
+    line-height: 26px;
+    margin-right: 10px;
+
+    p {
+      margin: 0;
+    }
+  }
+
+  .title-box {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    width: 90%;
+    overflow: hidden;
+    flex-wrap: nowrap;
+
+    .name {
+      padding-left: 15px;
+      color: #999;
+      display: block;
+      text-overflow: ellipsis; //文本溢出显示省略号
+      overflow: hidden;
+      white-space: nowrap; //文本不会换行
+    }
+  }
+
+  .oper {
+    flex: none;
+    line-height: 26px;
+    vertical-align: middle;
+  }
+}
+
+.disable {
+  pointer-events: all;
+}
+
+.tree-layout {
+  p {
+    color: #303133;
+  }
+
+  .sub-title {
+    font-size: 14px;
+    font-weight: bolder;
+  }
+}
+
+.right-layout {
+  display: flex;
+  height: 100%;
+  flex-direction: column;
+
+  .right-content {
+    flex: 1;
+    overflow-y: auto;
+  }
+}
+
+.tree-layout .tree-scene-name {
+  font-size: 10px;
+  margin: 0;
+  color: #999;
+  p {
+    margin: 0;
+    max-width: 90%;
+    text-overflow: ellipsis; //文本溢出显示省略号
+    overflow: hidden;
+    white-space: nowrap; //文本不会换行
+  }
+}
+</style>

+ 104 - 0
src/view/map/install.ts

@@ -0,0 +1,104 @@
+import { TileType, createMap } from "./openlayer";
+import { computed, ref, watch, watchEffect } from "vue";
+import { createBoard } from "drawing-board";
+import { Scene, ScenePoint, boardData, relicsId, scenes } from "@/store/scene";
+import { router } from "@/router";
+import { addOrUpdateDrawingFetch } from "@/request/drawing";
+
+// ---------map---------
+
+export const tileOptions: TileType[] = ["影像底图", "矢量底图"];
+export const tileType = ref<TileType>(tileOptions[0]);
+export const defaultCenter = [116.412611, 39.908866];
+
+export const mapManage = createMap();
+mapManage.setCenter(defaultCenter);
+watchEffect(() => mapManage.setTileType(tileType.value));
+
+export const noValidPoint = (pos: ScenePoint) =>
+  !pos.pos || pos.pos.length === 0 || pos.pos.some((i) => !i);
+export const validScene = (scene: Scene) => !scene.scenePos.every(noValidPoint);
+
+export const flyScene = (scene: Scene) => {
+  const totalPos = [0, 0];
+  let numCalc = 0;
+  for (let i = 0; i < scene.scenePos.length; i++) {
+    const coord = scene.scenePos[i].pos as number[];
+    if (!noValidPoint(scene.scenePos[i])) {
+      totalPos[0] += coord[0];
+      totalPos[1] += coord[1];
+      numCalc++;
+    }
+  }
+  console.log(scene);
+  totalPos[0] /= numCalc;
+  totalPos[1] /= numCalc;
+  mapManage.map.getView().setCenter(totalPos);
+};
+
+export const gotoPointPage = (point: ScenePoint) => {
+  router.push({
+    name: queryMode.value ? "query-pano" : "pano",
+    params: { pid: point.id },
+  });
+};
+
+export const autoInitPos = () => {
+  const scene = scenes.value.find(validScene);
+  if (scene) {
+    flyScene(scene);
+    return true;
+  } else {
+    return false;
+  }
+};
+
+watch(
+  () => scenes.value.find(validScene)?.sceneCode,
+  (code) => {
+    code && autoInitPos();
+  },
+  { immediate: true }
+);
+
+// -------board------
+export const board = createBoard({ map: mapManage.map });
+watch(
+  boardData,
+  (data, oldData) => {
+    data && board.setData(data);
+    console.log(data, data === oldData);
+  },
+  {
+    immediate: true,
+    flush: "pre",
+  }
+);
+
+export const boardDataChange = (dataChange?: () => void) => {
+  dataChange && dataChange();
+  return addOrUpdateDrawingFetch({
+    relicsId: relicsId.value.toString(),
+    data: boardData.value,
+  });
+};
+
+watch(
+  tileType,
+  (type) => {
+    if (type === "影像底图") {
+      board.scale.setColor("#fff");
+    } else {
+      board.scale.setColor("#000");
+    }
+  },
+  { flush: "post", immediate: true }
+);
+
+// -----------status----------
+
+export const queryMode = computed(
+  () =>
+    router.currentRoute.value.name &&
+    router.currentRoute.value.name.toString().includes("query")
+);

+ 325 - 0
src/view/map/layout.vue

@@ -0,0 +1,325 @@
+<template>
+  <div class="map-layout" v-loading="!loaded || captureing">
+    <div class="custom_bar">
+      <div class="back_container" v-if="!queryMode">
+        <el-button :icon="Back" circle type="primary" @click="router.back()" />
+      </div>
+      <div class="nav_container">
+        <div
+          v-for="menu in menus"
+          :key="menu.name"
+          class="nav_item"
+          :class="{
+            active: menu.router.includes(router.currentRoute.value.name.toString()),
+          }"
+          @click="router.replace({ name: menu.router[Number(queryMode)] })"
+        >
+          <el-icon size="20">
+            <component :is="menu.icon" />
+          </el-icon>
+          <span>{{ menu.name }}</span>
+        </div>
+      </div>
+    </div>
+
+    <div class="map-oper-layout">
+      <div class="map-container" :ref="setMapContainer">
+        <div class="board" :ref="setBoardContainer"></div>
+        <div class="map-top-out-pano">
+          <template v-if="!isCoordPage">
+            <el-button @click="capture" v-if="loaded && !queryMode">
+              提取位置图
+            </el-button>
+            <el-button @click="showPoints = !showPoints">
+              <el-checkbox :modelValue="showPoints" label="点位" size="large" />
+            </el-button>
+          </template>
+
+          <div class="tile-select">
+            <el-select
+              v-model="tileType"
+              placeholder="选择底图"
+              style="width: 120px"
+              class="tile-type-select"
+            >
+              <el-option
+                v-for="item in tileOptions"
+                :key="item"
+                :label="item"
+                :value="item"
+              />
+            </el-select>
+          </div>
+        </div>
+        <div class="map-bottom-out-pano" v-if="!isCoordPage">
+          <div class="point-info">
+            <div>
+              <el-icon size="20" color="rgb(230, 162, 60)">
+                <locationIcon />
+              </el-icon>
+              <p>RTK点位</p>
+            </div>
+            <div>
+              <el-icon size="20" color="rgba(64, 158, 255)">
+                <locationIcon />
+              </el-icon>
+              <p>地图选点</p>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="data-panel">
+        <RouterView v-slot="{ Component }" v-if="loaded">
+          <component :is="Component" />
+        </RouterView>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { router } from "@/router";
+import { Back } from "@element-plus/icons-vue";
+import vectorIcon from "@/assets/vector.svg";
+import locationIcon from "@/assets/location.svg";
+import { COORD_NAME, POYS_NAME, QUERY_COORD_NAME, QUERY_POYS_NAME } from "@/router";
+import {
+  mapManage,
+  board,
+  autoInitPos,
+  tileOptions,
+  defaultCenter,
+  tileType,
+} from "./install";
+import { computed, ref, watch } from "vue";
+import { initSelfRelics, relics } from "@/store/relics";
+import { queryMode } from "./install";
+import { PoPoint } from "drawing-board";
+import { boardData, scenePoints } from "@/store/scene";
+import saveAs from "@/util/file-serve";
+
+const menus = [
+  {
+    icon: locationIcon,
+    name: "坐标",
+    router: [COORD_NAME, QUERY_COORD_NAME],
+  },
+  {
+    icon: vectorIcon,
+    name: "矢量图",
+    router: [POYS_NAME, QUERY_POYS_NAME],
+  },
+];
+const setMapContainer = (dom: HTMLDivElement) => setTimeout(() => mapManage.mount(dom));
+const setBoardContainer = (dom: HTMLDivElement) =>
+  setTimeout(() => board.setProps({ dom }));
+
+const loaded = ref(false);
+
+const isCoordPage = computed(() => {
+  const name = router.currentRoute.value.name;
+  return name && [COORD_NAME, QUERY_COORD_NAME].includes(name.toString());
+});
+
+watch(
+  () => router.currentRoute.value.params?.relicsId,
+  (rid) => {
+    if (!rid) return;
+    loaded.value = false;
+    const isEditmode = [COORD_NAME, POYS_NAME].includes(
+      router.currentRoute.value.name.toString()
+    );
+
+    initSelfRelics(Number(rid), isEditmode).finally(() => {
+      if (!relics.value) {
+        return router.replace({ name: "relics" });
+      }
+      if (mapManage && !autoInitPos()) {
+        mapManage.flyUserCenter(defaultCenter);
+      }
+      loaded.value = true;
+    });
+  },
+  { immediate: true }
+);
+const showPoints = ref(true);
+
+watch(
+  () =>
+    [
+      isCoordPage.value,
+      boardData.value,
+      showPoints.value,
+      board.polygon.attrib.points.length,
+    ] as const,
+  ([isCoordPage, _, showPoints]) => {
+    if (!board.polygon) return;
+    const ids = scenePoints.value.map(({ id }) => id.toString());
+    board.polygon.children.forEach((entity) => {
+      if (entity instanceof PoPoint) {
+        if (isCoordPage) {
+          entity.visible(entity.attrib.rtk && ids.includes(entity.attrib.id));
+        } else {
+          entity.visible(showPoints);
+        }
+      } else {
+        entity.visible(!isCoordPage);
+      }
+    });
+  },
+  { immediate: true, flush: "post" }
+);
+
+const captureing = ref(false);
+const capture = async () => {
+  captureing.value = true;
+  await new Promise((resolve) => setTimeout(resolve, 300));
+  try {
+    const dataURL = await board.toDataURL(2);
+    await saveAs(dataURL, `${relics.value.name}-位置图.jpg`);
+  } finally {
+    captureing.value = false;
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.map-layout {
+  display: flex;
+  flex-direction: row;
+  height: 100%;
+}
+
+.custom_bar {
+  width: 60px;
+  height: 100%;
+  background-color: white;
+
+  // padding-top: 76px;
+  .back_container {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    color: #606266;
+    height: 76px;
+  }
+
+  .nav_container {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    color: #606266;
+
+    .nav_item {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      padding: 10px 0;
+      cursor: pointer;
+      user-select: none;
+      width: 100%;
+      span {
+        line-height: 26px;
+        font-size: var(--font14);
+      }
+
+      &.active {
+        .icon {
+          color: #409eff;
+        }
+
+        color: #409eff;
+        background-color: #ecf5ff;
+        position: relative;
+
+        &::before {
+          content: "";
+          height: 100%;
+          width: 4px;
+          position: absolute;
+          top: 0;
+          left: 0;
+          background-color: #409eff;
+        }
+      }
+    }
+  }
+}
+
+.map-oper-layout {
+  display: flex;
+  flex-direction: row;
+  height: 100%;
+  flex: 1;
+}
+
+.map-container {
+  flex: 1;
+  position: relative;
+
+  .map-component {
+    pointer-events: none;
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    left: 0;
+    top: 0;
+    z-index: 9;
+  }
+
+  .board {
+    position: absolute;
+    left: 0;
+    top: 0;
+    bottom: 0;
+    right: 0;
+    z-index: 1;
+  }
+}
+
+.data-panel {
+  width: 320px;
+  padding: 15px;
+  border-left: 1px solid var(--border-color);
+  position: relative;
+  z-index: 3;
+}
+.map-top-out-pano {
+  display: flex;
+  position: absolute;
+  right: 10px;
+  top: 10px;
+  z-index: 3;
+  > * {
+    margin-left: 10px;
+  }
+}
+.map-bottom-out-pano {
+  position: absolute;
+  right: 10px;
+  bottom: 10px;
+  z-index: 3;
+}
+.point-info {
+  background: #ffffff;
+  border-radius: 4px 4px 4px 4px;
+  padding: 10px;
+
+  > div {
+    display: flex;
+    align-items: center;
+    &:not(:last-child) {
+      margin-bottom: 10px;
+    }
+
+    p {
+      font-size: 16px;
+      color: #606266;
+      margin: 0;
+      margin-left: 6px;
+    }
+  }
+}
+</style>

+ 0 - 67
src/view/map/manage.ts

@@ -1,67 +0,0 @@
-import { Map, View } from "ol";
-import { TileType, baseTileLayer, geoTileLayer, setBaseTileType } from "./tile";
-import {
-  HotData,
-  addHots,
-  delHots,
-  hotLayer,
-  dynamicHots,
-  clearHots,
-} from "./hot";
-import { Emitter } from "mitt";
-
-const createMap = (container: HTMLDivElement) => {
-  const view = new View({
-    center: [113.59562585879772, 22.367660742553472],
-    projection: "EPSG:4326",
-    zoom: 18,
-  });
-
-  return new Map({
-    layers: [baseTileLayer, geoTileLayer, hotLayer],
-    view,
-    target: container,
-    controls: [],
-  });
-};
-
-export class Manage {
-  map: Map;
-  hotsBus: Emitter<{
-    active: any;
-    click: any;
-  }>;
-
-  constructor(container: HTMLDivElement) {
-    this.map = createMap(container);
-    this.hotsBus = dynamicHots(this.map);
-  }
-
-  setTileType(type: TileType) {
-    setBaseTileType(type);
-    this.map.render();
-  }
-
-  addHots(items: HotData[]) {
-    addHots(items);
-    this.map.render();
-  }
-
-  clearHots() {
-    clearHots();
-    this.map.render();
-  }
-
-  setCenter(center: number[]) {
-    this.map.getView().setCenter(center);
-  }
-
-  delHots(ids: HotData["id"][]) {
-    delHots(ids);
-    this.map.render();
-  }
-
-  render() {
-    this.map.render();
-  }
-}

+ 0 - 283
src/view/map/map-right.vue

@@ -1,283 +0,0 @@
-<template>
-  <div class="right-layout">
-    <div class="right-content">
-      <el-form :inline="false" v-if="router.currentRoute.value.name === 'map'">
-        <!-- <el-form-item v-if="relics">
-          <el-input v-model="relicsName" :maxlength="50" placeholder="不可移动文物名称">
-            <template #append>
-              <el-button type="primary" @click="updateRelics">修改</el-button>
-            </template>
-          </el-input>
-        </el-form-item> -->
-        <el-form-item>
-          <el-button type="primary" :icon="Plus" style="width: 100%" @click="addHandler">
-            添加场景
-          </el-button>
-        </el-form-item>
-      </el-form>
-      <div class="tree-layout">
-        <p>全部数据</p>
-        <el-tree
-          style="max-width: 600px"
-          :data="treeNode"
-          :props="{ disabled: 'run' }"
-          node-key="id"
-          ref="treeRef"
-          :show-checkbox="router.currentRoute.value.name === 'map'"
-          default-expand-all
-          :expand-on-click-node="false"
-        >
-          <template #default="{ node, data }">
-            <div
-              class="tree-item"
-              @click="!data.disable && emit((data.type === 'scene' ? 'flyScene' : 'flyPoint') as any, data.raw)"
-            >
-              <el-tooltip
-                v-if="data.type === 'scene'"
-                class="box-item"
-                effect="dark"
-                :content="data.raw.sceneName + ' ' + node.label"
-                placement="top"
-              >
-                <span :class="{ disable: data.disable }" class="title">
-                  <el-icon> <Grid /> </el-icon>
-                  {{ data.raw.sceneName }}
-                  <span class="tree-scene-name">{{ node.label }}</span>
-                </span>
-              </el-tooltip>
-              <el-tooltip
-                v-else
-                class="box-item"
-                effect="dark"
-                :content="node.label"
-                placement="top"
-              >
-                <span :class="{ disable: data.disable }" class="title">
-                  <el-icon>
-                    <LocationInformation v-if="!data.disable" />
-                    <DeleteLocation v-else />
-                  </el-icon>
-                  {{ node.label }}
-                </span>
-              </el-tooltip>
-              <span class="oper">
-                <template v-if="router.currentRoute.value.name === 'map'">
-                  <template v-if="data.type === 'scene'">
-                    <el-icon color="#409efc" v-if="data.raw.creationMethod !== 2">
-                      <Delete @click.stop="delSceneHandler([data.raw])" />
-                    </el-icon>
-                  </template>
-                  <el-icon v-else color="#409efc">
-                    <Edit @click.stop="inputPoint = data.raw" />
-                  </el-icon>
-                </template>
-                <el-icon color="#409efc" style="margin-left: 8px">
-                  <Link
-                    v-if="!data.run"
-                    @click.stop="
-                      data.type === 'scene'
-                        ? gotoScene(data.raw)
-                        : emit('gotoPoint', data.raw)
-                    "
-                  />
-                </el-icon>
-              </span>
-            </div>
-          </template>
-        </el-tree>
-      </div>
-    </div>
-
-    <template v-if="router.currentRoute.value.name === 'map'">
-      <el-button
-        type="primary"
-        :icon="Download"
-        style="width: 100%"
-        @click="exportFile(getSelectPoints(), 2, relics?.name)"
-      >
-        导出本体边界坐标
-      </el-button>
-      <el-button
-        type="primary"
-        :icon="Download"
-        style="width: 100%; margin-top: 20px; margin-left: 0"
-        @click="exportFile(getSelectPoints(), 1, relics?.name)"
-      >
-        导出绘制矢量数据
-      </el-button>
-
-      <el-button
-        type="primary"
-        :icon="Download"
-        style="width: 100%; margin-top: 20px; margin-left: 0"
-        @click="exportImage(getSelectPoints(), relics?.name)"
-      >
-        下载全景图
-      </el-button>
-    </template>
-  </div>
-
-  <SingleInput
-    :visible="!!inputPoint"
-    @update:visible="inputPoint = null"
-    :value="inputPoint?.name || ''"
-    :update-value="updatePointName"
-    title="修改点位名称"
-  />
-</template>
-
-<script setup lang="ts">
-import {
-  Plus,
-  Delete,
-  Grid,
-  Download,
-  LocationInformation,
-  DeleteLocation,
-  Edit,
-  Link,
-} from "@element-plus/icons-vue";
-import { computed, ref, watchEffect } from "vue";
-import {
-  Scene,
-  scenes,
-  ScenePoint,
-  updateScenePointName,
-  gotoScene,
-  relicsId,
-  refreshScenes,
-} from "@/store/scene";
-import { relics } from "@/store/relics";
-import SingleInput from "@/components/single-input.vue";
-import { router } from "@/router";
-import { selectScenes } from "../quisk";
-import { addRelicsScenesFetch, delRelicsScenesFetch } from "@/request";
-import { exportFile, exportImage } from "./pc4Helper";
-import { SceneStatus } from "@/store/scene";
-
-const emit = defineEmits<{
-  (e: "flyScene", data: Scene): void;
-  (e: "flyPoint", data: ScenePoint): void;
-  (e: "gotoPoint", data: ScenePoint): void;
-}>();
-
-const inputPoint = ref<ScenePoint | null>(null);
-const updatePointName = async (title: string) => {
-  await updateScenePointName(inputPoint.value!, title);
-};
-
-const relicsName = ref("");
-watchEffect(() => (relicsName.value = relics.value?.name || ""));
-// const updateRelics = async () => {
-//   await updateRelicsName(relicsName.value);
-//   ElMessage.success("修改成功");
-// };
-
-const treeRef = ref<any>();
-const treeNode = computed(() =>
-  scenes.value.map((scene) => ({
-    label: scene.sceneCode,
-    id: scene.id,
-    type: "scene",
-    run: scene.calcStatus !== SceneStatus.SUCCESS,
-    disable: scene.scenePos.every((pos) => !pos.pos || pos.pos.length === 0),
-    raw: scene,
-    children: scene.scenePos.map((pos) => ({
-      label: pos.name,
-      run: scene.calcStatus !== SceneStatus.SUCCESS,
-      disable: !pos.pos || pos.pos.length === 0,
-      id: pos.id,
-      type: "point",
-      raw: { ...pos, cameraType: scene.cameraType },
-    })),
-  }))
-);
-
-const getSelectPoints = () =>
-  treeRef
-    .value!.getCheckedNodes(false, false)
-    .filter((option: any) => option.type === "point")
-    .map((option: any) => option.raw) as ScenePoint[];
-
-const addHandler = async () => {
-  const sceneCodes = scenes.value.map((scene) => scene.sceneCode);
-  await selectScenes({
-    scenes: scenes.value,
-    selfScenes: scenes.value.filter((scene) => scene.creationMethod === 2),
-    submit: async (nScene) => {
-      const requests: Promise<any>[] = [];
-      const delScenes = sceneCodes
-        .filter((sceneCode) => !nScene.some((scene) => scene.sceneCode === sceneCode))
-        .map((sceneCode) => scenes.value.find((scene) => scene.sceneCode === sceneCode)!);
-
-      delScenes.length && requests.push(delRelicsScenes(delScenes));
-
-      const addScenes = nScene.filter(({ sceneCode }) => !sceneCodes.includes(sceneCode));
-      addScenes.length && requests.push(addSceneHandler(addScenes));
-
-      await Promise.all(requests);
-      requests.length && (await refreshScenes());
-    },
-  });
-};
-
-const delRelicsScenes = (scenes: Pick<Scene, "sceneId" | "sceneCode">[]) =>
-  delRelicsScenesFetch(
-    relicsId.value,
-    scenes.map((item) => ({ sceneCode: item.sceneCode, id: item.sceneId }))
-  );
-
-const delSceneHandler = async (scenes: Pick<Scene, "sceneId" | "sceneCode">[]) => {
-  await delRelicsScenes(scenes);
-  await refreshScenes();
-};
-
-const addSceneHandler = async (scenes: Pick<Scene, "sceneId" | "sceneCode">[]) =>
-  await addRelicsScenesFetch(
-    relicsId.value!,
-    scenes.map((item) => ({ sceneCode: item.sceneCode, id: item.sceneId }))
-  );
-</script>
-
-<style lang="scss" scoped>
-.tree-item {
-  display: flex;
-  width: calc(100% - 50px);
-  align-items: center;
-  justify-content: space-between;
-
-  .title {
-    flex: 1;
-    overflow: hidden;
-    text-overflow: ellipsis; //文本溢出显示省略号
-    white-space: nowrap; //文本不会换行
-  }
-  .oper {
-    flex: none;
-  }
-}
-.disable {
-  pointer-events: all;
-}
-
-.tree-layout {
-  p {
-    color: #303133;
-    font-size: 14px;
-  }
-}
-.right-layout {
-  display: flex;
-  height: 100%;
-  flex-direction: column;
-  .right-content {
-    flex: 1;
-    overflow-y: auto;
-  }
-}
-.tree-layout .tree-scene-name {
-  font-size: 10px;
-  margin: 0;
-  color: #999;
-}
-</style>

+ 0 - 340
src/view/map/map.vue

@@ -1,340 +0,0 @@
-<template>
-  <div class="map-layout" @click="activeId = null">
-    <div
-      id="map"
-      class="map-container"
-      ref="container"
-      :class="{ active: !!activeId }"
-      @click.stop
-      @mousedown="activeId = null"
-      @wheel="activeId = null"
-    >
-      <div class="map-component">
-        <el-tooltip
-          class="tooltip"
-          :visible="!!activeId"
-          :content="active?.name"
-          effect="light"
-          placement="top"
-          virtual-triggering
-          :virtual-ref="triggerRef"
-        />
-        <el-select
-          v-model="tileType"
-          placeholder="选择底图"
-          style="width: 120px"
-          class="tile-type-select"
-        >
-          <el-option
-            v-for="item in tileOptions"
-            :key="item"
-            :label="item"
-            :value="item"
-          />
-        </el-select>
-      </div>
-    </div>
-    <div class="right-control">
-      <MapRight
-        @fly-point="flyScenePoint"
-        @fly-scene="flyScene"
-        @goto-point="gotoPoint"
-      />
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import MapRight from "./map-right.vue";
-import { router, setDocTitle } from "@/router";
-import { TileType, createMap } from "./";
-import { ScenePoint, Scene, scenePoints, scenes } from "@/store/scene";
-import { initRelics, initSelfRelics, relics } from "@/store/relics";
-import { computed, onMounted, ref, watchEffect, watch, onUnmounted } from "vue";
-import { Manage } from "./manage";
-import ScaleLine from "ol/control/ScaleLine";
-
-const activeId = ref<ScenePoint["id"] | null>();
-const active = computed(() =>
-  scenePoints.value.find((point) => point.id === activeId.value)
-);
-
-const activePixel = ref<number[] | null>();
-const triggerRef = ref({
-  getBoundingClientRect() {
-    return DOMRect.fromRect({
-      x: activePixel.value![0],
-      y: activePixel.value![1],
-      width: 0,
-      height: 0,
-    });
-  },
-});
-
-const tileOptions: TileType[] = ["影像底图", "矢量底图"];
-const tileType = ref<TileType>(tileOptions[0]);
-
-const points = computed(() =>
-  scenePoints.value
-    .filter((point) => point.pos)
-    .map((point) => ({
-      data: point.pos,
-      id: point.id,
-      label: point.name,
-    }))
-);
-
-const gotoPoint = (point: ScenePoint) => {
-  router.push({
-    name: router.currentRoute.value.name === "map" ? "pano" : "query-pano",
-    params: { pid: point.id },
-  });
-};
-
-const flyUserCenter = () => {
-  mapManage.setCenter([116.412611, 39.908866]);
-  navigator.geolocation.getCurrentPosition(
-    (pos) => {
-      console.log("获取中心位置成功", pos);
-      mapManage.setCenter([pos.coords.longitude, pos.coords.latitude]);
-    },
-    (e) => {
-      console.error(e);
-      console.error("获取中心位置失败");
-    },
-    {
-      enableHighAccuracy: false,
-      timeout: 50000,
-      maximumAge: 0,
-    }
-  );
-};
-const center = [109.47293862712675, 30.26530938156551];
-const container = ref<HTMLDivElement>();
-let mapManage: Manage;
-onMounted(async () => {
-  mapManage = createMap(container.value!);
-  mapManage.setCenter(center);
-  mapManage.hotsBus.on("active", (id) => {
-    if (id) {
-      activeId.value = id;
-      active.value && activeScenePoint(active.value!);
-    } else {
-      activeId.value = null;
-      activePixel.value = null;
-    }
-  });
-  mapManage.hotsBus.on("click", (id) => {
-    const point = id && scenePoints.value.find((point) => point.id === id);
-    point && gotoPoint(point);
-  });
-  refreshHots();
-  refreshTileType();
-  const scaleLine = new ScaleLine({
-    className: "scale-view",
-    maxWidth: 150,
-    minWidth: 100,
-    units: "metric",
-  });
-  // 加载比例尺
-  mapManage.map.addControl(scaleLine);
-  watch(
-    tileType,
-    (type) => {
-      const el = (scaleLine as any).element as HTMLDivElement;
-      el.classList.add(type === "影像底图" ? "light" : "dark");
-      el.classList.remove(type === "影像底图" ? "dark" : "light");
-      console.log(el, type);
-    },
-    { flush: "post", immediate: true }
-  );
-});
-
-const activeScenePoint = (point: ScenePoint) => {
-  activePixel.value = mapManage.map.getPixelFromCoordinate(point.pos);
-  activeId.value = point.id;
-};
-
-const flyPos = (pos: number[]) => mapManage.map.getView().setCenter(pos);
-
-const flyScenePoint = (point: ScenePoint) => {
-  flyPos(point.pos);
-  setTimeout(() => {
-    activeScenePoint(point);
-  }, 16);
-};
-
-const flyScene = (scene: Scene) => {
-  const totalPos = [0, 0];
-  let numCalc = 0;
-  for (let i = 0; i < scene.scenePos.length; i++) {
-    const coord = scene.scenePos[i].pos as number[];
-    if (coord && coord.length > 0) {
-      totalPos[0] += coord[0];
-      totalPos[1] += coord[1];
-      numCalc++;
-    }
-  }
-
-  totalPos[0] /= numCalc;
-  totalPos[1] /= numCalc;
-  flyPos(totalPos);
-};
-
-const refreshHots = () => {
-  if (!mapManage) return;
-  mapManage.clearHots();
-  mapManage.addHots(points.value);
-};
-
-const refreshTileType = () => {
-  if (!mapManage) return;
-  mapManage.setTileType(tileType.value);
-};
-
-watch(points, refreshHots, { immediate: true });
-watch(tileType, refreshTileType, { immediate: true });
-watch(
-  () => [router.currentRoute.value.name, router.currentRoute.value.params?.relicsId],
-  ([name, rid], old) => {
-    if (["map", "query-map"].includes(name as string) && (!old || old[1] !== rid)) {
-      relics.value = undefined;
-      const fn = name === "map" ? initSelfRelics : initRelics;
-      fn(Number(rid)).finally(() => {
-        if (!relics.value) {
-          router.replace({ name: "relics" });
-        }
-        if (!autoInitPos()) {
-          flyUserCenter();
-        }
-      });
-    }
-  },
-  { immediate: true }
-);
-
-const autoInitPos = () => {
-  const scene = scenes.value.find(
-    (scene) => !scene.scenePos.every((pos) => !pos.pos || pos.pos.length === 0)
-  );
-  if (scene) {
-    flyScene(scene);
-    return true;
-  } else {
-    return false;
-  }
-};
-
-watch(
-  () => {
-    const scene = scenes.value.find(
-      (scene) => !scene.scenePos.every((pos) => !pos.pos || pos.pos.length === 0)
-    );
-    return scene?.sceneCode;
-  },
-  (firstCode) => {
-    if (firstCode) {
-      autoInitPos();
-    }
-  }
-);
-
-watchEffect(() => {
-  if (
-    ["map", "query-map"].includes(router.currentRoute.value.name as string) &&
-    relics.value
-  ) {
-    setDocTitle(relics.value.name);
-  }
-});
-
-onUnmounted(() => mapManage.map.dispose());
-</script>
-
-<style lang="scss">
-.tooltip {
-  pointer-events: none;
-}
-.map-layout {
-  display: flex;
-  flex-direction: row;
-  height: 100%;
-}
-
-.map-container {
-  flex: 1;
-  position: relative;
-}
-
-.right-control {
-  flex: none;
-  width: 300px;
-  padding: 15px;
-
-  border-left: 1px solid var(--border-color);
-}
-
-.map-component {
-  width: 100%;
-  height: 100%;
-  position: relative;
-}
-
-.active {
-  cursor: pointer;
-}
-
-.active-point {
-  position: absolute;
-  pointer-events: none;
-}
-
-.map-component {
-  pointer-events: none;
-  position: absolute;
-  width: 100%;
-  height: 100%;
-  left: 0;
-  top: 0;
-  z-index: 9;
-}
-.env {
-  width: 100%;
-  height: 100%;
-}
-
-.tile-type-select {
-  pointer-events: all;
-  position: absolute;
-  right: 10px;
-  top: 10px;
-}
-
-.scale-view {
-  --color: #fff;
-  position: absolute;
-  left: 20px;
-  bottom: 20px;
-  height: 8px;
-  color: var(--color);
-  text-align: center;
-  border: 1px solid var(--color);
-  border-top: none;
-  z-index: 1;
-  font-size: 14px;
-  display: flex;
-  align-items: end;
-  &.light {
-    --color: #fff;
-    > div {
-      text-shadow: 0 0 2px #000;
-    }
-  }
-  &.dark {
-    --color: #000;
-    > div {
-      text-shadow: 0 0 2px #fff;
-    }
-  }
-}
-</style>

src/view/map/hot.ts → src/view/map/openlayer/hot.ts


+ 3 - 1
src/view/map/index.ts

@@ -1,6 +1,8 @@
 import { Manage } from "./manage";
 export type { TileType } from "./tile";
 
-export const createMap = (dom: HTMLDivElement) => {
+export const createMap = (dom?: HTMLDivElement) => {
   return new Manage(dom);
 };
+
+export * from "./manage";

+ 115 - 0
src/view/map/openlayer/manage.ts

@@ -0,0 +1,115 @@
+import { Map, View } from "ol";
+import { TileType, baseTileLayer, geoTileLayer, setBaseTileType } from "./tile";
+import {
+  HotData,
+  addHots,
+  delHots,
+  hotLayer,
+  dynamicHots,
+  clearHots,
+} from "./hot";
+import { Emitter } from "mitt";
+import { boundingExtent } from "ol/extent";
+
+const createMap = (container?: HTMLDivElement) => {
+  const view = new View({
+    center: [113.59562585879772, 22.367660742553472],
+    projection: "EPSG:4326",
+    zoom: 18,
+  });
+
+  return new Map({
+    layers: [baseTileLayer, geoTileLayer, hotLayer],
+    view,
+    target: container,
+    controls: [],
+  });
+};
+
+export class Manage {
+  map: Map;
+  hotsBus: Emitter<{
+    active: any;
+    click: any;
+  }>;
+
+  moundDOM: HTMLDivElement;
+  constructor(container?: HTMLDivElement) {
+    this.moundDOM = document.createElement("div");
+    this.moundDOM.style.width = "300px";
+    this.moundDOM.style.height = "300px";
+
+    this.map = createMap(this.moundDOM);
+    this.hotsBus = dynamicHots(this.map);
+    if (container) {
+      this.mount(container);
+    }
+  }
+
+  setTileType(type: TileType) {
+    setBaseTileType(type);
+    this.map.render();
+  }
+
+  addHots(items: HotData[]) {
+    addHots(items);
+    this.map.render();
+  }
+
+  clearHots() {
+    clearHots();
+    this.map.render();
+  }
+
+  setCenter(center: number[]) {
+    this.map.getView().setCenter(center);
+    console.log(center);
+  }
+
+  delHots(ids: HotData["id"][]) {
+    delHots(ids);
+    this.map.render();
+  }
+
+  getBound() {
+    return this.map.getView().calculateExtent(this.map.getSize());
+  }
+  setBound(bound: number[]) {
+    const extent = boundingExtent([
+      [bound[0], bound[1]],
+      [bound[2], bound[3]],
+    ]);
+    this.map.getView().fit(extent, {
+      size: this.map.getSize(),
+      padding: [0, 0, 0, 0], // 根据需要调整边距
+    });
+  }
+  flyUserCenter(defaultCenter: number[]) {
+    this.setCenter(defaultCenter);
+    navigator.geolocation.getCurrentPosition(
+      (pos) => {
+        console.log("获取中心位置成功", pos);
+        this.setCenter([pos.coords.longitude, pos.coords.latitude]);
+      },
+      (e) => {
+        console.error(e);
+        console.error("获取中心位置失败");
+      },
+      {
+        enableHighAccuracy: false,
+        timeout: 50000,
+        maximumAge: 0,
+      }
+    );
+  }
+
+  render() {
+    this.map.render();
+  }
+
+  mount(dom: HTMLDivElement) {
+    dom.appendChild(this.moundDOM);
+    this.moundDOM.style.width = "100%";
+    this.moundDOM.style.height = "100%";
+  }
+}

+ 1 - 0
src/view/map/tile.ts

@@ -63,6 +63,7 @@ const getWMTS = (type: TileType, mapEpsg: string) => {
   const url = `https://t0.tianditu.gov.cn/${layer}_c/wmts?tk=${key}`;
   return new WMTS({
     url,
+    crossOrigin: "anonymous", // 设置跨域
     layer,
     version: "1.0.0",
     matrixSet: "c",

+ 3 - 1
src/view/map/pc4Helper.ts

@@ -9,6 +9,7 @@ import {
   downloadPointsXLSL1,
   downloadPointsXLSL2,
 } from "@/util/pc4xlsl";
+import { noValidPoint } from "./install";
 
 export const exportFile = async (
   points: ScenePoint[],
@@ -20,7 +21,8 @@ export const exportFile = async (
     return;
   }
   name = name ? name + "-" : "";
-  points = points.filter((point) => !!point.pos);
+
+  points = points.filter((point) => !noValidPoint(point));
 
   if (points.length === 0) {
     ElMessage.error("当前选择点位没有gis信息");

+ 317 - 0
src/view/map/polygons.vue

@@ -0,0 +1,317 @@
+<template>
+  <div class="right-layout" @click.stop="selectChange(null)">
+    <div class="right-content">
+      <div class="tree-layout">
+        <p class="sub-title">全部数据</p>
+        <div class="poly-list">
+          <template v-if="boardData.polygons.length > 0">
+            <div
+              v-for="item in boardData.polygons"
+              class="poly-list-item"
+              :class="{
+                active: [
+                  boardStatus.lightPolygonId,
+                  boardStatus.editPolygonId,
+                  selectId,
+                ].includes(item.id),
+              }"
+              @mouseenter="!selectId && (boardStatus.lightPolygonId = item.id)"
+              @mouseleave="!selectId && (boardStatus.lightPolygonId = null)"
+              @click.stop="!currentItem && selectChange(item.id)"
+            >
+              <el-tooltip
+                class="box-item"
+                effect="dark"
+                :content="item.name ? item.name : '本体边界'"
+                placement="top"
+              >
+                <div class="left">
+                  <span>{{ item.name ? item.name : "本体边界" }}</span>
+                </div>
+              </el-tooltip>
+              <div
+                class="right"
+                @click.stop
+                v-if="!boardStatus.editPolygonId && !queryMode"
+              >
+                <el-icon class="icon">
+                  <Delete @click="del(item.id)" />
+                </el-icon>
+                <el-icon class="icon">
+                  <Edit @click="handleShowEditModel(item)" />
+                </el-icon>
+                <el-icon class="icon">
+                  <Download @click="handleDownload(item)" />
+                </el-icon>
+              </div>
+            </div>
+          </template>
+          <template v-else>
+            <div class="empty">暂没数据</div>
+          </template>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <Teleport to="body" v-if="!queryMode">
+    <div class="draw-global-icon" @click="cleanupEdit ? cleanupEdit() : enterEdit()">
+      <el-icon size="36">
+        <Check v-if="cleanupEdit" />
+        <picpenIcon v-else-if="!selectId" />
+        <piceditIcon v-else></piceditIcon>
+      </el-icon>
+    </div>
+    <SingleInput
+      v-if="selectItem"
+      :visible="isShowPolyEditName"
+      @update:visible="isShowPolyEditName = false"
+      :value="selectItem.name || ''"
+      :update-value="(name) => boardDataChange(() => (selectItem.name = name))"
+      placeholder="请输入"
+      title="矢量图名称"
+      name="矢量图"
+    />
+  </Teleport>
+</template>
+
+<script setup lang="ts">
+import { computed, onBeforeUnmount, ref, shallowRef, watch } from "vue";
+import type { PolyDataType } from "@/request/drawing.ts";
+import { Delete, Download, Edit, Check } from "@element-plus/icons-vue";
+import SingleInput from "@/components/single-input.vue";
+import { downloadPointsXLSL1 } from "@/util/pc4xlsl";
+import { boardData, scenePoints } from "@/store/scene";
+import { getWholeLinePolygonPoints } from "drawing-board";
+import { board, boardDataChange, mapManage, queryMode } from "./install";
+import { confirm } from "@/helper/message";
+import picpenIcon from "@/assets/pic_pen.svg";
+import piceditIcon from "@/assets/pic_edit.svg";
+import { ElMessage } from "element-plus";
+
+const boardStatus = board.polygon.status;
+const selectId = ref<string>();
+
+const selectChange = (id: string) => {
+  if (currentItem.value) return;
+  if (selectId.value === id) {
+    boardStatus.lightPolygonId = null;
+    selectId.value = null;
+  } else {
+    selectId.value = id;
+    if (!selectItem.value) {
+      selectChange(null);
+    } else {
+      boardStatus.lightPolygonId = selectItem.value.id;
+      const points = getWholeLinePolygonPoints(boardData.value, selectItem.value.id);
+      if (points.length) {
+        const total = points.reduce((t, p) => [t[0] + p.x, t[1] + p.y], [0, 0]);
+        const view = mapManage.map.getView();
+        view.setZoom(20);
+        setTimeout(() => {
+          view.setCenter([total[0] / points.length, total[1] / points.length]);
+        }, 100);
+      }
+    }
+  }
+};
+
+board.polygon.bus.on("activePolygonId", selectChange);
+
+const selectItem = computed(() =>
+  boardData.value.polygons.find(({ id }) => id === selectId.value)
+);
+const currentItem = computed(() =>
+  boardData.value.polygons.find(({ id }) => id === boardStatus.editPolygonId)
+);
+
+const cleanupEdit = shallowRef<() => void>();
+const enterEdit = () => {
+  cleanupEdit.value && cleanupEdit.value();
+  const quitEdit = board.polygon.editPolygon(selectId.value);
+  const id = board.polygon.status.editPolygonId;
+  let needUpdate = false;
+  const stopWatch = watch(
+    () => currentItem.value,
+    () => (needUpdate = true),
+    { deep: true }
+  );
+  cleanupEdit.value = () => {
+    board.polygon.bus.off("penEndHandler", cleanupEdit.value);
+    quitEdit();
+    selectChange(null);
+    stopWatch();
+    const points = getWholeLinePolygonPoints(board.polygon.attrib, id);
+    if (points.length <= 2) {
+      board.polygon.removePolygon(id);
+      ElMessage.error("请至少绘制3个点");
+    }
+    needUpdate && boardDataChange();
+    cleanupEdit.value = null;
+  };
+  board.polygon.bus.on("penEndHandler", cleanupEdit.value);
+};
+
+onBeforeUnmount(() => {
+  cleanupEdit.value && cleanupEdit.value();
+  board.polygon.status.lightPointId = null;
+});
+
+const isShowPolyEditName = ref(false);
+const handleShowEditModel = (item: PolyDataType) => {
+  selectChange(item.id);
+  isShowPolyEditName.value = true;
+};
+
+const del = async (id: string) => {
+  if ((await confirm("确定要删除吗")) && !currentItem.value) {
+    if (selectId.value === id) {
+      selectChange(null);
+    }
+    boardDataChange(() => board.polygon.removePolygon(id));
+  }
+};
+
+const handleDownload = async (item: any) => {
+  const polygonPoints: any[] = getWholeLinePolygonPoints(boardData.value, item.id);
+
+  const points = polygonPoints.map((p) => {
+    const pos = [p.x, p.y, 0];
+    if (p.rtk) {
+      const sPoint = scenePoints.value.find(({ id }) => id.toString() === p.title);
+      if (sPoint) {
+        pos[2] = sPoint.pos[2];
+      }
+    }
+    return pos;
+  });
+  const dists = polygonPoints.map((p) => ({
+    title: p.title || p.id,
+    desc: p.title || p.id,
+  }));
+  await downloadPointsXLSL1(points, dists, `${item.name || "本体边界"}-绘制矢量数据`);
+};
+</script>
+
+<style lang="scss" scoped>
+.tree-item {
+  display: flex;
+  width: calc(100% - 50px);
+  align-items: center;
+  justify-content: space-between;
+
+  .title {
+    flex: 1;
+    overflow: hidden;
+    text-overflow: ellipsis; //文本溢出显示省略号
+    white-space: nowrap; //文本不会换行
+  }
+
+  .oper {
+    flex: none;
+  }
+}
+
+.disable {
+  pointer-events: all;
+}
+
+.tree-layout {
+  p {
+    color: #303133;
+    font-size: 14px;
+  }
+
+  .sub-title {
+    font-size: 14px;
+    font-weight: bolder;
+    margin-bottom: 30px;
+  }
+}
+
+.right-layout {
+  display: flex;
+  height: 100%;
+  flex-direction: column;
+  font-size: 16px;
+
+  .right-content {
+    flex: 1;
+    overflow-y: auto;
+  }
+}
+
+.tree-layout .tree-scene-name {
+  font-size: 10px;
+  margin: 0;
+  color: #999;
+}
+
+.poly-list {
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  font-size: 14px;
+  user-select: none;
+
+  .poly-list-item {
+    cursor: pointer;
+    width: 100%;
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 10px;
+
+    &.active {
+      color: #409eff;
+    }
+
+    .left {
+      flex: 0 0 220px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+
+    .icon {
+      margin-left: 8px;
+      font-size: 16px;
+      color: #409eff;
+      cursor: pointer;
+    }
+
+    .right {
+      flex: none;
+      width: 80px;
+    }
+  }
+
+  .empty {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 13px;
+    color: gray;
+    padding-top: 40px;
+  }
+}
+
+.draw-global-icon {
+  width: 64px;
+  height: 64px;
+  background: #ffffff;
+  border-radius: 50%;
+  position: fixed;
+  z-index: 1000;
+  transform: translateX(calc(-1 * calc(50% - 300px)));
+  left: calc(50% - 300px);
+  top: 90%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  color: #409eff;
+}
+</style>

+ 23 - 0
src/view/no-persession.vue

@@ -0,0 +1,23 @@
+<template>
+  <div class="no-permission">
+    <img :src="emptyNoRights" />
+    <span>无访问权限</span>
+  </div>
+</template>
+<script setup>
+import emptyNoRights from "@/assets/empty__no_rights.png";
+</script>
+<style lang="scss">
+.no-permission {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+  min-height: 500px;
+
+  span {
+    font-size: 14px;
+    color: #999999;
+  }
+}
+</style>

+ 186 - 0
src/view/organization-add.vue

@@ -0,0 +1,186 @@
+<template>
+  <!-- "ancestors": "",
+  "contact": "",
+  "orderNum": 0,
+  "orgId": 0,
+  "orgName": "",
+  "parentId": 0,
+  "password": "",
+  "type": 0,
+  "userName": "" -->
+  <el-form label-width="100px" :model="data" :rules="rules" ref="baseFormRef">
+    <el-form-item label="单位名称" prop="orgName" required>
+      <el-input
+        v-model.trim="data.orgName"
+        style="width: 300px"
+        :maxlength="50"
+        placeholder="请输入"
+      />
+    </el-form-item>
+    <el-form-item label="类型" prop="type" required>
+      <!-- <el-input v-model="data.type" style="width: 300px" :maxlength="500" placeholder="请输入" /> -->
+      <el-select style="width: 300px" v-model="data.type">
+        <el-option
+          :value="Number(key)"
+          :label="type"
+          v-for="(type, key) in OrganizationTypeDesc"
+        />
+      </el-select>
+    </el-form-item>
+
+    <el-form-item label="上级单位" prop="parentId">
+      <el-tree-select
+        :check-strictly="true"
+        :props="{
+        value: 'orgId',
+        label: (data: any) => data.orgName,
+      }"
+        style="width: 300px"
+        v-model="data.parentId"
+        :data="allOrgs"
+        node-key="orgId"
+        clearable
+      >
+      </el-tree-select>
+    </el-form-item>
+    <el-form-item label="联系人" prop="contact" required>
+      <el-input
+        v-model.trim="data.contact"
+        style="width: 300px"
+        :maxlength="50"
+        placeholder="请输入"
+      />
+    </el-form-item>
+    <el-form-item label="账号" prop="userName" required>
+      <el-input
+        v-model.trim="data.userName"
+        style="width: 300px"
+        :maxlength="11"
+        placeholder="请输入手机号"
+      />
+    </el-form-item>
+    <el-form-item label="密码" prop="password" required>
+      <el-input
+        autocomplete="off"
+        readonly
+        onfocus="this.removeAttribute('readonly');"
+        v-model.trim="data.password"
+        :type="addPassFlag ? 'text' : 'password'"
+        style="width: 300px"
+        :maxlength="500"
+        placeholder="请输入8-16位数字、字母大小写组合"
+      >
+        <template #suffix>
+          <span @click="addPassFlag = !addPassFlag" style="cursor: pointer">
+            <el-icon v-if="addPassFlag">
+              <View />
+            </el-icon>
+            <el-icon v-else>
+              <Hide />
+            </el-icon>
+          </span>
+        </template>
+      </el-input>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import { QuiskExpose } from "@/helper/mount";
+import type { FormInstance, FormRules } from "element-plus";
+// import { ElMessage } from "element-plus";
+import type { OrganizationType } from "@/request/organization";
+import { OrganizationTypeDesc } from "@/store/organization";
+import { globalPasswordRex } from "@/util/regex";
+import { ref, reactive, unref, watch, onMounted } from "vue";
+import { View, Hide } from "@element-plus/icons-vue";
+// import { user } from '@/store/user'
+import { getOrgListFetchList } from "@/request";
+
+const addPassFlag = ref(true); //图标显示标识
+
+type SelectType = {
+  orgName: string;
+  orgId: number;
+  children: SelectType[];
+};
+
+const baseFormRef = ref<FormInstance>();
+const allOrgs = ref<SelectType[]>([]);
+
+const rules = reactive<FormRules>({
+  orgName: [{ required: true, message: "请输入单位名称", trigger: "blur" }],
+  type: [{ required: true, message: "请选择类型", trigger: "change" }],
+  contact: [{ required: true, message: "请输入联系人", trigger: "blur" }],
+  userName: [
+    {
+      required: true,
+      pattern: /^1[3456789]\d{9}$/,
+      message: "请输入正确手机号",
+      trigger: "blur",
+    },
+  ],
+  password: [
+    {
+      required: true,
+      pattern: globalPasswordRex,
+      message: "请输入8-16位数字、字母大小写组合",
+      trigger: "blur",
+    },
+    { required: true, min: 8, message: "密码太短!", trigger: "blur" },
+  ],
+});
+
+const props = defineProps<{
+  submit: (data: OrganizationType) => Promise<any>;
+}>();
+const data = ref<OrganizationType & {}>({
+  ancestors: "",
+  contact: "",
+  orderNum: 0,
+  orgId: 0,
+  orgName: "",
+  parentId: null,
+  password: "",
+  type: null,
+  userName: "",
+});
+
+// const setParentId = () => {
+//   if (user.value) {
+//     const isSuper = user.value.roles.filter(item => item.roleKey === "super_admin").length > 0;
+//     data.value.parentId = isSuper ? 0 : Number(data.value.parentId)
+//   }
+// }
+
+onMounted(async () => {
+  const data = await getOrgListFetchList();
+  // console.log('allOrgs', data);
+  allOrgs.value = data as any as SelectType[];
+});
+watch(
+  data,
+  (newValue) => {
+    data.value.userName = newValue.userName.replace(/[^0-9]/g, "");
+  },
+  {
+    immediate: true,
+    deep: true,
+  }
+);
+
+defineExpose<QuiskExpose>({
+  async submit() {
+    if (unref(baseFormRef)) {
+      // setParentId();
+      const res = await unref(baseFormRef)?.validate();
+      if (res) {
+        console.log("data", data.value);
+        await props.submit(data.value as any as OrganizationType);
+      }
+    } else {
+      throw "";
+    }
+  },
+});
+</script>

+ 89 - 0
src/view/organization-edit.vue

@@ -0,0 +1,89 @@
+<template>
+
+  <el-form label-width="100px" :model="data" :rules="rules" ref="baseFormRef">
+    <el-form-item label="单位名称" prop="orgName" required>
+      <el-input v-model="data.orgName" style="width: 300px" :maxlength="50" placeholder="请输入" />
+    </el-form-item>
+    <el-form-item label="类型" prop="type" required>
+      <!-- <el-input v-model="data.type" style="width: 300px" :maxlength="500" placeholder="请输入" /> -->
+      <el-select style="width: 300px" v-model="data.type">
+        <el-option :value="Number(key)" :label="type" v-for="(type, key) in OrganizationTypeDesc" />
+      </el-select>
+    </el-form-item>
+
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import { QuiskExpose } from "@/helper/mount";
+import type { FormInstance, FormRules } from "element-plus";
+import type { OrganizationType } from "@/request/organization";
+import { OrganizationTypeDesc } from '@/store/organization'
+import { ref, reactive, unref, watchEffect } from "vue";
+import { user } from '@/store/user'
+import { globalPasswordRex } from "@/util/regex";
+const baseFormRef = ref<FormInstance>();
+
+const rules = reactive<FormRules>({
+  orgName: [
+    { required: true, message: "请输入单位名称", trigger: "blur" },
+  ],
+  type: [
+    { required: true, message: "请选择类型", trigger: "change" },
+  ],
+  contact: [
+    { required: true, message: "请输入联系人", trigger: "blur" },
+  ],
+  userName: [
+    { required: true, pattern: /^1[3456789]\d{9}$/, message: "请输入正确手机号", trigger: "blur" },
+  ],
+  password: [
+    { required: true, pattern: globalPasswordRex, message: "请输入8-16位数字、字母大小写组合", trigger: "blur" },
+    { required: true, min: 8, message: '密码太短!', trigger: "blur" },
+  ],
+},)
+
+const props = defineProps<{
+  org: OrganizationType,
+  submit: (data: OrganizationType) => Promise<any>;
+}>();
+const data = ref<OrganizationType & {}>({
+  ancestors: "",
+  contact: "",
+  orderNum: 0,
+  orgId: 0,
+  orgName: "",
+  parentId: 0,
+  password: "",
+  type: null,
+  userName: ""
+});
+
+const setParentId = () => {
+  if (user.value) {
+    const isSuper = user.value.roles.filter(item => item.roleKey === "super_admin").length > 0;
+    data.value.parentId = isSuper ? 0 : Number(user.value.orgId)
+  }
+}
+watchEffect(() => {
+  if (props.org) {
+    data.value = { ...props.org }
+  }
+
+})
+
+defineExpose<QuiskExpose>({
+  async submit() {
+
+    if (unref(baseFormRef)) {
+      setParentId();
+      const res = await unref(baseFormRef)?.validate();
+      if (res) {
+        await props.submit(data.value as any as OrganizationType);
+      }
+    } else {
+      throw "";
+    }
+  },
+});
+</script>

+ 202 - 0
src/view/organization.vue

@@ -0,0 +1,202 @@
+<template>
+  <div class="relics-layout">
+    <div class="relics-header">
+      <div class="search">
+        <el-form label-width="100px" inline>
+          <el-form-item label="单位名称">
+            <el-input
+              v-model.trim="pageProps.orgName"
+              clearable
+              style="width: 250px"
+              placeholder="请输入"
+            />
+          </el-form-item>
+          <el-form-item label="类型">
+            <el-select style="width: 250px" v-model="pageProps.type" clearable>
+              <el-option
+                :value="Number(key)"
+                :label="type"
+                v-for="(type, key) in OrganizationTypeDesc"
+              />
+            </el-select>
+          </el-form-item>
+
+          <el-form-item>
+            <el-button type="primary" @click="refresh">查询</el-button>
+            <el-button
+              type="primary"
+              plain
+              @click="pageProps = { ...initProps }"
+            >
+              重置
+            </el-button>
+            <el-button v-if="!isNotSuper" type="primary" @click="addHandler">
+              新增单位
+            </el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+    </div>
+
+    <div class="relics-content">
+      <el-table default-expand-all row-key="orgId" :data="relicsArray" border>
+        <el-table-column label="单位名称" prop="orgName"></el-table-column>
+        <el-table-column
+          label="类型"
+          prop="type"
+          v-slot:default="{ row }: { row: OrganizationType }"
+        >
+          {{ row.type ? OrganizationTypeDesc[row.type] : "" }}
+        </el-table-column>
+        <el-table-column label="单位账号" prop="userName"></el-table-column>
+        <el-table-column label="单位联系人" prop="contact"></el-table-column>
+
+        <el-table-column
+          label="创建时间"
+          prop="updateTime"
+          v-slot:default="{ row }"
+        >
+          {{ row.updateTime && row.updateTime.substr(0, 16) }}
+        </el-table-column>
+        <el-table-column label="创建人" prop="createByName"></el-table-column>
+        <el-table-column label="操作" width="100px" fixed="right">
+          <template #default="{ row }: { row: OrganizationType }">
+            <el-button
+              link
+              type="primary"
+              @click="editHandler(row)"
+              size="small"
+            >
+              编辑
+            </el-button>
+            <el-button
+              link
+              type="danger"
+              v-if="!isNotSuper" 
+              @click="delOrganization(row)"
+              size="small"
+            >
+              删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+    <div class="pag-layout">
+      <el-pagination
+        background
+        layout="total, prev, pager, next, sizes, jumper"
+        v-model:page-size="pageProps.pageSize"
+        :page-sizes="[10, 20, 50, 100]"
+        :total="total"
+        @current-change="(data: number) => pageProps.pageNum = data"
+        :current-page="pageProps.pageNum"
+      />
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { onActivated, ref, watch, computed } from "vue";
+import {
+  getOrgListFetch,
+  addOrgFetch,
+  delOrgFetch,
+  alterOrgFetch,
+  PageProps,
+} from "@/request";
+import type { OrganizationType } from "@/request/organization";
+import { OrganizationTypeDesc } from "@/store/organization";
+import { organizationAdd, organizationEdit } from "./quisk";
+import { debounce } from "@/util";
+import { user } from "@/store/user";
+import { ElMessageBox } from "element-plus";
+import { openLoading, closeLoading } from "@/helper/loading";
+
+const initProps: PageProps<Partial<OrganizationType>> = {
+  pageNum: 1,
+  pageSize: 10,
+  orgName: "",
+  orgId: undefined,
+  type: undefined,
+};
+const pageProps = ref({ ...initProps });
+const total = ref<number>(0);
+const relicsArray = ref<any[]>([]);
+
+const isNotSuper = computed(
+  () =>
+    user.value.roles.filter(
+      (item) =>
+        item.roleKey === "system_admin" || item.roleKey === "system_common"
+    ).length > 0
+);
+
+// 1省级 2市级 3县级 4服务商
+
+const refresh = debounce(async () => {
+  const data = await getOrgListFetch(pageProps.value);
+  total.value = data.total;
+  // console.log('parseTree', parseTree(data.records, 'orgId'))
+  // relicsArray.value = data.records.length > 1 ? parseTree(data.records, 'orgId') : data.records
+  relicsArray.value = data.records;
+});
+
+watch(pageProps, refresh, { deep: true, immediate: true });
+onActivated(refresh);
+
+const addHandler = async () => {
+  await organizationAdd({ submit: addOrgFetch });
+  await refresh();
+};
+
+const editHandler = async (org: OrganizationType) => {
+  await organizationEdit({ org: org, submit: alterOrgFetch });
+  await refresh();
+};
+const delOrganization = async (org: OrganizationType) => {
+  console.log("org", org);
+  const ok = await ElMessageBox.confirm("确定要删除吗", {
+    type: "warning",
+  });
+  if (ok) {
+    openLoading();
+    await delOrgFetch({
+      orgId: org.orgId,
+      orgName: org.orgName,
+      type: org.type,
+    });
+    await refresh();
+    closeLoading();
+  }
+};
+</script>
+
+<style scoped lang="scss">
+.relics-layout {
+  height: 100%;
+  overflow-y: auto;
+  padding: 30px;
+}
+
+.pag-layout {
+  margin-top: 20px;
+  display: flex;
+  justify-content: center;
+}
+
+.relics-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+
+  .search {
+    flex: 1;
+  }
+
+  .relics-oper {
+    flex: 0 0 100px;
+    text-align: right;
+  }
+}
+</style>

+ 90 - 21
src/view/pano/pano.vue

@@ -14,7 +14,7 @@
         size="large"
         style="margin-right: 20px; width: 100px"
         @click="copyGis"
-        v-if="point?.pos && point.pos.length"
+        v-if="point && !noValidPoint(point)"
       >
         复制经纬度
       </el-button>
@@ -25,24 +25,26 @@
         @click="update = true"
         v-if="router.currentRoute.value.name === 'pano'"
       >
-        修改名称
+        测点说明
       </el-button>
     </div>
   </div>
   <SingleInput
     v-if="point"
     :visible="update"
+    isAllowEmpty
     @update:visible="update = false"
     :value="point.name || ''"
     :update-value="tex => updateScenePointName(point!, tex)"
-    title="修改点位名称"
+    title="测点说明"
+    placeholder="请填写测点说明"
   />
 </template>
 
 <script setup lang="ts">
 import SingleInput from "@/components/single-input.vue";
 import { router, setDocTitle } from "@/router";
-import { mergeFuns } from "@/util";
+import { mergeFuns, round } from "@/util";
 import { computed, onMounted, onUnmounted, ref, watchEffect } from "vue";
 import { init } from "./env";
 import {
@@ -51,11 +53,12 @@ import {
   ScenePoint,
   scenePoints,
 } from "@/store/scene";
-import { copyText, toDegrees } from "@/util";
+import { copyText, toDegrees, getTextBound } from "@/util";
 import { ElMessage } from "element-plus";
 import saveAs from "@/util/file-serve";
 import { DeviceType } from "@/store/device";
 import { initRelics, relics } from "@/store/relics";
+import { noValidPoint } from "../map/install";
 
 type Params = { pid?: string; relicsId?: string } | null;
 const params = computed(() => router.currentRoute.value.params as Params);
@@ -86,26 +89,92 @@ const panoUrls = computed(() => {
 const update = ref(false);
 const loading = ref(false);
 
-const copyGis = async () => {
+const getGis = () => {
   const pos = point.value!.pos as number[];
-  await copyText(
-    `经度:${toDegrees(pos[0])}, 纬度: ${toDegrees(pos[1])}, 高程: ${pos[2]}`
-  );
+  return `经度: ${toDegrees(pos[0])}\n纬度: ${toDegrees(pos[1])}\n高程: ${round(
+    pos[2],
+    4
+  )}`;
+};
+
+const copyGis = async () => {
+  await copyText(getGis());
   ElMessage.success("经纬度高程复制成功");
 };
 
-const photo = () => {
+const canvas = document.createElement("canvas");
+// 水印添加函数
+const addWatermark = (imgURL: string, ration: number) => {
+  const ctx = canvas.getContext("2d");
+  const image = new Image();
+  image.src = imgURL;
+
+  return new Promise<string>((resolve, reject) => {
+    image.onload = () => {
+      canvas.width = image.width;
+      canvas.height = image.height;
+      ctx.drawImage(image, 0, 0, image.width, image.height);
+
+      const font = `${ration * 20}px Arial`;
+      const pos = point.value!.pos as number[];
+      const lines = `经度: ${toDegrees(pos[0])}\n纬度: ${toDegrees(pos[1])}`.split("\n");
+      const lineTopPadding = 5 * ration;
+      const lineBounds = lines.map((line) =>
+        getTextBound(line, font, [lineTopPadding, 0])
+      );
+      const bound = lineBounds.reduce(
+        (t, { width, height }) => {
+          t.width = Math.max(t.width, width);
+          t.height += height;
+          return t;
+        },
+        { width: 0, height: 0 }
+      );
+      const padding = 20 * ration;
+      const margin = 80 * ration;
+
+      const position = [
+        image.width - margin - bound.width,
+        image.height - margin - bound.height,
+      ];
+
+      ctx.rect(
+        position[0] - padding,
+        position[1] - padding,
+        bound.width + 2 * padding,
+        bound.height + 2 * padding
+      );
+      ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
+      ctx.fill();
+
+      ctx.font = font;
+      ctx.textBaseline = "top";
+      ctx.fillStyle = "#fff";
+      let itemTop = 0;
+      lines.forEach((line, ndx) => {
+        ctx.fillText(line, position[0], position[1] + itemTop + lineTopPadding);
+        itemTop += lineBounds[ndx].height;
+      });
+      resolve(canvas.toDataURL("image/jpg", 1));
+    };
+    image.onerror = reject;
+  });
+};
+
+const photo = async () => {
   loading.value = true;
-  setSize(3, 1920, 1080);
-  panoDomRef.value!.toBlob(async (blob) => {
-    if (blob) {
-      await saveAs(blob, `${relics.value?.name}.jpg`);
-      ElMessage.success("图片导出成功");
-    }
+  await new Promise((resolve) => setTimeout(resolve, 300));
+  const ration = 3;
+  setSize(ration, 1920, 1080);
+  let dataURL = panoDomRef.value.toDataURL("image/jpg", 1);
+  if (!noValidPoint(point.value)) {
+    dataURL = await addWatermark(dataURL, ration);
+  }
 
-    setSize(devicePixelRatio);
-    loading.value = false;
-  }, "image/jpg");
+  await saveAs(dataURL, `${relics.value?.name}.jpg`);
+  ElMessage.success("图片导出成功");
+  setSize(devicePixelRatio);
+  loading.value = false;
 };
 
 let pano: ReturnType<typeof init>;
@@ -141,8 +210,8 @@ onMounted(() => {
 
 onUnmounted(() => mergeFuns(...destroyFns)());
 watchEffect(() => {
-  if (router.currentRoute.value.name === "pano" && point.value) {
-    setDocTitle(point.value.name);
+  if (router.currentRoute.value.name.toString().includes("pano") && point.value) {
+    setDocTitle(point.value.index.toString() || relics.value.name);
   }
 });
 </script>

+ 28 - 0
src/view/quisk.ts

@@ -2,6 +2,11 @@ import { quiskMountFactory } from "@/helper/mount";
 import RelicsEdit from "./relics-edit.vue";
 import DeviceEdit from "./device-edit.vue";
 import SceneSelect from "./scene-select.vue";
+import OrganizationAdd from "./organization-add.vue";
+import OrganizationEdit from "./organization-edit.vue";
+import UsersAdd from "./users-add.vue";
+import UsersEdit from "./users-edit.vue";
+import UsersPasswordEdit from "./users-password-edit.vue";
 
 export const relicsEdit = quiskMountFactory(RelicsEdit, {
   title: "创建文物",
@@ -16,3 +21,26 @@ export const selectScenes = quiskMountFactory(SceneSelect, {
   title: "选择场景",
   width: 1000,
 });
+
+export const organizationAdd = quiskMountFactory(OrganizationAdd, {
+  title: "添加单位",
+  width: 520,
+});
+
+export const organizationEdit = quiskMountFactory(OrganizationEdit, {
+  title: "编辑单位",
+  width: 520,
+});
+
+export const usersAdd = quiskMountFactory(UsersAdd, {
+  title: "创建用户",
+  width: 520,
+});
+export const usersEdit = quiskMountFactory(UsersEdit, {
+  title: "编辑用户",
+  width: 520,
+});
+export const usersPasswordEdit = quiskMountFactory(UsersPasswordEdit, {
+  title: "修改密码",
+  width: 520,
+});

+ 364 - 0
src/view/register/register.vue

@@ -0,0 +1,364 @@
+<template>
+  <div class="register">
+    <el-form class="panel" :model="form" :rules="rules" ref="baseFormRef">
+      <h2>单位注册</h2>
+      <span class="desc">
+        此功能仅用于注册单位及单位管理员,<br/>
+        单位内其它用户可由单位管理员登录后创建。
+      </span>
+      <el-form-item
+        class="panel-form-item"
+        label="单位名称"
+        prop="orgName"
+        required
+      >
+        <el-input
+          :maxlength="50"
+          v-model.trim="form.orgName"
+          placeholder="请输入"
+        ></el-input>
+      </el-form-item>
+
+      <el-form-item class="panel-form-item" label="类型" prop="type">
+        <el-select v-model="form.type">
+          <el-option
+            class="register-select-option"
+            :value="Number(key)"
+            :label="type"
+            v-for="(type, key) in OrganizationTypeDesc"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        class="panel-form-item"
+        label="姓名"
+        prop="contact"
+        required
+      >
+        <el-input
+          :maxlength="50"
+          v-model.trim="form.contact"
+          placeholder="请输入"
+        ></el-input>
+      </el-form-item>
+
+      <el-form-item
+        class="panel-form-item"
+        prop="userName"
+        label="账号"
+        required
+      >
+        <el-input
+          :maxlength="11"
+          v-model.trim="form.userName"
+          placeholder="请输入手机号码"
+        >
+        </el-input>
+      </el-form-item>
+
+      <el-form-item
+        class="panel-form-item msgcode"
+        prop="msgAuthCode"
+        label="验证码"
+        required
+      >
+        <el-input
+          :maxlength="8"
+          v-model.trim="form.msgAuthCode"
+          placeholder="输入验证码"
+        >
+        </el-input>
+        <el-button
+          class="getMsgAuthCode"
+          :loading="checkCodeBtn.loading"
+          :disabled="checkCodeBtn.disabled"
+          style="margin-left: 10px"
+          @click="getCheckCode"
+        >
+          {{ checkCodeBtn.text }}</el-button
+        >
+      </el-form-item>
+
+      <!-- <el-form-item class="panel-form-item" label="密码" prop="password" required>
+                <el-input v-model.trim="form.password" placeholder="请输入8-16位数字、字母大小写组合">
+                </el-input>
+            </el-form-item> -->
+      <el-form-item
+        class="panel-form-item"
+        label="密码"
+        prop="password"
+        required
+      >
+        <el-input
+          autocomplete="off"
+          readonly
+          onfocus="this.removeAttribute('readonly');"
+          v-model.trim="form.password"
+          :type="addPassFlag ? 'text' : 'password'"
+          :maxlength="20"
+          placeholder="请输入8-16位数字、字母大小写组合"
+        >
+          <template #suffix>
+            <span @click="addPassFlag = !addPassFlag" style="cursor: pointer">
+              <el-icon v-if="addPassFlag">
+                <View />
+              </el-icon>
+              <el-icon v-else>
+                <Hide />
+              </el-icon>
+            </span>
+          </template>
+        </el-input>
+      </el-form-item>
+
+      <el-form-item
+        class="panel-form-item"
+        label="密码确认"
+        prop="confirmPwd"
+        required
+      >
+        <el-input
+          autocomplete="off"
+          readonly
+          onfocus="this.removeAttribute('readonly');"
+          v-model.trim="form.confirmPwd"
+          :type="addPassFlag1 ? 'text' : 'password'"
+          :maxlength="20"
+          placeholder="输入再次输入密码"
+        >
+          <template #suffix>
+            <span @click="addPassFlag1 = !addPassFlag1" style="cursor: pointer">
+              <el-icon v-if="addPassFlag1">
+                <View />
+              </el-icon>
+              <el-icon v-else>
+                <Hide />
+              </el-icon>
+            </span>
+          </template>
+        </el-input>
+      </el-form-item>
+
+      <el-form-item class="panel-form-item">
+        <el-button type="primary" class="fill submit" @click="submitClick"
+          >注册</el-button
+        >
+      </el-form-item>
+
+      <el-form-item class="panel-form-item">
+        <el-button
+          text
+          plain
+          type="default"
+          class="fill text-btn"
+          @click="toLogin"
+          >已注册,去登录</el-button
+        >
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+<script lang="ts" setup>
+import { reactive, ref, unref } from "vue";
+import { ElMessage, type FormInstance, type FormRules } from "element-plus";
+import { OrganizationTypeDesc } from "@/store/organization";
+import { View, Hide } from "@element-plus/icons-vue";
+import { registerOrganization } from "@/request/organization";
+import { globalPasswordRex } from "@/util/regex";
+// import { encodePwd } from "@/util";
+import { getMsgAuthCode } from "@/request";
+
+const emit = defineEmits(["done"]);
+const baseFormRef = ref<FormInstance>();
+const addPassFlag = ref(false); //图标显示标识
+const addPassFlag1 = ref(false); //图标显示标识
+
+let checkCodeBtn = reactive<any>({
+  text: "获取验证码",
+  loading: false,
+  disabled: false,
+  duration: 60,
+  timer: null,
+});
+
+const equalToPassword = (_, value: any, callback: any) => {
+  if (form.password !== value) {
+    callback(new Error("两次输入的密码不一致"));
+  } else {
+    callback();
+  }
+};
+
+const rules = reactive<FormRules>({
+  orgName: [{ required: true, message: "请输入单位名称", trigger: "blur" }],
+  msgAuthCode: [{ required: true, message: "请输入验证码", trigger: "change" }],
+  contact: [{ required: true, message: "请输入姓名", trigger: "blur" }],
+  userName: [
+    { required: true, message: "请输入手机号码", trigger: "blur" },
+    {
+      required: true,
+      pattern: /^1[3456789]\d{9}$/,
+      message: "请输入手机号码",
+      trigger: "blur",
+    },
+  ],
+  password: [
+    {
+      required: true,
+      pattern: globalPasswordRex,
+      message: "请输入8-16位数字、字母大小写组合",
+      trigger: "blur",
+    },
+    { required: true, min: 8, message: "密码太短!", trigger: "blur" },
+  ],
+  confirmPwd: [
+    {
+      required: true,
+      pattern: globalPasswordRex,
+      message: "请输入8-16位数字、字母大小写组合",
+      trigger: "blur",
+    },
+    { required: true, min: 8, message: "密码太短!", trigger: "blur" },
+    { required: true, validator: equalToPassword, trigger: "blur" },
+  ],
+});
+
+const form = reactive({
+  orgName: "",
+  type: "",
+  userName: "",
+  password: "",
+  contact: "",
+  confirmPwd: "",
+  msgAuthCode: "",
+});
+
+const getCheckCode = async () => {
+  // 倒计时期间按钮不能单击
+  await unref(baseFormRef)?.validateField("userName");
+
+  const phoneNum = form.userName;
+  console.log("getMsgCode", phoneNum);
+  const res = await getMsgAuthCode("+86", phoneNum);
+  if (res.success) {
+    ElMessage.success(res.data);
+  }
+  if (checkCodeBtn.duration == 60) {
+    checkCodeBtn.disabled = true;
+  }
+  // 清除掉定时器
+  checkCodeBtn.timer && clearInterval(checkCodeBtn.timer);
+  // 开启定时器
+  checkCodeBtn.timer = setInterval(() => {
+    const tmp = checkCodeBtn.duration--;
+    checkCodeBtn.text = `${tmp}秒`;
+    if (tmp <= 0) {
+      // 清除掉定时器
+      clearInterval(checkCodeBtn.timer);
+      checkCodeBtn.duration = 60;
+      checkCodeBtn.text = "重新获取";
+      // 设置按钮可以单击
+      checkCodeBtn.disabled = false;
+    }
+    console.info(checkCodeBtn.duration);
+  }, 1000);
+};
+
+const submitClick = async () => {
+  if (unref(baseFormRef)) {
+    const res = await unref(baseFormRef)?.validate();
+    if (res) {
+      const result = await registerOrganization(form);
+      console.log("result", result);
+      emit("done");
+      // ElMessage.success('新增成功!');
+    }
+  } else {
+    throw "";
+  }
+};
+
+const toLogin = () => {
+  emit("done");
+};
+</script>
+
+<style lang="scss" scoped>
+.register {
+  padding: 10px 0;
+
+  .panel {
+    width: 430px;
+  }
+
+  .panel-form-item {
+    padding-left: 0;
+    padding-right: 0;
+  }
+
+  h2 {
+    padding-left: 0;
+    margin-bottom: 0;
+  }
+
+  .desc {
+    color: #93795d;
+    display: block;
+    margin-bottom: 20px;
+  }
+
+  :deep(.panel-form-item .el-form-item__label) {
+    line-height: 40px;
+    font-size: 16px;
+    min-width: 90px;
+  }
+
+  :deep(.el-form-item__error) {
+    font-size: 14px;
+  }
+
+  :deep(.el-select) {
+    width: 100%;
+    height: 42px;
+    line-height: 42px;
+
+    .el-select__wrapper {
+      height: 100%;
+      font-size: 16px;
+    }
+  }
+
+  .msgcode {
+    position: relative;
+  }
+
+  .getMsgAuthCode {
+    border: 1px solid #93795d;
+    background: rgba(147, 121, 93, 0.05);
+    font-size: 14px;
+    position: absolute;
+    right: 5px;
+    top: 5px;
+    height: 32px;
+    line-height: 32px;
+    &:hover {
+      color: #93795d;
+    }
+  }
+
+  .fill {
+    width: 100%;
+  }
+}
+</style>
+<style>
+.register-select-option {
+  font-size: 16px;
+  min-height: 50px;
+  line-height: 50px;
+  /* padding: 5px 0; */
+}
+.el-form-item__label:before {
+  display: none;
+}
+</style>

+ 322 - 0
src/view/register/reset.vue

@@ -0,0 +1,322 @@
+<template>
+  <div class="register">
+    <el-form class="panel" :model="form" :rules="rules" ref="baseFormRef">
+      <h2>重置密码</h2>
+      <el-form-item
+        class="panel-form-item"
+        prop="userName"
+        label="账号"
+        required
+      >
+        <el-input
+          :maxlength="11"
+          v-model.trim="form.userName"
+          placeholder="请输入手机号码"
+        >
+        </el-input>
+      </el-form-item>
+
+      <el-form-item
+        class="panel-form-item msgcode"
+        prop="msgAuthCode"
+        label="验证码"
+        required
+      >
+        <el-input
+          :maxlength="8"
+          v-model.trim="form.msgAuthCode"
+          placeholder="输入验证码"
+        >
+        </el-input>
+        <el-button
+          class="getMsgAuthCode"
+          :loading="checkCodeBtn.loading"
+          :disabled="checkCodeBtn.disabled"
+          style="margin-left: 10px"
+          @click="getCheckCode"
+        >
+          {{ checkCodeBtn.text }}</el-button
+        >
+      </el-form-item>
+
+      <el-form-item
+        class="panel-form-item"
+        label="密码"
+        prop="password"
+        required
+      >
+        <el-input
+          autocomplete="off"
+          readonly
+          onfocus="this.removeAttribute('readonly');"
+          v-model.trim="form.password"
+          :type="addPassFlag ? 'text' : 'password'"
+          :maxlength="20"
+          placeholder="请输入8-16位数字、字母大小写组合"
+        >
+          <template #suffix>
+            <span @click="addPassFlag = !addPassFlag" style="cursor: pointer">
+              <el-icon v-if="addPassFlag">
+                <View />
+              </el-icon>
+              <el-icon v-else>
+                <Hide />
+              </el-icon>
+            </span>
+          </template>
+        </el-input>
+      </el-form-item>
+
+      <el-form-item
+        class="panel-form-item"
+        label="密码确认"
+        prop="confirmPwd"
+        required
+      >
+        <el-input
+          autocomplete="off"
+          readonly
+          onfocus="this.removeAttribute('readonly');"
+          v-model.trim="form.confirmPwd"
+          :type="addPassFlag1 ? 'text' : 'password'"
+          :maxlength="20"
+          placeholder="输入再次输入密码"
+        >
+          <template #suffix>
+            <span @click="addPassFlag1 = !addPassFlag1" style="cursor: pointer">
+              <el-icon v-if="addPassFlag1">
+                <View />
+              </el-icon>
+              <el-icon v-else>
+                <Hide />
+              </el-icon>
+            </span>
+          </template>
+        </el-input>
+      </el-form-item>
+
+      <el-form-item class="panel-form-item">
+        <el-button type="primary" class="fill submit" @click="submitClick"
+          >确定</el-button
+        >
+      </el-form-item>
+      <el-form-item class="panel-form-item">
+        <el-button
+          text
+          plain
+          type="default"
+          class="fill text-btn"
+          @click="toLogin"
+          >立即登录</el-button
+        >
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+<script lang="ts" setup>
+import { reactive, ref, unref } from "vue";
+import { changePassword } from "@/request";
+import { ElMessage, type FormInstance, type FormRules } from "element-plus";
+// import { OrganizationTypeDesc } from '@/store/organization';
+import { View, Hide } from "@element-plus/icons-vue";
+import { globalPasswordRex } from "@/util/regex";
+// import { registerOrganization } from '@/request/organization';
+// import { encodePwd } from "@/util";
+
+import { getMsgAuthCode } from "@/request";
+
+const emit = defineEmits(["done"]);
+
+const baseFormRef = ref<FormInstance>();
+const addPassFlag = ref(false); //图标显示标识
+const addPassFlag1 = ref(false); //图标显示标识
+
+let checkCodeBtn = reactive<any>({
+  text: "获取验证码",
+  loading: false,
+  disabled: false,
+  duration: 60,
+  timer: null,
+});
+
+const equalToPassword = (_, value: any, callback: any) => {
+  if (form.password !== value) {
+    callback(new Error("两次输入的密码不一致"));
+  } else {
+    callback();
+  }
+};
+const rules = reactive<FormRules>({
+  orgName: [{ required: true, message: "请选择单位名称", trigger: "select" }],
+  msgAuthCode: [{ required: true, message: "请输入验证码", trigger: "change" }],
+  contact: [{ required: true, message: "请输入姓名", trigger: "blur" }],
+  userName: [
+    { required: true, message: "请输入手机号码", trigger: "blur" },
+    {
+      required: true,
+      pattern: /^1[3456789]\d{9}$/,
+      message: "请输入手机号码",
+      trigger: "blur",
+    },
+  ],
+  password: [
+    {
+      required: true,
+      pattern: globalPasswordRex,
+      message: "请输入8-16位数字、字母大小写组合",
+      trigger: "blur",
+    },
+    { required: true, min: 8, message: "密码太短!", trigger: "blur" },
+  ],
+  confirmPwd: [
+    {
+      required: true,
+      pattern: globalPasswordRex,
+      message: "请输入8-16位数字、字母大小写组合",
+      trigger: "blur",
+    },
+    { required: true, min: 8, message: "密码太短!", trigger: "blur" },
+    { required: true, validator: equalToPassword, trigger: "blur" },
+  ],
+});
+
+const form = reactive({
+  orgName: "",
+  type: "",
+  userName: "",
+  // userName: "",
+  password: "",
+  contact: "",
+  confirmPwd: "",
+  msgAuthCode: "",
+});
+
+const getCheckCode = async () => {
+  // 倒计时期间按钮不能单击
+  await unref(baseFormRef)?.validateField("userName");
+
+  const phoneNum = form.userName;
+  console.log("getMsgCode", phoneNum);
+  const res = await getMsgAuthCode("+86", phoneNum);
+  if (res.success) {
+    ElMessage.success(res.data);
+  }
+  if (checkCodeBtn.duration == 60) {
+    checkCodeBtn.disabled = true;
+  }
+  // 清除掉定时器
+  checkCodeBtn.timer && clearInterval(checkCodeBtn.timer);
+  // 开启定时器
+  checkCodeBtn.timer = setInterval(() => {
+    const tmp = checkCodeBtn.duration--;
+    checkCodeBtn.text = `${tmp}秒`;
+    if (tmp <= 0) {
+      // 清除掉定时器
+      clearInterval(checkCodeBtn.timer);
+      checkCodeBtn.duration = 60;
+      checkCodeBtn.text = "重新获取";
+      // 设置按钮可以单击
+      checkCodeBtn.disabled = false;
+    }
+    console.info(checkCodeBtn.duration);
+  }, 1000);
+};
+
+const submitClick = async () => {
+  if (unref(baseFormRef)) {
+    const res = await unref(baseFormRef)?.validate();
+    if (res) {
+      console.log("form", form);
+      const result = await changePassword({
+        ...form,
+        phoneNum: form.userName,
+      });
+      console.log("result", result);
+      ElMessage.success("重置密码成功!");
+      emit("done");
+    }
+  } else {
+    throw "";
+  }
+};
+const toLogin = () => {
+  emit("done");
+};
+</script>
+
+<style lang="scss" scoped>
+.register {
+  padding: 10px 0;
+
+  .panel {
+    width: 430px;
+  }
+
+  .panel-form-item {
+    padding-left: 0;
+    padding-right: 0;
+  }
+
+  h2 {
+    padding-left: 0;
+    margin-bottom: 0;
+  }
+
+  .desc {
+    color: #93795d;
+    display: block;
+    margin-bottom: 20px;
+  }
+
+  :deep(.panel-form-item .el-form-item__label) {
+    line-height: 40px;
+    font-size: 16px;
+    min-width: 90px;
+  }
+
+  :deep(.el-form-item__error) {
+    font-size: 14px;
+  }
+
+  :deep(.el-select) {
+    width: 100%;
+    height: 42px;
+    line-height: 42px;
+
+    .el-select__wrapper {
+      height: 100%;
+      font-size: 16px;
+    }
+  }
+
+  .msgcode {
+    position: relative;
+  }
+
+  .getMsgAuthCode {
+    border: 1px solid #93795d;
+    background: rgba(147, 121, 93, 0.05);
+    font-size: 14px;
+    position: absolute;
+    right: 5px;
+    top: 5px;
+    height: 32px;
+    line-height: 32px;
+    &:hover {
+      color: #93795d;
+    }
+  }
+
+  .fill {
+    width: 100%;
+  }
+}
+</style>
+<style>
+.register-select-option {
+  font-size: 16px;
+  min-height: 50px;
+  line-height: 50px;
+  /* padding: 5px 0; */
+}
+</style>

+ 4 - 2
src/view/relics-edit.vue

@@ -37,9 +37,11 @@
     <el-form-item label="文物地址:">
       <el-input
         v-model="data.address"
-        style="width: 100%"
+        style="width: 100%;padding-bottom: 25px;"
         :maxlength="500"
         show-word-limit
+         :autosize="{ minRows: 3, maxRows: 6 }"
+      
         type="textarea"
         placeholder="请输入"
       />
@@ -77,7 +79,7 @@ defineExpose<QuiskExpose>({
       ElMessage.error("请输入文物名称!");
       throw "请输入文物名称!";
     }
-    props.submit(data.value);
+    props.submit(data.value as any as Relics);
   },
 });
 </script>

+ 6 - 3
src/view/relics.vue

@@ -101,7 +101,9 @@
               link
               type="primary"
               size="small"
-              @click="router.push({ name: 'map', params: { relicsId: row.relicsId } })"
+              @click="
+                router.push({ name: COORD_NAME, params: { relicsId: row.relicsId } })
+              "
             >
               数据提取
             </el-button>
@@ -141,11 +143,12 @@ import {
   relicsTypeDesc,
   creationMethodDesc,
 } from "@/store/relics";
-import { router } from "@/router";
+import { COORD_NAME, router } from "@/router";
 import { ElMessageBox } from "element-plus";
 import { relicsEdit } from "./quisk";
 import TexToolTip from "@/components/tex-tooltip.vue";
 import { debounce } from "@/util";
+import { QUERY_COORD_NAME } from "@/router";
 
 const initProps: RelicsPageProps = {
   pageNum: 1,
@@ -171,7 +174,7 @@ const delHandler = async (relicsId: number) => {
   }
 };
 const getQueryRouteLocation = (row: Relics) =>
-  router.resolve({ name: "query-map", params: { relicsId: row.relicsId } });
+  router.resolve({ name: QUERY_COORD_NAME, params: { relicsId: row.relicsId } });
 
 const shareHandler = async (row: Relics) => {
   const link = location.origin + location.pathname + getQueryRouteLocation(row).href;

+ 29 - 24
src/view/scene-select.vue

@@ -13,7 +13,7 @@ import { computed, ref, watch } from "vue";
 import SceneTable from "./scene.vue";
 import { ElMessage, ElTable } from "element-plus";
 import { QuiskExpose } from "@/helper/mount";
-import { Scene, SceneStatus } from "@/store/scene";
+import { Scene, SceneStatus, scenes as rScenes } from "@/store/scene";
 
 type SimpleScene = Pick<Scene, "sceneId" | "sceneCode">;
 const props = defineProps<{
@@ -29,6 +29,12 @@ const scenes = computed(() =>
     simpleScenes.value.some(({ sceneCode }) => scene.sceneCode === sceneCode)
   )
 );
+const sceneAlls: Scene[] = [...rScenes.value];
+const selectSelects = computed(() => {
+  return simpleScenes.value.map((sScene) =>
+    sceneAlls.find((s) => s.sceneCode === sScene.sceneCode)
+  );
+});
 
 const loaded = ref(false);
 const tableProps = {
@@ -40,24 +46,24 @@ const tableProps = {
       ({ sceneCode }) => !originSceneCodes.includes(sceneCode)
       // || !val.some((scene) => scene.sceneCode === sceneCode)
     );
-    console.log(val, [...simpleScenes.value], originSceneCodes);
 
     let tip = false;
-    simpleScenes.value.push(
-      ...val
-        .filter((scene) => {
-          if (scene.calcStatus !== SceneStatus.SUCCESS) {
-            console.log("fff", scene);
-            tableProps.tableRef.value!.toggleRowSelection(scene, false);
-            tip || ElMessage.error({ message: "计算中场景无法添加", repeatNum: 1 });
-            tip = true;
-            return false;
-          } else {
-            return true;
-          }
-        })
-        .map((scene) => ({ sceneCode: scene.sceneCode, sceneId: scene.sceneId }))
-    );
+    val.forEach((scene) => {
+      if (
+        selectSelects.value.length &&
+        selectSelects.value[0].cameraType !== scene.cameraType
+      ) {
+        tableProps.tableRef.value!.toggleRowSelection(scene, false);
+        tip || ElMessage.error({ message: "请添加相同类型的场景", repeatNum: 1 });
+        tip = true;
+      } else if (scene.calcStatus !== SceneStatus.SUCCESS) {
+        tableProps.tableRef.value!.toggleRowSelection(scene, false);
+        tip || ElMessage.error({ message: "计算中场景无法添加", repeatNum: 1 });
+        tip = true;
+      } else {
+        simpleScenes.value.push({ sceneCode: scene.sceneCode, sceneId: scene.sceneId });
+      }
+    });
 
     if (props.selfScenes) {
       const foreChecks = props.selfScenes.filter(
@@ -77,12 +83,12 @@ const tableProps = {
       }
     }
 
-    console.log([...simpleScenes.value]);
     tip = false;
   },
   tableDataChange(val: Scene[]) {
     loaded.value = false;
     originScenes.value = val;
+    sceneAlls.push(...val);
     setTimeout(checkedTable);
   },
   tableRef: ref<InstanceType<typeof ElTable>>(),
@@ -92,18 +98,17 @@ let time: NodeJS.Timeout;
 const checkedTable = () => {
   if (tableProps.tableRef.value) {
     tableProps.tableRef.value!.clearSelection();
-    console.log("1");
     scenes.value.forEach((item) => {
       tableProps.tableRef.value!.toggleRowSelection(item, true);
     });
-    clearTimeout(time);
-    time = setTimeout(() => {
-      loaded.value = true;
-    }, 100);
   }
+  clearTimeout(time);
+  time = setTimeout(() => {
+    loaded.value = true;
+  }, 100);
 };
 
-watch(tableProps.tableRef, checkedTable);
+watch(() => tableProps.tableRef.value, checkedTable);
 
 defineExpose<QuiskExpose>({
   async submit() {

+ 25 - 57
src/view/scene.vue

@@ -4,50 +4,23 @@
       <div class="search">
         <el-form label-width="100px" inline>
           <el-form-item label="场景标题:">
-            <el-input
-              clearable
-              v-model="pageProps.sceneName"
-              style="width: 250px"
-              placeholder="请输入"
-            />
+            <el-input clearable v-model="pageProps.sceneName" style="width: 250px" placeholder="请输入" />
           </el-form-item>
           <el-form-item label="场景码:">
-            <el-input
-              clearable
-              v-model="pageProps.sceneCode"
-              style="width: 250px"
-              placeholder="请输入"
-            />
+            <el-input clearable v-model="pageProps.sceneCode" style="width: 250px" placeholder="请输入" />
           </el-form-item>
           <template v-if="!simple">
             <el-form-item label="SN码:">
-              <el-input
-                clearable
-                v-model="pageProps.snCode"
-                style="width: 250px"
-                placeholder="请输入"
-              />
+              <el-input clearable v-model="pageProps.snCode" style="width: 250px" placeholder="请输入" />
             </el-form-item>
             <el-form-item label="设备类型:">
               <el-select style="width: 250px" v-model="pageProps.cameraType" clearable>
-                <el-option
-                  :value="Number(key)"
-                  :label="type"
-                  v-for="(type, key) in DeviceTypeDesc"
-                />
+                <el-option :value="Number(key)" :label="type" v-for="(type, key) in DeviceTypeDesc" />
               </el-select>
             </el-form-item>
             <el-form-item label="拍摄时间:">
-              <el-date-picker
-                clearable
-                type="daterange"
-                v-model="pageProps.shootTime"
-                start-placeholder="请选择"
-                end-placeholder="请选择"
-                range-separator="-"
-                placeholder="请选择"
-                style="width: 250px"
-              />
+              <el-date-picker clearable type="daterange" v-model="pageProps.shootTime" start-placeholder="请选择"
+                end-placeholder="请选择" range-separator="-" placeholder="请选择" style="width: 250px" />
             </el-form-item>
             <!-- <el-form-item label="绑定账号:">
               <el-input
@@ -69,13 +42,8 @@
     </div>
 
     <div class="relics-content">
-      <el-table
-        :data="sceneArray"
-        border
-        row-key="'sceneCode'"
-        @selection-change="(val) => tableProps && tableProps.selectionChange(val)"
-        :ref="(table) => tableProps && (tableProps.tableRef.value = table)"
-      >
+      <el-table :data="sceneArray" border row-key="'sceneCode'" @selection-change="handleTableSelect"
+        :ref="(d) => { tableProps && ((tableProps as any).tableRef.value = d) }">
         <slot name="table"></slot>
         <el-table-column label="场景标题" v-slot:default="{ row }">
           <a class="link" @click="gotoScene(row, false)">
@@ -108,7 +76,8 @@
           <TexToolTip :text="row.shootCount || '-'" />
         </el-table-column>
         <el-table-column label="拍摄位置" v-slot:default="{ row }">
-          <TexToolTip :text="row.gpsInfo" />
+          <TexToolTip v-if="row.gpsInfo" :text="row.gpsInfo" />
+          <span v-else>-</span>
         </el-table-column>
 
         <el-table-column label="状态" v-slot:default="{ row }">
@@ -123,13 +92,8 @@
             <el-button link type="primary" size="small" @click="gotoScene(row, true)">
               编辑
             </el-button>
-            <el-button
-              link
-              type="danger"
-              @click="delHandler(row.sceneId)"
-              size="small"
-              v-if="row.calcStatus !== SceneStatus.RUN"
-            >
+            <el-button link type="danger" @click="delHandler(row.sceneId)" size="small"
+              v-if="row.calcStatus !== SceneStatus.RUN">
               删除
             </el-button>
           </template>
@@ -137,15 +101,9 @@
       </el-table>
     </div>
     <div class="pag-layout">
-      <el-pagination
-        background
-        layout="total, prev, pager, next, sizes, jumper"
-        v-model:page-size="pageProps.pageSize"
-        :page-sizes="[10, 20, 50, 100]"
-        :total="total"
-        @current-change="(data: number) => pageProps.pageNum = data"
-        :current-page="pageProps.pageNum"
-      />
+      <el-pagination background layout="total, prev, pager, next, sizes, jumper" v-model:page-size="pageProps.pageSize"
+        :page-sizes="[10, 20, 50, 100]" :total="total" @current-change="(data: number) => pageProps.pageNum = data"
+        :current-page="pageProps.pageNum" />
     </div>
   </div>
 </template>
@@ -197,6 +155,13 @@ const delHandler = async (relicsId: number) => {
 
 watch(pageProps, refresh, { deep: true, immediate: true });
 onActivated(refresh);
+
+const handleTableSelect = (val: any) => {
+  if (props.tableProps && "selectionChange" in props.tableProps) {
+    console.log("selectionChange");
+    props.tableProps.selectionChange(val);
+  }
+};
 </script>
 
 <style scoped lang="scss">
@@ -205,6 +170,7 @@ onActivated(refresh);
   overflow-y: auto;
   padding: 30px;
 }
+
 .pag-layout {
   margin-top: 20px;
   display: flex;
@@ -215,9 +181,11 @@ onActivated(refresh);
   display: flex;
   align-items: center;
   margin-bottom: 20px;
+
   .search {
     flex: 1;
   }
+
   .relics-oper {
     flex: 0 0 100px;
     text-align: right;

+ 176 - 0
src/view/step-tree-v2/StepTree.vue

@@ -0,0 +1,176 @@
+<template>
+  <div class="tree-layout">
+    <div class="seize back" v-if="ctx" :style="seizeStyle">
+      <div
+        class="tree-group-back"
+        v-for="box in ctx.groupBoxs"
+        :style="{
+          top: box.bound.y + 'px',
+          width: ctx.size.w + ctx.offset.x + 'px',
+          height: box.bound.h + 'px',
+        }"
+      />
+    </div>
+    <svg
+      :viewBox="`${ctx.offset.x} ${ctx.offset.y} ${ctx.size.w} ${ctx.size.h}`"
+      v-if="ctx"
+      :style="{ width: ctx.size.w + 'px', height: ctx.size.h + 'px' }"
+      xmlns="http://www.w3.org/2000/svg"
+    >
+      <template v-for="box in ctx.boxs">
+        <polyline
+          v-for="line in box.lines"
+          :points="line.join(',')"
+          fill="none"
+          :stroke="lineColor"
+          :stroke-width="1"
+        />
+      </template>
+      <template v-for="gbox in ctx.groupBoxs">
+        <polyline
+          :points="gbox.line.join(',')"
+          fill="none"
+          stroke-dasharray="3 3"
+          :stroke="lineColor"
+          :stroke-width="1"
+        />
+      </template>
+    </svg>
+
+    <div class="seize steps" :style="seizeStyle">
+      <div
+        class="tree-item-layout"
+        v-for="(step, i) in fs.steps"
+        :ref="(dom: HTMLDivElement) => setDom(dom, i)"
+        :style="{
+          ...getStepStyle(step),
+          padding: margin.map((p) => p + 'px').join(' '),
+        }"
+      >
+        <div class="tree-item-inner">
+          <slot name="step" :data="step.raw" :start="ctx && ctx.offset" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { DataStepTree, flatSteps, getStepsTreeCtx, NStep } from "./helper-v2";
+import { computed, reactive, onUpdated, ref, watch, onMounted } from "vue";
+import { DataStep } from "./type";
+
+const props = withDefaults(
+  defineProps<{
+    margin?: number[];
+    data: DataStep[];
+    lineColor?: string;
+  }>(),
+  {
+    margin: () => [10, 10],
+  }
+);
+
+const fs = computed(() => {
+  return flatSteps(props.data);
+});
+const stepNodes = reactive([]) as HTMLDivElement[];
+
+const getStepStyle = (step: NStep<DataStepTree<DataStep>>) => {
+  if (!ctx.value) return {};
+  const box = ctx.value.boxs.find((box) => box.step === step);
+  return {
+    left: box.offset.x + "px",
+    top: box.offset.y + "px",
+  };
+};
+
+const setDom = (dom: HTMLDivElement, ndx: number) => {
+  if (loaded.value) {
+    stepNodes[ndx] = dom;
+  }
+};
+
+const loaded = ref(false);
+watch(
+  () => props,
+  () => {
+    stepNodes.length = 0;
+    loaded.value = false;
+  },
+  { flush: "pre", deep: true }
+);
+
+onUpdated(() => (loaded.value = true));
+onMounted(() => (loaded.value = true));
+
+const ctx = computed(() => {
+  if (stepNodes.length !== fs.value.steps.length || !loaded.value) return;
+  const ctx = getStepsTreeCtx(
+    fs.value.steps,
+    props.margin,
+    (step) => {
+      const ndx = fs.value.steps.findIndex(({ raw }) => raw === step);
+      const node = stepNodes[ndx];
+      return { w: node.offsetWidth, h: node.offsetHeight };
+    },
+    fs.value.group
+  );
+  console.log(ctx);
+  return ctx;
+});
+
+// 跟svg坐标系保持一致
+const seizeStyle = computed(() => {
+  if (!ctx.value) return {};
+  return {
+    left: -ctx.value.offset.x + "px",
+    top: -ctx.value.offset.y + "px",
+  };
+});
+</script>
+
+<style lang="scss" scoped>
+.tree-layout {
+  display: inline-block;
+  position: relative;
+  overflow: hidden;
+
+  .seize {
+    left: 0;
+    top: 0;
+    width: 9000px;
+    height: 9000px;
+    position: absolute;
+    pointer-events: none;
+
+    &.steps {
+      z-index: 2;
+    }
+
+    &.back {
+      z-index: 1;
+    }
+  }
+
+  svg {
+    position: relative;
+    z-index: 1;
+  }
+}
+
+.tree-group-back {
+  position: absolute;
+  background-color: #f2f2f2;
+  left: 0;
+}
+
+.tree-item-layout {
+  position: absolute;
+  box-sizing: border-box;
+
+  .tree-item-inner {
+    pointer-events: all;
+  }
+}
+</style>

+ 689 - 0
src/view/step-tree-v2/example/data/1.json

@@ -0,0 +1,689 @@
+{
+	"end_time": "2024_07_11_08:48:22",
+	"start_time": "20240711081512",
+	"status": "partsuccess",
+	"steps": [{
+		"displayName": "Stop AP7 And MA7",
+		"name": "step1",
+		"status": "success",
+		"steps": [{
+			"action": "stop",
+			"displayName": "Stop AP7",
+			"hosts": [{
+				"host": "qlaasap7",
+				"status": "success"
+			}],
+			"name": "step1_1",
+			"serviceType": "AASAP_part7",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "stop",
+			"displayName": "Stop MA7",
+			"hosts": [{
+				"host": "qlaasma7",
+				"status": "success"
+			}],
+			"name": "step1_2",
+			"serviceType": "AASMA_part7",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "step2",
+		"name": "step2",
+		"status": "success",
+		"steps": [{
+			"action": "humanWaiting",
+			"displayName": "AP7 & MA7 application have closed, next step deploy application",
+			"name": "step2_1",
+			"status": "success",
+			"type": "execution"
+		}]
+	},
+	{
+		"displayName": "Deploy AP7 MA7 PW7 SC7 HT7 And MH7",
+		"name": "step3",
+		"status": "success",
+		"steps": [{
+			"action": "deploy",
+			"displayName": "Deploy AP7",
+			"hosts": [{
+				"host": "qlaasap7",
+				"status": "success"
+			}],
+			"name": "step3_1",
+			"serviceType": "AASAP_part7",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy MA7",
+			"hosts": [{
+				"host": "qlaasma7",
+				"status": "success"
+			}],
+			"name": "step3_2",
+			"serviceType": "AASMA_part7",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy PW7",
+			"hosts": [{
+				"host": "qlaaspw7",
+				"status": "success"
+			}],
+			"name": "step3_3",
+			"serviceType": "AASPW_part7",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy SC7",
+			"hosts": [{
+				"host": "qlaassc7",
+				"status": "success"
+			}],
+			"name": "step3_4",
+			"serviceType": "AASSC_part7",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy HT7",
+			"hosts": [{
+				"host": "qlaasht7",
+				"status": "success"
+			}],
+			"name": "step3_5",
+			"serviceType": "AASHT_part7",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy MH7",
+			"hosts": [{
+				"host": "qlaasmh7",
+				"status": "success"
+			}],
+			"name": "step3_6",
+			"serviceType": "AASMH_part7",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "step4",
+		"name": "step4",
+		"status": "success",
+		"steps": [{
+			"action": "humanWaiting",
+			"displayName": "Waiting for verify internal environment",
+			"name": "step4_1",
+			"status": "success",
+			"type": "execution"
+		}]
+	},
+	{
+		"displayName": "stop BM1 BM2 application",
+		"name": "step5",
+		"status": "success",
+		"steps": [{
+			"action": "stop",
+			"displayName": "Stop BM1",
+			"hosts": [{
+				"host": "qlaasbm1",
+				"status": "success"
+			}],
+			"name": "step5_1",
+			"serviceType": "AASBM_part1",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "stop",
+			"displayName": "Stop BM2",
+			"hosts": [{
+				"host": "qlaasbm2",
+				"status": "success"
+			}],
+			"name": "step5_2",
+			"serviceType": "AASBM_part2",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "Deploy BM1 And BM2 application",
+		"name": "step6",
+		"status": "success",
+		"steps": [{
+			"action": "deploy",
+			"displayName": "Deploy BM1",
+			"hosts": [{
+				"host": "qlaasbm1",
+				"status": "success"
+			}],
+			"name": "step6_1",
+			"serviceType": "AASBM_part1",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy BM2",
+			"hosts": [{
+				"host": "qlaasbm2",
+				"status": "success"
+			}],
+			"name": "step6_2",
+			"serviceType": "AASBM_part2",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "step7",
+		"name": "step7",
+		"status": "success",
+		"steps": [{
+			"action": "humanWaiting",
+			"displayName": "Waiting for verify BM1 And BM2 application",
+			"name": "step7_1",
+			"status": "success",
+			"type": "execution"
+		}]
+	},
+	{
+		"displayName": "Stop HT1 And MH1 F5",
+		"name": "step8",
+		"status": "success",
+		"steps": [{
+			"action": "stop",
+			"displayName": "Stop HT1",
+			"hosts": [{
+				"host": "qlaasht3",
+				"status": "success"
+			}],
+			"name": "step8_1",
+			"serviceType": "AASHT_part1",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "stop",
+			"displayName": "Stop MH1",
+			"hosts": [{
+				"host": "qlaasmh1",
+				"status": "success"
+			}],
+			"name": "step8_2",
+			"serviceType": "AASMH_part1",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "step9",
+		"name": "step9",
+		"status": "success",
+		"steps": [{
+			"action": "humanWaiting",
+			"displayName": "Finish HT1 & MH1 closeF5, next step stop AP1 MA1 PW1 And SC1 application",
+			"name": "step9_1",
+			"status": "success",
+			"type": "execution"
+		}]
+	},
+	{
+		"displayName": "stop AP1 MA1 PW1 And SC1 application",
+		"name": "step10",
+		"status": "success",
+		"steps": [{
+			"action": "stop",
+			"displayName": "Stop AP1",
+			"hosts": [{
+				"host": "qlaasap1",
+				"status": "success"
+			}],
+			"name": "step10_1",
+			"serviceType": "AASAP_part1",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "stop",
+			"displayName": "Stop MA1",
+			"hosts": [{
+				"host": "qlaasma1",
+				"status": "success"
+			}],
+			"name": "step10_2",
+			"serviceType": "AASMA_part1",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "stop",
+			"displayName": "Stop PW1",
+			"hosts": [{
+				"host": "qlaaspw1",
+				"status": "success"
+			}],
+			"name": "step10_3",
+			"serviceType": "AASPW_part1",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "stop",
+			"displayName": "Stop SC1",
+			"hosts": [{
+				"host": "qlaassc1",
+				"status": "success"
+			}],
+			"name": "step10_4",
+			"serviceType": "AASSC_part1",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "step11",
+		"name": "step11",
+		"status": "success",
+		"steps": [{
+			"action": "humanWaiting",
+			"displayName": "all application have closed, next step Deploy all application",
+			"name": "step11_1",
+			"status": "success",
+			"type": "execution"
+		}]
+	},
+	{
+		"displayName": "Deploy AP1 MA1 PW1 And SC1",
+		"name": "step12",
+		"status": "success",
+		"steps": [{
+			"action": "deploy",
+			"displayName": "Deploy AP1",
+			"hosts": [{
+				"host": "qlaasap1",
+				"status": "success"
+			}],
+			"name": "step12_1",
+			"serviceType": "AASAP_part1",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy MA1",
+			"hosts": [{
+				"host": "qlaasma1",
+				"status": "success"
+			}],
+			"name": "step12_2",
+			"serviceType": "AASMA_part1",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy PW1",
+			"hosts": [{
+				"host": "qlaaspw1",
+				"status": "success"
+			}],
+			"name": "step12_3",
+			"serviceType": "AASPW_part1",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy SC1",
+			"hosts": [{
+				"host": "qlaassc1",
+				"status": "success"
+			}],
+			"name": "step12_4",
+			"serviceType": "AASSC_part1",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "Deploy HT1 And MH1",
+		"name": "step13",
+		"status": "success",
+		"steps": [{
+			"action": "deploy",
+			"displayName": "Deploy HT1",
+			"hosts": [{
+				"host": "qlaasht3",
+				"status": "success"
+			}],
+			"name": "step13_1",
+			"serviceType": "AASHT_part1",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy MH1",
+			"hosts": [{
+				"host": "qlaasmh1",
+				"status": "success"
+			}],
+			"name": "step13_2",
+			"serviceType": "AASMH_part1",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "step14",
+		"name": "step14",
+		"status": "success",
+		"steps": [{
+			"action": "humanWaiting",
+			"displayName": "Waiting for external services",
+			"name": "step14_1",
+			"status": "success",
+			"type": "execution"
+		}]
+	},
+	{
+		"displayName": "StartF5 HT1 And MH1",
+		"name": "step15",
+		"status": "success",
+		"steps": [{
+			"action": "start",
+			"displayName": "StartF5 HT1",
+			"hosts": [{
+				"host": "qlaasht3",
+				"status": "success"
+			}],
+			"name": "step15_1",
+			"serviceType": "AASHT_part1",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "start",
+			"displayName": "StartF5 MH1",
+			"hosts": [{
+				"host": "qlaasmh1",
+				"status": "success"
+			}],
+			"name": "step15_2",
+			"serviceType": "AASMH_part1",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "Stop HT2 And MH2 closeF5, stop ap2 ma2 pw2 and sc2 application",
+		"name": "step16",
+		"status": "success",
+		"steps": [{
+			"action": "stop",
+			"displayName": "Stop HT2",
+			"hosts": [{
+				"host": "qlaasht4",
+				"status": "success"
+			}],
+			"name": "step16_1",
+			"serviceType": "AASHT_part2",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "stop",
+			"displayName": "Stop MH2",
+			"hosts": [{
+				"host": "qlaasmh2",
+				"status": "success"
+			}],
+			"name": "step16_2",
+			"serviceType": "AASMH_part2",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "stop",
+			"displayName": "Stop AP2",
+			"hosts": [{
+				"host": "qlaasap2",
+				"status": "success"
+			}],
+			"name": "step16_3",
+			"serviceType": "AASAP_part2",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "stop",
+			"displayName": "Stop MA2",
+			"hosts": [{
+				"host": "qlaasma2",
+				"status": "success"
+			}],
+			"name": "step16_4",
+			"serviceType": "AASMA_part2",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "stop",
+			"displayName": "Stop PW2",
+			"hosts": [{
+				"host": "qlaaspw2",
+				"status": "success"
+			}],
+			"name": "step16_5",
+			"serviceType": "AASPW_part2",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "stop",
+			"displayName": "Stop SC2",
+			"hosts": [{
+				"host": "qlaassc2",
+				"status": "success"
+			}],
+			"name": "step16_6",
+			"serviceType": "AASSC_part2",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "step17",
+		"name": "step17",
+		"status": "success",
+		"steps": [{
+			"action": "humanWaiting",
+			"displayName": "Finish HT2 & MH2 closeF5, stop ap2 ma2 pw2 and sc2 application,next step Deploy AP2 MA2 PW2 And SC2",
+			"name": "step17_1",
+			"status": "success",
+			"type": "execution"
+		}]
+	},
+	{
+		"displayName": "Deploy AP2 MA2 PW2 And SC2",
+		"name": "step18",
+		"status": "success",
+		"steps": [{
+			"action": "deploy",
+			"displayName": "Deploy AP2",
+			"hosts": [{
+				"host": "qlaasap2",
+				"status": "success"
+			}],
+			"name": "step18_1",
+			"serviceType": "AASAP_part2",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy MA2",
+			"hosts": [{
+				"host": "qlaasma2",
+				"status": "success"
+			}],
+			"name": "step18_2",
+			"serviceType": "AASMA_part2",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy PW2",
+			"hosts": [{
+				"host": "qlaaspw2",
+				"status": "success"
+			}],
+			"name": "step18_3",
+			"serviceType": "AASPW_part2",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy SC2",
+			"hosts": [{
+				"host": "qlaassc2",
+				"status": "success"
+			}],
+			"name": "step18_4",
+			"serviceType": "AASSC_part2",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "Deploy HT2 And MH2",
+		"name": "step19",
+		"status": "success",
+		"steps": [{
+			"action": "deploy",
+			"displayName": "Deploy HT2",
+			"hosts": [{
+				"host": "qlaasht4",
+				"status": "success"
+			}],
+			"name": "step19_1",
+			"serviceType": "AASHT_part2",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy MH2",
+			"hosts": [{
+				"host": "qlaasmh2",
+				"status": "success"
+			}],
+			"name": "step19_2",
+			"serviceType": "AASMH_part2",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "StartF5 HT2 And MH2",
+		"name": "step20",
+		"status": "success",
+		"steps": [{
+			"action": "start",
+			"displayName": "StartF5 HT2",
+			"hosts": [{
+				"host": "qlaasht4",
+				"status": "success"
+			}],
+			"name": "step20_1",
+			"serviceType": "AASHT_part2",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "start",
+			"displayName": "StartF5 MH2",
+			"hosts": [{
+				"host": "qlaasmh2",
+				"status": "success"
+			}],
+			"name": "step20_2",
+			"serviceType": "AASMH_part2",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	}],
+	"subStepsParallel": false,
+	"yamlversion": "v1.0"
+}

+ 161 - 0
src/view/step-tree-v2/example/data/2.json

@@ -0,0 +1,161 @@
+{
+	"end_time": "2024_07_09_10:47:34",
+	"start_time": "20240709104623",
+	"status": "success",
+	"steps": [{
+		"displayName": "Stop All Services",
+		"name": "step1",
+		"status": "success",
+		"steps": [{
+			"action": "stop",
+			"displayName": "Stop app1_part1 Services",
+			"hosts": [{
+				"host": "quadpax1",
+				"status": "success"
+			}],
+			"name": "step1_1",
+			"serviceType": "app1_part1",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"displayName": "Stop app2 Services",
+			"name": "step1_2",
+			"status": "success",
+			"steps": [{
+				"action": "stop",
+				"displayName": "Stop app2_part1 Services",
+				"hosts": [{
+					"host": "quadpax2",
+					"status": "success"
+				}],
+				"name": "step1_2_1",
+				"serviceType": "app2_part1",
+				"serviceTypeParallel": true,
+				"status": "success",
+				"type": "execution"
+			},
+			{
+				"displayName": "Stop app2_part2 Services",
+				"name": "step1_2_2",
+				"status": "success",
+				"steps": [{
+					"action": "stop",
+					"displayName": "Stop app2_part2 Services",
+					"hosts": [{
+						"host": "quadpax3",
+						"status": "success"
+					},
+					{
+						"host": "qladpax3",
+						"status": "success"
+					}],
+					"name": "step1_2_2_1",
+					"serviceType": "app2_part2",
+					"serviceTypeParallel": true,
+					"status": "success",
+					"type": "execution"
+				},
+				{
+					"action": "stop",
+					"displayName": "Stop app2_part3 Services",
+					"hosts": [{
+						"host": "quadpax4",
+						"status": "success"
+					},
+					{
+						"host": "qladpax4",
+						"status": "success"
+					}],
+					"name": "step1_2_2_2",
+					"serviceType": "app2_part3",
+					"serviceTypeParallel": true,
+					"status": "success",
+					"type": "execution"
+				}],
+				"subStepsParallel": true
+			}],
+			"subStepsParallel": true
+		}],
+		"subStepsParallel": false
+	},
+	{
+		"displayName": "step2",
+		"name": "step2",
+		"status": "success",
+		"steps": [{
+			"action": "humanWaiting",
+			"displayName": "Waiting",
+			"name": "step2_1",
+			"status": "success",
+			"type": "execution"
+		}]
+	},
+	{
+		"displayName": "deploy All Services",
+		"name": "step3",
+		"status": "success",
+		"steps": [{
+			"action": "deploy",
+			"displayName": "deploy app1_part1 Services",
+			"hosts": [{
+				"host": "quadpax1",
+				"status": "success"
+			}],
+			"name": "step3_1",
+			"serviceType": "app1_part1",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "deploy app2_part1 Services",
+			"hosts": [{
+				"host": "quadpax2",
+				"status": "success"
+			}],
+			"name": "step3_2",
+			"serviceType": "app2_part1",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "start All Services",
+		"name": "step4",
+		"status": "success",
+		"steps": [{
+			"action": "start",
+			"displayName": "start app1_part1 Services",
+			"hosts": [{
+				"host": "quadpax1",
+				"status": "success"
+			}],
+			"name": "step4_1",
+			"serviceType": "app1_part1",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		},
+		{
+			"action": "start",
+			"displayName": "start app2_part1 Services",
+			"hosts": [{
+				"host": "quadpax2",
+				"status": "success"
+			}],
+			"name": "step4_2",
+			"serviceType": "app2_part1",
+			"serviceTypeParallel": true,
+			"status": "success",
+			"type": "execution"
+		}],
+		"subStepsParallel": false
+	}],
+	"subStepsParallel": false,
+	"yamlversion": "v1.0"
+}

+ 689 - 0
src/view/step-tree-v2/example/data/3.json

@@ -0,0 +1,689 @@
+{
+	"end_time": "",
+	"start_time": "20240611181515",
+	"status": "waiting",
+	"steps": [{
+		"displayName": "Stop AP7 And MA7",
+		"name": "step1",
+		"status": "error",
+		"steps": [{
+			"action": "stop",
+			"displayName": "Stop AP7",
+			"hosts": [{
+				"host": "qlaasap7",
+				"status": "error"
+			}],
+			"name": "step1_1",
+			"serviceType": "AASAP_part7",
+			"serviceTypeParallel": true,
+			"status": "error",
+			"type": "execution"
+		},
+		{
+			"action": "stop",
+			"displayName": "Stop MA7",
+			"hosts": [{
+				"host": "qlaasma7",
+				"status": "error"
+			}],
+			"name": "step1_2",
+			"serviceType": "AASMA_part7",
+			"serviceTypeParallel": true,
+			"status": "error",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "step2",
+		"name": "step2",
+		"status": "waiting",
+		"steps": [{
+			"action": "humanWaiting",
+			"displayName": "AP7 & MA7 application have closed, next step deploy application",
+			"name": "step2_1",
+			"status": "waiting",
+			"type": "execution"
+		}]
+	},
+	{
+		"displayName": "Deploy AP7 MA7 PW7 SC7 HT7 And MH7",
+		"name": "step3",
+		"status": "waiting",
+		"steps": [{
+			"action": "deploy",
+			"displayName": "Deploy AP7",
+			"hosts": [{
+				"host": "qlaasap7",
+				"status": "waiting"
+			}],
+			"name": "step3_1",
+			"serviceType": "AASAP_part7",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy MA7",
+			"hosts": [{
+				"host": "qlaasma7",
+				"status": "waiting"
+			}],
+			"name": "step3_2",
+			"serviceType": "AASMA_part7",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy PW7",
+			"hosts": [{
+				"host": "qlaaspw7",
+				"status": "waiting"
+			}],
+			"name": "step3_3",
+			"serviceType": "AASPW_part7",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy SC7",
+			"hosts": [{
+				"host": "qlaassc7",
+				"status": "waiting"
+			}],
+			"name": "step3_4",
+			"serviceType": "AASSC_part7",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy HT7",
+			"hosts": [{
+				"host": "qlaasht7",
+				"status": "waiting"
+			}],
+			"name": "step3_5",
+			"serviceType": "AASHT_part7",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy MH7",
+			"hosts": [{
+				"host": "qlaasmh7",
+				"status": "waiting"
+			}],
+			"name": "step3_6",
+			"serviceType": "AASMH_part7",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "step4",
+		"name": "step4",
+		"status": "waiting",
+		"steps": [{
+			"action": "humanWaiting",
+			"displayName": "Waiting for verify internal environment",
+			"name": "step4_1",
+			"status": "waiting",
+			"type": "execution"
+		}]
+	},
+	{
+		"displayName": "stop BM1 BM2 application",
+		"name": "step5",
+		"status": "waiting",
+		"steps": [{
+			"action": "stop",
+			"displayName": "Stop BM1",
+			"hosts": [{
+				"host": "qlaasbm1",
+				"status": "waiting"
+			}],
+			"name": "step5_1",
+			"serviceType": "AASBM_part1",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "stop",
+			"displayName": "Stop BM2",
+			"hosts": [{
+				"host": "qlaasbm2",
+				"status": "waiting"
+			}],
+			"name": "step5_2",
+			"serviceType": "AASBM_part2",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "Deploy BM1 And BM2 application",
+		"name": "step6",
+		"status": "waiting",
+		"steps": [{
+			"action": "deploy",
+			"displayName": "Deploy BM1",
+			"hosts": [{
+				"host": "qlaasbm1",
+				"status": "waiting"
+			}],
+			"name": "step6_1",
+			"serviceType": "AASBM_part1",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy BM2",
+			"hosts": [{
+				"host": "qlaasbm2",
+				"status": "waiting"
+			}],
+			"name": "step6_2",
+			"serviceType": "AASBM_part2",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "step7",
+		"name": "step7",
+		"status": "waiting",
+		"steps": [{
+			"action": "humanWaiting",
+			"displayName": "Waiting for verify BM1 And BM2 application",
+			"name": "step7_1",
+			"status": "waiting",
+			"type": "execution"
+		}]
+	},
+	{
+		"displayName": "Stop HT1 And MH1 F5",
+		"name": "step8",
+		"status": "waiting",
+		"steps": [{
+			"action": "stop",
+			"displayName": "Stop HT1",
+			"hosts": [{
+				"host": "qlaasht3",
+				"status": "waiting"
+			}],
+			"name": "step8_1",
+			"serviceType": "AASHT_part1",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "stop",
+			"displayName": "Stop MH1",
+			"hosts": [{
+				"host": "qlaasmh1",
+				"status": "waiting"
+			}],
+			"name": "step8_2",
+			"serviceType": "AASMH_part1",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "step9",
+		"name": "step9",
+		"status": "waiting",
+		"steps": [{
+			"action": "humanWaiting",
+			"displayName": "Finish HT1 & MH1 closeF5, next step stop AP1 MA1 PW1 And SC1 application",
+			"name": "step9_1",
+			"status": "waiting",
+			"type": "execution"
+		}]
+	},
+	{
+		"displayName": "stop AP1 MA1 PW1 And SC1 application",
+		"name": "step10",
+		"status": "waiting",
+		"steps": [{
+			"action": "stop",
+			"displayName": "Stop AP1",
+			"hosts": [{
+				"host": "qlaasap1",
+				"status": "waiting"
+			}],
+			"name": "step10_1",
+			"serviceType": "AASAP_part1",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "stop",
+			"displayName": "Stop MA1",
+			"hosts": [{
+				"host": "qlaasma1",
+				"status": "waiting"
+			}],
+			"name": "step10_2",
+			"serviceType": "AASMA_part1",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "stop",
+			"displayName": "Stop PW1",
+			"hosts": [{
+				"host": "qlaaspw1",
+				"status": "waiting"
+			}],
+			"name": "step10_3",
+			"serviceType": "AASPW_part1",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "stop",
+			"displayName": "Stop SC1",
+			"hosts": [{
+				"host": "qlaassc1",
+				"status": "waiting"
+			}],
+			"name": "step10_4",
+			"serviceType": "AASSC_part1",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "step11",
+		"name": "step11",
+		"status": "waiting",
+		"steps": [{
+			"action": "humanWaiting",
+			"displayName": "all application have closed, next step Deploy all application",
+			"name": "step11_1",
+			"status": "waiting",
+			"type": "execution"
+		}]
+	},
+	{
+		"displayName": "Deploy AP1 MA1 PW1 And SC1",
+		"name": "step12",
+		"status": "waiting",
+		"steps": [{
+			"action": "deploy",
+			"displayName": "Deploy AP1",
+			"hosts": [{
+				"host": "qlaasap1",
+				"status": "waiting"
+			}],
+			"name": "step12_1",
+			"serviceType": "AASAP_part1",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy MA1",
+			"hosts": [{
+				"host": "qlaasma1",
+				"status": "waiting"
+			}],
+			"name": "step12_2",
+			"serviceType": "AASMA_part1",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy PW1",
+			"hosts": [{
+				"host": "qlaaspw1",
+				"status": "waiting"
+			}],
+			"name": "step12_3",
+			"serviceType": "AASPW_part1",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy SC1",
+			"hosts": [{
+				"host": "qlaassc1",
+				"status": "waiting"
+			}],
+			"name": "step12_4",
+			"serviceType": "AASSC_part1",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "Deploy HT1 And MH1",
+		"name": "step13",
+		"status": "waiting",
+		"steps": [{
+			"action": "deploy",
+			"displayName": "Deploy HT1",
+			"hosts": [{
+				"host": "qlaasht3",
+				"status": "waiting"
+			}],
+			"name": "step13_1",
+			"serviceType": "AASHT_part1",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy MH1",
+			"hosts": [{
+				"host": "qlaasmh1",
+				"status": "waiting"
+			}],
+			"name": "step13_2",
+			"serviceType": "AASMH_part1",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "step14",
+		"name": "step14",
+		"status": "waiting",
+		"steps": [{
+			"action": "humanWaiting",
+			"displayName": "Waiting for external services",
+			"name": "step14_1",
+			"status": "waiting",
+			"type": "execution"
+		}]
+	},
+	{
+		"displayName": "StartF5 HT1 And MH1",
+		"name": "step15",
+		"status": "waiting",
+		"steps": [{
+			"action": "start",
+			"displayName": "StartF5 HT1",
+			"hosts": [{
+				"host": "qlaasht3",
+				"status": "waiting"
+			}],
+			"name": "step15_1",
+			"serviceType": "AASHT_part1",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "start",
+			"displayName": "StartF5 MH1",
+			"hosts": [{
+				"host": "qlaasmh1",
+				"status": "waiting"
+			}],
+			"name": "step15_2",
+			"serviceType": "AASMH_part1",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "Stop HT2 And MH2 closeF5, stop ap2 ma2 pw2 and sc2 application",
+		"name": "step16",
+		"status": "waiting",
+		"steps": [{
+			"action": "stop",
+			"displayName": "Stop HT2",
+			"hosts": [{
+				"host": "qlaasht4",
+				"status": "waiting"
+			}],
+			"name": "step16_1",
+			"serviceType": "AASHT_part2",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "stop",
+			"displayName": "Stop MH2",
+			"hosts": [{
+				"host": "qlaasmh2",
+				"status": "waiting"
+			}],
+			"name": "step16_2",
+			"serviceType": "AASMH_part2",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "stop",
+			"displayName": "Stop AP2",
+			"hosts": [{
+				"host": "qlaasap2",
+				"status": "waiting"
+			}],
+			"name": "step16_3",
+			"serviceType": "AASAP_part2",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "stop",
+			"displayName": "Stop MA2",
+			"hosts": [{
+				"host": "qlaasma2",
+				"status": "waiting"
+			}],
+			"name": "step16_4",
+			"serviceType": "AASMA_part2",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "stop",
+			"displayName": "Stop PW2",
+			"hosts": [{
+				"host": "qlaaspw2",
+				"status": "waiting"
+			}],
+			"name": "step16_5",
+			"serviceType": "AASPW_part2",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "stop",
+			"displayName": "Stop SC2",
+			"hosts": [{
+				"host": "qlaassc2",
+				"status": "waiting"
+			}],
+			"name": "step16_6",
+			"serviceType": "AASSC_part2",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "step17",
+		"name": "step17",
+		"status": "waiting",
+		"steps": [{
+			"action": "humanWaiting",
+			"displayName": "Finish HT2 & MH2 closeF5, stop ap2 ma2 pw2 and sc2 application,next step Deploy AP2 MA2 PW2 And SC2",
+			"name": "step17_1",
+			"status": "waiting",
+			"type": "execution"
+		}]
+	},
+	{
+		"displayName": "Deploy AP2 MA2 PW2 And SC2",
+		"name": "step18",
+		"status": "waiting",
+		"steps": [{
+			"action": "deploy",
+			"displayName": "Deploy AP2",
+			"hosts": [{
+				"host": "qlaasap2",
+				"status": "waiting"
+			}],
+			"name": "step18_1",
+			"serviceType": "AASAP_part2",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy MA2",
+			"hosts": [{
+				"host": "qlaasma2",
+				"status": "waiting"
+			}],
+			"name": "step18_2",
+			"serviceType": "AASMA_part2",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy PW2",
+			"hosts": [{
+				"host": "qlaaspw2",
+				"status": "waiting"
+			}],
+			"name": "step18_3",
+			"serviceType": "AASPW_part2",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy SC2",
+			"hosts": [{
+				"host": "qlaassc2",
+				"status": "waiting"
+			}],
+			"name": "step18_4",
+			"serviceType": "AASSC_part2",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "Deploy HT2 And MH2",
+		"name": "step19",
+		"status": "waiting",
+		"steps": [{
+			"action": "deploy",
+			"displayName": "Deploy HT2",
+			"hosts": [{
+				"host": "qlaasht4",
+				"status": "waiting"
+			}],
+			"name": "step19_1",
+			"serviceType": "AASHT_part2",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "deploy",
+			"displayName": "Deploy MH2",
+			"hosts": [{
+				"host": "qlaasmh2",
+				"status": "waiting"
+			}],
+			"name": "step19_2",
+			"serviceType": "AASMH_part2",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	},
+	{
+		"displayName": "StartF5 HT2 And MH2",
+		"name": "step20",
+		"status": "waiting",
+		"steps": [{
+			"action": "start",
+			"displayName": "StartF5 HT2",
+			"hosts": [{
+				"host": "qlaasht4",
+				"status": "waiting"
+			}],
+			"name": "step20_1",
+			"serviceType": "AASHT_part2",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		},
+		{
+			"action": "start",
+			"displayName": "StartF5 MH2",
+			"hosts": [{
+				"host": "qlaasmh2",
+				"status": "waiting"
+			}],
+			"name": "step20_2",
+			"serviceType": "AASMH_part2",
+			"serviceTypeParallel": true,
+			"status": "waiting",
+			"type": "execution"
+		}],
+		"subStepsParallel": true
+	}],
+	"subStepsParallel": false,
+	"yamlversion": "v1.0"
+}

+ 65 - 0
src/view/step-tree-v2/example/data/4.json

@@ -0,0 +1,65 @@
+{
+	"end_time": "2024_06_28_17:05:39",
+	"start_time": "20240628170507",
+	"status": "success",
+	"steps": [{
+		"displayName": "deploy MGSHZ ",
+		"name": "step1",
+		"status": "success",
+		"steps": [{
+			"action": "deploy",
+			"displayName": "deploy MGSHZ Services",
+			"hosts": [{
+				"host": "qlmgshz1",
+				"status": "success"
+			},
+			{
+				"host": "qlmgshz2",
+				"status": "success"
+			}],
+			"name": "step1_1",
+			"serviceType": "MGSHZ",
+			"serviceTypeParallel": false,
+			"status": "success",
+			"type": "execution"
+		}],
+		"subStepsParallel": false
+	},
+	{
+		"displayName": "deploy MGSAZ",
+		"name": "step2",
+		"status": "success",
+		"steps": [{
+			"action": "deploy",
+			"displayName": "deploy MGSAZ Services",
+			"hosts": [{
+				"host": "qlmgsaz1",
+				"status": "success"
+			},
+			{
+				"host": "qlmgsaz2",
+				"status": "success"
+			}],
+			"name": "step2_1",
+			"serviceType": "MGSAZ",
+			"serviceTypeParallel": false,
+			"status": "success",
+			"type": "execution"
+		}],
+		"subStepsParallel": false
+	},
+	{
+		"displayName": "step3",
+		"name": "step3",
+		"status": "success",
+		"steps": [{
+			"action": "humanWaiting",
+			"displayName": "waiting for delpoy DB",
+			"name": "step3_1",
+			"status": "success",
+			"type": "execution"
+		}]
+	}],
+	"subStepsParallel": false,
+	"yamlversion": "v1.0"
+}

+ 811 - 0
src/view/step-tree-v2/example/data/5.json

@@ -0,0 +1,811 @@
+{
+  "end_time": "",
+  "start_time": "20240601085011",
+  "status": "waiting",
+  "steps": [
+    {
+      "displayName": "Stop AP7 And MA7",
+      "name": "step1",
+      "status": "success",
+      "steps": [
+        {
+          "action": "stop",
+          "displayName": "Stop AP7",
+          "hosts": [
+            {
+              "host": "qlaasap7",
+              "status": "success"
+            }
+          ],
+          "name": "step1_1",
+          "serviceType": "AASAP_part7",
+          "serviceTypeParallel": true,
+          "status": "success",
+          "type": "execution"
+        },
+        {
+          "action": "stop",
+          "displayName": "Stop MA7",
+          "hosts": [
+            {
+              "host": "qlaasma7",
+              "status": "success"
+            }
+          ],
+          "name": "step1_2",
+          "serviceType": "AASMA_part7",
+          "serviceTypeParallel": true,
+          "status": "success",
+          "type": "execution"
+        }
+      ],
+      "subStepsParallel": true
+    },
+    {
+      "displayName": "step2",
+      "name": "step2",
+      "status": "success",
+      "steps": [
+        {
+          "action": "humanWaiting",
+          "displayName": "AP7 & MA7 application have closed, next step deploy application",
+          "name": "step2_1",
+          "status": "success",
+          "type": "execution"
+        }
+      ]
+    },
+    {
+      "displayName": "Deploy AP7 MA7 PW7 SC7 HT7 And MH7",
+      "name": "step3",
+      "status": "success",
+      "steps": [
+        {
+          "action": "deploy",
+          "displayName": "Deploy AP7",
+          "hosts": [
+            {
+              "host": "qlaasap7",
+              "status": "success"
+            }
+          ],
+          "name": "step3_1",
+          "serviceType": "AASAP_part7",
+          "serviceTypeParallel": true,
+          "status": "success",
+          "type": "execution"
+        },
+        {
+          "action": "deploy",
+          "displayName": "Deploy MA7",
+          "hosts": [
+            {
+              "host": "qlaasma7",
+              "status": "success"
+            }
+          ],
+          "name": "step3_2",
+          "serviceType": "AASMA_part7",
+          "serviceTypeParallel": true,
+          "status": "success",
+          "type": "execution"
+        },
+        {
+          "action": "deploy",
+          "displayName": "Deploy PW7",
+          "hosts": [
+            {
+              "host": "qlaaspw7",
+              "status": "success"
+            }
+          ],
+          "name": "step3_3",
+          "serviceType": "AASPW_part7",
+          "serviceTypeParallel": true,
+          "status": "success",
+          "type": "execution"
+        },
+        {
+          "action": "deploy",
+          "displayName": "Deploy SC7",
+          "hosts": [
+            {
+              "host": "qlaassc7",
+              "status": "success"
+            }
+          ],
+          "name": "step3_4",
+          "serviceType": "AASSC_part7",
+          "serviceTypeParallel": true,
+          "status": "success",
+          "type": "execution"
+        },
+        {
+          "action": "deploy",
+          "displayName": "Deploy HT7",
+          "hosts": [
+            {
+              "host": "qlaasht7",
+              "status": "success"
+            }
+          ],
+          "name": "step3_5",
+          "serviceType": "AASHT_part7",
+          "serviceTypeParallel": true,
+          "status": "success",
+          "type": "execution"
+        },
+        {
+          "action": "deploy",
+          "displayName": "Deploy MH7",
+          "hosts": [
+            {
+              "host": "qlaasmh7",
+              "status": "success"
+            }
+          ],
+          "name": "step3_6",
+          "serviceType": "AASMH_part7",
+          "serviceTypeParallel": true,
+          "status": "success",
+          "type": "execution"
+        }
+      ],
+      "subStepsParallel": true
+    },
+    {
+      "displayName": "step4",
+      "name": "step4",
+      "status": "success",
+      "steps": [
+        {
+          "action": "humanWaiting",
+          "displayName": "Waiting for verify internal environment",
+          "name": "step4_1",
+          "status": "success",
+          "type": "execution"
+        }
+      ]
+    },
+    {
+      "displayName": "stop BM1 BM2 application",
+      "name": "step5",
+      "status": "success",
+      "steps": [
+        {
+          "action": "stop",
+          "displayName": "Stop BM1",
+          "hosts": [
+            {
+              "host": "qlaasbm1",
+              "status": "success"
+            }
+          ],
+          "name": "step5_1",
+          "serviceType": "AASBM_part1",
+          "serviceTypeParallel": true,
+          "status": "success",
+          "type": "execution"
+        },
+        {
+          "action": "stop",
+          "displayName": "Stop BM2",
+          "hosts": [
+            {
+              "host": "qlaasbm2",
+              "status": "success"
+            }
+          ],
+          "name": "step5_2",
+          "serviceType": "AASBM_part2",
+          "serviceTypeParallel": true,
+          "status": "success",
+          "type": "execution"
+        }
+      ],
+      "subStepsParallel": true
+    },
+    {
+      "displayName": "Deploy BM1 And BM2 application",
+      "name": "step6",
+      "status": "success",
+      "steps": [
+        {
+          "action": "deploy",
+          "displayName": "Deploy BM1",
+          "hosts": [
+            {
+              "host": "qlaasbm1",
+              "status": "success"
+            }
+          ],
+          "name": "step6_1",
+          "serviceType": "AASBM_part1",
+          "serviceTypeParallel": true,
+          "status": "success",
+          "type": "execution"
+        },
+        {
+          "action": "deploy",
+          "displayName": "Deploy BM2",
+          "hosts": [
+            {
+              "host": "qlaasbm2",
+              "status": "success"
+            }
+          ],
+          "name": "step6_2",
+          "serviceType": "AASBM_part2",
+          "serviceTypeParallel": true,
+          "status": "success",
+          "type": "execution"
+        }
+      ],
+      "subStepsParallel": true
+    },
+    {
+      "displayName": "step7",
+      "name": "step7",
+      "status": "success",
+      "steps": [
+        {
+          "action": "humanWaiting",
+          "displayName": "Waiting for verify BM1 And BM2 application",
+          "name": "step7_1",
+          "status": "success",
+          "type": "execution"
+        }
+      ]
+    },
+    {
+      "displayName": "Stop HT1 And MH1 F5",
+      "name": "step8",
+      "status": "success",
+      "steps": [
+        {
+          "action": "stop",
+          "displayName": "Stop HT1",
+          "hosts": [
+            {
+              "host": "qlaasht3",
+              "status": "success"
+            }
+          ],
+          "name": "step8_1",
+          "serviceType": "AASHT_part1",
+          "serviceTypeParallel": true,
+          "status": "success",
+          "type": "execution"
+        },
+        {
+          "action": "stop",
+          "displayName": "Stop MH1",
+          "hosts": [
+            {
+              "host": "qlaasmh1",
+              "status": "success"
+            }
+          ],
+          "name": "step8_2",
+          "serviceType": "AASMH_part1",
+          "serviceTypeParallel": true,
+          "status": "success",
+          "type": "execution"
+        }
+      ],
+      "subStepsParallel": true
+    },
+    {
+      "displayName": "step9",
+      "name": "step9",
+      "status": "running",
+      "steps": [
+        {
+          "action": "humanWaiting",
+          "displayName": "Finish HT1 & MH1 closeF5, next step stop AP1 MA1 PW1 And SC1 application",
+          "name": "step9_1",
+          "status": "running",
+          "type": "execution"
+        }
+      ]
+    },
+    {
+      "displayName": "stop AP1 MA1 PW1 And SC1 application",
+      "name": "step10",
+      "status": "waiting",
+      "steps": [
+        {
+          "action": "stop",
+          "displayName": "Stop AP1",
+          "hosts": [
+            {
+              "host": "qlaasap1",
+              "status": "error"
+            }
+          ],
+          "name": "step10_1",
+          "serviceType": "AASAP_part1",
+          "serviceTypeParallel": true,
+          "status": "error",
+          "type": "execution"
+        },
+        {
+          "action": "stop",
+          "displayName": "Stop MA1",
+          "hosts": [
+            {
+              "host": "qlaasma1",
+              "status": "success"
+            }
+          ],
+          "name": "step10_2",
+          "serviceType": "AASMA_part1",
+          "serviceTypeParallel": true,
+          "status": "success",
+          "type": "execution"
+        },
+        {
+          "action": "stop",
+          "displayName": "Stop PW1",
+          "hosts": [
+            {
+              "host": "qlaaspw1",
+              "status": "success"
+            }
+          ],
+          "name": "step10_3",
+          "serviceType": "AASPW_part1",
+          "serviceTypeParallel": true,
+          "status": "success",
+          "type": "execution"
+        },
+        {
+          "action": "stop",
+          "displayName": "Stop SC1",
+          "hosts": [
+            {
+              "host": "qlaassc1",
+              "status": "success"
+            }
+          ],
+          "name": "step10_4",
+          "serviceType": "AASSC_part1",
+          "serviceTypeParallel": true,
+          "status": "success",
+          "type": "execution"
+        }
+      ],
+      "subStepsParallel": true
+    },
+    {
+      "displayName": "step11",
+      "name": "step11",
+      "status": "waiting",
+      "steps": [
+        {
+          "action": "humanWaiting",
+          "displayName": "all application have closed, next step Deploy all application",
+          "name": "step11_1",
+          "status": "waiting",
+          "type": "execution"
+        }
+      ]
+    },
+    {
+      "displayName": "Deploy AP1 MA1 PW1 And SC1",
+      "name": "step12",
+      "status": "waiting",
+      "steps": [
+        {
+          "action": "deploy",
+          "displayName": "Deploy AP1",
+          "hosts": [
+            {
+              "host": "qlaasap1",
+              "status": "waiting"
+            }
+          ],
+          "name": "step12_1",
+          "serviceType": "AASAP_part1",
+          "serviceTypeParallel": true,
+          "status": "waiting",
+          "type": "execution"
+        },
+        {
+          "action": "deploy",
+          "displayName": "Deploy MA1",
+          "hosts": [
+            {
+              "host": "qlaasma1",
+              "status": "waiting"
+            }
+          ],
+          "name": "step12_2",
+          "serviceType": "AASMA_part1",
+          "serviceTypeParallel": true,
+          "status": "waiting",
+          "type": "execution"
+        },
+        {
+          "action": "deploy",
+          "displayName": "Deploy PW1",
+          "hosts": [
+            {
+              "host": "qlaaspw1",
+              "status": "waiting"
+            }
+          ],
+          "name": "step12_3",
+          "serviceType": "AASPW_part1",
+          "serviceTypeParallel": true,
+          "status": "waiting",
+          "type": "execution"
+        },
+        {
+          "action": "deploy",
+          "displayName": "Deploy SC1",
+          "hosts": [
+            {
+              "host": "qlaassc1",
+              "status": "waiting"
+            }
+          ],
+          "name": "step12_4",
+          "serviceType": "AASSC_part1",
+          "serviceTypeParallel": true,
+          "status": "waiting",
+          "type": "execution"
+        }
+      ],
+      "subStepsParallel": true
+    },
+    {
+      "displayName": "Deploy HT1 And MH1",
+      "name": "step13",
+      "status": "waiting",
+      "steps": [
+        {
+          "action": "deploy",
+          "displayName": "Deploy HT1",
+          "hosts": [
+            {
+              "host": "qlaasht3",
+              "status": "waiting"
+            }
+          ],
+          "name": "step13_1",
+          "serviceType": "AASHT_part1",
+          "serviceTypeParallel": true,
+          "status": "waiting",
+          "type": "execution"
+        },
+        {
+          "action": "deploy",
+          "displayName": "Deploy MH1",
+          "hosts": [
+            {
+              "host": "qlaasmh1",
+              "status": "waiting"
+            }
+          ],
+          "name": "step13_2",
+          "serviceType": "AASMH_part1",
+          "serviceTypeParallel": true,
+          "status": "waiting",
+          "type": "execution"
+        }
+      ],
+      "subStepsParallel": true
+    },
+    {
+      "displayName": "step14",
+      "name": "step14",
+      "status": "waiting",
+      "steps": [
+        {
+          "action": "humanWaiting",
+          "displayName": "Waiting for external services",
+          "name": "step14_1",
+          "status": "waiting",
+          "type": "execution"
+        }
+      ]
+    },
+    {
+      "displayName": "StartF5 HT1 And MH1",
+      "name": "step15",
+      "status": "waiting",
+      "steps": [
+        {
+          "action": "start",
+          "displayName": "StartF5 HT1",
+          "hosts": [
+            {
+              "host": "qlaasht3",
+              "status": "waiting"
+            }
+          ],
+          "name": "step15_1",
+          "serviceType": "AASHT_part1",
+          "serviceTypeParallel": true,
+          "status": "waiting",
+          "type": "execution"
+        },
+        {
+          "action": "start",
+          "displayName": "StartF5 MH1",
+          "hosts": [
+            {
+              "host": "qlaasmh1",
+              "status": "waiting"
+            }
+          ],
+          "name": "step15_2",
+          "serviceType": "AASMH_part1",
+          "serviceTypeParallel": true,
+          "status": "waiting",
+          "type": "execution"
+        }
+      ],
+      "subStepsParallel": true
+    },
+    {
+      "displayName": "Stop HT2 And MH2 closeF5, stop ap2 ma2 pw2 and sc2 application",
+      "name": "step16",
+      "status": "waiting",
+      "steps": [
+        {
+          "action": "stop",
+          "displayName": "Stop HT2",
+          "hosts": [
+            {
+              "host": "qlaasht4",
+              "status": "waiting"
+            }
+          ],
+          "name": "step16_1",
+          "serviceType": "AASHT_part2",
+          "serviceTypeParallel": true,
+          "status": "waiting",
+          "type": "execution"
+        },
+        {
+          "action": "stop",
+          "displayName": "Stop MH2",
+          "hosts": [
+            {
+              "host": "qlaasmh2",
+              "status": "waiting"
+            }
+          ],
+          "name": "step16_2",
+          "serviceType": "AASMH_part2",
+          "serviceTypeParallel": true,
+          "status": "waiting",
+          "type": "execution"
+        },
+        {
+          "action": "stop",
+          "displayName": "Stop AP2",
+          "hosts": [
+            {
+              "host": "qlaasap2",
+              "status": "waiting"
+            }
+          ],
+          "name": "step16_3",
+          "serviceType": "AASAP_part2",
+          "serviceTypeParallel": true,
+          "status": "waiting",
+          "type": "execution"
+        },
+        {
+          "action": "stop",
+          "displayName": "Stop MA2",
+          "hosts": [
+            {
+              "host": "qlaasma2",
+              "status": "waiting"
+            }
+          ],
+          "name": "step16_4",
+          "serviceType": "AASMA_part2",
+          "serviceTypeParallel": true,
+          "status": "waiting",
+          "type": "execution"
+        },
+        {
+          "action": "stop",
+          "displayName": "Stop PW2",
+          "hosts": [
+            {
+              "host": "qlaaspw2",
+              "status": "waiting"
+            }
+          ],
+          "name": "step16_5",
+          "serviceType": "AASPW_part2",
+          "serviceTypeParallel": true,
+          "status": "waiting",
+          "type": "execution"
+        },
+        {
+          "action": "stop",
+          "displayName": "Stop SC2",
+          "hosts": [
+            {
+              "host": "qlaassc2",
+              "status": "waiting"
+            }
+          ],
+          "name": "step16_6",
+          "serviceType": "AASSC_part2",
+          "serviceTypeParallel": true,
+          "status": "waiting",
+          "type": "execution"
+        }
+      ],
+      "subStepsParallel": true
+    },
+    {
+      "displayName": "step17",
+      "name": "step17",
+      "status": "waiting",
+      "steps": [
+        {
+          "action": "humanWaiting",
+          "displayName": "Finish HT2 & MH2 closeF5, stop ap2 ma2 pw2 and sc2 application,next step Deploy AP2 MA2 PW2 And SC2",
+          "name": "step17_1",
+          "status": "waiting",
+          "type": "execution"
+        }
+      ]
+    },
+    {
+      "displayName": "Deploy AP2 MA2 PW2 And SC2",
+      "name": "step18",
+      "status": "waiting",
+      "steps": [
+        {
+          "action": "deploy",
+          "displayName": "Deploy AP2",
+          "hosts": [
+            {
+              "host": "qlaasap2",
+              "status": "waiting"
+            }
+          ],
+          "name": "step18_1",
+          "serviceType": "AASAP_part2",
+          "serviceTypeParallel": true,
+          "status": "waiting",
+          "type": "execution"
+        },
+        {
+          "action": "deploy",
+          "displayName": "Deploy MA2",
+          "hosts": [
+            {
+              "host": "qlaasma2",
+              "status": "waiting"
+            }
+          ],
+          "name": "step18_2",
+          "serviceType": "AASMA_part2",
+          "serviceTypeParallel": true,
+          "status": "waiting",
+          "type": "execution"
+        },
+        {
+          "action": "deploy",
+          "displayName": "Deploy PW2",
+          "hosts": [
+            {
+              "host": "qlaaspw2",
+              "status": "waiting"
+            }
+          ],
+          "name": "step18_3",
+          "serviceType": "AASPW_part2",
+          "serviceTypeParallel": true,
+          "status": "waiting",
+          "type": "execution"
+        },
+        {
+          "action": "deploy",
+          "displayName": "Deploy SC2",
+          "hosts": [
+            {
+              "host": "qlaassc2",
+              "status": "waiting"
+            }
+          ],
+          "name": "step18_4",
+          "serviceType": "AASSC_part2",
+          "serviceTypeParallel": true,
+          "status": "waiting",
+          "type": "execution"
+        }
+      ],
+      "subStepsParallel": true
+    },
+    {
+      "displayName": "Deploy HT2 And MH2",
+      "name": "step19",
+      "status": "waiting",
+      "steps": [
+        {
+          "action": "deploy",
+          "displayName": "Deploy HT2",
+          "hosts": [
+            {
+              "host": "qlaasht4",
+              "status": "waiting"
+            }
+          ],
+          "name": "step19_1",
+          "serviceType": "AASHT_part2",
+          "serviceTypeParallel": true,
+          "status": "waiting",
+          "type": "execution"
+        },
+        {
+          "action": "deploy",
+          "displayName": "Deploy MH2",
+          "hosts": [
+            {
+              "host": "qlaasmh2",
+              "status": "waiting"
+            }
+          ],
+          "name": "step19_2",
+          "serviceType": "AASMH_part2",
+          "serviceTypeParallel": true,
+          "status": "waiting",
+          "type": "execution"
+        }
+      ],
+      "subStepsParallel": true
+    },
+    {
+      "displayName": "StartF5 HT2 And MH2",
+      "name": "step20",
+      "status": "waiting",
+      "steps": [
+        {
+          "action": "start",
+          "displayName": "StartF5 HT2",
+          "hosts": [
+            {
+              "host": "qlaasht4",
+              "status": "waiting"
+            }
+          ],
+          "name": "step20_1",
+          "serviceType": "AASHT_part2",
+          "serviceTypeParallel": true,
+          "status": "waiting",
+          "type": "execution"
+        },
+        {
+          "action": "start",
+          "displayName": "StartF5 MH2",
+          "hosts": [
+            {
+              "host": "qlaasmh2",
+              "status": "waiting"
+            }
+          ],
+          "name": "step20_2",
+          "serviceType": "AASMH_part2",
+          "serviceTypeParallel": true,
+          "status": "waiting",
+          "type": "execution"
+        }
+      ],
+      "subStepsParallel": true
+    }
+  ],
+  "subStepsParallel": false,
+  "yamlversion": "v1.0"
+}

+ 191 - 0
src/view/step-tree-v2/example/data/6.json

@@ -0,0 +1,191 @@
+{
+    "end_time": "2024_07_12_09:08:54",
+    "start_time": "20240712090823",
+    "status": "success",
+    "steps": [
+        {
+            "displayName": "Stop All Services",
+            "name": "step1",
+            "status": "success",
+            "steps": [
+                {
+                    "action": "stop",
+                    "displayName": "Stop app1_part1 Services",
+                    "hosts": [
+                        {
+                            "host": "quadpax1",
+                            "status": "success"
+                        }
+                    ],
+                    "name": "step1_1",
+                    "serviceType": "app1_part1",
+                    "serviceTypeParallel": true,
+                    "status": "success",
+                    "type": "execution"
+                },
+                {
+                    "displayName": "Stop app2 Services",
+                    "name": "step1_2",
+                    "status": "success",
+                    "steps": [
+                        {
+                            "action": "stop",
+                            "displayName": "Stop app2_part1 Services",
+                            "hosts": [
+                                {
+                                    "host": "quadpax2",
+                                    "status": "success"
+                                }
+                            ],
+                            "name": "step1_2_1",
+                            "serviceType": "app2_part1",
+                            "serviceTypeParallel": true,
+                            "status": "success",
+                            "type": "execution"
+                        },
+                        {
+                            "displayName": "Stop app2_part2 Services",
+                            "name": "step1_2_2",
+                            "status": "success",
+                            "steps": [
+                                {
+                                    "action": "stop",
+                                    "displayName": "Stop app2_part2 Services",
+                                    "hosts": [
+                                        {
+                                            "host": "quadpax3",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "qladpax3",
+                                            "status": "success"
+                                        }
+                                    ],
+                                    "name": "step1_2_2_1",
+                                    "serviceType": "app2_part2",
+                                    "serviceTypeParallel": true,
+                                    "status": "success",
+                                    "type": "execution"
+                                },
+                                {
+                                    "action": "stop",
+                                    "displayName": "Stop app2_part3 Services",
+                                    "hosts": [
+                                        {
+                                            "host": "quadpax4",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "qladpax4",
+                                            "status": "success"
+                                        }
+                                    ],
+                                    "name": "step1_2_2_2",
+                                    "serviceType": "app2_part3",
+                                    "serviceTypeParallel": true,
+                                    "status": "success",
+                                    "type": "execution"
+                                }
+                            ],
+                            "subStepsParallel": true
+                        }
+                    ],
+                    "subStepsParallel": false
+                }
+            ],
+            "subStepsParallel": false
+        },
+        {
+            "displayName": "step2",
+            "name": "step2",
+            "status": "success",
+            "steps": [
+                {
+                    "action": "humanWaiting",
+                    "displayName": "Waiting",
+                    "name": "step2_1",
+                    "status": "success",
+                    "type": "execution"
+                }
+            ]
+        },
+        {
+            "displayName": "deploy All Services",
+            "name": "step3",
+            "status": "success",
+            "steps": [
+                {
+                    "action": "deploy",
+                    "displayName": "deploy app1_part1 Services",
+                    "hosts": [
+                        {
+                            "host": "quadpax1",
+                            "status": "success"
+                        }
+                    ],
+                    "name": "step3_1",
+                    "serviceType": "app1_part1",
+                    "serviceTypeParallel": true,
+                    "status": "success",
+                    "type": "execution"
+                },
+                {
+                    "action": "deploy",
+                    "displayName": "deploy app2_part1 Services",
+                    "hosts": [
+                        {
+                            "host": "quadpax2",
+                            "status": "success"
+                        }
+                    ],
+                    "name": "step3_2",
+                    "serviceType": "app2_part1",
+                    "serviceTypeParallel": true,
+                    "status": "success",
+                    "type": "execution"
+                }
+            ],
+            "subStepsParallel": true
+        },
+        {
+            "displayName": "start All Services",
+            "name": "step4",
+            "status": "success",
+            "steps": [
+                {
+                    "action": "start",
+                    "displayName": "start app1_part1 Services",
+                    "hosts": [
+                        {
+                            "host": "quadpax1",
+                            "status": "success"
+                        }
+                    ],
+                    "name": "step4_1",
+                    "serviceType": "app1_part1",
+                    "serviceTypeParallel": true,
+                    "status": "success",
+                    "type": "execution"
+                },
+                {
+                    "action": "start",
+                    "displayName": "start app2_part1 Services",
+                    "hosts": [
+                        {
+                            "host": "quadpax2",
+                            "status": "success"
+                        }
+                    ],
+                    "name": "step4_2",
+                    "serviceType": "app2_part1",
+                    "serviceTypeParallel": true,
+                    "status": "success",
+                    "type": "execution"
+                }
+            ],
+            "subStepsParallel": false
+        }
+    ],
+    "subStepsParallel": false,
+    "yamlversion": "v1.0"
+}

File diff ditekan karena terlalu besar
+ 1 - 0
src/view/step-tree-v2/example/data/7.json


File diff ditekan karena terlalu besar
+ 1 - 0
src/view/step-tree-v2/example/data/8.json


+ 380 - 0
src/view/step-tree-v2/example/data/9.json

@@ -0,0 +1,380 @@
+{
+    "end_time": "2024_07_12_09:08:54",
+    "start_time": "20240712090823",
+    "status": "success",
+    "steps": [
+        {
+            "displayName": "Stop All Services",
+            "name": "step1",
+            "status": "success",
+            "steps": [
+                {
+                    "displayName": "Stop app1 Services",
+                    "name": "step1_1",
+                    "status": "success",
+                    "steps": [
+                        {
+                            "displayName": "Stop app1_part1 Services",
+                            "name": "step1_1_1",
+                            "status": "success",
+                            "steps": [
+                                {
+                                    "action": "stop",
+                                    "displayName": "Stop app1_part2 Services",
+                                    "hosts": [
+                                        {
+                                            "host": "quadpax3",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax4",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax5",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax6",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax7",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax8",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax9",
+                                            "status": "success"
+                                        }
+                                    ],
+                                    "name": "step1_1_1_1",
+                                    "serviceType": "app1_part2",
+                                    "serviceTypeParallel": true,
+                                    "status": "success",
+                                    "type": "execution"
+                                },
+                                {
+                                    "action": "stop",
+                                    "displayName": "Stop app1_part3 Services",
+                                    "hosts": [
+                                        {
+                                            "host": "quadpax4",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "qladpax4",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax5",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax6",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax7",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax8",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax9",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax10",
+                                            "status": "success"
+                                        }
+                                    ],
+                                    "name": "step1_1_1_2",
+                                    "serviceType": "app1_part3",
+                                    "serviceTypeParallel": true,
+                                    "status": "success",
+                                    "type": "execution"
+                                }
+                            ],
+                            "subStepsParallel": true
+                        },
+                        {
+                            "displayName": "Stop app1_part2 Services",
+                            "name": "step1_1_2",
+                            "status": "success",
+                            "steps": [
+                                {
+                                    "action": "stop",
+                                    "displayName": "Stop app1_part2 Services",
+                                    "hosts": [
+                                        {
+                                            "host": "quadpax3",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "qladpax3",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax5",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax6",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax7",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax8",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax9",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax10",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax11",
+                                            "status": "success"
+                                        }
+                                    ],
+                                    "name": "step1_1_2_1",
+                                    "serviceType": "app1_part2",
+                                    "serviceTypeParallel": true,
+                                    "status": "success",
+                                    "type": "execution"
+                                },
+                                {
+                                    "action": "stop",
+                                    "displayName": "Stop app1_part3 Services",
+                                    "hosts": [
+                                        {
+                                            "host": "quadpax4",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "qladpax4",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax5",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax6",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax7",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax8",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax9",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax10",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax11",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "quadpax12",
+                                            "status": "success"
+                                        }
+                                    ],
+                                    "name": "step1_1_2_2",
+                                    "serviceType": "app1_part3",
+                                    "serviceTypeParallel": true,
+                                    "status": "success",
+                                    "type": "execution"
+                                }
+                            ],
+                            "subStepsParallel": false
+                        }
+                    ],
+                    "subStepsParallel": true
+                },
+                {
+                    "displayName": "Stop app2 Services",
+                    "name": "step1_2",
+                    "status": "success",
+                    "steps": [
+                        {
+                            "action": "stop",
+                            "displayName": "Stop app2_part1 Services",
+                            "hosts": [
+                                {
+                                    "host": "quadpax2",
+                                    "status": "success"
+                                }
+                            ],
+                            "name": "step1_2_1",
+                            "serviceType": "app2_part1",
+                            "serviceTypeParallel": true,
+                            "status": "success",
+                            "type": "execution"
+                        },
+                        {
+                            "displayName": "Stop app2_part2 Services",
+                            "name": "step1_2_2",
+                            "status": "success",
+                            "steps": [
+                                {
+                                    "action": "stop",
+                                    "displayName": "Stop app2_part2 Services",
+                                    "hosts": [
+                                        {
+                                            "host": "quadpax3",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "qladpax3",
+                                            "status": "success"
+                                        }
+                                    ],
+                                    "name": "step1_2_2_1",
+                                    "serviceType": "app2_part2",
+                                    "serviceTypeParallel": true,
+                                    "status": "success",
+                                    "type": "execution"
+                                },
+                                {
+                                    "action": "stop",
+                                    "displayName": "Stop app2_part3 Services",
+                                    "hosts": [
+                                        {
+                                            "host": "quadpax4",
+                                            "status": "success"
+                                        },
+                                        {
+                                            "host": "qladpax4",
+                                            "status": "success"
+                                        }
+                                    ],
+                                    "name": "step1_2_2_2",
+                                    "serviceType": "app2_part3",
+                                    "serviceTypeParallel": true,
+                                    "status": "success",
+                                    "type": "execution"
+                                }
+                            ],
+                            "subStepsParallel": true
+                        }
+                    ],
+                    "subStepsParallel": false
+                }
+            ],
+            "subStepsParallel": false
+        },
+        {
+            "displayName": "step2",
+            "name": "step2",
+            "status": "success",
+            "steps": [
+                {
+                    "action": "humanWaiting",
+                    "displayName": "Waiting",
+                    "name": "step2_1",
+                    "status": "success",
+                    "type": "execution"
+                }
+            ]
+        },
+        {
+            "displayName": "deploy All Services",
+            "name": "step3",
+            "status": "success",
+            "steps": [
+                {
+                    "action": "deploy",
+                    "displayName": "deploy app1_part1 Services",
+                    "hosts": [
+                        {
+                            "host": "quadpax1",
+                            "status": "success"
+                        }
+                    ],
+                    "name": "step3_1",
+                    "serviceType": "app1_part1",
+                    "serviceTypeParallel": true,
+                    "status": "success",
+                    "type": "execution"
+                },
+                {
+                    "action": "deploy",
+                    "displayName": "deploy app2_part1 Services",
+                    "hosts": [
+                        {
+                            "host": "quadpax2",
+                            "status": "success"
+                        }
+                    ],
+                    "name": "step3_2",
+                    "serviceType": "app2_part1",
+                    "serviceTypeParallel": true,
+                    "status": "success",
+                    "type": "execution"
+                }
+            ],
+            "subStepsParallel": true
+        },
+        {
+            "displayName": "start All Services",
+            "name": "step4",
+            "status": "success",
+            "steps": [
+                {
+                    "action": "start",
+                    "displayName": "start app1_part1 Services",
+                    "hosts": [
+                        {
+                            "host": "quadpax1",
+                            "status": "success"
+                        }
+                    ],
+                    "name": "step4_1",
+                    "serviceType": "app1_part1",
+                    "serviceTypeParallel": true,
+                    "status": "success",
+                    "type": "execution"
+                },
+                {
+                    "action": "start",
+                    "displayName": "start app2_part1 Services",
+                    "hosts": [
+                        {
+                            "host": "quadpax2",
+                            "status": "success"
+                        }
+                    ],
+                    "name": "step4_2",
+                    "serviceType": "app2_part1",
+                    "serviceTypeParallel": true,
+                    "status": "success",
+                    "type": "execution"
+                }
+            ],
+            "subStepsParallel": false
+        }
+    ],
+    "subStepsParallel": false,
+    "yamlversion": "v1.0"
+}

+ 109 - 0
src/view/step-tree-v2/example/example.vue

@@ -0,0 +1,109 @@
+<template>
+  <div class="status-box flex">
+    <!-- <span class="defualt">运行中</span> -->
+    <span class="waiting">等待中</span>
+    <span class="running">运行中</span>
+    <span class="succ">成功</span>
+    <span class="bf-suc">部分成功</span>
+    <span class="error">失败</span>
+    <select v-model="activeNdx">
+      <option :value="n" v-for="(_, n) in items">测试数据{{ n }}</option>
+    </select>
+  </div>
+  <div class="tree-cont-wrap" ref="treeWrapRef">
+    <StepTree :data="items[activeNdx].steps" :margin="[25, 25]" lineColor="#89beb2">
+      <template #step="{ data, start }">
+        <Step
+          :item="data"
+          :start="start"
+          @click="stepClickHandler(data)"
+          @clickHost="stepHostClickHandler"
+        />
+      </template>
+    </StepTree>
+  </div>
+</template>
+
+<script setup>
+import StepTree from "../StepTree.vue";
+import Step from "./step.vue";
+import data1 from "./data/1.json";
+import data2 from "./data/2.json";
+import data3 from "./data/3.json";
+import data4 from "./data/4.json";
+import data5 from "./data/5.json";
+import data6 from "./data/6.json";
+import data7 from "./data/7.json";
+import data8 from "./data/8.json";
+import data9 from "./data/9.json";
+import { ref, nextTick, toRefs, watchEffect } from "vue";
+import { useRouter } from "vue-router";
+
+const treeWrapRef = ref();
+const $router = useRouter();
+const items = [data1, data2, data3, data4, data5, data6, data7, data8, data9];
+const activeNdx = ref("8");
+
+const stepClickHandler = (step) => {
+  if (step.type === "startEnd") return;
+  let url = $router.resolve({
+    path: "/stepLogs",
+    params: {
+      key: 1,
+    },
+  }).href;
+  window.open(url, "_blank");
+};
+const stepHostClickHandler = (host) => {
+  console.log(host);
+};
+nextTick(() => {
+  treeWrapRef.value.scrollLeft =
+    (treeWrapRef.value.scrollWidth - treeWrapRef.value.clientWidth) / 2;
+});
+</script>
+
+<style scoped>
+.tree-cont-wrap {
+  width: 100%;
+  height: 100%;
+  margin: 0 auto;
+  text-align: center;
+}
+
+.flex {
+  display: flex;
+}
+.status-box {
+  width: 400px;
+}
+.status-box span {
+  display: inline-block;
+  width: 120px;
+  height: 30px;
+  line-height: 30px;
+  color: #333;
+  text-align: center;
+  margin: 0 5px;
+}
+/* 运行中 */
+.running {
+  background: #ecf752;
+}
+/* 错误 */
+.error {
+  background: #ff4238;
+}
+/* 成功 */
+.succ {
+  background: #30d567;
+}
+/* 等待中 */
+.waiting {
+  background: #89beb2;
+}
+/* 部分成功 */
+.bf-suc {
+  background: #c6f9ae;
+}
+</style>

TEMPAT SAMPAH
src/view/step-tree-v2/example/image/c.png


TEMPAT SAMPAH
src/view/step-tree-v2/example/image/g.png


TEMPAT SAMPAH
src/view/step-tree-v2/example/image/p.png


TEMPAT SAMPAH
src/view/step-tree-v2/example/image/x.png


+ 126 - 0
src/view/step-tree-v2/example/step.vue

@@ -0,0 +1,126 @@
+<template>
+  <span
+    class="title structure-title"
+    v-if="item.structure && start"
+    :style="{ left: -start.x + 'px', top: '-20px' }"
+    >{{ item.displayName }}</span
+  >
+  <div
+    class="step-layout"
+    @click="emit('click')"
+    :style="{ '--statusColor': currentColor }"
+  >
+    <div class="step-header">
+      <span class="type" v-if="!item.structure || item.structure === 'left'">{{
+        item.name
+      }}</span>
+      <span class="title" v-if="!item.structure || item.structure === 'right'">{{
+        item.displayName
+      }}</span>
+      <span class="icons" v-if="!item.structure || item.structure === 'left'">
+        <img src="./image/g.png" v-if="props.item.status === 'success'" />
+        <img src="./image/x.png" v-else-if="props.item.status === 'error'" />
+
+        <template v-if="'serviceTypeParallel' in item">
+          <img src="./image/p.png" v-if="item.serviceTypeParallel" />
+          <img src="./image/c.png" v-else />
+        </template>
+      </span>
+    </div>
+
+    <div class="step-hosts" v-if="item.hosts?.length">
+      <span v-for="host in item.hosts" @click.stop="$emit('clickHost', host)">
+        {{ host.host }}
+      </span>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { DataStep } from "../type";
+import { computed } from "vue";
+
+const props = defineProps<{ item: any; start: any }>();
+const emit = defineEmits<{
+  (e: "click"): void;
+  (e: "clickHost", host: DataStep["hosts"][number]): void;
+}>();
+
+const defaultColor = "#d4f8c3";
+const colorMap = {
+  success: "#30d567",
+  error: "#ff4238",
+  running: "#ecf752",
+  waiting: "#89beb2",
+};
+const currentColor = computed(() =>
+  props.item.status in colorMap ? colorMap[props.item.status] : defaultColor
+);
+</script>
+
+<style scoped lang="scss">
+.step-layout {
+  border-radius: 8px;
+  color: rgb(51, 51, 51);
+  font-family: "微软雅黑";
+  background-color: var(--pColor);
+  max-width: 400px;
+  min-width: 100px;
+  text-align: center;
+  overflow: hidden;
+  cursor: pointer;
+}
+.structure-title {
+  position: absolute;
+  width: 300px;
+  text-align: left;
+  pointer-events: none;
+  left: 0;
+}
+
+.step-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  font-size: 14px;
+  padding: 8px 10px;
+  background: #8ebeb2;
+
+  .type {
+    font-weight: bold;
+    text-align: center;
+  }
+
+  .title {
+    margin: 0 20px;
+  }
+
+  .icons {
+    img {
+      width: 20px;
+      height: 20px;
+      :not(:last-child) {
+        margin-right: 5px;
+      }
+    }
+  }
+}
+
+.step-hosts {
+  padding: 5px;
+  display: flex;
+  justify-content: center;
+  background: #fff;
+  font-size: 12px;
+  flex-wrap: wrap;
+
+  span {
+    padding: 6px 10px;
+    border: 2px solid var(--statusColor);
+    color: var(--statusColor);
+    border-radius: 6px;
+    cursor: pointer;
+    margin: 5px;
+  }
+}
+</style>

+ 264 - 0
src/view/step-tree-v2/helper-v2.ts

@@ -0,0 +1,264 @@
+import { getStepLine, Step, Steps } from "./tree-helper";
+import {
+  getStepsTreeCtx as getStepsTreeCtxRaw,
+  StepsCtx as StepsCtxRaw,
+} from "./tree-helper";
+
+type DataStep = {
+  serviceTypeParallel?: boolean | "True" | "Flase";
+  subStepsParallel?: boolean | "True" | "Flase";
+  structure?: string;
+};
+export type DataStepTree<T extends DataStep = DataStep> = T & {
+  steps: DataStepTree<T>[];
+};
+
+export type NStep<T extends DataStepTree> = Step<T>;
+
+const _flatSteps = <T extends DataStep>(
+  steps: DataStepTree<T>[],
+  nsteps: NStep<DataStepTree<T>>[] = [],
+  parents: NStep<DataStepTree<T>>[] = [],
+  parallel = false
+) => {
+  const lonelySteps: NStep<DataStepTree<T>>[] = [];
+  let tempParents = parents;
+
+  for (const step of steps) {
+    const stepParallel = parallel;
+    if (!stepParallel && lonelySteps.length) {
+      tempParents = [...lonelySteps];
+      lonelySteps.length = 0;
+    }
+    const nstep = {
+      raw: step,
+      children: [],
+      parents: tempParents,
+    } as NStep<DataStepTree<T>>;
+    nsteps.push(nstep);
+    tempParents.forEach((parent) => parent.children.push(nstep));
+
+    if (step.steps && step.steps.length) {
+      step.serviceTypeParallel;
+      lonelySteps.push(
+        ..._flatSteps(
+          step.steps,
+          nsteps,
+          [nstep],
+          step.subStepsParallel === "True" ||
+            step.serviceTypeParallel === "True" ||
+            !!step.subStepsParallel ||
+            !!step.serviceTypeParallel
+        )
+      );
+    } else {
+      lonelySteps.push(nstep);
+    }
+  }
+
+  return lonelySteps;
+};
+
+const start: any = { displayName: "开始", type: "startEnd" };
+const end: any = { displayName: "结束", type: "startEnd" };
+export const flatSteps = <T extends DataStep>(steps: DataStepTree<T>[]) => {
+  const nsteps: NStep<DataStepTree<T>>[] = [];
+  for (let i = 0; i < steps.length; i++) {
+    // steps[i].steps = [
+    //   {
+    //     ...steps[i],
+    //     subStepsParallel: false,
+    //     structure: "right",
+    //   },
+    // ];
+    steps[i].structure = "left";
+  }
+
+  _flatSteps([start, ...steps, end] as any, nsteps);
+  return {
+    steps: nsteps,
+    group: steps,
+  };
+};
+
+// 获取step所有子级
+const getStepFlatSteps = <T extends DataStep>(step: DataStepTree<T>) => {
+  if (!step.steps || step.steps.length === 0) return [];
+
+  const children: DataStepTree<T>[] = [];
+  for (const child of step.steps) {
+    children.push(child);
+    children.push(...getStepFlatSteps(child));
+  }
+
+  return children;
+};
+
+// 获取steps组成的bound
+const getStepsBound = <T>(nsteps: NStep<DataStepTree<T>>[]) => {
+  const bounds = nsteps.map((nstep) => {
+    return { ...nstep.box.offset, ...nstep.box.size };
+  });
+
+  let maxX = -Number.MAX_VALUE,
+    maxY = -Number.MAX_VALUE,
+    minX = Number.MAX_VALUE,
+    minY = Number.MAX_VALUE;
+  for (const bound of bounds) {
+    minX = Math.min(bound.x, minX);
+    minY = Math.min(bound.y, minY);
+    maxX = Math.max(bound.x + bound.w, maxX);
+    maxY = Math.max(bound.y + bound.h, maxY);
+  }
+  return {
+    x: minX,
+    y: minY,
+    w: maxX - minX,
+    h: maxY - minY,
+  };
+};
+
+export type StepsCtx<T> = StepsCtxRaw<T> & {
+  groupBoxs: {
+    step: Step<T>;
+    bound: { w: number; h: number; x: number; y: number };
+    line: number[][];
+  }[];
+};
+
+const setGroupBack = <T>(
+  steps: NStep<DataStepTree<T>>[],
+  ctx: StepsCtx<DataStepTree<T>>,
+  groups: NStep<DataStepTree<T>>[]
+) => {
+  const maxWidth = Math.max(...groups.map(({ box }) => box.size.w));
+  for (const groupStep of groups) {
+    const children = getStepFlatSteps(groupStep.raw);
+    const childSteps = children.map((raw) =>
+      steps.find((step) => raw === step.raw)
+    );
+    ctx.groupBoxs.push({
+      step: groupStep,
+      line: [],
+      bound: getStepsBound(childSteps),
+    });
+    groupStep.box.offset.x = -maxWidth + (maxWidth - groupStep.box.size.w) / 2;
+  }
+  ctx.offset.x = -maxWidth;
+  ctx.size.w += maxWidth;
+};
+
+const levelTraversalSteps = <T>(
+  steps: Steps<T>,
+  oper: (steps: Step<T>[]) => void,
+  reverse = false,
+  level = 0
+) => {
+  const cSteps = steps.filter((item) => item.box.level === level);
+  if (cSteps.length === 0) return;
+  reverse || oper(cSteps);
+  levelTraversalSteps(steps, oper, reverse, level + 1);
+  reverse && oper(cSteps);
+};
+
+const setGroupOffset = <T>(
+  steps: NStep<DataStepTree<T>>[],
+  ctx: StepsCtx<DataStepTree<T>>,
+  groupSteps: NStep<DataStepTree<T>>[],
+  margin: number
+) => {
+  // margin = 0;
+  const offsetYs: number[] = [];
+  const offsetLYs: number[] = [];
+  for (let i = 0; i < groupSteps.length; i++) {
+    const groupStep = groupSteps[i];
+    if (start === groupStep.raw) {
+      offsetYs[i] = 0;
+      offsetLYs[i] = 0;
+      // offsetLYs[i] = -groupStep.box.size.h + margin;
+    } else if (end === groupStep.raw) {
+      offsetYs[i] = offsetYs[i - 1];
+      offsetLYs[i] = offsetLYs[i - 1];
+    } else if (i > 0) {
+      offsetYs[i] = -groupStep.box.size.h + offsetYs[i - 1] + margin;
+      offsetLYs[i] = offsetLYs[i - 1] - groupStep.box.size.h + margin;
+    } else {
+      offsetYs[i] = -groupStep.box.size.h + margin;
+      offsetLYs[i] = -groupStep.box.size.h + margin;
+    }
+
+    groupStep.box.offset.y += margin;
+  }
+
+  let offsetNdx = offsetYs.length - 1;
+  let offsetLNdx = offsetYs.length - 1;
+  let prevG = null;
+
+  levelTraversalSteps(
+    steps,
+    (currents) => {
+      const isBorder = currents.some((current) => groupSteps.includes(current));
+      if (isBorder) {
+        offsetNdx -= 1;
+      }
+
+      for (const current of currents) {
+        if (end === prevG || offsetNdx <= 0) {
+          current.box.lines = [];
+        }
+        if (offsetNdx === -1) {
+          break;
+        }
+        current.box.offset.y += offsetYs[offsetNdx];
+        for (const points of current.box.lines) {
+          for (const point of points) {
+            point[1] = point[1] + offsetLYs[offsetLNdx];
+          }
+        }
+      }
+      prevG = currents[0].raw;
+      if (isBorder) {
+        offsetLNdx -= 1;
+      }
+    },
+    true
+  );
+
+  ctx.size.h += offsetYs[offsetYs.length - 1];
+};
+
+export const setGroupLine = <T>(
+  ctx: StepsCtx<DataStepTree<T>>,
+  groupSteps: NStep<DataStepTree<T>>[],
+  margin: number[]
+) => {
+  for (let i = 0; i < groupSteps.length - 1; i++) {
+    ctx.groupBoxs[i].line = getStepLine(
+      ctx,
+      groupSteps[i],
+      groupSteps[i + 1],
+      margin
+    );
+  }
+};
+
+export const getStepsTreeCtx = <T extends DataStep>(
+  steps: NStep<DataStepTree<T>>[],
+  margin: number[],
+  getStepSize: (step: T) => { w: number; h: number },
+  groups: DataStepTree<T>[]
+) => {
+  const ctx = getStepsTreeCtxRaw(steps, margin, getStepSize) as StepsCtx<
+    DataStepTree<T>
+  >;
+  console.log(steps, groups);
+  groups = [start, ...groups, end];
+  const groupSteps = groups.map((group) =>
+    steps.find((step) => group === step.raw)
+  );
+  ctx.groupBoxs = [];
+  setGroupOffset(steps, ctx, groupSteps, margin[0]);
+  setGroupBack(steps, ctx, groupSteps);
+  setGroupLine(ctx, groupSteps, margin);
+  return ctx;
+};

+ 355 - 0
src/view/step-tree-v2/tree-helper.ts

@@ -0,0 +1,355 @@
+export type Step<T> = {
+  raw: T;
+  parents: Step<T>[];
+  children: Step<T>[];
+  box: StepCtx<T>;
+};
+export type Steps<T> = Step<T>[];
+
+export type StepCtx<T> = {
+  level: number;
+  step: Step<T>;
+  // 自身大小
+  size: { w: number; h: number };
+  // 如果是多对一则存储多宽度
+  parallelWidth?: number;
+  // 树宽高
+  treeSize?: { w: number; h: number };
+  refTrees?: Step<T>[];
+  // 相对于兄弟
+  offset: { x: number; y: number };
+  treeOffset: { x: number; y: number };
+  lines: number[][][];
+};
+
+export type StepsCtx<T> = {
+  levelHeight: number[];
+  boxs: StepCtx<T>[];
+  roots: Step<T>[];
+  offset: { x: number; y: number };
+  size: { w: number; h: number };
+};
+
+type traversalStepsProps<T> = {
+  steps: Steps<T>;
+  ctx: StepsCtx<T>;
+  oper: (data: {
+    currents: Step<T>[];
+    prev: Step<T> | null;
+    next: Step<T> | null;
+    levelSteps: Step<T>[];
+  }) => void;
+  cs?: Step<T>[];
+  checkeds?: Steps<T>;
+  level?: number;
+  reverse?: boolean;
+};
+
+const setStepsLevel = <T>(
+  steps: Steps<T>,
+  ctx: StepsCtx<T>,
+  level = 0,
+  cs: Step<T>[] = ctx.roots
+) => {
+  if (cs && cs.length === 0) return;
+  const cSteps = cs;
+
+  for (let i = 0; i < cSteps.length; i++) {
+    const current = cSteps[i];
+    if ("level" in current.box) {
+      current.box.level = Math.max(level, current.box.level);
+    } else {
+      (current.box as any).level = level;
+    }
+    ctx.levelHeight[level] = 0;
+    setStepsLevel(steps, ctx, level + 1, current.children);
+  }
+};
+
+export const traversalSteps = <T>({
+  steps,
+  ctx,
+  oper,
+  cs = ctx.roots,
+  level = 0,
+  reverse = false,
+  checkeds = [],
+}: traversalStepsProps<T>) => {
+  if (cs.length === 0) return;
+  const cSteps = cs;
+
+  for (let i = 0; i < cSteps.length; i++) {
+    const current = cSteps[i];
+    if (checkeds.includes(current)) continue;
+    const children = current.children.filter(
+      (child) => child.box.level === level + 1
+    );
+
+    // 查看是否是多对一的情况,如果是这current为所有多的step
+    let currents =
+      children.length === 1 && children[0].parents.length > 1
+        ? children[0].parents
+        : [current];
+    currents = currents.filter((c) => c.box.level === level);
+    checkeds.push(...currents);
+
+    const props = {
+      currents: currents,
+      prev: cSteps[i - 1] || null,
+      next: cSteps[i + 1] || null,
+      level: level,
+      levelSteps: cSteps,
+    };
+
+    if (currents.length > 0) {
+      reverse || oper(props);
+    }
+    traversalSteps({
+      steps,
+      ctx,
+      oper,
+      cs: current.children,
+      level: level + 1,
+      reverse,
+      checkeds,
+    });
+
+    if (currents.length > 0) {
+      reverse && oper(props);
+    }
+  }
+};
+
+const setStepsBound = <T>(
+  steps: Steps<T>,
+  ctx: StepsCtx<T>,
+  getStepSize: (step: T) => { w: number; h: number }
+) => {
+  // 注入levelHeights, box size offset
+  traversalSteps({
+    steps,
+    ctx,
+    oper: ({ currents }) => {
+      currents.forEach((current) => {
+        const size = getStepSize(current.raw);
+        current.box.size = size;
+        ctx.levelHeight[current.box.level] = Math.max(
+          size.h,
+          ctx.levelHeight[current.box.level] || 0
+        );
+      });
+    },
+  });
+};
+
+const setStepsTreeSize = <T>(steps: Steps<T>, ctx: StepsCtx<T>) => {
+  // 从低到顶分别计算树大小
+  traversalSteps({
+    steps,
+    ctx,
+    oper: ({ currents }) => {
+      let levelHeight = 0;
+      const level = currents[0].box.level;
+      for (let i = 0; i <= level; i++) {
+        levelHeight += ctx.levelHeight[i];
+      }
+      const current = currents[0];
+      const treeSize = { w: 0, h: 0 };
+      const refTrees: Step<T>[] = [];
+      for (const child of current.children) {
+        if (child.box.treeSize) {
+          treeSize.w += child.box.treeSize.w;
+          treeSize.h = Math.max(child.box.treeSize.w, treeSize.w);
+        }
+        if (child.box.refTrees.length) {
+          refTrees.push(...child.box.refTrees);
+        } else {
+          refTrees.push(child);
+        }
+      }
+      treeSize.h += levelHeight;
+
+      if (currents.length === 1) {
+        // 一对一  一对多情况
+        if (current.box.size.w >= treeSize.w) {
+          treeSize.w = current.box.size.w;
+        } else {
+          current.box.refTrees = refTrees;
+        }
+        current.box.treeSize = treeSize;
+      } else {
+        // 多对一情况
+        let parallelWidth = 0;
+        for (const parallel of currents) {
+          parallelWidth += parallel.box.size.w;
+        }
+        if (parallelWidth >= treeSize.w) {
+          treeSize.w = parallelWidth;
+        } else {
+          currents[0].box.refTrees = refTrees;
+        }
+        currents[0].box.parallelWidth = parallelWidth;
+        currents[0].box.treeSize = treeSize;
+      }
+      // console.log(currents[0].raw.name, treeSize);
+    },
+    reverse: true,
+  });
+};
+
+const setStepsOffset = <T>(steps: Steps<T>, ctx: StepsCtx<T>) => {
+  // 从顶到底分别计算树偏移量以及step偏移
+  traversalSteps({
+    steps,
+    ctx,
+    oper: ({ currents, prev }) => {
+      const box = currents[0].box;
+      let levelHeight = 0;
+      const level = currents[0].box.level;
+      for (let i = 0; i < level; i++) {
+        levelHeight += ctx.levelHeight[i];
+      }
+
+      const treeOffset = { x: 0, y: levelHeight };
+
+      if (prev) {
+        // 上一个是普通树
+        if (prev.box.treeOffset) {
+          treeOffset.x += prev.box.treeOffset.x + prev.box.treeSize.w;
+        } else {
+          // 上一个是多对一树,需要找到box属性值
+          let prevTreeBox: StepCtx<T>;
+          for (const prevParent of prev.parents) {
+            for (let prevLevel of prevParent.children) {
+              if (prevLevel.box.treeOffset) {
+                prevTreeBox = prevLevel.box;
+                break;
+              }
+            }
+            if (prevTreeBox) break;
+          }
+
+          treeOffset.x += prevTreeBox.treeOffset.x + prevTreeBox.parallelWidth;
+        }
+      } else if (currents[0].parents.length) {
+        // 如果是第一个,则需要计算子级,相对父级做偏移
+        const parents = currents[0].parents;
+        const levels = new Set(parents.flatMap((parent) => parent.children));
+        let levelWidth = 0;
+        for (const levelStep of levels) {
+          if ("parallelWidth" in levelStep.box) {
+            levelWidth += levelStep.box.parallelWidth;
+          } else if (levelStep.box.treeSize) {
+            levelWidth += levelStep.box.treeSize.w || 0;
+          }
+        }
+
+        let parentWidth = 0;
+        for (const parent of parents) {
+          parentWidth +=
+            "parallelWidth" in parent.box
+              ? parent.box.parallelWidth
+              : "treeSize" in parent.box
+              ? parent.box.treeSize.w
+              : 0;
+        }
+
+        treeOffset.x +=
+          parents[0].box.treeOffset.x + (parentWidth - levelWidth) / 2;
+      }
+
+      box.treeOffset = { ...treeOffset };
+      if ("parallelWidth" in box) {
+        let lOffset = 0;
+        for (const current of currents) {
+          current.box.offset = {
+            x: treeOffset.x + lOffset,
+            y: treeOffset.y + (ctx.levelHeight[level] - current.box.size.h) / 2,
+          };
+          lOffset += current.box.size.w;
+        }
+      } else {
+        treeOffset.x += (box.treeSize.w - box.size.w) / 2;
+        treeOffset.y += (ctx.levelHeight[level] - box.size.h) / 2;
+
+        box.offset = treeOffset;
+      }
+    },
+  });
+};
+
+const setStepsLines = <T>(
+  steps: Steps<T>,
+  ctx: StepsCtx<T>,
+  margin: number[]
+) => {
+  traversalSteps({
+    steps,
+    ctx,
+    oper: ({ currents }) => {
+      for (const current of currents) {
+        current.box.lines = current.children.map((child) =>
+          getStepLine(ctx, current, child, margin)
+        );
+      }
+    },
+  });
+};
+
+export const getStepLine = <T>(
+  ctx: StepsCtx<T>,
+  topStep: Step<T>,
+  bottomStep: Step<T>,
+  margin: number[] = [0, 0]
+) => {
+  const top = topStep.box;
+  const bottom = bottomStep.box;
+  const bottomHeight = ctx.levelHeight[bottom.level];
+  const start = [
+    top.offset.x + top.size.w / 2,
+    top.offset.y + top.size.h - margin[0],
+  ];
+
+  const diffHeight = (bottomHeight - bottom.size.h) / 2;
+
+  const c1 = [top.offset.x + top.size.w / 2, bottom.offset.y - diffHeight];
+  const c2 = [
+    bottom.offset.x + bottom.size.w / 2,
+    bottom.offset.y - diffHeight,
+  ];
+  const end = [
+    bottom.offset.x + bottom.size.w / 2,
+    bottom.offset.y + margin[0],
+  ];
+  return [start, c1, c2, end];
+};
+
+export const getStepsTreeCtx = <T>(
+  steps: Steps<T>,
+  margin: number[],
+  getStepSize: (step: T) => { w: number; h: number }
+) => {
+  const roots = steps.filter((step) => step.parents.length === 0);
+  const ctx: StepsCtx<T> = {
+    size: { w: 0, h: 0 },
+    offset: { x: 0, y: 0 },
+    levelHeight: [],
+    boxs: steps.map((step) => {
+      const box = { step } as any;
+      step.box = box;
+      step.box.lines = [];
+      step.box.refTrees = [];
+      return box;
+    }),
+    roots: roots,
+  };
+  setStepsLevel(steps, ctx);
+  setStepsBound(steps, ctx, getStepSize);
+  setStepsTreeSize(steps, ctx);
+  setStepsOffset(steps, ctx);
+  setStepsLines(steps, ctx, margin);
+
+  ctx.size.w = roots.reduce((t, root) => t + root.box.treeSize.w, 0);
+  ctx.size.h = ctx.levelHeight.reduce((t, h) => t + h, 0);
+  return ctx;
+};

+ 11 - 0
src/view/step-tree-v2/type.ts

@@ -0,0 +1,11 @@
+export type DataStep = {
+  action?: string;
+  displayName: string;
+  hosts?: { host: string; status: string }[];
+  name: string;
+  serviceType?: string;
+  serviceTypeParallel?: boolean;
+  status?: string;
+  type?: string;
+  steps: DataStep[];
+};

+ 205 - 0
src/view/step-tree/StepTree.vue

@@ -0,0 +1,205 @@
+<template>
+  <svg
+    :viewBox="svgAttrib.viewBox.join(' ')"
+    v-if="svgAttrib"
+    xmlns="http://www.w3.org/2000/svg"
+    :style="{ width: svgAttrib.viewBox[2] + 'px', height: svgAttrib.viewBox[3] + 'px' }"
+  >
+    <Step
+      v-for="step in steps"
+      :key="step.id"
+      v-bind="getStepAttrib(step)"
+      :treeBoxMargin="treeBoxMargin"
+      @click="emit('stepClick', step)"
+      @click-host="(data) => emit('stepHostClick', data)"
+    />
+  </svg>
+</template>
+
+<script lang="ts" setup>
+import { computed } from "vue";
+import {
+  flatSteps,
+  attachBoundAttrib,
+  NStep,
+  getTextBound,
+  attachStepTreesBoundAttrib,
+} from "./helper";
+import Step from "./step.vue";
+
+const props = withDefaults(
+  defineProps<{
+    margin?: number[];
+    padding?: number[];
+    fontSize?: number;
+    treeBoxMargin: number[];
+    data: any;
+    fontFamily?: string;
+    hostFontSize?: number;
+    hostMargin?: number[];
+    hostPadding?: number[];
+    customStepStyle?: (step: any) => {};
+    lineGap: number;
+  }>(),
+  {
+    margin: () => [10, 10],
+    padding: () => [10, 10],
+    hostMargin: () => [2, 2],
+    hostPadding: () => [2, 2],
+    hostFontSize: 10,
+    lineGap: 5,
+    fontSize: 14,
+    fontFamily: "sans-serif",
+  }
+);
+
+const emit = defineEmits<{
+  (e: "stepClick", data: any): void;
+  (e: "stepHostClick", host: any): void;
+}>();
+
+const getStepSize = (step: any) => {
+  const size = getTextBound(
+    step.displayName,
+    props.padding,
+    props.margin,
+    `${props.fontSize}px normal ${props.fontFamily}`
+  );
+
+  if (step.hosts?.length) {
+    const hostsGroup = [];
+    const numGroup = 2;
+    for (let i = 0; i < step.hosts.length; i += numGroup) {
+      hostsGroup.push(step.hosts.slice(i, i + numGroup));
+    }
+    let top = 0;
+    const hostSizeGroup = hostsGroup.map((hosts) => {
+      let left = 0;
+      const hostSize = hosts.reduce(
+        (t: any, host: any) => {
+          const size = getTextBound(
+            host.host,
+            props.hostPadding,
+            props.hostMargin,
+            `${props.hostFontSize}px normal ${props.fontFamily}`
+          );
+          t.width += size.width;
+          t.height = Math.max(t.height, size.height);
+          host.bound = {
+            ...size,
+            left,
+            top,
+          };
+          left += size.width;
+          return t;
+        },
+        { width: 0, height: 0 }
+      );
+      top += hostSize.height;
+      return hostSize;
+    });
+
+    const hostSize = hostSizeGroup.reduceRight(
+      (t, hostSize) => {
+        t.width = Math.max(hostSize.width, t.width);
+        t.height += hostSize.height;
+        return t;
+      },
+      { width: 0, height: 0 }
+    );
+    step.hostSize = hostSize;
+    size.width = Math.max(
+      size.width,
+      hostSize.width + (props.padding[1] + props.margin[1]) * 2
+    );
+    size.height += hostSize.height;
+  }
+
+  return size;
+};
+
+const steps = computed(() => {
+  const steps = flatSteps(props.data);
+  return steps;
+});
+const bound = computed(() => {
+  const pageBound = attachBoundAttrib(steps.value, getStepSize);
+  attachStepTreesBoundAttrib(steps.value, props.data);
+  console.log(steps.value);
+  return {
+    ...pageBound,
+    left: pageBound.left - 10,
+    right: pageBound.right + 10,
+    top: pageBound.top - 10,
+    bottom: pageBound.bottom + 10,
+  };
+});
+
+const svgAttrib = computed(() => {
+  if (!bound.value) return null;
+  const { left, right, top, bottom } = bound.value;
+  return {
+    viewBox: [left, top, right - left, bottom - top],
+  };
+});
+
+const lineGap = computed(() => Math.min(props.lineGap, props.margin[0]));
+
+const getStepLines = (step: NStep) => {
+  if (!step.parentIds.length) return [];
+  const start = [
+    step.bound.left + step.bound.width / 2,
+    step.bound.top + props.margin[0],
+  ];
+  const points = [];
+  for (let parentId of step.parentIds) {
+    const parent = steps.value.find((step) => step.id === parentId)!;
+    const end = [
+      parent.bound.left + parent.bound.width / 2,
+      parent.bound.top + parent.bound.height - props.margin[0],
+    ];
+    const startLevelHeight = bound.value.levelHeights[step.level];
+    // const parentLevelHeight = bound.value.levelHeights[parent.level];
+    const offset = lineGap.value + (startLevelHeight - step.bound.height) / 2;
+
+    points.push([
+      ...start,
+      start[0],
+      start[1] - offset,
+      end[0],
+      start[1] - offset,
+      ...end,
+    ]);
+  }
+  return points;
+};
+
+const defaultStyle = {
+  lineColor: "#000",
+  lineWidth: 1,
+  textColor: "#000",
+  rectBorderColor: "#000",
+  rectBgColor: "#ffff",
+  rectRadius: 2,
+  rectBorderWidth: 1,
+};
+
+const getStepAttrib = (step: NStep) => {
+  let style = defaultStyle;
+  if (props.customStepStyle) {
+    style = { ...defaultStyle, ...props.customStepStyle(step.raw) };
+  }
+  return {
+    style,
+    step,
+    margin: props.margin,
+    padding: props.padding,
+    fontSize: props.fontSize,
+    hostMargin: props.hostMargin,
+    hostPadding: props.hostPadding,
+    hostFontSize: props.hostFontSize,
+    fontFamily: props.fontFamily,
+    lines: getStepLines(step),
+  };
+};
+</script>

+ 120 - 163
src/view/step-tree/example/data.ts

@@ -1,236 +1,193 @@
 export default [
   {
-    yamlversion: "v1.0",
-    subStepsParallel: "False", // false 串行,True并行,表示子步骤step1、step2串行执行
+    end_time: "2024_07_09_10:47:34",
+    start_time: "20240709104623",
+    status: "success",
     steps: [
       {
-        name: "step1",
         displayName: "Stop All Services",
-        subStepsParallel: "True",
+        name: "step1",
         status: "success",
         steps: [
           {
-            name: "step1_1",
-            displayName: "Stop app1 Services",
-            type: "execution",
-            serviceType: "app1",
-            status: "stop",
-            serviceTypeParallel: "True",
-
-            steps: [
+            action: "stop",
+            displayName: "Stop app1_part1 Services",
+            hosts: [
               {
-                name: "step1_1_1",
-                displayName: "Stop app1 Services",
-                type: "execution",
-                serviceType: "app1",
-                status: "waiting",
-                serviceTypeParallel: "False",
-                steps: [
-                  {
-                    name: "step1_1_1_1",
-                    displayName: "Stop app1 Services",
-                    type: "execution",
-                    serviceType: "app1",
-                    status: "waiting",
-                    serviceTypeParallel: "False",
-                    hosts: [
-                      { host: "qladpaxasdasdasd1", status: "success" },
-                      { host: "qladpax1", status: "success" },
-                      { host: "qladpax1", status: "success" },
-                      { host: "qladpax1", status: "success" },
-                      { host: "qladpax1", status: "success" },
-                      { host: "qladpax1", status: "success" },
-                      { host: "qladpax2", status: "lose" },
-                      { host: "qladpax3", status: "wating" },
-                    ],
-                  },
-                  {
-                    name: "step1_1_1_2",
-                    displayName: "Stop app1 Services",
-                    type: "execution",
-                    serviceType: "app1",
-                    status: "waiting",
-                    serviceTypeParallel: "False",
-                    hosts: [
-                      { host: "qladpax1", status: "success" },
-                      { host: "qladpax2", status: "lose" },
-                      { host: "qladpax3", status: "wating" },
-                    ],
-                  },
-                ],
-              },
-              {
-                name: "step1_1_2",
-                displayName: "Stop app1 Services",
-                type: "execution",
-                serviceType: "app1",
-                status: "waiting",
-                serviceTypeParallel: "True",
-                hosts: [
-                  { host: "qladpax1", status: "success" },
-                  { host: "qladpax2", status: "lose" },
-                  { host: "qladpax3", status: "wating" },
-                ],
-              },
-              {
-                name: "step1_1_3",
-                displayName: "Stop app1 Services",
-                type: "execution",
-                serviceType: "app1",
-                status: "waiting",
-                serviceTypeParallel: "True",
-              },
-              {
-                name: "step1_1_4",
-                displayName: "Stop app1 Services",
-                type: "execution",
-                serviceType: "app1",
-                status: "waiting",
-                serviceTypeParallel: "True",
+                host: "quadpax1",
+                status: "success",
               },
             ],
+            name: "step1_1",
+            serviceType: "app1_part1",
+            serviceTypeParallel: true,
+            status: "success",
+            type: "execution",
           },
           {
+            displayName: "Stop app2 Services",
             name: "step1_2",
-            displayName: "Stop app2_part1 Services",
-            type: "execution",
-            serviceType: "app2_part1",
-            status: "waiting",
-            serviceTypeParallel: "True",
+            status: "success",
             steps: [
               {
+                action: "stop",
+                displayName: "Stop app2_part1 Services",
+                hosts: [
+                  {
+                    host: "quadpax2",
+                    status: "success",
+                  },
+                ],
                 name: "step1_2_1",
-                displayName: "Stop app1 Services",
+                serviceType: "app2_part1",
+                serviceTypeParallel: true,
+                status: "success",
                 type: "execution",
-                serviceType: "app1",
-                status: "waiting",
-                serviceTypeParallel: "True",
               },
               {
+                displayName: "Stop app2_part2 Services",
                 name: "step1_2_2",
-                displayName: "Stop app1 Services",
-                type: "execution",
-                serviceType: "app1",
-                status: "waiting",
-                serviceTypeParallel: "True",
+                status: "success",
                 steps: [
                   {
-                    name: "step1_2aaaa",
-                    displayName: "Stop app2_part1 Services",
+                    action: "stop",
+                    displayName: "Stop app2_part2 Services",
+                    hosts: [
+                      {
+                        host: "quadpax3",
+                        status: "success",
+                      },
+                      {
+                        host: "qladpax3",
+                        status: "success",
+                      },
+                    ],
+                    name: "step1_2_2_1",
+                    serviceType: "app2_part2",
+                    serviceTypeParallel: true,
+                    status: "success",
                     type: "execution",
-                    serviceType: "app2_part1",
-                    status: "waiting",
-                    serviceTypeParallel: "True",
-                    steps: [
+                  },
+                  {
+                    action: "stop",
+                    displayName: "Stop app2_part3 Services",
+                    hosts: [
                       {
-                        name: "step1_2_1a",
-                        displayName: "Stop app1 Services",
-                        type: "execution",
-                        serviceType: "app1",
-                        status: "waiting",
-                        serviceTypeParallel: "True",
+                        host: "quadpax4",
+                        status: "success",
                       },
                       {
-                        name: "step1ccc",
-                        displayName: "Stop app1 Services",
-                        type: "execution",
-                        serviceType: "app1",
-                        status: "waiting",
-                        serviceTypeParallel: "True",
+                        host: "qladpax4",
+                        status: "success",
                       },
                     ],
+                    name: "step1_2_2_2",
+                    serviceType: "app2_part3",
+                    serviceTypeParallel: true,
+                    status: "success",
+                    type: "execution",
                   },
                 ],
+                subStepsParallel: true,
               },
             ],
+            subStepsParallel: true,
           },
         ],
+        subStepsParallel: false,
       },
       {
-        // name: "step2step2step2step2step2step2step2step2step2stestep2step2step2step2step2step2step2step2step2stestep2step2step2step2step2step2step2step2step2stestep2step2step2step2step2step2step2step2step2ste",
+        displayName: "step2",
         name: "step2",
-        displayName: "Stop All Services",
-        subStepsParallel: "True",
-        status: "waiting",
+        status: "success",
         steps: [
           {
-            name: "step2_1",
-            displayName: "Waiting",
-            type: "execution",
-            status: "waiting",
-          },
-          {
-            name: "step2_2",
+            action: "humanWaiting",
             displayName: "Waiting",
+            name: "step2_1",
+            status: "success",
             type: "execution",
-            status: "waiting",
           },
         ],
       },
       {
-        name: "step3",
         displayName: "deploy All Services",
-        subStepsParallel: "True",
-        status: "waiting",
+        name: "step3",
+        status: "success",
         steps: [
           {
-            name: "step3_1ssstep3",
-            displayName: "deploy app1 Services",
-            type: "execution",
-            serviceType: "app1",
-            status: "waiting",
-            serviceTypeParallel: "True",
-          },
-          {
-            name: "step3_1stes",
-            displayName: "deploy app1 Services",
-            type: "execution",
-            serviceType: "app1",
-            status: "waiting",
-            serviceTypeParallel: "True",
-          },
-          {
-            name: "step3_1s",
-            displayName: "deploy app1 Services",
+            action: "deploy",
+            displayName: "deploy app1_part1 Services",
+            hosts: [
+              {
+                host: "quadpax1",
+                status: "success",
+              },
+            ],
+            name: "step3_1",
+            serviceType: "app1_part1",
+            serviceTypeParallel: true,
+            status: "success",
             type: "execution",
-            serviceType: "app1",
-            status: "waiting",
-            serviceTypeParallel: "True",
           },
           {
-            name: "step3_2",
+            action: "deploy",
             displayName: "deploy app2_part1 Services",
-            type: "execution",
+            hosts: [
+              {
+                host: "quadpax2",
+                status: "success",
+              },
+            ],
+            name: "step3_2",
             serviceType: "app2_part1",
-            status: "waiting",
-            serviceTypeParallel: "True",
+            serviceTypeParallel: true,
+            status: "success",
+            type: "execution",
           },
         ],
+        subStepsParallel: true,
       },
       {
-        name: "step4",
         displayName: "start All Services",
-        subStepsParallel: "False",
-        status: "waiting",
+        name: "step4",
+        status: "success",
         steps: [
           {
+            action: "start",
+            displayName: "start app1_part1 Services",
+            hosts: [
+              {
+                host: "quadpax1",
+                status: "success",
+              },
+            ],
             name: "step4_1",
-            displayName: "start app1 Services",
+            serviceType: "app1_part1",
+            serviceTypeParallel: true,
+            status: "success",
             type: "execution",
-            serviceType: "app1",
-            status: "waiting",
-            serviceTypeParallel: "True",
           },
           {
-            name: "step4_2",
+            action: "start",
             displayName: "start app2_part1 Services",
-            type: "execution",
+            hosts: [
+              {
+                host: "quadpax2",
+                status: "success",
+              },
+            ],
+            name: "step4_2",
             serviceType: "app2_part1",
-            status: "waiting",
-            serviceTypeParallel: "True",
+            serviceTypeParallel: true,
+            status: "success",
+            type: "execution",
           },
         ],
+        subStepsParallel: false,
       },
     ],
+    subStepsParallel: false,
+    yamlversion: "v1.0",
   },
 ];

+ 156 - 19
src/view/step-tree/example/example.vue

@@ -1,13 +1,22 @@
 <template>
-  <div class="test">
+  <div class="status-box flex">
+    <!-- <span class="defualt">运行中</span> -->
+    <span class="waiting">等待中</span>
+    <span class="running">运行中</span>
+    <span class="succ">成功</span>
+    <span class="bf-suc">部分成功</span>
+    <span class="error">失败</span>
+  </div>
+  <div class="tree-cont-wrap" ref="treeWrapRef">
     <StepTree
       :data="treeData"
       :margin="[15, 15]"
       :padding="[10, 10]"
+      :tree-box-margin="[5, 5]"
       :font-size="16"
-      :hostFontSize="10"
-      :hostMargin="[3, 3]"
-      :hostPadding="[3, 3]"
+      :hostFontSize="16"
+      :hostMargin="[10, 5]"
+      :hostPadding="[8, 8]"
       font-family="微软雅黑"
       :custom-step-style="customStepStyle"
       @step-click="stepClickHandler"
@@ -19,47 +28,175 @@
 
 <script setup>
 import data from "./data";
-import StepTree from "../step-tree.vue";
+import StepTree from "../StepTree.vue";
 
-const treeData = [{ name: "开始" }, ...data[0].steps, { name: "结束" }];
+import { ref, nextTick, toRefs } from "vue";
+import { useRouter } from "vue-router";
+const treeWrapRef = ref();
+const $router = useRouter();
+const treeData = [
+  { displayName: "开始", type: "startEnd" },
+  ...data[0].steps,
+  { displayName: "结束", type: "startEnd" },
+];
 
+// 每个step的样式
 const customStepStyle = (step) => {
-  if (step.action === "stop") {
+  // 等待中,开始状态
+  if (step.status === "waiting") {
+    return {
+      lineColor: "#89beb2",
+      lineWidth: 1,
+      textColor: "#333",
+      rectBorderColor: "#333",
+      rectBgColor: "#89beb2",
+      rectRadius: 2,
+      rectBorderWidth: 1,
+    };
+  }
+  // 失败
+  else if (step.status === "error") {
+    return {
+      lineColor: "#89beb2",
+      lineWidth: 1,
+      textColor: "#333",
+      rectBorderColor: "#333",
+      rectBgColor: "red",
+      rectRadius: 2,
+      rectBorderWidth: 1,
+    };
+  }
+  // 成功
+  else if (step.status === "success") {
+    return {
+      lineColor: "#89beb2",
+      lineWidth: 1,
+      textColor: "#333",
+      rectBorderColor: "#333",
+      rectBgColor: "#30d567",
+      rectRadius: 2,
+      rectBorderWidth: 1,
+    };
+  }
+  // 部分成功
+  else if (step.status === "partsuccess") {
+    return {
+      lineColor: "#89beb2",
+      lineWidth: 1,
+      textColor: "#333",
+      rectBorderColor: "#333",
+      rectBgColor: "#d4f8c3",
+      rectRadius: 2,
+      rectBorderWidth: 1,
+    };
+  } else if (step.status === "running") {
     return {
-      lineColor: "red",
-      lineWidth: 3,
-      textColor: "red",
-      rectBorderColor: "red",
-      rectBorderColor: "red",
-      rectBgColor: "#ccc",
+      lineColor: "#89beb2",
+      lineWidth: 1,
+      textColor: "#333",
+      rectBorderColor: "#333",
+      rectBgColor: "#ecf752",
       rectRadius: 2,
       rectBorderWidth: 1,
     };
-  } else {
+  }
+  {
     return {
-      lineColor: "#000",
+      lineColor: "#89beb2",
       lineWidth: 1,
-      textColor: "#000",
-      rectBorderColor: "#000",
+      textColor: "#333",
+      rectBorderColor: "coral",
       rectBgColor: "#ffff",
       rectRadius: 2,
       rectBorderWidth: 1,
     };
   }
 };
+
+// hosts
+const customHostStyle = (step) => {
+  if (step.hosts?.length) {
+    step.hosts.forEach((item, idx) => {
+      if (item.status === "success") {
+        return {
+          lineColor: "#40dbd9",
+          lineWidth: 1,
+          textColor: "#ccc",
+          rectBorderColor: "#40dbd9",
+          rectBgColor: "#ffff",
+          rectRadius: 2,
+          rectBorderWidth: 1,
+        };
+      }
+    });
+  }
+};
+
 const stepClickHandler = (step) => {
   console.log(step);
+  if (step.raw.type === "startEnd") return;
+  let url = $router.resolve({
+    path: "/stepLogs",
+    params: {
+      key: 1,
+    },
+  }).href;
+  window.open(url, "_blank");
 };
 const stepHostClickHandler = (host) => {
   console.log(host);
 };
+nextTick(() => {
+  treeWrapRef.value.scrollLeft =
+    (treeWrapRef.value.scrollWidth - treeWrapRef.value.clientWidth) / 2;
+});
 </script>
 
 <style scoped>
-.test {
+.tree-cont-wrap {
   width: 100%;
   height: 100%;
-
+  margin: 0 auto;
   overflow: auto;
+  text-align: center;
+  /* display: flex;
+  justify-content: center;
+  align-items: center; */
+}
+
+.flex {
+  display: flex;
+}
+.status-box {
+  width: 400px;
+}
+.status-box span {
+  display: inline-block;
+  width: 120px;
+  height: 30px;
+  line-height: 30px;
+  color: #333;
+  text-align: center;
+  margin: 0 5px;
+}
+/* 运行中 */
+.running {
+  background: #ecf752;
+}
+/* 错误 */
+.error {
+  background: #ff4238;
+}
+/* 成功 */
+.succ {
+  background: #30d567;
+}
+/* 等待中 */
+.waiting {
+  background: #89beb2;
+}
+/* 部分成功 */
+.bf-suc {
+  background: #c6f9ae;
 }
 </style>

+ 0 - 0
src/view/step-tree/helper.ts


Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini