bill 8 mēneši atpakaļ
vecāks
revīzija
23b95bf005
48 mainītis faili ar 51389 papildinājumiem un 568 dzēšanām
  1. 14 0
      public/xfile-viewer/.prettierrc
  2. 247 0
      public/xfile-viewer/index.html
  3. BIN
      public/xfile-viewer/publish/20231025172413.dcm
  4. BIN
      public/xfile-viewer/publish/20231025172413.raw
  5. 9084 0
      public/xfile-viewer/publish/lib/cornerstone.js
  6. 3 0
      public/xfile-viewer/publish/lib/cornerstoneMath.min.js
  7. 38415 0
      public/xfile-viewer/publish/lib/cornerstoneTools.js
  8. 2 0
      public/xfile-viewer/publish/lib/cornerstoneWADOImageLoader.bundle.min.js
  9. 3 0
      public/xfile-viewer/publish/lib/cornerstoneWebImageLoader.min.js
  10. 3 0
      public/xfile-viewer/publish/lib/dicomParser.min.js
  11. 2643 0
      public/xfile-viewer/publish/lib/hammer.js
  12. 6 0
      src/api/constant.ts
  13. 0 19
      src/api/floder.ts
  14. 3 39
      src/api/folder-type.ts
  15. 52 39
      src/api/instance.ts
  16. 8 1
      src/api/scene.ts
  17. 3 3
      src/api/setup.ts
  18. 31 30
      src/api/sys.ts
  19. 22 0
      src/api/user.ts
  20. 11 11
      src/app.vue
  21. 2 8
      src/components/static-preview/index.vue
  22. 12 16
      src/components/static-preview/resource.vue
  23. 3 3
      src/components/tagging/sign.vue
  24. 1 0
      src/layout/edit/scene-select.vue
  25. 29 2
      src/layout/scene-list/index.vue
  26. 8 2
      src/layout/show/index.vue
  27. 12 14
      src/layout/show/slide-menu.vue
  28. 17 17
      src/main.ts
  29. 16 1
      src/model/app.vue
  30. 6 6
      src/router/config.ts
  31. 7 1
      src/router/index.ts
  32. 28 3
      src/store/floder-type.ts
  33. 0 1
      src/store/floder.ts
  34. 57 1
      src/store/scene.ts
  35. 5 1
      src/utils/meta.ts
  36. 1 8
      src/views/fire/index.vue
  37. 61 0
      src/views/folder/fire/index.vue
  38. 58 0
      src/views/folder/fire/info.vue
  39. 0 87
      src/views/folder/floder-root-view.vue
  40. 172 0
      src/views/folder/floder-view.vue
  41. 37 94
      src/views/folder/index.vue
  42. 114 0
      src/views/folder/modal-floder-view.vue
  43. 4 4
      src/views/record/shot.vue
  44. 100 87
      src/views/record/sign.vue
  45. 2 3
      src/views/tagging/style-type-select.vue
  46. 61 51
      src/views/view/index.vue
  47. 9 11
      src/views/view/show.vue
  48. 17 5
      vite.config.ts

+ 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


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 9084 - 0
public/xfile-viewer/publish/lib/cornerstone.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 3 - 0
public/xfile-viewer/publish/lib/cornerstoneMath.min.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 38415 - 0
public/xfile-viewer/publish/lib/cornerstoneTools.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 2 - 0
public/xfile-viewer/publish/lib/cornerstoneWADOImageLoader.bundle.min.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 3 - 0
public/xfile-viewer/publish/lib/cornerstoneWebImageLoader.min.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 3 - 0
public/xfile-viewer/publish/lib/dicomParser.min.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 2643 - 0
public/xfile-viewer/publish/lib/hammer.js


+ 6 - 0
src/api/constant.ts

@@ -14,7 +14,12 @@ export const UPLOAD_HEADS = {
   'Content-Type': 'multipart/form-data'
 }
 
+
+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 = `${namespace}/web/fireProject/getDetailWithoutAuth`
 
@@ -27,6 +32,7 @@ export const FUSE_DELETE_MODEL = `${namespace}/caseFusion/delete`
 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 = `${namespace}/caseTag/allList`

+ 0 - 19
src/api/floder.ts

@@ -54,24 +54,5 @@ export const fetchFloders = async () => {
       })
     }
   }))
