Parcourir la source

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

xzw il y a 6 mois
Parent
commit
922ae0c91f
43 fichiers modifiés avec 52940 ajouts et 1162 suppressions
  1. 5 0
      .env
  2. 4 0
      .offline.env
  3. 1 0
      index.html
  4. 16 0
      offline.html
  5. 2 0
      package.json
  6. 1262 834
      pnpm-lock.yaml
  7. 14 0
      public/xfile-viewer/.prettierrc
  8. 247 0
      public/xfile-viewer/index.html
  9. BIN
      public/xfile-viewer/publish/20231025172413.dcm
  10. BIN
      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. 45 3
      src/api/floder.ts
  19. 19 8
      src/api/folder-type.ts
  20. 12 7
      src/api/fuse-model.ts
  21. 67 40
      src/api/instance.ts
  22. 112 0
      src/api/offline.ts
  23. 2 2
      src/api/setup.ts
  24. 32 28
      src/components/static-preview/index.vue
  25. 85 0
      src/components/static-preview/resource.vue
  26. 24 42
      src/components/static-preview/sign.vue
  27. 2 1
      src/components/tagging/sign.vue
  28. 1 1
      src/env/index.ts
  29. 86 0
      src/hook/use-pixel.ts
  30. 21 21
      src/main.ts
  31. 65 8
      src/store/floder-type.ts
  32. 5 3
      src/store/floder.ts
  33. 21 2
      src/utils/meta.ts
  34. 12 0
      src/utils/params.ts
  35. 69 0
      src/views/folder/fire/index.vue
  36. 59 0
      src/views/folder/fire/info.vue
  37. 178 0
      src/views/folder/floder-view.vue
  38. 55 71
      src/views/folder/index.vue
  39. 136 0
      src/views/folder/modal-floder-view.vue
  40. 4 4
      src/views/record/shot.vue
  41. 100 87
      src/views/record/sign.vue
  42. 11 0
      src/vite-env.d.ts
  43. 13 0
      vite.config.ts

+ 5 - 0
.env

@@ -0,0 +1,5 @@
+VITE_LASER_HOST=
+VITE_LASER_OSS=/laser-data
+VITE_OSS=/oss
+VITE_PANO_OSS=/laser-data
+

+ 4 - 0
.offline.env

@@ -0,0 +1,4 @@
+VITE_LASER_HOST=
+VITE_LASER_OSS=/laser-data
+VITE_PANO_OSS=/laser-data
+

+ 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,9 +6,11 @@
   "scripts": {
     "dev": "vite",
     "build": "vite build",
+    "build-offline": " vite build ./ offline",
     "preview": "vite preview"
   },
   "dependencies": {
+    "@ant-design/icons-vue": "^7.0.1",
     "ant-design-vue": "3.2.20",
     "axios": "^0.27.2",
     "coordtransform": "^2.1.2",

Fichier diff supprimé car celui-ci est trop grand
+ 1262 - 834
pnpm-lock.yaml


+ 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>

BIN
public/xfile-viewer/publish/20231025172413.dcm


BIN
public/xfile-viewer/publish/20231025172413.raw


Fichier diff supprimé car celui-ci est trop grand
+ 9084 - 0
public/xfile-viewer/publish/lib/cornerstone.js


Fichier diff supprimé car celui-ci est trop grand
+ 3 - 0
public/xfile-viewer/publish/lib/cornerstoneMath.min.js


Fichier diff supprimé car celui-ci est trop grand
+ 38415 - 0
public/xfile-viewer/publish/lib/cornerstoneTools.js


Fichier diff supprimé car celui-ci est trop grand
+ 2 - 0
public/xfile-viewer/publish/lib/cornerstoneWADOImageLoader.bundle.min.js


Fichier diff supprimé car celui-ci est trop grand
+ 3 - 0
public/xfile-viewer/publish/lib/cornerstoneWebImageLoader.min.js


Fichier diff supprimé car celui-ci est trop grand
+ 3 - 0
public/xfile-viewer/publish/lib/dicomParser.min.js


Fichier diff supprimé car celui-ci est trop grand
+ 2643 - 0
public/xfile-viewer/publish/lib/hammer.js


+ 45 - 3
src/api/floder.ts

@@ -1,6 +1,7 @@
 import { params } from '@/env'
 import { FLODER_LIST } from './constant'
-import axios from './instance'
+import { addUnsetResErrorURLS,axios } from './instance'
+import {namespace} from '@/env'
 
 export interface Floder {
   filesId:	number,
@@ -13,5 +14,46 @@ export interface Floder {
 export type Floders = Floder[]
 
 
-export const fetchFloders = () => 
-  axios.get<Floders>(FLODER_LIST, { params: { caseId: params.caseId } })
+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 } })
+  // debugger
+  const otherFloders = [{
+    filesId: 88,
+    filesTypeId: 100,
+    filesTitle: '勘验笔录',
+    ex: `${namespace}/caseInquest/info`,
+    bex: `${namespace}/caseInquest/downDocx`
+  }, {
+    filesId: 89,
+    filesTypeId: 100,
+    filesTitle: '提取清单',
+    ex: `${namespace}/caseExtractDetail/info`,
+    bex: `${namespace}/caseExtractDetail/downDocx`
+  }, ]
+  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({
+        filesId: 100 + ndx,
+        filesTypeId: -1,
+        filesTitle: '照片制卷图',
+        caseId: params.caseId.toString(),
+        filesUrl: item.imgUrl
+      })
+    })
+  }
+  await Promise.all(otherFloders.map(async of => {
+    const kybl = await axios.get(of.ex, { params: { caseId: params.caseId } })
+    if (kybl) {
+      const data = await axios.get(of.bex, { params: { caseId: params.caseId }, responseType: 'blob' })
+      const blob = data.data
+      floders.push({
+        ...of,
+        caseId: params.caseId.toString(),
+        filesUrl: URL.createObjectURL(blob)
+      })
+    }
+  }))
+  return floders
+}

+ 19 - 8
src/api/folder-type.ts

@@ -1,13 +1,24 @@
-import { FOLDER_TYPE_LIST } from './constant'
-import axios from './instance'
+import { FOLDER_TYPE_LIST } from "./constant";
+import axios from "./instance";
 
 export interface FloderType {
-  filesTypeId: number,
-  filesTypeName: string
+  filesTypeId: number;
+  filesTypeName: string;
+  parentId: number;
+  modalShow?: boolean;
+  flatShow?: boolean;
 }
 
-export type FloderTypes = FloderType[]
+export type FloderTypes = FloderType[];
 
-
-export const fetchFloderTypes = () => 
-  axios.get<FloderTypes>(FOLDER_TYPE_LIST)
+export const fetchFloderTypes = async () => {
+  const types = await axios.get<FloderTypes>(FOLDER_TYPE_LIST);
+  types.push({
+    filesTypeId: -1,
+    filesTypeName: "照片制卷",
+    flatShow: false,
+    modalShow: false,
+    parentId: 39,
+  });
+  return types;
+};

+ 12 - 7
src/api/fuse-model.ts

@@ -47,9 +47,13 @@ interface ServiceFuseModel {
   sceneData: Scene;
 }
 
+export const uploadMaterialToModel = async (uploadId: number) => {
+  const model = await axios.post<{modelId: number}>('/fusion/model/addByMediaLibrary', {caseId: params.caseId, uploadId})
+  return model
+}
+
 export const getSceneUrl = (sceneData: Scene) => {
   let url: any = [""];
-  console.log(sceneData, sceneData.type);
   if (
     [SceneType.SWSS, SceneType.SWYDSS].includes(sceneData.type)
   ) {
@@ -60,14 +64,17 @@ export const getSceneUrl = (sceneData: Scene) => {
         url = sceneData.model3dgsUrl;
         break;
       case "shp":
-        url = sceneData.modelShpUrl;
+        url = sceneData.modelGlbUrl;
         break;
       default:
-        url = sceneData.modelGlbUrl;
+        url = sceneData.modelGlbUrl || sceneData.modelObjUrl;
     }
     try {
       url = JSON.parse(url);
     } catch (e) {
+      if (typeof url === 'string') {
+        url = [url]
+      }
       console.error(url, e);
     }
   }
@@ -97,7 +104,7 @@ const serviceToLocal = (
       : "-",
     modelId: serviceModel.sceneData?.modelId,
     fusionId: serviceModel.fusionId,
-    type: serviceModel.sceneData?.type,
+    type: serviceModel.sceneData?.type === SceneType.DSFXJ ? SceneType.SWKK : serviceModel.sceneData?.type,
     size: serviceModel.sceneData?.modelSize,
     raw: serviceModel.sceneData,
     time: serviceModel.sceneData?.createTime,
@@ -128,9 +135,7 @@ export const fetchFuseModels = async () => {
   const serviceModels = await axios.get<ServiceFuseModel[]>(FUSE_MODEL_LIST, {
     params: { caseId: params.caseId },
   });
-  console.error(
-    serviceModels.map((item, index) => serviceToLocal(item, index == 0))
-  );
+  console.log('===>', serviceModels.map((item, index) => serviceToLocal(item, index == 0)))
   return serviceModels.map((item, index) => serviceToLocal(item, index == 0));
 };
 

+ 67 - 40
src/api/instance.ts

@@ -1,19 +1,21 @@
-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 { Dialog, Message } from "bill/index";
+import { showLoad, hideLoad } from "@/utils";
+import * as URL from "./constant";
+import { ResCode, ResCodeDesc } from "./constant";
+import { baseURL, params } from "@/env";
+import GAxios from "axios";
 
-const instance = axiosFactory()
+const instance = axiosFactory();
 
 export const {
-  axios,
+  axios, 
   addUnsetTokenURLS,
   delUnsetTokenURLS,
   addReqErrorHandler,
   addResErrorHandler,
   delReqErrorHandler,
+  addUnsetResErrorURLS,
   delResErrorHandler,
   getToken,
   setToken,
@@ -21,50 +23,75 @@ 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'
-}
+  GAxios.post("/service/manage/login", {
+    password: "MRinIEn3ExMjM0NTY=Q5Lm39urQWzN7k4oCG",
+    userName: "super-admin",
+    username: "super-admin",
+  }).then((res) => {
+    setToken(res.data.data.token)
+    setTimeout(() => location.reload())
+  });
+  // 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())
+  //   });
+  // } else {
+  //   const loginHref = `/admin/#/statistics/scene`
+  //   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 if (data.code === ResCode.UN_AUTH) {
+      Dialog.alert({content: msg, okText: '我知道了'}).then(() => {
+        gotoLogin();
+      })
+      throw msg
+    } 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;

+ 112 - 0
src/api/offline.ts

@@ -0,0 +1,112 @@
+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 offlinePrev = import.meta.env.DEV ? './offlineData/' : './'
+export const setOfflineAxios = (axios: AxiosInstance) => {
+  const data: {[key in string]: any} = {}
+  Axios.get(`./${offlinePrev}package/data.json`, {headers: { Accept: "application/json"}}).then(res => {
+    Object.assign(data, res.data)
+    const prev = import.meta.env.DEV ? offlinePrev : offlinePrev.substring(0, offlinePrev.length - 1)
+    for (const key in data) {
+      data[prev + key] = data[key]
+    }
+    ;(window as any).__data = data
+    ;(window as any).offlineData = new Proxy({}, {
+      get(t, key) {
+        if (key in data) {
+          return data[key as any]
+        } else {
+          console.log('请求', key)
+          return Axios.get(key as any, {headers: { Accept: "application/json"}})
+            .then((res) => {
+              data[key as any] = res.data
+              return res.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)] 
+      console.log(data, item)
+      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))
+  };
+}

+ 2 - 2
src/api/setup.ts

@@ -1,6 +1,6 @@
 import Axios from 'axios'
 import { ResCode } from './constant'
-
+import { setOfflineAxios } from './offline'
 import type { AxiosResponse, AxiosRequestConfig } from 'axios'
 
 export type ResErrorHandler = <D, T extends ResData<D>>(response: AxiosResponse<T>, data?: T) => void
@@ -148,7 +148,7 @@ export const axiosFactory = () => {
       return config
     }
   )
-
+  offline && setOfflineAxios(axiosRaw)
   axiosRaw.interceptors.response.use(
     (response: AxiosResponse<ResData<any>>) => {
       for (const hook of axiosConfig.hook) {

+ 32 - 28
src/components/static-preview/index.vue

@@ -3,54 +3,58 @@
     <span class="close pc" @click="$emit('close')">
       <ui-icon type="close" ctrl />
     </span>
-    <div class="pull-preview pc">
-      <ui-slide v-if="items.length > 1" :currentIndex="current" showCtrl :items="items" showInfos>
-        <template v-slot="{ raw }">
-          <Sign :media="raw" />
+    <div class="pull-preview pc" :class="pclass">
+      <ui-slide
+        v-if="items.length > 1"
+        :currentIndex="current"
+        showCtrl
+        :items="items"
+        showInfos
+      >
+        <template v-slot="{ raw, active }">
+          <Sign :media="raw" :focus="active === raw" />
         </template>
       </ui-slide>
-      <Sign :media="items[0]" v-else />
+      <Sign :media="items[0]" focus v-else />
     </div>
   </teleport>
 </template>
 
 <script lang="ts">
-import { defineComponent, PropType, ref } from 'vue'
-import Sign from './sign.vue'
-
-export enum MediaType {
-  video,
-  img,
-  web
-}
+import { defineComponent, PropType, ref } from "vue";
+import Sign from "./sign.vue";
+import { MetaType } from "@/utils";
 
 export type MediaItem = {
-  url: Blob | string,
-  type: MediaType
-}
+  url: Blob | string;
+  type?: MetaType;
+};
 
-export const Preview =  defineComponent({
-  name: 'static-preview',
+export const Preview = defineComponent({
+  name: "static-preview",
   props: {
+    pclass: {
+      type: String,
+      required: false,
+    },
     items: {
       type: Array as PropType<MediaItem[]>,
-      required: true
+      required: true,
     },
     current: {
       type: Number,
-      default: 1
-    }
+      default: 1,
+    },
   },
   emits: {
-    close: () => true
+    close: () => true,
   },
   components: {
-    Sign
-  }
-})
-
+    Sign,
+  },
+});
 
-export default Preview
+export default Preview;
 </script>
 
-<style scoped lang="scss" src="./style.scss"></style>
+<style scoped lang="scss" src="./style.scss"></style>

+ 85 - 0
src/components/static-preview/resource.vue

@@ -0,0 +1,85 @@
+<template>
+  <div v-if="focus">
+   
+    <video
+      v-if="type === MetaType.video"
+      controls
+      autoplay
+      playsinline
+      webkit-playsinline
+    >
+      <source :src="url" />
+    </video>
+    <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 === MetaType.audio"
+      controls
+      autoplay
+      playsinline
+      webkit-playsinline
+    />
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { getResource } from "@/env";
+import { computed } from "vue";
+import { getUrlType, MetaType } from "@/utils/meta";
+
+const props = defineProps<{
+  data: string | Blob | File;
+  type?: MetaType;
+  focus?: boolean;
+}>();
+
+const url = computed(() =>
+  typeof props.data === "string"
+    ? getResource(props.data)
+    : URL.createObjectURL(props.data)
+);
+
+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;
+    return getUrlType(d);
+  } else {
+    return MetaType.other;
+  }
+});
+</script>
+
+<style scoped>
+div {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+audio,
+iframe {
+  width: 100%;
+  height: 100%;
+  display: block;
+}
+
+video,
+img {
+  max-width: 100%;
+  max-height: 100%;
+  display: block;
+  object-fit: cover;
+}
+
+iframe {
+  border: none;
+}
+</style>

+ 24 - 42
src/components/static-preview/sign.vue

@@ -1,54 +1,36 @@
 <template>
   <div class="preview-layer">
     <div class="pull-meta">
-      <video v-if="media.type === MediaType.video" controls autoplay playsinline webkit-playsinline>
-        <source :src="staticURL" />
-      </video>
-      <iframe v-else-if="media.type === MediaType.web" :src="staticURL"></iframe>
-      <img :src="staticURL" v-if="media.type === MediaType.img"/>
+      <ResourceView :data="media.url" :type="media.type" :focus="focus" />
     </div>
   </div>
 </template>
 
-<script lang="ts">
-import { ref, watchEffect, defineComponent, PropType } from 'vue'
-import { getResource } from '@/env'
-import { MediaType } from './index.vue'
-import type { MediaItem } from './index.vue'
+<script lang="ts" setup>
+import { ref, watchEffect } from "vue";
+import { getResource } from "@/env";
+import ResourceView from "./resource.vue";
+import type { MediaItem } from "./index.vue";
 
+const props = defineProps<{ media: MediaItem; focus?: boolean }>();
+defineEmits<{ (e: "close", v: boolean): void }>();
 
-export const Preview =  defineComponent({
-  name: 'static-preview',
-  props: {
-    media: {
-      type: Object as PropType<MediaItem>,
-        required: true
-    }
-  },
-  emits: {
-    close: () => true
-  },
-  setup(props) {
-    const staticURL = ref('')
-    watchEffect(() => {
-      const data = props.media.url
-      const url = typeof data === 'string'
-        ? getResource(data)
-        : URL.createObjectURL(data)
+const staticURL = ref("");
+watchEffect(() => {
+  const data = props.media.url;
+  const url = typeof data === "string" ? getResource(data) : URL.createObjectURL(data);
 
-      staticURL.value = url
-      return () => URL.revokeObjectURL(url)
-    })
-    
-    return {
-      staticURL,
-      MediaType
-    }
-  }
-})
-
-
-export default Preview
+  staticURL.value = url;
+  return () => URL.revokeObjectURL(url);
+});
 </script>
 
-<style scoped lang="scss" src="./style.scss"></style>
+<style scoped lang="scss" src="./style.scss"></style>
+
+<style scoped>
+.preview-layer,
+.pull-meta {
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 2 - 1
src/components/tagging/sign.vue

@@ -57,7 +57,8 @@ 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 { MetaType } from "@/utils";
 import { getTaggingStyle, getFuseModel } from '@/store';
 import { getFileUrl } from '@/utils'
 import { sdk } from '@/sdk'

+ 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))

