gemercheung před 1 rokem
rodič
revize
3d24656ff4
100 změnil soubory, kde provedl 24548 přidání a 1401 odebrání
  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. binární
      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. binární
      src/assets/avatar.png
  18. binární
      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. binární
      src/view/step-tree-v2/example/image/c.png
  90. binární
      src/view/step-tree-v2/example/image/g.png
  91. binární
      src/view/step-tree-v2/example/image/p.png
  92. binární
      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": {
   "scripts": {
     "dev": "vite",
     "dev": "vite",
     "build": "vue-tsc && vite build",
     "build": "vue-tsc && vite build",
+    "build-uat": "vue-tsc && vite build --mode uat",
     "preview": "vite preview"
     "preview": "vite preview"
   },
   },
   "dependencies": {
   "dependencies": {
@@ -15,10 +16,13 @@
     "gl-matrix": "^3.4.3",
     "gl-matrix": "^3.4.3",
     "js-base64": "^3.7.7",
     "js-base64": "^3.7.7",
     "jszip": "^3.10.1",
     "jszip": "^3.10.1",
+    "konva": "9.3.6",
     "mitt": "^3.0.1",
     "mitt": "^3.0.1",
     "ol": "^9.1.0",
     "ol": "^9.1.0",
+    "pinia": "^2.1.7",
     "proj4": "^2.11.0",
     "proj4": "^2.11.0",
     "qrcode": "^1.5.3",
     "qrcode": "^1.5.3",
+    "vite-svg-loader": "^5.1.0",
     "vue": "^3.4.21",
     "vue": "^3.4.21",
     "vue-router": "^4.3.0",
     "vue-router": "^4.3.0",
     "xlsx": "^0.18.5"
     "xlsx": "^0.18.5"

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 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="X-UA-Compatible" content="IE=edge,chrome=1">
   <meta http-equiv="expires" content="0">
   <meta http-equiv="expires" content="0">
   <meta content="telephone=no" name="format-detection">
   <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="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="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">
   <link rel="apple-touch-icon" sizes="180x180" href="//4dkk.4dage.com/FDKKIMG/icon/kankan_icon180.png">
@@ -90,14 +90,14 @@
     }
     }
     var ua = versions()
     var ua = versions()
     var domicon = document.querySelector('.icon-warp i')
     var domicon = document.querySelector('.icon-warp i')
-    const version = `1.1.0`
+    const version = `1.2.0`
 
 
     document.getElementById('btn').addEventListener('click', function (e) {
     document.getElementById('btn').addEventListener('click', function (e) {
       if (ua.weixin) {
       if (ua.weixin) {
         alert('微信/QQ内无法下载应用,请点击右上角,选择"浏览器中打开"')
         alert('微信/QQ内无法下载应用,请点击右上角,选择"浏览器中打开"')
       }
       }
       else {
       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
     document.querySelector('span[itemprop=softwareVersion]').innerHTML = version

binární
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) {
     if (exixts) {
       clearTimeout(timeout);
       clearTimeout(timeout);
       timeout = setTimeout(() => {
       timeout = setTimeout(() => {
-        console.log("close");
         loading!.close();
         loading!.close();
         loading = null;
         loading = null;
         exixts = false;
         exixts = false;

binární
src/assets/avatar.png


binární
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>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 7 - 0
src/assets/location_n.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 7 - 0
src/assets/location_o.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 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>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 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>
 <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>
     <template #footer>
       <div class="dialog-footer">
       <div class="dialog-footer">
         <el-button @click="emit('update:visible', false)">取消</el-button>
         <el-button @click="emit('update:visible', false)">取消</el-button>
@@ -25,12 +15,20 @@
 import { ElMessage } from "element-plus";
 import { ElMessage } from "element-plus";
 import { ref, watchEffect } from "vue";
 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<{
 const emit = defineEmits<{
   (e: "update:visible", visible: boolean): void;
   (e: "update:visible", visible: boolean): void;
 }>();
 }>();
@@ -41,8 +39,8 @@ watchEffect(() => {
 });
 });
 
 
 const submit = async () => {
 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);
   await props.updateValue(ivalue.value);
   emit("update:visible", false);
   emit("update:visible", false);

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1318 - 0
src/lib/board/4dmap.d.ts


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 14019 - 0
src/lib/board/4dmap.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 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 getDevicePage = `/relics/camera/page`;
 export const delDevice = `/relics/camera/del/:deviceId`;
 export const delDevice = `/relics/camera/del/:deviceId`;
 export const addDevice = `/relics/camera/add`;
 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,
   Param,
 } from "./state";
 } from "./state";
 import { ElMessage } from "element-plus";
 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);
 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>(
 export const sendFetch = <T>(
   url: string,
   url: string,
   init: RequestInit,
   init: RequestInit,
@@ -32,6 +41,9 @@ export const sendFetch = <T>(
       sendUrl =
       sendUrl =
         sendUrl + "?" + new URLSearchParams({ ...gParams, ...other.params });
         sendUrl + "?" + new URLSearchParams({ ...gParams, ...other.params });
     }
     }
+    if (other.noToken) {
+      delete gHeaders['relics-token']
+    }
   }
   }
   lifeHook.forEach(({ start }) => start());
   lifeHook.forEach(({ start }) => start());
 
 
@@ -39,9 +51,9 @@ export const sendFetch = <T>(
     ...init,
     ...init,
     headers: headers
     headers: headers
       ? {
       ? {
-          ...headers,
-          ...gHeaders,
-        }
+        ...headers,
+        ...gHeaders,
+      }
       : gHeaders,
       : gHeaders,
   })
   })
     .then((res) => {
     .then((res) => {
@@ -54,14 +66,18 @@ export const sendFetch = <T>(
       }
       }
     })
     })
     .then((data) => {
     .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 {
       } 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),
     body: JSON.stringify(props),
   });
   });
 
 
+  export const loginOutFetch = () =>
+    sendFetch<{ user: UserInfo; token: string }>(URL.logout, {
+      method: "post",
+      body: JSON.stringify({}),
+    });
+  
+
 export const userInfoFetch = () =>
 export const userInfoFetch = () =>
   sendFetch<UserInfo>(URL.getUserInfo, { method: "post" });
   sendFetch<UserInfo>(URL.getUserInfo, { method: "post" });
 
 
