Bladeren bron

火调大改

wangfumin 3 maanden geleden
bovenliggende
commit
04a8cf6303
84 gewijzigde bestanden met toevoegingen van 9549 en 73 verwijderingen
  1. 25 0
      package-lock.json
  2. 2 1
      package.json
  3. 6 4
      src/app/criminal/routeConfig.ts
  4. 1 1
      src/app/criminal/view/login/index.vue
  5. 30 13
      src/app/fire/routeConfig.ts
  6. 1 0
      src/app/fire/store/fire.ts
  7. 8 5
      src/app/fire/view/dispatch/fireDetails.vue
  8. 539 0
      src/assets/icon/newfire/demo.css
  9. 579 0
      src/assets/icon/newfire/demo_index.html
  10. 83 0
      src/assets/icon/newfire/iconfont.css
  11. 1 0
      src/assets/icon/newfire/iconfont.js
  12. 128 0
      src/assets/icon/newfire/iconfont.json
  13. BIN
      src/assets/icon/newfire/iconfont.ttf
  14. BIN
      src/assets/icon/newfire/iconfont.woff
  15. BIN
      src/assets/icon/newfire/iconfont.woff2
  16. 2 1
      src/assets/style/public.scss
  17. 6 0
      src/request/urls.ts
  18. 37 1
      src/router/config.ts
  19. 8 1
      src/router/index.ts
  20. 9 0
      src/router/routeName.ts
  21. 0 1
      src/store/caseFile.ts
  22. 8 0
      src/store/editCsae.ts
  23. 5 7
      src/store/permission.ts
  24. 14 10
      src/store/scene.ts
  25. 52 1
      src/store/system.ts
  26. 49 0
      src/util/video-cover.ts
  27. 4 3
      src/view/case/editMenuToDetail.vue
  28. 1 1
      src/view/case/moreMenu.vue
  29. 2 1
      src/view/layout/index.vue
  30. 28 19
      src/view/layout/top/index.vue
  31. 23 1
      src/view/layout/top/style.scss
  32. 123 0
      src/view/newFireCase/dyManager/downloadLog.vue
  33. 40 0
      src/view/newFireCase/dyManager/editModel.vue
  34. 46 0
      src/view/newFireCase/dyManager/index.vue
  35. 49 0
      src/view/newFireCase/dyManager/list.vue
  36. 192 0
      src/view/newFireCase/dyManager/modelContent.vue
  37. 56 0
      src/view/newFireCase/dyManager/pagging.ts
  38. 28 0
      src/view/newFireCase/dyManager/quisk.ts
  39. 134 0
      src/view/newFireCase/dyManager/sceneContent.vue
  40. 139 0
      src/view/newFireCase/dyManager/sceneDownload.vue
  41. 123 0
      src/view/newFireCase/meshManager/downloadLog.vue
  42. 40 0
      src/view/newFireCase/meshManager/editModel.vue
  43. 49 0
      src/view/newFireCase/meshManager/index.vue
  44. 49 0
      src/view/newFireCase/meshManager/list.vue
  45. 192 0
      src/view/newFireCase/meshManager/modelContent.vue
  46. 58 0
      src/view/newFireCase/meshManager/pagging.ts
  47. 28 0
      src/view/newFireCase/meshManager/quisk.ts
  48. 134 0
      src/view/newFireCase/meshManager/sceneContent.vue
  49. 139 0
      src/view/newFireCase/meshManager/sceneDownload.vue
  50. 123 0
      src/view/newFireCase/mix3dManager/downloadLog.vue
  51. 40 0
      src/view/newFireCase/mix3dManager/editModel.vue
  52. 24 0
      src/view/newFireCase/mix3dManager/index.vue
  53. 40 0
      src/view/newFireCase/mix3dManager/list.vue
  54. 31 0
      src/view/newFireCase/mix3dManager/pagging.ts
  55. 28 0
      src/view/newFireCase/mix3dManager/quisk.ts
  56. 83 0
      src/view/newFireCase/mix3dManager/sceneContent.vue
  57. 139 0
      src/view/newFireCase/mix3dManager/sceneDownload.vue
  58. 57 0
      src/view/newFireCase/newFireDetails/components/Toolbar.vue
  59. 289 0
      src/view/newFireCase/newFireDetails/components/basicInfo.vue
  60. 688 0
      src/view/newFireCase/newFireDetails/components/creatMap.vue
  61. 85 0
      src/view/newFireCase/newFireDetails/components/headerTop.vue
  62. 295 0
      src/view/newFireCase/newFireDetails/components/mix3d.vue
  63. 261 0
      src/view/newFireCase/newFireDetails/components/otherFiles.vue
  64. 431 0
      src/view/newFireCase/newFireDetails/components/scene.vue
  65. 179 0
      src/view/newFireCase/newFireDetails/components/screenShot.vue
  66. 413 0
      src/view/newFireCase/newFireDetails/components/shot.vue
  67. 1021 0
      src/view/newFireCase/newFireDetails/components/siteInspection.vue
  68. 145 0
      src/view/newFireCase/newFireDetails/editIndex.vue
  69. 172 0
      src/view/newFireCase/newFireDetails/index.vue
  70. 145 0
      src/view/newFireCase/newFireDetails/showIndex.vue
  71. 58 0
      src/view/newFireCase/newdispatch/editCrimical.vue
  72. 325 0
      src/view/newFireCase/newdispatch/editFire copy.vue
  73. 185 0
      src/view/newFireCase/newdispatch/editFire.vue
  74. 43 0
      src/view/newFireCase/newdispatch/editLeaveMsg.vue
  75. 143 0
      src/view/newFireCase/newdispatch/example.vue
  76. 252 0
      src/view/newFireCase/newdispatch/fireDetails.vue
  77. 99 0
      src/view/newFireCase/newdispatch/header.vue
  78. 112 0
      src/view/newFireCase/newdispatch/index.vue
  79. 116 0
      src/view/newFireCase/newdispatch/leaveMsgList.vue
  80. 116 0
      src/view/newFireCase/newdispatch/list.vue
  81. 81 0
      src/view/newFireCase/newdispatch/pagging.ts
  82. 35 0
      src/view/newFireCase/newdispatch/quisk.ts
  83. 2 1
      src/view/system/login.vue
  84. 23 1
      yarn.lock

+ 25 - 0
package-lock.json

@@ -25,6 +25,7 @@
         "qrcode.vue": "^3.4.1",
         "qs": "^6.11.2",
         "sass": "^1.64.2",
+        "simaqcore": "^1.2.0",
         "sortablejs": "^1.15.2",
         "swiper": "^11.1.4",
         "three": "^0.158.0",
@@ -1770,6 +1771,12 @@
       "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
       "license": "MIT"
     },
+    "node_modules/eventemitter3": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-4.0.7.tgz",
+      "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+      "license": "MIT"
+    },
     "node_modules/follow-redirects": {
       "version": "1.15.9",
       "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz",
@@ -3017,6 +3024,15 @@
         "fsevents": "~2.3.2"
       }
     },