+ 86 - 0
src/hook/use-pixel.ts

@@ -0,0 +1,86 @@
+import { sdk } from "@/sdk";
+import { ref, Ref, UnwrapRef, watch, watchEffect } from "vue";
+import { useViewStack } from "./viewStack";
+
+export const useCameraChange = <T>(change: () => T) => {
+  const data = ref(change());
+  let isPause = false
+  const update = () => {
+    if (isPause) return;
+    data.value = change() as UnwrapRef<T>;
+  };
+  useViewStack(() => {
+    sdk.sceneBus.on("cameraChange", update);
+    return () => {
+      sdk.sceneBus.off("cameraChange", update);
+    };
+  });
+  return [data, () => isPause = true, () => {
+    isPause = false
+    update()
+}] as const;
+};
+
+export const usePixel = (
+  getter: () => { localPos: SceneLocalPos; modelId?: string } | undefined
+) => {
+  const pos = ref(getter());
+  const getPosStyle = () => {
+    if (!pos.value) return void 0;
+    const screenPos = sdk.getScreenByPosition(
+      pos.value.localPos,
+      pos.value.modelId
+    );
+    if (!screenPos) return void 0;
+
+    return {
+      left: screenPos.pos.x + "px",
+      top: screenPos.pos.y + "px",
+    };
+  };
+  const [pixel, pause, recovery] = useCameraChange(getPosStyle);
+
+  useViewStack(() => {
+    watch(getter, (val) => (pos.value = val));
+    return watch(pos, () => {
+      pixel.value = getPosStyle()
+    }, { deep: true });
+  });
+
+  return [pixel, pos, pause, recovery] as const;
+};
+
+export const usePixels = (
+  getter: () => { localPos: SceneLocalPos; modelId: string }[]
+) => {
+  const positions = ref(getter());
+  watch(getter, (val) => {
+    positions.value = val;
+  });
+
+  const getPosStyle = () => {
+    const pixels: ({left: string, top: string} | null)[] = [];
+    for (let i = 0; i < positions.value.length; i++) {
+      const pos = positions.value[i];
+      const screenPos = sdk.getScreenByPosition(pos.localPos, pos.modelId);
+      if (screenPos) {
+        pixels[i] = {
+          left: screenPos.pos.x + "px",
+          top: screenPos.pos.y + "px",
+        };
+      } else {
+        pixels[i] = null;
+      }
+    }
+  };
+  const [pixels, pause, recovery] = useCameraChange(getPosStyle)
+
+  useViewStack(() => {
+    watch(getter, (val) => (positions.value = val));
+    return watch(positions, () => {
+      pixels.value = getPosStyle()
+    }, { deep: true });
+  });
+
+  return [pixels, positions, pause, recovery] as const;
+};