@@ -242,3 +265,11 @@ export const addDeviceFetch = (sn: string) =>
     method: "post",
     method: "post",
     body: JSON.stringify({ cameraSn: sn }),
     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,
   creationMethodDesc,
 } from "@/store/relics";
 } from "@/store/relics";
 import { SceneStatus } from "@/store/scene";
 import { SceneStatus } from "@/store/scene";
+import {
+  WholeLineLineAttrib,
+  WholeLinePointAttrib,
+  WholeLinePolygonAttrib,
+} from "drawing-board";
 
 
+type UserInfoRoles = {
+  roleId: number;
+  roleKey: string;
+  roleName: string;
+};
 export type UserInfo = {
 export type UserInfo = {
   head: string;
   head: string;
   nickName: string;
   nickName: string;
   userName: string;
   userName: string;
+  roles: UserInfoRoles[];
+  orgId: string;
+  orgName?: string;
+  userId?: number;
 };
 };
 
 
 export type Relics = {
 export type Relics = {
@@ -31,11 +45,19 @@ export type ResPage<T> = {
   records: T[];
   records: T[];
 };
 };
 
 
+export type ResResult = {
+  code: number;
+  data: any;
+  message: any;
+  success: boolean;
+  timestamp: string;
+};
 export type ScenePoint = {
 export type ScenePoint = {
   tbStatus: number;
   tbStatus: number;
   createTime: string;
   createTime: string;
   updateTime: string;
   updateTime: string;
   cameraType: DeviceType;
   cameraType: DeviceType;
+  index: number;
   id: number;
   id: number;
   uuid: number;
   uuid: number;
   name: string;
   name: string;
@@ -132,3 +154,15 @@ export type Device = {
   userId: 2;
   userId: 2;
   userName: string;
   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 { UserStatus, logintAuth, userStatus } from "./store/user";
 import { watch, watchEffect } from "vue";
 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 history = createWebHashHistory();
+
 const routes: RouteRecordRaw[] = [
 const routes: RouteRecordRaw[] = [
   {
   {
+    path: "/no-persession",
+    name: "no-persession",
+    meta: { title: "无权限", hidden: true },
+    component: () => import("@/view/no-persession.vue"),
+  },
+  {
     path: "/down-vision",
     path: "/down-vision",
     name: "down-vision",
     name: "down-vision",
+    meta: { title: "" },
     component: () => import("@/view/down-vision.vue"),
     component: () => import("@/view/down-vision.vue"),
   },
   },
   {
   {
@@ -16,6 +29,12 @@ const routes: RouteRecordRaw[] = [
     component: () => import("@/view/login.vue"),
     component: () => import("@/view/login.vue"),
   },
   },
   {
   {
+    path: "/tree2",
+    name: "query-tree-2",
+    meta: { title: "登录" },
+    component: () => import("@/view/step-tree-v2/example/example.vue"),
+  },
+  {
     path: "/tree",
     path: "/tree",
     name: "query-tree",
     name: "query-tree",
     meta: { title: "登录" },
     meta: { title: "登录" },
@@ -35,11 +54,31 @@ const routes: RouteRecordRaw[] = [
       {
       {
         path: "relics/:relicsId",
         path: "relics/:relicsId",
         children: [
         children: [
+          // {
+          //   path: "",
+          //   name: "map",
+          //   meta: { title: "文物", navClass: "map" },
+          //   component: () => import("@/view/map/map-board.vue"),
+          // },
           {
           {
-            path: "",
+            path: "map",
             name: "map",
             name: "map",
             meta: { title: "文物", navClass: "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",
             path: "pano/:pid",
@@ -61,6 +100,18 @@ const routes: RouteRecordRaw[] = [
         meta: { title: "设备管理" },
         meta: { title: "设备管理" },
         component: () => import("@/view/device.vue"),
         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: "",
             path: "",
             name: "query-map",
             name: "query-map",
             meta: { title: "文物", navClass: "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",
             path: "pano/:pid",
@@ -87,6 +152,8 @@ const routes: RouteRecordRaw[] = [
       },
       },
     ],
     ],
   },
   },
+  { path: '/:pathMatch(.*)*', component: import("@/view/layout/nav.vue") },
+
 ];
 ];
 
 
 export const findRoute = (
 export const findRoute = (
@@ -150,6 +217,20 @@ router.beforeEach((to, _, next) => {
     }
     }
     return;
     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) {
   if (to.meta?.title) {
     setDocTitle(to.meta.title as string);
     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 {
 import {
   relicsInfoFetch,
   relicsInfoFetch,
+  // relicsPolyginsFetch,
   relicsSelfCheckFetch,
   relicsSelfCheckFetch,
   updateRelicsFetch,
   updateRelicsFetch,
 } from "@/request";
 } from "@/request";
+import { errorHook } from "@/request/state";
 import { ref } from "vue";
 import { ref } from "vue";
 import { Relics } from "@/request/type";
 import { Relics } from "@/request/type";
 import { refreshScenes } from "./scene";
 import { refreshScenes } from "./scene";
+import { router } from '../router'
 
 
 export type { Relics } from "@/request/type";
 export type { Relics } from "@/request/type";
 export const relics = ref<Relics>();
 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) => {
 export const initRelics = async (relicsId: number) => {
   relics.value = await relicsInfoFetch(relicsId);
   relics.value = await relicsInfoFetch(relicsId);
   if (relics.value) {
   if (relics.value) {
     await refreshScenes();
     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);
   relics.value = await relicsInfoFetch(relicsId);
   if (relics.value) {
   if (relics.value) {
     await refreshScenes();
     await refreshScenes();

+ 114 - 29
src/store/scene.ts

@@ -1,10 +1,16 @@
 import { relicsScenesFetch, updateRelicsScenePosNameFetch } from "@/request";
 import { relicsScenesFetch, updateRelicsScenePosNameFetch } from "@/request";
-import { computed, ref } from "vue";
+import { computed, ref, watch } from "vue";
 import { Scene, ScenePoint } from "@/request/type";
 import { Scene, ScenePoint } from "@/request/type";
-import { gHeaders } from "@/request/state";
 import { relics } from "./relics";
 import { relics } from "./relics";
 import { DeviceType, DeviceType as SceneType } from "./device";
 import { DeviceType, DeviceType as SceneType } from "./device";
 import { conversionFactory } from "@/helper/coord-transform";
 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 };
 export type { Scene, ScenePoint };
 
 
@@ -20,6 +26,7 @@ export const scenePoints = computed(() =>
     return t;
     return t;
   }, [] as ScenePoint[])
   }, [] as ScenePoint[])
 );
 );
+
 export const relicsId = computed(() => relics.value!.relicsId);
 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
 // 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`;
     return `https://4dkk.4dage.com/scene_view_data/${point.sceneCode}/images/pan/high/${point.uuid}.jpg`;
   }
   }
 };
 };
+
 export const refreshScenes = async () => {
 export const refreshScenes = async () => {
   const sscenes = await relicsScenesFetch(relicsId.value);
   const sscenes = await relicsScenesFetch(relicsId.value);
   scenes.value = sscenes.map((scene) => {
   scenes.value = sscenes.map((scene) => {
@@ -69,28 +77,32 @@ export const refreshScenes = async () => {
 
 
     return {
     return {
       ...scene,
       ...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 (
 export const updateScenePointName = async (
@@ -101,21 +113,25 @@ export const updateScenePointName = async (
   relicsId.value && (await refreshScenes());
   relicsId.value && (await refreshScenes());
 };
 };
 
 
-export const gotoScene = (scene: Scene, edit = false) => {
+export const gotoScene = async (scene: Scene, edit = false) => {
   const params = new URLSearchParams();
   const params = new URLSearchParams();
   if (edit) {
   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");
   params.set("lang", "zh");
   if (scene.sceneCode.startsWith("KJ")) {
   if (scene.sceneCode.startsWith("KJ")) {
+    const qjURL = import.meta.env.VITE_QJ_URL;
     params.set("id", scene.sceneCode);
     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 {
   } else {
     params.set("m", scene.sceneCode);
     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.ERR]: "计算失败",
   [SceneStatus.SUCCESS]: "计算成功",
   [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 { errorHook, gHeaders } from "@/request/state";
 import { UserInfo } from "@/request/type";
 import { UserInfo } from "@/request/type";
 import { encodePwd } from "@/util";
 import { encodePwd } from "@/util";
-import { ref } from "vue";
+import { ref, computed } from "vue";
 
 
 export const user = ref<UserInfo>();
 export const user = ref<UserInfo>();
 export enum UserStatus {
 export enum UserStatus {
@@ -12,19 +12,22 @@ export enum UserStatus {
 }
 }
 export const userStatus = ref<UserStatus>(UserStatus.UNKNOWN);
 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) => {
 export const login = async (props: LoginProps) => {
   const data = await loginFetch({
   const data = await loginFetch({
     ...props,
     ...props,
     password: encodePwd(props.password),
     password: encodePwd(props.password),
   });
   });
   user.value = data.user;
   user.value = data.user;
-  gHeaders.token = data.token;
+  gHeaders['relics-token'] = data.token;
   localStorage.setItem("token", data.token);
   localStorage.setItem("token", data.token);
   await getUserInfo();
   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;
   userStatus.value = UserStatus.NOT_LOGIN;
 };
 };
 
 
@@ -43,7 +46,7 @@ export const logintAuth = getUserInfo;
 
 
 const token = localStorage.getItem("token");
 const token = localStorage.getItem("token");
 if (token) {
 if (token) {
-  gHeaders.token = token;
+  gHeaders['relics-token'] = token;
 } else {
 } else {
   userStatus.value = UserStatus.NOT_LOGIN;
   userStatus.value = UserStatus.NOT_LOGIN;
 }
 }

+ 16 - 12
src/style.scss

@@ -2,7 +2,7 @@ html,
 body,
 body,
 #app {
 #app {
   margin: 0;
   margin: 0;
-  width : 100vw;
+  width: 100vw;
   height: 100vh;
   height: 100vh;
 }
 }
 
 
@@ -15,30 +15,30 @@ body,
 }
 }
 
 
 .disable {
 .disable {
-  opacity       : 0.7;
+  opacity: 0.7;
   pointer-events: none;
   pointer-events: none;
 }
 }
 
 
 .relics-layout {
 .relics-layout {
-  height        : 100%;
-  display       : flex;
+  height: 100%;
+  display: flex;
   flex-direction: column;
   flex-direction: column;
-  overflow      : hidden !important;
+  overflow: hidden !important;
 
 
   .relics-header {
   .relics-header {
     flex: none;
     flex: none;
   }
   }
 
 
   .relics-content {
   .relics-content {
-    flex    : 1;
+    flex: 1;
     position: relative;
     position: relative;
 
 
     .el-table {
     .el-table {
       position: absolute;
       position: absolute;
-      left    : 0;
-      top     : 0;
-      width   : 100%;
-      height  : 100%;
+      left: 0;
+      top: 0;
+      width: 100%;
+      height: 100%;
     }
     }
   }
   }
 
 
@@ -48,6 +48,10 @@ body,
 }
 }
 
 
 .link {
 .link {
-  color : var(--el-color-primary);
+  color: var(--el-color-primary);
   cursor: pointer;
   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;
   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);
   downloadPointsXLSL1(points, desc, name);
   downloadPointsXLSL2(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>
           <el-form-item label="设备类型:">
           <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
               <el-option
                 :value="Number(key)"
                 :value="Number(key)"
                 :label="type"
                 :label="type"
@@ -23,7 +27,11 @@
 
 
           <el-form-item>
           <el-form-item>
             <el-button type="primary" @click="refresh">查询</el-button>
             <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>
             <el-button type="primary" @click="addHandler"> 添加设备 </el-button>
             <el-button type="primary" @click="addHandler"> 添加设备 </el-button>
@@ -43,16 +51,31 @@
         >
         >
           {{ DeviceTypeDesc[row.cameraType] }}
           {{ DeviceTypeDesc[row.cameraType] }}
         </el-table-column>
         </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="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) }}
           {{ row.createTime && row.createTime.substr(0, 16) }}
         </el-table-column>
         </el-table-column>
 
 
         <el-table-column label="操作" width="100px" fixed="right">
         <el-table-column label="操作" width="100px" fixed="right">
           <template #default="{ row }">
           <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>
             </el-button>
           </template>
           </template>

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

@@ -9,18 +9,43 @@
           @click="router.back()"
           @click="router.back()"
           v-if="showBack"
           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>
         </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>
     <div class="content">
     <div class="content">
       <ly-slide class="slide" v-if="user && !['pano', 'map'].includes(name)" />
       <ly-slide class="slide" v-if="user && !['pano', 'map'].includes(name)" />
@@ -36,19 +61,55 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { Back } from "@element-plus/icons-vue";
+import { Back, QuestionFilled } from "@element-plus/icons-vue";
 import { router } from "@/router";
 import { router } from "@/router";
-import { computed } from "vue";
+import { computed, reactive } from "vue";
 import { user, logout } from "@/store/user";
 import { user, logout } from "@/store/user";
 import { errorHook } from "@/request/state";
 import { errorHook } from "@/request/state";
 import lySlide from "./slide/index.vue";
 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 name = computed(() => router.currentRoute.value.meta?.navClass as string);
 const routeName = computed(() => router.currentRoute.value.name as string);
 const routeName = computed(() => router.currentRoute.value.name as string);
+let isLogout = false;
 const logoutHandler = () => {
 const logoutHandler = () => {
-  logout();
+  if (!isLogout) {
+    logout(true);
+    isLogout = true;
+  }
+  userStatus.value = UserStatus.NOT_LOGIN;
   router.replace({ name: "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) => {
 errorHook.push((code) => {
   if (code === 4008) {
   if (code === 4008) {
     router.replace({ name: "login" });
     router.replace({ name: "login" });
@@ -71,16 +132,47 @@ const showBack = computed(() => {
   flex-direction: column;
   flex-direction: column;
   height: 100%;
   height: 100%;
 }
 }
+
 .header {
 .header {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: space-between;
   justify-content: space-between;
+  min-height: 60px;
   padding: 4px 10px;
   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) {
   &:not(.pano, .map) {
     border-bottom: 1px solid var(--border-color);
     border-bottom: 1px solid var(--border-color);
     flex: 0 0 auto;
     flex: 0 0 auto;
   }
   }
+
   &.pano,
   &.pano,
   &.map {
   &.map {
     pointer-events: none;
     pointer-events: none;
@@ -91,6 +183,7 @@ const showBack = computed(() => {
     .avatar {
     .avatar {
       display: none;
       display: none;
     }
     }
+
     * {
     * {
       pointer-events: all;
       pointer-events: all;
     }
     }
@@ -137,8 +230,41 @@ const showBack = computed(() => {
 .title-span {
 .title-span {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
+
   > span {
   > span {
     margin-left: 10px;
     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>
 </style>

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

@@ -3,10 +3,12 @@
     <el-menu
     <el-menu
       :default-active="(router.currentRoute.value.name as string)"
       :default-active="(router.currentRoute.value.name as string)"
       @select="(name: string) => router.push({ name })"
       @select="(name: string) => router.push({ name })"
+      style="border-right: none"
     >
     >
       <sub-menu
       <sub-menu
         v-for="route in routes"
         v-for="route in routes"
         :meta="route.meta"
         :meta="route.meta"
+        v-show="!route.meta.hidden"
         :name="(route.name as string)"
         :name="(route.name as string)"
         :key="route.name"
         :key="route.name"
       />
       />
@@ -15,11 +17,37 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
+import { computed } from "vue";
 import subMenu from "./submenu.vue";
 import subMenu from "./submenu.vue";
+import { user } from "@/store/user";
+
 import { router, findRoute } from "@/router";
 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>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>

+ 156 - 71
src/view/login.vue

@@ -30,72 +30,96 @@
               <img class="code" src="/image/pic_camera@2x.png" />
               <img class="code" src="/image/pic_camera@2x.png" />
             </div>
             </div>
           </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>
       </div>
     </div>
     </div>
@@ -103,14 +127,20 @@
 </template>
 </template>
 
 
 <script lang="ts" setup>
 <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 { login } from "@/store/user";
 import { ElMessage } from "element-plus";
 import { ElMessage } from "element-plus";
 import { router } from "@/router";
 import { router } from "@/router";
 import qrCode from "qrcode";
 import qrCode from "qrcode";
 import DragVerify from "@/components/drag-verify.vue";
 import DragVerify from "@/components/drag-verify.vue";
-
+import register from "@/view/register/register.vue";
+import reset from "@/view/register/reset.vue";
 const PHONE = {
 const PHONE = {
   REG: /^1(3|4|5|6|7|8|9)\d{9}$/,
   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}$/,
   // 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 flag = ref(false);
 const verify = ref<any>();
 const verify = ref<any>();
 const isPassing2 = ref(false);
 const isPassing2 = ref(false);
+
+const registerStatus = ref(0);
+const currentStatus = computed(
+  () => (status: number) => status === registerStatus.value
+);
 // 表单
 // 表单
 const form = reactive({
 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: "" });
 const verification = reactive({ phone: "", psw: "" });
 
 
@@ -187,6 +222,19 @@ const submitClick = async () => {
   verify.value.reset();
   verify.value.reset();
   isPassing2.value = false;
   isPassing2.value = false;
 };
 };
+
+// 忘记密码
+const handleForgetPassword = () => {
+  registerStatus.value = 2;
+};
+// 注册
+const handleRegister = () => {
+  registerStatus.value = 1;
+};
+// 业务回到login
+const goTologin = () => {
+  registerStatus.value = 0;
+};
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
@@ -194,6 +242,7 @@ const submitClick = async () => {
   width: 100%;
   width: 100%;
   height: 100%;
   height: 100%;
 }
 }
+
 .content {
 .content {
   display: flex;
   display: flex;
   justify-content: center;
   justify-content: center;
@@ -202,8 +251,10 @@ const submitClick = async () => {
   box-sizing: border-box;
   box-sizing: border-box;
   max-width: 1620px;
   max-width: 1620px;
   height: 100vh;
   height: 100vh;
+  margin: 0 auto;
   padding: 0 50px 0 50px;
   padding: 0 50px 0 50px;
 }
 }
+
 .info {
 .info {
   color: #000;
   color: #000;
   flex: none;
   flex: none;
@@ -218,10 +269,12 @@ const submitClick = async () => {
 
 
   .top {
   .top {
     margin-top: 50px;
     margin-top: 50px;
+
     img {
     img {
       width: 142px;
       width: 142px;
     }
     }
   }
   }
+
   .bottom {
   .bottom {
     height: 470px;
     height: 470px;
     display: flex;
     display: flex;
@@ -257,13 +310,16 @@ const submitClick = async () => {
           background-size: 100% 100%;
           background-size: 100% 100%;
         }
         }
       }
       }
+
       .e-code {
       .e-code {
         width: 128px;
         width: 128px;
         margin-top: 13px;
         margin-top: 13px;
         position: relative;
         position: relative;
+
         > img {
         > img {
           width: 100%;
           width: 100%;
         }
         }
+
         .e-logo {
         .e-logo {
           position: absolute;
           position: absolute;
           top: 50%;
           top: 50%;
@@ -274,6 +330,7 @@ const submitClick = async () => {
           padding: 7px;
           padding: 7px;
           border-radius: 4px;
           border-radius: 4px;
           text-align: center;
           text-align: center;
+
           img {
           img {
             height: 100%;
             height: 100%;
             width: 100%;
             width: 100%;
@@ -281,6 +338,7 @@ const submitClick = async () => {
           }
           }
         }
         }
       }
       }
+
       p:last-child {
       p:last-child {
         font-weight: 400;
         font-weight: 400;
         font-size: 14px;
         font-size: 14px;
@@ -291,15 +349,18 @@ const submitClick = async () => {
 
 
   .center {
   .center {
     text-align: center;
     text-align: center;
+
     h1 {
     h1 {
       color: #781c0b;
       color: #781c0b;
       font-size: 48px;
       font-size: 48px;
       line-height: 3.7rem;
       line-height: 3.7rem;
       margin-bottom: 0.7rem;
       margin-bottom: 0.7rem;
     }
     }
+
     p {
     p {
       width: 100%;
       width: 100%;
       margin-top: 40px;
       margin-top: 40px;
+
       img {
       img {
         width: 320px;
         width: 320px;
       }
       }
@@ -312,14 +373,17 @@ const submitClick = async () => {
   pointer-events: none;
   pointer-events: none;
   height: 153px;
   height: 153px;
   min-width: 1200px;
   min-width: 1200px;
+
   img {
   img {
     position: absolute;
     position: absolute;
     right: 0;
     right: 0;
   }
   }
 }
 }
+
 .fill {
 .fill {
   width: 100%;
   width: 100%;
 }
 }
+
 .login {
 .login {
   width: 400px;
   width: 400px;
   // padding: 40px 40px 30px;
   // padding: 40px 40px 30px;
@@ -344,6 +408,7 @@ const submitClick = async () => {
   .panel-form-item {
   .panel-form-item {
     padding-left: 0;
     padding-left: 0;
     padding-right: 0;
     padding-right: 0;
+
     .icon-style {
     .icon-style {
       margin-right: 14px;
       margin-right: 14px;
       font-size: 20px;
       font-size: 20px;
@@ -379,6 +444,7 @@ const submitClick = async () => {
   background: no-repeat left bottom;
   background: no-repeat left bottom;
   background-size: auto 100%;
   background-size: auto 100%;
 }
 }
+
 .l-content {
 .l-content {
   display: flex;
   display: flex;
   width: 100%;
   width: 100%;
@@ -386,6 +452,17 @@ const submitClick = async () => {
   justify-content: center;
   justify-content: center;
   align-items: flex-start;
   align-items: flex-start;
 }
 }
+
+.register {
+  display: flex;
+  flex-direction: row;
+  flex: 1;
+  justify-content: center;
+
+  span {
+    padding: 0 10px;
+  }
+}
 </style>
 </style>
 
 
 <style>
 <style>
@@ -406,10 +483,12 @@ const submitClick = async () => {
 .login .code-form-item .el-input__inner {
 .login .code-form-item .el-input__inner {
   flex: 1;
   flex: 1;
 }
 }
+
 .login .code-form-item .el-input-group__append,
 .login .code-form-item .el-input-group__append,
 .login .code-form-item .el-input__inner {
 .login .code-form-item .el-input__inner {
   border-radius: 4px;
   border-radius: 4px;
 }
 }
+
 input[type="password"]::-ms-reveal {
 input[type="password"]::-ms-reveal {
   display: none;
   display: none;
 }
 }
@@ -476,7 +555,7 @@ input[type="password"]::-ms-reveal {
 .panel-form-item .el-button,
 .panel-form-item .el-button,
 .panel-form-item .el-input__inner {
 .panel-form-item .el-input__inner {
   height: 40px;
   height: 40px;
-  font-size: 1.14rem;
+  font-size: 16px;
 }
 }
 
 
 .panel-form-item .el-button {
 .panel-form-item .el-button {
@@ -488,6 +567,7 @@ input[type="password"]::-ms-reveal {
 .panel-form-item .el-form-item__label {
 .panel-form-item .el-form-item__label {
   line-height: 50px;
   line-height: 50px;
 }
 }
+
 .e-code img {
 .e-code img {
   width: 100%;
   width: 100%;
 }
 }
@@ -503,4 +583,9 @@ input[type="password"]::-ms-reveal {
 .drag_verify {
 .drag_verify {
   border: 1px solid #dcdfe6;
   border: 1px solid #dcdfe6;
 }
 }
+
+.register span {
+  cursor: pointer;
+}
+
 </style>
 </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";
 import { Manage } from "./manage";
 export type { TileType } from "./tile";
 export type { TileType } from "./tile";
 
 
-export const createMap = (dom: HTMLDivElement) => {
+export const createMap = (dom?: HTMLDivElement) => {
   return new Manage(dom);
   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}`;
   const url = `https://t0.tianditu.gov.cn/${layer}_c/wmts?tk=${key}`;
   return new WMTS({
   return new WMTS({
     url,
     url,
+    crossOrigin: "anonymous", // 设置跨域
     layer,
     layer,
     version: "1.0.0",
     version: "1.0.0",
     matrixSet: "c",
     matrixSet: "c",

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

@@ -9,6 +9,7 @@ import {
   downloadPointsXLSL1,
   downloadPointsXLSL1,
   downloadPointsXLSL2,
   downloadPointsXLSL2,
 } from "@/util/pc4xlsl";
 } from "@/util/pc4xlsl";
+import { noValidPoint } from "./install";
 
 
 export const exportFile = async (
 export const exportFile = async (
   points: ScenePoint[],
   points: ScenePoint[],
@@ -20,7 +21,8 @@ export const exportFile = async (
     return;
     return;
   }
   }
   name = name ? name + "-" : "";
   name = name ? name + "-" : "";
-  points = points.filter((point) => !!point.pos);
+
+  points = points.filter((point) => !noValidPoint(point));
 
 
   if (points.length === 0) {
   if (points.length === 0) {
     ElMessage.error("当前选择点位没有gis信息");
     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"
         size="large"
         style="margin-right: 20px; width: 100px"
         style="margin-right: 20px; width: 100px"
         @click="copyGis"
         @click="copyGis"
-        v-if="point?.pos && point.pos.length"
+        v-if="point && !noValidPoint(point)"
       >
       >
         复制经纬度
         复制经纬度
       </el-button>
       </el-button>
@@ -25,24 +25,26 @@
         @click="update = true"
         @click="update = true"
         v-if="router.currentRoute.value.name === 'pano'"
         v-if="router.currentRoute.value.name === 'pano'"
       >
       >
-        修改名称
+        测点说明
       </el-button>
       </el-button>
     </div>
     </div>
   </div>
   </div>
   <SingleInput
   <SingleInput
     v-if="point"
     v-if="point"
     :visible="update"
     :visible="update"
+    isAllowEmpty
     @update:visible="update = false"
     @update:visible="update = false"
     :value="point.name || ''"
     :value="point.name || ''"
     :update-value="tex => updateScenePointName(point!, tex)"
     :update-value="tex => updateScenePointName(point!, tex)"
-    title="修改点位名称"
+    title="测点说明"
+    placeholder="请填写测点说明"
   />
   />
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import SingleInput from "@/components/single-input.vue";
 import SingleInput from "@/components/single-input.vue";
 import { router, setDocTitle } from "@/router";
 import { router, setDocTitle } from "@/router";
-import { mergeFuns } from "@/util";
+import { mergeFuns, round } from "@/util";
 import { computed, onMounted, onUnmounted, ref, watchEffect } from "vue";
 import { computed, onMounted, onUnmounted, ref, watchEffect } from "vue";
 import { init } from "./env";
 import { init } from "./env";
 import {
 import {
@@ -51,11 +53,12 @@ import {
   ScenePoint,
   ScenePoint,
   scenePoints,
   scenePoints,
 } from "@/store/scene";
 } from "@/store/scene";
-import { copyText, toDegrees } from "@/util";
+import { copyText, toDegrees, getTextBound } from "@/util";
 import { ElMessage } from "element-plus";
 import { ElMessage } from "element-plus";
 import saveAs from "@/util/file-serve";
 import saveAs from "@/util/file-serve";
 import { DeviceType } from "@/store/device";
 import { DeviceType } from "@/store/device";
 import { initRelics, relics } from "@/store/relics";
 import { initRelics, relics } from "@/store/relics";
+import { noValidPoint } from "../map/install";
 
 
 type Params = { pid?: string; relicsId?: string } | null;
 type Params = { pid?: string; relicsId?: string } | null;
 const params = computed(() => router.currentRoute.value.params as Params);
 const params = computed(() => router.currentRoute.value.params as Params);
@@ -86,26 +89,92 @@ const panoUrls = computed(() => {
 const update = ref(false);
 const update = ref(false);
 const loading = ref(false);
 const loading = ref(false);
 
 
-const copyGis = async () => {
+const getGis = () => {
   const pos = point.value!.pos as number[];
   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("经纬度高程复制成功");
   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;
   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>;
 let pano: ReturnType<typeof init>;
@@ -141,8 +210,8 @@ onMounted(() => {
 
 
 onUnmounted(() => mergeFuns(...destroyFns)());
 onUnmounted(() => mergeFuns(...destroyFns)());
 watchEffect(() => {
 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>
 </script>

+ 28 - 0
src/view/quisk.ts

@@ -2,6 +2,11 @@ import { quiskMountFactory } from "@/helper/mount";
 import RelicsEdit from "./relics-edit.vue";
 import RelicsEdit from "./relics-edit.vue";
 import DeviceEdit from "./device-edit.vue";
 import DeviceEdit from "./device-edit.vue";
 import SceneSelect from "./scene-select.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, {
 export const relicsEdit = quiskMountFactory(RelicsEdit, {
   title: "创建文物",
   title: "创建文物",
@@ -16,3 +21,26 @@ export const selectScenes = quiskMountFactory(SceneSelect, {
   title: "选择场景",
   title: "选择场景",
   width: 1000,
   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-form-item label="文物地址:">
       <el-input
       <el-input
         v-model="data.address"
         v-model="data.address"
-        style="width: 100%"
+        style="width: 100%;padding-bottom: 25px;"
         :maxlength="500"
         :maxlength="500"
         show-word-limit
         show-word-limit
+         :autosize="{ minRows: 3, maxRows: 6 }"
+      
         type="textarea"
         type="textarea"
         placeholder="请输入"
         placeholder="请输入"
       />
       />
@@ -77,7 +79,7 @@ defineExpose<QuiskExpose>({
       ElMessage.error("请输入文物名称!");
       ElMessage.error("请输入文物名称!");
       throw "请输入文物名称!";
       throw "请输入文物名称!";
     }
     }
-    props.submit(data.value);
+    props.submit(data.value as any as Relics);
   },
   },
 });
 });
 </script>
 </script>

+ 6 - 3
src/view/relics.vue

@@ -101,7 +101,9 @@
               link
               link
               type="primary"
               type="primary"
               size="small"
               size="small"
-              @click="router.push({ name: 'map', params: { relicsId: row.relicsId } })"
+              @click="
+                router.push({ name: COORD_NAME, params: { relicsId: row.relicsId } })
+              "
             >
             >
               数据提取
               数据提取
             </el-button>
             </el-button>
@@ -141,11 +143,12 @@ import {
   relicsTypeDesc,
   relicsTypeDesc,
   creationMethodDesc,
   creationMethodDesc,
 } from "@/store/relics";
 } from "@/store/relics";
-import { router } from "@/router";
+import { COORD_NAME, router } from "@/router";
 import { ElMessageBox } from "element-plus";
 import { ElMessageBox } from "element-plus";
 import { relicsEdit } from "./quisk";
 import { relicsEdit } from "./quisk";
 import TexToolTip from "@/components/tex-tooltip.vue";
 import TexToolTip from "@/components/tex-tooltip.vue";
 import { debounce } from "@/util";
 import { debounce } from "@/util";
+import { QUERY_COORD_NAME } from "@/router";
 
 
 const initProps: RelicsPageProps = {
 const initProps: RelicsPageProps = {
   pageNum: 1,
   pageNum: 1,
@@ -171,7 +174,7 @@ const delHandler = async (relicsId: number) => {
   }
   }
 };
 };
 const getQueryRouteLocation = (row: Relics) =>
 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 shareHandler = async (row: Relics) => {
   const link = location.origin + location.pathname + getQueryRouteLocation(row).href;
   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 SceneTable from "./scene.vue";
 import { ElMessage, ElTable } from "element-plus";
 import { ElMessage, ElTable } from "element-plus";
 import { QuiskExpose } from "@/helper/mount";
 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">;
 type SimpleScene = Pick<Scene, "sceneId" | "sceneCode">;
 const props = defineProps<{
 const props = defineProps<{
@@ -29,6 +29,12 @@ const scenes = computed(() =>
     simpleScenes.value.some(({ sceneCode }) => scene.sceneCode === sceneCode)
     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 loaded = ref(false);
 const tableProps = {
 const tableProps = {
@@ -40,24 +46,24 @@ const tableProps = {
       ({ sceneCode }) => !originSceneCodes.includes(sceneCode)
       ({ sceneCode }) => !originSceneCodes.includes(sceneCode)
       // || !val.some((scene) => scene.sceneCode === sceneCode)
       // || !val.some((scene) => scene.sceneCode === sceneCode)
     );
     );
-    console.log(val, [...simpleScenes.value], originSceneCodes);
 
 
     let tip = false;
     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) {
     if (props.selfScenes) {
       const foreChecks = props.selfScenes.filter(
       const foreChecks = props.selfScenes.filter(
@@ -77,12 +83,12 @@ const tableProps = {
       }
       }
     }
     }
 
 
-    console.log([...simpleScenes.value]);
     tip = false;
     tip = false;
   },
   },
   tableDataChange(val: Scene[]) {
   tableDataChange(val: Scene[]) {
     loaded.value = false;
     loaded.value = false;
     originScenes.value = val;
     originScenes.value = val;
+    sceneAlls.push(...val);
     setTimeout(checkedTable);
     setTimeout(checkedTable);
   },
   },
   tableRef: ref<InstanceType<typeof ElTable>>(),
   tableRef: ref<InstanceType<typeof ElTable>>(),
@@ -92,18 +98,17 @@ let time: NodeJS.Timeout;
 const checkedTable = () => {
 const checkedTable = () => {
   if (tableProps.tableRef.value) {
   if (tableProps.tableRef.value) {
     tableProps.tableRef.value!.clearSelection();
     tableProps.tableRef.value!.clearSelection();
-    console.log("1");
     scenes.value.forEach((item) => {
     scenes.value.forEach((item) => {
       tableProps.tableRef.value!.toggleRowSelection(item, true);
       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>({
 defineExpose<QuiskExpose>({
   async submit() {
   async submit() {

+ 25 - 57
src/view/scene.vue

@@ -4,50 +4,23 @@
       <div class="search">
       <div class="search">
         <el-form label-width="100px" inline>
         <el-form label-width="100px" inline>
           <el-form-item label="场景标题:">
           <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>
           <el-form-item label="场景码:">
           <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>
           </el-form-item>
           <template v-if="!simple">
           <template v-if="!simple">
             <el-form-item label="SN码:">
             <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>
             <el-form-item label="设备类型:">
             <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"
-                  v-for="(type, key) in DeviceTypeDesc"
-                />
+                <el-option :value="Number(key)" :label="type" v-for="(type, key) in DeviceTypeDesc" />
               </el-select>
               </el-select>
             </el-form-item>
             </el-form-item>
             <el-form-item label="拍摄时间:">
             <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>
             <!-- <el-form-item label="绑定账号:">
             <!-- <el-form-item label="绑定账号:">
               <el-input
               <el-input
@@ -69,13 +42,8 @@
     </div>
     </div>
 
 
     <div class="relics-content">
     <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>
         <slot name="table"></slot>
         <el-table-column label="场景标题" v-slot:default="{ row }">
         <el-table-column label="场景标题" v-slot:default="{ row }">
           <a class="link" @click="gotoScene(row, false)">
           <a class="link" @click="gotoScene(row, false)">
@@ -108,7 +76,8 @@
           <TexToolTip :text="row.shootCount || '-'" />
           <TexToolTip :text="row.shootCount || '-'" />
         </el-table-column>
         </el-table-column>
         <el-table-column label="拍摄位置" v-slot:default="{ row }">
         <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>
 
 
         <el-table-column label="状态" v-slot:default="{ row }">
         <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 link type="primary" size="small" @click="gotoScene(row, true)">
               编辑
               编辑
             </el-button>
             </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>
             </el-button>
           </template>
           </template>
@@ -137,15 +101,9 @@
       </el-table>
       </el-table>
     </div>
     </div>
     <div class="pag-layout">
     <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>
   </div>
   </div>
 </template>
 </template>
@@ -197,6 +155,13 @@ const delHandler = async (relicsId: number) => {
 
 
 watch(pageProps, refresh, { deep: true, immediate: true });
 watch(pageProps, refresh, { deep: true, immediate: true });
 onActivated(refresh);
 onActivated(refresh);
+
+const handleTableSelect = (val: any) => {
+  if (props.tableProps && "selectionChange" in props.tableProps) {
+    console.log("selectionChange");
+    props.tableProps.selectionChange(val);
+  }
+};
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">
@@ -205,6 +170,7 @@ onActivated(refresh);
   overflow-y: auto;
   overflow-y: auto;
   padding: 30px;
   padding: 30px;
 }
 }
+
 .pag-layout {
 .pag-layout {
   margin-top: 20px;
   margin-top: 20px;
   display: flex;
   display: flex;
@@ -215,9 +181,11 @@ onActivated(refresh);
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   margin-bottom: 20px;
   margin-bottom: 20px;
+
   .search {
   .search {
     flex: 1;
     flex: 1;
   }
   }
+
   .relics-oper {
   .relics-oper {
     flex: 0 0 100px;
     flex: 0 0 100px;
     text-align: right;
     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"
+}

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/view/step-tree-v2/example/data/7.json


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 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>

binární
src/view/step-tree-v2/example/image/c.png


binární
src/view/step-tree-v2/example/image/g.png


binární
src/view/step-tree-v2/example/image/p.png


binární
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 [
 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: [
     steps: [
       {
       {
-        name: "step1",
         displayName: "Stop All Services",
         displayName: "Stop All Services",
-        subStepsParallel: "True",
+        name: "step1",
         status: "success",
         status: "success",
         steps: [
         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",
             name: "step1_2",
-            displayName: "Stop app2_part1 Services",
-            type: "execution",
-            serviceType: "app2_part1",
-            status: "waiting",
-            serviceTypeParallel: "True",
+            status: "success",
             steps: [
             steps: [
               {
               {
+                action: "stop",
+                displayName: "Stop app2_part1 Services",
+                hosts: [
+                  {
+                    host: "quadpax2",
+                    status: "success",
+                  },
+                ],
                 name: "step1_2_1",
                 name: "step1_2_1",
-                displayName: "Stop app1 Services",
+                serviceType: "app2_part1",
+                serviceTypeParallel: true,
+                status: "success",
                 type: "execution",
                 type: "execution",
-                serviceType: "app1",
-                status: "waiting",
-                serviceTypeParallel: "True",
               },
               },
               {
               {
+                displayName: "Stop app2_part2 Services",
                 name: "step1_2_2",
                 name: "step1_2_2",
-                displayName: "Stop app1 Services",
-                type: "execution",
-                serviceType: "app1",
-                status: "waiting",
-                serviceTypeParallel: "True",
+                status: "success",
                 steps: [
                 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",
                     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",
         name: "step2",
-        displayName: "Stop All Services",
-        subStepsParallel: "True",
-        status: "waiting",
+        status: "success",
         steps: [
         steps: [
           {
           {
-            name: "step2_1",
-            displayName: "Waiting",
-            type: "execution",
-            status: "waiting",
-          },
-          {
-            name: "step2_2",
+            action: "humanWaiting",
             displayName: "Waiting",
             displayName: "Waiting",
+            name: "step2_1",
+            status: "success",
             type: "execution",
             type: "execution",
-            status: "waiting",
           },
           },
         ],
         ],
       },
       },
       {
       {
-        name: "step3",
         displayName: "deploy All Services",
         displayName: "deploy All Services",
-        subStepsParallel: "True",
-        status: "waiting",
+        name: "step3",
+        status: "success",
         steps: [
         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",
             type: "execution",
-            serviceType: "app1",
-            status: "waiting",
-            serviceTypeParallel: "True",
           },
           },
           {
           {
-            name: "step3_2",
+            action: "deploy",
             displayName: "deploy app2_part1 Services",
             displayName: "deploy app2_part1 Services",
-            type: "execution",
+            hosts: [
+              {
+                host: "quadpax2",
+                status: "success",
+              },
+            ],
+            name: "step3_2",
             serviceType: "app2_part1",
             serviceType: "app2_part1",
-            status: "waiting",
-            serviceTypeParallel: "True",
+            serviceTypeParallel: true,
+            status: "success",
+            type: "execution",
           },
           },
         ],
         ],
+        subStepsParallel: true,
       },
       },
       {
       {
-        name: "step4",
         displayName: "start All Services",
         displayName: "start All Services",
-        subStepsParallel: "False",
-        status: "waiting",
+        name: "step4",
+        status: "success",
         steps: [
         steps: [
           {
           {
+            action: "start",
+            displayName: "start app1_part1 Services",
+            hosts: [
+              {
+                host: "quadpax1",
+                status: "success",
+              },
+            ],
             name: "step4_1",
             name: "step4_1",
-            displayName: "start app1 Services",
+            serviceType: "app1_part1",
+            serviceTypeParallel: true,
+            status: "success",
             type: "execution",
             type: "execution",
-            serviceType: "app1",
-            status: "waiting",
-            serviceTypeParallel: "True",
           },
           },
           {
           {
-            name: "step4_2",
+            action: "start",
             displayName: "start app2_part1 Services",
             displayName: "start app2_part1 Services",
-            type: "execution",
+            hosts: [
+              {
+                host: "quadpax2",
+                status: "success",
+              },
+            ],
+            name: "step4_2",
             serviceType: "app2_part1",
             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>
 <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
     <StepTree
       :data="treeData"
       :data="treeData"
       :margin="[15, 15]"
       :margin="[15, 15]"
       :padding="[10, 10]"
       :padding="[10, 10]"
+      :tree-box-margin="[5, 5]"
       :font-size="16"
       :font-size="16"
-      :hostFontSize="10"
-      :hostMargin="[3, 3]"
-      :hostPadding="[3, 3]"
+      :hostFontSize="16"
+      :hostMargin="[10, 5]"
+      :hostPadding="[8, 8]"
       font-family="微软雅黑"
       font-family="微软雅黑"
       :custom-step-style="customStepStyle"
       :custom-step-style="customStepStyle"
       @step-click="stepClickHandler"
       @step-click="stepClickHandler"
@@ -19,47 +28,175 @@
 
 
 <script setup>
 <script setup>
 import data from "./data";
 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) => {
 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 {
     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,
       rectRadius: 2,
       rectBorderWidth: 1,
       rectBorderWidth: 1,
     };
     };
-  } else {
+  }
+  {
     return {
     return {
-      lineColor: "#000",
+      lineColor: "#89beb2",
       lineWidth: 1,
       lineWidth: 1,
-      textColor: "#000",
-      rectBorderColor: "#000",
+      textColor: "#333",
+      rectBorderColor: "coral",
       rectBgColor: "#ffff",
       rectBgColor: "#ffff",
       rectRadius: 2,
       rectRadius: 2,
       rectBorderWidth: 1,
       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) => {
 const stepClickHandler = (step) => {
   console.log(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) => {
 const stepHostClickHandler = (host) => {
   console.log(host);
   console.log(host);
 };
 };
+nextTick(() => {
+  treeWrapRef.value.scrollLeft =
+    (treeWrapRef.value.scrollWidth - treeWrapRef.value.clientWidth) / 2;
+});
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>
-.test {
+.tree-cont-wrap {
   width: 100%;
   width: 100%;
   height: 100%;
   height: 100%;
-
+  margin: 0 auto;
   overflow: 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>
 </style>

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


Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů