瀏覽代碼

Merge branch 'v1.9.0-jm' of http://192.168.0.115:3000/bill/fuse-code into v1.9.0-jm

xzw 8 月之前
父節點
當前提交
d2d1e5f3a4
共有 63 個文件被更改,包括 51838 次插入631 次删除
  1. 1 0
      .env
  2. 1 0
      .gitignore
  3. 1 0
      index.html
  4. 16 0
      offline.html
  5. 2 0
      package.json
  6. 7 0
      pnpm-lock.yaml
  7. 14 0
      public/xfile-viewer/.prettierrc
  8. 247 0
      public/xfile-viewer/index.html
  9. 二進制
      public/xfile-viewer/publish/20231025172413.dcm
  10. 二進制
      public/xfile-viewer/publish/20231025172413.raw
  11. 9084 0
      public/xfile-viewer/publish/lib/cornerstone.js
  12. 3 0
      public/xfile-viewer/publish/lib/cornerstoneMath.min.js
  13. 38415 0
      public/xfile-viewer/publish/lib/cornerstoneTools.js
  14. 2 0
      public/xfile-viewer/publish/lib/cornerstoneWADOImageLoader.bundle.min.js
  15. 3 0
      public/xfile-viewer/publish/lib/cornerstoneWebImageLoader.min.js
  16. 3 0
      public/xfile-viewer/publish/lib/dicomParser.min.js
  17. 2643 0
      public/xfile-viewer/publish/lib/hammer.js
  18. 65 57
      src/api/constant.ts
  19. 7 8
      src/api/floder.ts
  20. 4 6
      src/api/folder-type.ts
  21. 52 39
      src/api/instance.ts
  22. 92 0
      src/api/offline.ts
  23. 8 1
      src/api/scene.ts
  24. 6 3
      src/api/setup.ts
  25. 31 30
      src/api/sys.ts
  26. 22 0
      src/api/user.ts
  27. 11 11
      src/app.vue
  28. 2 8
      src/components/static-preview/index.vue
  29. 12 16
      src/components/static-preview/resource.vue
  30. 3 3
      src/components/tagging/sign.vue
  31. 1 1
      src/env/index.ts
  32. 23 24
      src/hook/notice.ts
  33. 1 0
      src/layout/edit/scene-select.vue
  34. 29 2
      src/layout/scene-list/index.vue
  35. 8 2
      src/layout/show/index.vue
  36. 42 47
      src/layout/show/slide-menu.vue
  37. 17 17
      src/main.ts
  38. 34 9
      src/model/app.vue
  39. 1 0
      src/model/platform.ts
  40. 6 6
      src/router/config.ts
  41. 11 1
      src/router/index.ts
  42. 1 0
      src/sdk/sdk.ts
  43. 62 8
      src/store/floder-type.ts
  44. 2 1
      src/store/floder.ts
  45. 57 1
      src/store/scene.ts
  46. 5 0
      src/style.scss
  47. 5 1
      src/utils/meta.ts
  48. 12 0
      src/utils/params.ts
  49. 51 98
      src/views/fire/index.vue
  50. 58 0
      src/views/fire/info.vue
  51. 61 0
      src/views/folder/fire/index.vue
  52. 58 0
      src/views/folder/fire/info.vue
  53. 172 0
      src/views/folder/floder-view.vue
  54. 38 70
      src/views/folder/index.vue
  55. 114 0
      src/views/folder/modal-floder-view.vue
  56. 4 4
      src/views/record/shot.vue
  57. 100 87
      src/views/record/sign.vue
  58. 1 0
      src/views/tagging/index.vue
  59. 2 3
      src/views/tagging/style-type-select.vue
  60. 61 51
      src/views/view/index.vue
  61. 9 11
      src/views/view/show.vue
  62. 6 0
      src/vite-env.d.ts
  63. 29 5
      vite.config.ts

+ 1 - 0
.env

@@ -0,0 +1 @@
+VITE_LASER_HOST=/

+ 1 - 0
.gitignore

@@ -9,6 +9,7 @@ lerna-debug.log*
 
 node_modules
 dist
+public/__offline/*
 dist-ssr
 *.local
 

+ 1 - 0
index.html

@@ -7,6 +7,7 @@
     <title>案件信息</title>
     <link rel="stylesheet" type="text/css" href="./lib/Cesium/Widgets/CesiumWidget/CesiumWidget.css">
     <link rel="stylesheet" type="text/css" href="./lib/Cesium/Widgets/CesiumWidget/lighter.css">
+    <script>window.offline = false</script>
   </head>
   <body>
     <div id="app"></div>

+ 16 - 0
offline.html

@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>案件信息</title>
+    <link rel="stylesheet" type="text/css" href="./lib/Cesium/Widgets/CesiumWidget/CesiumWidget.css">
+    <link rel="stylesheet" type="text/css" href="./lib/Cesium/Widgets/CesiumWidget/lighter.css">
+    <script>window.offline = true</script>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 2 - 0
package.json

@@ -6,6 +6,7 @@
   "scripts": {
     "dev": "vite",
     "build": "vite build",
+    "build-offline": " vite build ./ offline",
     "preview": "vite preview"
   },
   "dependencies": {
@@ -16,6 +17,7 @@
     "less": "^4.1.3",
     "mitt": "^3.0.0",
     "simaqcore": "^1.2.0",
+    "swiper": "^11.1.15",
     "vite-plugin-mkcert": "^1.10.1",
     "vue": "^3.2.37",
     "vue-cropper": "1.0.2",

+ 7 - 0
pnpm-lock.yaml

@@ -11,6 +11,7 @@ specifiers:
   mitt: ^3.0.0
   sass: ^1.54.3
   simaqcore: ^1.2.0
+  swiper: ^11.1.15
   typescript: ^4.6.4
   vite: ^3.0.0
   vite-plugin-mkcert: ^1.10.1
@@ -28,6 +29,7 @@ dependencies:
   less: 4.1.3
   mitt: 3.0.0
   simaqcore: 1.2.0
+  swiper: 11.1.15
   vite-plugin-mkcert: 1.10.1_vite@3.0.4
   vue: 3.2.37
   vue-cropper: 1.0.2
@@ -1098,6 +1100,11 @@ packages:
     resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
     engines: {node: '>= 0.4'}
 
+  /swiper/11.1.15:
+    resolution: {integrity: sha512-IzWeU34WwC7gbhjKsjkImTuCRf+lRbO6cnxMGs88iVNKDwV+xQpBCJxZ4bNH6gSrIbbyVJ1kuGzo3JTtz//CBw==}
+    engines: {node: '>= 4.7.0'}
+    dev: false
+
   /throttle-debounce/5.0.2:
     resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
     engines: {node: '>=12.22'}

+ 14 - 0
public/xfile-viewer/.prettierrc

@@ -0,0 +1,14 @@
+{
+    "printWidth": 200,
+    "tabWidth": 4,
+    "useTabs": false,
+    "semi": false,
+    "singleQuote": true,
+    "arrowParens": "avoid",
+    "bracketSpacing": true,
+    "disableLanguages": [],
+    "eslintIntegration": false,
+    "stylelintIntegration": false,
+    "tslintIntegration": false,
+    "proseWrap": "preserve"
+}

+ 247 - 0
public/xfile-viewer/index.html

@@ -0,0 +1,247 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="UTF-8" />
+        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+        <title>文档浏览</title>
+        <script src="./publish/lib/hammer.js"></script>
+        <script src="./publish/lib/cornerstone.js"></script>
+        <script src="./publish/lib/cornerstoneMath.min.js"></script>
+        <script src="./publish/lib/cornerstoneWADOImageLoader.bundle.min.js"></script>
+        <script src="./publish/lib/cornerstoneWebImageLoader.min.js"></script>
+        <script src="./publish/lib/cornerstoneTools.js"></script>
+        <script src="./publish/lib/dicomParser.min.js"></script>
+        <style>
+            .disabled {
+                opacity: 0.5;
+                pointer-events: none;
+            }
+            select {
+                outline: none;
+            }
+            html,
+            body {
+                width: 100%;
+                height: 100%;
+            }
+            body {
+                margin: 0;
+                overflow: hidden;
+            }
+            #dicomImage {
+                width: 100%;
+                height: 100%;
+            }
+            #toolbar {
+                position: absolute;
+                left: 50%;
+                top: 24px;
+                padding: 10px;
+                transform: translateX(-50%);
+                background-color: #fff;
+                border: solid 1px #e5e5e5;
+                border-radius: 6px;
+                z-index: 999;
+                display: flex;
+                align-items: center;
+                justify-content: space-around;
+                box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.3);
+                font-size: 12px;
+            }
+            #toolbar > div {
+                cursor: pointer;
+                padding: 0 5px;
+            }
+            #toolbar > div.active {
+                color: #1779ed;
+            }
+            #statbar {
+                cursor: pointer;
+                position: absolute;
+                left: 50%;
+                bottom: 24px;
+                padding: 10px;
+                transform: translateX(-50%);
+                background-color: #fff;
+                border: solid 1px #e5e5e5;
+                border-radius: 6px;
+                z-index: 999;
+                display: none;
+                align-items: center;
+                justify-content: space-around;
+                box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.3);
+                font-size: 12px;
+            }
+        </style>
+    </head>
+    <body>
+        <div id="dicomImage"></div>
+        <div id="toolbar">
+            <div id="Length">
+                标注:
+                <select onchange="tools.annotation(this.value)">
+                    <option value="">请选择</option>
+                    <option value="ArrowAnnotate">箭头</option>
+                    <option value="Length">长度</option>
+                    <option value="Angle">角度</option>
+                    <option value="RectangleRoi">矩形</option>
+                    <option value="EllipticalRoi">椭圆</option>
+                    <option value="FreehandRoi">面积</option>
+                    <option value="Probe">针探</option>
+                </select>
+            </div>
+            <div id="Wwwc" onclick="tools.wwwc()">调整窗宽窗高</div>
+            <div onclick="tools.download()">下载</div>
+        </div>
+        <div id="statbar" onclick="tools.annotation('')">退出</div>
+        <script>
+            const urlParams = key => {
+                let querys = window.location.search.substr(1).split('&')
+                for (let i = 0; i < querys.length; i++) {
+                    let keypair = querys[i].split('=')
+                    if (keypair.length === 2 && keypair[0] === key) {
+                        return keypair[1]
+                    }
+                }
+                return ''
+            }
+
+            const tools = {
+                name: '',
+                set active(name) {
+                    document.querySelectorAll('#toolbar >div').forEach(el => el.classList.remove('active'))
+                    if (name) {
+                        this.name = name
+                        document.getElementById(name).classList.add('active')
+                    } else {
+                        this.name = ''
+                    }
+                },
+                download() {
+                    var link = document.createElement('a')
+                    link.download = 'preview.png'
+                    link.href = document.querySelector('canvas').toDataURL('image/png').replace('image/png', 'image/octet-stream')
+                    link.click()
+                },
+                wwwc() {
+                    const name = 'Wwwc'
+                    if (this.name == name) {
+                        cornerstoneTools.setToolActive('Pan', {
+                            mouseButtonMask: 1,
+                        })
+                        this.active = ''
+                    } else {
+                        cornerstoneTools.setToolActive('Wwwc', {
+                            mouseButtonMask: 1,
+                        })
+                        this.active = name
+                    }
+                },
+                annotation(name) {
+                    const wwwc = document.getElementById('Wwwc')
+                    const leave = document.getElementById('statbar')
+                    const select = document.querySelector('select')
+                    if (name) {
+                        if (this.name == 'Wwwc') {
+                            this.wwwc()
+                        }
+                        wwwc.classList.add('disabled')
+                        leave.style.display = 'flex'
+                        select.disabled = true
+                        cornerstoneTools.setToolActive(name, { mouseButtonMask: 1 })
+                    } else {
+                        wwwc.classList.remove('disabled')
+                        leave.style.display = 'none'
+                        select.disabled = false
+                        document.querySelector('select').value = ''
+                        cornerstoneTools.setToolPassive(this.name)
+                        cornerstoneTools.setToolActive('Pan', {
+                            mouseButtonMask: 1,
+                        })
+                    }
+                    this.name = name
+                },
+            }
+            // 注册并挂载cornerstone及其cornerstoneTools,固定操作
+            cornerstoneTools.external.cornerstone = cornerstone
+            cornerstoneTools.external.cornerstoneMath = cornerstoneMath
+            cornerstoneTools.external.Hammer = Hammer
+            cornerstoneWADOImageLoader.external.dicomParser = dicomParser
+            cornerstoneWADOImageLoader.external.cornerstone = cornerstone
+            var file = urlParams('file')
+            if (!file) {
+                alert('文档不能为空')
+            } else {
+                var imageId = 'wadouri: ' + decodeURIComponent(file) //http://192.168.0.11:80/20231025172413.dcm'
+                // 初始化cornerstoneTools工具
+                cornerstoneTools.init([
+                    {
+                        moduleName: 'globalConfiguration',
+                        configuration: {
+                            showSVGCursors: true,
+                        },
+                    },
+                    {
+                        moduleName: 'segmentation',
+                        configuration: {
+                            outlineWidth: 2,
+                        },
+                    },
+                ])
+
+                // 获取要用于加载图片的div区域
+                var element = document.getElementById('dicomImage')
+                //激活获取到的用于图片加载的区域
+                cornerstone.enable(element)
+                //   // 从cornerstoneTools库中获取窗宽,窗高工具
+                //   const WwwcTool = cornerstoneTools.WwwcTool;
+                //   //添加获取到的窗宽,窗高工具
+                //   cornerstoneTools.addTool(WwwcTool);
+                //   // 绑定工具操作功能到鼠标左键
+                //   cornerstoneTools.setToolActive("Wwwc", {
+                //     mouseButtonMask: 1,
+                //   });
+                //使用loadAndCacheImage()方法加载并缓存图片,然后使用displayImage()方法显示图片。
+                cornerstone.loadAndCacheImage(imageId).then(function (image) {
+                    cornerstone.displayImage(element, image)
+                })
+
+                /* 平移、缩放  */
+                const PanTool = cornerstoneTools.PanTool
+                cornerstoneTools.addTool(PanTool)
+                cornerstoneTools.setToolActive('Pan', { mouseButtonMask: 1 })
+
+                const ZoomMouseWheelTool = cornerstoneTools.ZoomMouseWheelTool
+                cornerstoneTools.addTool(ZoomMouseWheelTool)
+                cornerstoneTools.setToolActive('ZoomMouseWheel', { mouseButtonMask: 1 })
+
+                /*  标注工具 */
+                // 长度标注
+                const LengthTool = cornerstoneTools.LengthTool
+                cornerstoneTools.addTool(LengthTool)
+
+                const AngleTool = cornerstoneTools.AngleTool
+                cornerstoneTools.addTool(AngleTool)
+
+                const ArrowAnnotateTool = cornerstoneTools.ArrowAnnotateTool
+                cornerstoneTools.addTool(ArrowAnnotateTool)
+
+                const RectangleRoiTool = cornerstoneTools.RectangleRoiTool
+                cornerstoneTools.addTool(RectangleRoiTool)
+
+                const EllipticalRoiTool = cornerstoneTools.EllipticalRoiTool
+                cornerstoneTools.addTool(EllipticalRoiTool)
+
+                const FreehandRoiTool = cornerstoneTools.FreehandRoiTool
+                cornerstoneTools.addTool(FreehandRoiTool)
+
+                const ProbeTool = cornerstoneTools.ProbeTool
+                cornerstoneTools.addTool(ProbeTool)
+
+                const WwwcTool = cornerstoneTools.WwwcTool
+                //添加获取到的窗宽,窗高工具
+                cornerstoneTools.addTool(WwwcTool)
+            }
+        </script>
+    </body>
+</html>