+ 21 - 21
src/main.ts

@@ -17,33 +17,33 @@ app.mount('#app')
 if (import.meta.env.DEV) {
   import('@/hook/notice')
 }
+appStyleImport[params.app || '1']()
 
-appStyleImport[params.app]()
 watchEffect((onCleanup) => {
   if ([RoutesName.show, RoutesName.signModel].includes(currentLayout.value!)) {
 
-    const untokenURLS = params.share 
+    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.AUTH_PWD
-        ]
+        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]
-      
+
     const apiHook = {
       before(config: any) {
         if (config.headers) {

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

@@ -1,14 +1,71 @@
-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 || (type.parentId === undefined && parentId === null)) {
+      const item = {
+        id: type.filesTypeId,
+        title: type.filesTypeName,
+        flat: !!type.flatShow,
+        modal: !!type.modalShow,
+        floders: getFloderByType(type).map((floder) => {
+          !floder.filesUrl &&console.log(floder.filesUrl)
+          return{
+          ...floder,
+          metaType: floder.filesUrl && 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 };

+ 5 - 3
src/store/floder.ts

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

+ 21 - 2
src/utils/meta.ts

@@ -1,12 +1,16 @@
 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.video]: ['wmv', 'asf', 'asx', 'rm', 'rmvb', 'mp4', '3gp', 'mov', 'm4v', 'avi', 'dat', 'mkv', 'flv', 'vob']
+  [MetaType.image]: ['bmp', 'jpg', 'jpeg', 'png', 'tif', 'gif', 'pcx', 'tga', 'exif', 'fpx', 'svg', 'psd', 'cdr', 'pcd', 'dxf', 'ufo', 'eps', 'ai', 'WMF', 'webp', 'avif', 'apng'],
+  [MetaType.audio]: ['mp3'],
+  [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) => {
@@ -19,6 +23,21 @@ export const getExtname = (url: string) => {
 }
 
 export const getUrlType = (url: string) => {
+  if (url.includes(';base64')) {
+    const type = url.substring(url.indexOf(':') + 1, url.indexOf('/'))
+    if (type === 'image') {
+      return MetaType.image
+    } else if (type === 'video') {
+      return MetaType.video
+    } else if (type === 'audio') {
+      return MetaType.audio
+    } else if (type === 'raw' || type === 'dcm') {
+      return MetaType.xfile
+    } else {
+      return MetaType.other
+    }
+  }
+
   const extname = getExtname(url)?.toLowerCase()
   if (extname) {
     for (const [type, extnames] of Object.entries(metaTypeExtnames)) {

+ 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 ''
+  }
 }

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

@@ -0,0 +1,69 @@
+<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"
+    />
+    <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 === null ? "" : v ? "是" : "否")],
+  criminalCase: [
+    "是否刑案",
+    (v: any) => {
+      console.error(v);
+      return v === null ? "" : v ? "是" : "否";
+    },
+  ],
+  caseRegion: ["案发区域", (v: string[]) => v.join("-")],
+  caseAddress: "案发地点",
+  latAndLong: ["经纬度", (v: any) => (v ? v.split(",").reverse().join(",") : "")],
+} as LabelMap;
+
+const tmLabelMap2 = {
+  commandTime: "指挥中心电话时间",
+  alarmTime: "报警时间",
+  alarmName: "报警人",
+  assignDept: "指派/报告单位",
+  inquestDept: "现场勘验单位",
+  assignType: "指派方式",
+  inquestAddress: "勘验地点",
+  times: ["勘验时间", (v: string[]) => v.join("到")],
+} as LabelMap;
+</script>
+
+<style lang="scss" scoped></style>

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

@@ -0,0 +1,59 @@
+<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: 10px 0;
+    padding: 0 7px;
+    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: 10px;
+      color: rgba(255, 255, 255, 0.7);
+    }
+  }
+}
+</style>

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

@@ -0,0 +1,178 @@
+<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 }"
+      @click="showChildren = !showChildren"
+    >
+      <span> {{ 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="1200px"
+    :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, toRaw, watchEffect } 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);
+watchEffect(() => {
+  if (showChildren.value) {
+    console.log(toRaw(props.root));
+  }
+});
+</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;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    overflow: hidden;
+  }
+}
+</style>