-
-
-  // floders.forEach(item => item.filesTypeId = 102)
-  // floders.push(...floders.map(item => {
-  //   return {...item, filesTypeId: 101 }
-  // }))
-
-  floders.push(...floders.map(item => {
-    return {...item, filesTypeId: 1000 }
-  }))
-  floders.push(...floders.map(item => {
-    return {...item, filesTypeId: 1001 }
-  }))
-  floders.push(...floders.map(item => {
-    return {...item, filesTypeId: 1002 }
-  }))
-  floders.push(...floders.map(item => {
-    return {...item, filesTypeId: 1005 }
-  }))
   return floders
 }

+ 3 - 39
src/api/folder-type.ts

@@ -4,50 +4,14 @@ import axios from './instance'
 export interface FloderType {
   filesTypeId: number,
   filesTypeName: string,
-  parentFilesTypeId?: number,
+  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: '其他',
-  })
-
-  types.push({
-    filesTypeId: 101,
-    filesTypeName: '现场照片',
-  })
-  types.push({
-    parentFilesTypeId: 101,
-    filesTypeId: 102,
-    filesTypeName: '中心现场',
-  })
-
-  types.push({
-    filesTypeId: 1000,
-    parentFilesTypeId: 102,
-    filesTypeName: '方位',
-  }, {
-    filesTypeId: 1001,
-    parentFilesTypeId: 102,
-    filesTypeName: '概貌',
-  }, {
-    filesTypeId: 1002,
-    parentFilesTypeId: 102,
-    filesTypeName: '重点部位',
-  },{
-    filesTypeId: 1001,
-    parentFilesTypeId: 103,
-    filesTypeName: '概貌',
-  },)
-  types.push({
-    parentFilesTypeId: 101,
-    filesTypeId: 103,
-    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;

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

+ 3 - 3
src/api/setup.ts

@@ -15,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[],
@@ -66,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
   }
   

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

+ 12 - 14
src/layout/show/slide-menu.vue

@@ -30,18 +30,16 @@ defineEmits<{ (e: "changeItem", item: MenuItem): void }>();
 const items = computed(() => {
   const items = [
     {
+      name: RoutesName.fireInfo,
+      config: getRouteConfig(RoutesName.fireInfo),
+      ...metas[RoutesName.fireInfo],
+    },
+    {
       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],
-    });
-  }
 
   if (views.value.length) {
     items.push({
@@ -59,13 +57,13 @@ const items = computed(() => {
     });
   }
 
-  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;
 });

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

+ 16 - 1
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;
@@ -73,10 +74,24 @@ export const Model = defineComponent({
             [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.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]);
     });
 

+ 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')
+      // },
       
     ]
   },

+ 7 - 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, watchEffect } from 'vue'
 import { RoutesName } from './constant'
 import { metas } from './constant'
 
@@ -55,6 +55,12 @@ export const currentMeta = computed(() => {
   }
 })
 
+watchEffect(() => {
+  if (!currentLayout.value) {
+    router.replace({ name: RoutesName.fireInfo })
+  }
+})
+
 export * from './config'
 export * from './constant'
 

+ 28 - 3
src/store/floder-type.ts

@@ -1,7 +1,7 @@
 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";
 
@@ -14,19 +14,23 @@ export const initialFloderTypes = async () => {
 };
 
 export type FloderRoot = {
+  flat: boolean,
+  modal: boolean
   id: number;
   title: string;
   floders: (ReturnType<typeof getFloderByType>[number] & { metaType: MetaType })[];
   children?: FloderRoot[];
 };
-const gemerateRoot = (parentId?: number) => {
+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.parentFilesTypeId === parentId) {
+    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),
@@ -40,4 +44,25 @@ const gemerateRoot = (parentId?: number) => {
 };
 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 };

+ 0 - 1
src/store/floder.ts

@@ -9,7 +9,6 @@ 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 - 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
     }

+ 1 - 8
src/views/fire/index.vue

@@ -54,14 +54,7 @@ const tmLabelMap2 = {
   fireReason: "勘验时间",
 } as LabelMap;
 
-useViewStack(() => {
-  const f1 = showRightPanoStack.push(ref(false));
-  // const f2 = showLeftPanoStack.push(ref(false));
-  return () => {
-    f1();
-    // f2();
-  };
-});
+useViewStack(() => showRightPanoStack.push(ref(false)));
 </script>
 
 <style lang="scss" scoped></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>

+ 0 - 87
src/views/folder/floder-root-view.vue