二進制
public/xfile-viewer/publish/20231025172413.dcm


二進制
public/xfile-viewer/publish/20231025172413.raw


File diff suppressed because it is too large
+ 9084 - 0
public/xfile-viewer/publish/lib/cornerstone.js


File diff suppressed because it is too large
+ 3 - 0
public/xfile-viewer/publish/lib/cornerstoneMath.min.js


File diff suppressed because it is too large
+ 38415 - 0
public/xfile-viewer/publish/lib/cornerstoneTools.js


File diff suppressed because it is too large
+ 2 - 0
public/xfile-viewer/publish/lib/cornerstoneWADOImageLoader.bundle.min.js


File diff suppressed because it is too large
+ 3 - 0
public/xfile-viewer/publish/lib/cornerstoneWebImageLoader.min.js


File diff suppressed because it is too large
+ 3 - 0
public/xfile-viewer/publish/lib/dicomParser.min.js


File diff suppressed because it is too large
+ 2643 - 0
public/xfile-viewer/publish/lib/hammer.js


+ 65 - 57
src/api/constant.ts

@@ -1,3 +1,5 @@
+import {namespace} from '@/env'
+
 export enum ResCode {
   TOKEN_INVALID = 4008,
   SUCCESS = 0
@@ -12,93 +14,99 @@ export const UPLOAD_HEADS = {
   'Content-Type': 'multipart/form-data'
 }
 
-export const CASE_INFO = `/fusion/case/getInfo`
+
+export const USER_INFO = `${namespace}/web/user/getUserInfo`;
+
+export const CASE_INFO = `${namespace}/case/getInfo`
+export const CASE_FIRE_INFO = `${namespace}/caseInquestInfo/info`
+
 // 校验密码
-export const AUTH_PWD = `/fusion/web/fireProject/getDetailWithoutAuth`
+export const AUTH_PWD = `${namespace}/web/fireProject/getDetailWithoutAuth`
 
 // 融合模型列表
-export const FUSE_MODEL_LIST = `/fusion/caseFusion/list`
-export const FUSE_INSERT_MODEL = `/fusion/caseFusion/add`
-export const FUSE_UPDATE_MODEL = `/fusion/caseFusion/update`
-export const FUSE_DELETE_MODEL = `/fusion/caseFusion/delete`
+export const FUSE_MODEL_LIST = `${namespace}/caseFusion/list`
+export const FUSE_INSERT_MODEL = `${namespace}/caseFusion/add`
+export const FUSE_UPDATE_MODEL = `${namespace}/caseFusion/update`
+export const FUSE_DELETE_MODEL = `${namespace}/caseFusion/delete`
 // 场景列表
-export const SCENE_LIST_ALL = `/fusion/api/scene/list`
-export const MODEL_LIST = `/fusion/case/sceneList`
-export const MODEL_SIGN = `/fusion/model/getInfo`
+export const SCENE_LIST_ALL = `${namespace}/api/scene/list`
+export const MODEL_LIST = `${namespace}/case/sceneList`
+export const MODEL_SIGN = `${namespace}/model/getInfo`
+export const SYNC_INFO = `${namespace}/caseLive/getTakeLookRoom`;
 
 // 标签列表
-export const TAGGING_LIST = `/fusion/caseTag/allList`
-export const INSERT_TAGGING = `/fusion/caseTag/add`
-export const UPDATE_TAGGING = `/fusion/caseTag/update`
-export const DELETE_TAGGING = `/fusion/caseTag/delete`
+export const TAGGING_LIST = `${namespace}/caseTag/allList`
+export const INSERT_TAGGING = `${namespace}/caseTag/add`
+export const UPDATE_TAGGING = `${namespace}/caseTag/update`
+export const DELETE_TAGGING = `${namespace}/caseTag/delete`
 
 // 路线列表
-export const PATH_LIST = `/fusion/path/allList`
-export const INSERT_PATH = `/fusion/path/add`
-export const UPDATE_PATH = `/fusion/path/update`
-export const DELETE_PATH = `/fusion/path/delete`
+export const PATH_LIST = `${namespace}/path/allList`
+export const INSERT_PATH = `${namespace}/path/add`
+export const UPDATE_PATH = `${namespace}/path/update`
+export const DELETE_PATH = `${namespace}/path/delete`
 
 // 标签放置列表
-export const TAGGING_POINT_LIST = `/fusion/caseTagPoint/allList`
-export const INSERT_TAGGING_POINT = `/fusion/caseTagPoint/place`
-export const UPDATE_TAGGING_POINT = `/fusion/caseTagPoint/update`
-export const DELETE_TAGGING_POINT = `/fusion/caseTagPoint/delete`
+export const TAGGING_POINT_LIST = `${namespace}/caseTagPoint/allList`
+export const INSERT_TAGGING_POINT = `${namespace}/caseTagPoint/place`
+export const UPDATE_TAGGING_POINT = `${namespace}/caseTagPoint/update`
+export const DELETE_TAGGING_POINT = `${namespace}/caseTagPoint/delete`
 
 // 标签样式类型列表
-export const TAGGING_STYLE_LIST = '/fusion/edit/hotIcon/list'
-export const INSERT_TAGGING_STYLE = '/fusion/edit/hotIcon/add'
-export const DELETE_TAGGING_STYLE = '/fusion/edit/hotIcon/delete'
+export const TAGGING_STYLE_LIST = `${namespace}/edit/hotIcon/list`
+export const INSERT_TAGGING_STYLE = `${namespace}/edit/hotIcon/add`
+export const DELETE_TAGGING_STYLE = `${namespace}/edit/hotIcon/delete`
 
 // 测量线
-export const MESASURE_LIST = `/fusion/fusionMeter/allList`
-export const INSERT_MESASURE = `/fusion/fusionMeter/add`
-export const UPDATE_MESASURE = `/fusion/fusionMeter/updateMeter`
-export const DELETE_MESASURE = `/fusion/fusionMeter/delete`
+export const MESASURE_LIST = `${namespace}/fusionMeter/allList`
+export const INSERT_MESASURE = `${namespace}/fusionMeter/add`
+export const UPDATE_MESASURE = `${namespace}/fusionMeter/updateMeter`
+export const DELETE_MESASURE = `${namespace}/fusionMeter/delete`
 
 // 导览
-export const GUIDE_LIST = `/fusion/fusionGuide/allList`
-export const INSERT_GUIDE = `/fusion/fusionGuide/add`
-export const UPDATE_GUIDE = `/fusion/fusionGuide/update`
-export const DELETE_GUIDE = `/fusion/fusionGuide/delete`
+export const GUIDE_LIST = `${namespace}/fusionGuide/allList`
+export const INSERT_GUIDE = `${namespace}/fusionGuide/add`
+export const UPDATE_GUIDE = `${namespace}/fusionGuide/update`
+export const DELETE_GUIDE = `${namespace}/fusionGuide/delete`
 
 // 导览路线
-export const GUIDE_PATH_LIST = `/fusion/fusionGuidePath/allList`
-export const INSERT_GUIDE_PATH = `/fusion/fusionGuidePath/add`
-export const UPDATE_GUIDE_PATH = `/fusion/fusionGuidePath/update`
-export const DELETE_GUIDE_PATH = `/fusion/fusionGuidePath/delete`
+export const GUIDE_PATH_LIST = `${namespace}/fusionGuidePath/allList`
+export const INSERT_GUIDE_PATH = `${namespace}/fusionGuidePath/add`
+export const UPDATE_GUIDE_PATH = `${namespace}/fusionGuidePath/update`
+export const DELETE_GUIDE_PATH = `${namespace}/fusionGuidePath/delete`
 
 // 屏幕录制
-export const RECORD_LIST = `/fusion/caseVideoFolder/allList`
-export const RECORD_STATUS = `/fusion/caseVideo/uploadAddVideoProgress`
-export const INSERT_RECORD = `/fusion/caseVideo/uploadAddVideo`
-export const MERGE_RECORD = `/fusion/caseVideo/uploadAddVideo`
-export const UPDATE_RECORD = `/fusion/caseVideoFolder/updateNameOrSort`
-export const DELETE_RECORD = `/fusion/caseVideoFolder/delete`
+export const RECORD_LIST = `${namespace}/caseVideoFolder/allList`
+export const RECORD_STATUS = `${namespace}/caseVideo/uploadAddVideoProgress`
+export const INSERT_RECORD = `${namespace}/caseVideo/uploadAddVideo`
+export const MERGE_RECORD = `${namespace}/caseVideo/uploadAddVideo`
+export const UPDATE_RECORD = `${namespace}/caseVideoFolder/updateNameOrSort`
+export const DELETE_RECORD = `${namespace}/caseVideoFolder/delete`
 
 // 录制片段
-export const RECORD_FRAGMENT_LIST = `/fusion/caseVideo/allList`
-export const DELETE_RECORD_FRAGMENT = `/fusion/caseVideo/delete`
+export const RECORD_FRAGMENT_LIST = `${namespace}/caseVideo/allList`
+export const DELETE_RECORD_FRAGMENT = `${namespace}/caseVideo/delete`
 
 // 视图提取
-export const VIEW_LIST = `/fusion/caseView/allList`
-export const INSERT_VIEW = `/fusion/caseView/add`
-export const UPDATE_VIEW = `/fusion/caseView/updateNameOrSort`
-export const DELETE_VIEW = `/fusion/caseView/delete`
+export const VIEW_LIST = `${namespace}/caseView/allList`
+export const INSERT_VIEW = `${namespace}/caseView/add`
+export const UPDATE_VIEW = `${namespace}/caseView/updateNameOrSort`
+export const DELETE_VIEW = `${namespace}/caseView/delete`
 
-export const GET_SETTING = `/fusion/caseSettings/info`
-export const UPDATE_SETTING = `/fusion/caseSettings/saveOrUpdate`
+export const GET_SETTING = `${namespace}/caseSettings/info`
+export const UPDATE_SETTING = `${namespace}/caseSettings/saveOrUpdate`
 
 // 卷宗类型
-export const FOLDER_TYPE_LIST = `/fusion/caseFilesType/allList`
+export const FOLDER_TYPE_LIST = `${namespace}/caseFilesType/allList`
 
 // 卷宗
-export const FLODER_LIST = `/fusion/caseFiles/allList`
+export const FLODER_LIST = `${namespace}/caseFiles/allList`
 
 // 文件上传
-export const UPLOAD_FILE = `/fusion/upload/file`
+export const UPLOAD_FILE = `${namespace}/upload/file`
 
 // 素材库分页
-export const MATERIAL_PAG = `/fusion/material/allList`
-export const ADD_MATERIAL = `/fusion/material/add`
-export const DEL_MATERIAL = `/fusion/material/del`
-export const MATERIAL_GROUP_LIST = `/fusion/material/allList`
+export const MATERIAL_PAG = `${namespace}/material/allList`
+export const ADD_MATERIAL = `${namespace}/material/add`
+export const DEL_MATERIAL = `${namespace}/material/del`
+export const MATERIAL_GROUP_LIST = `${namespace}/material/allList`

+ 7 - 8
src/api/floder.ts

@@ -1,6 +1,7 @@
 import { params } from '@/env'
 import { FLODER_LIST } from './constant'
 import { addUnsetResErrorURLS,axios } from './instance'
+import {namespace} from '@/env'
 
 export interface Floder {
   filesId:	number,
@@ -13,23 +14,23 @@ export interface Floder {
 export type Floders = Floder[]
 
 
-addUnsetResErrorURLS('/fusion/caseInquest/downDocx', '/fusion/caseExtractDetail/downDocx', '/fusion/caseImg/getFfmpegImage')
+addUnsetResErrorURLS(`${namespace}/caseInquest/downDocx`, `${namespace}/caseExtractDetail/downDocx`, `${namespace}/caseImg/getFfmpegImage`)
 export const fetchFloders = async () => {
   const floders = await axios.get<Floders>(FLODER_LIST, { params: { caseId: params.caseId } })
   const otherFloders = [{
     filesId: 88,
     filesTypeId: 100,
     filesTitle: '勘验笔录',
-    ex: '/fusion/caseInquest/info',
-    bex: '/fusion/caseInquest/downDocx'
+    ex: `${namespace}/caseInquest/info`,
+    bex: `${namespace}/caseInquest/downDocx`
   }, {
     filesId: 89,
     filesTypeId: 100,
     filesTitle: '提取清单',
-    ex: '/fusion/caseExtractDetail/info',
-    bex: '/fusion/caseExtractDetail/downDocx'
+    ex: `${namespace}/caseExtractDetail/info`,
+    bex: `${namespace}/caseExtractDetail/downDocx`
   }, ]
-  const res = await axios.get('/fusion/caseImg/getFfmpegImage', {params: {caseId: params.caseId}})
+  const res = await axios.get(`${namespace}/caseImg/getFfmpegImage`, {params: {caseId: params.caseId}})
   if (res.data.data.length) {
     res.data.data.forEach((item:any, ndx: any) => {
       floders.push({
@@ -53,7 +54,5 @@ export const fetchFloders = async () => {
       })
     }
   }))
-
-
   return floders
 }

+ 4 - 6
src/api/folder-type.ts

@@ -3,17 +3,15 @@ import axios from './instance'
 
 export interface FloderType {
   filesTypeId: number,
-  filesTypeName: string
+  filesTypeName: string,
+  parentId: number,
+  modalShow?: boolean
+  flatShow?: boolean
 }
 
 export type FloderTypes = FloderType[]
 
-
 export const fetchFloderTypes = async () => {
   const types =await axios.get<FloderTypes>(FOLDER_TYPE_LIST)
-  types.push({
-    filesTypeId: 100,
-    filesTypeName: '其他',
-  })
   return types
 }

+ 52 - 39
src/api/instance.ts

@@ -1,11 +1,12 @@
-import { axiosFactory } from './setup'
-import { Message } from 'bill/index'
-import { showLoad, hideLoad } from '@/utils'
-import * as URL from './constant'
-import { ResCode, ResCodeDesc } from './constant'
-import { appBackRoot, appType, baseURL, params } from '@/env'
+import { axiosFactory } from "./setup";
+import { Message } from "bill/index";
+import { showLoad, hideLoad } from "@/utils";
+import * as URL from "./constant";
+import { ResCode, ResCodeDesc } from "./constant";
+import { appBackRoot, appType, baseURL, params } from "@/env";
+import GAxios from "axios";
 
-const instance = axiosFactory()
+const instance = axiosFactory();
 
 export const {
   axios,
@@ -22,50 +23,62 @@ export const {
   setDefaultURI,
   addHook,
   delHook,
-  setHook
-} = instance
+  setHook,
+} = instance;
 
 const gotoLogin = () => {
-  const loginHref = import.meta.env.DEV ? 'http://localhost:5174' : appBackRoot[params.app]
-  location.href = loginHref + '?redirect=' + escape(location.href) + '#/login'
-}
+  if (import.meta.env.DEV) {
+    GAxios.post("/service/manage/login", {
+      password: "MRinIEn3ExMjM0NTY=Q5Lm39urQWzN7k4oCG",
+      userName: "super-admin",
+      username: "super-admin",
+    }).then((res) => {
+      setToken(res.data.data.token)
+      setTimeout(() => location.reload())
+    });
+  }
+  throw "123";
+  // const loginHref = `http://192.168.0.25/admin/#/login`
+  // location.href = loginHref + '?redirect=' + escape(location.href)
+};
 
-addReqErrorHandler(err => {
+addReqErrorHandler((err) => {
   // Message.error(err.message)
-  console.error(err)
-  hideLoad()
-  // gotoLogin()
-})
+  console.error(err);
+  hideLoad();
+  gotoLogin();
+});
 
-addResErrorHandler(
-  (response, data) => {
-    if (response && response.status !== 200) {
-      Message.error(response.statusText)
-    } else if (data) {
-      const msg = data.code && ResCodeDesc[data.code] ? ResCodeDesc[data.code] : (data?.message || data?.msg)
-      if (data.code === ResCode.TOKEN_INVALID) {
-        gotoLogin()
-      } else {
-        Message.error(msg)
-      }
+addResErrorHandler((response, data) => {
+  if (response && response.status !== 200) {
+    Message.error(response.statusText);
+  } else if (data) {
+    const msg =
+      data.code && ResCodeDesc[data.code]
+        ? ResCodeDesc[data.code]
+        : data?.message || data?.msg;
+    if (data.code === ResCode.TOKEN_INVALID) {
+      gotoLogin();
+    } else {
+      Message.error(msg);
     }
   }
-)
+});
 
-addHook({ 
+addHook({
   before: (config) => {
     if (config.url !== URL.RECORD_STATUS) {
-      showLoad()
+      showLoad();
     }
-  }, 
+  },
   after: (config) => {
     if (!config || config.url !== URL.RECORD_STATUS) {
-      hideLoad()
+      hideLoad();
     }
-  } 
-})
-
-setDefaultURI(baseURL)
-params.token && setToken(params.token)
+  },
+});
 
-export default axios
+setDefaultURI(baseURL);
+const token = params.token || localStorage.getItem('token')
+token && setToken(token);
+export default axios;

+ 92 - 0
src/api/offline.ts

@@ -0,0 +1,92 @@
+import { params as envParams } from "@/env";
+import { paramsToStr, strToParams } from "@/utils/params";
+import { AxiosInstance } from "axios";
+import {namespace} from '@/env'
+import Axios from 'axios'
+
+export const setOfflineAxios = (axios: AxiosInstance) => {
+  const data: {[key in string]: any} = {}
+  const prev = import.meta.env.DEV ? '__offline/' : ''
+  Axios.get(`./${prev}package/data.json`, {headers: { Accept: "application/json"}}).then(res => {
+    Object.assign(data, res.data)
+    ;(window as any).offlineData = data
+  })
+
+  // 流接口
+  const files = {
+    [`${namespace}/caseExtractDetail/downDocx`]: './package/resource/caseExtractDetail.doc',
+    [`${namespace}/caseInquest/downDocx`]: './package/resource/caseInquest.doc',
+  } as any
+
+
+  // 添加请求拦截器
+  axios.interceptors.request.use(
+    async function (config) {
+      const params = {...config.params}
+      if (envParams.caseId) {
+        params.caseId = envParams.caseId
+      }
+      let item = data[config.url!+ paramsToStr(params)] 
+      if (!item) {
+        delete params.caseId
+        item = data[config.url!+ paramsToStr(params)] 
+      }
+
+      if (item) {
+        throw {
+          isFakeResponse: true,
+            config,
+            response: {
+              data: item,
+              status: 200,
+              statusText: 'OK',
+              headers: {},
+              config: config,
+            }
+        }
+      } else if (files[config.url!]) {
+        const res = await Axios.get(files[config.url!], {responseType: 'blob'})
+        throw {
+          isFakeResponse: true,
+          response: {
+            data: res.data,
+            status: 200,
+            statusText: 'OK',
+            headers: {},
+            config: config,
+          },
+        }
+      } else {
+        console.error(config.url, '未在离线包中!')
+      }
+      return config
+    },
+    function (error) {
+      // 对请求错误做些什么
+      return Promise.reject(error);
+    }
+  );
+
+
+  // 添加响应拦截器
+  axios.interceptors.response.use(
+    function (response) {
+      if (!files[response.config.url!]) {
+        console.error(response.config.url + paramsToStr(response.config.params), '正在添加到离线包中!')
+        data[response.config.url+ paramsToStr(response.config.params)!] = response.data
+      }
+      // 对响应数据做点什么
+      return response;
+    },
+    err => {
+      if (err.isFakeResponse) {
+        return Promise.resolve(err.response);
+      }
+    }
+  );
+
+  (window as any).proxyData = () => {
+    console.log(data)
+    console.log(JSON.stringify(data))
+  };
+}

+ 8 - 1
src/api/scene.ts

@@ -1,5 +1,5 @@
 import axios from './instance'
-import { MODEL_LIST, MODEL_SIGN, SCENE_LIST_ALL } from './constant'
+import { MODEL_LIST, MODEL_SIGN, SCENE_LIST_ALL, SYNC_INFO } from './constant'
 import { params } from '@/env'
 
 export enum SceneStatus {
@@ -38,6 +38,7 @@ export interface Scene {
   isLaser: boolean
   modelDateType: string
   modelGlbUrl: string
+  raw: any,
   model3dgsUrl: string;
   modelShpUrl: string;
   modelId: number
@@ -62,6 +63,12 @@ const toLocalScene = (scene: Scene) => ({
   name: scene.name || scene.sceneName || scene.modelTitle,
 })
 
+
+
+export const getSyncSceneInfo = async () => {
+  return (await axios.post<string>(SYNC_INFO, { caseId: params.caseId }));
+};
+
 export const fetchScenes = async () => {
   const scenes = await axios.get<Scenes>(MODEL_LIST, { params: { caseId: params.caseId } })
   return scenes.map(toLocalScene)

+ 6 - 3
src/api/setup.ts

@@ -1,5 +1,6 @@
 import Axios from 'axios'
 import { ResCode } from './constant'
+import { setOfflineAxios } from './offline'
 
 import type { AxiosResponse, AxiosRequestConfig } from 'axios'
 
@@ -14,7 +15,7 @@ export type Hook = {
 export const axiosFactory = () => {
   const axiosRaw = Axios.create()
   const axiosConfig = {
-    token: localStorage.getItem('fuseCodeToken'),
+    token: localStorage.getItem('token'),
     unTokenSet: [] as string[],
     unReqErrorSet: [] as string[],
     unResErrorSet: [] as string[],
@@ -65,11 +66,11 @@ export const axiosFactory = () => {
 
   const getToken = () => axiosConfig.token
   const setToken = (token: string) => {
-    localStorage.setItem('fuseCodeToken', token)
+    localStorage.setItem('token', token)
     axiosConfig.token = token
   }
   const delToken = () => {
-    localStorage.removeItem('fuseCodeToken')
+    localStorage.removeItem('token')
     axiosConfig.token = null
   }
   
@@ -148,6 +149,8 @@ export const axiosFactory = () => {
       return config
     }
   )
+  
+  offline && setOfflineAxios(axiosRaw)
 
   axiosRaw.interceptors.response.use(
     (response: AxiosResponse<ResData<any>>) => {

+ 31 - 30
src/api/sys.ts

@@ -1,4 +1,4 @@
-import { UPLOAD_FILE, UPLOAD_HEADS, CASE_INFO, AUTH_PWD } from "./constant";
+import { UPLOAD_FILE, UPLOAD_HEADS, CASE_INFO, AUTH_PWD, CASE_FIRE_INFO } from "./constant";
 import { axios } from "./instance";
 import { jsonToForm } from "@/utils";
 import { params } from "@/env";
@@ -30,42 +30,43 @@ export enum FireStatus {
 }
 
 export type FireProject = {
-  accidentDate: string;
-  createTime: string;
-  creatorDeptId: string;
-  caseId: number;
-  creatorId: string;
-  creatorName: string;
-  deptId: string;
-  editTime: string;
-  editorId: string;
-  editorName: string;
-  fireReason: string;
-  id: string;
-  isTeached: number;
-  organizerDeptName: string;
-  organizerUsers: string;
-  projectAddress: string;
-  projectName: string;
-  projectSite: string;
-  projectSiteCode: string;
-  projectSn: string;
-  
-  status: FireStatus;
-  statusDesc: string;
-  updateTime: string;
-  isDelete?: number;
+  "id": number,
+  "caseId": number,
+  "commandTime": string,
+  "alarmTime": string,
+  "alarmName": string,
+  "inquestDept": string,
+  "assignDept": string,
+  "assignType": string,
+  "times": string[],
+  "inquestAddress": string,
+  "tbStatus": number,
+  "createTime": string,
+  "updateTime": string
 };
 
 export interface Case {
   caseTitle: string;
-  latAndLong: string,
-  mapUrl: string
+  latAndLong: string;
+  mapUrl: string;
+
+  caseNum: string;
+  caseCategory: string;
+  caseRegion: string;
+  caseAddress: string;
+  homicideCase: number;
+  criminalCase: number;
   tmProject?: FireProject;
 }
 
-export const getCaseInfo = () =>
-  axios.get<Case>(CASE_INFO, { params: { caseId: params.caseId } });
+export const getCaseInfo = async () => {
+  const [caseInfo, fireInfo] = await Promise.all([
+    axios.get<Case>(CASE_INFO, { params: { caseId: params.caseId } }),
+    axios.get<FireProject>(CASE_FIRE_INFO, { params: { caseId: params.caseId } })
+  ])
+  caseInfo.tmProject = fireInfo
+  return caseInfo
+}
 
 // 校验密码
 export const authSharePassword = (randCode: string) =>

+ 22 - 0
src/api/user.ts

@@ -0,0 +1,22 @@
+import { USER_INFO } from "./constant"
+import axios from "./instance"
+
+export type UserInfo = {
+  avatar: string;
+  deptId: string;
+  deptName: string;
+  id: string;
+  deptLevel: number;
+  departmentId: string;
+  cameraSns: string[];
+  status: 1 | 0;
+  isAdmin: 1 | 0;
+  permsList: string[];
+  nickName: string;
+  roleId: string;
+  password: string;
+  userName: string;
+};
+export const getUserInfo = () => {
+  return axios.get<UserInfo>(USER_INFO)
+}

+ 11 - 11
src/app.vue

@@ -53,17 +53,17 @@ const stopWatch = watch(
     }
 
     // 单页面 非自己查看需要密码校验
-    if (currentLayout.value === RoutesName.show && !params.share) {
-      inputPwd.value = true;
-      await new Promise<void>((resolve) => {
-        const stopInputWatch = watchEffect(() => {
-          if (!inputPwd.value) {
-            stopInputWatch();
-            resolve();
-          }
-        });
-      });
-    }
+    // if (currentLayout.value === RoutesName.show && !params.share) {
+    //   inputPwd.value = true;
+    //   await new Promise<void>((resolve) => {
+    //     const stopInputWatch = watchEffect(() => {
+    //       if (!inputPwd.value) {
+    //         stopInputWatch();
+    //         resolve();
+    //       }
+    //     });
+    //   });
+    // }
 
     params.share = true;
     await refreshCase();

+ 2 - 8
src/components/static-preview/index.vue

@@ -23,17 +23,11 @@
 <script lang="ts">
 import { defineComponent, PropType, ref } from "vue";
 import Sign from "./sign.vue";
-
-export enum MediaType {
-  video = "video",
-  img = "img",
-  web = "web",
-  audio = "audio",
-}
+import { MetaType } from "@/utils";
 
 export type MediaItem = {
   url: Blob | string;
-  type?: MediaType;
+  type?: MetaType;
 };
 
 export const Preview = defineComponent({

+ 12 - 16
src/components/static-preview/resource.vue

@@ -1,12 +1,16 @@
 <template>
-  <video v-if="type === MediaType.video" controls autoplay playsinline webkit-playsinline>
+  <video v-if="type === MetaType.video" controls autoplay playsinline webkit-playsinline>
     <source :src="url" />
   </video>
-  <iframe v-else-if="type === MediaType.web" :src="url"></iframe>
-  <img :src="url" v-if="type === MediaType.img" />
+  <iframe v-else-if="type === MetaType.other" :src="url"></iframe>
+  <iframe
+    v-else-if="type === MetaType.xfile"
+    :src="`./xfile-viewer/index.html?file=${url}&time=${Date.now()}`"
+  ></iframe>
+  <img :src="url" v-if="type === MetaType.image" />
   <audio
     :src="url"
-    v-if="type === MediaType.audio"
+    v-if="type === MetaType.audio"
     controls
     autoplay
     playsinline
@@ -16,33 +20,25 @@
 
 <script lang="ts" setup>
 import { getResource } from "@/env";
-import { MediaType } from "./index.vue";
 import { computed } from "vue";
 import { getUrlType, MetaType } from "@/utils/meta";
 
-const props = defineProps<{ data: string | Blob | File; type?: MediaType }>();
+const props = defineProps<{ data: string | Blob | File; type?: MetaType }>();
 
 const url = computed(() =>
   typeof props.data === "string"
     ? getResource(props.data)
     : URL.createObjectURL(props.data)
 );
-console.log(url.value);
+
 const type = computed(() => {
   if (props.type) {
     return props.type;
   } else if (props.data instanceof File || typeof props.data === "string") {
     const d = props.data instanceof File ? props.data.name : props.data;
-    const otype = getUrlType(d);
-    const map = {
-      [MetaType.other]: MediaType.web,
-      [MetaType.audio]: MediaType.audio,
-      [MetaType.image]: MediaType.img,
-      [MetaType.video]: MediaType.video,
-    };
-    return map[otype];
+    return getUrlType(d);
   } else {
-    return MediaType.web;
+    return MetaType.other;
   }
 });
 </script>

+ 3 - 3
src/components/tagging/sign.vue

@@ -55,9 +55,9 @@ import { computed, ref, watchEffect, watch, onUnmounted } from "vue";
 import { router, RoutesName } from "@/router";
 import UIBubble from "bill/components/bubble/index.vue";
 import Images from "@/views/tagging/images.vue";
-import Preview, { MediaType } from "../static-preview/index.vue";
+import Preview from "../static-preview/index.vue";
 import { getTaggingStyle, getFuseModel } from "@/store";
-import { getFileUrl } from "@/utils";
+import { getFileUrl, MetaType } from "@/utils";
 import { sdk } from "@/sdk";
 import { custom, getResource } from "@/env";
 import { useViewStack } from "@/hook";
@@ -109,7 +109,7 @@ const pullIndex = ref(-1);
 const isHover = ref(false);
 const queryItems = computed(() =>
   props.tagging.images.map((image) => ({
-    type: MediaType.img,
+    type: MetaType.image,
     url: getResource(getFileUrl(image)),
   }))
 );

+ 1 - 1
src/env/index.ts

@@ -2,7 +2,7 @@ import { stackFactory, flatStacksValue, strToParams } from '@/utils'
 import { reactive, ref } from 'vue'
 
 import type { FuseModel, TaggingPosition, View } from '@/store'
-
+export const namespace = '/fusion'
 export const viewModeStack = stackFactory(ref<'full' | 'auto'>('auto'))
 export const showToolbarStack = stackFactory(ref<boolean>(false))
 export const showHeadBarStack = stackFactory(ref<boolean>(true))

+ 23 - 24
src/hook/notice.ts

@@ -1,58 +1,57 @@
 import { params } from "@/env";
 import { ref } from "vue";
-import type {ResData} from '@/api'
+import type { ResData } from "@/api";
+import { namespace } from "@/env";
 
-console.log('scoke 测试')
-const socketUrl = `wss://test-mix3d.4dkankan.com/fusion/ws/${params.caseId}`;
+const socketUrl = `wss://test-mix3d.4dkankan.com${namespace}/ws/${params.caseId}`;
 
 let websocket: WebSocket | null = null;
 function createWebSocket() {
   websocket = new WebSocket(socketUrl);
-  websocket.onopen = function() {
-    console.log('WebSocket 连接已打开');
+  websocket.onopen = function () {
+    console.log("WebSocket 连接已打开");
   };
 
-  websocket.onmessage = function(event) {
+  websocket.onmessage = function (event) {
     try {
-      const res = JSON.parse(event.data) as {command: string, content: ResData<any>}
-      switch(res.command) {
-        case 'ping':
+      const res = JSON.parse(event.data) as {
+        command: string;
+        content: ResData<any>;
+      };
+      switch (res.command) {
+        case "ping":
           break;
 
-        case 'notice':
+        case "notice":
           break;
       }
     } catch {
-      console.log('收到错误消息格式:', event.data);  
+      console.log("收到错误消息格式:", event.data);
     }
   };
 
-  websocket.onerror = function(event) {
-    console.error('WebSocket 出错:', event);
+  websocket.onerror = function (event) {
+    console.error("WebSocket 出错:", event);
   };
 
-  websocket.onclose = function(event) {
-    console.log('WebSocket 连接已关闭,将在 5 秒后重连');
+  websocket.onclose = function (event) {
+    console.log("WebSocket 连接已关闭,将在 5 秒后重连");
     setTimeout(createWebSocket, 5000);
   };
 }
 
-export const notity = ref({})
+export const notity = ref({});
 const handler = {
-  ping() {
-
-  },
-  notity() {
-
-  }
-}
+  ping() {},
+  notity() {},
+};
 
 // 发送数据到服务器
 function sendData(command: string, message: string) {
   if (websocket && websocket.readyState === WebSocket.OPEN) {
     websocket.send(message);
   } else {
-    console.log('WebSocket 连接未打开,无法发送消息');
+    console.log("WebSocket 连接未打开,无法发送消息");
   }
 }
 

+ 1 - 0
src/layout/edit/scene-select.vue

@@ -115,6 +115,7 @@ const typeFilterScenes = computed(() => {
       .filter((item) => item.name && item.modelId && item.name.includes(keyword.value))
       .filter((item) => item.type === type);
   }
+  console.log(typeScenes, origin.value);
   return typeScenes;
 });
 

+ 29 - 2
src/layout/scene-list/index.vue

@@ -27,7 +27,17 @@
         v-else
       >
         <p>{{ item.raw.name }}</p>
-        <p>{{ SceneTypeDesc[item.raw.type as SceneType] }}</p>
+        <p>
+          {{ SceneTypeDesc[item.raw.type as SceneType] }}
+          <span
+            style="float: right"
+            v-if="canSync(item as Scene)"
+            @click.stop="sync(item as Scene)"
+            class="fun-ctrl"
+          >
+            同屏勘验
+          </span>
+        </p>
       </div>
     </template>
   </List>
@@ -35,7 +45,14 @@
 
 <script lang="ts" setup>
 import { computed, nextTick, ref, watch } from "vue";
-import { scenes, SceneType, SceneTypeDesc, fuseModels, SceneStatus } from "@/store";
+import {
+  scenes,
+  SceneType,
+  SceneTypeDesc,
+  fuseModels,
+  SceneStatus,
+  getSWKKSyncLink,
+} from "@/store";
 import List from "@/components/list/index.vue";
 import ModelList from "../model-list/index.vue";
 import { fuseModel, getModelTypeDesc } from "@/model";
@@ -47,6 +64,16 @@ const emit = defineEmits<{ (e: "update:current", data: ModelType): void }>();
 const props = defineProps<{ current: ModelType }>();
 const showModelList = ref(true);
 
+const canSync = (scene: Scene) =>
+  [SceneType.SWKK, SceneType.SWKJ, SceneType.SWSSMX, SceneType.SWYDMX].includes(
+    scene.raw.type
+  );
+
+const sync = async (scene: Scene) => {
+  const link = await getSWKKSyncLink(scene);
+  window.open(link);
+};
+
 const list = computed(() => {
   const sceneList = scenes.value.map((scene) => ({
     raw: scene,

+ 8 - 2
src/layout/show/index.vue

@@ -20,8 +20,8 @@
 </template>
 
 <script lang="ts" setup>
-import { custom, params } from "@/env";
-import { computed, nextTick, ref, watch, watchEffect } from "vue";
+import { custom, showRightPanoStack } from "@/env";
+import { ref, watchEffect } from "vue";
 import { router, RoutesName } from "@/router";
 import { loadModel, fuseModel } from "@/model";
 import { asyncTimeout } from "@/utils";
@@ -46,6 +46,12 @@ import {
 
 const hasSingle = new URLSearchParams(location.search).has("single");
 
+watchEffect((onCleanup) => {
+  if (router.currentRoute.value.name === RoutesName.show) {
+    onCleanup(showRightPanoStack.push(ref(false)));
+  }
+});
+
 const loaded = ref(false);
 const initialSys = async () => {
   await Promise.all([

+ 42 - 47
src/layout/show/slide-menu.vue

@@ -1,8 +1,8 @@
 <template>
   <div class="slide-menu" id="slide-menu">
-    <div 
-      v-for="item in items" 
-      :class="{active: item.name === activeName}" 
+    <div
+      v-for="item in items"
+      :class="{ active: item.name === activeName }"
       :key="item.name"
       @click="$emit('changeItem', item as any)"
     >
@@ -12,80 +12,76 @@
 </template>
 
 <script lang="ts" setup>
-import { metas, RoutesName, router, getRouteConfig } from '@/router'
-import { views, records, floders } from '@/store';
-import { computed } from 'vue';
+import { metas, RoutesName, router, getRouteConfig } from "@/router";
+import { views, records, floders } from "@/store";
+import { computed } from "vue";
 
-import type { RouteRaw } from '@/router'
-import { appType, params, routeIncludeFire } from '@/env';
+import type { RouteRaw } from "@/router";
+import { params, routeIncludeFire } from "@/env";
 
 export type MenuItem = {
-  name: RoutesName,
-  config: RouteRaw,
-} & (typeof metas)[keyof typeof metas]
-
-defineProps<{ activeName: RoutesName }>()
-defineEmits<{ (e: 'changeItem', item: MenuItem): void }>()
+  name: RoutesName;
+  config: RouteRaw;
+} & typeof metas[keyof typeof metas];
 
+defineProps<{ activeName: RoutesName }>();
+defineEmits<{ (e: "changeItem", item: MenuItem): void }>();
 
 const items = computed(() => {
   const items = [
     {
-      name: RoutesName.summaryShow,
-      config: getRouteConfig(RoutesName.summaryShow),
-      ...metas[RoutesName.summaryShow]
-    }
-  ]
-  if (routeIncludeFire(params.app)) {
-    items.unshift({
       name: RoutesName.fireInfo,
       config: getRouteConfig(RoutesName.fireInfo),
-      ...metas[RoutesName.fireInfo]
-    }) 
-  }
+      ...metas[RoutesName.fireInfo],
+    },
+    {
+      name: RoutesName.summaryShow,
+      config: getRouteConfig(RoutesName.summaryShow),
+      ...metas[RoutesName.summaryShow],
+    },
+  ];
 
   if (views.value.length) {
     items.push({
       name: RoutesName.viewShow,
       config: getRouteConfig(RoutesName.viewShow),
-      ...metas[RoutesName.viewShow]
-    }) 
+      ...metas[RoutesName.viewShow],
+    });
   }
 
   if (records.value.length) {
     items.push({
       name: RoutesName.recordShow,
       config: getRouteConfig(RoutesName.recordShow),
-      ...metas[RoutesName.recordShow]
-    }) 
+      ...metas[RoutesName.recordShow],
+    });
   }
 
-  if (floders.value.length) {
-    items.push({
-      name: RoutesName.folderShow,
-      config: getRouteConfig(RoutesName.folderShow),
-      ...metas[RoutesName.folderShow]
-    }) 
-  }
+  // if (floders.value.length) {
+  //   items.push({
+  //     name: RoutesName.folderShow,
+  //     config: getRouteConfig(RoutesName.folderShow),
+  //     ...metas[RoutesName.folderShow],
+  //   });
+  // }
 
-  return items
-})
+  return items;
+});
 </script>
 
 <style lang="scss" scoped>
-
 .slide-menu {
   width: var(--editor-menu-width);
-  filter: var(--editor-menu-filter); 
+  filter: var(--editor-menu-filter);
   background-color: var(--editor-menu-back);
   position: fixed;
   left: var(--editor-menu-left);
   top: calc(var(--editor-head-height) + var(--header-top));
   bottom: 0;
-  z-index: 2000;
+  z-index: 200;
   overflow: hidden;
   backdrop-filter: blur(4px);
-  transition: all .3s ease;
+  transition: all 0.3s ease;
 
   > div {
     height: 70px;
@@ -94,23 +90,23 @@ const items = computed(() => {
     justify-content: center;
     position: relative;
     color: rgba(255, 255, 255, 0.6);
-    transition: color .3s ease;
+    transition: color 0.3s ease;
     cursor: pointer;
 
     &::before {
-      content: '';
+      content: "";
       position: absolute;
       left: 0;
       top: 0;
       bottom: 0;
       width: 0;
       background: currentColor;
-      transition: width .3s ease;
+      transition: width 0.3s ease;
     }
 
     &.active,
     &:hover {
-      color: #00C8AF;
+      color: #00c8af;
     }
 
     &.active::before {
@@ -121,7 +117,6 @@ const items = computed(() => {
       font-size: 24px;
       color: currentColor;
     }
-    
   }
 }
-</style>
+</style>

+ 17 - 17
src/main.ts

@@ -16,7 +16,7 @@ app.use(router)
 app.mount('#app')
 
 if (import.meta.env.DEV) {
-  import('@/hook/notice')
+  // import('@/hook/notice')
 }
 
 appStyleImport[params.app] && appStyleImport[params.app]()
@@ -25,22 +25,22 @@ watchEffect((onCleanup) => {
 
     const untokenURLS = params.share 
       ? [
-          URL.FUSE_MODEL_LIST,
-          URL.MODEL_LIST,
-          URL.GET_SETTING,
-          URL.TAGGING_LIST,
-          URL.TAGGING_POINT_LIST,
-          URL.TAGGING_STYLE_LIST,
-          URL.MESASURE_LIST,
-          URL.GUIDE_LIST,
-          URL.GUIDE_PATH_LIST,
-          URL.RECORD_LIST,
-          URL.RECORD_FRAGMENT_LIST,
-          URL.VIEW_LIST,
-          URL.FOLDER_TYPE_LIST,
-          URL.FLODER_LIST,
-          URL.MODEL_SIGN,
-          URL.CASE_INFO,
+          // URL.FUSE_MODEL_LIST,
+          // URL.MODEL_LIST,
+          // URL.GET_SETTING,
+          // URL.TAGGING_LIST,
+          // URL.TAGGING_POINT_LIST,
+          // URL.TAGGING_STYLE_LIST,
+          // URL.MESASURE_LIST,
+          // URL.GUIDE_LIST,
+          // URL.GUIDE_PATH_LIST,
+          // URL.RECORD_LIST,
+          // URL.RECORD_FRAGMENT_LIST,
+          // URL.VIEW_LIST,
+          // URL.FOLDER_TYPE_LIST,
+          // URL.FLODER_LIST,
+          // URL.MODEL_SIGN,
+          // URL.CASE_INFO,
           URL.AUTH_PWD
         ]
       : [URL.AUTH_PWD]

+ 34 - 9
src/model/app.vue

@@ -13,6 +13,7 @@ import { SceneType } from "@/store";
 import { params } from "@/env";
 import { fuseModel, modelProps } from "./index";
 import { modelSDKFactory } from "./platform";
+import { getToken } from "@/api";
 
 const typeChange = () => {
   const oldType = modelProps.type;
@@ -58,15 +59,39 @@ export const Model = defineComponent({
         return setUrl("");
       }
       const type = scene.value.type;
-      const urls = {
-        [SceneType.SWKK]: `/swkk/spg.html?m=${scene.value.num}`,
-        [SceneType.SWKJ]: `/swkk/spg.html?m=${scene.value.num}`,
-        [SceneType.SWSS]: `/swss/index.html?m=${scene.value.num}`,
-        [SceneType.SWSSMX]: `/swkk/spg.html?m=${scene.value.num}`,
-        [SceneType.SWMX]: `index.html?caseId=${params.caseId}&app=${params.app}&modelId=${scene.value.num}&share=1#sign-model`,
-        [SceneType.SWYDSS]: `/swss/index.html?m=${scene.value.num}`,
-        [SceneType.SWYDMX]: `/swkk/spg.html?m=${scene.value.num}`,
-      };
+      const urls = offline
+        ? {
+            [SceneType.SWKK]: `/swkk/${scene.value.num}/wwwroot/spg.html?m=${scene.value.num}&lang=zh`,
+            [SceneType.SWKJ]: `/swkk/${scene.value.num}/wwwroot/spg.html?m=${scene.value.num}&lang=zh`,
+            [SceneType.SWSS]: `/swss/${scene.value.num}/www/offline.html?m=${scene.value.num}&lang=zh`,
+            [SceneType.SWSSMX]: `/swkk/${scene.value.num}/wwwroot/spg.html?m=${scene.value.num}&lang=zh`,
+            [SceneType.SWMX]: `offline.html?caseId=${params.caseId}&app=${params.app}&modelId=${scene.value.num}&share=1#sign-model`,
+            [SceneType.SWYDSS]: `/swss/${scene.value.num}/www/offline.html?m=${scene.value.num}&lang=zh`,
+            [SceneType.SWYDMX]: `/swkk/${scene.value.num}/wwwroot/spg.html?m=${scene.value.num}&lang=zh`,
+          }
+        : {
+            [SceneType.SWKK]: `/swkk/spg.html?m=${scene.value.num}`,
+            [SceneType.SWKJ]: `/swkk/spg.html?m=${scene.value.num}`,
+            [SceneType.SWSS]: `/swss/index.html?m=${scene.value.num}`,
+            [SceneType.SWSSMX]: `/swkk/spg.html?m=${scene.value.num}`,
+            [SceneType.SWMX]: `index.html?caseId=${params.caseId}&modelId=${scene.value.num}#sign-model`,
+            [SceneType.SWYDSS]: `/swss/index.html?m=${scene.value.num}`,
+            [SceneType.SWYDMX]: `/swkk/spg.html?m=${scene.value.num}`,
+          };
+
+      if (import.meta.env.DEV && !offline) {
+        Object.assign(urls, {
+          [SceneType.SWKK]: `/swkk/spg.html?m=${scene.value.num}&toen=${getToken()}`,
+          [SceneType.SWKJ]: `/swkk/spg.html?m=${scene.value.num}&toen=${getToken()}`,
+          [SceneType.SWSS]: `/swss/index.html?m=${scene.value.num}&toen=${getToken()}`,
+          [SceneType.SWSSMX]: `/swkk/spg.html?m=${scene.value.num}&toen=${getToken()}`,
+          [SceneType.SWMX]: `index.html?caseId=${params.caseId}&modelId=${
+            scene.value.num
+          }#sign-model&toen=${getToken()}`,
+          [SceneType.SWYDSS]: `/swss/index.html?m=${scene.value.num}&toen=${getToken()}`,
+          [SceneType.SWYDMX]: `/swkk/spg.html?m=${scene.value.num}&toen=${getToken()}`,
+        });
+      }
       setUrl(urls[type]);
     });
 

+ 1 - 0
src/model/platform.ts

@@ -38,6 +38,7 @@ export async function modelSDKFactory(
   if (type === fuseModel) {
     if (!fuseInitialed) {
       await initialSDK({
+        laserRoot: import.meta.env.VITE_LASER_HOST,
         layout: dom,
         scenes: scenes.value,
         lonlat: center,

+ 6 - 6
src/router/config.ts

@@ -91,7 +91,7 @@ export const routes = [
       {
         path: paths[RoutesName.fireInfo],
         name: RoutesName.fireInfo,
-        component: () => import('@/views/fire/index.vue')
+        component: () => import('@/views/folder/index.vue')
       },
       {
         path: paths[RoutesName.viewShow],
@@ -103,11 +103,11 @@ export const routes = [
         name: RoutesName.recordShow,
         component: () => import('@/views/record/show.vue')
       },
-      {
-        path: paths[RoutesName.folderShow],
-        name: RoutesName.folderShow,
-        component: () => import('@/views/folder/index.vue')
-      },
+      // {
+      //   path: paths[RoutesName.folderShow],
+      //   name: RoutesName.folderShow,
+      //   component: () => import('@/views/folder/index.vue')
+      // },
       
     ]
   },

+ 11 - 1
src/router/index.ts

@@ -1,6 +1,6 @@
 import { createRouter, createWebHashHistory } from 'vue-router'
 import { routes } from './config'
-import { computed } from 'vue'
+import { computed, watch, watchEffect } from 'vue'
 import { RoutesName } from './constant'
 import { metas } from './constant'
 
@@ -55,6 +55,16 @@ export const currentMeta = computed(() => {
   }
 })
 
+let timeout: any
+watch(currentLayout, () => {
+  clearTimeout(timeout)
+  timeout = setTimeout(() => {
+    if (!currentLayout.value) {
+      router.replace({ name: RoutesName.fireInfo })
+    }
+  }, 100)
+})
+
 export * from './config'
 export * from './constant'
 

+ 1 - 0
src/sdk/sdk.ts

@@ -336,6 +336,7 @@ export type Tagging3D = {
 
 export let sdk: SDK;
 export type InialSDKProps = {
+  laserRoot?: string
   layout: HTMLDivElement;
   scenes: Scene[];
   lonlat?: number[];

+ 62 - 8
src/store/floder-type.ts

@@ -1,14 +1,68 @@
-import { ref } from 'vue'
-import { fetchFloderTypes } from '@/api'
+import { computed, ref } from "vue";
+import { fetchFloderTypes } from "@/api";
 
-import type { FloderTypes, FloderType } from '@/api'
+import type { FloderTypes, FloderType, Floder } from "@/api";
+import { getFloderByType } from "./floder";
+import { getUrlType, MetaType } from "@/utils";
 
-export const floderTypes = ref<FloderTypes>([])
-export const getFloderType = (id: FloderType['filesTypeId']) => 
-  floderTypes.value.find(type => type.filesTypeId === id)
+export const floderTypes = ref<FloderTypes>([]);
+export const getFloderType = (id: FloderType["filesTypeId"]) =>
+  floderTypes.value.find((type) => type.filesTypeId === id);
 
 export const initialFloderTypes = async () => {
-  floderTypes.value = await fetchFloderTypes()
+  floderTypes.value = await fetchFloderTypes();
+};
+
+export type FloderRoot = {
+  flat: boolean,
+  modal: boolean
+  id: number;
+  title: string;
+  floders: (ReturnType<typeof getFloderByType>[number] & { metaType: MetaType })[];
+  children?: FloderRoot[];
+};
+const gemerateRoot = (parentId: number | null = null) => {
+  const items: FloderRoot[] = [];
+  for (let i = 0; i < floderTypes.value.length; i++) {
+    const type = floderTypes.value[i];
+    if (type.parentId === parentId) {
+      const item = {
+        id: type.filesTypeId,
+        title: type.filesTypeName,
+        flat: !!type.flatShow,
+        modal: !!type.modalShow,
+        floders: getFloderByType(type).map((floder) => ({
+          ...floder,
+          metaType: getUrlType(floder.filesUrl),
+        })),
+        children: gemerateRoot(type.filesTypeId)
+      };
+      items.push(item)
+    }
+  }
+  return items
+};
+export const floderRoots = computed(gemerateRoot);
+
+export const getLevelRoot = (floder: Floder, roots = floderRoots.value): FloderRoot | undefined => {
+  for (const root of roots) {
+    if (root.floders.some(f => f.filesId === floder.filesId)) {
+      return root;
+    } else if (root.children?.length) {
+      const cRoot = getLevelRoot(floder, root.children)
+      if (cRoot) {
+        return cRoot
+      }
+    }
+  }
+}
+
+export const getFlatFloders = (root: FloderRoot, floders: FloderRoot['floders'] = []) => {
+  floders.push(...root.floders)
+  if (root.children?.length) {
+    root.children.forEach(child => getFlatFloders(child, floders))
+  }
+  return floders
 }
 
-export type { FloderType, FloderTypes }
+export type { FloderType, FloderTypes };

+ 2 - 1
src/store/floder.ts

@@ -1,4 +1,4 @@
-import { ref } from 'vue'
+import { computed, ref } from 'vue'
 import { fetchFloders } from '@/api'
 
 import type { Floders } from '@/api'
@@ -8,6 +8,7 @@ export const floders = ref<Floders>([])
 export const getFloderByType = (type: FloderType) => 
   floders.value.filter(floder => floder.filesTypeId === type.filesTypeId)
 
+
 export const initialFloders = async () => {
   floders.value = await fetchFloders()
 }

+ 57 - 1
src/store/scene.ts

@@ -1,8 +1,10 @@
 import { ref } from 'vue'
-import { fetchScenes } from '@/api'
+import { fetchScenes, getSyncSceneInfo, SceneType, SceneTypeDesc } from '@/api'
 import { fetchStoreItems } from '@/utils'
 
 import type { Scene, Scenes } from '@/api'
+import { Dialog } from 'bill/expose-common'
+import { getUserInfo } from '@/api/user'
 export type { Scene, Scenes } from '@/api'
 
 
@@ -16,3 +18,57 @@ export const initialScenes = fetchStoreItems(
 )
 
 export { SceneType, SceneTypeDesc, SceneStatus } from '@/api'
+
+export const SceneTypePaths: { [key in SceneType]: string[] } = {
+  [SceneType.SWKK]: [
+    "/swkk/spg.html",
+    "/swkk/epg.html",
+    `/livestream/fd/fire.html`,
+  ],
+  [SceneType.SWKJ]: ["/swkk/spg.html", "/swkk/epg.html"],
+  [SceneType.SWSS]: ["/swss/index.html", "/swss/index.html"],
+  [SceneType.SWMX]: import.meta.env.DEV
+    ? ["/dev-code/index.html", "/dev-code/index.html"]
+    : ["/code/index.html", "/code/index.html"],
+  [SceneType.SWSSMX]: ["/swkk/spg.html", "/swkk/epg.html"],
+  [SceneType.SWYDSS]: ["/swss/index.html", "/swss/index.html"],
+  [SceneType.SWYDMX]: ["/swkk/spg.html", "/swkk/epg.html"],
+};
+
+export const getSWKKSyncLink = async (scene: Scene) => {
+  const supportTypes = [SceneType.SWKJ, SceneType.SWSSMX, SceneType.SWYDMX];
+  const kkScenes = scenes.value.filter((scene) =>
+    supportTypes.includes(scene.type)
+  );
+
+  let msg: string | null = null;
+  if (!kkScenes.length) {
+    msg = `带看仅支持${supportTypes
+      .map((type) => SceneTypeDesc[type])
+      .join("、")}类型场景,请添加此类型场景。`;
+  }
+  if (msg) {
+    Dialog.alert(msg);
+    throw msg;
+  }
+
+  const url = new URL(SceneTypePaths[SceneType.SWKK][2], window.location.href);
+  const userInfo = await getUserInfo();
+  const roomId = await getSyncSceneInfo();
+  const params = {
+    vruserId: userInfo.userName,
+    // platform: "fd",
+    roomId,
+    // domain: location.href,
+    // fromMiniApp: "0",
+    role: "leader",
+    avatar: userInfo.avatar,
+    redirect: encodeURIComponent(location.href),
+    name: userInfo.userName,
+    m: scene.num,
+  };
+  for (const [name, val] of Object.entries(params)) {
+    url.searchParams.append(name, val || "");
+  }
+  return url;
+};

+ 5 - 0
src/style.scss

@@ -146,4 +146,9 @@ input::-ms-clear,input::-ms-reveal {
   content:attr(placeholder);
   color:grey;
   // font-style:italic;
+}
+
+
+.ant-modal-mask {
+  background-color: rgba(0, 0, 0, 0.6) !important;
 }

+ 5 - 1
src/utils/meta.ts

@@ -2,13 +2,15 @@ export enum MetaType {
   image = 'image',
   video = 'video',
   audio = 'audio',
+  xfile = 'xfile',
   other = 'other'
 }
 
 export const metaTypeExtnames = {
   [MetaType.image]: ['bmp', 'jpg', 'png', 'tif', 'gif', 'pcx', 'tga', 'exif', 'fpx', 'svg', 'psd', 'cdr', 'pcd', 'dxf', 'ufo', 'eps', 'ai', 'raw', 'WMF', 'webp', 'avif', 'apng'],
   [MetaType.audio]: ['mp3'],
-  [MetaType.video]: ['wmv', 'asf', 'asx', 'rm', 'rmvb', 'mp4', '3gp', 'mov', 'm4v', 'avi', 'dat', 'mkv', 'flv', 'vob']
+  [MetaType.video]: ['wmv', 'asf', 'asx', 'rm', 'rmvb', 'mp4', '3gp', 'mov', 'm4v', 'avi', 'dat', 'mkv', 'flv', 'vob'],
+  [MetaType.xfile]: [".raw", ".dcm"]
 }
 
 export const getExtname = (url: string) => {
@@ -29,6 +31,8 @@ export const getUrlType = (url: string) => {
       return MetaType.video
     } else if (type === 'audio') {
       return MetaType.audio
+    } else if (type === 'raw' || type === 'dcm') {
+      return MetaType.xfile
     } else {
       return MetaType.other
     }

+ 12 - 0
src/utils/params.ts

@@ -15,4 +15,16 @@ export const strToParams = (str: string) => {
   }
 
   return result
+}
+
+
+export const paramsToStr = (params: {[key in string]: string}) => {
+  
+  if (params && Object.keys(params).length > 0) {
+    const entitys = Object.entries(params)
+    entitys.sort((a, b) => a[0].localeCompare(b[0]))
+    return '?' + entitys.map(([k, v]) => `${k}=${v}`).join('&')
+  } else {
+    return ''
+  }
 }

+ 51 - 98
src/views/fire/index.vue

@@ -1,107 +1,60 @@
 <template>
-  <LeftPano>
-    <div class="info" v-if="caseProject?.tmProject">
-      <h2>案件信息</h2>
-      <p>
-        <span>项目编号:</span>
-        {{ caseProject.tmProject.projectSn }}
-      </p>
-      <p>
-        <span>起火地址:</span>
-        {{ caseProject.tmProject.projectAddress }}
-      </p>
-      <p>
-        <span>起火场所:</span>
-        {{ caseProject.tmProject.projectSite }}
-      </p>
-      <p>
-        <span>承办单位:</span>
-        {{ caseProject.tmProject.organizerDeptName }}
-      </p>
-      <p>
-        <span>起火对象:</span>
-        {{ caseProject.tmProject.projectName }}
-      </p>
-      <p>
-        <span>承办人员:</span>
-        {{ caseProject.tmProject.organizerUsers }}
-      </p>
-      <p>
-        <span>事故日期:</span>
-        {{ caseProject.tmProject.accidentDate }}
-      </p>
-      <p>
-        <span>火灾原因:</span>
-        {{ caseProject.tmProject.fireReason }}
-      </p>
-      <p>
-        <span>项目状态:</span>
-        {{ caseProject.tmProject.statusDesc }}
-      </p>
-      <p>
-        <span>教学项目:</span>
-        {{ caseProject.tmProject.isTeached ? '是' : '否' }}
-      </p>
-      <p>
-        <span>创建人:</span>
-        {{ caseProject.tmProject.creatorName }}
-      </p>
-      <p>
-        <span>编辑人:</span>
-        {{ caseProject.tmProject.editorName }}
-      </p>
-      <p>
-        <span>创建时间:</span>
-        {{ caseProject.tmProject.createTime }}
-      </p>
-      <p>
-        <span>最新编辑:</span>
-        {{ caseProject.tmProject.editTime }}
-      </p>
-    </div>
-  </LeftPano>
+  <Modal
+    width="1200px"
+    :title="title"
+    @cancel="router.push({ name: RoutesName.show })"
+    :open="router.currentRoute.value.name === RoutesName.fireInfo"
+    :footer="null"
+  >
+    <Info
+      title="案件信息"
+      :data="caseProject.tmProject"
+      :label-map="tmLabelMap1"
+      v-if="caseProject?.tmProject"
+    />
+    <Info
+      title="勘验信息"
+      :data="caseProject.tmProject"
+      :label-map="tmLabelMap2"
+      v-if="caseProject?.tmProject"
+    />
+  </Modal>
 </template>
 
-
 <script setup lang="ts">
-import { showRightPanoStack } from '@/env';
-import { useViewStack } from '@/hook';
-import { LeftPano } from '@/layout'
-import { caseProject } from '@/store/case'
-import { ref } from 'vue';
+import { Modal } from "ant-design-vue";
+import Info from "./info.vue";
+import { showRightPanoStack } from "@/env";
+import { useViewStack } from "@/hook";
+import router, { RoutesName } from "@/router";
+import { title } from "@/store";
+import { caseProject } from "@/store/case";
+import { ref } from "vue";
 
+type LabelMap = Record<string, string | [string, (v: any) => any]>;
 
-useViewStack(() => showRightPanoStack.push(ref(false)))
-</script>
+const tmLabelMap1 = {
+  projectSn: "案件名称",
+  projectAddress: "立案编号",
+  projectSite: "案件类别",
+  organizerDeptName: "案发时间",
+  projectName: ["是否命案", (v: any) => (v ? "是" : "否")],
+  organizerUsers: ["是否刑件", (v: any) => (v ? "是" : "否")],
+  accidentDate: "案发区域",
+  fireReason: "案发地点",
+  statusDesc: "经纬度",
+} as LabelMap;
 
-<style lang="scss" scoped>
-.info {
-  h2 {
-    padding: 20px;
-    font-weight: bold;
-    display: flex;
-    justify-content: space-between;
-    border-bottom: 1px solid rgba(255, 255, 255, 0.16);
-    align-items: center;
-    margin-bottom: 0;
-  }
+const tmLabelMap2 = {
+  creatorName: "指挥中心电话时间",
+  editorName: "报警时间",
+  createTime: "现场勘验单位",
+  editTime: "指派方式",
+  statusDesc: "勘验地点",
+  fireReason: "勘验时间",
+} as LabelMap;
 
-  p {
-    padding: 0 30px;
-    margin: 20px 0;
-    color: rgba(255, 255, 255, 1);
-    font-size: 14px;
-    display: flex;
-    word-break: break-all;
+useViewStack(() => showRightPanoStack.push(ref(false)));
+</script>
 
-    span {
-      flex: none;
-      display: inline-block;
-      width: 70px;
-      height: 100%;
-      margin-right: 20px;
-      color: rgba(255, 255, 255, 0.70);
-    }
-  }
-}
-</style>
+<style lang="scss" scoped></style>

+ 58 - 0
src/views/fire/info.vue

@@ -0,0 +1,58 @@
+<template>
+  <div class="info" v-if="data">
+    <h2>{{ title }}</h2>
+    <div>
+      <p v-for="(label, key) in labelMap">
+        <span>{{ typeof label === "string" ? label : label[0] }}:</span>
+        {{ typeof label === "string" ? data[key] : label[1](data[key]) }}
+      </p>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+defineProps<{
+  title: string;
+  data: Record<string, any>;
+  labelMap: Record<string, string | [string, (v: any) => any]>;
+}>();
+</script>
+
+<style lang="scss" scoped>
+.info {
+  margin-bottom: 30px;
+  h2 {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 10px;
+    font-size: 16px;
+  }
+  > div {
+    display: flex;
+    flex-wrap: wrap;
+
+    p {
+      width: 33.33%;
+    }
+  }
+
+  p {
+    margin: 5px 0;
+    color: rgba(255, 255, 255, 1);
+    font-size: 14px;
+    display: flex;
+    word-break: break-all;
+
+    span {
+      flex: none;
+      display: inline-block;
+      width: 70px;
+      text-align: right;
+      height: 100%;
+      margin-right: 20px;
+      color: rgba(255, 255, 255, 0.7);
+    }
+  }
+}
+</style>

+ 61 - 0
src/views/folder/fire/index.vue

@@ -0,0 +1,61 @@
+<template>
+  <Modal
+    width="1200px"
+    :title="title"
+    @cancel="$emit('update:open', false)"
+    :open="open"
+    :footer="null"
+  >
+    <Info
+      title="案件信息"
+      :data="caseProject"
+      :label-map="tmLabelMap1"
+      v-if="caseProject?.tmProject"
+    />
+    <Info
+      title="勘验信息"
+      :data="caseProject.tmProject"
+      :label-map="tmLabelMap2"
+      v-if="caseProject?.tmProject"
+    />
+  </Modal>
+</template>
+
+<script setup lang="ts">
+import { Modal } from "ant-design-vue";
+import Info from "./info.vue";
+import { showRightPanoStack } from "@/env";
+import { useViewStack } from "@/hook";
+import router, { RoutesName } from "@/router";
+import { title } from "@/store";
+import { caseProject } from "@/store/case";
+import { ref } from "vue";
+
+defineProps<{ open: boolean }>();
+defineEmits<{ (e: "update:open", v: boolean): void }>();
+
+type LabelMap = Record<string, string | [string, (v: any) => any]>;
+
+const tmLabelMap1 = {
+  caseTitle: "案件名称",
+  caseNum: "立案编号",
+  caseCategory: "案件类别",
+  crimeTime: "案发时间",
+  homicideCase: ["是否命案", (v: any) => (v ? "是" : "否")],
+  criminalCase: ["是否刑件", (v: any) => (v ? "是" : "否")],
+  caseRegion: ["案发区域", (v: string[]) => v.join("-")],
+  caseAddress: "案发地点",
+  latAndLong: "经纬度",
+} as LabelMap;
+
+const tmLabelMap2 = {
+  commandTime: "指挥中心电话时间",
+  alarmTime: "报警时间",
+  inquestDept: "现场勘验单位",
+  assignType: "指派方式",
+  inquestAddress: "勘验地点",
+  times: ["勘验时间", (v: string[]) => v.join("到")],
+} as LabelMap;
+</script>
+
+<style lang="scss" scoped></style>

+ 58 - 0
src/views/folder/fire/info.vue

@@ -0,0 +1,58 @@
+<template>
+  <div class="info" v-if="data">
+    <h2>{{ title }}</h2>
+    <div>
+      <p v-for="(label, key) in labelMap">
+        <span>{{ typeof label === "string" ? label : label[0] }}:</span>
+        {{ typeof label === "string" ? data[key] : label[1](data[key]) }}
+      </p>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+defineProps<{
+  title: string;
+  data: Record<string, any>;
+  labelMap: Record<string, string | [string, (v: any) => any]>;
+}>();
+</script>
+
+<style lang="scss" scoped>
+.info {
+  margin-bottom: 30px;
+  h2 {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 10px;
+    font-size: 16px;
+  }
+  > div {
+    display: flex;
+    flex-wrap: wrap;
+
+    p {
+      width: 33.33%;
+    }
+  }
+
+  p {
+    margin: 5px 0;
+    color: rgba(255, 255, 255, 1);
+    font-size: 14px;
+    display: flex;
+    word-break: break-all;
+
+    span {
+      flex: none;
+      display: inline-block;
+      width: 70px;
+      text-align: right;
+      height: 100%;
+      margin-right: 20px;
+      color: rgba(255, 255, 255, 0.7);
+    }
+  }
+}
+</style>

+ 172 - 0
src/views/folder/floder-view.vue

@@ -0,0 +1,172 @@
+<template>
+  <div
+    :class="{ root: index === 1 }"
+    class="tree"
+    v-if="getFlatFloders(root).length !== 0"
+  >
+    <div
+      class="solid header"
+      :class="{ ['root-header']: index === 1 }"
+      :style="{ '--index': index }"
+    >
+      <span @click="showChildren = !showChildren">
+        {{ root.title }}
+      </span>
+      <ui-icon
+        :type="`pull-${showChildren ? 'up' : 'down'}`"
+        class="icon"
+        ctrl
+        v-if="floders.length || root.children?.length"
+      />
+    </div>
+
+    <template v-if="!root.modal && showChildren && (floders.length || children?.length)">
+      <div class="items" :class="{ ['root-items']: index === 1 }">
+        <template v-if="floders.length">
+          <div
+            :style="{ '--index': index }"
+            v-for="floder in floders"
+            :key="floder.filesId"
+            class="fun-ctrl solid item"
+            @click="$emit('preview', [floder, root])"
+          >
+            <ui-icon :type="typeIcons[floder.metaType]" v-if="floder.metaType" />
+            <p>{{ floder.filesTitle }}</p>
+          </div>
+        </template>
+        <template v-if="children?.length">
+          <FloderView
+            v-for="item in children"
+            @preview="(v: any) => emit('preview', v)"
+            :root="item"
+            :index="index + 1"
+          />
+        </template>
+      </div>
+    </template>
+  </div>
+
+  <Modal
+    v-if="root.modal"
+    width="800px"
+    :title="root.title"
+    @cancel="showChildren = false"
+    :open="showChildren"
+    :footer="null"
+  >
+    <div class="modal-root-content">
+      <ModalFloderView :root="root" @preview="(v) => emit('preview', v)" />
+    </div>
+  </Modal>
+</template>
+<script lang="ts" setup>
+import { Floder, FloderRoot, getFlatFloders } from "@/store";
+import { computed, ref } from "vue";
+import { MetaType } from "@/utils";
+import ModalFloderView from "./modal-floder-view.vue";
+import { Modal } from "ant-design-vue";
+
+const props = defineProps<{ root: FloderRoot; index?: number }>();
+const emit = defineEmits<{ (e: "preview", v: [Floder, FloderRoot]): void }>();
+
+const index = props.index || 1;
+const typeIcons = {
+  [MetaType.image]: "pic",
+  [MetaType.video]: "a-film",
+  [MetaType.other]: "nav-edit",
+  [MetaType.audio]: "nav-edit",
+  [MetaType.xfile]: "nav-edit",
+};
+
+const floders = computed(() => {
+  if (props.root.flat) {
+    return getFlatFloders(props.root);
+  } else {
+    return props.root.floders;
+  }
+});
+const children = computed(() => {
+  if (props.root.flat || props.root.modal) {
+    return [];
+  } else {
+    return props.root.children;
+  }
+});
+const showChildren = ref(props.root.modal ? false : true);
+</script>
+
+<style lang="scss" scoped>
+.tree {
+  margin-bottom: 0;
+}
+.modal-root-content {
+  max-height: 700px;
+  overflow-y: auto;
+}
+.root-items {
+  background: rgba(0, 0, 0, 0.5);
+}
+
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 0;
+  cursor: pointer;
+  padding: 20px 0;
+  position: relative;
+  padding-left: calc(var(--index) * 20px);
+  padding-right: 20px;
+
+  span {
+    font-weight: normal;
+    font-size: 14px;
+  }
+
+  &.root-header {
+    padding: 20px;
+    span {
+      font-weight: bold;
+      font-size: 16px;
+    }
+  }
+
+  .icon {
+    font-size: 14px;
+  }
+}
+
+.solid {
+  &::after {
+    content: "";
+    position: absolute;
+    left: 20px;
+    right: 20px;
+    height: 1px;
+    background: rgba(255, 255, 255, 0.16);
+    bottom: 0;
+  }
+}
+
+.item {
+  margin-right: 20px;
+  padding: 20px 0;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  position: relative;
+  padding-left: calc(var(--index) * 20px);
+  padding-right: 0;
+
+  &.solid::after {
+    right: 0;
+  }
+
+  p {
+    margin-left: 10px;
+    font-size: 12px;
+    color: currentColor;
+    word-break: break-all;
+  }
+}
+</style>

+ 38 - 70
src/views/folder/index.vue

@@ -1,93 +1,59 @@
 <template>
   <LeftPano>
-    <template v-for="item in types">
-      <div :key="item.id" class="types" v-if="item.floders.length">
-        <h2 @click="item.show.value = !item.show.value">
-          {{ item.title }}
-          <ui-icon :type="`pull-${item.show.value ? 'up' : 'down'}`" class="icon" ctrl />
-        </h2>
-
-        <div class="floders" v-if="item.show.value">
-          <div
-            v-for="floder in item.floders"
-            :key="floder.filesId"
-            class="fun-ctrl"
-            @click="preview(floder)"
-          >
-            <ui-icon :type="typeIcons[floder.metaType]" v-if="floder.metaType" />
-            <p>{{ floder.filesTitle }}</p>
-          </div>
-        </div>
-      </div>
+    <div class="types">
+      <h2 @click="showInfo = true">
+        案件概要
+        <ui-icon :type="`pull-${showInfo ? 'up' : 'down'}`" class="icon" ctrl />
+      </h2>
+    </div>
+    <template v-for="item in floderRoots" :key="item.id">
+      <FloderView :root="item" @preview="(v) => preview(v)" />
     </template>
   </LeftPano>
 
-  <Preview :items="[currentFile]" v-if="currentFile" @close="currentFile = null" />
+  <Preview
+    :items="currentItems"
+    :current="currentNdx"
+    v-if="~currentNdx"
+    @close="currentNdx = -1"
+  />
+  <Fire v-model:open="showInfo" />
 </template>
 
 <script lang="ts" setup>
 import { LeftPano } from "@/layout";
-import { computed, ref } from "vue";
+import { ref } from "vue";
 import { getUrlType, MetaType, saveAs } from "@/utils";
-import { Preview, MediaItem, MediaType } from "@/components/static-preview/index.vue";
-import { floderTypes, getFloderByType } from "@/store";
+import { Preview, MediaItem } from "@/components/static-preview/index.vue";
+import { floderRoots, getFlatFloders } from "@/store";
+import FloderView from "./floder-view.vue";
+import Fire from "./fire/index.vue";
 
-import type { Floder } from "@/store";
+import type { Floder, FloderRoot } from "@/store";
 import { useViewStack } from "@/hook";
 import { showRightPanoStack } from "@/env";
 
-const types = computed(() =>
-  floderTypes.value.map((type) => ({
-    show: ref(true),
-    id: type.filesTypeId,
-    title: type.filesTypeName,
-    floders: getFloderByType(type).map((floder) => ({
-      ...floder,
-      metaType: getUrlType(floder.filesUrl),
-    })),
-  }))
-);
-
-const typeIcons = {
-  [MetaType.image]: "pic",
-  [MetaType.video]: "a-film",
-  [MetaType.other]: "nav-edit",
-};
-
-const currentFile = ref<MediaItem | null>(null);
-const preview = async (floder: Floder) => {
-  const ext = floder.filesUrl
-    .substring(floder.filesUrl.lastIndexOf("."))
-    .toLocaleLowerCase();
-  if ([".raw", ".dcm"].includes(ext)) {
-    window.open(
-      `/xfile-viewer/index.html?file=${floder.filesUrl}&name=${floder.filesTitle}&time=` +
-        Date.now()
-    );
-    return;
-  }
-
-  const type = getUrlType(floder.filesUrl);
-  const mediaType =
-    type === MetaType.image
-      ? MediaType.img
-      : type === MetaType.video
-      ? MediaType.video
-      : null;
-
-  if (!mediaType) {
+const showInfo = ref(false);
+const currentNdx = ref(-1);
+const currentItems = ref<MediaItem[]>([]);
+const preview = async ([floder, root]: [Floder, FloderRoot]) => {
+  const metaType = getUrlType(floder.filesUrl);
+  if (metaType === MetaType.other) {
     const isBlob = floder.filesUrl.includes("blob");
-
     if (floder.filesTypeId === 100) {
       saveAs(floder.filesUrl, floder.filesTitle + ".doc");
     } else {
       window.open(floder.filesUrl + (!isBlob ? "?time=" + Date.now() : ""));
     }
   } else {
-    currentFile.value = {
-      type: mediaType,
-      url: floder.filesUrl,
-    };
+    const floders = root.flat ? getFlatFloders(root) : root.floders;
+    const items = floders.map((item) => ({
+      type: getUrlType(item.filesUrl),
+      id: item.filesId,
+      url: item.filesUrl,
+    }));
+    currentNdx.value = items.findIndex((item) => item.id === floder.filesId);
+    currentItems.value = items;
   }
 };
 useViewStack(() => showRightPanoStack.push(ref(false)));
@@ -96,7 +62,9 @@ useViewStack(() => showRightPanoStack.push(ref(false)));
 <style lang="scss" scoped>
 .types {
   h2 {
-    padding: 20px;
+    padding: 20px 0;
+    margin: 0 20px;
+    font-size: 16px;
     font-weight: bold;
     display: flex;
     justify-content: space-between;

+ 114 - 0
src/views/folder/modal-floder-view.vue

@@ -0,0 +1,114 @@
+<template>
+  <ui-group v-if="floders.length">
+    <ui-group-option>
+      <span @click="canAll && (showAll = !showAll)" :class="{ ['fun-ctrl']: canAll }">
+        {{ root.title }}
+        <template v-if="canAll">
+          <UpOutlined v-if="showAll" />
+          <DownOutlined v-else />
+        </template>
+      </span>
+    </ui-group-option>
+    <ui-group-option>
+      <div class="items">
+        <div class="img-item" v-for="(_, i) in showLen" :key="floders[i].filesId">
+          <div class="img-item-content">
+            <img :src="floders[i].filesUrl" @click="clickHandler(floders[i])" />
+          </div>
+        </div>
+      </div>
+    </ui-group-option>
+  </ui-group>
+
+  <Tabs v-if="!emptyTabs" v-model:activeKey="activeTab">
+    <template v-for="children in root.children">
+      <TabPane
+        :tab="children.title"
+        :key="children.id"
+        v-if="getFlatFloders(children).length"
+      >
+        <ModalFloderView :root="children" @preview="(v: any) => emit('preview', v)" />
+      </TabPane>
+    </template>
+  </Tabs>
+
+  <template v-for="c in children" :key="c.id">
+    <ModalFloderView
+      :root="c"
+      v-if="isLastLevel(c)"
+      @preview="(v: any) => emit('preview', v)"
+    />
+  </template>
+</template>
+<script lang="ts" setup>
+import { Floder, FloderRoot, getFlatFloders } from "@/store";
+import { computed, ref } from "vue";
+import { TabPane, Tabs } from "ant-design-vue";
+import { DownOutlined, UpOutlined } from "@ant-design/icons-vue";
+
+const props = defineProps<{ root: FloderRoot }>();
+const emit = defineEmits<{ (e: "preview", v: [Floder, FloderRoot]): void }>();
+const isLastLevel = (root: FloderRoot) => {
+  return !root.children?.length;
+};
+const emptyTabs = computed(
+  () => props.root.children?.every((r) => isLastLevel(r)) && !props.root.flat
+);
+const oneTabs = computed(() => {
+  if (!emptyTabs.value) return null;
+  return props.root.children!.find((i) => !isLastLevel(i));
+});
+const clickHandler = (floder: Floder) => {
+  emit("preview", [floder, props.root]);
+};
+const activeTab = ref(oneTabs.value?.id);
+const floders = computed(() => {
+  if (props.root.flat) {
+    return getFlatFloders(props.root);
+  } else {
+    return props.root.floders;
+  }
+});
+const children = computed(() => {
+  if (props.root.flat) {
+    return [];
+  } else {
+    return props.root.children;
+  }
+});
+const len = computed(() => floders.value.length);
+const showAll = ref(false);
+const samLen = 3;
+const showLen = computed(() => (showAll.value ? len.value : Math.min(samLen, len.value)));
+const canAll = computed(() => len.value > samLen);
+</script>
+
+<style lang="scss" scoped>
+.items {
+  display: flex;
+  flex-wrap: wrap;
+}
+.img-item {
+  width: 33.33%;
+  padding: 5px;
+  .img-item-content {
+    padding-top: 56.25%;
+    position: relative;
+    cursor: pointer;
+    img {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      left: 0;
+      top: 0;
+      object-fit: cover;
+    }
+  }
+}
+
+.mySwiper {
+  --swiper-pagination-fraction-color: #000;
+  --swiper-theme-color: #03ad98;
+  --swiper-navigation-size: 30px;
+}
+</style>

+ 4 - 4
src/views/record/shot.vue

@@ -41,7 +41,7 @@
 
     <Preview
       v-if="palyUrl"
-      :items="[{ type: MediaType.video, url: palyUrl }]"
+      :items="[{ type: MetaType.video, url: palyUrl }]"
       @close="palyUrl = null"
     />
 
@@ -62,8 +62,8 @@ import {
 } from "vue";
 import { VideoRecorder } from "simaqcore";
 import { sdk } from "@/sdk";
-import { getVideoCover, togetherCallback } from "@/utils";
-import { MediaType, Preview } from "@/components/static-preview/index.vue";
+import { getVideoCover, MetaType, togetherCallback } from "@/utils";
+import { Preview } from "@/components/static-preview/index.vue";
 import { Record, getRecordFragmentBlobs } from "@/store";
 import ShotImiate from "./shot-imitate.vue";
 import {
@@ -251,7 +251,7 @@ export default defineComponent({
     });
 
     return {
-      MediaType,
+      MetaType,
       complete,
       pause,
       close,

+ 100 - 87
src/views/record/sign.vue

@@ -1,23 +1,26 @@
 <template>
-  <ui-group-option class=" record-sign" :class="{sign: record.status === RecordStatus.SUCCESS}">
+  <ui-group-option
+    class="record-sign"
+    :class="{ sign: record.status === RecordStatus.SUCCESS }"
+  >
     <div class="content">
       <span class="cover">
-        <img :src="getResource(getFileUrl(record.cover))" alt="" v-if="record.cover">
-        <ui-icon 
-          type="preview" 
-          ctrl 
-          class="preview" 
-          @click="actions.play()"  
+        <img :src="getResource(getFileUrl(record.cover))" alt="" v-if="record.cover" />
+        <ui-icon
+          type="preview"
+          ctrl
+          class="preview"
+          @click="actions.play()"
           v-if="record.status === RecordStatus.SUCCESS"
         />
       </span>
-      <ui-input 
-        type="text" 
-        :modelValue="record.title" 
+      <ui-input
+        type="text"
+        :modelValue="record.title"
         @update:modelValue="(title: string) => $emit('updateTitle', title.trim())"
-        v-show="isEditTitle" 
-        ref="inputRef" 
-        height="28px" 
+        v-show="isEditTitle"
+        ref="inputRef"
+        height="28px"
         :maxlength="15"
       />
       <div class="title" v-show="!isEditTitle">
@@ -27,112 +30,124 @@
     </div>
     <div class="action" v-if="edit && record.status === RecordStatus.SUCCESS">
       <ui-icon type="order" ctrl />
-      <ui-more 
-        :options="menus" 
-        style="margin-left: 20px" 
-        @click="(action: keyof typeof actions) => actions[action]()" 
+      <ui-more
+        :options="menus"
+        style="margin-left: 20px"
+        @click="(action: keyof typeof actions) => actions[action]()"
       />
     </div>
 
-    <Shot 
-      v-if="isShot" 
+    <Shot
+      v-if="isShot"
       @close="closeHandler"
-      @append="appendFragment" 
-      @updateCover="(cover: string) => $emit('updateCover', cover)" 
+      @append="appendFragment"
+      @updateCover="(cover: string) => $emit('updateCover', cover)"
       @deleteRecord="$emit('delete')"
-      :record="record" />
-    <Preview 
-      v-if="isPlayVideo" 
-      :items="[{ type: MediaType.video, url: record.url! }]"
-      @close="isPlayVideo = false" 
+      :record="record"
+    />
+    <Preview
+      v-if="isPlayVideo"
+      :items="[{ type: MetaType.video, url: record.url! }]"
+      @close="isPlayVideo = false"
     />
   </ui-group-option>
 </template>
 
 <script lang="ts">
-import type {PropType} from 'vue'
-import {computed, defineComponent, ref, watchEffect} from 'vue'
-import {getExtname, getFileUrl, loadPack, saveAs} from '@/utils'
-import {useFocus} from 'bill/hook/useFocus'
-import {createRecordFragment, getRecordFragmentBlobs, isTemploraryID, recordFragments, RecordStatus} from '@/store'
-import {MediaType, Preview} from '@/components/static-preview/index.vue'
-import {getResource} from '@/env'
-import Shot from './shot.vue'
-import type {RecordProcess} from './help'
-import {Message} from 'bill/index'
+import type { PropType } from "vue";
+import { computed, defineComponent, ref, watchEffect } from "vue";
+import { getExtname, getFileUrl, loadPack, MetaType, saveAs } from "@/utils";
+import { useFocus } from "bill/hook/useFocus";
+import {
+  createRecordFragment,
+  getRecordFragmentBlobs,
+  isTemploraryID,
+  recordFragments,
+  RecordStatus,
+} from "@/store";
+import { Preview } from "@/components/static-preview/index.vue";
+import { getResource } from "@/env";
+import Shot from "./shot.vue";
+import type { RecordProcess } from "./help";
+import { Message } from "bill/index";
 
 export default defineComponent({
   props: {
     record: {
       type: Object as PropType<RecordProcess>,
-      required: true
+      required: true,
     },
     edit: {
       type: Boolean,
       required: false,
-      default: true
-    }
+      default: true,
+    },
   },
   emits: {
-    'updateCover': (cover: string) => true,
-    'updateTitle': (title: string) => true,
-    'delete': () => true
+    updateCover: (cover: string) => true,
+    updateTitle: (title: string) => true,
+    delete: () => true,
   },
   setup(props, { emit }) {
     const menus = computed(() => {
-      const base = []
+      const base = [];
       if ([RecordStatus.SUCCESS, RecordStatus.UN].includes(props.record.status)) {
         base.push(
-          { label: '重命名', value: 'rename' },
-          { label: '继续录制', value: 'continue' },
-        )
+          { label: "重命名", value: "rename" },
+          { label: "继续录制", value: "continue" }
+        );
 
         if (props.record.status === RecordStatus.SUCCESS) {
-          base.push({ label: '下载', value: 'download' },)
+          base.push({ label: "下载", value: "download" });
         }
       }
-      base.push({ label: '删除', value: 'delete' })
-      return base
-    })
+      base.push({ label: "删除", value: "delete" });
+      return base;
+    });
 
-    const isShot = ref<boolean>(false)
-    const inputRef = ref()
-    const isEditTitle = useFocus(computed(() => inputRef.value?.vmRef.root))
+    const isShot = ref<boolean>(false);
+    const inputRef = ref();
+    const isEditTitle = useFocus(computed(() => inputRef.value?.vmRef.root));
 
     watchEffect(() => {
       if (!isEditTitle.value && !props.record.title.length) {
-        isEditTitle.value = true
-        Message.warning('视频名称不可为空')
+        isEditTitle.value = true;
+        Message.warning("视频名称不可为空");
       }
-    })
+    });
 
-    const isPlayVideo = ref(false)
+    const isPlayVideo = ref(false);
     const actions = {
-      continue: () => isShot.value = true,
-      delete: () => emit('delete'),
-      rename: () => isEditTitle.value = true,
-      play: () => isPlayVideo.value = true,
+      continue: () => (isShot.value = true),
+      delete: () => emit("delete"),
+      rename: () => (isEditTitle.value = true),
+      play: () => (isPlayVideo.value = true),
       download() {
-        const url = getResource(props.record.url!)
-        const ext = getExtname(url) || 'mp4'
-        loadPack(saveAs(url, `${props.record.title}.${ext}`))
+        const url = getResource(props.record.url!);
+        const ext = getExtname(url) || "mp4";
+        loadPack(saveAs(url, `${props.record.title}.${ext}`));
       },
-    }
-    props.record.immediately && actions.continue()
+    };
+    props.record.immediately && actions.continue();
 
     const closeHandler = () => {
-      if (getRecordFragmentBlobs(props.record).length === 0 && isTemploraryID(props.record.id)) {
-        emit('delete')
+      if (
+        getRecordFragmentBlobs(props.record).length === 0 &&
+        isTemploraryID(props.record.id)
+      ) {
+        emit("delete");
       }
-      isShot.value = false
-    }
+      isShot.value = false;
+    };
 
     const appendFragment = (blobs: Blob[]) => {
       recordFragments.value.push(
-        ...blobs.map(blob => createRecordFragment({ url: blob, recordId: props.record.id }))
-      )
-      props.record.status = RecordStatus.UN
-    }
+        ...blobs.map((blob) =>
+          createRecordFragment({ url: blob, recordId: props.record.id })
+        )
+      );
+      props.record.status = RecordStatus.UN;
+    };
 
     return {
       menus,
@@ -142,26 +157,24 @@ export default defineComponent({
       closeHandler,
       inputRef,
       RecordStatus,
-      MediaType,
+      MetaType,
       isPlayVideo,
       getResource,
       getFileUrl,
-      appendFragment
-    }
+      appendFragment,
+    };
   },
   components: {
     Shot,
-    Preview
-  }
-})
+    Preview,
+  },
+});
 </script>
 
-
-<style lang="scss" src="./style.scss" scoped>
-</style>
+<style lang="scss" src="./style.scss" scoped></style>
 
 <style>
-  .record-sign .ui-input .text.suffix input {
-    padding-right: 60px;
-  }
-</style>
+.record-sign .ui-input .text.suffix input {
+  padding-right: 60px;
+}
+</style>

+ 1 - 0
src/views/tagging/index.vue

@@ -128,6 +128,7 @@ const selectTagging = ref<Tagging | null>(null);
 useViewStack(() => {
   const stopAuth = autoSaveTaggings();
   const stop = watchEffect((onCleanup) => {
+    console.log(taggingsGroup, taggingsGroup.changeCanMove);
     taggingsGroup.changeCanMove(true);
     taggingsGroup.showDelete(true);
     onCleanup(() => {

+ 2 - 3
src/views/tagging/style-type-select.vue

@@ -1,8 +1,7 @@
 <template>
   <!-- <Menu style="width: 256px" mode="vertical" :items="getItems()" @click="handleClick" /> -->
-  <Dropdown placement="bottom">
+  <Dropdown placement="bottom" v-if="current">
     <span>
-      {{ current.title }}
       <span class="count" v-if="count">({{ current.count }})</span>
       <DownOutlined />
     </span>
@@ -20,7 +19,7 @@
 
 <script lang="ts" setup>
 import { styleTypes } from "@/api";
-import { computed } from "vue";
+import { computed, watchEffect } from "vue";
 import { Menu, Dropdown } from "ant-design-vue";
 import { DownOutlined } from "@ant-design/icons-vue";
 import { taggings, getTaggingStyle } from "@/store";

+ 61 - 51
src/views/view/index.vue

@@ -9,15 +9,15 @@
       </div>
     </template>
 
-    <ui-group title="全部视图" class="tree" >
+    <ui-group title="全部视图" class="tree">
       <Draggable :list="views" draggable=".sign" itemKey="id">
         <template #item="{ element: view }">
-          <Sign 
-            :view="view" 
-            :key="view.id" 
+          <Sign
+            :view="view"
+            :key="view.id"
             @delete="() => deleteView(view)"
-            @updateTitle="title => view.title = title"
-            @updateCover="cover => view.cover = cover"
+            @updateTitle="(title) => (view.title = title)"
+            @updateCover="(cover) => (view.cover = cover)"
           />
         </template>
       </Draggable>
@@ -26,67 +26,77 @@
 </template>
 
 <script lang="ts" setup>
-import { views, createView, autoSaveViews, initialViews, initialTaggingStyles, initialTaggings, initialMeasures } from '@/store'
-import { RightFillPano } from '@/layout'
-import { useViewStack } from '@/hook'
-import Draggable from 'vuedraggable'
-import Sign from './sign.vue'
-import { loadModel, currentModel, fuseModel } from '@/model'
-import { loadPack, togetherCallback } from '@/utils'
-import { Message } from 'bill/index'
-import { showLeftPanoStack } from '@/env'
-import { ref, watch } from 'vue'
+import {
+  views,
+  createView,
+  autoSaveViews,
+  initialViews,
+  initialTaggingStyles,
+  initialTaggings,
+  initialMeasures,
+} from "@/store";
+import { RightFillPano } from "@/layout";
+import { useViewStack } from "@/hook";
+import Draggable from "vuedraggable";
+import Sign from "./sign.vue";
+import { loadModel, currentModel, fuseModel } from "@/model";
+import { loadPack, togetherCallback } from "@/utils";
+import { Message } from "bill/index";
+import { showLeftPanoStack, showRightPanoStack } from "@/env";
+import { ref, watch } from "vue";
 
-import type { View } from '@/store'
+import type { View } from "@/store";
 
-initialViews()
-initialTaggingStyles(), 
-initialTaggings(), 
-initialMeasures()
+initialViews();
+initialTaggingStyles(), initialTaggings(), initialMeasures();
 const getView = async () => {
   try {
     const { image, flyData } = await loadPack(async () => {
-      const modelSDK = await loadModel(currentModel.value)
-      return await modelSDK.getView()
-    })
+      const modelSDK = await loadModel(currentModel.value);
+      return await modelSDK.getView();
+    });
 
-    const type = currentModel.value !== fuseModel 
-      ? { numType: currentModel.value.type, num: currentModel.value.num }
-      : {}
+    const type =
+      currentModel.value !== fuseModel
+        ? { numType: currentModel.value.type, num: currentModel.value.num }
+        : {};
 
-    views.value.push(createView({
-      flyData,
-      cover: {
-        blob: image,
-        url: URL.createObjectURL(image)
-      },
-      ...type
-    }))
+    views.value.push(
+      createView({
+        flyData,
+        cover: {
+          blob: image,
+          url: URL.createObjectURL(image),
+        },
+        ...type,
+      })
+    );
   } catch (e: any) {
-    console.error(e)
-    Message.error(e.message)
+    console.error(e);
+    Message.error(e.message);
   }
-}
+};
 const deleteView = (record: View) => {
-  const index = views.value.indexOf(record)
+  const index = views.value.indexOf(record);
   if (~index) {
-    views.value.splice(index, 1)
+    views.value.splice(index, 1);
   }
-}
+};
 
-const showLeftPano = ref(false)
+const showLeftPano = ref(false);
 watch(currentModel, () => {
   if (currentModel.value) {
-    showLeftPano.value = false
+    showLeftPano.value = false;
   }
-})
+});
 
-
-useViewStack(autoSaveViews)
-useViewStack(() => togetherCallback([
-  showLeftPanoStack.push(showLeftPano) 
-]))
+useViewStack(autoSaveViews);
+useViewStack(() =>
+  togetherCallback([
+    showLeftPanoStack.push(showLeftPano),
+    showRightPanoStack.push(ref(false)),
+  ])
+);
 </script>
 
-<style lang="scss" src="./style.scss" scoped>
-</style>
+<style lang="scss" src="./style.scss" scoped></style>

+ 9 - 11
src/views/view/show.vue

@@ -1,25 +1,23 @@
 <template>
   <LeftPano>
     <ui-group class="pano-group">
-      <Sign 
-        v-for="view in views" 
-        :view="view" 
-        :key="view.id" 
-        :edit="false"
-      />
+      <Sign v-for="view in views" :view="view" :key="view.id" :edit="false" />
     </ui-group>
   </LeftPano>
 </template>
 
 <script setup lang="ts">
-import Sign from './sign.vue'
-import { views } from '@/store'
-import { LeftPano } from '@/layout'
-
+import Sign from "./sign.vue";
+import { views } from "@/store";
+import { LeftPano } from "@/layout";
+import { useViewStack } from "@/hook";
+import { showRightPanoStack } from "@/env";
+import { ref } from "vue";
+useViewStack(() => showRightPanoStack.push(ref(false)));
 </script>
 
 <style lang="scss" scoped>
 .pano-group {
   padding: 0 20px 40px;
 }
-</style>
+</style>

+ 6 - 0
src/vite-env.d.ts

@@ -6,6 +6,12 @@ declare module '*.vue' {
   export default component
 }
 
+interface ImportMetaEnv {
+  readonly VITE_LASER_HOST: string
+}
+
+declare const offline: boolean
+
 type ToChangeAPI<T extends Record<string, any>> = {
   [key in keyof T as `change${Capitalize<key & string>}`]: (prop: T[key]) => void
 }

+ 29 - 5
vite.config.ts

@@ -5,6 +5,7 @@ import mkcert from 'vite-plugin-mkcert'
 
 import { resolve } from 'path'
 
+const ip = `http://192.168.0.25`
 const proxy = {
   '/fusion/ws': {
     target: 'wss://test-mix3d.4dkankan.com/',
@@ -17,33 +18,56 @@ const proxy = {
     rewrite: path => path.replace(/^\/local/, '')
   },
   '/fusion': {
-    target: config.dev ? 'https://test-mix3d.4dkankan.com' : 'https://mix3d.4dkankan.com',
+    target: ip,
     changeOrigin: true,
     rewrite: path => path.replace(/^\/api/, '')
   },
   '/swkk': {
-    target: config.dev ? 'https://test.4dkankan.com' : 'https://www.4dkankan.com',
+    target: `${ip}/`,
     changeOrigin: true,
     rewrite: path => path.replace(/^\/swkk/, '')
   },
+  // '/fdkk': {
+  //   target: `${ip}/`,
+  //   changeOrigin: true,
+  //   rewrite: path => path.replace(/^\/fdkk/, '/fdkk')
+  // },
+  '/laser-data': {
+    target: `${ip}/`,
+    changeOrigin: true,
+    rewrite: path => path.replace(/^\/laser-data/, '/laser-data')
+  },
   '/service': {
-    target: config.dev ? 'https://test.4dkankan.com' : 'https://www.4dkankan.com',
+    target: ip,
     changeOrigin: true,
+    rewrite: path => path.replace(/^\/service/, '/service')
   },
   '/swss': {
-    target: config.dev ? 'https://uat-laser.4dkankan.com/uat' : 'https://laser.4dkankan.com',
+    target: `${ip}/mega`,
     changeOrigin: true,
     rewrite: path => path.replace(/^\/swss/, '')
   },
   '/laser': {
-    target: config.dev ? 'https://uat-laser.4dkankan.com' : 'https://laser.4dkankan.com',
+    target: ip,
     changeOrigin: true,
     rewrite: path => path.replace(/^\/laser/, '/laser')
   }
 }
 
+let app = "index";
+if (process.argv.length > 3) {
+  app = process.argv[process.argv.length - 1].trim();
+}
+const input = {
+  [app]: resolve(__dirname, `${app}.html`),
+}
 // https://vitejs.dev/config/
 export default defineConfig({
+  build: {
+    rollupOptions:  {
+      input
+    },
+  },
   plugins: [vue(), mkcert() ],
   css: {
     preprocessorOptions: {