+ 55 - 71
src/views/folder/index.vue

@@ -1,90 +1,74 @@
 <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 { getUrlType, MetaType } from '@/utils'
-import { Preview, MediaItem, MediaType } from '@/components/static-preview/index.vue'
-import { floderTypes, getFloderByType } from '@/store'
-
-import type { Floder } from '@/store'
-import { useViewStack } from '@/hook';
-import { showRightPanoStack } from '@/env';
+import { LeftPano } from "@/layout";
+import { ref } from "vue";
+import { getUrlType, MetaType, saveAs } from "@/utils";
+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";
 
-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)
-      })),
-  }))
-)
+import type { Floder, FloderRoot } from "@/store";
+import { useViewStack } from "@/hook";
+import { showRightPanoStack } from "@/env";
 
-const typeIcons = {
-  [MetaType.image]: 'pic',
-  [MetaType.video]: 'a-film',
-  [MetaType.other]: 'nav-edit'
-}
-
-const currentFile = ref<MediaItem | null>(null)
-const preview = (floder: Floder) => {
-  const type = getUrlType(floder.filesUrl)
-  const mediaType = type === MetaType.image 
-      ? MediaType.img 
-      : type === MetaType.video
-        ? MediaType.video
-        : null
-  
-  if (!mediaType) {
-    window.open(floder.filesUrl)
-  } else {
-    currentFile.value = {
-      type: mediaType,
-      url: floder.filesUrl
+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 {
+    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)))
-
+};
+useViewStack(() => showRightPanoStack.push(ref(false)));
 </script>
 
 <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;