@@ -1,87 +0,0 @@
-<template>
-  <ui-group v-if="root.floders.length">
-    <ui-group-option>{{ root.title }}</ui-group-option>
-    <ui-group-option>
-      <swiper
-        :slidesPerView="3"
-        :spaceBetween="10"
-        :pagination="{
-          type: 'fraction',
-        }"
-        :navigation="true"
-        :modules="[Pagination, Navigation]"
-        class="mySwiper"
-      >
-        <swiper-slide v-for="floder in root.floders" :key="floder.filesId">
-          <div class="img-item">
-            <img :src="floder.filesUrl" @click="clickHandler(floder)" />
-          </div>
-        </swiper-slide>
-      </swiper>
-    </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="!isLastLevel(children)">
-        <FloderRootView :root="children" @preview="(f: Floder) => emit('preview', f)" />
-      </TabPane>
-    </template>
-  </Tabs>
-
-  <template v-for="children in root.children" :key="children.id">
-    <FloderRootView
-      :root="children"
-      v-if="isLastLevel(children)"
-      @preview="(f: Floder) => emit('preview', f)"
-    />
-  </template>
-</template>
-<script lang="ts" setup>
-import { Floder, FloderRoot, Floders } from "@/store";
-import { computed, ref } from "vue";
-import { TabPane, Tabs } from "ant-design-vue";
-import { Swiper, SwiperSlide } from "swiper/vue";
-import { Pagination, Navigation } from "swiper/modules";
-import "swiper/css";
-import "swiper/css/pagination";
-import "swiper/css/navigation";
-
-const props = defineProps<{ root: FloderRoot }>();
-const emit = defineEmits<{ (e: "preview", v: Floder): void }>();
-const isLastLevel = (root: FloderRoot) => {
-  return !root.children?.length;
-};
-const emptyTabs = computed(() => props.root.children?.every((r) => isLastLevel(r)));
-const oneTabs = computed(() => {
-  if (!emptyTabs.value) return null;
-  return props.root.children!.find((i) => !isLastLevel(i));
-});
-const clickHandler = (floder: Floder) => {
-  console.log("click");
-  emit("preview", floder);
-};
-const activeTab = ref(oneTabs.value?.id);
-</script>
-
-<style lang="scss">
-.img-item {
-  cursor: pointer;
-  padding-top: 50%;
-  position: relative;
-  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>

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

+ 37 - 94
src/views/folder/index.vue

@@ -1,118 +1,59 @@
 <template>
   <LeftPano>
-    <template v-for="item in types" :key="item.id">
-      <div class="types" v-if="item.floders.length || item.children?.length">
-        <h2 @click="showHanlder(item)">
-          {{ 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" />
-  <Modal
-    width="800px"
-    :title="showModalRoot?.title"
-    @cancel="showModalRoot = void 0"
-    :open="!!showModalRoot"
-    :footer="null"
-  >
-    <ShowFloderRoot :root="showModalRoot" v-if="showModalRoot" @preview="preview" />
-  </Modal>
+  <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, ref } from "vue";
+import { ref } from "vue";
 import { getUrlType, MetaType, saveAs } from "@/utils";
-import { Preview, MediaItem, MediaType } from "@/components/static-preview/index.vue";
-import { floderRoots, floderTypes, getFloderByType } from "@/store";
-import { Modal } from "ant-design-vue";
-import ShowFloderRoot from "./floder-root-view.vue";
+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, FloderRoot } from "@/store";
 import { useViewStack } from "@/hook";
 import { showRightPanoStack } from "@/env";
 
-const showModalRoot = ref<FloderRoot>();
-const types = computed(() =>
-  floderRoots.value.map((type) => {
-    let show: Ref<boolean>;
-    if (type.children?.length) {
-      show = computed(() => showModalRoot.value?.id === type.id);
-    } else {
-      show = ref(true);
-    }
-    return {
-      show,
-      ...type,
-    };
-  })
-);
-
-const typeIcons = {
-  [MetaType.image]: "pic",
-  [MetaType.video]: "a-film",
-  [MetaType.other]: "nav-edit",
-  [MetaType.audio]: "nav-edit",
-};
-
-const showHanlder = (item: FloderRoot & { show: Ref<boolean> }) => {
-  if (item.children?.length) {
-    showModalRoot.value = item;
-  } else {
-    item.show.value = !item.show.value;
-  }
-};
-
-const currentFile = ref<MediaItem | null>(null);
-const preview = async (floder: Floder) => {
-  console.log("???");
-  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)));
@@ -121,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>

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

+ 17 - 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,26 +18,37 @@ 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')
   }