+    "node_modules/rxjs": {
+      "version": "7.5.7",
+      "resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.5.7.tgz",
+      "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "tslib": "^2.1.0"
+      }
+    },
     "node_modules/safe-array-concat": {
       "version": "1.1.3",
       "resolved": "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
@@ -3243,6 +3259,15 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/simaqcore": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/simaqcore/-/simaqcore-1.2.0.tgz",
+      "integrity": "sha512-WViM7DhEJ/2JlF23apBIYP0KcjHqTjqxlMOlPXJsZ6Fp/726APxT0s9P/hsApHYJMu3/Ztkd/3iTOGEOFTPE0Q==",
+      "dependencies": {
+        "eventemitter3": "^4.0.7",
+        "rxjs": "~7.5.7"
+      }
+    },
     "node_modules/sortablejs": {
       "version": "1.15.3",
       "resolved": "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.15.3.tgz",

+ 2 - 1
package.json

@@ -4,7 +4,7 @@
   "version": "0.0.0",
   "type": "module",
   "scripts": {
-    "dev": "vite --mode=fire",
+    "dev": "vite --mode=criminal",
     "devxm": "vite --mode=xmfire",
     "devcjz": "vite --mode=cjzfire",
     "build": "npm run build-quisk",
@@ -35,6 +35,7 @@
     "qrcode.vue": "^3.4.1",
     "qs": "^6.11.2",
     "sass": "^1.64.2",
+    "simaqcore": "^1.2.0",
     "sortablejs": "^1.15.2",
     "swiper": "^11.1.4",
     "three": "^0.158.0",

+ 6 - 4
src/app/criminal/routeConfig.ts

@@ -3,15 +3,17 @@ import { Routes } from "@/router/config";
 
 export const CriminalRouteName = {
   ...RouteName,
-  example: "example",
 } as const;
 
 export const menuRouteNames = [
   // CriminalRouteName.statistics,
-  CriminalRouteName.vrmodel,
+  CriminalRouteName.example,
+  CriminalRouteName.meshManager,
+  CriminalRouteName.dyManager,
+  CriminalRouteName.mix3dManager,
+  // CriminalRouteName.vrmodel,
   CriminalRouteName.mediaLibrary,
   CriminalRouteName.camera,
-  CriminalRouteName.example,
   CriminalRouteName.organization,
   CriminalRouteName.role,
   CriminalRouteName.user,
@@ -22,7 +24,7 @@ export const routes: Routes = [
   {
     name: CriminalRouteName.example,
     path: "example",
-    component: () => import("@/app/criminal/view/example/index.vue"),
+    component: () => import("@/view/newFireCase/newdispatch/example.vue"),
     meta: { title: "案件管理", icon: "iconfire_management" },
   },
   {

+ 1 - 1
src/app/criminal/view/login/index.vue

@@ -131,7 +131,7 @@ const submitClick = async () => {
       window.localStorage.setItem('token', user.value.token)
       window.location.replace(url);
     } else {
-      router.replace({ name: RouteName.scene });
+      router.replace({ name: RouteName.example });
     }
   } catch (e) {
     console.error(e);

+ 30 - 13
src/app/fire/routeConfig.ts

@@ -9,25 +9,34 @@ export const FireRouteName = {
 } as const;
 
 export const menuRouteNames = [
-  FireRouteName.vrmodel,
-  FireRouteName.mediaLibrary,
-  FireRouteName.camera,
   FireRouteName.dispatch,
+  FireRouteName.meshManager,
+  FireRouteName.dyManager,
+  FireRouteName.mix3dManager,
+  // FireRouteName.vrmodel,
+  FireRouteName.mediaLibrary,
   FireRouteName.teaching,
   FireRouteName.statistics,
   FireRouteName.organization,
   FireRouteName.downloadLog,
   FireRouteName.role,
   FireRouteName.user,
+  FireRouteName.camera,
+  FireRouteName.recycle,
   FireRouteName.setting,
-  FireRouteName.recycle
 ];
 
 export const routes: Routes = [
+  // {
+  //   name: FireRouteName.dispatch,
+  //   path: "dispatch",
+  //   component: () => import("./view/dispatch/index.vue"),
+  //   meta: { title: "火调管理", icon: "iconfire_management" },
+  // },
   {
     name: FireRouteName.dispatch,
     path: "dispatch",
-    component: () => import("./view/dispatch/index.vue"),
+    component: () => import("@/view/newFireCase/newdispatch/index.vue"),
     meta: { title: "火调管理", icon: "iconfire_management" },
   },
   {
@@ -42,12 +51,20 @@ export const routes: Routes = [
     component: () => import("./view/dispatch/index.vue"),
     meta: { title: "回收站", icon: "icon-del" },
   },
-  {
-    path: '/fire/dispatch/fireDetails/:caseId',
-    name: 'fireDetails',
-    component: () => import('@/app/fire/view/dispatch/fireDetails.vue'),
-    meta: {
-      title: '火灾详情'
-    }
-  }
+  // {
+  //   path: '/fire/dispatch/fireDetails/:caseId',
+  //   name: 'fireDetails',
+  //   component: () => import('@/app/fire/view/dispatch/fireDetails.vue'),
+  //   meta: {
+  //     title: '火灾详情'
+  //   }
+  // }
+  // {
+  //   path: '/fireDetails/:caseId',
+  //   name: 'fireDetails',
+  //   component: () => import('@/view/newFireCase/newFireDetails/index.vue'),
+  //   meta: {
+  //     title: '火灾详情'
+  //   }
+  // }
 ];

+ 1 - 0
src/app/fire/store/fire.ts

@@ -42,6 +42,7 @@ export type Fire = {
   projectName: string;
   projectSite: string;
   projectSiteCode: string;
+  fireType: string;
   projectSn: string;
   status: FireStatus;
   statusDesc: string;

+ 8 - 5
src/app/fire/view/dispatch/fireDetails.vue

@@ -128,11 +128,14 @@ const expandFireNews = () => {
 }
 
 // 开始录制方法
+const isShot = ref<boolean>(false);
+
 const startRecording = () => {
-  if (!caseId.value) return;
-  const currentCaseId = caseId.value;
-  const fuseLink = getFuseCodeLink(currentCaseId);
-  checkScenesOpen(currentCaseId, `${fuseLink}#sceneEdit/record`);
+  isShot.value = true;
+  // if (!caseId.value) return;
+  // const currentCaseId = caseId.value;
+  // const fuseLink = getFuseCodeLink(currentCaseId);
+  // checkScenesOpen(currentCaseId, `${fuseLink}#sceneEdit/record`);
 };
 
 const editHandler = async (row: any) => {
@@ -210,7 +213,7 @@ const menus = computed(() => {
       key: "screenRecord", // 修改为唯一的 key,避免与勘验笔录冲突
       label: "屏幕录制",
       onClick: () => {
-        checkScenesOpen(currentCaseId, `${fuseLink}#sceneEdit/record`);
+        //checkScenesOpen(currentCaseId, `${fuseLink}#sceneEdit/record`);
       },
     },
   ];

+ 539 - 0
src/assets/icon/newfire/demo.css

@@ -0,0 +1,539 @@
+/* Logo 字体 */
+@font-face {
+  font-family: "iconfont logo";
+  src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
+  src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
+    url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
+    url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
+    url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
+}
+
+.logo {
+  font-family: "iconfont logo";
+  font-size: 160px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+/* tabs */
+.nav-tabs {
+  position: relative;
+}
+
+.nav-tabs .nav-more {
+  position: absolute;
+  right: 0;
+  bottom: 0;
+  height: 42px;
+  line-height: 42px;
+  color: #666;
+}
+
+#tabs {
+  border-bottom: 1px solid #eee;
+}
+
+#tabs li {
+  cursor: pointer;
+  width: 100px;
+  height: 40px;
+  line-height: 40px;
+  text-align: center;
+  font-size: 16px;
+  border-bottom: 2px solid transparent;
+  position: relative;
+  z-index: 1;
+  margin-bottom: -1px;
+  color: #666;
+}
+
+
+#tabs .active {
+  border-bottom-color: #f00;
+  color: #222;
+}
+
+.tab-container .content {
+  display: none;
+}
+
+/* 页面布局 */
+.main {
+  padding: 30px 100px;
+  width: 960px;
+  margin: 0 auto;
+}
+
+.main .logo {
+  color: #333;
+  text-align: left;
+  margin-bottom: 30px;
+  line-height: 1;
+  height: 110px;
+  margin-top: -50px;
+  overflow: hidden;
+  *zoom: 1;
+}
+
+.main .logo a {
+  font-size: 160px;
+  color: #333;
+}
+
+.helps {
+  margin-top: 40px;
+}
+
+.helps pre {
+  padding: 20px;
+  margin: 10px 0;
+  border: solid 1px #e7e1cd;
+  background-color: #fffdef;
+  overflow: auto;
+}
+
+.icon_lists {
+  width: 100% !important;
+  overflow: hidden;
+  *zoom: 1;
+}
+
+.icon_lists li {
+  width: 100px;
+  margin-bottom: 10px;
+  margin-right: 20px;
+  text-align: center;
+  list-style: none !important;
+  cursor: default;
+}
+
+.icon_lists li .code-name {
+  line-height: 1.2;
+}
+
+.icon_lists .icon {
+  display: block;
+  height: 100px;
+  line-height: 100px;
+  font-size: 42px;
+  margin: 10px auto;
+  color: #333;
+  -webkit-transition: font-size 0.25s linear, width 0.25s linear;
+  -moz-transition: font-size 0.25s linear, width 0.25s linear;
+  transition: font-size 0.25s linear, width 0.25s linear;
+}
+
+.icon_lists .icon:hover {
+  font-size: 100px;
+}
+
+.icon_lists .svg-icon {
+  /* 通过设置 font-size 来改变图标大小 */
+  width: 1em;
+  /* 图标和文字相邻时,垂直对齐 */
+  vertical-align: -0.15em;
+  /* 通过设置 color 来改变 SVG 的颜色/fill */
+  fill: currentColor;
+  /* path 和 stroke 溢出 viewBox 部分在 IE 下会显示
+      normalize.css 中也包含这行 */
+  overflow: hidden;
+}
+
+.icon_lists li .name,
+.icon_lists li .code-name {
+  color: #666;
+}
+
+/* markdown 样式 */
+.markdown {
+  color: #666;
+  font-size: 14px;
+  line-height: 1.8;
+}
+
+.highlight {
+  line-height: 1.5;
+}
+
+.markdown img {
+  vertical-align: middle;
+  max-width: 100%;
+}
+
+.markdown h1 {
+  color: #404040;
+  font-weight: 500;
+  line-height: 40px;
+  margin-bottom: 24px;
+}
+
+.markdown h2,
+.markdown h3,
+.markdown h4,
+.markdown h5,
+.markdown h6 {
+  color: #404040;
+  margin: 1.6em 0 0.6em 0;
+  font-weight: 500;
+  clear: both;
+}
+
+.markdown h1 {
+  font-size: 28px;
+}
+
+.markdown h2 {
+  font-size: 22px;
+}
+
+.markdown h3 {
+  font-size: 16px;
+}
+
+.markdown h4 {
+  font-size: 14px;
+}
+
+.markdown h5 {
+  font-size: 12px;
+}
+
+.markdown h6 {
+  font-size: 12px;
+}
+
+.markdown hr {
+  height: 1px;
+  border: 0;
+  background: #e9e9e9;
+  margin: 16px 0;
+  clear: both;
+}
+
+.markdown p {
+  margin: 1em 0;
+}
+
+.markdown>p,
+.markdown>blockquote,
+.markdown>.highlight,
+.markdown>ol,
+.markdown>ul {
+  width: 80%;
+}
+
+.markdown ul>li {
+  list-style: circle;
+}
+
+.markdown>ul li,
+.markdown blockquote ul>li {
+  margin-left: 20px;
+  padding-left: 4px;
+}
+
+.markdown>ul li p,
+.markdown>ol li p {
+  margin: 0.6em 0;
+}
+
+.markdown ol>li {
+  list-style: decimal;
+}
+
+.markdown>ol li,
+.markdown blockquote ol>li {
+  margin-left: 20px;
+  padding-left: 4px;
+}
+
+.markdown code {
+  margin: 0 3px;
+  padding: 0 5px;
+  background: #eee;
+  border-radius: 3px;
+}
+
+.markdown strong,
+.markdown b {
+  font-weight: 600;
+}
+
+.markdown>table {
+  border-collapse: collapse;
+  border-spacing: 0px;
+  empty-cells: show;
+  border: 1px solid #e9e9e9;
+  width: 95%;
+  margin-bottom: 24px;
+}
+
+.markdown>table th {
+  white-space: nowrap;
+  color: #333;
+  font-weight: 600;
+}
+
+.markdown>table th,
+.markdown>table td {
+  border: 1px solid #e9e9e9;
+  padding: 8px 16px;
+  text-align: left;
+}
+
+.markdown>table th {
+  background: #F7F7F7;
+}
+
+.markdown blockquote {
+  font-size: 90%;
+  color: #999;
+  border-left: 4px solid #e9e9e9;
+  padding-left: 0.8em;
+  margin: 1em 0;
+}
+
+.markdown blockquote p {
+  margin: 0;
+}
+
+.markdown .anchor {
+  opacity: 0;
+  transition: opacity 0.3s ease;
+  margin-left: 8px;
+}
+
+.markdown .waiting {
+  color: #ccc;
+}
+
+.markdown h1:hover .anchor,
+.markdown h2:hover .anchor,
+.markdown h3:hover .anchor,
+.markdown h4:hover .anchor,
+.markdown h5:hover .anchor,
+.markdown h6:hover .anchor {
+  opacity: 1;
+  display: inline-block;
+}
+
+.markdown>br,
+.markdown>p>br {
+  clear: both;
+}
+
+
+.hljs {
+  display: block;
+  background: white;
+  padding: 0.5em;
+  color: #333333;
+  overflow-x: auto;
+}
+
+.hljs-comment,
+.hljs-meta {
+  color: #969896;
+}
+
+.hljs-string,
+.hljs-variable,
+.hljs-template-variable,
+.hljs-strong,
+.hljs-emphasis,
+.hljs-quote {
+  color: #df5000;
+}
+
+.hljs-keyword,
+.hljs-selector-tag,
+.hljs-type {
+  color: #a71d5d;
+}
+
+.hljs-literal,
+.hljs-symbol,
+.hljs-bullet,
+.hljs-attribute {
+  color: #0086b3;
+}
+
+.hljs-section,
+.hljs-name {
+  color: #63a35c;
+}
+
+.hljs-tag {
+  color: #333333;
+}
+
+.hljs-title,
+.hljs-attr,
+.hljs-selector-id,
+.hljs-selector-class,
+.hljs-selector-attr,
+.hljs-selector-pseudo {
+  color: #795da3;
+}
+
+.hljs-addition {
+  color: #55a532;
+  background-color: #eaffea;
+}
+
+.hljs-deletion {
+  color: #bd2c00;
+  background-color: #ffecec;
+}
+
+.hljs-link {
+  text-decoration: underline;
+}
+
+/* 代码高亮 */
+/* PrismJS 1.15.0
+https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
+/**
+ * prism.js default theme for JavaScript, CSS and HTML
+ * Based on dabblet (http://dabblet.com)
+ * @author Lea Verou
+ */
+code[class*="language-"],
+pre[class*="language-"] {
+  color: black;
+  background: none;
+  text-shadow: 0 1px white;
+  font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
+  text-align: left;
+  white-space: pre;
+  word-spacing: normal;
+  word-break: normal;
+  word-wrap: normal;
+  line-height: 1.5;
+
+  -moz-tab-size: 4;
+  -o-tab-size: 4;
+  tab-size: 4;
+
+  -webkit-hyphens: none;
+  -moz-hyphens: none;
+  -ms-hyphens: none;
+  hyphens: none;
+}
+
+pre[class*="language-"]::-moz-selection,
+pre[class*="language-"] ::-moz-selection,
+code[class*="language-"]::-moz-selection,
+code[class*="language-"] ::-moz-selection {
+  text-shadow: none;
+  background: #b3d4fc;
+}
+
+pre[class*="language-"]::selection,
+pre[class*="language-"] ::selection,
+code[class*="language-"]::selection,
+code[class*="language-"] ::selection {
+  text-shadow: none;
+  background: #b3d4fc;
+}
+
+@media print {
+
+  code[class*="language-"],
+  pre[class*="language-"] {
+    text-shadow: none;
+  }
+}
+
+/* Code blocks */
+pre[class*="language-"] {
+  padding: 1em;
+  margin: .5em 0;
+  overflow: auto;
+}
+
+:not(pre)>code[class*="language-"],
+pre[class*="language-"] {
+  background: #f5f2f0;
+}
+
+/* Inline code */
+:not(pre)>code[class*="language-"] {
+  padding: .1em;
+  border-radius: .3em;
+  white-space: normal;
+}
+
+.token.comment,
+.token.prolog,
+.token.doctype,
+.token.cdata {
+  color: slategray;
+}
+
+.token.punctuation {
+  color: #999;
+}
+
+.namespace {
+  opacity: .7;
+}
+
+.token.property,
+.token.tag,
+.token.boolean,
+.token.number,
+.token.constant,
+.token.symbol,
+.token.deleted {
+  color: #905;
+}
+
+.token.selector,
+.token.attr-name,
+.token.string,
+.token.char,
+.token.builtin,
+.token.inserted {
+  color: #690;
+}
+
+.token.operator,
+.token.entity,
+.token.url,
+.language-css .token.string,
+.style .token.string {
+  color: #9a6e3a;
+  background: hsla(0, 0%, 100%, .5);
+}
+
+.token.atrule,
+.token.attr-value,
+.token.keyword {
+  color: #07a;
+}
+
+.token.function,
+.token.class-name {
+  color: #DD4A68;
+}
+
+.token.regex,
+.token.important,
+.token.variable {
+  color: #e90;
+}
+
+.token.important,
+.token.bold {
+  font-weight: bold;
+}
+
+.token.italic {
+  font-style: italic;
+}
+
+.token.entity {
+  cursor: help;
+}

+ 579 - 0
src/assets/icon/newfire/demo_index.html

@@ -0,0 +1,579 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8"/>
+  <title>iconfont Demo</title>
+  <link rel="shortcut icon" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg" type="image/x-icon"/>
+  <link rel="icon" type="image/svg+xml" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg"/>
+  <link rel="stylesheet" href="https://g.alicdn.com/thx/cube/1.3.2/cube.min.css">
+  <link rel="stylesheet" href="demo.css">
+  <link rel="stylesheet" href="iconfont.css">
+  <script src="iconfont.js"></script>
+  <!-- jQuery -->
+  <script src="https://a1.alicdn.com/oss/uploads/2018/12/26/7bfddb60-08e8-11e9-9b04-53e73bb6408b.js"></script>
+  <!-- 代码高亮 -->
+  <script src="https://a1.alicdn.com/oss/uploads/2018/12/26/a3f714d0-08e6-11e9-8a15-ebf944d7534c.js"></script>
+  <style>
+    .main .logo {
+      margin-top: 0;
+      height: auto;
+    }
+
+    .main .logo a {
+      display: flex;
+      align-items: center;
+    }
+
+    .main .logo .sub-title {
+      margin-left: 0.5em;
+      font-size: 22px;
+      color: #fff;
+      background: linear-gradient(-45deg, #3967FF, #B500FE);
+      -webkit-background-clip: text;
+      -webkit-text-fill-color: transparent;
+    }
+  </style>
+</head>
+<body>
+  <div class="main">
+    <h1 class="logo"><a href="https://www.iconfont.cn/" title="iconfont 首页" target="_blank">
+      <img width="200" src="https://img.alicdn.com/imgextra/i3/O1CN01Mn65HV1FfSEzR6DKv_!!6000000000514-55-tps-228-59.svg">
+      
+    </a></h1>
+    <div class="nav-tabs">
+      <ul id="tabs" class="dib-box">
+        <li class="dib active"><span>Unicode</span></li>
+        <li class="dib"><span>Font class</span></li>
+        <li class="dib"><span>Symbol</span></li>
+      </ul>
+      
+      <a href="https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=5058908" target="_blank" class="nav-more">查看项目</a>
+      
+    </div>
+    <div class="tab-container">
+      <div class="content unicode" style="display: block;">
+          <ul class="icon_lists dib-box">
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe64c;</span>
+                <div class="name">search</div>
+                <div class="code-name">&amp;#xe64c;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7d8;</span>
+                <div class="name">Left</div>
+                <div class="code-name">&amp;#xe7d8;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7d9;</span>
+                <div class="name">Right</div>
+                <div class="code-name">&amp;#xe7d9;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7d5;</span>
+                <div class="name">play</div>
+                <div class="code-name">&amp;#xe7d5;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7d6;</span>
+                <div class="name">record</div>
+                <div class="code-name">&amp;#xe7d6;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7d7;</span>
+                <div class="name">pause</div>
+                <div class="code-name">&amp;#xe7d7;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7d4;</span>
+                <div class="name">rename_s</div>
+                <div class="code-name">&amp;#xe7d4;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7d0;</span>
+                <div class="name">CloseCircle</div>
+                <div class="code-name">&amp;#xe7d0;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7d1;</span>
+                <div class="name">Edit</div>
+                <div class="code-name">&amp;#xe7d1;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7d2;</span>
+                <div class="name">Up</div>
+                <div class="code-name">&amp;#xe7d2;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7d3;</span>
+                <div class="name">Down</div>
+                <div class="code-name">&amp;#xe7d3;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7b5;</span>
+                <div class="name">window_m</div>
+                <div class="code-name">&amp;#xe7b5;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7b4;</span>
+                <div class="name">window_n</div>
+                <div class="code-name">&amp;#xe7b4;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7cf;</span>
+                <div class="name">download</div>
+                <div class="code-name">&amp;#xe7cf;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7c6;</span>
+                <div class="name">rename</div>
+                <div class="code-name">&amp;#xe7c6;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7c9;</span>
+                <div class="name">import</div>
+                <div class="code-name">&amp;#xe7c9;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7c8;</span>
+                <div class="name">Upload</div>
+                <div class="code-name">&amp;#xe7c8;</div>
+              </li>
+          
+          </ul>
+          <div class="article markdown">
+          <h2 id="unicode-">Unicode 引用</h2>
+          <hr>
+
+          <p>Unicode 是字体在网页端最原始的应用方式,特点是:</p>
+          <ul>
+            <li>支持按字体的方式去动态调整图标大小,颜色等等。</li>
+            <li>默认情况下不支持多色,直接添加多色图标会自动去色。</li>
+          </ul>
+          <blockquote>
+            <p>注意:新版 iconfont 支持两种方式引用多色图标:SVG symbol 引用方式和彩色字体图标模式。(使用彩色字体图标需要在「编辑项目」中开启「彩色」选项后并重新生成。)</p>
+          </blockquote>
+          <p>Unicode 使用步骤如下:</p>
+          <h3 id="-font-face">第一步:拷贝项目下面生成的 <code>@font-face</code></h3>
+<pre><code class="language-css"
+>@font-face {
+  font-family: 'iconfont';
+  src: url('iconfont.woff2?t=1763006144468') format('woff2'),
+       url('iconfont.woff?t=1763006144468') format('woff'),
+       url('iconfont.ttf?t=1763006144468') format('truetype');
+}
+</code></pre>
+          <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
+<pre><code class="language-css"
+>.iconfont {
+  font-family: "iconfont" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+</code></pre>
+          <h3 id="-">第三步:挑选相应图标并获取字体编码,应用于页面</h3>
+<pre>
+<code class="language-html"
+>&lt;span class="iconfont"&gt;&amp;#x33;&lt;/span&gt;
+</code></pre>
+          <blockquote>
+            <p>"iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。</p>
+          </blockquote>
+          </div>
+      </div>
+      <div class="content font-class">
+        <ul class="icon_lists dib-box">
+          
+          <li class="dib">
+            <span class="icon iconfont icon-search"></span>
+            <div class="name">
+              search
+            </div>
+            <div class="code-name">.icon-search
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-Left"></span>
+            <div class="name">
+              Left
+            </div>
+            <div class="code-name">.icon-Left
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-Right"></span>
+            <div class="name">
+              Right
+            </div>
+            <div class="code-name">.icon-Right
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-play"></span>
+            <div class="name">
+              play
+            </div>
+            <div class="code-name">.icon-play
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-record"></span>
+            <div class="name">
+              record
+            </div>
+            <div class="code-name">.icon-record
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-pause"></span>
+            <div class="name">
+              pause
+            </div>
+            <div class="code-name">.icon-pause
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-rename_s"></span>
+            <div class="name">
+              rename_s
+            </div>
+            <div class="code-name">.icon-rename_s
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-CloseCircle"></span>
+            <div class="name">
+              CloseCircle
+            </div>
+            <div class="code-name">.icon-CloseCircle
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-Edit"></span>
+            <div class="name">
+              Edit
+            </div>
+            <div class="code-name">.icon-Edit
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-Up"></span>
+            <div class="name">
+              Up
+            </div>
+            <div class="code-name">.icon-Up
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-Down"></span>
+            <div class="name">
+              Down
+            </div>
+            <div class="code-name">.icon-Down
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-window_m"></span>
+            <div class="name">
+              window_m
+            </div>
+            <div class="code-name">.icon-window_m
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-window_n"></span>
+            <div class="name">
+              window_n
+            </div>
+            <div class="code-name">.icon-window_n
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-download"></span>
+            <div class="name">
+              download
+            </div>
+            <div class="code-name">.icon-download
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-rename"></span>
+            <div class="name">
+              rename
+            </div>
+            <div class="code-name">.icon-rename
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-import"></span>
+            <div class="name">
+              import
+            </div>
+            <div class="code-name">.icon-import
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-Upload"></span>
+            <div class="name">
+              Upload
+            </div>
+            <div class="code-name">.icon-Upload
+            </div>
+          </li>
+          
+        </ul>
+        <div class="article markdown">
+        <h2 id="font-class-">font-class 引用</h2>
+        <hr>
+
+        <p>font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。</p>
+        <p>与 Unicode 使用方式相比,具有如下特点:</p>
+        <ul>
+          <li>相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。</li>
+          <li>因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。</li>
+        </ul>
+        <p>使用步骤如下:</p>
+        <h3 id="-fontclass-">第一步:引入项目下面生成的 fontclass 代码:</h3>
+<pre><code class="language-html">&lt;link rel="stylesheet" href="./iconfont.css"&gt;
+</code></pre>
+        <h3 id="-">第二步:挑选相应图标并获取类名,应用于页面:</h3>
+<pre><code class="language-html">&lt;span class="iconfont icon-xxx"&gt;&lt;/span&gt;
+</code></pre>
+        <blockquote>
+          <p>"
+            iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。</p>
+        </blockquote>
+      </div>
+      </div>
+      <div class="content symbol">
+          <ul class="icon_lists dib-box">
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-search"></use>
+                </svg>
+                <div class="name">search</div>
+                <div class="code-name">#icon-search</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-Left"></use>
+                </svg>
+                <div class="name">Left</div>
+                <div class="code-name">#icon-Left</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-Right"></use>
+                </svg>
+                <div class="name">Right</div>
+                <div class="code-name">#icon-Right</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-play"></use>
+                </svg>
+                <div class="name">play</div>
+                <div class="code-name">#icon-play</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-record"></use>
+                </svg>
+                <div class="name">record</div>
+                <div class="code-name">#icon-record</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-pause"></use>
+                </svg>
+                <div class="name">pause</div>
+                <div class="code-name">#icon-pause</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-rename_s"></use>
+                </svg>
+                <div class="name">rename_s</div>
+                <div class="code-name">#icon-rename_s</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-CloseCircle"></use>
+                </svg>
+                <div class="name">CloseCircle</div>
+                <div class="code-name">#icon-CloseCircle</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-Edit"></use>
+                </svg>
+                <div class="name">Edit</div>
+                <div class="code-name">#icon-Edit</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-Up"></use>
+                </svg>
+                <div class="name">Up</div>
+                <div class="code-name">#icon-Up</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-Down"></use>
+                </svg>
+                <div class="name">Down</div>
+                <div class="code-name">#icon-Down</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-window_m"></use>
+                </svg>
+                <div class="name">window_m</div>
+                <div class="code-name">#icon-window_m</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-window_n"></use>
+                </svg>
+                <div class="name">window_n</div>
+                <div class="code-name">#icon-window_n</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-download"></use>
+                </svg>
+                <div class="name">download</div>
+                <div class="code-name">#icon-download</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-rename"></use>
+                </svg>
+                <div class="name">rename</div>
+                <div class="code-name">#icon-rename</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-import"></use>
+                </svg>
+                <div class="name">import</div>
+                <div class="code-name">#icon-import</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-Upload"></use>
+                </svg>
+                <div class="name">Upload</div>
+                <div class="code-name">#icon-Upload</div>
+            </li>
+          
+          </ul>
+          <div class="article markdown">
+          <h2 id="symbol-">Symbol 引用</h2>
+          <hr>
+
+          <p>这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇<a href="">文章</a>
+            这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:</p>
+          <ul>
+            <li>支持多色图标了,不再受单色限制。</li>
+            <li>通过一些技巧,支持像字体那样,通过 <code>font-size</code>, <code>color</code> 来调整样式。</li>
+            <li>兼容性较差,支持 IE9+,及现代浏览器。</li>
+            <li>浏览器渲染 SVG 的性能一般,还不如 png。</li>
+          </ul>
+          <p>使用步骤如下:</p>
+          <h3 id="-symbol-">第一步:引入项目下面生成的 symbol 代码:</h3>
+<pre><code class="language-html">&lt;script src="./iconfont.js"&gt;&lt;/script&gt;
+</code></pre>
+          <h3 id="-css-">第二步:加入通用 CSS 代码(引入一次就行):</h3>
+<pre><code class="language-html">&lt;style&gt;
+.icon {
+  width: 1em;
+  height: 1em;
+  vertical-align: -0.15em;
+  fill: currentColor;
+  overflow: hidden;
+}
+&lt;/style&gt;
+</code></pre>
+          <h3 id="-">第三步:挑选相应图标并获取类名,应用于页面:</h3>
+<pre><code class="language-html">&lt;svg class="icon" aria-hidden="true"&gt;
+  &lt;use xlink:href="#icon-xxx"&gt;&lt;/use&gt;
+&lt;/svg&gt;
+</code></pre>
+          </div>
+      </div>
+
+    </div>
+  </div>
+  <script>
+  $(document).ready(function () {
+      $('.tab-container .content:first').show()
+
+      $('#tabs li').click(function (e) {
+        var tabContent = $('.tab-container .content')
+        var index = $(this).index()
+
+        if ($(this).hasClass('active')) {
+          return
+        } else {
+          $('#tabs li').removeClass('active')
+          $(this).addClass('active')
+
+          tabContent.hide().eq(index).fadeIn()
+        }
+      })
+    })
+  </script>
+</body>
+</html>

+ 83 - 0
src/assets/icon/newfire/iconfont.css

@@ -0,0 +1,83 @@
+@font-face {
+  font-family: "iconfont"; /* Project id 5058908 */
+  src: url('iconfont.woff2?t=1763006144468') format('woff2'),
+       url('iconfont.woff?t=1763006144468') format('woff'),
+       url('iconfont.ttf?t=1763006144468') format('truetype');
+}
+
+.iconfont {
+  font-family: "iconfont" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-search:before {
+  content: "\e64c";
+}
+
+.icon-Left:before {
+  content: "\e7d8";
+}
+
+.icon-Right:before {
+  content: "\e7d9";
+}
+
+.icon-play:before {
+  content: "\e7d5";
+}
+
+.icon-record:before {
+  content: "\e7d6";
+}
+
+.icon-pause:before {
+  content: "\e7d7";
+}
+
+.icon-rename_s:before {
+  content: "\e7d4";
+}
+
+.icon-CloseCircle:before {
+  content: "\e7d0";
+}
+
+.icon-Edit:before {
+  content: "\e7d1";
+}
+
+.icon-Up:before {
+  content: "\e7d2";
+}
+
+.icon-Down:before {
+  content: "\e7d3";
+}
+
+.icon-window_m:before {
+  content: "\e7b5";
+}
+
+.icon-window_n:before {
+  content: "\e7b4";
+}
+
+.icon-download:before {
+  content: "\e7cf";
+}
+
+.icon-rename:before {
+  content: "\e7c6";
+}
+
+.icon-import:before {
+  content: "\e7c9";
+}
+
+.icon-Upload:before {
+  content: "\e7c8";
+}
+

File diff suppressed because it is too large
+ 1 - 0
src/assets/icon/newfire/iconfont.js


+ 128 - 0
src/assets/icon/newfire/iconfont.json

@@ -0,0 +1,128 @@
+{
+  "id": "5058908",
+  "name": "项目中心",
+  "font_family": "iconfont",
+  "css_prefix_text": "icon-",
+  "description": "",
+  "glyphs": [
+    {
+      "icon_id": "25631464",
+      "name": "search",
+      "font_class": "search",
+      "unicode": "e64c",
+      "unicode_decimal": 58956
+    },
+    {
+      "icon_id": "46003485",
+      "name": "Left",
+      "font_class": "Left",
+      "unicode": "e7d8",
+      "unicode_decimal": 59352
+    },
+    {
+      "icon_id": "46003486",
+      "name": "Right",
+      "font_class": "Right",
+      "unicode": "e7d9",
+      "unicode_decimal": 59353
+    },
+    {
+      "icon_id": "46003440",
+      "name": "play",
+      "font_class": "play",
+      "unicode": "e7d5",
+      "unicode_decimal": 59349
+    },
+    {
+      "icon_id": "46003439",
+      "name": "record",
+      "font_class": "record",
+      "unicode": "e7d6",
+      "unicode_decimal": 59350
+    },
+    {
+      "icon_id": "46003438",
+      "name": "pause",
+      "font_class": "pause",
+      "unicode": "e7d7",
+      "unicode_decimal": 59351
+    },
+    {
+      "icon_id": "46003195",
+      "name": "rename_s",
+      "font_class": "rename_s",
+      "unicode": "e7d4",
+      "unicode_decimal": 59348
+    },
+    {
+      "icon_id": "46002896",
+      "name": "CloseCircle",
+      "font_class": "CloseCircle",
+      "unicode": "e7d0",
+      "unicode_decimal": 59344
+    },
+    {
+      "icon_id": "46002898",
+      "name": "Edit",
+      "font_class": "Edit",
+      "unicode": "e7d1",
+      "unicode_decimal": 59345
+    },
+    {
+      "icon_id": "46002897",
+      "name": "Up",
+      "font_class": "Up",
+      "unicode": "e7d2",
+      "unicode_decimal": 59346
+    },
+    {
+      "icon_id": "46002895",
+      "name": "Down",
+      "font_class": "Down",
+      "unicode": "e7d3",
+      "unicode_decimal": 59347
+    },
+    {
+      "icon_id": "44890706",
+      "name": "window_m",
+      "font_class": "window_m",
+      "unicode": "e7b5",
+      "unicode_decimal": 59317
+    },
+    {
+      "icon_id": "44890707",
+      "name": "window_n",
+      "font_class": "window_n",
+      "unicode": "e7b4",
+      "unicode_decimal": 59316
+    },
+    {
+      "icon_id": "46001982",
+      "name": "download",
+      "font_class": "download",
+      "unicode": "e7cf",
+      "unicode_decimal": 59343
+    },
+    {
+      "icon_id": "45263630",
+      "name": "rename",
+      "font_class": "rename",
+      "unicode": "e7c6",
+      "unicode_decimal": 59334
+    },
+    {
+      "icon_id": "45434742",
+      "name": "import",
+      "font_class": "import",
+      "unicode": "e7c9",
+      "unicode_decimal": 59337
+    },
+    {
+      "icon_id": "45434743",
+      "name": "Upload",
+      "font_class": "Upload",
+      "unicode": "e7c8",
+      "unicode_decimal": 59336
+    }
+  ]
+}

BIN
src/assets/icon/newfire/iconfont.ttf


BIN
src/assets/icon/newfire/iconfont.woff


BIN
src/assets/icon/newfire/iconfont.woff2


+ 2 - 1
src/assets/style/public.scss

@@ -1,5 +1,6 @@
 @import "../icon/fire/iconfont.css";
 @import "../icon/fuse/iconfont.css";
+@import "../icon/newfire/iconfont.css";
 * {
   margin : 0;
   padding: 0;
@@ -174,7 +175,7 @@ body {
   padding: 0 24px;
 }
 .el-tabs--top .el-tabs__nav{
-  padding-bottom: 16px;
+  padding-bottom: 0px;
 }
 
 .el-tabs__item {

+ 6 - 0
src/request/urls.ts

@@ -279,3 +279,9 @@ export const updateCaseOverview = `/fusion/caseOverview/addOrUpdate`;
 // 删除方位图和平面图
 export const delCaseTabulation = `/fusion/caseTabulation/del`;
 export const delCaseOverview = `/fusion/caseOverview/del`;
+
+// 录制
+export const recordCaseVideo = `/fusion/caseVideoFolder/allList`;
+
+// 设备列表
+export const cameraTypeAllList = `/fusion/cameraType/allList`;

+ 37 - 1
src/router/config.ts

@@ -32,17 +32,35 @@ export const system: Routes = [
     component: () => import("@/view/system/index.vue"),
     meta: { title: "重置密码" },
   },
+  {
+    path: '/fireDetails/:caseId',
+    name: 'fireDetails',
+    component: () => import('@/view/newFireCase/newFireDetails/index.vue'),
+    meta: {
+      title: '火灾详情'
+    }
+  }
 ];
 
 export const routes: Routes = [
   {
+    // name: RouteName.viewLayout,
+    // path: "/",
+    // component: () => import("@/view/layout/index.vue"),
+    // meta: { title: "VR看房管理" },
     name: RouteName.viewLayout,
     path: "/",
     component: () => import("@/view/layout/index.vue"),
-    meta: { title: "VR看房管理" },
+    meta: { title: "火灾管理" },
     children: [
       ...system,
       {
+        name: RouteName.dispatch,
+        path: "dispatch",
+        component: () => import("@/view/newFireCase/newdispatch/index.vue"),
+        meta: { title: "火调管理", icon: "iconfire_management" },
+      },
+      {
         name: RouteName.downloadLog,
         path: "download-log",
         component: () => import("@/view/vrmodel/downloadLog.vue"),
@@ -103,6 +121,24 @@ export const routes: Routes = [
         component: () => import("@/view/setting/index.vue"),
         meta: { title: "系统设置", icon: "icon-nav-setup" },
       },
+      { // 以下是新增的
+        name: RouteName.meshManager,
+        path: "meshManager",
+        component: () => import("@/view/newFireCase/meshManager/index.vue"),
+        meta: { title: "Mesh场景", icon: "iconfire_scenes" },
+      },
+      {
+        name: RouteName.dyManager,
+        path: "dyManager",
+        component: () => import("@/view/newFireCase/dyManager/index.vue"),
+        meta: { title: "点云场景", icon: "iconfire_scenes" },
+      },
+      {
+        name: RouteName.mix3dManager,
+        path: "mix3dManager",
+        component: () => import("@/view/newFireCase/mix3dManager/index.vue"),
+        meta: { title: "多元融合", icon: "iconfire_scenes" },
+      },
     ],
   },
   {

+ 8 - 1
src/router/index.ts

@@ -29,7 +29,14 @@ $style.setAttribute("type", "text/css");
 document.body.appendChild($style);
 router.beforeEach((to, from, next) => {
   if (!to.name || to.name === RouteName.viewLayout) {
-    router.replace({ name: RouteName.scene });
+    console.log('appId', appId)
+    // router.replace({ name: RouteName.scene });
+    if(['fire'].includes(appId)){
+      router.replace({ name: RouteName.dispatch });
+    } else {
+      // criminal跳转
+      router.replace({ name: RouteName.scene });
+    }
     return;
   }
   try {

+ 9 - 0
src/router/routeName.ts

@@ -18,6 +18,15 @@ export const RouteName = {
   sceneVisitor: "sceneVisitor",
   setting: "system",
   noCase: "no-case",
+ 
+  // 新版新增路由
+  dispatch: "dispatch",
+  meshManager: "meshManager",
+  dyManager: "dyManager",
+  mix3dManager: "mix3dManager",
+
+  // 公安
+  example: "example",
 } as const;
 
 type RouteNamesType = typeof RouteName;

+ 0 - 1
src/store/caseFile.ts

@@ -86,7 +86,6 @@ export const getCaseFileImageInfo = async (fileId: number) => {
     } as CaseFile)
   );
 };
-
 export type SaveCaseFileImageInfo = Pick<CaseFile, "caseId" | "filesTitle"> & {
   filesId?: number;
   imgType: BoardType;

+ 8 - 0
src/store/editCsae.ts

@@ -0,0 +1,8 @@
+import {
+  axios,
+  recordCaseVideo
+} from "@/request";
+
+export const getRecordCaseVideo = async (props: {
+  caseId: number;
+}) => (await axios.get(recordCaseVideo, { params: props })).data;

+ 5 - 7
src/store/permission.ts

@@ -11,7 +11,7 @@ export type UserPermission = {
 };
 
 export const permission = ref(getLocal("permission", [] as UserPermission[]));
-console.log(permission.value, 9898);
+
 changSaveLocal("permission", () => permission.value);
 
 /**
@@ -19,12 +19,10 @@ changSaveLocal("permission", () => permission.value);
  * @param routeNames 所有路由
  */
 export const getPermissionRoutes = (routeNames: string[]) => {
-  return routeNames
-    .filter((routeName) =>
-      permission.value.some((p) => p.resourceKey === routeName)
-    )
-    .map((routeName) => findRoute(routeName))
-    .filter((route) => route) as Routes;
+  permission.value.push({resourceKey: 'meshManager', type: 'menu', dataScope: 1})
+  permission.value.push({resourceKey: 'dyManager', type: 'menu', dataScope: 1})
+  permission.value.push({resourceKey: 'mix3dManager', type: 'menu', dataScope: 1})
+  return routeNames.filter((routeName) => permission.value.some((p) => p.resourceKey === routeName)).map((routeName) => findRoute(routeName)).filter((route) => route) as Routes;
 };
 
 export const setPermission = (perms: UserPermission[]) => {

+ 14 - 10
src/store/scene.ts

@@ -18,6 +18,7 @@ import {
   getSceneList,
   updateModelScene,
   uploadModel,
+  cameraTypeAllList,
 } from "@/request";
 import saveAs from "@/util/file-serve";
 import { ElMessage } from "element-plus";
@@ -25,7 +26,8 @@ import { ElMessage } from "element-plus";
 interface BaseScene {
   title: string;
   time: string;
-  type: SceneType;
+  searchType: any;
+  // type: SceneType;
   id: string;
 }
 
@@ -176,24 +178,20 @@ export const getModelSceneStatus = async (scene: ModelScene) => {
 // *-----公用
 
 type ScenePaggingParams = PaggingReq<
-  Pick<BaseScene, "type"> & {
+  Pick<BaseScene, "searchType"> & {
     modelTitle: string;
     sceneName: string;
     status?: number;
     caseId?: number;
     deptId: string;
     snCode: string;
+    cameraType: string;
+    searchType: any;
+    isObj: number;
   }
 >;
 export const getScenePagging = async (params: ScenePaggingParams) => {
-  const data = (
-    await axios.get(
-      params.type === SceneType.SWMX ? getModelSceneList : getSceneList,
-      {
-        params,
-      }
-    )
-  ).data as PaggingRes<Scene>;
+  const data = (await axios.get(getSceneList,{params})).data as PaggingRes<Scene>;
 
   return data;
 };
@@ -255,3 +253,9 @@ export const genMeshScene = async (scene: QuoteScene) => {
     ElMessage.success("生成obj将同时生成Mesh场景,需要较长时间,请耐心等待;");
   }
 };
+
+// 获取设备列表
+export const getCameraTypeAllList = async () =>{
+  let data = (await axios.get(cameraTypeAllList)).data as any;
+  return data
+}

+ 52 - 1
src/store/system.ts

@@ -8,7 +8,7 @@ import { encodePwd } from "@/util";
 import { user } from "./user";
 import { refreshRole } from "./role";
 import { appConstant } from "@/app";
-import { ref, watchEffect } from "vue";
+import { ref, watchEffect, reactive } from "vue";
 
 export type LoginProps = {
   phoneNum: string;
@@ -48,3 +48,54 @@ if (user.value.token) {
 export const uploadFile = async (file: File) => {
   return (await axios.post<string>(uploadFileUrl, { file })).data;
 };
+
+export const appEl = ref<HTMLDivElement | null>(null);
+
+export const recordFragments = ref<any>([])
+export const getRecordFragments = (record: any) =>  recordFragments.value.filter(fragment => fragment.recordId === record.id)
+
+export const getRecordFragmentBlobs = (record) => getRecordFragments(record)
+  .filter(fragment => typeof fragment.url !== 'string')
+  .map(fragment => fragment.url as Blob)
+
+export const createRecordFragment = (recordFragment: Partial<any> = {}): any => ({
+  id: 2,
+  recordId: '',
+  cover: '',
+  url: '',
+  sort: Math.min(...recordFragments.value.map(item => item.sort)) + 1,
+  ...recordFragment
+})
+
+// 字符串转params对象
+export const strToParams = (str: string) => {
+  if (str[0] === '?') {
+      str = str.substr(1)
+  }
+
+  const result: { [key: string]: string } = {}
+  const splitRG = /([^=&]+)(?:=([^&]*))?&?/
+
+  let rgRet
+  while ((rgRet = str.match(splitRG))) {
+    result[rgRet[1]] = rgRet[2] === undefined ? '' : rgRet[2]
+    str = str.substr(rgRet[0].length)
+  }
+
+  return result
+}
+export const params = reactive(
+  strToParams(location.search)
+) as any
+export const baseURL = params.baseURL ? params.baseURL : "";
+
+export const getResource = (uri: string) => {
+  if (~uri.indexOf("base64") || ~uri.indexOf("bolb") || ~uri.indexOf("//"))
+    return uri;
+
+  if (uri[0] === "/") {
+    return `${baseURL}${uri}`;
+  } else {
+    return `${baseURL}/${uri}`;
+  }
+};

+ 49 - 0
src/util/video-cover.ts

@@ -0,0 +1,49 @@
+
+export const getVideoCover = (data: string | Blob,  seekTo: number = 0.0, width?: number, height?: number) : Promise<string> => {
+  const url = typeof data !== 'string' 
+    ? URL.createObjectURL(data)
+    : data
+
+  return new Promise(function (resolve, reject) {
+    const video = document.createElement('video')
+
+    video.setAttribute('crossOrigin', 'anonymous')// 处理跨域,需要服务器支持跨域
+    video.setAttribute('src', url)
+    video.setAttribute('muted', '')
+    if (width && height) {
+      video.setAttribute('width', `${width}px`)
+      video.setAttribute('height', `${height}px`)
+      video.setAttribute('style', 'object-fit:scale-down')
+    }
+    video.load()
+    video.addEventListener('loadedmetadata', function () {
+      if (video.duration < seekTo) {
+        reject(new Error('视频长度不够'));
+        return;
+      }
+
+      setTimeout(() => video.currentTime = seekTo, 200);
+
+      video.addEventListener('seeked', () => {
+        const canvas = document.createElement('canvas')
+        if (width && height) {
+          canvas.width = width
+          canvas.height = height
+        } else {
+          canvas.width = video.videoWidth
+          canvas.height = video.videoHeight
+        }
+        canvas.getContext('2d')!.drawImage(video, 0, 0, width!, height!)
+
+        const dataURL = canvas.toDataURL('image/jpeg') 
+
+        if (typeof data !== 'string') {
+          video.pause()
+          URL.revokeObjectURL(url)
+        }
+        resolve(dataURL)
+      })
+    })
+  })
+
+}

+ 4 - 3
src/view/case/editMenuToDetail.vue

@@ -9,14 +9,15 @@
 import { computed } from "vue";
 import { RouteName, router } from "@/router";
 const props = defineProps<{
-  caseId: number;
+  caseId: any;
   fromRoute: string,
   row: object;
 }>();
 const goToDetails = (caseId, title) => {
   const routeData = router.resolve({
-    path: `/fire/dispatch/fireDetails/${caseId}`,
-    // query: { title }
+    // path: `/fire/dispatch/fireDetails/${caseId}`,
+    path: `/fireDetails/${caseId}`, 
+    query: { editOrShow: 'show' }
   });
   // router.push({
   //   path: `/fire/dispatch/fireDetails/${caseId}`,

+ 1 - 1
src/view/case/moreMenu.vue

@@ -34,7 +34,7 @@ const { state, refresh, queryReset, del, changPageSize, changPageCurrent } = use
   paramsTemlate: { caseTitle: "", deptId: "" },
 });
 const props = defineProps<{
-  caseId: number;
+  caseId: any;
   title?: string;
   prevMenu?: MenuItem[];
   lastMenu?: MenuItem[];

+ 2 - 1
src/view/layout/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="layer">
+  <div class="layer" :ref="(el: any) => appEl = (el as HTMLDivElement)">
     <ly-top class="top" v-if="!isSystem" />
     <div class="content">
       <router-view v-slot="{ Component }" v-if="isSystem">
@@ -25,6 +25,7 @@ import lySlide from "./slide/index.vue";
 import { routeIsSystem, router } from "@/router";
 import { computed } from "vue";
 import { menuRouteNames } from "@/app";
+import { appEl } from "@/store/system";
 
 const isSystem = computed(() => routeIsSystem());
 

+ 28 - 19
src/view/layout/top/index.vue

@@ -3,27 +3,31 @@
     <div class="title">
       <h2>
         {{ title }}
-        <span>{{ desc }}</span>
+        <!-- <span>{{ desc }}</span> -->
       </h2>
     </div>
-    <div class="oper-btns" v-if="user.info">
-      <div class="user-menu">
-        <img :src="user.info.avatar ? user.info.avatar : defAvatar" />
-        <el-dropdown>
-          <div style="outline: none">
-            <span class="oper-down">{{ user.info.nickName }}</span>
-            <!-- <i class="el-icon-arrow-down el-icon--right"></i> -->
-            <el-icon><ArrowDown /></el-icon>
-          </div>
-          <template v-slot:dropdown>
-            <el-dropdown-menu class="menu-items-user">
-              <el-dropdown-item @click="updatePwdHandler">修改密码</el-dropdown-item>
-              <el-dropdown-item @click="logout"
-                ><span style="color: #fa5555">退出登录</span></el-dropdown-item
-              >
-            </el-dropdown-menu>
-          </template>
-        </el-dropdown>
+    <div class="right-top">
+      <div class="screen-url" @click="openMap(user.info.deptId)">大屏链接</div>
+      <div class="org-name"><img :src="user.info.avatar ? user.info.avatar : defAvatar" />{{ user.info.deptName }}</div>
+      <div class="oper-btns" v-if="user.info">
+        <div class="user-menu">
+          <img :src="user.info.avatar ? user.info.avatar : defAvatar" />
+          <el-dropdown>
+            <div style="outline: none">
+              <span class="oper-down">{{ user.info.nickName }}</span>
+              <!-- <i class="el-icon-arrow-down el-icon--right"></i> -->
+              <el-icon><ArrowDown /></el-icon>
+            </div>
+            <template v-slot:dropdown>
+              <el-dropdown-menu class="menu-items-user">
+                <el-dropdown-item @click="updatePwdHandler">修改密码</el-dropdown-item>
+                <el-dropdown-item @click="logout"
+                  ><span style="color: #fa5555">退出登录</span></el-dropdown-item
+                >
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+        </div>
       </div>
     </div>
   </div>
@@ -39,7 +43,12 @@ import { updatePwd } from "@/view/system/quisk";
 import { title, desc } from "@/store/system";
 
 refreshRole();
+console.log('user', user)
+const openMap = (deptId) => {
+  // const result = location.protocol + "//" + location.host + location.pathname + `map.html?deptId=${data.id}`
+  window.open(`./map.html?deptId=${deptId}`);
 
+};
 const loginoutRaw = async () => {
   await logoutRaw();
   roleId.value = "";

+ 23 - 1
src/view/layout/top/style.scss

@@ -1,4 +1,6 @@
 .header-top {
+  display: flex;
+  justify-content: space-between;
   &.system {
     background-color: #fff;
 
@@ -28,7 +30,27 @@
       }
     }
   }
-
+  .right-top {
+    display: flex;
+    align-items: center;
+    
+    .screen-url{
+      display: flex;
+      margin-right: 14px;
+      color: #fff;
+      cursor: pointer;
+    }
+    .org-name{
+      display: flex;
+      align-items: center;
+      margin-right: 14px;
+      color: #fff;
+      img {
+        width: 30px;
+        margin-right: 6px;
+      }
+    }
+  }
   .oper-btns {
     .user-menu {
       display    : flex;

+ 123 - 0
src/view/newFireCase/dyManager/downloadLog.vue

@@ -0,0 +1,123 @@
+<template>
+  <com-head :options="[{ name: '下载记录', value: '2' }]" showCtrl>
+    <el-form label-width="84px">
+      <el-form-item label="所属架构:">
+        <com-company v-model="state.query.deptId" />
+      </el-form-item>
+      <el-form-item label="用户姓名:">
+        <el-input v-model="state.query.nickName" placeholder="请输入"></el-input>
+      </el-form-item>
+      <el-form-item label="用户账号:">
+        <el-input v-model="state.query.userName" placeholder="请输入手机号"></el-input>
+      </el-form-item>
+      <el-form-item label="下载时间:">
+        <el-date-picker
+          type="daterange"
+          format="YYYY-MM-DD"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          v-model="createTime"
+          placeholder="请选择"
+          :defaultTime="defaultTime"
+          style="width: 100%"
+        />
+      </el-form-item>
+      <el-form-item label="场景标题:">
+        <el-input v-model="state.query.sceneTitle" placeholder="请输入"></el-input>
+      </el-form-item>
+      <el-form-item label="场景码:">
+        <el-input v-model="state.query.sceneNum" placeholder="请输入"></el-input>
+      </el-form-item>
+      <el-form-item label="SN码:">
+        <el-input v-model="state.query.snCode" placeholder="请输入"></el-input>
+      </el-form-item>
+
+      <el-form-item class="searh-btns" style="grid-area: 1/4/4/5">
+        <el-button type="primary" @click="refresh">查询</el-button>
+        <el-button type="primary" plain @click="queryReset">重置</el-button>
+      </el-form-item>
+    </el-form>
+  </com-head>
+
+  <div class="body-layer" style="padding-top: 8px">
+    <el-table
+      class="user-table"
+      :data="state.table.rows"
+      style="width: 100%; max-height: 480px"
+      size="large"
+    >
+      <el-table-column label="序号" width="100" v-slot:default="{ $index }">
+        <span style="text-align: center">
+          {{ state.pag.size * (state.pag.currentPage - 1) + $index + 1 }}
+        </span>
+      </el-table-column>
+      <el-table-column label="组织名称" prop="deptName"></el-table-column>
+      <el-table-column label="组织类型" prop="deptLevelStr"></el-table-column>
+      <el-table-column label="用户姓名" prop="nickName"></el-table-column>
+      <el-table-column label="用户账号" prop="userName"></el-table-column>
+      <el-table-column label="下载时间" prop="createTime"></el-table-column>
+      <el-table-column label="场景标题" prop="sceneTitle"></el-table-column>
+      <el-table-column label="场景码" prop="sceneNum"></el-table-column>
+      <el-table-column label="SN码" prop="snCode"></el-table-column>
+    </el-table>
+
+    <com-pagination
+      @size-change="changPageSize"
+      @current-change="changPageCurrent"
+      :current-page="state.pag.currentPage"
+      :page-size="state.pag.size"
+      :total="state.pag.total"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { usePagging } from "@/hook/pagging";
+import comHead from "@/components/head/index.vue";
+import comCompany from "@/components/company-select/index.vue";
+import comPagination from "@/components/pagination/index.vue";
+import { getDownloadQuoteScene } from "@/store/scene";
+import { ref, watchEffect } from "vue";
+import { dateFormat } from "@/util";
+
+const {
+  state,
+  queryReset: queryResetRaw,
+  refresh,
+  changPageCurrent,
+  changPageSize,
+} = usePagging({
+  get: getDownloadQuoteScene,
+  paramsTemlate: {
+    nickName: "",
+    deptId: "",
+    userName: "",
+    startCreateTime: "",
+    endCreateTime: "",
+    sceneTitle: "",
+    sceneNum: "",
+    snCode: "",
+  },
+});
+
+const defaultTime: [Date, Date] = [
+  new Date(2000, 1, 1, 0, 0, 0),
+  new Date(2000, 2, 1, 23, 59, 59),
+];
+const createTime = ref<Date[] | null>(null);
+watchEffect(() => {
+  if (createTime.value && createTime.value.length === 2) {
+    state.query.startCreateTime = dateFormat(createTime.value[0], "yyyy-MM-dd hh:mm:ss");
+    state.query.endCreateTime = dateFormat(createTime.value[1], "yyyy-MM-dd hh:mm:ss");
+  } else {
+    state.query.startCreateTime = null as any;
+    state.query.endCreateTime = null as any;
+  }
+});
+const queryReset = () => {
+  queryResetRaw();
+  createTime.value = null;
+};
+</script>
+
+<style scoped lang="scss"></style>

+ 40 - 0
src/view/newFireCase/dyManager/editModel.vue

@@ -0,0 +1,40 @@
+<template>
+  <el-form ref="form" label-width="84px">
+    <el-form-item label="模型名称">
+      <el-input
+        v-model="bindModel.modelTitle"
+        maxlength="50"
+        placeholder="请输入模型名称"
+      />
+    </el-form-item>
+    <el-form-item label="渲染方式" v-if="bindModel.modelDateType === 'obj'">
+      <el-select placeholder="请选择" v-model="bindModel.renderType">
+        <el-option label="基础材质(无光照)" value="base" />
+        <el-option label="标准材质(有光照,适用于无贴图模型)" value="normal" />
+      </el-select>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import { ref } from "vue";
+import { ModelScene, setModelScene } from "@/store/scene";
+import { ElMessage } from "element-plus";
+import { QuiskExpose } from "@/helper/mount";
+
+const props = defineProps<{ model: ModelScene }>();
+const bindModel = ref<ModelScene>({
+  ...props.model,
+  renderType: props.model.renderType || "base",
+});
+
+defineExpose<QuiskExpose>({
+  async submit() {
+    if (!bindModel.value.modelTitle || !bindModel.value.modelTitle.trim()) {
+      ElMessage.error("模型名称不能为空");
+      throw "模型名称不能为空";
+    }
+    await setModelScene(bindModel.value);
+  },
+});
+</script>

+ 46 - 0
src/view/newFireCase/dyManager/index.vue

@@ -0,0 +1,46 @@
+<template>
+  <List :params="params">
+    <template v-slot:header>
+      <el-form-item label="标题:">
+        <el-input v-model="params.keyword" placeholder="请输入"></el-input>
+      </el-form-item>
+      <el-form-item label="设备类型:">
+        <el-select placeholder="请选择" v-model="params.pagging.state.query.type">
+          <el-option
+            v-for="option in deviceTypeList"
+            :key="option.value"
+            :value="option.value"
+            :label="option.name"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="S/N码:" v-if="!params.isSwmx">
+        <el-input
+          v-model="params.pagging.state.query.snCode"
+          placeholder="请输入"
+        ></el-input>
+      </el-form-item>
+    </template>
+    <template v-slot:content>
+      <component :is="component" :pagging="params.pagging" />
+    </template>
+  </List>
+</template>
+
+<script setup lang="ts">
+import List from "./list.vue";
+import SceneContent from "./sceneContent.vue";
+import ModelContent from "./modelContent.vue";
+import { SceneType } from "@/store/scene";
+import { SceneTypeDesc } from "@/constant/scene";
+import { useScenePaggingParams } from "./pagging";
+import { computed } from "vue";
+
+const deviceTypeList = [
+  { value: 2, name: '激光转台' },
+  { value: 5, name: '激光移动' },
+  { value: 6, name: '激光手持' },
+];
+const params = useScenePaggingParams();
+const component = computed(() => (params.isSwmx ? ModelContent : SceneContent));
+</script>

+ 49 - 0
src/view/newFireCase/dyManager/list.vue

@@ -0,0 +1,49 @@
+<template>
+  <com-head :options="listOptions" v-model="params.pagging.state.query.searchType">
+    <el-form label-width="84px" inline>
+      <slot name="header" />
+      <el-form-item class="searh-btns" style="grid-area: 1 / 4 / 2 / 4">
+        <el-button type="primary" @click="params.pagging.refresh">查询</el-button>
+        <el-button type="primary" plain @click="params.pagging.queryReset"
+          >重置</el-button
+        >
+      </el-form-item>
+    </el-form>
+  </com-head>
+
+  <div class="body-layer">
+    <slot name="content" />
+    <com-pagination
+      @size-change="params.pagging.changPageSize"
+      @current-change="params.pagging.changPageCurrent"
+      :current-page="params.pagging.state.pag.currentPage"
+      :page-size="params.pagging.state.pag.size"
+      :total="params.pagging.state.pag.total"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import comHead from "@/components/head/index.vue";
+import comPagination from "@/components/pagination/index.vue";
+import { SceneType } from "@/store/scene";
+import { SceneTypeDesc } from "@/constant/scene";
+import { useScenePaggingParams } from "./pagging";
+
+defineProps<{ params: ReturnType<typeof useScenePaggingParams> }>();
+
+const headOptions = [
+  { value: SceneType.SWKK, name: SceneTypeDesc[SceneType.SWKK] },
+  { value: SceneType.SWKJ, name: SceneTypeDesc[SceneType.SWKJ] },
+  { value: SceneType.SWSS, name: SceneTypeDesc[SceneType.SWSS] },
+  { value: SceneType.SWSSMX, name: SceneTypeDesc[SceneType.SWSSMX] },
+  { value: SceneType.SWYDSS, name: SceneTypeDesc[SceneType.SWYDSS] },
+  { value: SceneType.SWYDMX, name: SceneTypeDesc[SceneType.SWYDMX] },
+  // { value: SceneType.SWMX, name: SceneTypeDesc[SceneType.SWMX] },
+];
+const listOptions = [
+  { name: "场景列表", value: "0" },
+  { name: "场景共享", value: "1" },
+  { name: "全部", value: "2" },
+];
+</script>

+ 192 - 0
src/view/newFireCase/dyManager/modelContent.vue

@@ -0,0 +1,192 @@
+<template>
+  <div class="body-head">
+    <h3 style="visibility: hidden">场景管理</h3>
+
+    <el-tooltip
+      class="item"
+      effect="dark"
+      :content="`请上传${format}(支持obj/ply/las/laz/osgb/b3dm格式的数据),大小在${size}以内 `"
+      placement="bottom-start"
+      ><el-upload
+        class="upload-demo"
+        :multiple="false"
+        :limit="1"
+        :accept="accept"
+        :show-file-list="false"
+        :http-request="() => {}"
+        :file-list="fileList"
+        :disabled="percentage || !operateIsPermissionByPath('sync')"
+        :before-upload="uploadCheck"
+      >
+        <el-button v-pdpath="'sync'" type="primary">
+          <el-icon><Upload /></el-icon>{{ percentage ? "文件上传中" : "上传数据" }}
+        </el-button>
+      </el-upload>
+    </el-tooltip>
+  </div>
+
+  <el-table
+    :data="pagging.state.table.rows"
+    tooltip-effect="dark"
+    style="width: 100%"
+    size="large"
+  >
+    <el-table-column label="序号" width="100" v-slot:default="{ $index }">
+      <span style="text-align: center">
+        {{ pagging.state.pag.size * (pagging.state.pag.currentPage - 1) + $index + 1 }}
+      </span>
+    </el-table-column>
+    <el-table-column label="标题" prop="modelTitle"></el-table-column>
+    <el-table-column label="原始数据格式" prop="modelDateType"></el-table-column>
+    <el-table-column label="大小" prop="modelSize"></el-table-column>
+    <el-table-column label="上传时间" v-slot:default="{ row }: { row: ModelScene }">
+      {{ getStatusText(row) }}
+    </el-table-column>
+    <el-table-column label="所属架构" prop="deptName"></el-table-column>
+    <el-table-column label="操作" v-slot:default="{ row }" width="350px">
+      <template v-if="row.createStatus === ModelSceneStatus.SUCCESS">
+        <span class="oper-span" @click="downOrigin(row)" v-if="row.fileNewName">
+          下载原始资源
+        </span>
+        <span class="oper-span" @click="downHash(row)"> Hash </span>
+        <span class="oper-span" @click="copyHanlder(row)"> 复制 </span>
+        <span class="oper-span" v-pdpath="['edit']" @click="editHanlder(row)">
+          修改
+        </span>
+        <span
+          class="oper-span"
+          v-pdpath="['view']"
+          @click="openSceneUrl(row, OpenType.query)"
+        >
+          查看
+        </span>
+      </template>
+      <span
+        v-if="row.createStatus !== ModelSceneStatus.REV"
+        class="oper-span delBtn"
+        @click="delOrCancel(row)"
+        v-pdpath="'del'"
+      >
+        {{ row.createStatus !== ModelSceneStatus.RUN ? "删除" : "取消上传" }}
+      </span>
+
+      <span v-else class="oper-span" v-pdpath="['viewaaa']"> 模型转换中… </span>
+    </el-table-column>
+  </el-table>
+
+  <el-dialog
+    :model-value="!!percentage"
+    :show-close="false"
+    title="文件上传中"
+    :close-on-click-modal="false"
+  >
+    <el-progress :percentage="percentage" />
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import {
+  ModelSceneStatus,
+  ModelScene,
+  cancelUploadModelScene,
+  uploadModelScene,
+  delModelScene,
+  getModelSceneStatus,
+  copyModelScene,
+  downModelSceneHash,
+} from "@/store/scene";
+import {
+  ModelMaxSize,
+  ModelSceneStatusDesc,
+  ModelSupportFormats,
+} from "@/constant/scene";
+import { confirm } from "@/helper/message";
+import { useUpload } from "@/hook/upload";
+import { ScenePagging } from "./pagging";
+import { watchPolling } from "@/hook/watchPolling";
+import { OpenType, openSceneUrl } from "../../case/help";
+import { operateIsPermissionByPath } from "@/directive/permission";
+import { editModelScene } from "./quisk";
+import saveAs from "@/util/file-serve";
+
+const props = defineProps<{ pagging: ScenePagging }>();
+
+const getStatusText = (scene: ModelScene) => {
+  let desc = ModelSceneStatusDesc[scene.createStatus];
+  if (scene.createStatus === ModelSceneStatus.RUN && scene.progress) {
+    desc += ` ${scene.progress}% `;
+  } else if (scene.createStatus === ModelSceneStatus.SUCCESS) {
+    desc = scene.createTime;
+  }
+  return desc;
+};
+
+const delOrCancel = async (scene: ModelScene) => {
+  const isDel = scene.createStatus !== ModelSceneStatus.RUN;
+  const msg = isDel ? "确定要删除此数据?" : "确定要取消上传吗?";
+
+  if (await confirm(msg)) {
+    isDel ? await delModelScene(scene) : await cancelUploadModelScene(scene);
+    props.pagging.refresh();
+  }
+};
+
+const editHanlder = async (scene: ModelScene) => {
+  if (await editModelScene({ model: scene })) {
+    props.pagging.refresh();
+  }
+};
+
+const copyHanlder = async (scene: ModelScene) => {
+  if (await copyModelScene(scene)) {
+    props.pagging.refresh();
+  }
+};
+const downHash = async (scene: ModelScene) => {
+  downModelSceneHash(scene);
+};
+
+const {
+  percentage,
+  upload: uploadCheck,
+  fileList,
+  size,
+  format,
+  removeFile,
+  accept,
+} = useUpload({
+  maxSize: ModelMaxSize,
+  formats: ModelSupportFormats,
+  upload: async (file, onPercentage) => {
+    try {
+      await uploadModelScene(file, onPercentage);
+      props.pagging.refresh();
+    } catch {}
+    removeFile();
+  },
+});
+
+// 处理后台正在处理的模型类
+const refreshStatus = (models: ModelScene[]) => {
+  const refreshStatusAll = models.map(async (scene) => {
+    const { status, progress } = await getModelSceneStatus(scene);
+    scene.createStatus = status;
+    scene.progress = progress;
+    if (status == ModelSceneStatus.SUCCESS) {
+      props.pagging.refresh();
+    }
+  });
+  return Promise.all(refreshStatusAll);
+};
+
+const downOrigin = async (model: ModelScene) => {
+  await saveAs(model.fileNewName, model.modelTitle + ".zip");
+};
+
+watchPolling(() => {
+  const payload = (props.pagging.state.table.rows as ModelScene[]).filter(
+    (item) => item.createStatus === ModelSceneStatus.RUN
+  );
+  return { start: payload.length > 0, payload };
+}, refreshStatus);
+</script>

+ 56 - 0
src/view/newFireCase/dyManager/pagging.ts

@@ -0,0 +1,56 @@
+import { usePagging } from "@/hook/pagging";
+import { SceneType, getScenePagging } from "@/store/scene";
+import { computed, reactive, watch, watchEffect } from "vue";
+
+export const useScenePaggingParams = () => {
+  const pagging = usePagging({
+    get: getScenePagging,
+    paramsTemlate: {
+      type: SceneType.SWSS, // 这里先填默认值,到时更改接口后再换成空值
+      searchType: "0",
+      sceneName: "",
+      modelTitle: "",
+      deptId: "",
+      snCode: "",
+    },
+  });
+
+  const isSwmx = computed(() => pagging.state.query.type === SceneType.SWMX);
+  const keyword = computed({
+    get: () =>
+      isSwmx.value
+        ? pagging.state.query.modelTitle
+        : pagging.state.query.sceneName,
+    set: (val: string) => {
+      pagging.state.query.modelTitle = val;
+      pagging.state.query.sceneName = val;
+    },
+  });
+
+  let oldSnCode = pagging.state.query.snCode;
+  watchEffect(() => {
+    if (isSwmx.value) {
+      oldSnCode = pagging.state.query.snCode;
+      pagging.state.query.snCode = "";
+    } else {
+      pagging.state.query.snCode = oldSnCode;
+    }
+  });
+
+  watch(
+    () => pagging.state.query.type,
+    () => {
+      pagging.state.pag.currentPage = 1;
+    }
+  );
+
+  const queryResetRaw = pagging.queryReset;
+  pagging.queryReset = () => {
+    const type = pagging.state.query.searchType;
+    queryResetRaw();
+    pagging.state.query.searchType = type;
+  };
+
+  return reactive({ pagging, keyword, isSwmx });
+};
+export type ScenePagging = ReturnType<typeof useScenePaggingParams>["pagging"];

+ 28 - 0
src/view/newFireCase/dyManager/quisk.ts

@@ -0,0 +1,28 @@
+import { QuoteScene, SceneType } from "@/store/scene";
+import EditModel from "./editModel.vue";
+import SceneDownload from "./sceneDownload.vue";
+import { quiskMountFactory } from "@/helper/mount";
+import { axios, checkHasDownload } from "@/request";
+
+export const editModelScene = quiskMountFactory(EditModel, {
+  title: "编辑模型",
+  width: 500,
+});
+
+export type SceneDpwnloadProps = { scene: QuoteScene };
+export const sceneDownload = async(props: SceneDpwnloadProps) => {
+  const params = {
+    num: props.scene.num,
+    isObj: Number(![SceneType.SWSS, SceneType.SWYDSS].includes(props.scene.type)),
+  };
+  const res = await axios.get(checkHasDownload, { params });
+  const hideFloor = Number(res.data.downloadStatus) !== 3
+
+  const sceneDownloadDialog = quiskMountFactory(SceneDownload, {
+    title: "场景离线包下载",
+    width: 500,
+    hideFloor: hideFloor,
+    enterText: '下 载'
+  });
+  return await sceneDownloadDialog(props);
+}

+ 134 - 0
src/view/newFireCase/dyManager/sceneContent.vue

@@ -0,0 +1,134 @@
+<template>
+  <div class="body-head">
+    <h3 style="visibility: hidden">场景管理</h3>
+  </div>
+
+  <el-table
+    :data="pagging.state.table.rows"
+    tooltip-effect="dark"
+    style="width: 100%"
+    size="large"
+  >
+    <!-- -1 计算失败  0 计算中 1 计算成功并可以外网访问,不能编辑 2计算成功只能内网,能编辑 -->
+    <el-table-column label="序号" width="100" v-slot:default="{ $index }">
+      <span style="text-align: center">
+        {{ pagging.state.pag.size * (pagging.state.pag.currentPage - 1) + $index + 1 }}
+      </span>
+    </el-table-column>
+    <el-table-column label="场景标题" prop="name"></el-table-column>
+    <el-table-column label="S/N码" prop="snCode"></el-table-column>
+    <!-- <el-table-column label="浏览数量" prop="viewCount"></el-table-column> -->
+    <el-table-column label="拍摄时间" prop="createTime" v-slot:default="{ row }">
+      {{ row.createTime.substr(0, 16) }}
+    </el-table-column>
+    <el-table-column label="状态" v-slot:default="{ row }: { row: QuoteScene }">
+      {{ QuoteSceneStatusDesc[row.status] }}
+    </el-table-column>
+    <el-table-column label="所属架构" prop="deptName"></el-table-column>
+    <el-table-column
+      label="操作"
+      v-slot:default="{ row }: { row: QuoteScene }"
+      width="400px"
+    >
+      <span
+        class="oper-span"
+        @click="downHash(row)"
+        v-if="row.status === QuoteSceneStatus.SUCCESS"
+      >
+        Hash
+      </span>
+      <span
+        class="oper-span"
+        @click="copySceneHandler(row)"
+        v-if="row.status === QuoteSceneStatus.SUCCESS"
+      >
+        复制
+      </span>
+      <span
+        class="oper-span"
+        v-pdpath="['view']"
+        @click="openSceneUrl(row, OpenType.query)"
+        v-if="row.status === QuoteSceneStatus.SUCCESS"
+      >
+        查看
+      </span>
+      <span
+        class="oper-span"
+        v-pdpath="['edit']"
+        @click="openSceneUrl(row, OpenType.edit)"
+        v-if="row.status === QuoteSceneStatus.SUCCESS"
+      >
+        编辑
+      </span>
+      <span
+        v-if="
+          [SceneType.SWSS, SceneType.SWYDSS].includes(row.type) &&
+          [QuoteSceneStatus.SUCCESS].includes(row.status) &&
+          row.location === LocationEnum.Scene_Location_PointCloud
+        "
+        v-pdpath="['gen']"
+        class="oper-span"
+        @click="genMeshScene(row)"
+      >
+        生成obj
+      </span>
+
+      <span
+        v-if="
+          ![QuoteSceneStatus.RUN, QuoteSceneStatus.QUEUE].includes(row.status) &&
+          ![SceneType.SWSSMX, SceneType.SWYDMX].includes(row.type)
+        "
+        class="oper-span delBtn"
+        @click="delSceneHandler(row)"
+        v-pdpath="'del'"
+      >
+        删除
+      </span>
+      <span
+        class="oper-span"
+        @click="sceneDownloadHandler(row)"
+        v-pdpath="['download']"
+        v-if="row.num && row.status === QuoteSceneStatus.SUCCESS"
+      >
+        下载
+      </span>
+    </el-table-column>
+  </el-table>
+</template>
+
+<script setup lang="ts">
+import {
+  QuoteScene,
+  QuoteSceneStatus,
+  delQuoteScene,
+  SceneType,
+  genMeshScene,
+  LocationEnum,
+  copyQuoteScene,
+  downQuoteSceneHash,
+} from "@/store/scene";
+import { ScenePagging } from "./pagging";
+import { QuoteSceneStatusDesc } from "@/constant/scene";
+import { OpenType, openSceneUrl } from "../../case/help";
+import { confirm } from "@/helper/message";
+import { sceneDownload } from "./quisk";
+import { downSceneHash } from "@/request";
+
+const props = defineProps<{ pagging: ScenePagging }>();
+const delSceneHandler = async (scene: QuoteScene) => {
+  if (await confirm("确定要删除当前场景吗?")) {
+    await delQuoteScene(scene);
+    props.pagging.refresh();
+  }
+};
+const copySceneHandler = async (scene: QuoteScene) => {
+  await copyQuoteScene(scene);
+  props.pagging.refresh();
+};
+const downHash = async (scene: QuoteScene) => {
+  downQuoteSceneHash(scene);
+};
+const sceneDownloadHandler = (scene: QuoteScene) => {
+  sceneDownload({ scene });
+};
+</script>

+ 139 - 0
src/view/newFireCase/dyManager/sceneDownload.vue

@@ -0,0 +1,139 @@
+<template>
+  <!-- hideFloor: state === State.package -->
+  <div>
+    <div class="title">
+      {{ stateTitle[state] }}
+    </div>
+
+    <div v-if="state === State.package">
+      <div class="text" style="display: flex; justify-content: space-between; margin-top: 15px">
+        <span>{{ filename }}</span>
+        <span>{{ percent }}%</span>
+      </div>
+      <div style="pointer-events: none">
+        <el-slider v-model="percent" :show-tooltip="false" />
+      </div>
+    </div>
+    <div v-else-if="state === State.readDown">
+      <span>正在下载中……</span>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, onUnmounted, ref } from "vue";
+import saveAs from "@/util/file-serve";
+import { checkHasDownload, getDownloadProcess, downloadScene, axios } from "@/request";
+import { ElLoading, ElMessage } from "element-plus";
+import { QuoteScene, SceneType } from "@/store/scene";
+import { QuiskExpose } from "@/helper/mount";
+
+const props = defineProps<{ scene: QuoteScene }>();
+enum State {
+  uncreate,
+  package,
+  readDown,
+}
+const getState = (type: number) => {
+  const stateTypes = [
+    { codes: [0, 2], state: State.uncreate },
+    { codes: [1], state: State.package },
+    { codes: [3], state: State.readDown },
+  ];
+  return (
+    stateTypes.find((stateType) => stateType.codes.includes(type))?.state ||
+    State.uncreate
+  );
+};
+
+const state = ref<State>(State.uncreate);
+const count = ref<number>(0);
+const filename = ref<string>(props.scene.title + ".zip");
+const downloadURL = ref<string>();
+const percent = ref(0);
+
+const stateTitle = {
+  [State.uncreate]: "下载场景离线数据包,可在本地运行查看。",
+  [State.package]: "正在打包场景离线数据",
+  [State.readDown]: filename.value,
+};
+
+const params = {
+  num: props.scene.num,
+  isObj: Number(![SceneType.SWSS, SceneType.SWYDSS].includes(props.scene.type)),
+};
+// 初始化
+const initial = async () => {
+  const res = await axios.get(checkHasDownload, { params });
+  state.value = getState(res.data.downloadStatus);
+  count.value = res.data.count;
+  downloadURL.value = res.data.downloadUrl;
+
+  if (state.value === State.uncreate) {
+    const downRes = await axios.get(downloadScene, { params });
+    state.value = getState(downRes.data.downloadStatus);
+    // const unCountFlag =
+    //   count.value == 0 ||(await axios.get(downloadScene, { params })).data.downloadStatus !== 1;
+    if (state.value === State.uncreate) {
+      ElMessage.error("下载失败,请联系管理员");
+      throw "暂无剩余下载次数";
+    }
+  }
+
+  if (state.value === State.package) {
+    await new Promise<void>((resolve) => requestUpdateURL(resolve));
+  } else {
+    downloadURL.value = res.data.downloadUrl;
+  }
+};
+
+// 下载
+const download = () => {
+  if (!downloadURL.value) {
+    ElMessage.error("下载链接未生成,请稍等!");
+    throw "下载链接未生成,请稍等!";
+  } else {
+
+    if (String(downloadURL.value.includes('http'))) {
+      downloadURL.value = downloadURL.value;
+    } else {
+      if (!downloadURL.value.startsWith("/")) {
+        downloadURL.value = "/" + downloadURL.value;
+      }
+    }
+    console.error("downloadURL.value", downloadURL.value);
+    return saveAs(downloadURL.value, filename.value);
+  }
+};
+
+// 进度请求
+let timer: any;
+const requestUpdateURL = async (callback: () => void) => {
+  const res = await axios.get(getDownloadProcess, { params });
+
+  percent.value = parseInt(res.data.percent);
+  downloadURL.value = res.data.url;
+  if (downloadURL.value) {
+    state.value = State.readDown;
+    callback();
+  } else {
+    timer = setTimeout(() => requestUpdateURL(callback), 1000);
+  }
+};
+
+onUnmounted(() => clearTimeout(timer));
+
+defineExpose<QuiskExpose>({
+  submit: async () => {
+    await initial();
+    const loading = ElLoading.service({
+      lock: true,
+      text: "下载中",
+      background: "rgba(255, 255, 255, 0.4)",
+    });
+    await download();
+    loading.close();
+    ElMessage.success("下载完成");
+  },
+});
+</script>

+ 123 - 0
src/view/newFireCase/meshManager/downloadLog.vue

@@ -0,0 +1,123 @@
+<template>
+  <com-head :options="[{ name: '下载记录', value: '2' }]" showCtrl>
+    <el-form label-width="84px">
+      <el-form-item label="所属架构:">
+        <com-company v-model="state.query.deptId" />
+      </el-form-item>
+      <el-form-item label="用户姓名:">
+        <el-input v-model="state.query.nickName" placeholder="请输入"></el-input>
+      </el-form-item>
+      <el-form-item label="用户账号:">
+        <el-input v-model="state.query.userName" placeholder="请输入手机号"></el-input>
+      </el-form-item>
+      <el-form-item label="下载时间:">
+        <el-date-picker
+          type="daterange"
+          format="YYYY-MM-DD"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          v-model="createTime"
+          placeholder="请选择"
+          :defaultTime="defaultTime"
+          style="width: 100%"
+        />
+      </el-form-item>
+      <el-form-item label="场景标题:">
+        <el-input v-model="state.query.sceneTitle" placeholder="请输入"></el-input>
+      </el-form-item>
+      <el-form-item label="场景码:">
+        <el-input v-model="state.query.sceneNum" placeholder="请输入"></el-input>
+      </el-form-item>
+      <el-form-item label="SN码:">
+        <el-input v-model="state.query.snCode" placeholder="请输入"></el-input>
+      </el-form-item>
+
+      <el-form-item class="searh-btns" style="grid-area: 1/4/4/5">
+        <el-button type="primary" @click="refresh">查询</el-button>
+        <el-button type="primary" plain @click="queryReset">重置</el-button>
+      </el-form-item>
+    </el-form>
+  </com-head>
+
+  <div class="body-layer" style="padding-top: 8px">
+    <el-table
+      class="user-table"
+      :data="state.table.rows"
+      style="width: 100%; max-height: 480px"
+      size="large"
+    >
+      <el-table-column label="序号" width="100" v-slot:default="{ $index }">
+        <span style="text-align: center">
+          {{ state.pag.size * (state.pag.currentPage - 1) + $index + 1 }}
+        </span>
+      </el-table-column>
+      <el-table-column label="组织名称" prop="deptName"></el-table-column>
+      <el-table-column label="组织类型" prop="deptLevelStr"></el-table-column>
+      <el-table-column label="用户姓名" prop="nickName"></el-table-column>
+      <el-table-column label="用户账号" prop="userName"></el-table-column>
+      <el-table-column label="下载时间" prop="createTime"></el-table-column>
+      <el-table-column label="场景标题" prop="sceneTitle"></el-table-column>
+      <el-table-column label="场景码" prop="sceneNum"></el-table-column>
+      <el-table-column label="SN码" prop="snCode"></el-table-column>
+    </el-table>
+
+    <com-pagination
+      @size-change="changPageSize"
+      @current-change="changPageCurrent"
+      :current-page="state.pag.currentPage"
+      :page-size="state.pag.size"
+      :total="state.pag.total"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { usePagging } from "@/hook/pagging";
+import comHead from "@/components/head/index.vue";
+import comCompany from "@/components/company-select/index.vue";
+import comPagination from "@/components/pagination/index.vue";
+import { getDownloadQuoteScene } from "@/store/scene";
+import { ref, watchEffect } from "vue";
+import { dateFormat } from "@/util";
+
+const {
+  state,
+  queryReset: queryResetRaw,
+  refresh,
+  changPageCurrent,
+  changPageSize,
+} = usePagging({
+  get: getDownloadQuoteScene,
+  paramsTemlate: {
+    nickName: "",
+    deptId: "",
+    userName: "",
+    startCreateTime: "",
+    endCreateTime: "",
+    sceneTitle: "",
+    sceneNum: "",
+    snCode: "",
+  },
+});
+
+const defaultTime: [Date, Date] = [
+  new Date(2000, 1, 1, 0, 0, 0),
+  new Date(2000, 2, 1, 23, 59, 59),
+];
+const createTime = ref<Date[] | null>(null);
+watchEffect(() => {
+  if (createTime.value && createTime.value.length === 2) {
+    state.query.startCreateTime = dateFormat(createTime.value[0], "yyyy-MM-dd hh:mm:ss");
+    state.query.endCreateTime = dateFormat(createTime.value[1], "yyyy-MM-dd hh:mm:ss");
+  } else {
+    state.query.startCreateTime = null as any;
+    state.query.endCreateTime = null as any;
+  }
+});
+const queryReset = () => {
+  queryResetRaw();
+  createTime.value = null;
+};
+</script>
+
+<style scoped lang="scss"></style>

+ 40 - 0
src/view/newFireCase/meshManager/editModel.vue

@@ -0,0 +1,40 @@
+<template>
+  <el-form ref="form" label-width="84px">
+    <el-form-item label="模型名称">
+      <el-input
+        v-model="bindModel.modelTitle"
+        maxlength="50"
+        placeholder="请输入模型名称"
+      />
+    </el-form-item>
+    <el-form-item label="渲染方式" v-if="bindModel.modelDateType === 'obj'">
+      <el-select placeholder="请选择" v-model="bindModel.renderType">
+        <el-option label="基础材质(无光照)" value="base" />
+        <el-option label="标准材质(有光照,适用于无贴图模型)" value="normal" />
+      </el-select>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import { ref } from "vue";
+import { ModelScene, setModelScene } from "@/store/scene";
+import { ElMessage } from "element-plus";
+import { QuiskExpose } from "@/helper/mount";
+
+const props = defineProps<{ model: ModelScene }>();
+const bindModel = ref<ModelScene>({
+  ...props.model,
+  renderType: props.model.renderType || "base",
+});
+
+defineExpose<QuiskExpose>({
+  async submit() {
+    if (!bindModel.value.modelTitle || !bindModel.value.modelTitle.trim()) {
+      ElMessage.error("模型名称不能为空");
+      throw "模型名称不能为空";
+    }
+    await setModelScene(bindModel.value);
+  },
+});
+</script>

+ 49 - 0
src/view/newFireCase/meshManager/index.vue

@@ -0,0 +1,49 @@
+<template>
+  <List :params="params">
+    <template v-slot:header>
+      <el-form-item label="标题:">
+        <el-input v-model="params.keyword" placeholder="请输入"></el-input>
+      </el-form-item>
+      <el-form-item label="设备类型:">
+        <el-select placeholder="请选择" v-model="params.pagging.state.query.cameraType">
+          <el-option
+            v-for="option in cameraTypeList"
+            :key="option.cameraType"
+            :value="option.cameraType"
+            :label="option.cameraName"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="S/N码:" v-if="!params.isSwmx">
+        <el-input
+          v-model="params.pagging.state.query.snCode"
+          placeholder="请输入"
+        ></el-input>
+      </el-form-item>
+    </template>
+    <template v-slot:content>
+      <component :is="component" :pagging="params.pagging" />
+    </template>
+  </List>
+</template>
+
+<script setup lang="ts">
+import List from "./list.vue";
+import SceneContent from "./sceneContent.vue";
+import ModelContent from "./modelContent.vue";
+import { SceneType } from "@/store/scene";
+import { SceneTypeDesc } from "@/constant/scene";
+import { useScenePaggingParams } from "./pagging";
+import { computed, onMounted, ref } from "vue";
+import { getCameraTypeAllList } from "@/store/scene";
+
+const cameraTypeList = ref([])
+onMounted(() => {
+  getCameraTypeAllList().then(res => {
+    cameraTypeList.value = res || []
+  })
+})
+
+const params = useScenePaggingParams();
+const component = computed(() => (params.isSwmx ? ModelContent : SceneContent));
+</script>

+ 49 - 0
src/view/newFireCase/meshManager/list.vue

@@ -0,0 +1,49 @@
+<template>
+  <com-head :options="listOptions" v-model="params.pagging.state.query.searchType">
+    <el-form label-width="84px" inline>
+      <slot name="header" />
+      <el-form-item class="searh-btns" style="grid-area: 1 / 4 / 2 / 4">
+        <el-button type="primary" @click="params.pagging.refresh">查询</el-button>
+        <el-button type="primary" plain @click="params.pagging.queryReset"
+          >重置</el-button
+        >
+      </el-form-item>
+    </el-form>
+  </com-head>
+
+  <div class="body-layer">
+    <slot name="content" />
+    <com-pagination
+      @size-change="params.pagging.changPageSize"
+      @current-change="params.pagging.changPageCurrent"
+      :current-page="params.pagging.state.pag.currentPage"
+      :page-size="params.pagging.state.pag.size"
+      :total="params.pagging.state.pag.total"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import comHead from "@/components/head/index.vue";
+import comPagination from "@/components/pagination/index.vue";
+import { SceneType } from "@/store/scene";
+import { SceneTypeDesc } from "@/constant/scene";
+import { useScenePaggingParams } from "./pagging";
+
+defineProps<{ params: ReturnType<typeof useScenePaggingParams> }>();
+
+const headOptions = [
+  { value: SceneType.SWKK, name: SceneTypeDesc[SceneType.SWKK] },
+  { value: SceneType.SWKJ, name: SceneTypeDesc[SceneType.SWKJ] },
+  { value: SceneType.SWSS, name: SceneTypeDesc[SceneType.SWSS] },
+  { value: SceneType.SWSSMX, name: SceneTypeDesc[SceneType.SWSSMX] },
+  { value: SceneType.SWYDSS, name: SceneTypeDesc[SceneType.SWYDSS] },
+  { value: SceneType.SWYDMX, name: SceneTypeDesc[SceneType.SWYDMX] },
+  // { value: SceneType.SWMX, name: SceneTypeDesc[SceneType.SWMX] },
+];
+const listOptions = [
+  { name: "场景列表", value: "0" },
+  { name: "场景共享", value: "1" },
+  { name: "全部", value: "2" },
+];
+</script>

+ 192 - 0
src/view/newFireCase/meshManager/modelContent.vue

@@ -0,0 +1,192 @@
+<template>
+  <div class="body-head">
+    <h3 style="visibility: hidden">场景管理</h3>
+
+    <el-tooltip
+      class="item"
+      effect="dark"
+      :content="`请上传${format}(支持obj/ply/las/laz/osgb/b3dm格式的数据),大小在${size}以内 `"
+      placement="bottom-start"
+      ><el-upload
+        class="upload-demo"
+        :multiple="false"
+        :limit="1"
+        :accept="accept"
+        :show-file-list="false"
+        :http-request="() => {}"
+        :file-list="fileList"
+        :disabled="percentage || !operateIsPermissionByPath('sync')"
+        :before-upload="uploadCheck"
+      >
+        <el-button v-pdpath="'sync'" type="primary">
+          <el-icon><Upload /></el-icon>{{ percentage ? "文件上传中" : "上传数据" }}
+        </el-button>
+      </el-upload>
+    </el-tooltip>
+  </div>
+
+  <el-table
+    :data="pagging.state.table.rows"
+    tooltip-effect="dark"
+    style="width: 100%"
+    size="large"
+  >
+    <el-table-column label="序号" width="100" v-slot:default="{ $index }">
+      <span style="text-align: center">
+        {{ pagging.state.pag.size * (pagging.state.pag.currentPage - 1) + $index + 1 }}
+      </span>
+    </el-table-column>
+    <el-table-column label="标题" prop="modelTitle"></el-table-column>
+    <el-table-column label="原始数据格式" prop="modelDateType"></el-table-column>
+    <el-table-column label="大小" prop="modelSize"></el-table-column>
+    <el-table-column label="上传时间" v-slot:default="{ row }: { row: ModelScene }">
+      {{ getStatusText(row) }}
+    </el-table-column>
+    <el-table-column label="所属架构" prop="deptName"></el-table-column>
+    <el-table-column label="操作" v-slot:default="{ row }" width="350px">
+      <template v-if="row.createStatus === ModelSceneStatus.SUCCESS">
+        <span class="oper-span" @click="downOrigin(row)" v-if="row.fileNewName">
+          下载原始资源
+        </span>
+        <span class="oper-span" @click="downHash(row)"> Hash </span>
+        <span class="oper-span" @click="copyHanlder(row)"> 复制 </span>
+        <span class="oper-span" v-pdpath="['edit']" @click="editHanlder(row)">
+          修改
+        </span>
+        <span
+          class="oper-span"
+          v-pdpath="['view']"
+          @click="openSceneUrl(row, OpenType.query)"
+        >
+          查看
+        </span>
+      </template>
+      <span
+        v-if="row.createStatus !== ModelSceneStatus.REV"
+        class="oper-span delBtn"
+        @click="delOrCancel(row)"
+        v-pdpath="'del'"
+      >
+        {{ row.createStatus !== ModelSceneStatus.RUN ? "删除" : "取消上传" }}
+      </span>
+
+      <span v-else class="oper-span" v-pdpath="['viewaaa']"> 模型转换中… </span>
+    </el-table-column>
+  </el-table>
+
+  <el-dialog
+    :model-value="!!percentage"
+    :show-close="false"
+    title="文件上传中"
+    :close-on-click-modal="false"
+  >
+    <el-progress :percentage="percentage" />
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import {
+  ModelSceneStatus,
+  ModelScene,
+  cancelUploadModelScene,
+  uploadModelScene,
+  delModelScene,
+  getModelSceneStatus,
+  copyModelScene,
+  downModelSceneHash,
+} from "@/store/scene";
+import {
+  ModelMaxSize,
+  ModelSceneStatusDesc,
+  ModelSupportFormats,
+} from "@/constant/scene";
+import { confirm } from "@/helper/message";
+import { useUpload } from "@/hook/upload";
+import { ScenePagging } from "./pagging";
+import { watchPolling } from "@/hook/watchPolling";
+import { OpenType, openSceneUrl } from "../../case/help";
+import { operateIsPermissionByPath } from "@/directive/permission";
+import { editModelScene } from "./quisk";
+import saveAs from "@/util/file-serve";
+
+const props = defineProps<{ pagging: ScenePagging }>();
+
+const getStatusText = (scene: ModelScene) => {
+  let desc = ModelSceneStatusDesc[scene.createStatus];
+  if (scene.createStatus === ModelSceneStatus.RUN && scene.progress) {
+    desc += ` ${scene.progress}% `;
+  } else if (scene.createStatus === ModelSceneStatus.SUCCESS) {
+    desc = scene.createTime;
+  }
+  return desc;
+};
+
+const delOrCancel = async (scene: ModelScene) => {
+  const isDel = scene.createStatus !== ModelSceneStatus.RUN;
+  const msg = isDel ? "确定要删除此数据?" : "确定要取消上传吗?";
+
+  if (await confirm(msg)) {
+    isDel ? await delModelScene(scene) : await cancelUploadModelScene(scene);
+    props.pagging.refresh();
+  }
+};
+
+const editHanlder = async (scene: ModelScene) => {
+  if (await editModelScene({ model: scene })) {
+    props.pagging.refresh();
+  }
+};
+
+const copyHanlder = async (scene: ModelScene) => {
+  if (await copyModelScene(scene)) {
+    props.pagging.refresh();
+  }
+};
+const downHash = async (scene: ModelScene) => {
+  downModelSceneHash(scene);
+};
+
+const {
+  percentage,
+  upload: uploadCheck,
+  fileList,
+  size,
+  format,
+  removeFile,
+  accept,
+} = useUpload({
+  maxSize: ModelMaxSize,
+  formats: ModelSupportFormats,
+  upload: async (file, onPercentage) => {
+    try {
+      await uploadModelScene(file, onPercentage);
+      props.pagging.refresh();
+    } catch {}
+    removeFile();
+  },
+});
+
+// 处理后台正在处理的模型类
+const refreshStatus = (models: ModelScene[]) => {
+  const refreshStatusAll = models.map(async (scene) => {
+    const { status, progress } = await getModelSceneStatus(scene);
+    scene.createStatus = status;
+    scene.progress = progress;
+    if (status == ModelSceneStatus.SUCCESS) {
+      props.pagging.refresh();
+    }
+  });
+  return Promise.all(refreshStatusAll);
+};
+
+const downOrigin = async (model: ModelScene) => {
+  await saveAs(model.fileNewName, model.modelTitle + ".zip");
+};
+
+watchPolling(() => {
+  const payload = (props.pagging.state.table.rows as ModelScene[]).filter(
+    (item) => item.createStatus === ModelSceneStatus.RUN
+  );
+  return { start: payload.length > 0, payload };
+}, refreshStatus);
+</script>

+ 58 - 0
src/view/newFireCase/meshManager/pagging.ts

@@ -0,0 +1,58 @@
+import { usePagging } from "@/hook/pagging";
+import { SceneType, getScenePagging } from "@/store/scene";
+import { computed, reactive, watch, watchEffect } from "vue";
+
+export const useScenePaggingParams = () => {
+  const pagging = usePagging({
+    get: getScenePagging,
+    paramsTemlate: {
+      searchType: "0",
+      cameraType: '',
+      sceneName: "",
+      modelTitle: "",
+      deptId: "",
+      snCode: "",
+      isObj: 1,
+    },
+  });
+
+  const isSwmx = computed(() => pagging.state.query.type === SceneType.SWMX);
+  const keyword = computed({
+    get: () =>
+      isSwmx.value
+        ? pagging.state.query.modelTitle
+        : pagging.state.query.sceneName,
+    set: (val: string) => {
+      pagging.state.query.modelTitle = val;
+      pagging.state.query.sceneName = val;
+    },
+  });
+
+  let oldSnCode = pagging.state.query.snCode;
+  watchEffect(() => {
+    if (isSwmx.value) {
+      oldSnCode = pagging.state.query.snCode;
+      pagging.state.query.snCode = "";
+    } else {
+      pagging.state.query.snCode = oldSnCode;
+    }
+  });
+
+  watch(
+    () => pagging.state.query.type,
+    () => {
+      pagging.state.pag.currentPage = 1;
+    }
+  );
+
+  const queryResetRaw = pagging.queryReset;
+  pagging.queryReset = () => {
+    const type = pagging.state.query.searchType;
+    queryResetRaw();
+    pagging.state.query.searchType = type;
+  };
+
+  return reactive({ pagging, keyword, isSwmx });
+};
+export type ScenePagging = ReturnType<typeof useScenePaggingParams>["pagging"];
+

+ 28 - 0
src/view/newFireCase/meshManager/quisk.ts

@@ -0,0 +1,28 @@
+import { QuoteScene, SceneType } from "@/store/scene";
+import EditModel from "./editModel.vue";
+import SceneDownload from "./sceneDownload.vue";
+import { quiskMountFactory } from "@/helper/mount";
+import { axios, checkHasDownload } from "@/request";
+
+export const editModelScene = quiskMountFactory(EditModel, {
+  title: "编辑模型",
+  width: 500,
+});
+
+export type SceneDpwnloadProps = { scene: QuoteScene };
+export const sceneDownload = async(props: SceneDpwnloadProps) => {
+  const params = {
+    num: props.scene.num,
+    isObj: Number(![SceneType.SWSS, SceneType.SWYDSS].includes(props.scene.type)),
+  };
+  const res = await axios.get(checkHasDownload, { params });
+  const hideFloor = Number(res.data.downloadStatus) !== 3
+
+  const sceneDownloadDialog = quiskMountFactory(SceneDownload, {
+    title: "场景离线包下载",
+    width: 500,
+    hideFloor: hideFloor,
+    enterText: '下 载'
+  });
+  return await sceneDownloadDialog(props);
+}

+ 134 - 0
src/view/newFireCase/meshManager/sceneContent.vue

@@ -0,0 +1,134 @@
+<template>
+  <div class="body-head">
+    <h3 style="visibility: hidden">场景管理</h3>
+  </div>
+
+  <el-table
+    :data="pagging.state.table.rows"
+    tooltip-effect="dark"
+    style="width: 100%"
+    size="large"
+  >
+    <!-- -1 计算失败  0 计算中 1 计算成功并可以外网访问,不能编辑 2计算成功只能内网,能编辑 -->
+    <el-table-column label="序号" width="100" v-slot:default="{ $index }">
+      <span style="text-align: center">
+        {{ pagging.state.pag.size * (pagging.state.pag.currentPage - 1) + $index + 1 }}
+      </span>
+    </el-table-column>
+    <el-table-column label="场景标题" prop="name"></el-table-column>
+    <el-table-column label="S/N码" prop="snCode"></el-table-column>
+    <!-- <el-table-column label="浏览数量" prop="viewCount"></el-table-column> -->
+    <el-table-column label="拍摄时间" prop="createTime" v-slot:default="{ row }">
+      {{ row.createTime.substr(0, 16) }}
+    </el-table-column>
+    <el-table-column label="状态" v-slot:default="{ row }: { row: QuoteScene }">
+      {{ QuoteSceneStatusDesc[row.status] }}
+    </el-table-column>
+    <el-table-column label="所属架构" prop="deptName"></el-table-column>
+    <el-table-column
+      label="操作"
+      v-slot:default="{ row }: { row: QuoteScene }"
+      width="400px"
+    >
+      <span
+        class="oper-span"
+        @click="downHash(row)"
+        v-if="row.status === QuoteSceneStatus.SUCCESS"
+      >
+        Hash
+      </span>
+      <span
+        class="oper-span"
+        @click="copySceneHandler(row)"
+        v-if="row.status === QuoteSceneStatus.SUCCESS"
+      >
+        复制
+      </span>
+      <span
+        class="oper-span"
+        v-pdpath="['view']"
+        @click="openSceneUrl(row, OpenType.query)"
+        v-if="row.status === QuoteSceneStatus.SUCCESS"
+      >
+        查看
+      </span>
+      <span
+        class="oper-span"
+        v-pdpath="['edit']"
+        @click="openSceneUrl(row, OpenType.edit)"
+        v-if="row.status === QuoteSceneStatus.SUCCESS"
+      >
+        编辑
+      </span>
+      <span
+        v-if="
+          [SceneType.SWSS, SceneType.SWYDSS].includes(row.type) &&
+          [QuoteSceneStatus.SUCCESS].includes(row.status) &&
+          row.location === LocationEnum.Scene_Location_PointCloud
+        "
+        v-pdpath="['gen']"
+        class="oper-span"
+        @click="genMeshScene(row)"
+      >
+        生成obj
+      </span>
+
+      <span
+        v-if="
+          ![QuoteSceneStatus.RUN, QuoteSceneStatus.QUEUE].includes(row.status) &&
+          ![SceneType.SWSSMX, SceneType.SWYDMX].includes(row.type)
+        "
+        class="oper-span delBtn"
+        @click="delSceneHandler(row)"
+        v-pdpath="'del'"
+      >
+        删除
+      </span>
+      <span
+        class="oper-span"
+        @click="sceneDownloadHandler(row)"
+        v-pdpath="['download']"
+        v-if="row.num && row.status === QuoteSceneStatus.SUCCESS"
+      >
+        下载
+      </span>
+    </el-table-column>
+  </el-table>
+</template>
+
+<script setup lang="ts">
+import {
+  QuoteScene,
+  QuoteSceneStatus,
+  delQuoteScene,
+  SceneType,
+  genMeshScene,
+  LocationEnum,
+  copyQuoteScene,
+  downQuoteSceneHash,
+} from "@/store/scene";
+import { ScenePagging } from "./pagging";
+import { QuoteSceneStatusDesc } from "@/constant/scene";
+import { OpenType, openSceneUrl } from "../../case/help";
+import { confirm } from "@/helper/message";
+import { sceneDownload } from "./quisk";
+import { downSceneHash } from "@/request";
+
+const props = defineProps<{ pagging: ScenePagging }>();
+const delSceneHandler = async (scene: QuoteScene) => {
+  if (await confirm("确定要删除当前场景吗?")) {
+    await delQuoteScene(scene);
+    props.pagging.refresh();
+  }
+};
+const copySceneHandler = async (scene: QuoteScene) => {
+  await copyQuoteScene(scene);
+  props.pagging.refresh();
+};
+const downHash = async (scene: QuoteScene) => {
+  downQuoteSceneHash(scene);
+};
+const sceneDownloadHandler = (scene: QuoteScene) => {
+  sceneDownload({ scene });
+};
+</script>

+ 139 - 0
src/view/newFireCase/meshManager/sceneDownload.vue

@@ -0,0 +1,139 @@
+<template>
+  <!-- hideFloor: state === State.package -->
+  <div>
+    <div class="title">
+      {{ stateTitle[state] }}
+    </div>
+
+    <div v-if="state === State.package">
+      <div class="text" style="display: flex; justify-content: space-between; margin-top: 15px">
+        <span>{{ filename }}</span>
+        <span>{{ percent }}%</span>
+      </div>
+      <div style="pointer-events: none">
+        <el-slider v-model="percent" :show-tooltip="false" />
+      </div>
+    </div>
+    <div v-else-if="state === State.readDown">
+      <span>正在下载中……</span>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, onUnmounted, ref } from "vue";
+import saveAs from "@/util/file-serve";
+import { checkHasDownload, getDownloadProcess, downloadScene, axios } from "@/request";
+import { ElLoading, ElMessage } from "element-plus";
+import { QuoteScene, SceneType } from "@/store/scene";
+import { QuiskExpose } from "@/helper/mount";
+
+const props = defineProps<{ scene: QuoteScene }>();
+enum State {
+  uncreate,
+  package,
+  readDown,
+}
+const getState = (type: number) => {
+  const stateTypes = [
+    { codes: [0, 2], state: State.uncreate },
+    { codes: [1], state: State.package },
+    { codes: [3], state: State.readDown },
+  ];
+  return (
+    stateTypes.find((stateType) => stateType.codes.includes(type))?.state ||
+    State.uncreate
+  );
+};
+
+const state = ref<State>(State.uncreate);
+const count = ref<number>(0);
+const filename = ref<string>(props.scene.title + ".zip");
+const downloadURL = ref<string>();
+const percent = ref(0);
+
+const stateTitle = {
+  [State.uncreate]: "下载场景离线数据包,可在本地运行查看。",
+  [State.package]: "正在打包场景离线数据",
+  [State.readDown]: filename.value,
+};
+
+const params = {
+  num: props.scene.num,
+  isObj: Number(![SceneType.SWSS, SceneType.SWYDSS].includes(props.scene.type)),
+};
+// 初始化
+const initial = async () => {
+  const res = await axios.get(checkHasDownload, { params });
+  state.value = getState(res.data.downloadStatus);
+  count.value = res.data.count;
+  downloadURL.value = res.data.downloadUrl;
+
+  if (state.value === State.uncreate) {
+    const downRes = await axios.get(downloadScene, { params });
+    state.value = getState(downRes.data.downloadStatus);
+    // const unCountFlag =
+    //   count.value == 0 ||(await axios.get(downloadScene, { params })).data.downloadStatus !== 1;
+    if (state.value === State.uncreate) {
+      ElMessage.error("下载失败,请联系管理员");
+      throw "暂无剩余下载次数";
+    }
+  }
+
+  if (state.value === State.package) {
+    await new Promise<void>((resolve) => requestUpdateURL(resolve));
+  } else {
+    downloadURL.value = res.data.downloadUrl;
+  }
+};
+
+// 下载
+const download = () => {
+  if (!downloadURL.value) {
+    ElMessage.error("下载链接未生成,请稍等!");
+    throw "下载链接未生成,请稍等!";
+  } else {
+
+    if (String(downloadURL.value.includes('http'))) {
+      downloadURL.value = downloadURL.value;
+    } else {
+      if (!downloadURL.value.startsWith("/")) {
+        downloadURL.value = "/" + downloadURL.value;
+      }
+    }
+    console.error("downloadURL.value", downloadURL.value);
+    return saveAs(downloadURL.value, filename.value);
+  }
+};
+
+// 进度请求
+let timer: any;
+const requestUpdateURL = async (callback: () => void) => {
+  const res = await axios.get(getDownloadProcess, { params });
+
+  percent.value = parseInt(res.data.percent);
+  downloadURL.value = res.data.url;
+  if (downloadURL.value) {
+    state.value = State.readDown;
+    callback();
+  } else {
+    timer = setTimeout(() => requestUpdateURL(callback), 1000);
+  }
+};
+
+onUnmounted(() => clearTimeout(timer));
+
+defineExpose<QuiskExpose>({
+  submit: async () => {
+    await initial();
+    const loading = ElLoading.service({
+      lock: true,
+      text: "下载中",
+      background: "rgba(255, 255, 255, 0.4)",
+    });
+    await download();
+    loading.close();
+    ElMessage.success("下载完成");
+  },
+});
+</script>

+ 123 - 0
src/view/newFireCase/mix3dManager/downloadLog.vue

@@ -0,0 +1,123 @@
+<template>
+  <com-head :options="[{ name: '下载记录', value: '2' }]" showCtrl>
+    <el-form label-width="84px">
+      <el-form-item label="所属架构:">
+        <com-company v-model="state.query.deptId" />
+      </el-form-item>
+      <el-form-item label="用户姓名:">
+        <el-input v-model="state.query.nickName" placeholder="请输入"></el-input>
+      </el-form-item>
+      <el-form-item label="用户账号:">
+        <el-input v-model="state.query.userName" placeholder="请输入手机号"></el-input>
+      </el-form-item>
+      <el-form-item label="下载时间:">
+        <el-date-picker
+          type="daterange"
+          format="YYYY-MM-DD"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          v-model="createTime"
+          placeholder="请选择"
+          :defaultTime="defaultTime"
+          style="width: 100%"
+        />
+      </el-form-item>
+      <el-form-item label="场景标题:">
+        <el-input v-model="state.query.sceneTitle" placeholder="请输入"></el-input>
+      </el-form-item>
+      <el-form-item label="场景码:">
+        <el-input v-model="state.query.sceneNum" placeholder="请输入"></el-input>
+      </el-form-item>
+      <el-form-item label="SN码:">
+        <el-input v-model="state.query.snCode" placeholder="请输入"></el-input>
+      </el-form-item>
+
+      <el-form-item class="searh-btns" style="grid-area: 1/4/4/5">
+        <el-button type="primary" @click="refresh">查询</el-button>
+        <el-button type="primary" plain @click="queryReset">重置</el-button>
+      </el-form-item>
+    </el-form>
+  </com-head>
+
+  <div class="body-layer" style="padding-top: 8px">
+    <el-table
+      class="user-table"
+      :data="state.table.rows"
+      style="width: 100%; max-height: 480px"
+      size="large"
+    >
+      <el-table-column label="序号" width="100" v-slot:default="{ $index }">
+        <span style="text-align: center">
+          {{ state.pag.size * (state.pag.currentPage - 1) + $index + 1 }}
+        </span>
+      </el-table-column>
+      <el-table-column label="组织名称" prop="deptName"></el-table-column>
+      <el-table-column label="组织类型" prop="deptLevelStr"></el-table-column>
+      <el-table-column label="用户姓名" prop="nickName"></el-table-column>
+      <el-table-column label="用户账号" prop="userName"></el-table-column>
+      <el-table-column label="下载时间" prop="createTime"></el-table-column>
+      <el-table-column label="场景标题" prop="sceneTitle"></el-table-column>
+      <el-table-column label="场景码" prop="sceneNum"></el-table-column>
+      <el-table-column label="SN码" prop="snCode"></el-table-column>
+    </el-table>
+
+    <com-pagination
+      @size-change="changPageSize"
+      @current-change="changPageCurrent"
+      :current-page="state.pag.currentPage"
+      :page-size="state.pag.size"
+      :total="state.pag.total"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { usePagging } from "@/hook/pagging";
+import comHead from "@/components/head/index.vue";
+import comCompany from "@/components/company-select/index.vue";
+import comPagination from "@/components/pagination/index.vue";
+import { getDownloadQuoteScene } from "@/store/scene";
+import { ref, watchEffect } from "vue";
+import { dateFormat } from "@/util";
+
+const {
+  state,
+  queryReset: queryResetRaw,
+  refresh,
+  changPageCurrent,
+  changPageSize,
+} = usePagging({
+  get: getDownloadQuoteScene,
+  paramsTemlate: {
+    nickName: "",
+    deptId: "",
+    userName: "",
+    startCreateTime: "",
+    endCreateTime: "",
+    sceneTitle: "",
+    sceneNum: "",
+    snCode: "",
+  },
+});
+
+const defaultTime: [Date, Date] = [
+  new Date(2000, 1, 1, 0, 0, 0),
+  new Date(2000, 2, 1, 23, 59, 59),
+];
+const createTime = ref<Date[] | null>(null);
+watchEffect(() => {
+  if (createTime.value && createTime.value.length === 2) {
+    state.query.startCreateTime = dateFormat(createTime.value[0], "yyyy-MM-dd hh:mm:ss");
+    state.query.endCreateTime = dateFormat(createTime.value[1], "yyyy-MM-dd hh:mm:ss");
+  } else {
+    state.query.startCreateTime = null as any;
+    state.query.endCreateTime = null as any;
+  }
+});
+const queryReset = () => {
+  queryResetRaw();
+  createTime.value = null;
+};
+</script>
+
+<style scoped lang="scss"></style>

+ 40 - 0
src/view/newFireCase/mix3dManager/editModel.vue

@@ -0,0 +1,40 @@
+<template>
+  <el-form ref="form" label-width="84px">
+    <el-form-item label="模型名称">
+      <el-input
+        v-model="bindModel.modelTitle"
+        maxlength="50"
+        placeholder="请输入模型名称"
+      />
+    </el-form-item>
+    <el-form-item label="渲染方式" v-if="bindModel.modelDateType === 'obj'">
+      <el-select placeholder="请选择" v-model="bindModel.renderType">
+        <el-option label="基础材质(无光照)" value="base" />
+        <el-option label="标准材质(有光照,适用于无贴图模型)" value="normal" />
+      </el-select>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import { ref } from "vue";
+import { ModelScene, setModelScene } from "@/store/scene";
+import { ElMessage } from "element-plus";
+import { QuiskExpose } from "@/helper/mount";
+
+const props = defineProps<{ model: ModelScene }>();
+const bindModel = ref<ModelScene>({
+  ...props.model,
+  renderType: props.model.renderType || "base",
+});
+
+defineExpose<QuiskExpose>({
+  async submit() {
+    if (!bindModel.value.modelTitle || !bindModel.value.modelTitle.trim()) {
+      ElMessage.error("模型名称不能为空");
+      throw "模型名称不能为空";
+    }
+    await setModelScene(bindModel.value);
+  },
+});
+</script>

+ 24 - 0
src/view/newFireCase/mix3dManager/index.vue

@@ -0,0 +1,24 @@
+<template>
+  <List :params="params">
+    <template v-slot:header>
+      <el-form-item label="标题:">
+        <el-input v-model="params.pagging.state.query.modelTitle" placeholder="请输入"></el-input>
+      </el-form-item>
+    </template>
+    <template v-slot:content>
+      <component :is="SceneContent" :pagging="params.pagging" />
+    </template>
+  </List>
+</template>
+
+<script setup lang="ts">
+import List from "./list.vue";
+import SceneContent from "./sceneContent.vue";
+import { SceneType } from "@/store/scene";
+import { SceneTypeDesc } from "@/constant/scene";
+import { useScenePaggingParams } from "./pagging";
+import { computed } from "vue";
+
+const params = useScenePaggingParams();
+
+</script>

+ 40 - 0
src/view/newFireCase/mix3dManager/list.vue

@@ -0,0 +1,40 @@
+<template>
+  <com-head :options="listOptions" v-model="params.pagging.state.query.searchType">
+    <el-form label-width="84px" inline>
+      <slot name="header" />
+      <el-form-item class="searh-btns" style="grid-area: 1 / 4 / 2 / 4">
+        <el-button type="primary" @click="params.pagging.refresh">查询</el-button>
+        <el-button type="primary" plain @click="params.pagging.queryReset"
+          >重置</el-button
+        >
+      </el-form-item>
+    </el-form>
+  </com-head>
+
+  <div class="body-layer">
+    <slot name="content" />
+    <com-pagination
+      @size-change="params.pagging.changPageSize"
+      @current-change="params.pagging.changPageCurrent"
+      :current-page="params.pagging.state.pag.currentPage"
+      :page-size="params.pagging.state.pag.size"
+      :total="params.pagging.state.pag.total"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import comHead from "@/components/head/index.vue";
+import comPagination from "@/components/pagination/index.vue";
+import { SceneType } from "@/store/scene";
+import { SceneTypeDesc } from "@/constant/scene";
+import { useScenePaggingParams } from "./pagging";
+
+defineProps<{ params: ReturnType<typeof useScenePaggingParams> }>();
+
+const listOptions = [
+  { name: "多元融合列表", value: "0" },
+  { name: "多元融合共享", value: "1" },
+  { name: "全部", value: "2" },
+];
+</script>

+ 31 - 0
src/view/newFireCase/mix3dManager/pagging.ts

@@ -0,0 +1,31 @@
+import { usePagging } from "@/hook/pagging";
+import { SceneType, getScenePagging } from "@/store/scene";
+import { computed, reactive, watch, watchEffect } from "vue";
+
+export const useScenePaggingParams = () => {
+  const pagging = usePagging({
+    get: getScenePagging,
+    paramsTemlate: {
+      type: SceneType.SWKK, // 这里先填默认值,到时更改接口后再换成空值
+      searchType: "0",
+      modelTitle: "",
+    },
+  });
+
+  watch(
+    () => pagging.state.query.searchType,
+    () => {
+      pagging.state.pag.currentPage = 1;
+    }
+  );
+
+  const queryResetRaw = pagging.queryReset;
+  pagging.queryReset = () => {
+    const type = pagging.state.query.searchType;
+    queryResetRaw();
+    pagging.state.query.searchType = type;
+  };
+
+  return reactive({ pagging });
+};
+export type ScenePagging = ReturnType<typeof useScenePaggingParams>["pagging"];

+ 28 - 0
src/view/newFireCase/mix3dManager/quisk.ts

@@ -0,0 +1,28 @@
+import { QuoteScene, SceneType } from "@/store/scene";
+import EditModel from "./editModel.vue";
+import SceneDownload from "./sceneDownload.vue";
+import { quiskMountFactory } from "@/helper/mount";
+import { axios, checkHasDownload } from "@/request";
+
+export const editModelScene = quiskMountFactory(EditModel, {
+  title: "编辑模型",
+  width: 500,
+});
+
+export type SceneDpwnloadProps = { scene: QuoteScene };
+export const sceneDownload = async(props: SceneDpwnloadProps) => {
+  const params = {
+    num: props.scene.num,
+    isObj: Number(![SceneType.SWSS, SceneType.SWYDSS].includes(props.scene.type)),
+  };
+  const res = await axios.get(checkHasDownload, { params });
+  const hideFloor = Number(res.data.downloadStatus) !== 3
+
+  const sceneDownloadDialog = quiskMountFactory(SceneDownload, {
+    title: "场景离线包下载",
+    width: 500,
+    hideFloor: hideFloor,
+    enterText: '下 载'
+  });
+  return await sceneDownloadDialog(props);
+}

+ 83 - 0
src/view/newFireCase/mix3dManager/sceneContent.vue

@@ -0,0 +1,83 @@
+<template>
+  <div class="body-head" style="display: flex;justify-content: flex-end;">
+    <el-button type="primary" @click="addMix3d">新增多元融合</el-button>
+  </div>
+
+  <el-table
+    :data="pagging.state.table.rows"
+    tooltip-effect="dark"
+    style="width: 100%"
+    size="large"
+  >
+    <el-table-column label="场景标题" prop="name">
+      <template v-slot:default="{ row }">
+        <span @click="openSceneUrl(row, OpenType.query)">{{ row.name }}</span>
+      </template>
+    </el-table-column>
+
+    <el-table-column label="所属架构" prop="deptName"></el-table-column>
+    <!-- 创建人需要获取新值 -->
+    <el-table-column label="创建人" v-slot:default="{ row }: { row: QuoteScene }">
+      {{ QuoteSceneStatusDesc[row.status] }}
+    </el-table-column>
+    <el-table-column label="拍摄时间" prop="createTime" v-slot:default="{ row }">
+      {{ row.createTime.substr(0, 16) }}
+    </el-table-column>
+    <el-table-column
+      label="操作"
+      v-slot:default="{ row }: { row: QuoteScene }"
+      width="400px"
+    >
+      <span class="oper-span" v-pdpath="['edit']" @click="openSceneUrl(row, OpenType.edit)" v-if="row.status === QuoteSceneStatus.SUCCESS">
+        编辑
+      </span>
+      <span v-if="![QuoteSceneStatus.RUN, QuoteSceneStatus.QUEUE].includes(row.status) && ![SceneType.SWSSMX, SceneType.SWYDMX].includes(row.type)"
+        class="oper-span delBtn"
+        @click="delSceneHandler(row)"
+        v-pdpath="'del'"
+      >
+        删除
+      </span>
+    </el-table-column>
+  </el-table>
+</template>
+
+<script setup lang="ts">
+import {
+  QuoteScene,
+  QuoteSceneStatus,
+  delQuoteScene,
+  SceneType,
+  genMeshScene,
+  LocationEnum,
+  copyQuoteScene,
+  downQuoteSceneHash,
+} from "@/store/scene";
+import { ScenePagging } from "./pagging";
+import { QuoteSceneStatusDesc } from "@/constant/scene";
+import { OpenType, openSceneUrl } from "../../case/help";
+import { confirm } from "@/helper/message";
+import { sceneDownload } from "./quisk";
+import { downSceneHash } from "@/request";
+
+const props = defineProps<{ pagging: ScenePagging }>();
+const delSceneHandler = async (scene: QuoteScene) => {
+  if (await confirm("确定要删除当前场景吗?")) {
+    await delQuoteScene(scene);
+    props.pagging.refresh();
+  }
+};
+const copySceneHandler = async (scene: QuoteScene) => {
+  await copyQuoteScene(scene);
+  props.pagging.refresh();
+};
+const downHash = async (scene: QuoteScene) => {
+  downQuoteSceneHash(scene);
+};
+const sceneDownloadHandler = (scene: QuoteScene) => {
+  sceneDownload({ scene });
+};
+const addMix3d = () => {
+  console.log(1111)
+};
+</script>

+ 139 - 0
src/view/newFireCase/mix3dManager/sceneDownload.vue

@@ -0,0 +1,139 @@
+<template>
+  <!-- hideFloor: state === State.package -->
+  <div>
+    <div class="title">
+      {{ stateTitle[state] }}
+    </div>
+
+    <div v-if="state === State.package">
+      <div class="text" style="display: flex; justify-content: space-between; margin-top: 15px">
+        <span>{{ filename }}</span>
+        <span>{{ percent }}%</span>
+      </div>
+      <div style="pointer-events: none">
+        <el-slider v-model="percent" :show-tooltip="false" />
+      </div>
+    </div>
+    <div v-else-if="state === State.readDown">
+      <span>正在下载中……</span>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, onUnmounted, ref } from "vue";
+import saveAs from "@/util/file-serve";
+import { checkHasDownload, getDownloadProcess, downloadScene, axios } from "@/request";
+import { ElLoading, ElMessage } from "element-plus";
+import { QuoteScene, SceneType } from "@/store/scene";
+import { QuiskExpose } from "@/helper/mount";
+
+const props = defineProps<{ scene: QuoteScene }>();
+enum State {
+  uncreate,
+  package,
+  readDown,
+}
+const getState = (type: number) => {
+  const stateTypes = [
+    { codes: [0, 2], state: State.uncreate },
+    { codes: [1], state: State.package },
+    { codes: [3], state: State.readDown },
+  ];
+  return (
+    stateTypes.find((stateType) => stateType.codes.includes(type))?.state ||
+    State.uncreate
+  );
+};
+
+const state = ref<State>(State.uncreate);
+const count = ref<number>(0);
+const filename = ref<string>(props.scene.title + ".zip");
+const downloadURL = ref<string>();
+const percent = ref(0);
+
+const stateTitle = {
+  [State.uncreate]: "下载场景离线数据包,可在本地运行查看。",
+  [State.package]: "正在打包场景离线数据",
+  [State.readDown]: filename.value,
+};
+
+const params = {
+  num: props.scene.num,
+  isObj: Number(![SceneType.SWSS, SceneType.SWYDSS].includes(props.scene.type)),
+};
+// 初始化
+const initial = async () => {
+  const res = await axios.get(checkHasDownload, { params });
+  state.value = getState(res.data.downloadStatus);
+  count.value = res.data.count;
+  downloadURL.value = res.data.downloadUrl;
+
+  if (state.value === State.uncreate) {
+    const downRes = await axios.get(downloadScene, { params });
+    state.value = getState(downRes.data.downloadStatus);
+    // const unCountFlag =
+    //   count.value == 0 ||(await axios.get(downloadScene, { params })).data.downloadStatus !== 1;
+    if (state.value === State.uncreate) {
+      ElMessage.error("下载失败,请联系管理员");
+      throw "暂无剩余下载次数";
+    }
+  }
+
+  if (state.value === State.package) {
+    await new Promise<void>((resolve) => requestUpdateURL(resolve));
+  } else {
+    downloadURL.value = res.data.downloadUrl;
+  }
+};
+
+// 下载
+const download = () => {
+  if (!downloadURL.value) {
+    ElMessage.error("下载链接未生成,请稍等!");
+    throw "下载链接未生成,请稍等!";
+  } else {
+
+    if (String(downloadURL.value.includes('http'))) {
+      downloadURL.value = downloadURL.value;
+    } else {
+      if (!downloadURL.value.startsWith("/")) {
+        downloadURL.value = "/" + downloadURL.value;
+      }
+    }
+    console.error("downloadURL.value", downloadURL.value);
+    return saveAs(downloadURL.value, filename.value);
+  }
+};
+
+// 进度请求
+let timer: any;
+const requestUpdateURL = async (callback: () => void) => {
+  const res = await axios.get(getDownloadProcess, { params });
+
+  percent.value = parseInt(res.data.percent);
+  downloadURL.value = res.data.url;
+  if (downloadURL.value) {
+    state.value = State.readDown;
+    callback();
+  } else {
+    timer = setTimeout(() => requestUpdateURL(callback), 1000);
+  }
+};
+
+onUnmounted(() => clearTimeout(timer));
+
+defineExpose<QuiskExpose>({
+  submit: async () => {
+    await initial();
+    const loading = ElLoading.service({
+      lock: true,
+      text: "下载中",
+      background: "rgba(255, 255, 255, 0.4)",
+    });
+    await download();
+    loading.close();
+    ElMessage.success("下载完成");
+  },
+});
+</script>

+ 57 - 0
src/view/newFireCase/newFireDetails/components/Toolbar.vue

@@ -0,0 +1,57 @@
+<template>
+    <transition name="slide-up" mode="in-out" v-if="!disabledAnimation">
+        <div class="ui-editor-toolbar strengthen-left" v-if="props.showBottomBar">
+            <slot></slot>
+        </div>
+    </transition>
+
+    <div class="ui-editor-toolbar strengthen-left" v-else-if="props.showBottomBar">
+        <slot></slot>
+    </div>
+</template>
+<script setup lang="ts">
+import { watch } from 'vue'
+
+const props = defineProps<{ showBottomBar?: Boolean, disabledAnimation?: Boolean }>();
+watch(() => props.showBottomBar, (n, o) => {
+    console.log(n, o, 66666)
+})
+
+</script>
+<style lang="scss">
+.ui-editor-toolbar {
+    position: absolute;
+    bottom: 0;
+    right: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex: 1;
+    height:60px;
+    background-color: rgba(27,27,28,.8);
+    pointer-events: all;
+    left: 0;
+    z-index: 2;
+    backdrop-filter: blur(4px);
+    /* transition: all .3s ease; */
+}
+</style>
+<style lang="scss" scoped>
+.slide-up-enter-active,
+.slide-up-leave-active {
+    will-change: transform;
+    transition: all 0.2s ease-in-out;
+}
+.slide-up-enter-from {
+    opacity: 0;
+    transform: translate3d(0, 100%, 0);
+}
+.slide-up-enter {
+    opacity: 1;
+    transform: translate3d(0, -100%, 0);
+}
+.slide-up-leave-active {
+    opacity: 0;
+    transform: translate3d(0, 100%, 0);
+}
+</style>

+ 289 - 0
src/view/newFireCase/newFireDetails/components/basicInfo.vue

@@ -0,0 +1,289 @@
+<template>
+  <div class="basic-info">
+    <!-- 展示模式 -->
+    <div v-if="props.editOrShow === 'show'" class="camera-from show-view">
+      <div class="form-title">案件信息</div>
+      <div class="info-row"><span class="label">项目编号:</span><span class="value">{{ bindFire.projectSn || '-' }}</span></div>
+      <div class="info-row"><span class="label">起火对象:</span><span class="value">{{ bindFire.projectName || '-' }}</span></div>
+      <div class="info-row"><span class="label">详细地址:</span><span class="value">{{ bindFire.mapUrl || '-' }}</span></div>
+      <div class="info-row"><span class="label">起火地址:</span><span class="value">{{ bindFire.projectAddress || '-' }}</span></div>
+      <div class="info-row"><span class="label">起火场所:</span><span class="value">{{ bindFire.projectSite || '-' }}</span></div>
+      <div class="info-row"><span class="label">承办单位:</span><span class="value">{{ bindFire.organizerDeptName || '-' }}</span></div>
+      <div class="info-row"><span class="label">承办人员:</span><span class="value">{{ bindFire.organizerUsers || '-' }}</span></div>
+      <div class="info-row"><span class="label">事故日期:</span><span class="value">{{ bindFire.accidentDate || '-' }}</span></div>
+      <div class="info-row"><span class="label">火灾原因:</span><span class="value">{{ bindFire.fireReason || '-' }}</span></div>
+    </div>
+
+    <!-- 编辑模式 -->
+    <el-form v-else ref="form" label-width="84px" class="camera-from">
+      <div class="form-title">案件信息</div>
+      <el-form-item label="项目编号" class="mandatory">
+        <el-input
+          v-model="bindFire.projectSn"
+          maxlength="18"
+          placeholder="请输入项目编号"
+        />
+      </el-form-item>
+      <el-form-item label="起火对象" class="mandatory">
+        <el-input
+          v-model="bindFire.projectName"
+          maxlength="50"
+          placeholder="请输入起火对象"
+        />
+      </el-form-item>
+      <el-form-item label="详细地址" class="mandatory">
+        <el-input
+          v-model="bindFire.mapUrl"
+          placeholder="输入名称搜索"
+          clearable
+          readonly
+          class="mandatory"
+        >
+          <template #append>
+            <el-button :icon="Search" @click="searchAMapAddress" />
+          </template>
+        </el-input>
+      </el-form-item>
+
+      <el-form-item label="起火地址" class="mandatory">
+        <el-input
+          v-model="bindFire.projectAddress"
+          maxlength="50"
+          placeholder="请输入起火地址"
+        />
+      </el-form-item>
+      <el-form-item label="起火场所" class="mandatory">
+        <el-cascader
+          style="width: 100%"
+          v-model="projectSite"
+          placeholder="起火场所"
+          :options="place"
+          :props="{ expandTrigger: 'hover' }"
+        />
+      </el-form-item>
+      <el-form-item label="承办单位" class="mandatory">
+        <companySelect v-model="bindFire.deptId" hideAll :notUpdate="true" disabled />
+      </el-form-item>
+      <el-form-item label="承办人员" class="mandatory" placeholder="请输入承办人员">
+        <el-input v-model="bindFire.organizerUsers" maxlength="50" />
+      </el-form-item>
+      <el-form-item label="事故日期" class="mandatory" placeholder="请选择事故日期">
+        <el-date-picker
+          type="date"
+          v-model="accidentDate"
+          style="width: 100%"
+          :disabled-date="(date) => date.getTime() > new Date().getTime()"
+        />
+      </el-form-item>
+      <el-form-item label="火灾原因" class="mandatory">
+        <el-cascader
+          style="width: 100%"
+          v-model="fireReason"
+          placeholder="火灾原因:"
+          :options="reason"
+          :props="{ expandTrigger: 'hover' }"
+        />
+      </el-form-item>
+    </el-form>
+  </div>
+  <creatMap v-model="showMapDialog" :caseId="caseId" @confirm="handleMapConfirm" />
+</template>
+
+<script setup lang="ts">
+import companySelect from "@/components/company-select/index.vue";
+import { ref, watch, toRef } from "vue";
+import { Fire, setFire, addFire } from "@/app/fire/store/fire";
+import { reason, place } from "@/app/fire/constant/fire";
+import { ElMessage } from "element-plus";
+import { dateFormat, debounce } from "@/util";
+import { genCascaderValue, getCode } from "@/helper/cascader";
+import { QuiskExpose } from "@/helper/mount";
+import { user } from "@/store/user";
+import { Search } from "@element-plus/icons-vue";
+import { selectMapImage } from "@/view/case/quisk";
+import creatMap from "./creatMap.vue";
+
+const props = defineProps<{ fire?: Fire, caseId?: number, editOrShow?: string }>();
+const caseId = toRef(props, 'caseId');
+
+let bindFire = ref<Fire>( props.fire ? { ...props.fire } : ({ deptId: user.value.info.deptId} as Fire));
+const accidentDate = ref(
+  bindFire.value.accidentDate ? new Date(bindFire.value.accidentDate) : new Date()
+);
+watch(() => props.fire, (newVal, oldVal) => {
+  bindFire.value = newVal ? { ...newVal } : ({ deptId: user.value.info.deptId} as Fire)
+  accidentDate.value = bindFire.value.accidentDate ? new Date(bindFire.value.accidentDate) : new Date()
+})
+const showMapDialog = ref(false)
+const fireReason = genCascaderValue(bindFire, "fireReason");
+const projectSite = genCascaderValue(bindFire, "projectSite");
+
+// ========== 自动保存(有变化就保存,无需按钮) ==========
+// 仅在数据满足基本必填项时才触发保存;使用防抖避免频繁请求
+const isValidForAutoSave = () => {
+  const v = bindFire.value;
+  // 自动保存不弹错误,只在必填项齐备时保存
+  return (
+    !!v.latAndLong && !!v.projectAddress && !!v.projectSn && !!v.projectName &&
+    !!v.projectSite && !!v.deptId && !!v.organizerUsers && !!fireReason.value && !!accidentDate.value
+  );
+};
+
+// 用于避免重复保存同一状态
+let lastSavedSnapshot = "";
+const getSnapshot = () => {
+  const v = { ...bindFire.value } as any;
+  v.accidentDate = dateFormat(accidentDate.value, "yyyy-MM-dd");
+  v.projectSiteCode = getCode(place, bindFire.value.projectSite);
+  return JSON.stringify({
+    projectSn: v.projectSn,
+    projectName: v.projectName,
+    mapUrl: v.mapUrl,
+    projectAddress: v.projectAddress,
+    latAndLong: v.latAndLong,
+    deptId: v.deptId,
+    organizerUsers: v.organizerUsers,
+    projectSite: v.projectSite,
+    projectSiteCode: v.projectSiteCode,
+    fireReason: v.fireReason,
+    accidentDate: v.accidentDate,
+    caseId: v.caseId,
+    id: v.id,
+  });
+};
+
+const autoSave = async () => {
+  if (!isValidForAutoSave()) return;
+  const snapshot = getSnapshot();
+  if (snapshot === lastSavedSnapshot) return;
+
+  // 写入派生字段
+  bindFire.value.accidentDate = dateFormat(accidentDate.value, "yyyy-MM-dd");
+  bindFire.value.projectSiteCode = getCode(place, bindFire.value.projectSite);
+
+  try {
+    if (bindFire.value.id) {
+      await setFire(bindFire.value);
+    } else {
+      await addFire(bindFire.value as any);
+    }
+    lastSavedSnapshot = snapshot;
+    // 自动保存成功后不刷新页面,也不打扰用户
+  } catch (e) {
+    // 自动保存失败不打断填写,可在控制台查看错误
+    console.error("auto-save error", e);
+  }
+};
+
+// 防抖触发:用户停顿一会儿再保存
+const triggerAutoSave = debounce(() => autoSave(), 800);
+
+// 深度监听表单对象;日期单独监听
+watch(bindFire, () => triggerAutoSave(), { deep: true });
+watch(accidentDate, () => triggerAutoSave());
+
+defineExpose<QuiskExpose>({
+  async submit() {
+    if (!bindFire.value.latAndLong || !bindFire.value.latAndLong.trim()) {
+      ElMessage.error("详细地址不能为空");
+      throw "详细地址不能为空!";
+    } else if (!bindFire.value.projectAddress || !bindFire.value.projectAddress.trim()) {
+      ElMessage.error("起火地址不能为空!");
+      throw "起火地址不能为空!";
+    } else if (!bindFire.value.projectSn || !bindFire.value.projectSn.trim()) {
+      ElMessage.error("项目编号不能为空!");
+      throw "项目编号不能为空!";
+    } else if (!bindFire.value.projectName || !bindFire.value.projectName.trim()) {
+      ElMessage.error("起火对象不能为空!");
+      throw "起火对象不能为空!";
+    } else if (!bindFire.value.projectSite || !bindFire.value.projectSite.trim()) {
+      ElMessage.error("起火场所不能为空!");
+      throw "起火场所不能为空!";
+    } else if (!bindFire.value.deptId || !bindFire.value.deptId.trim()) {
+      ElMessage.error("承办单位不能为空!");
+      throw "承办单位不能为空!";
+    } else if (!bindFire.value.organizerUsers || !bindFire.value.organizerUsers.trim()) {
+      ElMessage.error("承办人员不能为空!");
+      throw "承办人员不能为空!";
+    } else if (!accidentDate) {
+      ElMessage.error("事故日期不能为空!");
+      throw "事故日期不能为空!";
+    } else if (!bindFire.value.fireReason || !bindFire.value.fireReason.trim()) {
+      ElMessage.error("火灾原因不能为空!");
+      throw "火灾原因不能为空!";
+    }
+
+    bindFire.value.accidentDate = dateFormat(accidentDate.value, "yyyy-MM-dd");
+    bindFire.value.projectSiteCode = getCode(place, bindFire.value.projectSite);
+    
+    // 保存数据
+    if (bindFire.value.id) {
+      await setFire(bindFire.value);
+    } else {
+      await addFire(bindFire.value as any);
+    }
+    
+    // 保存成功后,刷新fireDetails页面的数据
+    // 通过事件总线或全局事件触发刷新
+    window.location.reload()
+  },
+});
+
+// 打开地图弹窗
+const searchAMapAddress = async () => {
+  showMapDialog.value = true
+};
+// 处理地图确认选择
+const handleMapConfirm = (LocationInfo: any) => {
+  // console.log(LocationInfo, 666)
+  const {cityname, adname, address, name, location} = LocationInfo;
+  bindFire.value.mapUrl = cityname + adname + address + name;
+  bindFire.value.latlng = bindFire.value.latAndLong = `${location.lat},${location.lng}`;
+  showMapDialog.value = false;
+}
+</script>
+
+<style scoped lang="scss">
+.basic-info{
+  width: 100%;
+  height: calc(100% - 200px);
+  background: #f5f7fa;
+  padding: 24px 0;
+}
+.camera-from {
+  width: 60%;
+  height: calc(100% - 140px);
+  background: #FFFFFF;
+  margin: 0 auto;
+  padding: 48px 100px 48px;
+  overflow: auto;
+  .form-title{
+    text-align: center;
+    font-family: Microsoft YaHei, Microsoft YaHei;
+    font-weight: 400;
+    font-size: 24px;
+    color: rgba(0,0,0,0.85);
+    line-height: 36px;
+    margin-bottom: 44px;
+  }
+}
+</style>
+
+<style scoped lang="scss">
+.show-view {
+  .info-row {
+    display: flex;
+    align-items: center;
+    margin-bottom: 16px;
+    .label {
+      width: 84px;
+      color: rgba(0, 0, 0, 0.85);
+    }
+    .value {
+      flex: 1;
+      text-align: left;
+      color: rgba(0, 0, 0, 0.85);
+    }
+  }
+}
+</style>

+ 688 - 0
src/view/newFireCase/newFireDetails/components/creatMap.vue

@@ -0,0 +1,688 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    title="选择地图位置"
+    width="1200px"
+    :before-close="handleClose"
+    destroy-on-close
+    class="map-fire-dialog"
+  >
+    <div class="map-dialog-content">
+      <!-- 搜索框 -->
+      <div id="panel" class="scrollbar1">
+            <div id="searchBar">
+                <el-select class="search-select" v-model="selectedSearchAdress" placeholder="地址">
+                  <el-option label="地址" value="1"></el-option>
+                  <el-option label="经纬度" value="2"></el-option>
+                </el-select>
+                <input class="search-input-latlng" v-if="selectedSearchAdress === '2'" @input="handleSearch" autocomplete="off" @keydown="handleKeyDown" placeholder="输入经纬度搜索,如23.117661,113.281272" />
+                <input class="search-input-address" v-else id="searchInput" @input="handleSearch" autocomplete="off" placeholder="输入地址搜索" />
+                <!-- <input id="searchInput" @input="handleSearch" autocomplete="off" @keydown="handleKeyDown" :placeholder="selectedSearchAdress === '1' ? '输入地址' : '输入经纬度'" /> -->
+            </div>
+            <div id="searchResults">暂无数据</div>
+        </div>
+
+      <!-- 地图容器 -->
+      <div class="map-container" v-if="visible">
+        <div id="container" class="map" style="width: 800px; height: 600px" tabindex="0"></div>
+      </div>
+    </div>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="handleClose">取消</el-button>
+        <el-button type="primary" @click="handleConfirm">
+          确定
+        </el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, watch, nextTick, onUnmounted, onMounted } from 'vue'
+import { ElDialog, ElInput, ElButton, ElDescriptions, ElDescriptionsItem, ElIcon, ElMessage } from 'element-plus'
+import { Search } from '@element-plus/icons-vue'
+import AMapLoader from '@amap/amap-jsapi-loader'
+import html2canvas from 'html2canvas'
+import { uploadFileToServer, canvasToBlob, blobToFile } from '@/util/upload-utils'
+import { axios } from '@/request'
+import { addOrUpdateCaseTabulation } from '@/request/urls'
+import { user } from "@/store/user";
+
+// 添加高德地图类型声明
+declare const AMap: any
+declare const AMapUI: any
+
+// 定义组件props和emits
+interface Props {
+  modelValue: boolean
+  caseId?: string | number
+}
+
+interface LocationInfo {
+  name?: string
+  address?: string
+  lat?: number
+  lng?: number
+  adcode?: string
+  citycode?: string
+  district?: string
+  screenshotUrl?: string
+  screenshotFileName?: string
+}
+
+const props = defineProps<Props>()
+const mapInput = ref('')
+const emit = defineEmits<{
+  'update:modelValue': [value: boolean]
+  'confirm': [location: any]
+}>()
+
+// 响应式变量
+const visible = ref(false)
+const keyword = ref('')
+const selectedLocation = ref<LocationInfo | null>(null)
+const mapContainer = ref<HTMLDivElement>()
+
+// 高德地图相关变量
+let map: any = null
+let placeSearch: any = null
+let geocoder: any = null
+let currentMarker: any = null
+let poiPicker: any = null
+
+// 高德地图配置
+const AMAP_KEY = '2ae5a7713612a8d5a65cfd54c989c969'
+const selectedSearchAdress = ref('1')
+const searchInputValue = ref('')
+// 根基app打开不同地址
+const appId = import.meta.env.VITE_APP_APP || 'fire'
+const url = import.meta.env.VITE_DRAW_URL || 'http://mix3d.4dkankan.com'
+// 防抖定时器
+let searchDebounceTimer: number | null = null
+// 监听visible变化
+watch(() => props.modelValue, (newVal) => {
+  visible.value = newVal
+  if (newVal) {
+    nextTick(() => {
+      initMap()
+    })
+  }
+}, { immediate: true })
+
+watch(visible, (newVal) => {
+  selectedSearchAdress.value = '1'
+  emit('update:modelValue', newVal)
+})
+
+// 监听搜索类型变化,切换时清空输入框和重置搜索
+watch(selectedSearchAdress, (newVal, oldVal) => {
+  if (newVal !== oldVal) {
+    // 清空输入框
+    searchInputValue.value = ''
+    const searchInput = document.getElementById('searchInput') as HTMLInputElement
+    if (searchInput) {
+      searchInput.value = ''
+    }
+    
+    // 重置搜索结果
+    const searchResults = document.getElementById('searchResults')
+    if (searchResults) {
+      searchResults.innerHTML = '暂无数据'
+    }
+    
+    // 清空地图标记
+    if (map) {
+      map.clearMap()
+    }
+    
+    // 根据搜索类型调整POI选择器的显示
+    if (poiPicker) {
+      if (newVal === '2') {
+        // 经纬度搜索模式,隐藏选择列表
+        poiPicker.hideSearchResults()
+      } else {
+        // 地址搜索模式,可以显示选择列表
+        poiPicker.searchByKeyword('')
+      }
+    }
+  }
+})
+// 初始化地图
+const initMap = async () => {
+    map = new AMap.Map('container', {
+        zoom: 11,
+        key: AMAP_KEY, // 替换为你的API密钥
+        center: [116.397513, 39.908739],
+        WebGLParams: {
+            preserveDrawingBuffer: true
+        }
+    });
+
+    AMapUI.loadUI(['misc/PoiPicker'], function(PoiPicker) {
+
+    poiPicker = new PoiPicker({
+        input: 'searchInput',
+        placeSearchOptions: {
+            map: map,
+            pageSize: 7,
+            pageIndex: 1,
+        },
+        searchResultsContainer: 'searchResults'
+    });
+    poiPicker.on('poiPicked', function(poiResult) {
+        selectedLocation.value = poiResult.item;//选中的信息
+        poiPicker.hideSearchResults();
+        map.clearMap();
+        var source = poiResult.source, poi = poiResult.item;
+        if (source !== 'search') {
+            //suggest来源的,同样调用搜索
+            poiPicker.searchByKeyword(poi.name);
+
+        } else {
+          // 舒琪说不需要marker
+            // addMarker(poi.location.lng, poi.location.lat)
+            // console.log(poi);
+        }
+    });
+
+    poiPicker.onCityReady(function() {
+        poiPicker.searchByKeyword('');
+    });
+});
+
+}
+const handleKeyDown = (e: KeyboardEvent) => {
+  if (e.key === 'Enter') {
+    if(selectedSearchAdress.value === '2'){
+      console.log(11111)
+      e.preventDefault()
+      e.stopPropagation()
+      return
+    }
+  }
+}
+// 验证经纬度格式的函数
+const validateCoordinates = (input: string): { isValid: boolean; lat?: number; lng?: number } => {
+  // 移除空格
+  const trimmed = input.trim()
+  
+  // 检查是否包含逗号
+  if (!trimmed.includes(',')) {
+    return { isValid: false }
+  }
+  
+  // 分割经纬度
+  const parts = trimmed.split(',')
+  if (parts.length !== 2) {
+    return { isValid: false }
+  }
+  
+  // 转换为数字并验证
+  const lat = parseFloat(parts[0].trim())
+  const lng = parseFloat(parts[1].trim())
+  
+  // 验证是否为有效数字
+  if (isNaN(lat) || isNaN(lng)) {
+    return { isValid: false }
+  }
+  
+  // 验证纬度范围 (-90 到 90)
+  if (lat < -90 || lat > 90) {
+    return { isValid: false }
+  }
+  
+  // 验证经度范围 (-180 到 180)
+  if (lng < -180 || lng > 180) {
+    return { isValid: false }
+  }
+  
+  return { isValid: true, lat, lng }
+}
+
+// 根据经纬度定位地图
+const locateByCoordinates = (lat: number, lng: number) => {
+  if (!map) {
+    ElMessage.error('地图未初始化')
+    return
+  }
+  
+  try {
+    // 创建经纬度点
+    const lngLat = new AMap.LngLat(lng, lat)
+    
+    // 设置地图中心点并调整缩放级别
+    map.setZoomAndCenter(15, lngLat)
+    
+    // 清除之前的标记
+    map.clearMap()
+    
+    // 添加标记
+    const marker = new AMap.Marker({
+      position: lngLat,
+      content: '<div class="amap_lib_placeSearch_poi"></div>',
+      offset: new AMap.Pixel(-10, -31)
+    })
+    map.add(marker)
+    
+    // 清空搜索结果显示
+    const searchResults = document.getElementById('searchResults')
+    if (searchResults) {
+      searchResults.innerHTML = `
+        <div style="padding: 16px; text-align: center; color: #606266;width:345px;height: 106px;border: 1px solid #D9D9D9;">
+          <div style="font-weight: 500; margin-bottom: 8px;color:#67C23A;">经纬度定位成功</div>
+          <div style="font-size: 14px;color: #A7A7A7;">
+            纬度: ${lat}<br/>
+            经度: ${lng}
+          </div>
+        </div>
+      `
+    }
+    
+    ElMessage.success('经纬度定位成功')
+  } catch (error) {
+    console.error('经纬度定位失败:', error)
+    ElMessage.error('经纬度定位失败')
+  }
+}
+
+// 清除防抖定时器
+const clearSearchDebounce = () => {
+  if (searchDebounceTimer) {
+    clearTimeout(searchDebounceTimer)
+    searchDebounceTimer = null
+  }
+}
+
+// 防抖搜索函数
+const debouncedSearch = (inputValue: string, searchType: string) => {
+  clearSearchDebounce()
+  
+  searchDebounceTimer = setTimeout(() => {
+    if (searchType === '2') {
+      // 经纬度搜索
+      if (!inputValue.trim()) {
+        // 输入为空时,清空搜索结果
+        const searchResults = document.getElementById('searchResults')
+        if (searchResults) {
+          searchResults.innerHTML = '暂无数据'
+        }
+        return
+      }
+      
+      const validation = validateCoordinates(inputValue)
+      
+      if (validation.isValid && validation.lat !== undefined && validation.lng !== undefined) {
+        // 经纬度格式正确,进行定位
+        locateByCoordinates(validation.lat, validation.lng)
+      } else {
+        // 经纬度格式错误,只在搜索结果区域显示提示,不弹窗
+        const searchResults = document.getElementById('searchResults')
+        if (searchResults) {
+          searchResults.innerHTML = `
+            <div style="padding: 16px; text-align: center; width:345px;height: 106px;border: 1px solid #D9D9D9;">
+              <div style="font-weight: 600; margin-bottom: 8px;color: #B3261E;">经纬度格式错误</div>
+              <div style="font-size: 12px; line-height: 1.5;color:#A7A7A7;">
+                请输入正确的经纬度格式:<br/>
+                纬度,经度(例如:23.11766,113.28122)<br/>
+                纬度范围:-90 到 90<br/>
+                经度范围:-180 到 180
+              </div>
+            </div>
+          `
+        }
+      }
+    } else {
+      console.log(22222)
+      // 地址搜索,保持原有逻辑
+      if (poiPicker) {
+        poiPicker.placeSearch.opt.pageIndex = 1;        
+        poiPicker.searchByKeyword(inputValue)
+      }
+    }
+  }, 500) // 500ms防抖延迟
+}
+
+const handleSearch = (e) => {
+    console.log('handleSearch', e.target.value)
+    const inputValue = e.target.value
+    searchInputValue.value = inputValue
+    
+    // 对于经纬度搜索,强制隐藏POI选择列表
+    if (selectedSearchAdress.value === '2' && poiPicker) {
+      poiPicker.hideSearchResults()
+    }
+    
+    // 使用防抖搜索
+    debouncedSearch(inputValue, selectedSearchAdress.value)
+}
+
+// 添加标记
+const addMarker = (lng: number, lat: number) => {
+
+  // 添加新标记
+  var marker = new AMap.Marker({
+    position: new AMap.LngLat(lng, lat), 
+    content: '<div class="amap_lib_placeSearch_poi"></div>',
+    offset: new AMap.Pixel(-10, -31)
+  });
+  map.add(marker)
+}
+
+
+// 处理弹窗关闭
+const handleClose = () => {
+  visible.value = false
+  selectedLocation.value = null
+  keyword.value = ''
+  
+  // 清理地图
+  if (map) {
+    map.destroy()
+    map = null
+  }
+}
+
+const getMapSize = () => {
+  if (!map) {
+    ElMessage.error('地图未初始化')
+    return
+  }
+
+  try {
+    // 获取当前地图的可视区域边界
+    const bounds = map.getBounds()
+    const southwest = bounds.getSouthWest() // 西南角
+    const northeast = bounds.getNorthEast() // 东北角
+    const northwest = new (window as any).AMap.LngLat(southwest.lng, northeast.lat) // 西北角
+    const southeast = new (window as any).AMap.LngLat(northeast.lng, southwest.lat) // 东南角
+    
+    // 使用高德地图的几何工具计算距离(单位:米)
+    const AMap = (window as any).AMap
+    
+    // 计算宽度(东西方向距离)
+    const width = AMap.GeometryUtil.distance(southwest, southeast)
+    
+    // 计算高度(南北方向距离)
+    const height = AMap.GeometryUtil.distance(southwest, northwest)
+    
+    // 计算面积(平方米)
+    const area = width * height
+    
+    // 获取当前缩放级别
+    const zoom = map.getZoom()
+    
+    // 获取地图中心点
+    const center = map.getCenter()
+    
+    const viewportInfo = {
+      width: width, // 宽度(米)
+      height: height, // 高度(米)
+      area: Math.round(area), // 面积(平方米)
+      zoom: zoom, // 缩放级别
+      center: {
+        lat: center.lat,
+        lng: center.lng
+      },
+      bounds: {
+        southwest: { lat: southwest.lat, lng: southwest.lng },
+        northeast: { lat: northeast.lat, lng: northeast.lng }
+      }
+    }
+    
+    console.log('当前可视区域信息:', viewportInfo)
+    
+    // ElMessage.success(`
+    //   可视区域信息:
+    //   宽度:${viewportInfo.width.toLocaleString()} 米
+    //   高度:${viewportInfo.height.toLocaleString()} 米
+    //   面积:${viewportInfo.area.toLocaleString()} 平方米
+    //   缩放级别:${viewportInfo.zoom.toFixed(2)}
+    // `)
+    return {
+      width: viewportInfo.width,
+      height: viewportInfo.height,
+    }
+  } catch (error) {
+    console.error('计算可视区域失败:', error)
+    ElMessage.error('计算可视区域失败')
+  }
+}
+
+const getCanvasImage = async (): Promise<{url: string, fileName: string} | null> => {
+  try {
+    // 使用html2canvas截图高德地图可视化区域
+    const mapElement = document.getElementById('container')
+    if (!mapElement) {
+      ElMessage.error('地图容器未找到')
+      return null
+    }
+
+    // ElMessage.info('正在生成地图截图...')
+    
+    // 配置html2canvas选项
+    const canvas = await html2canvas(mapElement, {
+      useCORS: true,
+      allowTaint: true,
+      scale: 1,
+      logging: false,
+      imageTimeout: 15000,
+      // 忽略某些元素(比如控件)
+      ignoreElements: (element) => {
+        return element.classList.contains('amap-logo') || 
+               element.classList.contains('amap-copyright') ||
+               element.classList.contains('amap-controls')
+      }
+    })
+
+    // 将canvas转换为Blob
+    const blob = await canvasToBlob(canvas, 0.8, 'image/jpeg')
+    
+    // 生成文件名
+    const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
+    const fileName = `map-screenshot-${timestamp}.jpg`
+    
+    // 转换为File对象
+    const file = blobToFile(blob, fileName)
+    
+    // 上传文件
+    const uploadResult = await uploadFileToServer(file, (progressEvent) => {
+      // 可选:显示上传进度
+      if (progressEvent.lengthComputable) {
+        const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total)
+        console.log(`上传进度: ${percentCompleted}%`)
+      }
+    })
+
+    console.log('上传成功:', uploadResult)
+    return uploadResult
+  } catch (error) {
+    console.error('截图或上传失败:', error)
+    ElMessage.error('截图或上传失败,请重试')
+    return null
+  }
+}
+// 处理确认选择
+const handleConfirm = async () => {
+  try {
+    emit('confirm', selectedLocation.value)
+    handleClose()
+  } catch (error) {
+    console.error('保存方位图失败:', error)
+    // ElMessage.error('保存方位图失败,请重试')
+  }
+}
+
+// 组件卸载时清理
+onUnmounted(() => {
+  if (map) {
+    map.destroy()
+  }
+  // 清除防抖定时器
+  clearSearchDebounce()
+})
+</script>
+
+<style lang="scss">
+.map-fire-dialog{
+    .el-dialog__header{
+        text-align: left;
+        border-bottom: 1px solid #dcdfe6;
+        margin-bottom: 16px;
+    }
+    // https://a.amap.com/jsapi/static/image/plugin/marker_red.png
+    .amap-marker{
+        display: none!important;
+    }
+    .amap-info-contentContainer{
+      display: none!important;
+    }
+}
+</style>
+<style scoped lang="scss">
+.map-dialog-content {
+  height: 600px;
+  display: flex;
+//   flex-direction: column;
+}
+#panel {
+    // position: absolute;
+    // top: 24px;
+    // left: 24px;
+    height: 100%;
+    overflow: auto;
+    width: 400px;
+    padding-right: 16px;
+    // z-index: 999;
+    background: #fff;
+    :deep(.amap-ui-poi-picker-sugg-container) {
+        display: none;
+    }
+    #searchBar{
+        display: flex;
+        width: 380px;
+        text-align: left;
+        margin-bottom: 16px;
+    }
+    .search-select{
+        width: 100px;
+        height: 40px!important;
+        
+        :deep(.el-select__wrapper){
+          border-radius: 4px 0 0 4px;
+          height: 40px;
+        }
+    }
+    .search-input-latlng{
+      width: 260px;
+      height: 38px;
+      border-radius: 0 4px 4px 0;
+      border: 1px solid #dcdfe6;
+      border-left: 0;
+      padding: 0 10px;
+      font-size: 14px;
+      color: #303133;
+      background: #fff;
+      outline: none;
+    }
+    :deep(#searchInput) {
+        width: 260px;
+        height: 38px;
+        border-radius: 0 4px 4px 0;
+        border: 1px solid #dcdfe6;
+        border-left: 0;
+        padding: 0 10px;
+        font-size: 14px;
+        color: #303133;
+        background: #fff;
+        outline: none;
+    }
+}
+#searchResults {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+    width: 100%;
+    height: 540px;
+    overflow: auto;
+}
+.search-section {
+  position: relative;
+  margin-bottom: 16px;
+  z-index: 1000;
+}
+
+.search-results {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  right: 0;
+  background: white;
+  border: 1px solid #dcdfe6;
+  border-top: none;
+  border-radius: 0 0 4px 4px;
+  max-height: 200px;
+  overflow-y: auto;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.search-item {
+  padding: 12px 16px;
+  cursor: pointer;
+  border-bottom: 1px solid #f0f0f0;
+  transition: background-color 0.2s;
+
+  &:hover {
+    background-color: #f5f7fa;
+  }
+
+  &:last-child {
+    border-bottom: none;
+  }
+}
+
+.item-name {
+  font-weight: 500;
+  color: #303133;
+  margin-bottom: 4px;
+}
+
+.item-address {
+  font-size: 12px;
+  color: #909399;
+}
+
+.map-container {
+  width: 800px;
+  height: 600px;
+  position: relative;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.amap-wrapper {
+  width: 100%;
+  height: 100%;
+}
+
+.location-info {
+  margin-top: 16px;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+}
+
+// 覆盖高德地图控件样式
+:deep(.amap-logo) {
+  display: none !important;
+}
+
+:deep(.amap-copyright) {
+  display: none !important;
+}
+</style>

+ 85 - 0
src/view/newFireCase/newFireDetails/components/headerTop.vue

@@ -0,0 +1,85 @@
+<template>
+  <div class="new-header">
+    <div class="left-title">
+      <span class="edit-title">编辑<span class="line">|</span></span><span>起火场所</span>
+    </div>
+    <div class="right-title" v-if="editOrShow === 'edit'">
+      <span class="change-btn" @click="openRenameDialog"><i class="iconfont icon-rename" /></span>
+      <el-button plain type="primary">预览</el-button>
+    </div>
+  </div>
+  <el-dialog
+    title="重命名"
+    v-model="renameVisible"
+    width="400px"
+    class="rename-dialog"
+  >
+    <div class="dialog-content">
+      <el-input v-model="renameTitle" placeholder="请输入新的标题" />
+    </div>
+    <template #footer>
+      <el-button plain type="primary" @click="renameVisible = false">取消</el-button>
+      <el-button type="primary" @click="renameVisible = false">确定</el-button>
+    </template>
+</el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from "vue";
+import { useRoute, useRouter } from 'vue-router';
+const props = defineProps<{
+  caseId: number;
+  currentRecord: object;
+  editOrShow: string;
+}>();
+const renameVisible = ref(false);
+const renameTitle = ref("");
+const openRenameDialog = () => {
+  renameVisible.value = true;
+  console.log(props.currentRecord)
+  renameTitle.value = props.currentRecord.caseTitle || "";
+}
+onMounted(() => {
+  
+});
+
+const emit = defineEmits<{
+  
+}>()
+
+</script>
+
+<style lang="scss" scoped>
+.new-header{
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 64px 48px 24px;
+  box-sizing: border-box;
+  .left-title{
+    font-size: 32px;
+    color: #303133;
+    .edit-title{
+      margin-right: 16px;
+    }
+    .line{
+      margin-left: 16px;
+      color: rgba(0,0,0,0.1);
+    }
+  }
+  .change-btn{
+    margin-right: 26px;
+    .icon-rename{
+      font-size: 24px;
+    }
+  }
+  .right-title{
+    font-size: 32px;
+  }
+}
+.rename-dialog{
+  .dialog-content{
+    margin: 24px;
+  }
+}
+</style>

File diff suppressed because it is too large
+ 295 - 0
src/view/newFireCase/newFireDetails/components/mix3d.vue


+ 261 - 0
src/view/newFireCase/newFireDetails/components/otherFiles.vue

@@ -0,0 +1,261 @@
+<template>
+  <div class="other-files">
+    <!-- 左侧:上传 + 列表 -->
+    <div class="left-panel">
+      <!-- 顶部拖拽上传区 -->
+      <el-upload
+        class="upload-drop"
+        v-if="editOrShow === 'edit'"
+        drag
+        :show-file-list="false"
+        :http-request="handleUploadRequest"
+        :before-upload="validateUpload"
+        :accept="accept"
+      >
+        <div class="upload-tip">
+          <el-icon class="upload-icon"><Upload /></el-icon>
+          <div class="tip-title">点击此区域文件上传</div>
+          <div class="tip-sub">支持100M以内的pdf、doc、docx、jpg、jpeg、png、raw、dcm文件</div>
+        </div>
+      </el-upload>
+
+      <!-- 文件列表 -->
+      <div class="file-list">
+        <div
+          class="file-item"
+          v-for="file in files"
+          :key="file.filesId"
+        >
+          <div class="file-left" @click="handleView(file)">
+            <div class="file-avatar">W</div>
+            <span class="file-name" :title="file.filesTitle">{{ file.filesTitle || '未命名' }}</span>
+          </div>
+          <div class="file-actions" v-if="editOrShow === 'edit'">
+            <el-tooltip content="更改" placement="top">
+              <el-button link @click.stop="handleView(file)">
+                <i class="iconfont icon-rename" />
+              </el-button>
+            </el-tooltip>
+            <el-tooltip content="下载" placement="top">
+              <el-button link @click.stop="handleDownload(file)">
+                <i class="iconfont icon-download" />
+              </el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button link @click.stop="handleDelete(file)">
+                <i class="iconfont icon-CloseCircle" />
+              </el-button>
+            </el-tooltip>
+          </div>
+        </div>
+        <div v-if="!files.length" class="empty-tip">暂无文件</div>
+      </div>
+    </div>
+  </div>
+  
+  <!-- 图片全屏预览(查看非右侧区域时用) -->
+  <!-- <el-image-viewer
+    v-if="showViewer"
+    :url-list="[previewUrl!]"
+    :initial-index="0"
+    @close="showViewer = false"
+  /> -->
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue';
+import { useRoute } from 'vue-router';
+import { ElMessage } from 'element-plus';
+import { Upload, View, Download, Close } from '@element-plus/icons-vue';
+import { useUpload } from '@/hook/upload';
+import { getCaseFileTypes, getCaseFiles, addCaseFile, delCaseFile, type CaseFile } from '@/store/caseFile';
+import { confirm } from '@/helper/message';
+
+const props = defineProps<{ caseId?: number; fire?: any, editOrShow?: string }>();
+const route = useRoute();
+const appId = import.meta.env.VITE_APP_APP || 'fire';
+
+// 案件ID
+const caseId = computed(() => props.caseId ?? Number(route.params.caseId));
+
+// 其他资料类型ID(通过类型名称检索,找不到时兜底为6)
+const otherTypeId = ref<number | null>(null);
+
+// 文件列表与预览
+const files = ref<CaseFile[]>([]);
+const previewUrl = ref<string>('');
+const showViewer = ref(false);
+
+// 上传限制:100MB,支持 pdf、doc、docx、jpg、jpeg、png、raw、dcm
+const { upload, accept } = useUpload({ maxSize: 100 * 1024 * 1024, formats: ['.pdf', '.doc', '.docx', '.jpg', '.jpeg', '.png', '.raw', '.dcm'] });
+const validateUpload = async (rawFile: File) => {
+  const ok = await upload(rawFile);
+  return !!ok;
+};
+
+const refresh = async () => {
+  if (!caseId.value || !otherTypeId.value) return;
+  files.value = await getCaseFiles({ caseId: caseId.value!, filesTypeId: otherTypeId.value! });
+};
+
+// 上传请求:直接入库并刷新列表
+const handleUploadRequest = async (options: any) => {
+  try {
+    const file: File = options.file as File;
+    const title = file.name.replace(/\.[^/.]+$/, '');
+    await addCaseFile({ caseId: caseId.value!, filesTitle: title, filesTypeId: otherTypeId.value!, file });
+    ElMessage.success('上传成功');
+    await refresh();
+  } catch (err) {
+    ElMessage.error('上传失败');
+    console.error(err);
+  }
+};
+
+// 查看:图片右侧预览,其它打开新标签(raw/dcm走专用查看器)
+const handleView = (file: CaseFile) => {
+  const ext = file.filesUrl.substring(file.filesUrl.lastIndexOf('.')).toLocaleLowerCase();
+  const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
+  if (imageExts.includes(ext)) {
+    previewUrl.value = file.filesUrl;
+    showViewer.value = false;
+  } else if ([".raw", ".dcm"].includes(ext)) {
+    window.open(`/${appId}/xfile-viewer/index.html?file=${file.filesUrl}&name=${file.filesTitle}&time=` + Date.now());
+  } else {
+    window.open(file.filesUrl + '?time=' + Date.now());
+  }
+};
+
+const handleDownload = (file: CaseFile) => {
+  window.open(file.filesUrl + '?time=' + Date.now());
+};
+
+const handleDelete = async (file: CaseFile) => {
+  if (await confirm('确定要删除此文件?')) {
+    await delCaseFile({ caseId: caseId.value!, filesId: file.filesId });
+    ElMessage.success('删除成功');
+    if (previewUrl.value === file.filesUrl) previewUrl.value = '';
+    refresh();
+  }
+};
+
+onMounted(async () => {
+  const types = await getCaseFileTypes();
+  const other = types.find((t) => /其他/.test(t.filesTypeName));
+  otherTypeId.value = other ? other.filesTypeId : 6;
+  await refresh();
+});
+</script>
+
+<style scoped lang="scss">
+.other-files {
+  display: flex;
+  justify-content: center;
+  height: calc(100% - 220px);
+  padding: 0 48px;
+}
+
+.left-panel {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 100%;
+  height: 100%;
+  background: #F5F5F5;
+  padding: 32px 0;
+  .file-list {
+    width: 80%;
+  }
+  .file-item {
+    height: 80px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 0 40px;
+    border: 1px solid #D9D9D9;
+    border-radius: 6px;
+    margin-top: 16px; 
+  }
+  .file-left {
+    display: flex;
+    align-items: center;
+    cursor: pointer;
+  }
+  .file-avatar {
+    width: 26px;
+    height: 26px;
+    border-radius: 50%;
+    background: #c0c4cc;
+    color: #fff;
+    font-size: 12px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+  .file-name {
+    margin-left: 10px;
+    color: #333;
+  }
+  .file-actions {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+  }
+}
+
+.upload-drop {
+  width: 80%;
+  margin-bottom: 12px;
+  :deep(.el-upload-dragger) {
+    width: 100%;
+    padding: 28px 0;
+    border-radius: 6px;
+  }
+}
+
+.upload-tip {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  color: #606266;
+  .upload-icon {
+    font-size: 24px;
+    color: #d23c3c;
+    margin-bottom: 8px;
+  }
+  .tip-title {
+    font-size: 14px;
+    margin-bottom: 4px;
+  }
+  .tip-sub {
+    font-size: 12px;
+    color: #909399;
+  }
+}
+.empty-tip {
+  text-align: center;
+  color: #909399;
+  padding: 12px 0;
+}
+
+.right-panel {
+  border: 1px dashed #d9d9d9;
+  border-radius: 6px;
+  background: #fafafa;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.preview-content,
+.preview-empty {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.preview-empty {
+  color: #909399;
+}
+</style>

File diff suppressed because it is too large
+ 431 - 0
src/view/newFireCase/newFireDetails/components/scene.vue


+ 179 - 0
src/view/newFireCase/newFireDetails/components/screenShot.vue

@@ -0,0 +1,179 @@
+<template>
+  <div class="screen-shot-container">
+    <div class="left-panel">
+      <div class="screen-shot" v-if="editOrShow === 'edit'" @click="startShot">
+        <i class="iconfont icon-record" />开始录制
+      </div>
+      <!-- 文件列表 -->
+      <div class="file-list">
+        <div
+          class="file-item"
+          v-for="file in files"
+          :key="file.filesId"
+        >
+          <div class="file-left" @click="handleView(file)">
+            <div class="file-avatar">
+              <img :src="file.videoFolderCover" alt="">
+              <i class="iconfont icon-play play-icon" />
+            </div>
+            <span class="file-name" :title="file.filesTitle">{{ file.videoFolderName }}</span>
+          </div>
+          <div class="file-actions" v-if="editOrShow === 'edit'">
+            <el-tooltip content="查看" placement="top">
+              <el-button link @click.stop="handleView(file)">
+                <i class="iconfont icon-rename" />
+              </el-button>
+            </el-tooltip>
+            <el-tooltip content="继续录制" placement="top">
+              <el-button link @click.stop="handleView(file)">
+                <i class="iconfont icon-record" />
+              </el-button>
+            </el-tooltip>
+            <el-tooltip content="下载" placement="top">
+              <el-button link @click.stop="handleDownload(file)">
+                <i class="iconfont icon-download" />
+              </el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button link @click.stop="handleDelete(file)">
+                <i class="iconfont icon-CloseCircle" />
+              </el-button>
+            </el-tooltip>
+          </div>
+        </div>
+        <div v-if="!files.length" class="empty-tip">暂无文件</div>
+      </div>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import { ref, computed, watch } from "vue";
+import { useRoute, useRouter } from 'vue-router';
+import { getRecordCaseVideo } from '@/store/editCsae';
+import { getResource } from '@/store/system';
+
+const props = defineProps<{ fire?: any, caseId?: number, editOrShow?: string }>();
+const emit = defineEmits<{
+  'start': any,
+  'playVideo': [value: string | Blob],
+}>()
+const startShot = () => {
+  emit("start");
+}
+const files = ref<any[]>([]);
+const loading = ref(false);
+
+// 获取录制列表
+const fetchRecordList = async () => {
+  if (!props.caseId) return;
+  loading.value = true;
+  try {
+    const res = await getRecordCaseVideo({ caseId: props.caseId });
+    const list = Array.isArray(res) ? res : (res?.data ?? res?.list ?? []);
+    files.value = list || [];
+  } catch (e) {
+    console.error('获取录制列表失败', e);
+  } finally {
+    loading.value = false;
+  }
+}
+
+watch(() => props.caseId, () => {
+  fetchRecordList();
+}, { immediate: true });
+
+// 查看视频:寻找可用的视频地址并抛出事件到上层弹窗
+const handleView = (file: any) => {
+  const candidates = [
+    file?.videoMergeUrl
+  ].filter(Boolean);
+  const rawUrl = candidates[0];
+  if (!rawUrl) return;
+  const url = typeof rawUrl === 'string' ? getResource(rawUrl) : rawUrl;
+  emit('playVideo', url);
+};
+const handleDownload = (file: any) => {};
+const handleDelete = (file: any) => {};
+</script>
+<style scoped lang="scss">
+.screen-shot-container {
+  display: flex;
+  justify-content: center;
+  height: calc(100% - 220px);
+  padding: 0 48px;
+  .left-panel {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    width: 100%;
+    height: 100%;
+    background: #F5F5F5;
+    padding: 32px 0;
+    .file-list {
+      width: 80%;
+    }
+    .file-item {
+      height: 80px;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      padding: 0 40px;
+      border: 1px solid #D9D9D9;
+      border-radius: 6px;
+      margin-top: 16px; 
+    }
+    .file-left {
+      display: flex;
+      align-items: center;
+      cursor: pointer;
+    }
+    .file-avatar {
+      position: relative;
+      width: 32px;
+      height: 32px;
+      background: #c0c4cc;
+      color: #fff;
+      font-size: 12px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      img {
+        width: 100%;
+        height: 100%;
+      }
+      .play-icon{
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+      }
+    }
+    .file-name {
+      margin-left: 10px;
+      color: #333;
+    }
+    .file-actions {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+    }
+    .empty-tip {
+      text-align: center;
+      color: #909399;
+      padding: 12px 0;
+    }
+  }
+  .screen-shot {
+    border: 1px solid #D9D9D9;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    gap: 10px;
+    width: 80%;
+    height: 100px;
+    background: #FAFAFA;
+    border-radius: 2px 2px 2px 2px;
+    cursor: pointer;
+  }
+}
+</style>

+ 413 - 0
src/view/newFireCase/newFireDetails/components/shot.vue

@@ -0,0 +1,413 @@
+<template>
+  <teleport :to="appEl" v-if="appEl">
+    <Toolbar :showBottomBar="showBottomBar" class="shot-ctrl">
+      <el-button type="primary" class="btn" @click="close">取消</el-button>
+      <el-button
+        type="primary"
+        class="btn"
+        :class="{ disabled: blobs.length === 0 }"
+        @click="complete"
+        >合并视频</el-button
+      >
+      <div :style="{ bottom: barHeight }" class="other">
+        <span @click="start" style="background: #000;color: #fff;">继续录制</span>
+        <!-- <ui-icon
+          class="icon"
+          type="video1"
+          ctrl
+          @click="start"
+          tip="继续录制"
+          tipV="top"
+        /> -->
+      </div>
+      <div class="video-list" v-if="videoList.length">
+        <div class="layout" :style="{ width: `${videoList.length * 130}px` }">
+          <div v-for="video in videoList" :key="video.cover" class="cover">
+            <img :src="video.cover" />
+            <el-icon color="#fff" class="preview" @click="openVideo(video.origin)"><VideoPlay /></el-icon>
+            <!-- <ui-icon
+              type="preview"
+              ctrl
+              class="preview"
+              @click="palyUrl = video.origin"
+            /> -->
+          </div>
+        </div>
+      </div>
+    </Toolbar>
+    <!-- <Preview
+      v-if="palyUrl"
+      :items="[{ type: MetaType.video, url: palyUrl }]"
+      @close="palyUrl = null"
+    /> -->
+  </teleport>
+</template>
+
+<script setup lang="ts">
+import {
+  ref,
+  defineComponent,
+  onUnmounted,
+  watch,
+  shallowReactive,
+  computed,
+  watchEffect,
+} from "vue";
+import Toolbar from "./Toolbar.vue";
+import { ElMessage } from "element-plus";
+import { VideoRecorder } from "simaqcore";
+import { getVideoCover } from "@/util/video-cover";
+import { getRecordFragmentBlobs, appEl, getResource } from "@/store/system";
+
+type VideoItem = { origin: Blob | string; cover: string };
+const props = defineProps<{ record?: any }>();
+const emit = defineEmits<{
+  'append': [blobs: Blob[]],
+  'updateCover': [cover: any], 
+  'close': [value: any],
+  'preview': [value: any],
+  'deleteRecord': [value: any],
+  'playVideo': [value: any],
+}>()
+
+const config: any = {
+  uploadUrl: "",
+  resolution: "4k",
+  debug: false,
+};
+const MetaType = 'video';
+const videoRecorder = new VideoRecorder(config);
+let showBottomBar = ref(false);
+const MAX_SIZE = 2 * 1024 * 1024 * 1024;
+const MAX_TIME = 30 * 60 * 1000;
+const previewVisible = ref(false);
+
+const countdown = ref(0);
+let interval: NodeJS.Timer;
+let recordIng = ref(false);
+const start = () => {
+  if (size.value > MAX_SIZE || pauseTime.value < 2000) {
+    return ElMessage.warning("已超出限制大小无法继续录制,可保存后继续录制!");
+  }
+
+  showBottomBar.value = false;
+  countdown.value = 2;
+  const timeiffe = () => {
+    if (--countdown.value <= 0) {
+      clearInterval(interval);
+      videoRecorder.startRecord();
+      recordIng.value = true;
+    } else {
+      interval = setTimeout(timeiffe, 300);
+    }
+  };
+  timeiffe();
+};
+
+const pause = () => {
+  console.log('appEl:', appEl.value)
+  if (countdown.value === 0 && recordIng.value) {
+    videoRecorder.endRecord();
+    recordIng.value = false;
+  }
+  countdown.value = 0;
+  showBottomBar.value = true;
+  console.log(showBottomBar.value, 444)
+  clearInterval(interval);
+};
+
+watch(recordIng, (_n, _o, onCleanup) => {
+  if (recordIng.value) {
+    const timeout = setTimeout(() => videoRecorder.endRecord(), pauseTime.value);
+    onCleanup(() => clearTimeout(timeout));
+  }
+});
+
+const blobs: File[] = shallowReactive([]);
+const size = computed(() => {
+  console.log('videoList:', videoList);
+  return videoList.reduce(
+    (t, f) => (typeof f.origin === "string" ? t : t + f.origin.size),
+    0
+  );
+});
+const pauseTime = computed(() => (MAX_TIME / MAX_SIZE) * (MAX_SIZE - size.value));
+videoRecorder.off("*");
+videoRecorder.on("record", (blob) => {
+  if (recordIng.value) {
+    blobs.push(new File([blob], "录屏.mp4", { type: "video/mp4; codecs=h264" }));
+  }
+});
+videoRecorder.on("cancelRecord", pause);
+videoRecorder.on("endRecord", pause);
+
+const videoList: any = [];
+let initial = false;
+watch([blobs, props], async () => {
+    const existsVideos = [];
+    if (props.record.url) {
+      existsVideos.push(getResource(props.record.url));
+    }
+    const fragmentBlobs = getRecordFragmentBlobs(props.record);
+    existsVideos.push(...fragmentBlobs, ...blobs);
+    for (const blob of existsVideos) {
+      if (videoList.some((item) => item.origin === blob)) {
+        continue;
+      }
+      const cover = await getVideoCover(blob, 3, 120, 80);
+      videoList.push({ origin: blob, cover });
+    }
+    for (let i = 0; i < videoList.length; i++) {
+      if (!existsVideos.some((blob) => videoList[i].origin === blob)) {
+        videoList.splice(i--, 1);
+      }
+    }
+    if (!props.record.cover && videoList.length) {
+      emit("updateCover", videoList[0].cover);
+    }
+    if (!initial) {
+      initial = true;
+      start();
+    }
+  },
+  { immediate: true }
+);
+const openVideo = (url: string | Blob) => {
+  const playUrl = typeof url === "string"
+    ? getResource(url)
+    : URL.createObjectURL(url)
+  emit('playVideo', playUrl)
+}
+
+const upHandler = (ev: KeyboardEvent) => ev.code === `Escape` && videoRecorder.endRecord();
+document.body.addEventListener("keyup", upHandler, { capture: true });
+
+const complete = () => {
+  emit("append", blobs);
+  close();
+};
+const close = () => {
+  pause();
+  emit("close");
+};
+const barHeight = computed(() => (videoList.length ? "180px" : "60px"));
+onUnmounted(() => {
+  document.body.removeEventListener("keyup", upHandler, { capture: true });
+});
+</script>
+
+<style lang="scss" scoped>
+
+.btns {
+  display: flex;
+
+  .unit,
+  .start {
+    height: 38px;
+  }
+  .unit {
+    flex: none;
+    margin-left: 10px;
+  }
+
+  .start {
+    flex: 1;
+  }
+}
+
+.tree {
+  margin-top: 20px;
+  padding-bottom: 100px;
+}
+
+.header-btns {
+  margin: 0 -20px;
+  padding: 0 20px 20px;
+  border-bottom: 1px solid rgba(255,255,255,0.1600);;
+}
+
+
+.sign {
+  padding: 20px 0;
+  border-top: 1px solid rgba(255,255,255,0.1600);
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 0 !important;
+
+  &:last-child {
+    border-bottom: 1px solid rgba(255,255,255,0.1600);
+  }
+}
+
+.content {
+  display: flex;
+  align-items: center;
+
+  .cover {
+    display: flex;
+    position: relative;
+    width: 48px;
+    height: 48px;
+    align-items: center;
+    justify-content: center;
+    color: #fff;
+    font-size: 16px;
+    margin-right: 10px;
+    flex: none;
+    cursor: pointer;
+
+    img,
+    &::before {
+      position: absolute;
+      left: 0;
+      top: 0;
+      bottom: 0;
+      right: 0;
+    } 
+    
+    &::before{
+      content: '';
+      z-index: 2;
+      background: rgba(0,0,0,0.5);
+    }
+
+    img {
+      position: absolute;
+      z-index: 1;
+      left: 0;
+      top: 0;
+      width: 100%;
+      height: 100%;
+    }
+
+    .preview {
+      position: relative;
+      z-index: 3;
+    }
+  }
+
+  .title {
+    p {
+      font-size: 14px;
+    }
+    span {
+      font-size: 12px;
+      color: rgba(255,255,255,0.4000);
+    }
+  }
+}
+
+.action {
+  color: #fff;
+  font-size: 14px;
+  flex: none;
+  margin-left: 10px;
+}
+
+.countdown {
+  font-size: 14px;
+  color: rgba(255,255,255,0.6);
+  background-color: rgba(27, 27, 28, .8);
+  position: absolute;
+  z-index: 99;
+  left: 50%;
+  top: 50%;
+  transform: translate(-50%, -50%);
+  padding: 30px 60px;
+  pointer-events: none;
+
+  p:not(:last-child) {
+    margin-bottom: 15px;
+  }
+
+  .title {
+    color: #fff;
+
+    span {
+      font-size: 32px;
+      font-weight: bold;
+      color: #00C8AF;
+      margin-right: 14px;
+    }
+  }
+}
+
+.shot-ctrl {
+  z-index: 2;
+  .btn {
+    flex: none;
+    width: 160px;
+
+    &:not(:last-child) {
+      margin-right: 20px;
+    }
+  }
+
+  .other {
+    position: absolute;
+    bottom: calc(100% + 120px);
+    left: 50%;
+    transform: translateX(-50%) ;
+    .icon {
+      margin: 20px;
+      display: inline-block;
+      width: 64px;
+      height: 64px;
+      border-radius: 50%;
+      background-color: rgba(27, 27, 28, .8);
+      color: rgba(255,255,255,.8);
+      font-size: 34px;
+      text-align: center;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+    }
+  }
+}
+
+
+.video-list {
+  position: absolute;
+  bottom: 100%;
+  left: 0;
+  width: 100%;
+  height: 120px;
+  overflow-x: auto;
+  background-color: rgba(27, 27, 28, .8);
+  border-bottom: 1px solid rgba(255,255,255,0.1600);
+
+  .layout {
+    display: flex;
+    align-items: center;
+    height: 100%;
+    justify-content: space-around;
+  }
+
+  .cover {
+    height: 80px;
+    position: relative;
+    cursor: pointer;
+    
+    &::before {
+      position: absolute;
+      left: 0;
+      top: 0;
+      bottom: 0;
+      right: 0;
+      content: '';
+      z-index: 2;
+      background: rgba(0,0,0,0.5);
+    } 
+    .preview {
+      position: absolute;
+      z-index: 3;
+      left: 50%;
+      top: 50%;
+      transform: translate(-50%, -50%);
+      font-size: 22px;
+    }
+  }
+}
+</style>

File diff suppressed because it is too large
+ 1021 - 0
src/view/newFireCase/newFireDetails/components/siteInspection.vue


+ 145 - 0
src/view/newFireCase/newFireDetails/editIndex.vue

@@ -0,0 +1,145 @@
+<template>
+  <div class="new-fire-details">
+    <el-menu :default-active="currentMenuKey" mode="horizontal" class="menu-vertical">
+      <el-menu-item 
+        v-for="menu in menus" 
+        :key="menu.key"
+        :index="menu.key"
+        @click="handleMenuClick(menu)"
+      >
+        {{ menu.label }}
+      </el-menu-item>
+    </el-menu>
+
+    <template v-if="currentMenuKey === 'info'">
+      <basicInfo :fire="tempFire" :caseId="caseId" :editOrShow="'edit'" />
+    </template>
+    <template v-if="currentMenuKey === 'screenRecord'">
+      <ScreenShot :fire="tempFire" :caseId="caseId" :editOrShow="'edit'" @start="startShot" @playVideo="(url) => emit('playVideo', url)" />
+    </template>
+    <template v-if="currentMenuKey === 'scene'">
+      <Scene :fire="tempFire" :caseId="caseId" :editOrShow="'edit'" />
+    </template>
+    <template v-if="currentMenuKey === 'mix3d'">
+      <Mix3d :fire="tempFire" :caseId="caseId" :editOrShow="'edit'" />
+    </template>
+    <template v-if="currentMenuKey === 'siteInspection'">
+      <SiteInspection :fire="tempFire" :caseId="caseId" :editOrShow="'edit'" />
+    </template>
+    <template v-if="currentMenuKey === 'otherFiles'">
+      <OtherFiles :fire="tempFire" :caseId="caseId" :editOrShow="'edit'" />
+    </template>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch } from "vue";
+import { useRoute, useRouter } from 'vue-router';
+import BasicInfo from './components/basicInfo.vue';
+import ScreenShot from './components/screenShot.vue';
+import Scene from './components/scene.vue';
+import Mix3d from './components/mix3d.vue';
+import SiteInspection from './components/siteInspection.vue';
+import OtherFiles from './components/otherFiles.vue';
+
+const props = defineProps<{
+  caseId: number;
+  currentRecord: object;
+}>();
+const emit = defineEmits<{
+  'start': any,
+  'playVideo': [value: string | Blob],
+}>()
+// 从路由获取参数
+const route = useRoute();
+const vueRouter = useRouter();
+const caseId = computed(() => Number(route.params.caseId));
+let tempFire = ref(null);
+watch(() => props.currentRecord, (newVal, oldVal) => {
+  console.log(newVal, newVal.tmProject, 8888)
+  tempFire.value = newVal.tmProject;
+})
+const startShot = () => {
+  emit("start");
+}
+// 从路由查询参数中获取当前菜单项,如果没有则默认为 'info'
+let currentMenuKey = ref(route.query.tab as string || 'info');
+// 处理菜单点击事件
+const handleMenuClick = async(menu) => {
+  currentMenuKey.value = menu.key;
+    // 更新路由,保留当前的 tab 参数
+    vueRouter.replace({
+      path: route.path,
+      query: {
+        ...route.query,
+        tab: menu.key
+      }
+    });
+    
+    // 执行菜单项的点击事件
+    menu.onClick();
+    // const caseInfo = await getCaseInfo(caseId.value!);
+    // if (caseInfo) {
+    //   // 设置页面标题,使用案件信息
+    //   pageTitle.value = caseInfo.caseTitle + " | 编辑"; // 使用 pageTitle
+    //   desc.value = "";
+    // }
+};
+const menus = computed(() => {
+  if (!caseId.value) {
+    return [];
+  }
+  const currentCaseId = caseId.value;
+  // const fuseLink = getFuseCodeLink(currentCaseId);
+
+  return [
+    {
+      key: "info",
+      disabled: false, // 默认不禁用,与原来不同
+      label: "基本信息",
+      onClick: () => {
+        // 不需要再次设置 currentMenuKey,因为已经在 handleMenuClick 中设置了
+      },
+    },
+    {
+      key: "scene",
+      disabled: false, // 默认不禁用,与原来不同
+      label: "实景三维",
+      onClick: () => {
+        // 不需要再次设置 currentMenuKey,因为已经在 handleMenuClick 中设置了
+      },
+    },
+    {
+      key: "mix3d",
+      disabled: false,
+      label: "多元融合",
+      onClick: () => {
+        // 不需要再次设置 currentMenuKey,因为已经在 handleMenuClick 中设置了
+      },
+    },
+    {
+      key: "siteInspection",
+      disabled: false,
+      label: "现场勘验",
+      onClick: () => {
+        // 不需要再次设置 currentMenuKey,因为已经在 handleMenuClick 中设置了
+      },
+    },
+    {
+      key: "otherFiles",
+      disabled: false,
+      label: "其他资料",
+      onClick: () => {
+        // 不需要再次设置 currentMenuKey,因为已经在 handleMenuClick 中设置了
+      },
+    }, 
+    { 
+      key: "screenRecord", // 录制
+      label: "现场讲解",
+      onClick: () => {
+        //checkScenesOpen(currentCaseId, `${fuseLink}#sceneEdit/record`);
+      },
+    },
+  ];
+});
+</script>

+ 172 - 0
src/view/newFireCase/newFireDetails/index.vue

@@ -0,0 +1,172 @@
+<template>
+  <div class="new-fire-details">
+    <!-- 顶部标题栏 -->
+    <headerTop :caseId="caseId" :currentRecord="currentRecord" :editOrShow="editOrShow" />
+    <!-- 查看页 -->
+    <showIndex :caseId="caseId" :currentRecord="currentRecord" @playVideo="playVideo" v-if="editOrShow === 'show'" />
+    <!-- 编辑页 -->
+    <editIndex :caseId="caseId" :currentRecord="currentRecord" @start="startShot" @playVideo="playVideo" v-else />
+  </div>
+  <shot v-if="isShot" @close="closeHandler" @append="appendFragment" @playVideo="playVideo"
+    @updateCover="(cover: string) => $emit('updateCover', cover)" @deleteRecord="$emit('delete')" :record="record" />
+  <el-dialog
+      v-model="previewVisible"
+      width="90%"
+      class="preview-dialog"
+      @close="closePreview"
+    >
+      <video
+        class="vidoe-play"
+        controls
+        autoplay
+        playsinline
+        webkit-playsinline
+      >
+        <source :src="palyUrl" />
+      </video>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from "vue";
+import { useRoute, useRouter } from 'vue-router';
+import showIndex from './showIndex.vue';
+import editIndex from './editIndex.vue';
+import { copyCase, getCaseSceneList, getCaseInfo, updateCaseInfo } from "@/store/case";
+import { RouteName, router } from "@/router";
+import shot from './components/shot.vue';
+import headerTop from './components/headerTop.vue';
+import { createRecordFragment } from '@/store/system'
+
+// 从路由获取参数
+const route = useRoute();
+const vueRouter = useRouter();
+const caseId = computed(() => Number(route.params.caseId));
+const editOrShow = computed(() => route.query.editOrShow as string || 'show');
+const currentRecord = ref<any>({}); // 当前的caseID获取的row
+onMounted(() => {
+  setTimeout(async() => {
+    try {
+      const caseInfo = await getCaseInfo(caseId.value!);
+      if (caseInfo) {
+        currentRecord.value = caseInfo;
+        currentRecord.value.tmProject.mapUrl = caseInfo.mapUrl || '';
+        currentRecord.value.tmProject.latAndLong = caseInfo.latAndLong || '';
+        
+        // const menu = menus.value.find(menu => menu.key === currentMenuKey.value);
+        // if (menu) {
+        //   menu.onClick();
+        // }
+        console.log(currentRecord.value, 8888)
+      } else {
+        console.error("该案件不存在!");
+        throw "该案件不存在!";
+      }
+    } catch (error) {
+      // 跳转到无权限页面
+      vueRouter.replace({
+        name: RouteName.noCase,
+        query: {}
+      });
+    }
+  }, 0);
+  
+});
+
+const emit = defineEmits<{
+  'updateCover': [cover: any],
+  'delete': any, 
+}>()
+
+const getSignRecord = (record: any) => ({
+  ...record,
+  immediately: record.status === -2,
+});
+const isShot = ref(false)
+let records = ref<any>([])
+let record = ref<any>({})
+let showBottomBar = ref(false)
+const previewVisible = ref(false)
+const palyUrl = ref<string | Blob | null>(null);
+const continueHandler = () => (isShot.value = true);
+const deleteHandler = () => emit("delete");
+const recordFragments = ref<any>([])
+const appendFragment = (blobs: Blob[]) => {
+//   recordFragments.value.push(
+//     ...blobs.map((blob) =>
+//       createRecordFragment({ url: blob, recordId: record.id })
+//     )
+//   );
+//  record.value.status = 1
+};
+const startShot = () => {
+  records.value.push({
+    id: 111,
+    title: '讲解视频' + (records.value.length + 1),
+    cover: '',
+    url: '',
+    status: 1,
+    sort: 0,
+  })
+  isShot.value = true
+  showBottomBar.value = true
+}
+const closeHandler = () => {
+  isShot.value = false
+}
+const playVideo = (url: string | Blob) => {
+  console.log(url, 9999)
+  palyUrl.value = url;
+  previewVisible.value = true;
+} 
+const closePreview = () => {
+  previewVisible.value = false;
+  palyUrl.value = null;
+}
+</script>
+
+<style lang="scss">
+.layer .content .view .main{
+  margin: 0!important;
+}
+.vidoe-play{
+  width: 100%;
+  height: 100%;
+}
+.preview-dialog{
+  height: 85%;
+  --el-dialog-margin-top: 5vh!important;
+  padding: 0!important;
+  background: transparent!important;
+  box-shadow: none!important;
+  .el-dialog__headerbtn{
+    top: -30px;
+    right: -30px;
+    font-size: 30px;
+    .el-dialog__close{
+      color: #fff;
+    }
+    &:hover{
+      .el-dialog__close{
+        color: #fff;
+      }
+    }
+  }
+}
+</style>
+<style lang="scss" scoped>
+.new-fire-details{
+  width: 100%;
+  height: 100%;
+  :deep(.el-menu){
+    padding-left: 24px;
+    .el-menu-item{
+      padding: 0;
+      margin: 0 24px;
+    }
+    .el-menu-item:not(.is-disabled):hover, .el-menu-item:not(.is-disabled):focus{
+      background-color: transparent;
+    }
+  }
+}
+</style>

+ 145 - 0
src/view/newFireCase/newFireDetails/showIndex.vue

@@ -0,0 +1,145 @@
+<template>
+  <div class="new-fire-details">
+    <el-menu :default-active="currentMenuKey" mode="horizontal" class="menu-vertical">
+      <el-menu-item 
+        v-for="menu in menus" 
+        :key="menu.key"
+        :index="menu.key"
+        @click="handleMenuClick(menu)"
+      >
+        {{ menu.label }}
+      </el-menu-item>
+    </el-menu>
+
+    <template v-if="currentMenuKey === 'info'">
+      <basicInfo :fire="tempFire" :caseId="caseId" :editOrShow="'show'" />
+    </template>
+    <template v-if="currentMenuKey === 'screenRecord'">
+      <ScreenShot :fire="tempFire" :caseId="caseId" :editOrShow="'show'" @start="startShot" @playVideo="(url) => emit('playVideo', url)" />
+    </template>
+    <template v-if="currentMenuKey === 'scene'">
+      <Scene :fire="tempFire" :caseId="caseId" :editOrShow="'show'" />
+    </template>
+    <template v-if="currentMenuKey === 'mix3d'">
+      <Mix3d :fire="tempFire" :caseId="caseId" :editOrShow="'show'" />
+    </template>
+    <template v-if="currentMenuKey === 'siteInspection'">
+      <SiteInspection :fire="tempFire" :caseId="caseId" :editOrShow="'show'" />
+    </template>
+    <template v-if="currentMenuKey === 'otherFiles'">
+      <OtherFiles :fire="tempFire" :caseId="caseId" :editOrShow="'show'" />
+    </template>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch } from "vue";
+import { useRoute, useRouter } from 'vue-router';
+import BasicInfo from './components/basicInfo.vue';
+import ScreenShot from './components/screenShot.vue';
+import Scene from './components/scene.vue';
+import Mix3d from './components/mix3d.vue';
+import SiteInspection from './components/siteInspection.vue';
+import OtherFiles from './components/otherFiles.vue';
+
+const props = defineProps<{
+  caseId: number;
+  currentRecord: object;
+}>();
+const emit = defineEmits<{
+  'start': any,
+  'playVideo': [value: string | Blob],
+}>()
+// 从路由获取参数
+const route = useRoute();
+const vueRouter = useRouter();
+const caseId = computed(() => Number(route.params.caseId));
+let tempFire = ref(null);
+watch(() => props.currentRecord, (newVal, oldVal) => {
+  console.log(newVal, newVal.tmProject, 8888)
+  tempFire.value = newVal.tmProject;
+})
+const startShot = () => {
+  emit("start");
+}
+// 从路由查询参数中获取当前菜单项,如果没有则默认为 'info'
+let currentMenuKey = ref(route.query.tab as string || 'info');
+// 处理菜单点击事件
+const handleMenuClick = async(menu) => {
+  currentMenuKey.value = menu.key;
+    // 更新路由,保留当前的 tab 参数
+    vueRouter.replace({
+      path: route.path,
+      query: {
+        ...route.query,
+        tab: menu.key
+      }
+    });
+    
+    // 执行菜单项的点击事件
+    menu.onClick();
+    // const caseInfo = await getCaseInfo(caseId.value!);
+    // if (caseInfo) {
+    //   // 设置页面标题,使用案件信息
+    //   pageTitle.value = caseInfo.caseTitle + " | 编辑"; // 使用 pageTitle
+    //   desc.value = "";
+    // }
+};
+const menus = computed(() => {
+  if (!caseId.value) {
+    return [];
+  }
+  const currentCaseId = caseId.value;
+  // const fuseLink = getFuseCodeLink(currentCaseId);
+
+  return [
+    {
+      key: "info",
+      disabled: false, // 默认不禁用,与原来不同
+      label: "基本信息",
+      onClick: () => {
+        // 不需要再次设置 currentMenuKey,因为已经在 handleMenuClick 中设置了
+      },
+    },
+    {
+      key: "scene",
+      disabled: false, // 默认不禁用,与原来不同
+      label: "实景三维",
+      onClick: () => {
+        // 不需要再次设置 currentMenuKey,因为已经在 handleMenuClick 中设置了
+      },
+    },
+    {
+      key: "mix3d",
+      disabled: false,
+      label: "多元融合",
+      onClick: () => {
+        // 不需要再次设置 currentMenuKey,因为已经在 handleMenuClick 中设置了
+      },
+    },
+    {
+      key: "siteInspection",
+      disabled: false,
+      label: "现场勘验",
+      onClick: () => {
+        // 不需要再次设置 currentMenuKey,因为已经在 handleMenuClick 中设置了
+      },
+    },
+    {
+      key: "otherFiles",
+      disabled: false,
+      label: "其他资料",
+      onClick: () => {
+        // 不需要再次设置 currentMenuKey,因为已经在 handleMenuClick 中设置了
+      },
+    }, 
+    { 
+      key: "screenRecord", // 录制
+      label: "现场讲解",
+      onClick: () => {
+        //checkScenesOpen(currentCaseId, `${fuseLink}#sceneEdit/record`);
+      },
+    },
+  ];
+});
+</script>

+ 58 - 0
src/view/newFireCase/newdispatch/editCrimical.vue

@@ -0,0 +1,58 @@
+<template>
+  <el-form ref="form" label-width="84px">
+    <!-- {{ bindExample }} -->
+    <el-form-item label="案件名称">
+      <el-input v-model="bindExample.caseTitle" maxlength="50" placeholder="请输入案件名称" />
+    </el-form-item>
+    <el-form-item label="详细地址">
+      <el-input v-model="bindExample.mapUrl" placeholder="输入名称搜索" clearable disabled>
+        <template #append>
+          <el-button :icon="Search" @click="searchAMapAddress" />
+        </template>
+      </el-input>
+    </el-form-item>
+    <el-form-item label="首页显示">
+      <el-switch v-model="bindExample.mapShow" :disabled="!bindExample.latAndLong" />
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import { ref } from "vue";
+import { Example, setExample, addExample } from "@/app/criminal/store/example";
+import { ElMessage } from "element-plus";
+import { QuiskExpose } from "@/helper/mount";
+import { Search } from "@element-plus/icons-vue";
+import { selectMapImage } from "@/view/case/quisk";
+
+const props = defineProps<{ example?: Example }>();
+const bindExample = ref<Example>(props.example ? { ...props.example } : ({} as Example));
+
+defineExpose<QuiskExpose>({
+  async submit() {
+    if (!bindExample.value.caseTitle || !bindExample.value.caseTitle.trim()) {
+      ElMessage.error("案件名称不能为空");
+      throw "案件名称不能为空";
+    } else if (!bindExample.value.latAndLong || !bindExample.value.latAndLong.trim()) {
+      ElMessage.error("详细地址不能为空");
+      throw "详细地址不能为空!";
+    }
+    await (bindExample.value.caseId
+      ? setExample(bindExample.value)
+      : addExample(bindExample.value));
+  },
+});
+
+
+
+const searchAMapAddress = async () => {
+  const data = await selectMapImage({});
+  if (!data?.search) return;
+
+  bindExample.value.mapUrl = data.search.text;
+  bindExample.value.latlng = bindExample.value.latAndLong = `${data.search.lat},${data.search.lng}`;
+  if (!data.search.text) {
+    bindExample.value.mapUrl = bindExample.value.latAndLong;
+  }
+};
+</script>

+ 325 - 0
src/view/newFireCase/newdispatch/editFire copy.vue

@@ -0,0 +1,325 @@
+<template>
+  <el-form ref="form" label-width="106px" class="camera-from">
+    <div class="def-select-map" ref="mapEl"></div>
+    <div class="el-form-item">
+      <el-col :span="12">
+        <el-form-item label="项目编号" class="mandatory">
+          <el-input
+            v-model="bindFire.projectSn"
+            maxlength="18"
+            placeholder="请输入项目编号"
+          />
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="起火对象" class="mandatory">
+          <el-input
+            v-model="bindFire.projectName"
+            maxlength="50"
+            placeholder="请输入起火对象"
+          />
+        </el-form-item>
+      </el-col>
+    </div>
+    <el-form-item label="详细地址" class="mandatory asdasd">
+      <!-- <el-input
+        v-model="bindFire.projectAddress"
+        maxlength="50"
+        placeholder="请输入详细地址"
+      /> -->
+      <el-input
+        v-model="bindFire.projectAddress"
+        placeholder="输入名称搜索"
+        clearable
+        disabled
+      >
+        <template #append>
+          <el-button :icon="Search" @click="searchAMapAddress" />
+        </template>
+      </el-input>
+    </el-form-item>
+    <el-form-item label="勘验地址">
+      <el-input v-model="bindFire.field1" maxlength="50" placeholder="请输入勘验地址" />
+    </el-form-item>
+    <el-form-item label="起火场所" class="mandatory">
+      <el-cascader
+        style="width: 100%"
+        v-model="projectSite"
+        placeholder="起火场所"
+        :options="place"
+        :props="{ expandTrigger: 'hover' }"
+      />
+    </el-form-item>
+    <!-- <el-form-item label="全宗名称">
+      <el-input v-model="bindFire.field2" maxlength="50" placeholder="请输入全宗名称" />
+    </el-form-item> -->
+
+    <div class="el-form-item">
+      <el-col :span="12">
+        <el-form-item label="勘验单位" class="mandatory">
+          <companySelect v-model="bindFire.deptId" hideAll :notUpdate="true" disabled />
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="勘验人员" class="mandatory">
+          <el-input
+            v-model="bindFire.organizerUsers"
+            maxlength="50"
+            placeholder="请输入勘验人员"
+          />
+        </el-form-item>
+      </el-col>
+    </div>
+    <div class="el-form-item">
+      <!-- <el-col :span="12">
+        <el-form-item label="勘验人姓名">
+          <el-input
+            v-model="bindFire.field3"
+            maxlength="18"
+            placeholder="请输入勘验人姓名"
+          />
+        </el-form-item>
+      </el-col> -->
+      <el-col :span="12">
+        <el-form-item label="勘验人职务">
+          <el-input
+            v-model="bindFire.field4"
+            maxlength="50"
+            placeholder="请输入勘验人职务"
+          />
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="火灾原因" class="mandatory">
+          <el-cascader
+            style="width: 100%"
+            v-model="fireReason"
+            placeholder="火灾原因:"
+            :options="reason"
+            :props="{ expandTrigger: 'hover' }"
+          />
+        </el-form-item>
+      </el-col>
+    </div>
+
+    <div class="el-form-item">
+      <el-col :span="12">
+        <el-form-item label="事故日期" class="mandatory" placeholder="请选择事故日期">
+          <el-date-picker
+            type="date"
+            v-model="accidentDate"
+            style="width: 100%"
+            :disabled-date="(date) => date.getTime() > new Date().getTime()"
+          />
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="大屏显示" class="mandatory">
+          <el-switch v-model="bindFire.mapShow" :disabled="!bindFire.latlng" />
+        </el-form-item>
+      </el-col>
+    </div>
+
+    <div class="el-form-item">
+      <el-col :span="12">
+        <el-form-item label="事件分类">
+          <el-input
+            v-model="bindFire.field5"
+            maxlength="18"
+            placeholder="请输入事件分类"
+          />
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="分类登记">
+          <el-input
+            v-model="bindFire.field6"
+            maxlength="50"
+            placeholder="请输入分类登记"
+          />
+        </el-form-item>
+      </el-col>
+    </div>
+
+    <div class="el-form-item">
+      <el-col :span="12">
+        <el-form-item label="天气情况">
+          <el-input
+            v-model="bindFire.field7"
+            placeholder="请输入天气情况"
+            show-word-limit
+            maxlength="100"
+          />
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item
+          label="勘验起止时间"
+          class="mandatory"
+          placeholder="请选择勘验起止时间"
+        >
+          <el-date-picker
+            v-model="f8"
+            type="daterange"
+            range-separator="-"
+            start-placeholder="开始时间"
+            end-placeholder="结束时间"
+            :disabled-date="(date) => date.getTime() > new Date().getTime()"
+          />
+          <!-- <el-date-picker
+            type="date"
+            v-model="f8"
+            style="width: 100%"
+            :disabled-date="(date) => date.getTime() > new Date().getTime()"
+          /> -->
+        </el-form-item>
+      </el-col>
+    </div>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import companySelect from "@/components/company-select/index.vue";
+import { ref, watchEffect } from "vue";
+import { Fire, setFire, addFire } from "@/app/fire/store/fire";
+import { reason, place } from "@/app/fire/constant/fire";
+import { ElMessage } from "element-plus";
+import { dateFormat, debounce } from "@/util";
+import { genCascaderValue, getCode } from "@/helper/cascader";
+import { QuiskExpose } from "@/helper/mount";
+import AMapLoader from "@amap/amap-jsapi-loader";
+import { user } from "@/store/user";
+import { Search } from "@element-plus/icons-vue";
+import { selectMapImage } from "@/view/case/quisk";
+
+const props = defineProps<{ fire?: Fire }>();
+
+const bindFire = ref<Fire>(
+  props.fire
+    ? { ...props.fire }
+    : ({
+        deptId: user.value.info.deptId,
+      } as Fire)
+);
+
+const fireReason = genCascaderValue(bindFire, "fireReason");
+const projectSite = genCascaderValue(bindFire, "projectSite");
+const accidentDate = ref(
+  bindFire.value.accidentDate ? new Date(bindFire.value.accidentDate) : new Date()
+);
+
+const f8s = [new Date(), new Date()];
+if (bindFire.value.field8) {
+  const s = bindFire.value.field8.split("至");
+  console.log(s);
+  if (s.length > 1) {
+    f8s[0] = new Date(s[0]);
+    f8s[1] = new Date(s[1]);
+  } else {
+    f8s[0] = new Date(s[0]);
+  }
+}
+
+const f8 = ref(f8s);
+const searchAMapAddress = async () => {
+  const data = await selectMapImage({});
+  if (!data?.search) return;
+
+  bindFire.value.projectAddress = data.search.text;
+  bindFire.value.latlng = bindFire.value.latAndLong = `${data.search.lat},${data.search.lng}`;
+  if (!data.search.text) {
+    bindFire.value.projectAddress = bindFire.value.latAndLong;
+  }
+};
+
+defineExpose<QuiskExpose>({
+  async submit() {
+    if (!bindFire.value.projectAddress || !bindFire.value.projectAddress.trim()) {
+      ElMessage.error("详细地址不能为空!");
+      throw "详细地址不能为空!";
+    } else if (!bindFire.value.projectSn || !bindFire.value.projectSn.trim()) {
+      ElMessage.error("项目编号不能为空!");
+      throw "项目编号不能为空!";
+    } else if (!bindFire.value.projectName || !bindFire.value.projectName.trim()) {
+      ElMessage.error("起火对象不能为空!");
+      throw "起火对象不能为空!";
+    } else if (!bindFire.value.projectSite || !bindFire.value.projectSite.trim()) {
+      ElMessage.error("起火场所不能为空!");
+      throw "起火场所不能为空!";
+    } else if (!bindFire.value.deptId || !bindFire.value.deptId.trim()) {
+      ElMessage.error("勘验单位不能为空!");
+      throw "勘验单位不能为空!";
+    } else if (!bindFire.value.organizerUsers || !bindFire.value.organizerUsers.trim()) {
+      ElMessage.error("勘验人员不能为空!");
+      throw "勘验人员不能为空!";
+    } else if (!accidentDate) {
+      ElMessage.error("事故日期不能为空!");
+      throw "事故日期不能为空!";
+    } else if (!bindFire.value.fireReason || !bindFire.value.fireReason.trim()) {
+      ElMessage.error("火灾原因不能为空!");
+      throw "火灾原因不能为空!";
+    }
+
+    bindFire.value.accidentDate = dateFormat(accidentDate.value, "yyyy-MM-dd");
+    bindFire.value.field8 =
+      dateFormat(f8.value[0], "yyyy-MM-dd") +
+      "至" +
+      dateFormat(f8.value[1], "yyyy-MM-dd");
+    bindFire.value.projectSiteCode = getCode(place, bindFire.value.projectSite);
+    bindFire.value.id
+      ? await setFire(bindFire.value)
+      : await addFire(bindFire.value as any);
+  },
+});
+</script>
+
+<style scoped>
+.search-result {
+  position: absolute;
+  left: 0;
+  right: 0;
+  z-index: 1;
+  overflow: hidden;
+  top: 100%;
+
+  &.show {
+    max-height: 450px;
+    overflow-y: auto;
+  }
+}
+
+.def-map-info {
+  margin-top: 10px;
+  p {
+    font-size: 14px;
+    color: rgba(0, 0, 0, 0.85);
+    display: inline;
+    &:not(:last-child)::after {
+      content: ",";
+      margin-right: 6px;
+    }
+  }
+
+  span::after {
+    content: ":";
+  }
+}
+
+.def-select-map {
+  position: absolute;
+  opacity: 0;
+  z-index: -1;
+  width: 540px;
+  height: 390px;
+  z-index: 1;
+  left: -100vw;
+  top: -100vh;
+  display: none;
+}
+
+.asdasd {
+  position: relative;
+  margin-bottom: 15px;
+  z-index: 2;
+}
+</style>

+ 185 - 0
src/view/newFireCase/newdispatch/editFire.vue

@@ -0,0 +1,185 @@
+<template>
+  <el-form ref="form" label-width="84px" class="camera-from">
+    <div class="el-form-item">
+      <el-col :span="12">
+        <el-form-item label="项目编号" class="mandatory">
+          <el-input
+            v-model="bindFire.projectSn"
+            maxlength="18"
+            placeholder="请输入项目编号"
+          />
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="起火对象" class="mandatory">
+          <el-input
+            v-model="bindFire.projectName"
+            maxlength="50"
+            placeholder="请输入起火对象"
+          />
+        </el-form-item>
+      </el-col>
+    </div>
+    <el-form-item label="详细地址" class="mandatory">
+      <el-input
+        v-model="bindFire.mapUrl"
+        placeholder="输入名称搜索"
+        clearable
+        disabled
+        class="mandatory"
+      >
+        <template #append>
+          <el-button :icon="Search" @click="searchAMapAddress" />
+        </template>
+      </el-input>
+    </el-form-item>
+
+    <el-form-item label="起火地址" class="mandatory">
+      <el-input
+        v-model="bindFire.projectAddress"
+        maxlength="50"
+        placeholder="请输入起火地址"
+      />
+    </el-form-item>
+    <el-form-item label="起火场所" class="mandatory">
+      <el-cascader
+        style="width: 100%"
+        v-model="projectSite"
+        placeholder="起火场所"
+        :options="place"
+        :props="{ expandTrigger: 'hover' }"
+      />
+    </el-form-item>
+    <div class="el-form-item">
+      <el-col :span="12">
+        <el-form-item label="承办单位" class="mandatory">
+          <companySelect v-model="bindFire.deptId" hideAll :notUpdate="true" disabled />
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="承办人员" class="mandatory" placeholder="请输入承办人员">
+          <el-input v-model="bindFire.organizerUsers" maxlength="50" />
+        </el-form-item>
+      </el-col>
+    </div>
+    <div class="el-form-item">
+      <el-col :span="12">
+        <el-form-item label="事故日期" class="mandatory" placeholder="请选择事故日期">
+          <el-date-picker
+            type="date"
+            v-model="accidentDate"
+            style="width: 100%"
+            :disabled-date="(date) => date.getTime() > new Date().getTime()"
+          />
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="火灾原因" class="mandatory">
+          <el-cascader
+            style="width: 100%"
+            v-model="fireReason"
+            placeholder="火灾原因:"
+            :options="reason"
+            :props="{ expandTrigger: 'hover' }"
+          />
+        </el-form-item>
+      </el-col>
+  
+    </div>
+    <div class="el-form-item">
+      <el-col :span="12">
+        <el-form-item label="首页显示" class="mandatory">
+          <el-switch v-model="bindFire.mapShow" :disabled="!bindFire.latAndLong" />
+        </el-form-item>
+      </el-col>
+    </div>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import companySelect from "@/components/company-select/index.vue";
+import { ref } from "vue";
+import { Fire, setFire, addFire } from "@/app/fire/store/fire";
+import { reason, place } from "@/app/fire/constant/fire";
+import { ElMessage } from "element-plus";
+import { dateFormat } from "@/util";
+import { genCascaderValue, getCode } from "@/helper/cascader";
+import { QuiskExpose } from "@/helper/mount";
+import { user } from "@/store/user";
+import { Search } from "@element-plus/icons-vue";
+import { selectMapImage } from "@/view/case/quisk";
+
+const props = defineProps<{ fire?: Fire }>();
+
+const bindFire = ref<Fire>(
+  props.fire
+    ? { ...props.fire }
+    : ({
+        deptId: user.value.info.deptId,
+      } as Fire)
+);
+
+const fireReason = genCascaderValue(bindFire, "fireReason");
+const projectSite = genCascaderValue(bindFire, "projectSite");
+const accidentDate = ref(
+  bindFire.value.accidentDate ? new Date(bindFire.value.accidentDate) : new Date()
+);
+
+defineExpose<QuiskExpose>({
+  async submit() {
+    if (!bindFire.value.latAndLong || !bindFire.value.latAndLong.trim()) {
+      ElMessage.error("详细地址不能为空");
+      throw "详细地址不能为空!";
+    } else if (!bindFire.value.projectAddress || !bindFire.value.projectAddress.trim()) {
+      ElMessage.error("起火地址不能为空!");
+      throw "起火地址不能为空!";
+    } else if (!bindFire.value.projectSn || !bindFire.value.projectSn.trim()) {
+      ElMessage.error("项目编号不能为空!");
+      throw "项目编号不能为空!";
+    } else if (!bindFire.value.projectName || !bindFire.value.projectName.trim()) {
+      ElMessage.error("起火对象不能为空!");
+      throw "起火对象不能为空!";
+    } else if (!bindFire.value.projectSite || !bindFire.value.projectSite.trim()) {
+      ElMessage.error("起火场所不能为空!");
+      throw "起火场所不能为空!";
+    } else if (!bindFire.value.deptId || !bindFire.value.deptId.trim()) {
+      ElMessage.error("承办单位不能为空!");
+      throw "承办单位不能为空!";
+    } else if (!bindFire.value.organizerUsers || !bindFire.value.organizerUsers.trim()) {
+      ElMessage.error("承办人员不能为空!");
+      throw "承办人员不能为空!";
+    } else if (!accidentDate) {
+      ElMessage.error("事故日期不能为空!");
+      throw "事故日期不能为空!";
+    } else if (!bindFire.value.fireReason || !bindFire.value.fireReason.trim()) {
+      ElMessage.error("火灾原因不能为空!");
+      throw "火灾原因不能为空!";
+    }
+
+    bindFire.value.accidentDate = dateFormat(accidentDate.value, "yyyy-MM-dd");
+    bindFire.value.projectSiteCode = getCode(place, bindFire.value.projectSite);
+    
+    // 保存数据
+    if (bindFire.value.id) {
+      await setFire(bindFire.value);
+    } else {
+      await addFire(bindFire.value as any);
+    }
+    
+    // 保存成功后,刷新fireDetails页面的数据
+    // 通过事件总线或全局事件触发刷新
+    window.location.reload()
+  },
+});
+
+const searchAMapAddress = async () => {
+  const data = await selectMapImage({});
+  if (!data?.search) return;
+
+  bindFire.value.mapUrl = data.search.text;
+  bindFire.value.latlng = bindFire.value.latAndLong = `${data.search.lat},${data.search.lng}`;
+  if (!data.search.text) {
+    bindFire.value.mapUrl = bindFire.value.latAndLong;
+  }
+};
+</script>

+ 43 - 0
src/view/newFireCase/newdispatch/editLeaveMsg.vue

@@ -0,0 +1,43 @@
+<template>
+  <el-form ref="form" label-width="84px" class="leave-from">
+    <el-input
+      type="textarea"
+      placeholder="请输入留言内容,限200字"
+      maxlength="200"
+      show-word-limit
+      v-model="content"
+    >
+    </el-input>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import { ElMessage } from "element-plus";
+import { ref } from "vue";
+import { QuiskExpose } from "@/helper/mount";
+import { addFireLeaveMsg } from "@/app/fire/store/fire";
+
+const props = defineProps<{
+  projectId: string;
+}>();
+const content = ref("");
+
+defineExpose<QuiskExpose>({
+  async submit() {
+    if (!content.value.trim()) {
+      ElMessage.error("留言内容不能为空!");
+      throw "留言内容不能为空!";
+    }
+    await addFireLeaveMsg({
+      content: content.value,
+      projectId: props.projectId,
+    });
+  },
+});
+</script>
+
+<style>
+.leave-from textarea {
+  height: 200px;
+}
+</style>

+ 143 - 0
src/view/newFireCase/newdispatch/example.vue

@@ -0,0 +1,143 @@
+<template>
+  <com-head :options="head" v-model="state.query.searchType">
+    <el-form label-width="84px" inline>
+      <el-form-item label="标题:">
+        <el-input v-model="state.query.caseTitle" placeholder="请输入"></el-input>
+      </el-form-item>
+      <el-form-item label="承办单位:">
+        <com-company v-model="state.query.deptId" />
+      </el-form-item>
+      <el-form-item class="searh-btns" style="grid-area: 1 / 4 / 2 / 6">
+        <el-button type="primary" @click="refresh">查询</el-button>
+        <el-button type="primary" plain @click="queryReset">重置</el-button>
+      </el-form-item>
+    </el-form>
+  </com-head>
+
+  <div class="body-layer">
+    <div class="body-head">
+      <h3 style="visibility: hidden">案件列表</h3>
+      <div class="table-ctrl-right">
+        <el-button type="primary" @click="addHandler" v-pdpath="['add']">
+          新增案件
+        </el-button>
+      </div>
+    </div>
+
+    <el-table
+      :data="state.table.rows"
+      id="multipleTable"
+      style="width: 100%"
+      class="table"
+      size="large"
+    >
+      <el-table-column label="序号" width="100" v-slot:default="{ $index }">
+        <span style="text-align: center">
+          {{ state.pag.size * (state.pag.currentPage - 1) + $index + 1 }}
+        </span>
+      </el-table-column>
+      <el-table-column label="标题" prop="caseTitle">
+        <template #default="{ row }">
+          <p class="oper-span tip clickable" @click="gotoQuery(row.caseId)" v-pdpath="['view']">{{ row.caseTitle }}</p>
+        </template>
+      </el-table-column>
+      <el-table-column label="承办单位" prop="deptName"></el-table-column>
+      <el-table-column label="创建时间" prop="createTime"></el-table-column>
+      <el-table-column
+        label="操作"
+        v-slot:default="{ row }: { row: Example }"
+        :width="240"
+      >
+    
+        <!-- <CaseEditMenu
+          :caseId="row.caseId"
+          :title="row.caseTitle"
+          :prevMenu="[
+            { key: 'info', label: '编辑案件', onClick: () => editHandler(row) },
+          ]"
+          v-if="row.caseId"
+        /> -->
+        <EditMenuToDetail :caseId="row.caseId" :fromRoute="'criminal'" :row="row"></EditMenuToDetail>
+        <!-- <span class="oper-span" @click="gotoQuery(row.caseId)" v-pdpath="['view']"
+          >查看</span
+        > -->
+        <MoreMenu :caseId="row.caseId" :title="row.caseTitle" @copy="copy" />
+        <!-- <span class="oper-span" @click="copy(row.caseId)" v-pdpath="['view']">
+            复制
+          </span> -->
+        <span
+          class="oper-span"
+          @click="del(row)"
+          style="color: var(--primaryColor)"
+          v-pdpath="['del']"
+        >
+          删除
+        </span>
+      </el-table-column>
+    </el-table>
+
+    <com-pagination
+      @size-change="changPageSize"
+      @current-change="changPageCurrent"
+      :current-page="state.pag.currentPage"
+      :page-size="state.pag.size"
+      :total="state.pag.total"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, watch } from "vue";
+import comHead from "@/components/head/index.vue";
+import comCompany from "@/components/company-select/index.vue";
+import comPagination from "@/components/pagination/index.vue";
+import { usePagging } from "@/hook/pagging";
+import { Example, delExample, getExamplePagging } from "@/app/criminal/store/example";
+import CaseEditMenu from "@/view/case/editMenu.vue";
+import { gotoQuery } from "@/view/case/help";
+import { addExample, editExample } from "./quisk";
+import { copyCase } from "@/store/case";
+import EditMenuToDetail from "@/view/case/editMenuToDetail.vue";
+import MoreMenu from "@/view/case/moreMenu.vue";
+
+let { state, refresh, queryReset, del, changPageSize, changPageCurrent } = usePagging({
+  get: getExamplePagging,
+  del: delExample,
+  mapper: {
+    delMsg: "删除案件,相关档案也会一并删除,确定要删除吗?",
+  },
+  paramsTemlate: { caseTitle: "", deptId: "", searchType: "0" },
+});
+// 菜单切换时,重置查询参数,但是菜单不重置
+const queryResetRaw = queryReset;
+queryReset = () => {
+  const type = state.query.searchType;
+  queryResetRaw();
+  state.query.searchType = type;
+};
+const head = computed(() => [
+  { name: "案件列表", value: "0" },
+  { name: "火调共享", value: "1" },
+  { name: "全部", value: "2" },
+]);
+watch(() => state.query.searchType, (newVal) => {
+  console.log(newVal, 666)
+  refresh();
+});
+const addHandler = async () => {
+  if (await addExample({})) {
+    refresh();
+  }
+};
+
+const editHandler = async (example: Example) => {
+  if (await editExample({ example })) {
+    refresh();
+  }
+};
+const copy = async (caseId: number) => {
+  await copyCase(caseId);
+  refresh();
+};
+
+</script>

+ 252 - 0
src/view/newFireCase/newdispatch/fireDetails.vue

@@ -0,0 +1,252 @@
+<template>
+  <div class="fire-details">
+    <div class="sidebar">
+      <el-menu
+        :default-active="currentMenuKey"
+        class="menu-vertical"
+      >
+        <div class="el-menu-item" slot="Menu-Item" @click="expandFireNews">
+          火调信息
+        </div>
+        <el-menu-item 
+          v-for="menu in menus" 
+          :key="menu.key"
+          :index="menu.key"
+          @click="handleMenuClick(menu)"
+        >
+          {{ menu.label }}
+        </el-menu-item>
+      </el-menu>
+    </div>
+    <div class="content">
+      <!-- 内容区域 -->
+      <div class="content-placeholder">
+        <!-- 这里可以根据选中的菜单项显示对应的内容 -->
+        <SceneList 
+          v-if="currentMenuKey === 'scene'"
+          :case-id="caseId"
+          :on-add-scenes="() => addCaseScenes({ caseId })"
+        />
+        <newCaseFile
+          v-if="!['info', 'scene', 'screenRecord'].includes(currentMenuKey)"
+          :currentMenuKey = currentMenuKey
+          :case-id="caseId"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, ref, onMounted, onUnmounted } from "vue";
+import { useRoute, useRouter } from 'vue-router';
+import { Fire } from "@/app/fire/store/fire";
+import { getFuseCodeLink, checkScenesOpen, MenuItem, getSWKKSyncLink } from "@/view/case/help";
+import { showCaseScenes, addCaseScenes, shareCase, downloadCase } from "@/view/case/quisk";
+import { RouteName, router } from "@/router";
+import { copyCase, getCaseSceneList, getCaseInfo, updateCaseInfo } from "@/store/case";
+import SceneList from "@/view/case/sceneList.vue";
+import newCaseFile from "@/view/case/newCaseFile.vue";
+import { title as pageTitle, desc } from "@/store/system"; // 重命名为 pageTitle
+import { appConstant } from "@/app/criminal/constant"; // 导入 appConstant 以便在组件卸载时恢复默认值
+import { addFire, editFire, showLeaveMsgList, addLeaveMsg } from "./quisk";
+import { useFirePagging } from "./pagging";
+
+const { pagging } = useFirePagging();
+// 从路由获取参数
+const route = useRoute();
+const vueRouter = useRouter();
+const caseId = computed(() => Number(route.params.caseId));
+const title = computed(() => route.query.title as string);
+
+// 从路由查询参数中获取当前菜单项,如果没有则默认为 'scene'
+const currentMenuKey = ref(route.query.tab as string || 'scene');
+
+// 页面加载时自动触发当前选中的菜单项的点击事件
+let currentRecord = ref<any>({}); // 当前的caseID获取的row
+onMounted(() => {
+  setTimeout(async() => {
+    try {
+      const caseInfo = await getCaseInfo(caseId.value!);
+      if (caseInfo) {
+        // 设置页面标题,使用案件信息
+        pageTitle.value = caseInfo.caseTitle + " | 编辑"; // 使用 pageTitle
+        desc.value = "";
+        currentRecord.value = caseInfo;
+        currentRecord.value.tmProject.mapUrl = caseInfo.mapUrl || '';
+        currentRecord.value.tmProject.latAndLong = caseInfo.latAndLong || '';
+        
+        const menu = menus.value.find(menu => menu.key === currentMenuKey.value);
+        if (menu) {
+          menu.onClick();
+        }
+      } else {
+        console.error("该案件不存在!");
+        throw "该案件不存在!";
+      }
+    } catch (error) {
+      // 跳转到无权限页面
+      vueRouter.replace({
+        name: RouteName.noCase,
+        query: {}
+      });
+    }
+  }, 0);
+  
+});
+
+// 处理菜单点击事件
+const handleMenuClick = async(menu) => {
+  currentMenuKey.value = menu.key;
+    // 更新路由,保留当前的 tab 参数
+    vueRouter.replace({
+      path: route.path,
+      query: {
+        ...route.query,
+        tab: menu.key
+      }
+    });
+    
+    // 执行菜单项的点击事件
+    menu.onClick();
+    const caseInfo = await getCaseInfo(caseId.value!);
+    if (caseInfo) {
+      // 设置页面标题,使用案件信息
+      pageTitle.value = caseInfo.caseTitle + " | 编辑"; // 使用 pageTitle
+      desc.value = "";
+    }
+};
+const expandFireNews = () => {
+  // 不切换标签页,只打开编辑弹窗
+  editHandler(currentRecord.value.tmProject);
+}
+
+const startRecording = () => {
+  if (!caseId.value) return;
+  const currentCaseId = caseId.value;
+  const fuseLink = getFuseCodeLink(currentCaseId);
+  checkScenesOpen(currentCaseId, `${fuseLink}#sceneEdit/record`);
+};
+
+const editHandler = async (row: any) => {
+  (await editFire({ fire: row }));
+};
+
+const menus = computed(() => {
+  if (!caseId.value) {
+    return [];
+  }
+  const currentCaseId = caseId.value;
+  const fuseLink = getFuseCodeLink(currentCaseId);
+
+  return [
+    // {
+    //   key: "info",
+    //   disabled: false, // 默认不禁用,与原来不同
+    //   label: "火调信息",
+    //   onClick: () => {
+    //     // 不切换标签页,只打开编辑弹窗
+    //     editHandler(currentRecord.value.tmProject);
+    //     // 恢复之前的标签页
+    //     currentMenuKey.value = route.query.tab as string || 'scene';
+    //   },
+    // },
+    {
+      key: "scene",
+      disabled: false, // 默认不禁用,与原来不同
+      label: "场景管理",
+      onClick: () => {
+        // 不需要再次设置 currentMenuKey,因为已经在 handleMenuClick 中设置了
+      },
+    },
+    {
+      key: "drawing",
+      disabled: false,
+      label: "绘图管理",
+      onClick: () => {
+        // 不需要再次设置 currentMenuKey,因为已经在 handleMenuClick 中设置了
+      },
+    },
+    {
+      key: "photo",
+      disabled: false,
+      label: "照片制卷",
+      onClick: () => {
+        // 不需要再次设置 currentMenuKey,因为已经在 handleMenuClick 中设置了
+      },
+    },
+    {
+      key: "record",
+      disabled: false,
+      label: "勘验笔录",
+      onClick: () => {
+        // 不需要再次设置 currentMenuKey,因为已经在 handleMenuClick 中设置了
+      },
+    },
+    {
+      key: "list",
+      disabled: false,
+      label: "提取清单",
+      onClick: () => {
+        // 不需要再次设置 currentMenuKey,因为已经在 handleMenuClick 中设置了
+      },
+    },
+    {
+      key: "other",
+      disabled: false,
+      label: "其他资料",
+      onClick: () => {
+        // 不需要再次设置 currentMenuKey,因为已经在 handleMenuClick 中设置了
+      },
+    }, 
+    { 
+      key: "screenRecord", // 修改为唯一的 key,避免与勘验笔录冲突
+      label: "屏幕录制",
+      onClick: () => {
+        //checkScenesOpen(currentCaseId, `${fuseLink}#sceneEdit/record`);
+      },
+    },
+  ];
+});
+
+// 获取当前选中的菜单项
+const currentMenu = computed(() => {
+  return menus.value.find(menu => menu.key === currentMenuKey.value);
+});
+</script>
+<style>
+.layer .content .view .main{
+  margin: 0!important;
+}
+</style>
+<style lang="scss" scoped>
+.fire-details {
+  display: flex;
+  height: 100%;
+  .el-menu{
+    border-right: 0;
+  }
+  .sidebar {
+    width: 200px;
+    // border-right: 1px solid #e6e6e6;
+    .menu-vertical {
+      width: 200px;
+      padding-left: 30px;
+      height: 100%;
+      text-align: center;
+    }
+  }
+  
+  .content {
+    flex: 1;
+    padding: 0 16px 16px 16px;
+    
+    .content-placeholder {
+      text-align: center;
+      background: #fff;
+      padding: 16px;
+      height: calc(100% - 34px);
+    }
+  }
+}
+</style>

+ 99 - 0
src/view/newFireCase/newdispatch/header.vue

@@ -0,0 +1,99 @@
+<template>
+  <com-head :options="head" v-model="pagging.state.query.fireType" showCtrl>
+    <el-form label-width="84px">
+      <el-form-item label="项目编号:">
+        <el-input v-model="pagging.state.query.projectSn" placeholder="请输入"></el-input>
+      </el-form-item>
+      <el-form-item label="起火对象:">
+        <el-input
+          v-model="pagging.state.query.projectName"
+          placeholder="请输入"
+        ></el-input>
+      </el-form-item>
+      <el-form-item label="起火地址:">
+        <el-input
+          v-model="pagging.state.query.projectAddress"
+          placeholder="请输入"
+        ></el-input>
+      </el-form-item>
+      <el-form-item label="起火场所:">
+        <el-cascader
+          style="width: 100%"
+          v-model="projectSite"
+          placeholder="请选择"
+          :options="[{ label: '全部', value: UN_REQ_NUM.toString() }].concat(place)"
+          :props="{ expandTrigger: 'hover', checkStrictly: true }"
+        ></el-cascader>
+      </el-form-item>
+
+      <el-form-item label="承办单位:">
+        <com-company v-model="pagging.state.query.deptId" />
+      </el-form-item>
+      <el-form-item label="事故日期:">
+        <el-date-picker
+          type="date"
+          v-model="pagging.state.query.accidentDate"
+          placeholder="请选择"
+          style="width: 100%"
+        />
+      </el-form-item>
+      <el-form-item label="火灾原因:">
+        <el-cascader
+          style="width: 100%"
+          v-model="fireReason"
+          placeholder="请选择"
+          :options="[{ label: '全部', value: UN_REQ_NUM.toString() }].concat(reason)"
+          :props="{ expandTrigger: 'hover', checkStrictly: true }"
+        ></el-cascader>
+      </el-form-item>
+      <el-form-item label="项目状态:">
+        <el-select placeholder="请选择" v-model="pagging.state.query.status" showAll>
+          <el-option
+            v-for="option in fireStatuOptions"
+            :key="option.value"
+            :value="option.value"
+            :label="option.label"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item class="searh-btns" style="grid-area: 1/4/4/5">
+        <el-button type="primary" @click="pagging.refresh">查询</el-button>
+        <el-button type="primary" plain @click="pagging.queryReset">重置</el-button>
+      </el-form-item>
+    </el-form>
+  </com-head>
+</template>
+
+<script lang="ts" setup>
+import { FirePagging } from "./pagging";
+import comCompany from "@/components/company-select/index.vue";
+import { genCascaderValue, getCode, getRaw, getValue } from "@/helper/cascader";
+import { reason, place, fireStatuOptions } from "@/app/fire/constant/fire";
+import comHead from "@/components/head/index.vue";
+import { computed, ref, watchEffect, watch } from "vue";
+import { UN_REQ_NUM } from "@/constant/sys";
+
+const props = defineProps<{ pagging: FirePagging; isTeached: boolean }>();
+const head = computed(() => [
+  { name: "火调列表", value: "0" },
+  { name: "火调共享", value: "1" },
+  { name: "全部", value: "2" },
+]);
+
+watch(() => props.pagging.state.query.fireType, (newVal) => {
+  console.log(newVal, 666)
+  props.pagging.queryReset();
+});
+const projectSite = genCascaderValue(
+  computed(() => props.pagging.state.query),
+  "projectSite",
+  UN_REQ_NUM.toString()
+);
+watchEffect(() => {
+  props.pagging.state.query.projectSiteCode = getCode(place, getRaw(projectSite.value!));
+});
+const fireReason = genCascaderValue(
+  computed(() => props.pagging.state.query),
+  "fireReason"
+);
+</script>

+ 112 - 0
src/view/newFireCase/newdispatch/index.vue

@@ -0,0 +1,112 @@
+<template>
+  <Header :pagging="pagging" :isTeached="isTeached" />
+  <List
+    :pagging="pagging"
+    :checkPerm="'cancel'"
+    :isRecycle="isRecycle"
+    @view-item="row => gotoDetails(row)"
+  >
+    <template v-slot:tableCtrl>
+      <template v-if="!isRecycle">
+        <el-button type="primary" @click="addHandler" v-pdpath="'add'">
+          新增火调项目
+        </el-button>
+        <el-button type="primary" @click="setTeaching" v-pdpath="'teach'" :class="{ disable: !pagging.state.table.selectRows.length }">
+          设为教学项目
+        </el-button>
+      </template>
+    </template>
+    <template v-slot:appendColumn v-if="!isTeached">
+      <el-table-column label="教学项目" v-slot:default="{ row }: { row: Fire }">
+        {{ row.isTeached ? "是" : "否" }}
+      </el-table-column>
+    </template>
+
+    <template v-slot:rowCtrl="{ row }: { row: Fire }">
+      <template v-if="!isRecycle">
+        <EditMenuToDetail :caseId="row.caseId" :fromRoute="'fire'" :row="row"></EditMenuToDetail>
+        <MoreMenu :caseId="row.caseId" :title="row.projectSn" @copy="copy" />
+        <span class="oper-span" @click="pagging.del(row)" style="color: #D8000A;" v-pdpath="['del']">
+          删除
+        </span>
+      </template>
+    </template>
+  </List>
+</template>
+
+<script setup lang="ts">
+import { appConstant } from "@/app";
+import Header from "./header.vue";
+import List from "./list.vue";
+import { useFirePagging } from "./pagging";
+import { Fire, revokeFireTeachs, setFireTeachs, setFire } from "@/app/fire/store/fire";
+import { copyCase } from "@/store/case";
+import CaseEditMenu from "@/view/case/editMenu.vue";
+import EditMenuToDetail from "@/view/case/editMenuToDetail.vue";
+import MoreMenu from "@/view/case/moreMenu.vue";
+import { gotoQuery } from "@/view/case/help";
+import { confirm } from "@/helper/message";
+import { addFire, editFire, showLeaveMsgList, addLeaveMsg } from "./quisk";
+import { shareCase } from "@/view/case/quisk";
+import { ElMessage } from "element-plus";
+import { RouteName, router } from "@/router";
+
+const { pagging, isTeached, isRecycle } = useFirePagging();
+const copy = async (caseId: number) => {
+  await copyCase(caseId);
+  pagging.refresh();
+};
+
+// 撤销教学
+const revokeTeaching = async (row?: Fire) => {
+  if (!(await confirm("撤销教学,火调项目将不再显示在教学平台。(火调项目不会删除)"))) {
+    return;
+  }
+  const items = row ? [row] : pagging.state.table.selectRows;
+  await revokeFireTeachs(items.map(({ id }) => id));
+  pagging.refresh();
+  ElMessage.success(
+    items.map(({ projectSn }) => projectSn).join(",") + "已成功从教学项目撤销"
+  );
+};
+
+const showMessageHandler = (row: Fire) => {
+  showLeaveMsgList({
+    projectId: row.id,
+    onAddLeaveMsg: () => addLeaveMsg({ projectId: row.id }),
+  });
+};
+
+const setTeaching = async () => {
+  let items = pagging.state.table.selectRows || [];
+  if (!items.length) {
+    return ElMessage.error("请先选择操作项");
+  }
+  if (
+    !(await confirm(
+      "将火调项目设为教学项目后,总队及以下全部队伍均可查看。设置后可在教学平台取消设置。"
+    ))
+  ) {
+    return;
+  }
+  await setFireTeachs(items.map(({ id }) => id));
+  pagging.refresh();
+  ElMessage.success(
+    "已成功设置" + items.length + "个火调项目到教学平台,已设置的项目不重复设置。"
+  );
+};
+
+const editHandler = async (row: Fire) => {
+  (await editFire({ fire: row })) && pagging.refresh();
+};
+const addHandler = async () => {
+  (await addFire({})) && pagging.refresh();
+};
+const gotoDetails = (row: Fire) => {
+  const routeData = router.resolve({
+    path: `/fireDetails/${row.caseId}`,
+    query: { editOrShow: 'show' }
+  });
+  window.open(routeData.href, '_blank');
+};
+</script>

+ 116 - 0
src/view/newFireCase/newdispatch/leaveMsgList.vue

@@ -0,0 +1,116 @@
+<template>
+  <!-- power="message:add" -->
+  <div class="body-layer" style="padding: 0">
+    <el-table
+      ref="multipleTable"
+      :data="state.table.rows"
+      tooltip-effect="dark"
+      style="width: 100%; max-height: 540px"
+    >
+      <el-table-column label="留言内容" prop="content" v-slot:default="{ row }">
+        <el-tooltip
+          class="item"
+          effect="dark"
+          :content="row.content"
+          placement="bottom-start"
+          v-if="row.content && row.content.length > 30"
+        >
+          <a class="msg-content"> {{ row.content.substr(0, 30) }}... </a>
+        </el-tooltip>
+        <a class="msg-content" v-else>
+          {{ row.content }}
+        </a>
+      </el-table-column>
+      <el-table-column label="创建人" prop="userName" width="150"></el-table-column>
+      <el-table-column
+        label="创建日期"
+        prop="createTime"
+        v-slot:default="{ row }"
+        width="180"
+      >
+        {{ format(row.createTime) }}
+      </el-table-column>
+    </el-table>
+
+    <com-pagination
+      @size-change="changPageSize"
+      @current-change="changPageCurrent"
+      :current-page="state.pag.currentPage"
+      :page-size="state.pag.size"
+      :size="8"
+      :total="state.pag.total"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { usePagging } from "@/hook/pagging";
+import comPagination from "@/components/pagination/index.vue";
+import { FireLeaveMsg, getFireLeaveMsgPagging } from "@/app/fire/store/fire";
+import { dateFormat } from "@/util";
+import { QuiskExpose } from "@/helper/mount";
+
+const props = defineProps<{
+  projectId: string;
+  onAddLeaveMsg: (projectId: string) => boolean | Promise<boolean>;
+}>();
+
+const { state, refresh, changPageSize, changPageCurrent } = usePagging({
+  get: getFireLeaveMsgPagging,
+  paramsTemlate: { projectId: props.projectId, pageSize: 8 },
+});
+state.pag.size = 8;
+changPageSize(8);
+
+const format = (date: number) => dateFormat(new Date(date), "yyyy-MM-dd hh:mm");
+
+defineExpose<QuiskExpose>({
+  async submit() {
+    if (await props.onAddLeaveMsg(props.projectId)) {
+      refresh();
+    }
+    throw "不退出";
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+.body-layer {
+  overflow-y: auto;
+}
+.table-ctrl-right {
+  .search-scene {
+    margin: 0 20px 0 26px;
+  }
+  i {
+    margin-left: 20px;
+    font-size: 1.7rem;
+    vertical-align: middle;
+    cursor: pointer;
+
+    &.active {
+      color: var(--primaryColor);
+    }
+  }
+}
+
+.leave-from {
+  // width: 520px;
+  margin: 0 auto;
+}
+
+.title {
+  font-weight: bold;
+  color: #26559b;
+  line-height: 19px;
+  font-size: 14px;
+}
+
+.msg-content {
+  overflow: hidden; //超出的文本隐藏
+  text-overflow: ellipsis; //溢出用省略号显示
+  white-space: nowrap; //溢出不换行
+  position: relative;
+  cursor: pointer;
+}
+</style>

+ 116 - 0
src/view/newFireCase/newdispatch/list.vue

@@ -0,0 +1,116 @@
+<template>
+  <div class="body-layer">
+    <div class="body-head">
+      <h3 style="visibility: hidden">项目列表</h3>
+      <div class="table-ctrl-right">
+        <slot name="tableCtrl" />
+      </div>
+    </div>
+
+    <el-table
+      :data="pagging.state.table.rows"
+      id="multipleTable"
+      style="width: 100%"
+      class="table"
+      size="large"
+      @selection-change="pagging.changeSelectRows"
+    >
+      <el-table-column
+        type="selection"
+        width="50"
+        :selectable="() => !!operateIsPermissionByPath(checkPerm)"
+      />
+      <el-table-column label="序号" width="100" v-slot:default="{ $index }">
+        <span>
+          {{ pagging.state.pag.size * (pagging.state.pag.currentPage - 1) + $index + 1 }}
+        </span>
+      </el-table-column>
+      <el-table-column label="项目编号" prop="projectSn"></el-table-column>
+      <el-table-column label="起火对象" prop="projectName" v-slot:default="{ row }">
+        <el-tooltip
+          class="item"
+          effect="dark"
+          :content="row.projectName"
+          placement="bottom-start"
+          v-if="row.projectName && row.projectName.length > 15"
+        >
+          <p class="tip oper-user clickable" @click="$emit('view-item', row)" v-pdpath="['view']">{{ row.projectName }}</p>
+        </el-tooltip>
+        <p class="tip clickable" @click="$emit('view-item', row)" v-pdpath="['view']" v-else>{{ row.projectName }}</p>
+      </el-table-column>
+      <el-table-column label="起火地址" prop="projectAddress" v-slot:default="{ row }">
+        <el-tooltip
+          class="item"
+          effect="dark"
+          :content="row.projectAddress"
+          placement="bottom-start"
+          v-if="row.projectAddress && row.projectAddress.length > 15"
+        >
+          <p class="tip oper-user">{{ row.projectAddress }}</p>
+        </el-tooltip>
+        <p class="tip" v-else>{{ row.projectAddress }}</p>
+      </el-table-column>
+      <el-table-column label="起火场所" prop="projectSite" v-slot:default="{ row }">
+        <el-tooltip
+          class="item"
+          effect="dark"
+          :content="row.projectSite"
+          placement="bottom-start"
+          v-if="row.projectSite && row.projectSite.length > 10"
+        >
+          <p class="tip oper-user">{{ row.projectSite }}</p>
+        </el-tooltip>
+        <p class="tip" v-else>{{ row.projectSite }}</p>
+      </el-table-column>
+      <el-table-column label="承办单位" prop="organizerDeptName"></el-table-column>
+      <el-table-column label="事故日期" prop="accidentDate"></el-table-column>
+      <el-table-column label="火灾原因" prop="fireReason" v-slot:default="{ row }">
+        <el-tooltip
+          class="item"
+          effect="dark"
+          :content="row.fireReason"
+          placement="bottom-start"
+          v-if="row.fireReason && row.fireReason.length > 15"
+        >
+          <p class="tip oper-user">{{ row.fireReason }}</p>
+        </el-tooltip>
+        <p class="tip" v-else>{{ row.fireReason }}</p>
+      </el-table-column>
+      <el-table-column label="项目状态" v-slot:default="{ row }">
+        {{ fireStatusDesc[row.status as FireStatus] }}
+      </el-table-column>
+      <slot name="appendColumn" />
+      <el-table-column label="操作" v-slot:default="{ row }" :width="220">
+        <slot name="rowCtrl" :row="row" />
+      </el-table-column>
+    </el-table>
+
+    <com-pagination
+      @size-change="pagging.changPageSize"
+      @current-change="pagging.changPageCurrent"
+      :current-page="pagging.state.pag.currentPage"
+      :page-size="pagging.state.pag.size"
+      :total="pagging.state.pag.total"
+    />
+  </div>
+</template>
+
+<script lang="ts" setup>
+import comPagination from "@/components/pagination/index.vue";
+import { fireStatusDesc } from "@/app/fire/constant/fire";
+import { FireStatus } from "@/app/fire/store/fire";
+import { operateIsPermissionByPath } from "@/directive/permission";
+import { FirePagging } from "./pagging";
+
+defineProps<{ pagging: FirePagging; checkPerm: string }>();
+defineEmits<{
+  (e: 'view-item', row: any): void
+}>();
+</script>
+
+<style scoped>
+.clickable {
+  cursor: pointer;
+  color: #26559b;
+}
+</style>

+ 81 - 0
src/view/newFireCase/newdispatch/pagging.ts

@@ -0,0 +1,81 @@
+import { usePagging } from "@/hook/pagging";
+import {
+  FirePaggingRoute,
+  FireStatus,
+  delFire,
+  getFirePagging,
+} from "@/app/fire/store/fire";
+import { computed, watch, watchEffect } from "vue";
+import { router } from "@/router";
+import { FireRouteName } from "@/app/fire/routeConfig";
+import { UN_REQ_NUM } from "@/constant/sys";
+
+export const useFirePagging = () => {
+  const isTeached = computed(
+    () => router.currentRoute.value.name === FireRouteName.teaching
+  );
+  const isRecycle = computed(
+    () => router.currentRoute.value.name === FireRouteName.recycle
+  );
+
+  const pagging = usePagging({
+    get: getFirePagging,
+    del: async (raw) => {
+      await delFire(raw, isRecycle.value ? 1 : 2);
+    },
+    mapper: {
+      delMsg: () =>
+        isRecycle.value
+          ? "删除后将无法恢复,确定要删除吗?"
+          : "删除火调项目,相关档案也会一并删除,确定要删除吗?",
+    },
+    paramsTemlate: {
+      fireType: '0', // 列表,共享,全部切换
+      projectSn: "",
+      projectName: "",
+      projectAddress: "",
+      deptId: "",
+      isDelete: 0,
+      accidentDate: "",
+      status: FireStatus.all,
+      projectSiteCode: "",
+      fireReason: UN_REQ_NUM.toString(),
+      organizerUsers: "",
+    } as any,
+  });
+
+  watch(() => [pagging.state.query.queryType, pagging.state.query.fireType, isTeached.value, isRecycle.value],() => {
+      if (isRecycle.value) {
+        pagging.state.query.queryType = FirePaggingRoute.fire;
+        pagging.state.query.isDelete = 2;
+      } else {
+        pagging.state.query.isDelete = 0;
+        pagging.state.query.queryType = isTeached.value
+          ? FirePaggingRoute.teached
+          : FirePaggingRoute.fire;
+      }
+      pagging.state.pag.currentPage = 1;
+    },
+    { flush: "post", immediate: true }
+  );
+
+  // 菜单切换时,重置查询参数,但是菜单不重置
+  const queryResetRaw = pagging.queryReset;
+  pagging.queryReset = () => {
+    const type = pagging.state.query.fireType;
+    queryResetRaw();
+    pagging.state.query.fireType = type;
+  };
+
+  const params = [{ ...pagging.state.query }, { ...pagging.state.query }];
+  watchEffect(
+    () => {
+      pagging.state.query = params[isTeached.value ? 0 : 1];
+      // pagging.state.query = params[isRecycle.value ? 0 : 1];
+    },
+    { flush: "sync" }
+  );
+
+  return { pagging, isTeached, isRecycle };
+};
+export type FirePagging = ReturnType<typeof useFirePagging>["pagging"];

+ 35 - 0
src/view/newFireCase/newdispatch/quisk.ts

@@ -0,0 +1,35 @@
+import EditFire from "./editFire.vue";
+import EditLeaveMsg from "./editLeaveMsg.vue";
+import LeaveMsgList from "./leaveMsgList.vue";
+import Edit from "./editCrimical.vue";
+import { quiskMountFactory } from "@/helper/mount";
+
+export const addFire = quiskMountFactory(EditFire, {
+  title: "新增火调项目",
+});
+
+export const editFire = quiskMountFactory(EditFire, {
+  title: "编辑火调项目",
+});
+
+export const addLeaveMsg = quiskMountFactory(EditLeaveMsg, {
+  title: "新增留言",
+  width: 540,
+});
+
+export const showLeaveMsgList = quiskMountFactory(LeaveMsgList, {
+  title: "留言",
+  enterText: "发表留言",
+  power: "message:add",
+});
+
+// 公安模块的
+export const addExample = quiskMountFactory(Edit, {
+  title: "新增案件",
+  width: 500,
+});
+
+export const editExample = quiskMountFactory(Edit, {
+  title: "编辑案件",
+  width: 500,
+});

+ 2 - 1
src/view/system/login.vue

@@ -146,7 +146,8 @@ const submitClick = async () => {
       window.localStorage.setItem('token', user.value.token)
       window.location.replace(url);
     } else {
-      router.replace({ name: RouteName.scene });
+      // router.replace({ name: RouteName.scene });
+      router.replace({ name: RouteName.dispatch });
     }
   } catch (e) {
     console.error(e);

+ 23 - 1
yarn.lock

@@ -786,6 +786,11 @@ estree-walker@^2.0.2:
   resolved "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz"
   integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
 
+eventemitter3@^4.0.7:
+  version "4.0.7"
+  resolved "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-4.0.7.tgz"
+  integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
+
 follow-redirects@^1.15.6:
   version "1.15.9"
   resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz"
@@ -1420,6 +1425,7 @@ proxy-from-env@^1.1.0:
     axios "^1.4.0"
     echarts "^5.4.3"
     element-plus "^2.3.8"
+    html2canvas "^1.4.1"
     js-base64 "^3.7.5"
     mime "^3.0.0"
     mitt "^3.0.1"
@@ -1427,6 +1433,7 @@ proxy-from-env@^1.1.0:
     qrcode.vue "^3.4.1"
     qs "^6.11.2"
     sass "^1.64.2"
+    simaqcore "^1.2.0"
     sortablejs "^1.15.2"
     swiper "^11.1.4"
     three "^0.158.0"
@@ -1505,6 +1512,13 @@ rollup@^1.20.0||^2.0.0||^3.0.0||^4.0.0, rollup@^3.27.1:
   optionalDependencies:
     fsevents "~2.3.2"
 
+rxjs@~7.5.7:
+  version "7.5.7"
+  resolved "https://registry.npmmirror.com/rxjs/-/rxjs-7.5.7.tgz"
+  integrity sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==
+  dependencies:
+    tslib "^2.1.0"
+
 safe-array-concat@^1.1.3:
   version "1.1.3"
   resolved "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz"
@@ -1640,6 +1654,14 @@ side-channel@^1.0.6, side-channel@^1.1.0:
     side-channel-map "^1.0.1"
     side-channel-weakmap "^1.0.2"
 
+simaqcore@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/simaqcore/-/simaqcore-1.2.0.tgz"
+  integrity sha512-WViM7DhEJ/2JlF23apBIYP0KcjHqTjqxlMOlPXJsZ6Fp/726APxT0s9P/hsApHYJMu3/Ztkd/3iTOGEOFTPE0Q==
+  dependencies:
+    eventemitter3 "^4.0.7"
+    rxjs "~7.5.7"
+
 sortablejs@^1.15.2:
   version "1.15.3"
   resolved "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.15.3.tgz"
@@ -1757,7 +1779,7 @@ to-fast-properties@^2.0.0:
   resolved "https://registry.npmmirror.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz"
   integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==
 
-tslib@2.3.0:
+tslib@^2.1.0, tslib@2.3.0:
   version "2.3.0"
   resolved "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz"
   integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==