-    border-bottom: 1px solid rgba(255,255,255,0.16);
+    border-bottom: 1px solid rgba(255, 255, 255, 0.16);
     align-items: center;
     margin-bottom: 0;
     cursor: pointer;
@@ -96,12 +80,12 @@ useViewStack(() => showRightPanoStack.push(ref(false)))
 }
 
 .floders {
-  background: rgba(0,0,0,0.5);
+  background: rgba(0, 0, 0, 0.5);
 
   > div {
     margin: 0 20px;
     padding: 20px 10px;
-    border-bottom: 1px solid rgba(255,255,255,0.16);
+    border-bottom: 1px solid rgba(255, 255, 255, 0.16);
     cursor: pointer;
     display: flex;
     align-items: center;
@@ -110,8 +94,8 @@ useViewStack(() => showRightPanoStack.push(ref(false)))
       margin-left: 10px;
       font-size: 12px;
       color: currentColor;
-      word-break:break-all;
+      word-break: break-all;
     }
   }
 }
-</style>
+</style>

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

@@ -0,0 +1,136 @@
+<template>
+  <ui-group v-if="floders.length">
+    <ui-group-option>
+      <span @click="canAll && (showAll = !showAll)" :class="{ ['fun-ctrl']: canAll }">
+        <template v-if="canAll">
+          <UpOutlined v-if="showAll" />
+          <DownOutlined v-else />
+        </template>
+        {{ root.title }}
+      </span>
+    </ui-group-option>
+    <ui-group-option>
+      <div class="items">
+        <div
+          class="img-item"
+          v-for="(_, i) in showLen"
+          :key="floders[i].filesId"
+          :style="{ '--rawLen': samLen }"
+        >
+          <div class="img-item-content">
+            <div>
+              <Sign
+                focus
+                :media="{ url: floders[i].filesUrl }"
+                @click="clickHandler(floders[i])"
+              />
+            </div>
+            <!-- <img :src="floders[i].filesUrl"  /> -->
+          </div>
+        </div>
+      </div>
+    </ui-group-option>
+  </ui-group>
+
+  <Tabs v-if="!emptyTabs" v-model:activeKey="activeTab" class="f-tabs">
+    <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" v-else>
+    <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";
+import Sign from "@/components/static-preview/sign.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 = 6;
+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: calc(100% / var(--rawLen));
+  padding-right: 5px;
+  .img-item-content {
+    padding-top: 56.25%;
+    position: relative;
+    cursor: pointer;
+
+    > div {
+      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>
+
+<style>
+.f-tabs.ant-tabs-top > .ant-tabs-nav {
+  margin-bottom: 30px;
+}
+.f-tabs.ant-tabs-top > .ant-tabs-nav::before {
+  display: none !important;
+}
+</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>

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

@@ -6,12 +6,23 @@ declare module '*.vue' {
   export default component
 }
 
+interface ImportMetaEnv {
+  readonly VITE_LASER_HOST: string
+  readonly VITE_LASER_OSS: string
+  readonly VITE_PANO_OSS: string
+  readonly VITE_OSS: 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
 }
 
 type SceneLocalPos = { x: number, y: number, z: number }
 type ScreenLocalPos = { x: number, y: number }
+type Rotation = { x: number, y: number, z: number, w: number }
 
 type LocalFile = { url: string, blob: Blob }
 type LocalMode<T, K> = T extends any[]

+ 13 - 0
vite.config.ts

@@ -42,8 +42,21 @@ const proxy = {
   }
 }
 
+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: {