瀏覽代碼

修改全部火调

wangfumin 1 月之前
父節點
當前提交
deb0d7393f
共有 50 個文件被更改,包括 3757 次插入1061 次删除
  1. 1 0
      .env.fire
  2. 13 0
      package-lock.json
  3. 2 1
      package.json
  4. 6 6
      src/app/criminal/routeConfig.ts
  5. 5 0
      src/request/config.ts
  6. 14 0
      src/request/urls.ts
  7. 30 4
      src/store/case.ts
  8. 80 3
      src/store/editCsae.ts
  9. 10 1
      src/store/scene.ts
  10. 6 1
      src/store/system.ts
  11. 2 0
      src/view/case/addPhotoFileAll.vue
  12. 4 4
      src/view/case/editMenuToDetail.vue
  13. 14 13
      src/view/case/moreMenu.vue
  14. 178 0
      src/view/case/newShare.vue
  15. 210 0
      src/view/case/newphotos/draggable.vue
  16. 154 0
      src/view/case/newphotos/edit.vue
  17. 570 0
      src/view/case/newphotos/index.vue
  18. 8 1
      src/view/case/photos/index.vue
  19. 4 2
      src/view/case/quisk.ts
  20. 2 2
      src/view/case/share.vue
  21. 19 14
      src/view/newFireCase/dyManager/index.vue
  22. 0 192
      src/view/newFireCase/dyManager/modelContent.vue
  23. 15 14
      src/view/newFireCase/dyManager/pagging.ts
  24. 7 52
      src/view/newFireCase/dyManager/sceneContent.vue
  25. 4 1
      src/view/newFireCase/meshManager/index.vue
  26. 0 192
      src/view/newFireCase/meshManager/modelContent.vue
  27. 15 16
      src/view/newFireCase/meshManager/pagging.ts
  28. 14 53
      src/view/newFireCase/meshManager/sceneContent.vue
  29. 5 1
      src/view/newFireCase/mix3dManager/index.vue
  30. 5 4
      src/view/newFireCase/mix3dManager/pagging.ts
  31. 6 10
      src/view/newFireCase/mix3dManager/sceneContent.vue
  32. 1 1
      src/view/newFireCase/newFireDetails/components/Toolbar.vue
  33. 209 128
      src/view/newFireCase/newFireDetails/components/basicInfo.vue
  34. 55 6
      src/view/newFireCase/newFireDetails/components/headerTop.vue
  35. 225 27
      src/view/newFireCase/newFireDetails/components/mix3d.vue
  36. 229 78
      src/view/newFireCase/newFireDetails/components/scene.vue
  37. 153 11
      src/view/newFireCase/newFireDetails/components/screenShot.vue
  38. 15 27
      src/view/newFireCase/newFireDetails/components/shot.vue
  39. 325 125
      src/view/newFireCase/newFireDetails/components/siteInspection.vue
  40. 502 0
      src/view/newFireCase/newFireDetails/editFilePage.vue
  41. 13 14
      src/view/newFireCase/newFireDetails/editIndex.vue
  42. 247 0
      src/view/newFireCase/newFireDetails/editInspection.vue
  43. 201 43
      src/view/newFireCase/newFireDetails/index.vue
  44. 104 0
      src/view/newFireCase/newFireDetails/photoEdit.vue
  45. 48 7
      src/view/newFireCase/newFireDetails/showIndex.vue
  46. 1 0
      src/view/newFireCase/newdispatch/fireDetails.vue
  47. 3 3
      src/view/newFireCase/newdispatch/index.vue
  48. 12 4
      src/view/newFireCase/newdispatch/list.vue
  49. 6 0
      vite.config.ts
  50. 5 0
      yarn.lock

+ 1 - 0
.env.fire

@@ -1,5 +1,6 @@
 VITE_APP_APP="fire"
 VITE_SEVER_URL="https://test-mix3d.4dkankan.com"
+# VITE_SEVER_URL="http://192.168.0.72:8808"
 VITE_DEVCODE_URL="https://test-mix3d.4dkankan.com/code"
 VITE_SWKK_URL="https://test.4dkankan.com"
 VITE_SERVICE_URL="https://test.4dkankan.com"

+ 13 - 0
package-lock.json

@@ -25,6 +25,7 @@
         "qrcode.vue": "^3.4.1",
         "qs": "^6.11.2",
         "sass": "^1.64.2",
+        "screenfull": "^6.0.2",
         "simaqcore": "^1.2.0",
         "sortablejs": "^1.15.2",
         "swiper": "^11.1.4",
@@ -3102,6 +3103,18 @@
         "node": ">=14.0.0"
       }
     },
+    "node_modules/screenfull": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmmirror.com/screenfull/-/screenfull-6.0.2.tgz",
+      "integrity": "sha512-AQdy8s4WhNvUZ6P8F6PB21tSPIYKniic+Ogx0AacBMjKP1GUHN2E9URxQHtCusiwxudnCKkdy4GrHXPPJSkCCw==",
+      "license": "MIT",
+      "engines": {
+        "node": "^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/semver": {
       "version": "5.7.2",
       "resolved": "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz",

+ 2 - 1
package.json

@@ -4,7 +4,7 @@
   "version": "0.0.0",
   "type": "module",
   "scripts": {
-    "dev": "vite --mode=criminal",
+    "dev": "vite --mode=fire",
     "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",
+    "screenfull": "^6.0.2",
     "simaqcore": "^1.2.0",
     "sortablejs": "^1.15.2",
     "swiper": "^11.1.4",

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

@@ -27,10 +27,10 @@ export const routes: Routes = [
     component: () => import("@/view/newFireCase/newdispatch/example.vue"),
     meta: { title: "案件管理", icon: "iconfire_management" },
   },
-  {
-    name: 'criminalDetails',
-    path: "/example/criminalDetails/:caseId",
-    component: () => import("@/app/criminal/view/example/criminalDetails.vue"),
-    meta: { title: "案件管理详情", icon: "iconfire_management" },
-  },
+  // {
+  //   name: 'criminalDetails',
+  //   path: "/example/criminalDetails/:caseId",
+  //   component: () => import("@/app/criminal/view/example/criminalDetails.vue"),
+  //   meta: { title: "案件管理详情", icon: "iconfire_management" },
+  // },
 ];

+ 5 - 0
src/request/config.ts

@@ -27,6 +27,7 @@ import {
   ffmpegMergeImage,
   addOrUpdateCaseTabulation
 } from "./urls";
+import { recordStatus } from "./urls";
 
 // 不需要登录就能请求的接口
 export const notLoginUrls = [
@@ -78,4 +79,8 @@ export const notOpenUrls: string[] = [
   uploadModel,
   getDownloadProcess,
   getCaseHasDownloadProcess,
+  // 录制进度轮询不应触发全局Loading
+  recordStatus,
+  // 照片合并(ffmpegMergeImage)不使用全局 Loading,以免遮挡标注页操作
+  ffmpegMergeImage,
 ];

+ 14 - 0
src/request/urls.ts

@@ -71,6 +71,7 @@ export const copyScene = "/fusion/scene/copyScene";
 export const downSceneHash = `/fusion/scene/downMD5`;
 export const checkGenMeshScene = "/fusion/scene/sceneDetail";
 export const genMeshSceneByCloud = "/fusion/scene/buildSceneObj";
+export const getMix3dList = "/fusion/caseFusion/pageList";
 
 // 统计
 export const sceneStatistics = `/fusion/data/sceneGroupByDept`;
@@ -198,6 +199,7 @@ export const caseExtractDetailExport = "/fusion/caseExtractDetail/downDocx";
 //标注
 export const getCaseImgTag = "/fusion/caseImgTag/info";
 export const saveCaseImgTag = "/fusion/caseImgTag/saveOrUpdate";
+export const getFfmpegImage = "/fusion/caseImg/getFfmpegImage"; // 列表
 
 // 火调链接地址设置密码
 export const setCasePsw = "/fusion/web/fireProject/updateRandomCode";
@@ -282,6 +284,18 @@ export const delCaseOverview = `/fusion/caseOverview/del`;
 
 // 录制
 export const recordCaseVideo = `/fusion/caseVideoFolder/allList`;
+export const recordStatus = `/fusion/caseVideo/uploadAddVideoProgress`
+export const insertRecord = `/fusion/caseVideo/uploadAddVideo`
+export const mergeRecord = `/fusion/caseVideo/uploadAddVideo`
+export const updateRecord = `/fusion/caseVideoFolder/updateNameOrSort`
+export const deleteRecord = `/fusion/caseVideoFolder/delete`
 
 // 设备列表
 export const cameraTypeAllList = `/fusion/cameraType/allList`;
+
+// 案件里面的列表
+export const getFusionAndScene = `/fusion/case/getFusionAndScene`;
+
+// 案件中添加多元融合
+export const addFusionIds = `/fusion/case/addFusionIds`;
+

+ 30 - 4
src/store/case.ts

@@ -22,6 +22,7 @@ import {
   saveCaseImgTag,
   getCaseImgTag,
   ffmpegMergeImage,
+  getFfmpegImage,
   getcaseTabulationList,
   getcaseOverviewList,
   updateCaseTabulation as updateCaseTabulationUrl,
@@ -98,6 +99,7 @@ export const getSceneKey = (scene: Scene) =>
 
 export type CaseScenes = { type: SceneType; numList: (string | number)[] }[];
 export const getCaseScenes = (scenes: Scene[]) => {
+  console.log(scenes, 'scenes')
   const typeIdents = [
     { type: SceneType.SWKJ, numList: [] },
     { type: SceneType.SWKK, numList: [] },
@@ -115,11 +117,31 @@ export const getCaseScenes = (scenes: Scene[]) => {
   return typeIdents;
 };
 
+// 列表有一个值不通
+export const getCaseScenesBySceneType = (scenes: Scene[]) => {
+  console.log(scenes, 'scenes')
+  const typeIdents = [
+    { type: SceneType.SWKJ, numList: [] },
+    { type: SceneType.SWKK, numList: [] },
+    { type: SceneType.SWMX, numList: [] },
+    { type: SceneType.SWSS, numList: [] },
+    { type: SceneType.SWSSMX, numList: [] },
+    { type: SceneType.SWYDSS, numList: [] },
+    { type: SceneType.SWYDMX, numList: [] },
+  ] as CaseScenes;
+
+  for (const scene of scenes) {
+    const typeIdent = typeIdents.find((ident) => ident.type === scene.sceneType)!;
+    typeIdent.numList.push(getSceneKey(scene));
+  }
+  return typeIdents;
+};
+
 export const replaceCaseScenes = (caseId: number, caseScenes: CaseScenes) =>
   axios.post(repCaseScenes, { sceneNumParam: caseScenes, caseId });
 
-export const caseImgList = (caseId: number, orderBy: string | null) =>
-  axios.post(caseApiList, { orderBy: orderBy || "", caseId });
+export const caseImgList = (caseId: number, orderBy: string | null, parentId?: number) =>
+  axios.post(caseApiList, { orderBy: orderBy || "", caseId, parentId });
 
 export const saveOrUpdate = (params: CaseImg) =>
   axios.post(saveApiOrUpdate, { ...params });
@@ -161,8 +183,12 @@ export const exportCaseDetailInfo = (caseId: number) =>
 export const saveCaseImgTagData = (params: any) =>
   axios.post(saveCaseImgTag, { ...params });
 
-export const getCaseImgTagData = (caseId: number) =>
-  axios.get(getCaseImgTag, { params: { caseId } });
+// 获取照片卷标注信息:支持按案件ID与可选的图片ID查询
+export const getCaseImgTagData = (caseId: number, imgId?: number) =>
+  axios.get(getCaseImgTag, { params: { caseId, imgId } });
+
+export const getFfmpegImageList = (caseId: any) =>
+  axios.get(getFfmpegImage, { params: { caseId } });
 
 
 export const submitMergePhotos = (data) => axios.post(ffmpegMergeImage, { ...data })

+ 80 - 3
src/store/editCsae.ts

@@ -1,8 +1,85 @@
 import {
   axios,
-  recordCaseVideo
+  recordCaseVideo,
+  getFusionAndScene,
+  addFusionIds,
+  // 录制与合并相关接口
+  recordStatus,
+  insertRecord,
+  mergeRecord,
+  updateRecord,
+  deleteRecord,
 } from "@/request";
+import { uploadFile } from '@/store/system';
 
-export const getRecordCaseVideo = async (props: {
+export const getRecordCaseVideo = async (props) => (await axios.get(recordCaseVideo, { params: { ...props, ingoreRes: true } })).data;
+
+export const getFusionAndSceneList = async (props) => (await axios.get(getFusionAndScene, { params: props })).data;
+
+export const addMix3dFusionIds = async (props) => (await axios.post(addFusionIds, props)).data;
+
+// =============== 录制视频:上传片段并触发合并/保存 =================
+
+// 上传录制片段并保存(后端同一接口同时承担上传与合并逻辑)
+export const uploadRecordFragments = async (props: {
   caseId: number;
-}) => (await axios.get(recordCaseVideo, { params: props })).data;
+  folderId?: number | string;
+  files: (Blob | File)[];
+}) => {
+  console.log(props, 9999)
+  const fd = new FormData();
+  fd.append("caseId", String(props.caseId));
+  if (props.folderId !== undefined && props.folderId !== null) {
+    fd.append("folderId", String(props.folderId));
+  }
+  (props.files || []).forEach((f) => fd.append("files", f as any));
+  // 同一接口:insertRecord/mergeRecord 指向同一路径;选用 insertRecord
+  return (await axios.post(insertRecord, fd)).data;
+};
+
+// 查询上传合并进度(可用于轮询显示)
+export const getUploadRecordProgress = async (props: {
+  folderId?: number | string;
+}) => (await axios.get(recordStatus, { params: { ...props, ingoreRes: true } })).data;
+
+// 更新视频文件夹名称或排序
+export const updateRecordFolder = async (props: {
+  id: number | string;
+  videoFolderName?: string;
+  sort?: number;
+}) => (await axios.post(updateRecord, props)).data;
+
+// 删除视频文件夹(导出以便统一调用)
+export const deleteRecordFolder = async (id: number | string) =>
+  (await axios.post(deleteRecord, { id })).data;
+
+// 更新录制视频信息(支持更改名称、排序、封面等)
+export const updateRecordInfo = async (props: {
+  sort?: number;
+  uploadStatus?: number;
+  videoFolderCover?: string | Blob | File;
+  videoFolderId: number | string;
+  videoFolderName?: string;
+  videoMergeUrl?: string;
+}) => {
+  console.log(props, 111)
+  let cover = props.videoFolderCover as any;
+  // 若封面是 Blob/File,则先上传获取 URL
+  if (cover && typeof cover !== 'string') {
+    try {
+      cover = await uploadFile(cover as File);
+    } catch (e) {
+      // 上传失败时保持原值(后端可忽略)
+      cover = props.videoFolderCover;
+    }
+  }
+  const payload: any = {
+    videoFolderId: props.videoFolderId,
+    videoFolderName: props.videoFolderName,
+    sort: props.sort,
+    uploadStatus: props.uploadStatus,
+    videoFolderCover: cover,
+    videoMergeUrl: props.videoMergeUrl,
+  };
+  return (await axios.post(updateRecord, payload)).data;
+};

+ 10 - 1
src/store/scene.ts

@@ -19,6 +19,7 @@ import {
   updateModelScene,
   uploadModel,
   cameraTypeAllList,
+  getMix3dList,
 } from "@/request";
 import saveAs from "@/util/file-serve";
 import { ElMessage } from "element-plus";
@@ -53,6 +54,7 @@ export type QuoteScene = BaseScene & {
   location: LocationEnum;
   viewCount: number;
   status: QuoteSceneStatus;
+  fusionTitle: any;
 };
 
 // 普通场景状态
@@ -179,7 +181,6 @@ export const getModelSceneStatus = async (scene: ModelScene) => {
 
 type ScenePaggingParams = PaggingReq<
   Pick<BaseScene, "searchType"> & {
-    modelTitle: string;
     sceneName: string;
     status?: number;
     caseId?: number;
@@ -188,6 +189,7 @@ type ScenePaggingParams = PaggingReq<
     cameraType: string;
     searchType: any;
     isObj: number;
+    fusionTitle: any;
   }
 >;
 export const getScenePagging = async (params: ScenePaggingParams) => {
@@ -259,3 +261,10 @@ export const getCameraTypeAllList = async () =>{
   let data = (await axios.get(cameraTypeAllList)).data as any;
   return data
 }
+
+// 获取多远融合列表
+export const getMix3dPagging = async (params: ScenePaggingParams) => {
+  const data = (await axios.post(getMix3dList, params)).data as PaggingRes<Scene>;
+
+  return data;
+};

+ 6 - 1
src/store/system.ts

@@ -51,6 +51,11 @@ export const uploadFile = async (file: File) => {
 
 export const appEl = ref<HTMLDivElement | null>(null);
 
+let currentTempIndex = 0;
+export const isTemploraryID = (id: string) =>
+  id.includes("__currentTempIndex__");
+export const createTemploraryID = () =>
+  `__currentTempIndex__${currentTempIndex++}`;
 export const recordFragments = ref<any>([])
 export const getRecordFragments = (record: any) =>  recordFragments.value.filter(fragment => fragment.recordId === record.id)
 
@@ -59,7 +64,7 @@ export const getRecordFragmentBlobs = (record) => getRecordFragments(record)
   .map(fragment => fragment.url as Blob)
 
 export const createRecordFragment = (recordFragment: Partial<any> = {}): any => ({
-  id: 2,
+  id: createTemploraryID(),
   recordId: '',
   cover: '',
   url: '',

+ 2 - 0
src/view/case/addPhotoFileAll.vue

@@ -70,6 +70,7 @@ import type { UploadProps } from "element-plus";
 const props = defineProps<{
   caseId: number;
   data: CaseImg;
+  parentId?: number;
 }>();
 
 const defaultUpload = (
@@ -174,6 +175,7 @@ defineExpose<QuiskExpose>({
           imgUrl: item.raw && item.raw.url,
           imgInfo: item.name.replace(/\.[^/.]+$/, ""),
           caseId: props.caseId,
+          parentId: props.parentId,
         };
       });
       await saveOrAndSave({ imgUrls });

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

@@ -17,7 +17,7 @@ const goToDetails = (caseId, title) => {
   const routeData = router.resolve({
     // path: `/fire/dispatch/fireDetails/${caseId}`,
     path: `/fireDetails/${caseId}`, 
-    query: { editOrShow: 'show' }
+    query: { editOrShow: 'edit', fromRoute: props.fromRoute }
   });
   // router.push({
   //   path: `/fire/dispatch/fireDetails/${caseId}`,
@@ -26,9 +26,9 @@ const goToDetails = (caseId, title) => {
 };
 const goToDetailCriminal = (caseId, title) => {
   const routeData = router.resolve({
-    name: 'criminalDetails', // 假设这是正确的路由名称
-    params: { caseId }
-    // query: { title }
+    // path: `/fire/dispatch/fireDetails/${caseId}`,
+    path: `/fireDetails/${caseId}`, 
+    query: { editOrShow: 'show', fromRoute: props.fromRoute }
   });
   window.open(routeData.href, '_blank');
 };

+ 14 - 13
src/view/case/moreMenu.vue

@@ -24,20 +24,21 @@ import { copyCase, getCaseSceneList } from "@/store/case";
 import { alert } from "@/helper/message";
 import { operateIsPermissionByPath } from "@/directive/permission";
 import { usePagging } from "@/hook/pagging";
-import { Example, delExample, getExamplePagging } from "@/app/criminal/store/example";
-const { state, refresh, queryReset, del, changPageSize, changPageCurrent } = usePagging({
-  get: getExamplePagging,
-  del: delExample,
-  mapper: {
-    delMsg: "删除案件,相关档案也会一并删除,确定要删除吗?",
-  },
-  paramsTemlate: { caseTitle: "", deptId: "" },
-});
+// import { Example, delExample, getExamplePagging } from "@/app/criminal/store/example";
+// const { state, refresh, queryReset, del, changPageSize, changPageCurrent } = usePagging({
+//   get: getExamplePagging,
+//   del: delExample,
+//   mapper: {
+//     delMsg: "删除案件,相关档案也会一并删除,确定要删除吗?",
+//   },
+//   paramsTemlate: { caseTitle: "", deptId: "" },
+// });
 const props = defineProps<{
   caseId: any;
+  projectName?: string;
   title?: string;
-  prevMenu?: MenuItem[];
-  lastMenu?: MenuItem[];
+  prevMenu?: any;
+  lastMenu?: any;
 }>();
 const emit = defineEmits(['copy', 'refresh']);
 const menus = computed(() => {
@@ -61,7 +62,7 @@ const menus = computed(() => {
     },
     {
       key: "share",
-      label: "分享",
+      label: "权限",
       permiss: 'edit',
       onClick: async () => {
         const scenes = await getCaseSceneList(caseId);
@@ -70,7 +71,7 @@ const menus = computed(() => {
         // } else {
         //   shareCase({ caseId: caseId });
         // }
-        shareCase({ caseId: caseId });
+        shareCase({ caseId: caseId, projectName: props.projectName || '' });
       },
     },
     {

+ 178 - 0
src/view/case/newShare.vue

@@ -0,0 +1,178 @@
+<template>
+  <el-form ref="form" label-width="130px" class="new-share">
+    <!-- 在数据大屏可见 -->
+    <el-form-item label="在数据大屏可见">
+      <el-switch v-model="visibleInDashboard" />
+      <span class="sub-tip">开启后,案件可在数据大屏中展示</span>
+    </el-form-item>
+
+    <!-- 上级组织共享权限 -->
+    <el-form-item class="org-share-item" label="上级组织共享权限">
+      <div class="org-share-row">
+        <el-cascader
+          class="org-picker"
+          :modelValue="superiorValue"
+          @update:modelValue="(val: string[]) => updateSuperiorValue(val)"
+          :options="organTrees"
+          :props="{ checkStrictly: true, label: 'name', value: 'id', disabled: 'disabled' }"
+          placeholder="请选择上级组织"
+        />
+      </div>
+      <p class="maker">说明:设置共享后,上级组织按所选权限访问案件数据</p>
+      <div class="org-perm">
+        <el-radio-group v-model="orgSharePerm" class="org-perm">
+          <el-radio :label="'none'">不共享</el-radio>
+          <el-radio :label="'read'">允许上级组织查看</el-radio>
+          <el-radio :label="'edit'">允许上级组织查看和编辑</el-radio>
+        </el-radio-group>
+      </div>
+    </el-form-item>
+
+    <!-- 查看权限 -->
+    <el-form-item label="查看权限">
+      <el-radio-group class="view-perm" v-model="viewScope">
+        <el-radio :label="'login'">仅登录用户可访问</el-radio>
+        <el-radio :label="'public'">公开且支持复制查看链接或加密链接分享</el-radio>
+      </el-radio-group>
+    </el-form-item>
+
+    <!-- 公开分享内容:复用 share.vue -->
+    <div v-if="viewScope === 'public'" class="public-share">
+      <p class="share-title">分享</p>
+      <ShareForm :caseId="caseId" :projectName="projectName" ref="shareRef" />
+    </div>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, ref } from "vue";
+import ShareForm from "./share.vue";
+import { QuiskExpose } from "@/helper/mount";
+import { copyText } from "@/util";
+import { getQuery } from "@/view/case/help";
+import { Organization, getOrganizationTree } from "@/store/organization";
+import { ElMessage } from "element-plus";
+
+const props = defineProps<{ caseId: number, projectName?: string }>();
+
+// 数据大屏可见
+const visibleInDashboard = ref<boolean>(true);
+
+// 上级组织选择与权限
+const organTrees = ref<Organization[]>([]);
+const selectedParentId = ref<string>("");
+const orgSharePerm = ref<"none" | "read" | "edit">("none");
+
+const queryPathById = (
+  id: string,
+  current: Organization[]
+): string[] | null => {
+  for (const item of current) {
+    if (item.id === id) {
+      return [item.id];
+    } else if (item.children) {
+      const cItem = queryPathById(id, item.children);
+      if (cItem) {
+        return [item.id, ...cItem];
+      }
+    }
+  }
+  return null;
+};
+
+const superiorValue = computed(() => {
+  return selectedParentId.value
+    ? queryPathById(selectedParentId.value, organTrees.value) || []
+    : [];
+});
+
+const updateSuperiorValue = (value: string[]) => {
+  selectedParentId.value = value[value.length - 1];
+};
+
+// 查看权限
+const viewScope = ref<"login" | "public">("login");
+const shareRef = ref<InstanceType<typeof ShareForm> | null>(null);
+
+defineExpose<QuiskExpose>({
+  async submit() {
+    // 这里只做前端配置与链接复制;实际保存逻辑可按后端接口接入
+    if (viewScope.value === "public") {
+      // 调用子组件的复制逻辑(复制链接+密码)
+      return await (shareRef.value as any)?.submit();
+    } else {
+      // 登录可见:仅复制查看链接
+      const link = getQuery(props.caseId, true);
+      copyText(`链接:${link}`);
+      ElMessage.success("链接复制成功");
+      return `链接:${link}`;
+    }
+  },
+});
+
+onMounted(async () => {
+  const trees = await getOrganizationTree("1");
+  const changeStatus = (trees: Organization[], fDisabled: boolean = false) =>
+    trees.map((item) => ({
+      ...item,
+      disabled: fDisabled,
+      children: item.children && changeStatus(item.children, fDisabled),
+    }));
+  organTrees.value = changeStatus(trees);
+});
+</script>
+
+<style scoped lang="scss">
+.new-share {
+  width: 650px;
+  margin: 0 auto;
+}
+.share-from{
+  margin-top: 20px;
+  padding-left: 88px;
+}
+.sub-tip {
+  margin-left: 12px;
+  color: #969799;
+}
+
+.org-share-row {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+}
+
+.org-picker {
+  min-width: 260px;
+}
+
+.org-perm {
+  margin-top: 10px;
+}
+
+.view-perm {
+  :deep(.el-radio) {
+    height: 30px;
+  }
+}
+.org-share-item{
+  :deep(.el-form-item__content){
+    display: block;
+  }
+}
+.maker {
+  width: 400px;
+  margin-top: 20px;
+  font-weight: 400;
+  color: #969799;
+  line-height: 20px;
+}
+
+.public-share {
+  margin-top: 8px;
+  .share-title{
+    width: 116px;
+    text-align: right;
+  }
+}
+</style>

+ 210 - 0
src/view/case/newphotos/draggable.vue

@@ -0,0 +1,210 @@
+<template>
+  <div class="VueDraggable">
+    <el-input
+      @change="handleSearch"
+      clearable
+      size="medium"
+      placeholder="请输入内容"
+      suffix-icon="search"
+      v-model="search"
+      class="input-with-select"
+    >
+    </el-input>
+    <VueDraggable
+      v-if="list.length"
+      :move="search ? false : null"
+      class="draggable"
+      ref="el"
+      v-model="list"
+      @sort="onChange"
+    >
+      <div
+        class="item"
+        v-for="(item, index) in search ? searchList : list"
+        :key="item.id"
+        @click="handleItem(item.id)"
+      >
+        <img class="itemImg" :src="item.imgUrl" alt="" />
+        <div class="text">
+          <div :title="item.imgInfo">{{ item.imgInfo }}</div>
+          <EditPen @click.stop="handleEdit(item)" class="EditPen" />
+        </div>
+        <CircleCloseFilled
+          @click.stop="handleDet(index, item.id)"
+          class="itemIcon"
+        />
+      </div>
+    </VueDraggable>
+    <el-empty class="empty" v-else description="请上传现场照片" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, watch, computed } from "vue";
+import { caseImgList, CaseImg, caseDel, caseUpdateSort } from "@/store/case";
+import { VueDraggable } from "vue-draggable-plus";
+import { openErrorMsg } from "@/request/errorMsg.js";
+import { addCaseImgFile } from "../quisk";
+import { ElMessage, ElMessageBox } from "element-plus";
+// import { IconRabbish } from '@element-plus/icons-vue'
+const props = defineProps({ sortType: Boolean, caseId: Number, parentId: Number });
+const emit = defineEmits<{
+  (e: "changeList", value: CaseImg[] | null): void;
+  (e: "handleItem", value: Number | null): void;
+}>();
+const list = ref<CaseImg[]>([]);
+const search = ref("");
+const searchList = computed(() => {
+  let searchText = search.value.toLowerCase();
+  return list.value.filter((item) => {
+    return item.imgInfo?.toLowerCase().includes(searchText);
+  });
+});
+
+watch(
+  () => props.sortType,
+  (newValue, oldValue) => {
+    emit("changeList", list.value);
+  },
+  { deep: true, immediate: true }
+);
+
+async function onChange({ newIndex, oldIndex }) {
+  if (search.value) {
+    openErrorMsg("搜索状态禁止拖动");
+    return;
+  }
+  const imageIDs = Array.from(list.value)
+    .filter((i, index) => index === newIndex || index === oldIndex)
+    .reduce((prev, current) => prev.concat(current["id"]), []);
+  console.log("draggable-imageIDs", imageIDs);
+  emit("delImage", imageIDs);
+  setTimeout(async () => {
+    let apiList = searchList.value.map((item, index) => {
+      return { ...item, sort: index + 1 };
+    });
+    console.log(apiList);
+    await caseUpdateSort(apiList);
+    emit("changeList", apiList);
+  }, 500);
+}
+function handleItem(id) {
+  setTimeout(() => {
+    let index = list.value.findIndex((item) => item.id === id);
+    console.log(index, list.value);
+    emit("handleItem", index);
+  }, 500);
+}
+function handleSearch(val: string) {
+  console.log("handleSearch", val, search.value);
+}
+async function getList() {
+  let lists = await caseImgList(props.caseId, "desc", props.parentId);
+  list.value = lists.data;
+  emit("changeList", list.value);
+}
+async function handleDet(index: Number, id: Number) {
+  const res = await ElMessageBox.confirm(
+    "删除图像后会重新排版并清空标记数据,是否继续?",
+    "温馨提示",
+    {
+      confirmButtonText: "确定",
+      cancelButtonText: "取消",
+      type: "default",
+    }
+  );
+  if (res) {
+    caseDel(id).then((res) => {
+      emit("delImage", [id]);
+      list.value.splice(index, 1);
+      emit("changeList", list.value);
+    });
+  }
+}
+async function handleEdit(params) {
+  await addCaseImgFile({
+    caseId: props.caseId,
+    data: {
+      ...params,
+    },
+  });
+  getList();
+}
+function filterItem() {
+  let searchText = search.value.toLowerCase();
+  return list.value.filter((item) => {
+    return item.imgInfo?.toLowerCase().includes(searchText);
+  });
+}
+onMounted(() => {
+  getList();
+  // emit("update:list", props.list.value);
+});
+defineExpose({
+  getList,
+  getImageIds: () => list.value.map((i) => i.id),
+});
+</script>
+<style lang="scss" scoped>
+.empty {
+  width: 200px;
+}
+.input-with-select {
+  margin-top: 16px;
+}
+.draggable {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+  .item {
+    position: relative;
+    // flex: 0 0 50%; /* 每个子元素占用50%的宽度 */
+    width: calc(50% - 4px);
+    margin-top: 16px;
+    .itemImg {
+      width: 100%;
+      height: 62px;
+      object-fit: cover;
+    }
+    .text {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      height: 20px;
+      div {
+        width: 100%;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+        overflow: hidden;
+      }
+      .EditPen {
+        width: 20px;
+        height: 20px;
+        display: none;
+      }
+      &:hover {
+        div {
+          width: calc(100% - 20px);
+        }
+        .EditPen {
+          display: block;
+        }
+      }
+    }
+    .itemIcon {
+      width: 20px;
+      height: 20px;
+      color: var(--el-color-primary);
+      position: absolute;
+      right: -10px;
+      top: -10px;
+      display: none;
+    }
+    &:hover {
+      .itemIcon {
+        display: block;
+      }
+    }
+  }
+}
+</style>

+ 154 - 0
src/view/case/newphotos/edit.vue

@@ -0,0 +1,154 @@
+<template>
+  <div class="layout" v-if="isShow">
+    <el-icon class="close" @click="handleClose">
+      <Close />
+    </el-icon>
+    <el-form :inline="true" :model="form" label-width="auto">
+      <el-form-item label="内容">
+        <el-input type="input" :maxlength="40" v-model="form.text" />
+      </el-form-item>
+
+      <el-form-item label="字号:">
+        <el-select
+          v-model="form.fontsize"
+          placeholder="选择字号"
+          style="width: 200px"
+        >
+          <el-option
+            v-for="item in fontSizeOptions"
+            v-bind="item"
+            :key="item.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="颜色:">
+        <el-color-picker
+          v-model="form.color"
+          color-format="rgba"
+          show-alpha
+          :predefine="predefineColors"
+        />
+      </el-form-item>
+      <el-form-item label="删除:">
+        <el-button type="primary" @click="handleDel">删除</el-button>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+<script setup>
+import { reactive, ref, watch } from "vue";
+
+const isShow = ref(false);
+const props = defineProps({ show: Boolean, data: Object });
+const emit = defineEmits(["update", "del", "close"]);
+
+const predefineColors = [
+  "#ff0f00",
+  "#ffbe00",
+  "#1a9bff",
+  "#1aad19",
+  "#000000",
+  "#ffffff",
+  "#666666",
+];
+
+watch(
+  props,
+  ({ show, data }) => {
+    isShow.value = show;
+    form.text = data.text;
+    form.id = data.id;
+    form.type = data.type;
+    form.pos = data.pos;
+    form.fontsize = data.fontsize || 12;
+    console.log("data", data);
+  },
+  {
+    deep: true,
+  }
+);
+
+// do not use same name with ref
+const defaultfrom = {
+  id: "",
+  text: "",
+  fontsize: 12,
+  type: null,
+  pos: null,
+  color: "#000000",
+};
+let form = reactive(defaultfrom);
+
+watch(
+  form,
+  () => {
+    handleUpdate();
+  },
+  {
+    deep: true,
+  }
+);
+
+const fontSizeRange = [8, 30];
+const fontSizeOptions = [];
+for (let i = fontSizeRange[0]; i <= fontSizeRange[1]; i++) {
+  fontSizeOptions.push({ value: i, label: i.toString() });
+}
+
+const handleClose = () => {
+  isShow.value = false;
+  emit("close", form);
+  form = reactive(defaultfrom);
+};
+const handleUpdate = () => {
+  emit("update", form);
+};
+
+const handleDel = () => {
+  isShow.value = false;
+  emit("del", form);
+  form = reactive(defaultfrom);
+};
+</script>
+
+<style>
+.layout {
+  position: absolute;
+  top: 0;
+  left: 50%;
+  transform: translateX(-50%);
+  right: 0;
+  background: #fff;
+  box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
+  z-index: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 15px 25px 0 10px;
+  width: fit-content;
+  /* width: 300px; */
+}
+.layout .el-form {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  justify-content: flex-start;
+  max-width: 300px;
+}
+.layout .el-form-item {
+  margin-left: 10px;
+  margin-right: 10px;
+  flex: 0 0 auto;
+  display: flex;
+  align-items: center;
+}
+.close {
+  align-self: start;
+  font-size: 14px;
+  color: rgba(0, 0, 0, 0.85);
+  position: absolute !important;
+  right: 10px;
+  top: 10px;
+  cursor: pointer;
+}
+</style>

+ 570 - 0
src/view/case/newphotos/index.vue

@@ -0,0 +1,570 @@
+<template>
+  <div class="photo">
+    <div class="left">
+      <div class="upload my-photo-upload">
+        <el-button type="primary" @click="addCaseFileHandlerAll">
+          上传照片
+        </el-button>
+        <el-button
+          type="primary"
+          @click="handleSwitchGrid"
+          :icon="sortType ? FullScreen : Menu"
+          >{{ sortType ? "横排" : "竖排" }}</el-button
+        >
+      </div>
+      <draggable
+        ref="childRef"
+        :caseId="caseId"
+        :parentId="parentId"
+        :sortType="sortType"
+        @changeList="changeList"
+        @handleItem="handleItem"
+        @delImage="handleImageDel"
+      />
+    </div>
+    <div class="right">
+      <div class="tools">
+        <el-button @click="handleMark">
+          <i class="iconfont icon-arrows1" />
+          箭头
+        </el-button>
+        <el-button @click="handleLine">
+          <i class="iconfont icon-index" />
+          标引
+        </el-button>
+        <el-button @click="handleSymbol">
+          <i class="iconfont icon-symbol" />
+          符号
+        </el-button>
+        <el-button @click="handleText">
+          <i class="iconfont icon-text1" />
+          文本</el-button
+        >
+        <!-- 保存按钮在 headerTop 中触发,此处隐藏 -->
+        <!-- <el-button @click="handleSave" class="save">保存</el-button> -->
+
+        <el-button @click="handleClear" v-if="hasDrawData" class="opt"
+          >清空</el-button
+        >
+        <el-button @click="handleFree" v-if="isShowExitEdit" class="opt"
+          >退出编辑</el-button
+        >
+      </div>
+
+      <canvas id="canvas" v-show="true"></canvas>
+      <edit
+        :show="editing.show"
+        :data="editing.data"
+        @update="handleEditingUpdate"
+        @del="handleEditingDel"
+        @close="handleEditingClose"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { onMounted, ref, computed, onUnmounted, reactive } from "vue";
+import { useRoute } from "vue-router";
+import { Menu, FullScreen } from "@element-plus/icons-vue";
+import { Swiper, SwiperSlide } from "swiper/vue";
+import "swiper/css";
+// import { addCaseFile } from "@/store/caseFile";
+import { addCaseImgFile, addCaseImgFileAll } from "../quisk";
+import {
+  saveCaseImgTagData,
+  getCaseImgTagData,
+  submitMergePhotos,
+} from "@/store/case";
+import Scene from "@/core/Scene.js";
+import draggable from "./draggable.vue";
+import edit from "./edit.vue";
+import saveAs from "@/util/file-serve";
+import { ElMessage, ElMessageBox } from "element-plus";
+
+const props = defineProps({ caseId: Number, title: String });
+const route = useRoute();
+
+const editing = ref({
+  show: false,
+  data: {},
+});
+const newlist = ref([]);
+const fileList = ref([]);
+const swiperRef = ref(null);
+const childRef = ref(null);
+const isSenseLoaded = ref(false);
+const caseId = ref(props.caseId);
+const parentId = computed(() => {
+  const val = route.query.parentId;
+  return typeof val === 'string' ? Number(val) : '';
+});
+const sortType = ref(false);
+const drawMode = ref(0);
+const isShowExitEdit = computed(() => drawMode.value > 0);
+const loadedDrawData = ref();
+const hasDrawData = ref(false);
+let scene = null;
+// 当前拖拽列表的所有图片ID集合
+const imgIds = ref([]);
+
+const addCaseFileHandler = async () => {
+  await addCaseImgFile({
+    caseId: caseId.value,
+    data: {
+      imgUrl: "",
+      imgInfo: "",
+      id: "",
+      sort: "",
+    },
+  });
+  refresh();
+};
+
+const addCaseFileHandlerAll = async () => {
+  await addCaseImgFileAll({
+    caseId: caseId.value,
+    parentId: parentId.value,
+    data: {
+      imgUrl: "",
+      imgInfo: "",
+      id: "",
+      sort: "",
+    },
+  });
+  refresh();
+};
+
+function refresh() {
+  console.log("changeList", childRef.value);
+
+  if (childRef.value) {
+    childRef.value.getList();
+  }
+}
+const changeList = async (list) => {
+  //同步数据
+  if (!loadedDrawData.value) {
+    const imgId = typeof route.query.imgId === 'string' ? Number(route.query.imgId) : undefined;
+    const res = await getCaseImgTagData(caseId.value, imgId);
+    if (res.data) {
+      if (res.data.data) {
+        loadedDrawData.value = res.data.data;
+      }
+      if ("isHorizontal" in res.data) {
+        // console.error("sortType.value", sortType.value, !res.data.isHorizontal);
+        sortType.value = !res.data.isHorizontal;
+      }
+    } else {
+      loadedDrawData.value = [];
+    }
+  }
+
+  // 记录当前列表的所有图片ID
+  try {
+    imgIds.value = Array.isArray(list) ? list.map((item) => item.id).filter((id) => id != null) : [];
+  } catch (e) {
+    imgIds.value = [];
+  }
+
+  let newList = [];
+  list.map((item, index) => {
+    if (sortType.value) {
+      newList.push([item]);
+    } else {
+      if (index % 2 == 0) {
+        let newItem = list[index + 1] ? [item, list[index + 1]] : [item];
+        newList.push(newItem);
+      }
+    }
+  });
+  newlist.value = newList;
+  const arr = [];
+  newList.map((i) => arr.push(JSON.parse(JSON.stringify(i))));
+
+  const type = sortType.value ? 2 : 1;
+
+  if (scene) {
+    scene.load(arr, type, loadedDrawData.value || []);
+    console.log("changeList", arr, type, loadedDrawData.value);
+  }
+};
+const renderCanvas = () => {
+  const canvas = document.getElementById("canvas");
+
+  scene = new Scene(canvas);
+  scene.init();
+  window.scene = scene;
+  scene.on("mode", (mode) => {
+    console.warn("mode", mode);
+    drawMode.value = mode;
+  });
+  scene.on("markerExist", () => {
+    ElMessage.error("该案件已有方向标注!");
+  });
+  scene.on("confirmDelete", async ({ id, type }) => {
+    const res = await ElMessageBox.confirm("是否删除该部件?", "温馨提示", {
+      confirmButtonText: "确定",
+      cancelButtonText: "取消",
+      type: "default",
+    });
+    if (res) {
+      window.scene.deleteItemById(id, type);
+    }
+  });
+  scene.on("data", (data) => {
+    let hasData = false;
+    Object.keys(data).forEach((key) => {
+      if (Array.isArray(data[key])) {
+        if (data[key].length > 0) {
+          hasData = true;
+        }
+      }
+    });
+    hasDrawData.value = hasData;
+    console.log("sync", data, hasData);
+    // editing.value.data = data;
+    loadedDrawData.value = data;
+  });
+  scene.on("edit", (editData) => {
+    console.log("editData", editData);
+    editing.value.show = true;
+    editing.value.data = editData;
+  });
+  scene.on("devicePixelRatio", () => {
+    ElMessage.error(
+      `当前浏览器缩放${Math.floor(
+        window.devicePixelRatio * 100
+      )}%比例,请设置缩放比例为100%,方可正常导出`
+    );
+  });
+  scene.on("loaded", () => {
+    isSenseLoaded.value = true;
+  });
+  scene.on("autoSave", () => {
+    console.log("autoSave");
+    handleAutoSave();
+  });
+  scene.on("submitScreenshot", (save) => {
+    if (window.scene) {
+      const params = {
+        files: window.scene.blobScreens.map(
+          (b, index) => new File([b], `${Date.now()}-${index}.jpg`)
+        ),
+        caseId: caseId.value,
+        parentId: parentId.value,
+        imgIds: (childRef?.value && typeof childRef.value.getImageIds === 'function')
+          ? childRef.value.getImageIds()
+          : imgIds.value,
+      };
+
+      setTimeout(async () => {
+        try {
+          const res = await submitMergePhotos(params);
+          console.log("res", res);
+          const { data, code } = res;
+          const title = `${props.title}-照片卷.jpg`;
+          if (data && data.imgUrl) {
+            if (save) {
+              // debugger;
+              // saveAs(data.imgUrl, title);
+            }
+          }
+          window.scene.endScreenshot();
+          isSenseLoaded.value = true;
+        } catch (error) {
+          window.scene.endScreenshot();
+          isSenseLoaded.value = true;
+        }
+      }, 500);
+    }
+  });
+};
+const onSwiper = (swiper) => {
+  console.log("onSwiper");
+  swiperRef.value = swiper;
+};
+const onSlideChange = (swiper) => {
+  console.log(swiper);
+};
+const handleChange = (val, list) => {
+  fileList.value = list;
+  console.log("handleChange", val, list, fileList.value);
+};
+const handleRequest = (val, list) => {
+  console.log("handleRequest", val, list);
+};
+const handleUpload = (val) => {
+  console.log("handleUpload", val);
+};
+const handleItem = (item) => {
+  let active = sortType.value ? item : Math.floor(item / 2);
+  // swiperRef.value.slideTo(active);
+  console.log("handleItem", item, active);
+};
+const handleDetele = async (item) => {
+  if (
+    await confirm("删除该场景,将同时从案件和融合模型中移除,确定要删除吗?")
+  ) {
+    const scenes = getCaseScenes(list.value.filter((item) => item !== scene));
+    await replaceCaseScenes(props.caseId, scenes);
+    refresh();
+  }
+};
+const handleSwitchGrid = async () => {
+  const res = await ElMessageBox.confirm(
+    "切换模版不包括标注内容,确定要切换吗?",
+    "温馨提示",
+    {
+      confirmButtonText: "确定",
+      cancelButtonText: "取消",
+      type: "default",
+    }
+  );
+  if (res) {
+    sortType.value = !sortType.value;
+    window.scene.setMode(0);
+    window.scene.clearScene();
+    handleClear();
+  }
+};
+
+const handleLine = () => {
+  if (window.scene) {
+    window.scene.setMode(1);
+  }
+};
+const handleMark = () => {
+  if (window.scene) {
+    window.scene.setMode(2);
+  }
+};
+
+const handleSymbol = () => {
+  if (window.scene) {
+    window.scene.setMode(3);
+  }
+};
+const handleText = () => {
+  if (window.scene) {
+    window.scene.setMode(4);
+  }
+};
+const handleSave = async () => {
+  if (window.scene) {
+    const data = scene.player.getDrawData();
+    scene.player.syncDrawData();
+    console.log("data", data);
+    const imgId = typeof route.query.imgId === 'string' ? Number(route.query.imgId) : undefined;
+    const res = await saveCaseImgTagData({
+      caseId: caseId.value,
+      imgId,
+      data: data,
+      isHorizontal: !sortType.value,
+    });
+
+    console.log("res", res);
+    // 保存后自动导出,不打开新链接
+    if (newlist.value.length > 0) {
+      if (!window.isExportScreenshot) {
+        isSenseLoaded.value = false;
+        // 传入 true 以执行保存文件,不打开新窗口
+        window.scene.exportScreenshot(true);
+      }
+    }
+    ElMessage.success("保存成功!");
+  }
+};
+
+const handleAutoSave = async () => {
+  if (window.scene) {
+    const data = scene.player.getDrawData();
+    scene.player.syncDrawData();
+    const imgId = typeof route.query.imgId === 'string' ? Number(route.query.imgId) : undefined;
+    await saveCaseImgTagData({
+      caseId: caseId.value,
+      imgId,
+      data: data,
+      isHorizontal: !sortType.value,
+    });
+  }
+};
+const handleFree = () => {
+  if (window.scene) {
+    window.scene.setMode(0);
+  }
+};
+const handleClear = () => {
+  if (window.scene) {
+    window.scene.player.clear();
+  }
+};
+
+const handleEditingUpdate = (data) => {
+  // console.log("update", data);
+  if (window.scene) {
+    window.scene.editing(data);
+  }
+};
+const handleEditingDel = (form) => {
+  if (window.scene) {
+    const { id, type } = form;
+    console.log("handleEditingDel", form);
+    window.scene.deleteItemById(id, type);
+    window.scene.setMode(0);
+  }
+};
+const handleEditingClose = () => {
+  window.scene.setMode(0);
+};
+
+const handleImageDel = (ids) => {
+  console.log("handleImageDel", ids);
+  if (window.scene) {
+    window.scene.deleteImageDataByIds(ids);
+  }
+};
+const handleExport = () => {
+  if (window.scene) {
+    if (!window.isExportScreenshot) {
+      isSenseLoaded.value = false;
+      window.scene.exportScreenshot();
+    }
+  }
+};
+
+onMounted(() => {
+  renderCanvas();
+  console.warn("renderCanvas");
+});
+</script>
+<style lang="scss">
+.my-photo-upload {
+  .upload-demo {
+    display: inline-block;
+    margin-right: 20px;
+    position: relative;
+    bottom: -1px;
+    .el-upload-list {
+      display: none;
+    }
+  }
+}
+</style>
+<style lang="scss" scoped>
+#canvas {
+  width: 100%;
+  height: 100%;
+}
+
+.photo {
+  display: flex;
+  height: 100%;
+
+  .left {
+    width: 260px;
+    padding: 16px 24px 30px 0;
+    height: calc(100% - 46.16px);
+    overflow-y: auto;
+    background: #ffffff;
+    box-shadow: 10px 0 10px -10px rgba(0, 0, 0, 0.15);
+    // box-shadow: 0px 2px 8px 0px rgba(0,0,0,0.15);
+  }
+
+  .right {
+    width: calc(100% - 260px);
+    background-color: var(--bgColor);
+    padding-left: 24px;
+    height: 100%;
+    position: relative;
+    display: block;
+
+    .tools {
+      position: absolute;
+      top: 15px;
+      left: 30px;
+    }
+
+    .swiperItem {
+      height: calc(100vh - 155.16px);
+      width: calc((100vh - 156.16px) * 0.707);
+      background: #ffffff;
+
+      .swiperList {
+        padding: 0 60px;
+        height: 100%;
+
+        .page {
+          font-weight: 400;
+          font-size: 12px;
+          color: rgba(0, 0, 0, 0.85);
+          line-height: 22px;
+          text-align: right;
+          margin-top: 30px;
+        }
+
+        .itemper {
+          height: calc(50% - 100px);
+          padding: 60px 0 0 0;
+
+          .text {
+            margin-top: 16px;
+            border-radius: 0px 0px 0px 0px;
+            border: 1px dotted #cccccc;
+            text-align: center;
+            font-family: Microsoft YaHei, Microsoft YaHei;
+            font-weight: 400;
+            font-size: 14px;
+            line-height: 30px;
+            color: rgba(0, 0, 0, 0.85);
+          }
+
+          .itemImg {
+            width: 100%;
+            height: calc(100% - 48px);
+            display: block;
+            object-fit: cover;
+          }
+        }
+
+        .oneItemper {
+          height: calc(100% - 120px);
+
+          .itemImg {
+          }
+        }
+      }
+    }
+  }
+}
+</style>
+<style scoped>
+:global(.body-layer) {
+  padding-right: 0 !important;
+  overflow: hidden;
+}
+.save {
+  background-color: #24569e;
+  color: white;
+  &:hover {
+    background-color: #14396c;
+    color: white;
+    border-color: #14396c;
+  }
+}
+.opt {
+  background-color: #deeafe;
+  color: #24569e;
+  border: #24569e 1px solid;
+  &:hover {
+    background-color: #bdcce6;
+    color: #14396c;
+    border-color: #14396c;
+  }
+}
+.tools {
+  i {
+    margin-right: 4px;
+  }
+}
+</style>

+ 8 - 1
src/view/case/photos/index.vue

@@ -83,6 +83,7 @@
 
 <script setup>
 import { onMounted, ref, computed, onUnmounted, reactive } from "vue";
+import { useRoute } from "vue-router";
 import { Menu, FullScreen } from "@element-plus/icons-vue";
 import { Swiper, SwiperSlide } from "swiper/vue";
 import "swiper/css";
@@ -100,6 +101,7 @@ import saveAs from "@/util/file-serve";
 import { ElMessage, ElMessageBox } from "element-plus";
 
 const props = defineProps({ caseId: Number, title: String });
+const route = useRoute();
 
 const editing = ref({
   show: false,
@@ -154,7 +156,8 @@ function refresh() {
 const changeList = async (list) => {
   //同步数据
   if (!loadedDrawData.value) {
-    const res = await getCaseImgTagData(caseId.value);
+    const imgId = typeof route.query.imgId === 'string' ? Number(route.query.imgId) : undefined;
+    const res = await getCaseImgTagData(caseId.value, imgId);
     if (res.data) {
       if (res.data.data) {
         loadedDrawData.value = res.data.data;
@@ -352,8 +355,10 @@ const handleSave = async () => {
     const data = scene.player.getDrawData();
     scene.player.syncDrawData();
     console.log("data", data);
+    const imgId = typeof route.query.imgId === 'string' ? Number(route.query.imgId) : undefined;
     const res = await saveCaseImgTagData({
       caseId: caseId.value,
+      imgId,
       data: data,
       isHorizontal: !sortType.value,
     });
@@ -371,8 +376,10 @@ const handleAutoSave = async () => {
   if (window.scene) {
     const data = scene.player.getDrawData();
     scene.player.syncDrawData();
+    const imgId = typeof route.query.imgId === 'string' ? Number(route.query.imgId) : undefined;
     await saveCaseImgTagData({
       caseId: caseId.value,
+      imgId,
       data: data,
       isHorizontal: !sortType.value,
     });

+ 4 - 2
src/view/case/quisk.ts

@@ -2,7 +2,8 @@ import addPhotoFile from "./addPhotoFile.vue";
 import addPhotoFileAll from "./addPhotoFileAll.vue";
 import AddCaseFile from "./addCaseFile.vue";
 import AddScenes from "./addScenes.vue";
-import ShareCase from "./share.vue";
+// import ShareCase from "./share.vue";
+import ShareCase from "./newShare.vue";
 import EditEshapeTable, {
   EshapeTableContent,
 } from "./draw/editEshapeTable.vue";
@@ -62,8 +63,9 @@ export const selectMapImage = quiskMountFactory(SelectMapImage, {
 })<MapImage>;
 
 export const shareCase = quiskMountFactory(ShareCase, {
-  title: "分享",
+  title: "权限",
   enterText: "复制链接及密码",
+  width: 750,
 })<string>;
 
 export type caseDownloadProps = { caseId: number; title: string };

+ 2 - 2
src/view/case/share.vue

@@ -31,7 +31,7 @@ import { Fire, getFire } from "@/app/fire/store/fire";
 import { getCaseSharePWD, setCaseSharePWD } from "@/store/case";
 import { QuiskExpose } from "@/helper/mount";
 
-const props = defineProps<{ caseId: number }>();
+const props = defineProps<{ caseId: number, projectName?: string }>();
 const randCode = ref("");
 const oldRandCode = ref("");
 
@@ -59,7 +59,7 @@ defineExpose<QuiskExpose>({
         randCode: randCode.value,
       });
     }
-    const result = `链接:${shareLink.value} 密码:${randCode.value}`;
+    const result = `起火对象:${props.projectName || ''} 链接:${shareLink.value} 密码:${randCode.value}`;
     copyText(result);
     ElMessage.success("链接复制成功");
     return result;

+ 19 - 14
src/view/newFireCase/dyManager/index.vue

@@ -1,20 +1,23 @@
 <template>
   <List :params="params">
     <template v-slot:header>
+      <el-form-item label="所属架构:" v-if="params.pagging.state.query.searchType === '2'">
+        <com-select v-model="params.pagging.state.query.deptId" />
+      </el-form-item>
       <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-select placeholder="请选择" v-model="params.pagging.state.query.cameraType">
           <el-option
-            v-for="option in deviceTypeList"
-            :key="option.value"
-            :value="option.value"
-            :label="option.name"
+            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-form-item label="S/N码:">
         <el-input
           v-model="params.pagging.state.query.snCode"
           placeholder="请输入"
@@ -28,19 +31,21 @@
 </template>
 
 <script setup lang="ts">
+import comSelect from "@/components/company-select/index.vue";
 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";
+import { computed, onMounted, ref } from "vue";
+import { getCameraTypeAllList } from "@/store/scene";
 
-const deviceTypeList = [
-  { value: 2, name: '激光转台' },
-  { value: 5, name: '激光移动' },
-  { value: 6, name: '激光手持' },
-];
+const cameraTypeList = ref([])
+onMounted(() => {
+  getCameraTypeAllList().then(res => {
+    cameraTypeList.value = res || []
+  })
+})
 const params = useScenePaggingParams();
-const component = computed(() => (params.isSwmx ? ModelContent : SceneContent));
+const component = computed(() => (SceneContent));
 </script>

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

@@ -1,192 +0,0 @@
-<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>

+ 15 - 14
src/view/newFireCase/dyManager/pagging.ts

@@ -6,40 +6,41 @@ export const useScenePaggingParams = () => {
   const pagging = usePagging({
     get: getScenePagging,
     paramsTemlate: {
-      type: SceneType.SWSS, // 这里先填默认值,到时更改接口后再换成空值
       searchType: "0",
+      cameraType: '',
       sceneName: "",
       modelTitle: "",
       deptId: "",
       snCode: "",
+      isObj: 0,
     },
   });
 
-  const isSwmx = computed(() => pagging.state.query.type === SceneType.SWMX);
   const keyword = computed({
-    get: () =>
-      isSwmx.value
-        ? pagging.state.query.modelTitle
-        : pagging.state.query.sceneName,
+    get: () => 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;
+    pagging.state.query.snCode = oldSnCode;
+  });
+
+  // 当搜索类型切换为列表/共享(非“全部”)时,组织架构传空
+  watchEffect(() => {
+    if (pagging.state.query.searchType !== "2") {
+      pagging.state.query.deptId = "";
     }
   });
 
   watch(
-    () => pagging.state.query.type,
+    () => pagging.state.query.searchType,
     () => {
+      pagging.state.query.deptId = "";
+      pagging.state.query.snCode = "";
+      pagging.state.query.sceneName = "";
       pagging.state.pag.currentPage = 1;
     }
   );
@@ -51,6 +52,6 @@ export const useScenePaggingParams = () => {
     pagging.state.query.searchType = type;
   };
 
-  return reactive({ pagging, keyword, isSwmx });
+  return reactive({ pagging, keyword });
 };
 export type ScenePagging = ReturnType<typeof useScenePaggingParams>["pagging"];

+ 7 - 52
src/view/newFireCase/dyManager/sceneContent.vue

@@ -25,70 +25,25 @@
       {{ 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"
-      >
+    <el-table-column label="操作" v-slot:default="{ row }: { row: QuoteScene }" width="300px">
+      <span class="oper-span" @click="downHash(row)">
         Hash
       </span>
-      <span
-        class="oper-span"
-        @click="copySceneHandler(row)"
-        v-if="row.status === QuoteSceneStatus.SUCCESS"
-      >
+      <span class="oper-span" @click="copySceneHandler(row)">
         复制
       </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 class="oper-span"  @click="openSceneUrl(row, OpenType.edit)">
         编辑
       </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)"
-      >
+      <!-- <span 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 delBtn" @click="delSceneHandler(row)">
         删除
       </span>
       <span
         class="oper-span"
         @click="sceneDownloadHandler(row)"
-        v-pdpath="['download']"
-        v-if="row.num && row.status === QuoteSceneStatus.SUCCESS"
       >
         下载
       </span>

+ 4 - 1
src/view/newFireCase/meshManager/index.vue

@@ -1,6 +1,9 @@
 <template>
   <List :params="params">
     <template v-slot:header>
+      <el-form-item label="所属架构:" v-if="params.pagging.state.query.searchType === '2'">
+        <com-select v-model="params.pagging.state.query.deptId" />
+      </el-form-item>
       <el-form-item label="标题:">
         <el-input v-model="params.keyword" placeholder="请输入"></el-input>
       </el-form-item>
@@ -28,9 +31,9 @@
 </template>
 
 <script setup lang="ts">
+import comSelect from "@/components/company-select/index.vue";
 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";

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

@@ -1,192 +0,0 @@
-<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>

+ 15 - 16
src/view/newFireCase/meshManager/pagging.ts

@@ -9,50 +9,49 @@ export const useScenePaggingParams = () => {
       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,
+    get: () => 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;
+    pagging.state.query.snCode = oldSnCode;
+  });
+
+  // 当搜索类型切换为列表/共享(非“全部”)时,组织架构传空
+  watchEffect(() => {
+    if (pagging.state.query.searchType !== "2") {
+      pagging.state.query.deptId = "";
     }
   });
 
   watch(
-    () => pagging.state.query.type,
+    () => pagging.state.query.searchType,
     () => {
+      pagging.state.query.deptId = "";
+      pagging.state.query.snCode = "";
+      pagging.state.query.sceneName = "";
       pagging.state.pag.currentPage = 1;
     }
   );
 
   const queryResetRaw = pagging.queryReset;
   pagging.queryReset = () => {
-    const type = pagging.state.query.searchType;
+    const searchType = pagging.state.query.searchType;
     queryResetRaw();
-    pagging.state.query.searchType = type;
+    pagging.state.query.searchType = searchType; 
   };
 
-  return reactive({ pagging, keyword, isSwmx });
+  return reactive({ pagging, keyword });
 };
 export type ScenePagging = ReturnType<typeof useScenePaggingParams>["pagging"];
 

+ 14 - 53
src/view/newFireCase/meshManager/sceneContent.vue

@@ -15,7 +15,13 @@
         {{ 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="场景标题" prop="name">
+      <template v-slot:default="{ row }">
+        <span class="oper-span" @click="openSceneUrl(row, OpenType.query)">
+          {{ row.name }}
+        </span>
+      </template>
+    </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 }">
@@ -25,70 +31,25 @@
       {{ 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"
-      >
+    <el-table-column label="操作" v-slot:default="{ row }: { row: QuoteScene }" width="300px">
+      <span class="oper-span" @click="downHash(row)">
         Hash
       </span>
-      <span
-        class="oper-span"
-        @click="copySceneHandler(row)"
-        v-if="row.status === QuoteSceneStatus.SUCCESS"
-      >
+      <span class="oper-span" @click="copySceneHandler(row)">
         复制
       </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 class="oper-span"  @click="openSceneUrl(row, OpenType.edit)">
         编辑
       </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)"
-      >
+      <!-- <span 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 delBtn" @click="delSceneHandler(row)">
         删除
       </span>
       <span
         class="oper-span"
         @click="sceneDownloadHandler(row)"
-        v-pdpath="['download']"
-        v-if="row.num && row.status === QuoteSceneStatus.SUCCESS"
       >
         下载
       </span>

+ 5 - 1
src/view/newFireCase/mix3dManager/index.vue

@@ -1,8 +1,11 @@
 <template>
   <List :params="params">
     <template v-slot:header>
+      <el-form-item label="所属架构:" v-if="params.pagging.state.query.searchType === '2'">
+        <com-select v-model="params.pagging.state.query.deptId" />
+      </el-form-item>
       <el-form-item label="标题:">
-        <el-input v-model="params.pagging.state.query.modelTitle" placeholder="请输入"></el-input>
+        <el-input v-model="params.pagging.state.query.fusionTitle" placeholder="请输入"></el-input>
       </el-form-item>
     </template>
     <template v-slot:content>
@@ -12,6 +15,7 @@
 </template>
 
 <script setup lang="ts">
+import comSelect from "@/components/company-select/index.vue";
 import List from "./list.vue";
 import SceneContent from "./sceneContent.vue";
 import { SceneType } from "@/store/scene";

+ 5 - 4
src/view/newFireCase/mix3dManager/pagging.ts

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

+ 6 - 10
src/view/newFireCase/mix3dManager/sceneContent.vue

@@ -11,31 +11,27 @@
   >
     <el-table-column label="场景标题" prop="name">
       <template v-slot:default="{ row }">
-        <span @click="openSceneUrl(row, OpenType.query)">{{ row.name }}</span>
+        <span class="oper-span" @click="openSceneUrl(row, OpenType.query)">{{ row.fusionTitle }}</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] }}
+      {{ row.createUserName }}
     </el-table-column>
     <el-table-column label="拍摄时间" prop="createTime" v-slot:default="{ row }">
-      {{ row.createTime.substr(0, 16) }}
+      {{ row.createTime }}
     </el-table-column>
     <el-table-column
       label="操作"
       v-slot:default="{ row }: { row: QuoteScene }"
-      width="400px"
+      width="200px"
     >
-      <span class="oper-span" v-pdpath="['edit']" @click="openSceneUrl(row, OpenType.edit)" v-if="row.status === QuoteSceneStatus.SUCCESS">
+      <span class="oper-span" @click="openSceneUrl(row, OpenType.edit)">
         编辑
       </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 class="oper-span delBtn" @click="delSceneHandler(row)">
         删除
       </span>
     </el-table-column>

+ 1 - 1
src/view/newFireCase/newFireDetails/components/Toolbar.vue

@@ -28,7 +28,7 @@ watch(() => props.showBottomBar, (n, o) => {
     justify-content: center;
     flex: 1;
     height:60px;
-    background-color: rgba(27,27,28,.8);
+    background-color: #fff;
     pointer-events: all;
     left: 0;
     z-index: 2;

+ 209 - 128
src/view/newFireCase/newFireDetails/components/basicInfo.vue

@@ -1,97 +1,125 @@
 <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 v-if="props.editOrShow === 'show'">
+      <!-- criminal 展示 -->
+      <div v-if="props.fromRoute === 'criminal'" class="camera-from show-view">
+        <div class="form-title">案件信息{{ props.fromRoute }}</div>
+        <div class="info-row"><span class="label">案件名称:</span><span class="value">{{ bindFire?.caseTitle || '-' }}</span></div>
+        <div class="info-row"><span class="label">详细地址:</span><span class="value">{{ bindFire?.mapUrl || '-' }}</span></div>
+      </div>
+      <!-- fire 展示(原有) -->
+      <div v-else 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>
     </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>
+    <!-- 编辑模式:按来源路由分支展示 -->
+    <template v-else>
+      <!-- criminal 编辑(复原 edit.vue) -->
+      <el-form v-if="props.fromRoute === 'criminal'" ref="form" label-width="84px" class="camera-from">
+        <div class="form-title">案件信息</div>
+        <el-form-item label="案件名称">
+          <el-input v-model="bindFire.caseTitle" maxlength="50" placeholder="请输入案件名称" />
+        </el-form-item>
+        <el-form-item label="详细地址">
+          <el-input v-model="bindFire.mapUrl" placeholder="输入名称搜索" clearable disabled>
+            <template #append>
+              <el-button :icon="Search" @click="searchAMapAddress" />
+            </template>
+          </el-input>
+        </el-form-item>
+      </el-form>
 
-      <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>
+      <!-- fire 编辑(原有) -->
+      <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>
+    </template>
   </div>
-  <creatMap v-model="showMapDialog" :caseId="caseId" @confirm="handleMapConfirm" />
+  <!-- 地图弹窗仅 fire 使用 -->
+  <creatMap v-if="props.fromRoute !== 'criminal'" 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 { ref, watch, toRef, onMounted, onUnmounted } from "vue";
 import { Fire, setFire, addFire } from "@/app/fire/store/fire";
 import { reason, place } from "@/app/fire/constant/fire";
 import { ElMessage } from "element-plus";
@@ -101,11 +129,13 @@ import { QuiskExpose } from "@/helper/mount";
 import { user } from "@/store/user";
 import { Search } from "@element-plus/icons-vue";
 import { selectMapImage } from "@/view/case/quisk";
+import { Example, setExample as setCriminalExample, addExample as addCriminalExample } from "@/app/criminal/store/example";
+import { getCaseInfo } from "@/store/case";
 import creatMap from "./creatMap.vue";
 
-const props = defineProps<{ fire?: Fire, caseId?: number, editOrShow?: string }>();
+const props = defineProps<{ fire?: Fire, caseId?: number, editOrShow?: string, fromRoute?: string }>();
 const caseId = toRef(props, 'caseId');
-
+console.log(props.fromRoute, 'props.fromRoute')
 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()
@@ -114,6 +144,7 @@ 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");
@@ -153,6 +184,8 @@ const getSnapshot = () => {
 };
 
 const autoSave = async () => {
+  // criminal 路由不触发 fire 自动保存
+  if (props.fromRoute === 'criminal') return;
   if (!isValidForAutoSave()) return;
   const snapshot = getSnapshot();
   if (snapshot === lastSavedSnapshot) return;
@@ -168,6 +201,11 @@ const autoSave = async () => {
       await addFire(bindFire.value as any);
     }
     lastSavedSnapshot = snapshot;
+    // 保存成功后派发标题更新事件,供父组件同步 currentRecord
+    try {
+      const title = props.fromRoute === 'criminal' ? (bindFire.value as any).caseTitle : (bindFire.value as any).projectName;
+      window.dispatchEvent(new CustomEvent('fireDetails:updateTitle', { detail: { title } }));
+    } catch (e) {}
     // 自动保存成功后不刷新页面,也不打扰用户
   } catch (e) {
     // 自动保存失败不打断填写,可在控制台查看错误
@@ -182,56 +220,99 @@ 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 "火灾原因不能为空!";
+// 监听来自 header 的重命名事件,并直接触发保存以更新标题
+onMounted(() => {
+  const renameHandler = (evt: any) => {
+    const title = evt?.detail?.title || '';
+    if (!title || !String(title).trim()) return;
+    if (props.fromRoute === 'criminal') {
+      (bindFire.value as any).caseTitle = title;
+      // criminal 不自动保存,这里不触发 autoSave
+    } else {
+      (bindFire.value as any).projectName = title;
+      // fire 路由:直接调用 autoSave 立即保存
+      autoSave();
     }
+  };
+  window.addEventListener('fireDetails:renameTitle', renameHandler as any);
+  onUnmounted(() => {
+    window.removeEventListener('fireDetails:renameTitle', renameHandler as any);
+  });
+});
 
-    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);
+defineExpose<QuiskExpose>({
+  async submit() {
+    if (props.fromRoute === 'criminal') {
+      if (!bindFire.value.caseTitle || !bindFire.value.caseTitle.trim()) {
+        ElMessage.error("案件名称不能为空");
+        throw "案件名称不能为空";
+      } else if (!bindFire.value.latAndLong || !bindFire.value.latAndLong.trim()) {
+        ElMessage.error("详细地址不能为空");
+        throw "详细地址不能为空!";
+      }
+      await (bindFire.value.caseId
+        ? setCriminalExample(bindFire.value)
+        : addCriminalExample(bindFire.value as any));
     } else {
-      await addFire(bindFire.value as any);
+      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()
     }
-    
-    // 保存成功后,刷新fireDetails页面的数据
-    // 通过事件总线或全局事件触发刷新
-    window.location.reload()
   },
 });
 
-// 打开地图弹窗
+// 打开地图/选择地址
 const searchAMapAddress = async () => {
-  showMapDialog.value = true
+  if (props.fromRoute === 'criminal') {
+    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;
+    }
+  } else {
+    showMapDialog.value = true
+  }
 };
 // 处理地图确认选择
 const handleMapConfirm = (LocationInfo: any) => {

+ 55 - 6
src/view/newFireCase/newFireDetails/components/headerTop.vue

@@ -1,11 +1,15 @@
 <template>
   <div class="new-header">
     <div class="left-title">
-      <span class="edit-title">编辑<span class="line">|</span></span><span>起火场所</span>
+      <el-icon v-if="showSave" class="back-icon" @click="$emit('back')">
+        <ArrowLeft />
+      </el-icon>
+      <span v-if="isPreview != 'abstract'" class="edit-title">编辑<span v-if="isPreview != 'abstract'" class="line">|</span></span><span>{{ headerTitle }}</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>
+      <span class="change-btn" v-if="!showSave" @click="openRenameDialog"><i class="iconfont icon-rename" /></span>
+      <el-button plain v-if="!showSave" type="primary" @click="preview">预览</el-button>
+      <el-button type="primary" v-if="showSave" @click="$emit('save')">保存</el-button>
     </div>
   </div>
   <el-dialog
@@ -19,32 +23,66 @@
     </div>
     <template #footer>
       <el-button plain type="primary" @click="renameVisible = false">取消</el-button>
-      <el-button type="primary" @click="renameVisible = false">确定</el-button>
+      <el-button type="primary" @click="confirmRename">确定</el-button>
     </template>
 </el-dialog>
 </template>
 
 <script setup lang="ts">
 import { ref, computed, onMounted } from "vue";
+import { ArrowLeft } from "@element-plus/icons-vue";
 import { useRoute, useRouter } from 'vue-router';
+import { RouteName, router } from "@/router";
+import { ElMessage } from "element-plus";
 const props = defineProps<{
   caseId: number;
   currentRecord: object;
   editOrShow: string;
+  showSave?: boolean;
 }>();
+const route = useRoute();
+const vueRouter = useRouter();
 const renameVisible = ref(false);
 const renameTitle = ref("");
+const isPreview = computed(() => route.query.preview as string || '');
+const fromRoute = computed(() => route.query.fromRoute as string || '');
+// 标题:fire 使用 tmProject.projectName;criminal 使用 caseTitle
+const headerTitle = computed(() => {
+  const cr: any = props.currentRecord || {};
+  return fromRoute.value === 'fire' ? (cr?.tmProject?.projectName || '') : (cr?.caseTitle || '');
+});
 const openRenameDialog = () => {
   renameVisible.value = true;
   console.log(props.currentRecord)
-  renameTitle.value = props.currentRecord.caseTitle || "";
+  const cr: any = props.currentRecord || {};
+  renameTitle.value = fromRoute.value === 'fire' ? (cr?.tmProject?.projectName || '') : (cr?.caseTitle || '');
+}
+const preview = () => {
+  const routeData = router.resolve({
+    // path: `/fire/dispatch/fireDetails/${caseId}`,
+    path: `/fireDetails/${props.caseId}`, 
+    query: { editOrShow: 'show', fromRoute: fromRoute.value, preview: 'abstract' }
+  });
+  window.open(routeData.href, '_blank');
+}
+const confirmRename = () => {
+  const title = (renameTitle.value || '').trim();
+  if (!title) {
+    ElMessage.warning('标题不能为空');
+    return;
+  }
+  try {
+    window.dispatchEvent(new CustomEvent('fireDetails:renameTitle', { detail: { title } }));
+  } catch (e) {}
+  renameVisible.value = false;
 }
 onMounted(() => {
   
 });
 
 const emit = defineEmits<{
-  
+  save: [],
+  back: []
 }>()
 
 </script>
@@ -59,6 +97,17 @@ const emit = defineEmits<{
   .left-title{
     font-size: 32px;
     color: #303133;
+    display: flex;
+    align-items: center;
+    .back-icon{
+      cursor: pointer;
+      margin-right: 16px;
+      font-size: 24px;
+      color: #909399;
+      &:hover{
+        color: #606266;
+      }
+    }
     .edit-title{
       margin-right: 16px;
     }

文件差異過大導致無法顯示
+ 225 - 27
src/view/newFireCase/newFireDetails/components/mix3d.vue


文件差異過大導致無法顯示
+ 229 - 78
src/view/newFireCase/newFireDetails/components/scene.vue


+ 153 - 11
src/view/newFireCase/newFireDetails/components/screenShot.vue

@@ -13,19 +13,20 @@
         >
           <div class="file-left" @click="handleView(file)">
             <div class="file-avatar">
-              <img :src="file.videoFolderCover" alt="">
+              <img v-if="file.videoFolderCover" :src="file.videoFolderCover" alt="">
               <i class="iconfont icon-play play-icon" />
             </div>
             <span class="file-name" :title="file.filesTitle">{{ file.videoFolderName }}</span>
+            <span class="file-tip" v-if="isProcessing(file)">(后台正在处理...)</span>
           </div>
-          <div class="file-actions" v-if="editOrShow === 'edit'">
-            <el-tooltip content="查看" placement="top">
-              <el-button link @click.stop="handleView(file)">
+          <div class="file-actions" v-if="editOrShow === 'edit' && !isProcessing(file)">
+            <el-tooltip content="更改" placement="top">
+              <el-button link @click.stop="handleRename(file)">
                 <i class="iconfont icon-rename" />
               </el-button>
             </el-tooltip>
             <el-tooltip content="继续录制" placement="top">
-              <el-button link @click.stop="handleView(file)">
+              <el-button link @click.stop="handleContinue(file)">
                 <i class="iconfont icon-record" />
               </el-button>
             </el-tooltip>
@@ -49,10 +50,14 @@
 <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';
+import { getRecordCaseVideo, getUploadRecordProgress } from '@/store/editCsae';
+import { getResource, baseURL } from '@/store/system';
+import { saveAs } from '@/util/file-serve';
+import { axios, deleteRecord as deleteRecordUrl } from '@/request';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { updateRecordInfo } from '@/store/editCsae';
 
-const props = defineProps<{ fire?: any, caseId?: number, editOrShow?: string }>();
+const props = defineProps<{ fire?: any, caseId?: number, editOrShow?: string, processingIds?: (number | string)[], recentAddedItem?: any | null }>();
 const emit = defineEmits<{
   'start': any,
   'playVideo': [value: string | Blob],
@@ -62,6 +67,51 @@ const startShot = () => {
 }
 const files = ref<any[]>([]);
 const loading = ref(false);
+// 处理中的判定:uploadStatus 为 0;若没有明确状态,则参考父组件 processingIds
+const isProcessing = (file: any) => {
+  const id = file?.videoFolderId || file?.filesId || file?.id;
+  const ids = props.processingIds || [];
+  const byStatus = file?.uploadStatus === 0;
+  const hasExplicitStatus = typeof file?.uploadStatus === 'number';
+  return byStatus || (!hasExplicitStatus && id !== undefined && id !== null && ids.includes(id));
+};
+
+// 轮询定时器表
+const pollingTimers = new Map<any, any>();
+const startProgressPolling = (folderId: number | string) => {
+  if (pollingTimers.has(folderId)) return;
+  const timer = setInterval(async () => {
+    try {
+      const res: any = await getUploadRecordProgress({ folderId });
+      const percent = typeof res?.data === 'number' ? res.data : (res || 0);
+      if (percent >= 100) {
+        clearInterval(timer);
+        pollingTimers.delete(folderId);
+        // 更新对应项的状态为 1
+        const idx = files.value.findIndex(f => (f?.videoFolderId || f?.filesId || f?.id) === folderId);
+        if (idx >= 0) files.value[idx].uploadStatus = 1;
+        // 轮询结束后主动刷新列表,获取最新的合并结果
+        fetchRecordList();
+      }
+    } catch (e) {
+      console.error('进度轮询失败', e);
+    }
+  }, 2000);
+  pollingTimers.set(folderId, timer);
+};
+
+// 追加最近新增项到列表(避免重复)并启动轮询
+watch(() => props.recentAddedItem, (item) => {
+  if (!item) return;
+  const id = item?.videoFolderId || item?.filesId || item?.id;
+  const existIndex = files.value.findIndex(f => (f?.videoFolderId || f?.filesId || f?.id) === id);
+  if (existIndex >= 0) {
+    files.value[existIndex] = { ...files.value[existIndex], ...item, uploadStatus: 0 };
+  } else {
+    files.value.unshift({ ...item, uploadStatus: 0 });
+  }
+  if (id !== undefined && id !== null) startProgressPolling(id);
+});
 
 // 获取录制列表
 const fetchRecordList = async () => {
@@ -70,7 +120,15 @@ const fetchRecordList = async () => {
   try {
     const res = await getRecordCaseVideo({ caseId: props.caseId });
     const list = Array.isArray(res) ? res : (res?.data ?? res?.list ?? []);
-    files.value = list || [];
+    files.value = (list || []).map((f: any) => ({
+      ...f,
+      uploadStatus: typeof f?.uploadStatus === 'number' ? f.uploadStatus : (typeof f?.status === 'number' ? f.status : undefined),
+    }));
+    // 拉取列表后,对 uploadStatus=0 的项进行轮询
+    (files.value || []).forEach(f => {
+      const id = f?.videoFolderId || f?.filesId || f?.id;
+      if (f?.uploadStatus === 0 && id !== undefined && id !== null) startProgressPolling(id);
+    });
   } catch (e) {
     console.error('获取录制列表失败', e);
   } finally {
@@ -92,8 +150,92 @@ const handleView = (file: any) => {
   const url = typeof rawUrl === 'string' ? getResource(rawUrl) : rawUrl;
   emit('playVideo', url);
 };
-const handleDownload = (file: any) => {};
-const handleDelete = (file: any) => {};
+// 继续录制:将当前文件信息抛到上层以便进入录制弹窗
+const handleContinue = (file: any) => {
+  emit('start', {
+    videoFolderId: file?.videoFolderId || file?.filesId || file?.id,
+    videoFolderName: file?.videoFolderName || file?.filesTitle || '讲解视频',
+    videoMergeUrl: file?.videoMergeUrl,
+    videoFolderCover: file?.videoFolderCover,
+  });
+};
+// 更改视频名称
+const handleRename = async (file: any) => {
+  try {
+    const currentName = file?.videoFolderName || file?.filesTitle || '';
+    const { value, action } = await ElMessageBox.prompt('请输入新的视频名称', '更改名称', {
+      inputValue: currentName,
+      inputPlaceholder: '视频名称',
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      inputPattern: /.+/,
+      inputErrorMessage: '名称不可为空',
+    }) as any;
+    if (action !== 'confirm') return;
+    console.log(file, 8888)
+    const id = file?.videoFolderId;
+    if (!id) {
+      ElMessage.error('无法识别文件ID');
+      return;
+    }
+    
+    await updateRecordInfo({
+      videoFolderId: id,
+      videoFolderName: String(value).trim(),
+      sort: file?.sort,
+      uploadStatus: file?.uploadStatus,
+      videoFolderCover: file?.videoFolderCover,
+      videoMergeUrl: file?.videoMergeUrl,
+    });
+    ElMessage.success('名称已更新');
+    // 刷新列表以展示最新名称
+    fetchRecordList();
+  } catch (e) {
+    if (String(e).includes('cancel')) return;
+    console.error('更改名称失败', e);
+    ElMessage.error('更改名称失败');
+  }
+};
+// 下载合并后的视频(通过 Blob 流下载)
+const handleDownload = async (file: any) => {
+  const rawUrl = file?.videoMergeUrl;
+  if (!rawUrl) {
+    ElMessage.warning('暂无可下载的视频');
+    return;
+  }
+  const url = typeof rawUrl === 'string' ? getResource(rawUrl) : rawUrl;
+  try {
+    const defaultName = `${file?.videoFolderName || file?.filesTitle || '录制视频'}.mp4`;
+    // 使用 axios 以 Blob 流下载;通过 ingoreRes 绕过响应体 code 校验
+    const blob: Blob = await axios.get(url, {
+      responseType: 'blob',
+      params: { ingoreRes: true },
+      // 防止将 ingoreRes 追加到真实资源 URL 上导致签名或鉴权失败
+      paramsSerializer: { serialize: () => '' } as any,
+    });
+    await saveAs(blob, defaultName);
+  } catch (e) {
+    console.error('下载失败', e);
+    ElMessage.error('下载失败');
+  }
+};
+
+// 删除录制文件夹(后端删除)
+const handleDelete = async (file: any) => {
+  const id = file?.videoFolderId || file?.filesId || file?.id;
+  if (!id) {
+    ElMessage.error('无法识别文件ID');
+    return;
+  }
+  try {
+    await axios.post(deleteRecordUrl, { videoFolderId: id });
+    ElMessage.success('删除成功');
+    fetchRecordList();
+  } catch (e) {
+    console.error('删除失败', e);
+    ElMessage.error('删除失败');
+  }
+};
 </script>
 <style scoped lang="scss">
 .screen-shot-container {

+ 15 - 27
src/view/newFireCase/newFireDetails/components/shot.vue

@@ -10,18 +10,10 @@
         >合并视频</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"
-        /> -->
+        <span @click="start" class="other-icon" style="color: #000;"><i class="iconfont icon-record" />继续录制</span>
       </div>
       <div class="video-list" v-if="videoList.length">
-        <div class="layout" :style="{ width: `${videoList.length * 130}px` }">
+        <div class="shot-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>
@@ -345,24 +337,19 @@ onUnmounted(() => {
   }
 
   .other {
+    width: 100vw;
+    height: 56px;
     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;
+    background-color: #fff;
+    .other-icon{
       display: flex;
+      height: 100%;
       align-items: center;
       justify-content: center;
-
+      font-size: 14px;
+      color: #000;
+      gap: 8px;
     }
   }
 }
@@ -375,10 +362,11 @@ onUnmounted(() => {
   width: 100%;
   height: 120px;
   overflow-x: auto;
-  background-color: rgba(27, 27, 28, .8);
-  border-bottom: 1px solid rgba(255,255,255,0.1600);
+  background-color: #fff;
+  border-bottom: 1px solid #E6E6E6;
+  border-top: 1px solid #E6E6E6;
 
-  .layout {
+  .shot-layout {
     display: flex;
     align-items: center;
     height: 100%;
@@ -398,7 +386,7 @@ onUnmounted(() => {
       right: 0;
       content: '';
       z-index: 2;
-      background: rgba(0,0,0,0.5);
+      background: rgba(0,0,0,0.3);
     } 
     .preview {
       position: absolute;

+ 325 - 125
src/view/newFireCase/newFireDetails/components/siteInspection.vue

@@ -48,7 +48,7 @@
                       </span>
                     </el-tooltip>
                     <el-tooltip content="编辑" placement="top">
-                      <span class="action-icon" @click="editSelected">
+                      <span class="action-icon" @click="editSelected('tabulation')">
                         <el-icon><Edit /></el-icon>
                       </span>
                     </el-tooltip>
@@ -95,7 +95,7 @@
                         </span>
                       </el-tooltip>
                       <el-tooltip content="编辑" placement="top">
-                        <span class="action-icon" @click="editSelected">
+                        <span class="action-icon" @click="editSelected('overview')">
                           <el-icon><Edit /></el-icon>
                         </span>
                       </el-tooltip>
@@ -111,13 +111,8 @@
               </div>
             </div>
           </div>
-          <!-- 地图选择弹窗 -->
-          <CreatMap 
-            v-model="showMapDialog" 
-            :caseId="caseId" 
-            @confirm="handleMapConfirm" 
-          />
-        </template>
+          
+          </template>
 
         <!-- 勘验笔录:列表 -->
         <template v-else-if="activeTab === 'inspection'">
@@ -140,17 +135,17 @@
                   <span class="name">{{ rec.title }}</span>
                   <div class="header-actions" v-if="editOrShow === 'edit'">
                       <el-tooltip content="下载" placement="top">
-                        <span class="action-icon" @click="renameSelected">
+                        <span class="action-icon" @click="downloadSelected">
                           <el-icon><Download /></el-icon>
                         </span>
                       </el-tooltip>
                       <el-tooltip content="编辑" placement="top">
-                        <span class="action-icon" @click="editSelected">
+                        <span class="action-icon" @click="onEditInspection">
                           <el-icon><EditPen /></el-icon>
                         </span>
                       </el-tooltip>
                       <el-tooltip content="删除" placement="top">
-                        <span class="action-icon" @click="deleteSelected">
+                        <span class="action-icon" @click="deleteItem">
                           <el-icon><CircleClose /></el-icon>
                         </span>
                       </el-tooltip>
@@ -168,8 +163,8 @@
             <div class="block-header">
               <!-- <div class="title">提取清单</div> -->
               <div class="actions" v-if="editOrShow === 'edit'">
-                <div class="action-item" @click="onFillInspection"><el-icon size="16"><Edit /></el-icon>填写</div>
-                <div class="action-item" @click="onUploadInspection"><el-icon size="16"><Upload /></el-icon>上传</div>
+                <div class="action-item" @click="onFillExtraction"><el-icon size="16"><Edit /></el-icon>填写</div>
+                <div class="action-item" @click="onUploadExtraction"><el-icon size="16"><Upload /></el-icon>上传</div>
               </div>
             </div>
             <div class="block-body">
@@ -183,17 +178,17 @@
                   <span class="name">{{ rec.title }}</span>
                   <div class="header-actions" v-if="editOrShow === 'edit'">
                     <el-tooltip content="下载" placement="top">
-                      <span class="action-icon" @click="renameSelected">
+                      <span class="action-icon" @click="downloadSelected">
                         <el-icon><Download /></el-icon>
                       </span>
                     </el-tooltip>
                     <el-tooltip content="编辑" placement="top">
-                      <span class="action-icon" @click="editSelected">
+                      <span class="action-icon" @click="onEditExtraction">
                         <el-icon><EditPen /></el-icon>
                       </span>
                     </el-tooltip>
                     <el-tooltip content="删除" placement="top">
-                      <span class="action-icon" @click="deleteSelected">
+                      <span class="action-icon" @click="deleteItem">
                         <el-icon><CircleClose /></el-icon>
                       </span>
                     </el-tooltip>
@@ -209,9 +204,9 @@
         <template v-else-if="activeTab === 'album'">
           <div class="content-block">
             <div class="block-header">
-              <div class="title">照片卷</div>
+              <!-- <div class="title">照片卷</div> -->
               <div class="actions" v-if="editOrShow === 'edit'">
-                <div class="action-item" @click="onFillInspection"><el-icon size="16"><Edit /></el-icon>制卷</div>
+                <div class="action-item" @click="openPhotoEdit()"><el-icon size="16"><Edit /></el-icon>制卷</div>
               </div>
             </div>
             <div class="block-body">
@@ -225,17 +220,17 @@
                   <span class="name">{{ alb.title }}</span>
                   <div class="header-actions" v-if="editOrShow === 'edit'">
                     <el-tooltip content="下载" placement="top">
-                      <span class="action-icon" @click="renameSelected">
+                      <span class="action-icon" @click="downloadSelected">
                         <el-icon><Download /></el-icon>
                       </span>
                     </el-tooltip>
                     <el-tooltip content="编辑" placement="top">
-                      <span class="action-icon" @click="editSelected">
+                      <span class="action-icon" @click="openPhotoEdit(alb.id)">
                         <el-icon><EditPen /></el-icon>
                       </span>
                     </el-tooltip>
                     <el-tooltip content="删除" placement="top">
-                      <span class="action-icon" @click="deleteSelected">
+                      <span class="action-icon" @click="deleteItem">
                         <el-icon><CircleClose /></el-icon>
                       </span>
                     </el-tooltip>
@@ -286,18 +281,10 @@
           <div v-else class="viewer-placeholder">暂无提取清单</div>
         </template>
 
-        <!-- 照片卷右侧:图片网格,可点击放大 -->
+        <!-- 照片卷右侧:单图预览,可点击放大 -->
         <template v-else-if="activeTab === 'album'">
-          <div v-if="currentAlbum" class="album-grid">
-            <div
-              class="album-image"
-              v-for="(img, idx) in currentAlbum.images"
-              :key="img.url + idx"
-              @click="openAlbumViewer(idx)"
-            >
-              <el-image :src="img.url" fit="cover" />
-              <div class="caption">{{ img.name || ('照片 ' + (idx + 1)) }}</div>
-            </div>
+          <div v-if="currentAlbum" class="scene-image" @click="openAlbumViewer(0)">
+            <el-image :src="currentAlbum.images[0].url" fit="contain" class="inline-image" />
           </div>
           <div v-else class="viewer-placeholder">暂无照片卷</div>
           <el-image-viewer
@@ -312,15 +299,17 @@
   </div>
 </template>
 <script setup lang="ts">
-import { ref, computed, watch, onMounted } from 'vue';
+import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
-import CreatMap from '@/view/case/drawMap/creatMap.vue';
+// 弹窗已移除,直接跳转到绘制页面
 import { getCaseFiles, CaseFile, BoardType, setCaseFile, delCaseFile } from '@/store/caseFile';
 import { FileDrawType } from '@/constant/caseFile';
 import { addCaseFile as addCaseFileDialog } from '@/view/case/quisk';
 import { router, RouteName } from '@/router';
 import { user } from '@/store/user';
-import { getCaseInquestInfo, getCaseDetailInfo } from '@/store/case';
+import { getCaseInquestInfo, getCaseDetailInfo, getFfmpegImageList, exportCaseInquestInfo, exportCaseDetailInfo, caseDel } from '@/store/case';
+import { axios } from '@/request';
+import { saveAs } from '@/util/file-serve';
 
 const appId = import.meta.env.VITE_APP_APP || 'fire';
 const url = import.meta.env.VITE_DRAW_URL || 'http://mix3d.4dkankan.com';
@@ -441,28 +430,20 @@ const renameSelected = async () => {
 };
 
 // 编辑当前选中(根据类型跳转)
-const editSelected = () => {
+const editSelected = (type) => {
   const file = currentSelectedFile.value as any;
   if (!file) {
     ElMessage.warning('请先在左侧列表选择一个文件');
     return;
   }
+  console.log('file', type, file);
   // 新版绘图:tabulation/overview 分开处理;旧数据走 RouteName.drawCaseFile
-  if (file.type === 'tabulation' && file.tabulationId) {
-    if (appId === 'fire') {
-      window.open(`${url}/draw/fire/index.html#/tabulation?caseId=${caseId.value}&tabulationId=${file.tabulationId}&token=${user.value.token}`, '_blank');
-    } else if (appId === 'criminal') {
-      window.open(`${url}/draw/criminal/index.html#/tabulation?caseId=${caseId.value}&tabulationId=${file.tabulationId}&token=${user.value.token}`, '_blank');
-    } else if (appId === 'cjzfire') {
-      window.open(`${url}/draw/cjzfire/index.html#/tabulation?caseId=${caseId.value}&tabulationId=${file.tabulationId}&token=${user.value.token}`, '_blank');
-    } else if (appId === 'xmfire') {
-      window.open(`${url}/draw/xmfire/index.html#/tabulation?caseId=${caseId.value}&tabulationId=${file.tabulationId}&token=${user.value.token}`, '_blank');
-    } else {
-      window.open(`${url}/draw/fire/index.html#/tabulation?caseId=${caseId.value}&tabulationId=${file.tabulationId}&token=${user.value.token}`, '_blank');
-    }
+  if (type === 'tabulation' && file.tabulationId) {
+    console.log(1111)
+    openTabulation(file.tabulationId);
     return;
   }
-  if (file.type === 'overview' && file.overviewId) {
+  if (type === 'overview' && file.overviewId) {
     if (appId === 'fire') {
       window.open(`${url}/draw/fire/index.html#/overview?caseId=${caseId.value!}&overviewId=${file.overviewId}&token=${user.value.token}`, '_blank');
     } else if (appId === 'criminal') {
@@ -525,12 +506,56 @@ const inquestData = ref<any>({
   ],
 });
 
-// 左侧列表使用的摘要
+// 左侧列表使用的摘要 + 原始数据
 const inspectionList = ref<{ title: string; content: string }[]>([]);
+const inquestRawList = ref<any[]>([]);
 const selectedInspectionIndex = ref(0);
 const currentInspection = computed(() => inspectionList.value[selectedInspectionIndex.value]);
-const selectInspection = (idx: number) => (selectedInspectionIndex.value = idx);
-
+const selectInspection = (idx: number) => {
+  selectedInspectionIndex.value = idx;
+  // 同步当前原始数据,供编辑页预填
+  inquestData.value = inquestRawList.value[idx] || {};
+};
+// 勘验笔录:拉取并生成预览文本
+const loadInspection = async () => {
+  if (!caseId.value) return;
+  try {
+    const res: any = await getCaseInquestInfo(caseId.value!);
+    const payload = res?.data ?? [];
+    if (Array.isArray(payload)) {
+      // 新格式:数组,每条记录生成一个列表项
+      inquestRawList.value = payload;
+      inspectionList.value = payload.map((item: any, idx: number) => ({
+        title: item?.count ? `勘验笔录(第${item.count}次)` : `勘验笔录 ${idx + 1}`,
+        content: formatInquest(item),
+      }));
+      // 默认选中第一条,并同步右侧与预填原始数据
+      if (inspectionList.value.length) {
+        selectedInspectionIndex.value = 0;
+        inquestData.value = inquestRawList.value[0] || {};
+      }
+    } else {
+      // 兼容旧格式:单对象
+      const content = formatInquest(payload || {});
+      inquestRawList.value = [payload || {}];
+      if (!inspectionList.value.length) {
+        inspectionList.value = [{ title: '勘验笔录', content }];
+        selectedInspectionIndex.value = 0;
+        inquestData.value = inquestRawList.value[0];
+      } else {
+        inspectionList.value[selectedInspectionIndex.value] = {
+          title: inspectionList.value[selectedInspectionIndex.value]?.title || '勘验笔录',
+          content,
+        };
+        inquestData.value = inquestRawList.value[selectedInspectionIndex.value] || {};
+      }
+    }
+  } catch (e) {
+    console.error('获取勘验笔录失败:', e);
+    inspectionList.value = [];
+    inquestRawList.value = [];
+  }
+};
 // 将接口数据格式化为预览文本
 const formatInquest = (d: any) => {
   const s = d.startTime || {}; const e = d.endTime || {};
@@ -554,25 +579,46 @@ const formatInquest = (d: any) => {
   );
 };
 
-// 调取接口并填充列表
-const loadInspection = async () => {
+// 调取接口并填充列表(提取清单)
+const loadExtraction = async () => {
   if (!caseId.value) return;
   try {
-    const res: any = await getCaseInquestInfo(caseId.value!);
-    const payload = res?.data || {};
-    // 将返回数据写入到 inquestData
-    const target = inquestData.value;
-    for (const k in target) {
-      if (Object.prototype.hasOwnProperty.call(payload, k)) {
-        (target as any)[k] = payload[k];
+    const res: any = await getCaseDetailInfo(caseId.value!);
+    const payload = res?.data ?? [];
+    if (Array.isArray(payload)) {
+      // 新格式:数组
+      extractionRawList.value = payload;
+      extractionList.value = payload.map((item: any, idx: number) => ({
+        title: item?.address ? `提取清单(${item.address})` : `提取清单 ${idx + 1}`,
+        content: formatExtraction(item),
+      }));
+      if (extractionList.value.length) {
+        selectedExtractionIndex.value = 0;
+        extractionData.value = extractionRawList.value[0] || {};
+      }
+    } else {
+      // 旧格式:单对象
+      const target = extractionData.value;
+      for (const k in target) {
+        if (Object.prototype.hasOwnProperty.call(payload, k)) {
+          (target as any)[k] = (payload as any)[k];
+        }
+      }
+      const content = formatExtraction(target);
+      if (!extractionList.value.length) {
+        extractionList.value = [{ title: '提取清单', content }];
+        selectedExtractionIndex.value = 0;
+      } else {
+        extractionList.value[selectedExtractionIndex.value] = {
+          title: extractionList.value[selectedExtractionIndex.value]?.title || '提取清单',
+          content,
+        };
       }
     }
-    const content = formatInquest(target);
-    inspectionList.value = [{ title: '勘验笔录', content }];
-    selectedInspectionIndex.value = 0;
   } catch (e) {
-    console.error('获取勘验笔录失败:', e);
-    inspectionList.value = [];
+    console.error('获取提取清单失败:', e);
+    extractionList.value = [];
+    extractionRawList.value = [];
   }
 };
 
@@ -629,61 +675,23 @@ const formatExtraction = (d: any) => {
 };
 
 const extractionList = ref<{ title: string; content: string }[]>([]);
+const extractionRawList = ref<any[]>([]);
 const selectedExtractionIndex = ref(0);
 const currentExtraction = computed(() => extractionList.value[selectedExtractionIndex.value]);
 const selectExtraction = (idx: number) => (selectedExtractionIndex.value = idx);
 
-// 复原接口逻辑:拉取并写入 extractionData,然后生成预览文本
-const loadExtraction = async () => {
-  if (!caseId.value) return;
-  try {
-    const res: any = await getCaseDetailInfo(caseId.value!);
-    const payload = res?.data || {};
-    const target = extractionData.value;
-    for (const k in target) {
-      if (Object.prototype.hasOwnProperty.call(payload, k)) {
-        (target as any)[k] = payload[k];
-      }
-    }
-    const content = formatExtraction(target);
-    // 左侧列表:如果为空则初始化一个;否则更新当前项内容
-    if (!extractionList.value.length) {
-      extractionList.value = [{ title: '提取清单', content }];
-      selectedExtractionIndex.value = 0;
-    } else {
-      extractionList.value[selectedExtractionIndex.value] = {
-        title: extractionList.value[selectedExtractionIndex.value]?.title || '提取清单',
-        content,
-      };
-    }
-  } catch (e) {
-    console.error('获取提取清单失败:', e);
-    if (!extractionList.value.length) extractionList.value = [];
-  }
-};
-
-// 切换多个提取清单项时,重新调用接口更新内容
+// 切换多个提取清单项时,同步当前原始数据用于编辑预填
 watch(selectedExtractionIndex, () => {
   if (activeTab.value === 'extraction') {
-    loadExtraction();
+    extractionData.value = extractionRawList.value[selectedExtractionIndex.value] || {};
   }
 });
 
 // 照片卷数据与交互
-const albumList = ref<{ title: string; images: PicItem[] }[]>([
-  {
-    title: '照片卷 1',
-    images: [
-      { url: '/jmlogo.png', name: '现场照片 1' },
-      { url: '/favicon.ico', name: '现场照片 2' },
-      { url: '/fire.ico', name: '现场照片 3' },
-      { url: '/police.ico', name: '现场照片 4' },
-      { url: '/logo_big.ico', name: '现场照片 5' },
-    ],
-  },
-]);
+const albumList = ref<{ id?: number; title: string; images: PicItem[] }[]>([]);
 const selectedAlbumIndex = ref(0);
 const currentAlbum = computed(() => albumList.value[selectedAlbumIndex.value]);
+console.log(currentAlbum.value, 999)
 const selectAlbum = (idx: number) => (selectedAlbumIndex.value = idx);
 const openAlbumViewer = (index: number) => {
   const list = currentAlbum.value?.images || [];
@@ -692,6 +700,94 @@ const openAlbumViewer = (index: number) => {
   showViewer.value = true;
 };
 
+// 照片制卷列表:改用 getFfmpegImageList 接口获取
+const loadAlbum = async () => {
+  if (!caseId.value) return;
+  try {
+    const res: any = await getFfmpegImageList(caseId.value!);
+    const list: any[] = Array.isArray(res?.data) ? res.data : Array.isArray(res?.data?.list) ? res.data.list : [];
+    albumList.value = list.map((item: any, idx: number) => {
+      const url = item.imgUrl || item.filesUrl || item.url;
+      const name = item.imgInfo || item.filesTitle || item.name || `照片卷 ${idx + 1}`;
+      return { id: item.id, title: name, images: url ? [{ url, name }] : [] };
+    });
+    // 若无数据,置空;否则默认选中第一项
+    selectedAlbumIndex.value = albumList.value.length ? 0 : 0;
+  } catch (e) {
+    console.error('获取照片卷失败:', e);
+    albumList.value = [];
+  }
+};
+
+// 统一下载:按当前 tab 类型以 blob 方式下载
+const downloadSelected = async () => {
+  try {
+    if (activeTab.value === 'inspection') {
+      if (!caseId.value) return;
+      const title = (inspectionList.value[selectedInspectionIndex.value]?.title || '勘验笔录') + '.docx';
+      const blob: Blob = await (exportCaseInquestInfo(caseId.value!) as any);
+      await saveAs(blob, title);
+      return;
+    }
+    if (activeTab.value === 'extraction') {
+      if (!caseId.value) return;
+      const title = (extractionList.value[selectedExtractionIndex.value]?.title || '提取清单') + '.docx';
+      const blob: Blob = await (exportCaseDetailInfo(caseId.value!) as any);
+      await saveAs(blob, title);
+      return;
+    }
+    if (activeTab.value === 'album') {
+      const img = currentAlbum.value?.images?.[0];
+      if (!img?.url) {
+        ElMessage.warning('当前照片卷暂无图片可下载');
+        return;
+      }
+      const resp: any = await axios.get(img.url, { responseType: 'blob', params: { ingoreRes: true } });
+      await saveAs(resp as Blob, (currentAlbum.value?.title || '照片卷') + '.jpg');
+      return;
+    }
+    if (activeTab.value === 'scene') {
+      const url = currentSceneImageUrl.value;
+      if (!url) {
+        ElMessage.warning('请先选择一张现场图');
+        return;
+      }
+      const resp: any = await axios.get(url, { responseType: 'blob', params: { ingoreRes: true } });
+      await saveAs(resp as Blob, '现场图.jpg');
+      return;
+    }
+  } catch (e) {
+    console.error('下载失败:', e);
+    ElMessage.error('下载失败,请稍后重试');
+  }
+};
+
+// 统一删除:照片卷调用 caseDel,其他沿用原逻辑或不支持
+const deleteItem = async () => {
+  try {
+    if (activeTab.value === 'album') {
+      const alb = currentAlbum.value;
+      if (!alb?.id) {
+        ElMessage.warning('未选中照片卷或缺少ID');
+        return;
+      }
+      await ElMessageBox.confirm('确定删除当前照片卷?删除后不可恢复。', '删除确认', { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' });
+      await caseDel(alb.id);
+      ElMessage.success('已删除');
+      await loadAlbum();
+      return;
+    }
+    if (activeTab.value === 'scene') {
+      // 现场图删除沿用原有逻辑
+      await deleteSelected();
+      return;
+    }
+    ElMessage.info('当前列表暂不支持删除');
+  } catch (e) {
+    // 取消或失败不提示错误
+  }
+};
+
 // Tab 切换时默认选中第一个项,并关闭任何图片查看器
 watch(activeTab, (val) => {
   if (val === 'scene') {
@@ -714,6 +810,8 @@ watch(activeTab, (val) => {
     showViewer.value = false;
     if (val === 'inspection' && inspectionList.value.length) {
       selectedInspectionIndex.value = 0;
+      // 同步当前原始数据
+      inquestData.value = inquestRawList.value[0] || {};
     } else if (val === 'extraction' && extractionList.value.length) {
       selectedExtractionIndex.value = 0;
     } else if (val === 'album' && albumList.value.length) {
@@ -722,10 +820,21 @@ watch(activeTab, (val) => {
   }
 }, { immediate: true });
 
-// 地图选择弹窗与新增逻辑
-const showMapDialog = ref(false);
-const openMapDialog = () => {
-  showMapDialog.value = true;
+// 直接打开方位图绘制页面(tabulation),支持传入已选项的 tabulationId
+const openTabulation = (tabulationId?: string | number) => {
+  if (!caseId.value) return;
+  const extra = tabulationId ? `&tabulationId=${tabulationId}` : '';
+  if (appId === 'fire') {
+    window.open(`${url}/draw/fire/index.html#/tabulation?caseId=${caseId.value!}&token=${user.value.token}${extra}`, '_blank');
+  } else if (appId === 'criminal') {
+    window.open(`${url}/draw/criminal/index.html#/tabulation?caseId=${caseId.value!}&token=${user.value.token}${extra}`, '_blank');
+  } else if (appId === 'cjzfire') {
+    window.open(`${url}/draw/cjzfire/index.html#/tabulation?caseId=${caseId.value!}&token=${user.value.token}${extra}`, '_blank');
+  } else if (appId === 'xmfire') {
+    window.open(`${url}/draw/xmfire/index.html#/tabulation?caseId=${caseId.value!}&token=${user.value.token}${extra}`, '_blank');
+  } else {
+    window.open(`${url}/draw/fire/index.html#/tabulation?caseId=${caseId.value!}&token=${user.value.token}${extra}`, '_blank');
+  }
 };
 
 // 新增平面图(overview)
@@ -762,7 +871,7 @@ const onAdd = (type: 'plane' | 'orientation') => {
   if (type === 'plane') {
     openOverView();
   } else {
-    openMapDialog();
+    openTabulation();
   }
 };
 const onUpload = async (_type: 'plane' | 'orientation') => {
@@ -771,19 +880,107 @@ const onUpload = async (_type: 'plane' | 'orientation') => {
   await refreshSceneFiles();
 };
 
-const onFillInspection = () => ElMessage.info('勘验笔录填写功能未接入');
+// 打开覆盖式编辑页(编辑当前项):勘验笔录/提取清单
+const openEditOverlay = (type: 'inquest' | 'extraction') => {
+  const current = router.currentRoute.value;
+  // 勘验笔录仍走原有 records 编辑页;提取清单改为新的 editInspection 页
+  const editSub = type === 'extraction' ? 'editInspection' : 'records';
+  const query: any = { ...current.query, editSub, type };
+  if (type === 'extraction') {
+    // 使用 sessionStorage 传递预填数据,避免 URL 过长
+    try {
+      const key = `extractionPreset:${caseId.value!}`;
+      const currentItem = extractionData.value || {};
+      sessionStorage.setItem(key, JSON.stringify(currentItem));
+      query.presetKey = String(caseId.value!);
+      if (currentItem && currentItem.id !== undefined) {
+        query.id = String(currentItem.id);
+      }
+    } catch (e) {
+      console.warn('保存预填数据失败', e);
+    }
+  } else if (type === 'inquest') {
+    // 勘验笔录:传递当前选中项内容与ID,用于编辑页预填与更新
+    try {
+      const key = `inquestPreset:${caseId.value!}`;
+      const currentItem = inquestData.value || {};
+      sessionStorage.setItem(key, JSON.stringify(currentItem));
+      query.presetKey = String(caseId.value!);
+      if (currentItem && currentItem.id !== undefined) {
+        query.id = String(currentItem.id);
+      }
+    } catch (e) {
+      console.warn('保存勘验预填数据失败', e);
+    }
+  }
+  router.push({
+    name: current.name || RouteName.fireDetails,
+    params: current.params,
+    query,
+  });
+};
+// 打开覆盖式编辑页(新增空白):不携带任何信息
+const openAddOverlay = (type: 'inquest' | 'extraction') => {
+  const current = router.currentRoute.value;
+  const editSub = type === 'extraction' ? 'editInspection' : 'records';
+  // 基于当前查询构建,但显式清理会导致回显的参数
+  const query: any = { ...current.query, editSub, type };
+  // 清理上一次编辑可能遗留的预填参数
+  if ('presetKey' in query) delete query.presetKey;
+  if ('id' in query) delete query.id;
+  // 不设置 presetKey 或 id,编辑页将保持空白
+  router.push({
+    name: current.name || RouteName.fireDetails,
+    params: current.params,
+    query,
+  });
+};
+
+const onFillInspection = () => openAddOverlay('inquest');
 const onUploadInspection = () => ElMessage.info('勘验笔录上传功能未接入');
-const onFillExtraction = () => ElMessage.info('提取清单填写功能未接入');
+const onFillExtraction = () => openAddOverlay('extraction');
+const onEditInspection = () => openEditOverlay('inquest');
+const onEditExtraction = () => openEditOverlay('extraction');
 const onUploadExtraction = () => ElMessage.info('提取清单上传功能未接入');
-const onEditAlbum = () => ElMessage.info('照片卷编纂功能未接入');
+const onEditAlbum = () => openPhotoEdit();
 const onUploadAlbum = () => ElMessage.info('照片卷上传功能未接入');
 
+// 打开照片卷编纂页(photoEdit.vue),支持传递 parentId
+const openPhotoEdit = (imgId?: number) => {
+  const current = router.currentRoute.value;
+  const query: any = { ...current.query, editSub: 'photoEdit' };
+  // 编辑按钮:携带 imgId 与 parentId;制卷按钮:parentId 为空
+  if (typeof imgId === 'number' && Number.isFinite(imgId)) {
+    query.imgId = String(imgId);
+    query.parentId = String(imgId);
+  } else {
+    delete query.imgId;
+    delete query.parentId;
+  }
+  router.push({
+    name: current.name || RouteName.fireDetails,
+    params: current.params,
+    query,
+  });
+};
+
 onMounted(() => {
   refreshSceneFiles();
   loadInspection();
   loadExtraction();
+  loadAlbum();
+  // 监听返回刷新事件,仅刷新勘验笔录、提取清单与照片卷
+  const refreshListsHandler = () => {
+    loadInspection();
+    loadExtraction();
+    loadAlbum();
+  };
+  window.addEventListener('fireDetails:refreshLists', refreshListsHandler as any);
+  onUnmounted(() => {
+    window.removeEventListener('fireDetails:refreshLists', refreshListsHandler as any);
+  });
 });
-watch(caseId, () => { refreshSceneFiles(); loadInspection(); loadExtraction(); });
+watch(caseId, () => { refreshSceneFiles(); loadInspection(); loadExtraction(); loadAlbum(); });
 </script>
 <style lang="scss" scoped>
 .scene-3dmix {
@@ -998,16 +1195,14 @@ watch(caseId, () => { refreshSceneFiles(); loadInspection(); loadExtraction(); }
     }
   }
   .album-grid {
-    display: grid;
-    grid-template-columns: repeat(4, 1fr);
-    gap: 12px;
-    padding: 16px;
+    width: 100%;
+    height: 100%;
+    background: #F5F5F5;
   }
   .album-image {
-    border: 1px solid #f0f0f0;
+    height: 100%;
     border-radius: 4px;
     overflow: hidden;
-    background: #fafafa;
     cursor: pointer;
     .caption {
       padding: 6px 8px;
@@ -1015,6 +1210,11 @@ watch(caseId, () => { refreshSceneFiles(); loadInspection(); loadExtraction(); }
       color: rgba(0,0,0,.65);
       text-align: center;
     }
+    :deep(img){
+      height: 100%;
+      width: 100%;
+      object-fit: cover;
+    }
   }
 }
 

+ 502 - 0
src/view/newFireCase/newFireDetails/editFilePage.vue

@@ -0,0 +1,502 @@
+<template>
+  <div v-if="visible" class="edit-file-page">
+    <div class="page-body">
+      <template v-if="type === 'inquest'">
+        <h3 class="title">勘验笔录</h3>
+        <div class="content">
+          <div class="line">
+            <span>勘验次数:</span>
+            <span>第</span>
+            <el-input class="input" v-model="inquest.count" placeholder="" style="width: 80px" />
+            <span>次勘验</span>
+          </div>
+
+          <div class="line">
+            <span>勘验时间:</span>
+            <div>
+              <el-input class="input" :maxlength="4" type="text" v-model="inquest.startTime.year" placeholder="" style="width: 80px" />
+              <span>年</span>
+              <el-input class="input" :maxlength="2" type="text" v-model="inquest.startTime.month" placeholder="" style="width: 80px" />
+              <span>月</span>
+              <el-input class="input" :maxlength="2" type="text" v-model="inquest.startTime.day" placeholder="" style="width: 80px" />
+              <span>日</span>
+              <el-input class="input" :maxlength="2" type="text" v-model="inquest.startTime.hour" placeholder="" style="width: 80px" />
+              <span>时</span>
+              <el-input class="input" :maxlength="2" type="text" v-model="inquest.startTime.min" placeholder="" style="width: 80px" />
+              <span>分</span>
+            </div>
+            <span style="width: 60px; text-align: center">至</span>
+            <div>
+              <el-input class="input" :maxlength="4" type="text" v-model="inquest.endTime.year" placeholder="" style="width: 80px" />
+              <span>年</span>
+              <el-input class="input" :maxlength="2" type="text" v-model="inquest.endTime.month" placeholder="" style="width: 80px" />
+              <span>月</span>
+              <el-input class="input" :maxlength="2" type="text" v-model="inquest.endTime.day" placeholder="" style="width: 80px" />
+              <span>日</span>
+              <el-input class="input" :maxlength="2" type="text" v-model="inquest.endTime.hour" placeholder="" style="width: 80px" />
+              <span>时</span>
+              <el-input class="input" :maxlength="2" type="text" v-model="inquest.endTime.min" placeholder="" style="width: 80px" />
+              <span>分</span>
+            </div>
+          </div>
+
+          <div class="line">
+            <span>勘验地点:</span>
+            <el-input class="input" type="tel" v-model="inquest.address" placeholder="" style="width: 100%" />
+          </div>
+          <div class="line">
+            <span>勘验人员姓名、勘验人职务(含技术职务):</span>
+            <el-input class="input" type="tel" v-model="inquest.userInfo" placeholder="" style="width: 100%" />
+          </div>
+          <div class="line">
+            <span>勘验气象条件(天气、风力、温度):</span>
+            <el-input class="input" type="tel" v-model="inquest.weather" placeholder="" style="width: 100%" />
+          </div>
+
+          <div class="textarea">
+            <p class="child-title">勘验情况:</p>
+            <el-input type="textarea" :rows="4" v-model="inquest.situation" placeholder="" style="width: 100%" />
+          </div>
+          <div class="textarea">
+            <p class="child-title">一、环境勘验</p>
+            <el-input type="textarea" :rows="4" v-model="inquest.environment" placeholder="" style="width: 100%" />
+          </div>
+          <div class="textarea">
+            <p class="child-title">二、初步勘验</p>
+            <el-input type="textarea" :rows="4" v-model="inquest.firstInquest" placeholder="" style="width: 100%" />
+          </div>
+          <div class="textarea">
+            <p class="child-title">三、细项勘验</p>
+            <el-input type="textarea" :rows="4" v-model="inquest.carefulInquest" placeholder="" style="width: 100%" />
+          </div>
+          <div class="textarea">
+            <p class="child-title">四、专项勘验</p>
+            <el-input type="textarea" :rows="6" v-model="inquest.specialInquest" placeholder="" style="width: 100%" />
+          </div>
+          <div class="textarea">
+            <p class="child-title">提取物品描述:</p>
+            <el-input type="textarea" :rows="6" v-model="inquest.itemDescription" placeholder="" style="width: 100%" />
+          </div>
+          <div class="textarea">
+            <p class="child-title">现场拍照制图描述:</p>
+            <el-input type="textarea" :rows="6" v-model="inquest.imgDescription" placeholder="" style="width: 100%" />
+          </div>
+
+          <div class="info">
+            <p class="sub-tit">勘验信息:</p>
+            <div class="inner">
+              <div class="sec">
+                <span>勘验负责人</span>
+                <el-input class="input" v-model="inquest.leader" placeholder="" />
+              </div>
+              <div class="sec">
+                <span>记录人</span>
+                <el-input class="input" v-model="inquest.recorder" placeholder="" />
+              </div>
+              <div class="sec">
+                <span>勘验人</span>
+                <el-input class="input" v-model="inquest.inspector" placeholder="" />
+              </div>
+            </div>
+          </div>
+
+          <div class="gap"></div>
+          <template v-for="(wit, idx) in inquest.witnessInfo" :key="'wit-'+idx">
+            <div class="witnessInfo">
+              <p class="sub-tit">证人信息:</p>
+              <div class="line">
+                <span>证人或当事人:</span>
+                <el-input class="input" v-model="wit.name" placeholder="" style="width: 180px" />
+                <div>
+                  <el-input class="input" v-model="wit.year" placeholder="" style="width: 80px" />
+                  <span>年</span>
+                  <el-input class="input" v-model="wit.month" placeholder="" style="width: 80px" />
+                  <span>月</span>
+                  <el-input class="input" v-model="wit.day" placeholder="" style="width: 80px" />
+                  <span>日</span>
+                </div>
+                <span style="margin-left: 50px">身份证件号码:</span>
+                <el-input class="input" v-model="wit.id" placeholder="" style="width: 280px" />
+              </div>
+              <div class="line">
+                <span>单位或住址:</span>
+                <el-input class="input" v-model="wit.address" placeholder="" style="width: 100%" />
+              </div>
+            </div>
+          </template>
+
+          <div class="btn-container">
+            <el-button class="btn" @click="addWitness">+新增</el-button>
+          </div>
+        </div>
+      </template>
+
+      <template v-else>
+        <h3 class="title">提取清单</h3>
+        <div class="content">
+          <div class="line">
+            <span>起火单位/地址:</span>
+            <el-input class="input" v-model="extract.address" placeholder="" style="width: 100%" />
+          </div>
+          <div class="line">
+            <span>提取日期:</span>
+            <div>
+              <el-input class="input" v-model="extract.time.year" placeholder="" style="width: 80px" />
+              <span>年</span>
+              <el-input class="input" v-model="extract.time.month" placeholder="" style="width: 80px" />
+              <span>月</span>
+              <el-input class="input" v-model="extract.time.day" placeholder="" style="width: 80px" />
+              <span>日</span>
+            </div>
+          </div>
+          <div class="detail">
+            <span class="sub-tit">提取清单:</span>
+            <template v-for="(item, index) in extract.detail" :key="'d-'+index">
+              <div class="witnessInfo">
+                <span class="sub-tit">编号 {{ index + 1 }}:</span>
+                <div class="info">
+                  <div class="inner">
+                    <div class="sec"><span>名称</span><el-input class="input" v-model="item.name" placeholder="" /></div>
+                    <div class="sec"><span>规格</span><el-input class="input" v-model="item.spec" placeholder="" /></div>
+                    <div class="sec"><span>数量</span><el-input class="input" v-model="item.num" placeholder="" /></div>
+                  </div>
+                  <div class="inner"><div class="sec"><span>提取部位</span><el-input class="input" v-model="item.part" placeholder="" /></div></div>
+                  <div class="inner"><div class="sec"><span>特征</span><el-input class="input" v-model="item.desc" placeholder="" /></div></div>
+                </div>
+              </div>
+            </template>
+            <div class="btn-container"><el-button class="btn" @click="addDetail">+新增</el-button></div>
+          </div>
+          <div class="extractUser">
+            <span class="sub-tit">提取人:</span>
+            <template v-for="(u, i) in extract.extractUser" :key="'eu-'+i">
+              <div class="line">
+                <span>姓名:</span><el-input class="input" v-model="u.name" placeholder="" style="width: 20%" />
+                <span>工作单位:</span><el-input class="input" v-model="u.address" placeholder="" style="width: 70%" />
+              </div>
+            </template>
+            <div class="btn-container"><el-button class="btn" @click="addExtractUser">+新增</el-button></div>
+          </div>
+          <div>
+            <p class="sub-tit">证人或当事人:</p>
+            <template v-for="(wit, i) in extract.witnessInfo" :key="'w-'+i">
+              <div class="witnessInfo">
+                <div class="line">
+                  <span>姓名:</span><el-input class="input" v-model="wit.name" placeholder="" style="width: 180px" />
+                  <span style="margin-left: 50px">身份证件号码:</span><el-input class="input" v-model="wit.id" placeholder="" style="width: 280px" />
+                  <span style="margin-left: 50px">联系电话:</span><el-input class="input" v-model="wit.phone" placeholder="" style="width: 280px" />
+                </div>
+                <div class="line"><span>单位或住址:</span><el-input class="input" v-model="wit.address" placeholder="" style="width: 100%" /></div>
+              </div>
+            </template>
+            <div class="btn-container"><el-button class="btn" @click="addWitnessExtract">+新增</el-button></div>
+          </div>
+        </div>
+      </template>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, computed, watch, onMounted } from 'vue';
+import { useRoute } from 'vue-router';
+import { ElMessage } from 'element-plus';
+import { getCaseInquestInfo, saveCaseInquestInfo, getCaseDetailInfo, saveCaseDetailInfo } from '@/store/case';
+
+const props = defineProps<{ caseId: number; currentRecord: object; editOrShow: string }>();
+const route = useRoute();
+const visible = computed(() => route.query.editSub === 'records' || route.query.editSub === 'editInspection');
+const type = computed(() => (route.query.type as string) === 'extraction' ? 'extraction' : 'inquest');
+
+const inquest = reactive<any>({
+  count: '',
+  startTime: { year: '', month: '', day: '', hour: '', min: '' },
+  endTime: { year: '', month: '', day: '', hour: '', min: '' },
+  address: '', userInfo: '', weather: '', situation: '', environment: '', firstInquest: '', carefulInquest: '', specialInquest: '', itemDescription: '', imgDescription: '', leader: '', recorder: '', inspector: '',
+  witnessInfo: [ { name: '', year: '', month: '', day: '', id: '', address: '' } ],
+});
+const extract = reactive<any>({
+  address: '', time: { year: '', month: '', day: '' }, location: '',
+  detail: [ { name: '', spec: '', num: '', part: '', desc: '' } ],
+  extractUser: [ { name: '', address: '', id: '' } ],
+  witnessInfo: [ { name: '', address: '', phone: '', id: '' } ],
+});
+
+const resetInquest = () => {
+  Object.assign(inquest, {
+    count: '',
+    startTime: { year: '', month: '', day: '', hour: '', min: '' },
+    endTime: { year: '', month: '', day: '', hour: '', min: '' },
+    address: '',
+    userInfo: '',
+    weather: '',
+    situation: '',
+    environment: '',
+    firstInquest: '',
+    carefulInquest: '',
+    specialInquest: '',
+    itemDescription: '',
+    imgDescription: '',
+    leader: '',
+    recorder: '',
+    inspector: '',
+    witnessInfo: [ { name: '', year: '', month: '', day: '', id: '', address: '' } ],
+  });
+  // 确保新增模式不携带旧的 id
+  delete (inquest as any).id;
+};
+
+const resetExtract = () => {
+  Object.assign(extract, {
+    address: '',
+    time: { year: '', month: '', day: '' },
+    location: '',
+    detail: [ { name: '', spec: '', num: '', part: '', desc: '' } ],
+    extractUser: [ { name: '', address: '', id: '' } ],
+    witnessInfo: [ { name: '', address: '', phone: '', id: '' } ],
+  });
+  // 确保新增模式不携带旧的 id
+  delete (extract as any).id;
+};
+
+const addWitness = () => inquest.witnessInfo.push({ name: '', year: '', month: '', day: '', id: '', address: '' });
+const addDetail = () => extract.detail.push({ name: '', spec: '', num: '', part: '', desc: '' });
+const addExtractUser = () => extract.extractUser.push({ name: '', address: '', id: '' });
+const addWitnessExtract = () => extract.witnessInfo.push({ name: '', address: '', phone: '', id: '' });
+
+const loadInquest = async () => {
+  try {
+    const idParam = route.query.id ? Number(route.query.id) : undefined;
+    const hasPreset = !!route.query.presetKey;
+    const isNew = idParam === undefined && !hasPreset;
+    if (isNew) { resetInquest(); return; }
+    const res: any = await getCaseInquestInfo(props.caseId);
+    const data = res?.data ?? {};
+    // 新格式:数组,按路由ID选择
+    if (Array.isArray(data)) {
+      const picked = idParam !== undefined ? data.find((d: any) => Number(d?.id) === idParam) : undefined;
+      if (picked) Object.assign(inquest, { ...inquest, ...(picked || {}) });
+    } else {
+      // 旧格式:单对象,配合 preset 使用;若无 preset,仍允许编辑现有对象
+      if (hasPreset) {
+        Object.assign(inquest, { ...inquest, ...(data || {}) });
+      }
+    }
+  } catch (e) {
+    console.error('获取勗验笔录失败', e);
+  }
+};
+
+// 读取预填数据(如有)
+const applyInquestPreset = () => {
+  const presetKey = route.query.presetKey as string | undefined;
+  if (!presetKey) return;
+  try {
+    const txt = sessionStorage.getItem('inquestPreset:' + presetKey);
+    if (txt) {
+      const preset = JSON.parse(txt);
+      Object.assign(inquest, { ...inquest, ...(preset || {}) });
+    }
+  } catch (e) {
+    console.warn('读取勘验预填数据失败', e);
+  }
+};
+const loadExtract = async () => {
+  try {
+    const idParam = route.query.id ? Number(route.query.id) : undefined;
+    const hasPreset = !!route.query.presetKey;
+    const isNew = idParam === undefined && !hasPreset;
+    if (isNew) { resetExtract(); return; }
+    const res: any = await getCaseDetailInfo(props.caseId);
+    const data = res?.data ?? {};
+    if (Array.isArray(data)) {
+      const picked = idParam !== undefined ? data.find((d: any) => Number(d?.id) === idParam) : undefined;
+      if (picked) Object.assign(extract, { ...extract, ...(picked || {}) });
+    } else {
+      if (hasPreset) {
+        Object.assign(extract, { ...extract, ...(data || {}) });
+      }
+    }
+  } catch (e) {
+    console.error('获取提取清单失败', e);
+  }
+};
+
+// 读取提取清单预填数据(如有)
+const applyExtractPreset = () => {
+  const presetKey = route.query.presetKey as string | undefined;
+  if (!presetKey) return;
+  try {
+    const txt = sessionStorage.getItem('extractionPreset:' + presetKey);
+    if (txt) {
+      const preset = JSON.parse(txt);
+      Object.assign(extract, { ...extract, ...(preset || {}) });
+    }
+  } catch (e) {
+    console.warn('读取提取预填数据失败', e);
+  }
+};
+
+watch([visible, type], async ([v, t]) => {
+  if (!v) return;
+  if (t === 'inquest') {
+    await loadInquest();
+    applyInquestPreset();
+  } else {
+    await loadExtract();
+    applyExtractPreset();
+  }
+}, { immediate: true });
+
+const handleSave = async (): Promise<boolean> => {
+  try {
+    if (type.value === 'inquest') {
+      const idParam = route.query.id ? Number(route.query.id) : undefined;
+      const payload = idParam !== undefined ? { id: idParam, ...inquest } : { ...inquest };
+      await saveCaseInquestInfo(props.caseId, payload);
+    } else {
+      const idParam = route.query.id ? Number(route.query.id) : undefined;
+      const payload = idParam !== undefined ? { id: idParam, ...extract } : { ...extract };
+      await saveCaseDetailInfo(props.caseId, payload);
+    }
+    return true;
+  } catch (e) {
+    console.error('保存失败', e);
+    ElMessage.error('保存失败,请稍后重试');
+    return false;
+  }
+};
+
+defineExpose({ handleSave });
+</script>
+
+<style lang="scss" scoped>
+.edit-file-page {
+  position: fixed;
+  display: flex;
+  justify-content: center;
+  width: 100vw;
+  top: 120px;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: #f5f5f5;
+  z-index: 1000;
+  overflow: auto;
+  padding: 32px 0;
+}
+
+.page-body {
+  max-width: 1280px;
+  margin: 0 auto;
+  padding: 20px 28px;
+  background: #fff;
+  overflow: auto;
+
+  .input {
+    height: 32px;
+    line-height: 32px;
+    margin: 0 8px;
+  }
+
+  .textarea {
+    margin-right: 8px;
+    margin-bottom: 20px;
+
+    span {
+      padding: 10px 0;
+      display: inline-block;
+    }
+    .child-title {
+      padding: 10px 0;
+      text-align: left;
+    }
+  }
+
+  .line {
+    display: inline-flex;
+    width: 100%;
+    flex-direction: row;
+    align-items: center;
+    margin-bottom: 25px;
+    line-height: 38px;
+
+    span {
+      white-space: nowrap;
+    }
+  }
+}
+
+.title {
+  text-align: center;
+}
+
+.sub-tit {
+  display: block !important;
+  padding-bottom: 20px;
+  text-align: left;
+}
+
+.info {
+  display: block;
+
+  .inner {
+    display: flex;
+    flex-direction: row;
+    width: 100%;
+
+    .input {
+      flex: 1;
+    }
+
+    .sec {
+      flex: 1;
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+    }
+  }
+}
+
+.witnessInfo {
+  background: #f5f5f5;
+  padding: 15px;
+  margin-top: 20px;
+  margin-right: 8px;
+}
+
+.gap {
+  margin: 15px 0;
+}
+
+.btn-container {
+  padding: 20px 0;
+
+  .btn {
+    color: #26559b;
+    width: 100%;
+
+    &:hover {
+      background: #f5f5f5;
+      border-color: #dcdfe6;
+    }
+  }
+}
+
+.detail {
+  .inner {
+    display: flex;
+    flex-direction: row;
+    gap: 24px;
+    margin-bottom: 8px;
+    align-items: center;
+  }
+  .sec {
+    display: inline-flex;
+    align-items: center;
+    justify-content: flex-start;
+    gap: 8px;
+    width: 100%;
+  }
+}
+</style>

+ 13 - 14
src/view/newFireCase/newFireDetails/editIndex.vue

@@ -12,10 +12,10 @@
     </el-menu>
 
     <template v-if="currentMenuKey === 'info'">
-      <basicInfo :fire="tempFire" :caseId="caseId" :editOrShow="'edit'" />
+      <basicInfo :fire="tempFire" :caseId="caseId" :editOrShow="'edit'" :fromRoute="fromRoute" />
     </template>
     <template v-if="currentMenuKey === 'screenRecord'">
-      <ScreenShot :fire="tempFire" :caseId="caseId" :editOrShow="'edit'" @start="startShot" @playVideo="(url) => emit('playVideo', url)" />
+      <ScreenShot :fire="tempFire" :caseId="caseId" :editOrShow="'edit'" :processingIds="processingIds" :recentAddedItem="recentAddedItem" @start="startShot" @playVideo="(url) => emit('playVideo', url)" />
     </template>
     <template v-if="currentMenuKey === 'scene'">
       <Scene :fire="tempFire" :caseId="caseId" :editOrShow="'edit'" />
@@ -45,6 +45,9 @@ import OtherFiles from './components/otherFiles.vue';
 const props = defineProps<{
   caseId: number;
   currentRecord: object;
+  fromRoute: string;
+  processingIds?: (number | string)[];
+  recentAddedItem?: any | null;
 }>();
 const emit = defineEmits<{
   'start': any,
@@ -54,13 +57,16 @@ const emit = defineEmits<{
 const route = useRoute();
 const vueRouter = useRouter();
 const caseId = computed(() => Number(route.params.caseId));
-let tempFire = ref(null);
+let tempFire = ref({});
 watch(() => props.currentRecord, (newVal, oldVal) => {
-  console.log(newVal, newVal.tmProject, 8888)
-  tempFire.value = newVal.tmProject;
+  if(props.fromRoute === 'fire') {
+    tempFire.value = newVal?.tmProject;
+  } else if(props.fromRoute === 'criminal') {
+    tempFire.value = newVal;
+  }
 })
-const startShot = () => {
-  emit("start");
+const startShot = (payload?: any) => {
+  emit("start", payload);
 }
 // 从路由查询参数中获取当前菜单项,如果没有则默认为 'info'
 let currentMenuKey = ref(route.query.tab as string || 'info');
@@ -75,15 +81,8 @@ const handleMenuClick = async(menu) => {
         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) {

+ 247 - 0
src/view/newFireCase/newFireDetails/editInspection.vue

@@ -0,0 +1,247 @@
+<template>
+  <div v-if="visible" class="edit-inspection-page">
+    <div class="records">
+      <div class="content">
+        <div class="line">
+          <span>起火单位/地址:</span>
+          <el-input class="input" v-model="data.address" placeholder="" style="width: 100%" />
+        </div>
+
+        <div class="line">
+          <span>提取日期:</span>
+          <div>
+            <el-input class="input" :maxlength="4" type="text" v-model="data.time.year" placeholder="" style="width: 80px" />
+            <span>年</span>
+            <el-input class="input" :maxlength="2" type="text" v-model="data.time.month" placeholder="" style="width: 80px" />
+            <span>月</span>
+            <el-input class="input" :maxlength="2" type="text" v-model="data.time.day" placeholder="" style="width: 80px" />
+            <span>日</span>
+          </div>
+        </div>
+
+        <div class="detail">
+          <span class="sub-tit">提取清单:</span>
+          <template v-for="(item, index) in data.detail" :key="'d-'+index">
+            <div class="con">
+              <span class="sub-tit">编号 {{ index + 1 }}: </span>
+              <div class="info">
+                <div class="inner">
+                  <div class="sec"><span>名称: </span><el-input class="input" v-model="item.name" placeholder="" /></div>
+                  <div class="sec"><span>规格: </span><el-input class="input" v-model="item.spec" placeholder="" /></div>
+                  <div class="sec"><span>数量: </span><el-input class="input" v-model="item.num" placeholder="" /></div>
+                </div>
+                <div class="inner"><div class="sec"><span>提取部位: </span><el-input class="input" v-model="item.part" placeholder="" /></div></div>
+                <div class="inner"><div class="sec"><span>特征: </span><el-input class="input" v-model="item.desc" placeholder="" /></div></div>
+              </div>
+            </div>
+          </template>
+        </div>
+        <div class="btn-container"><el-button class="btn" @click="addItem">+新增</el-button></div>
+        <div class="gap"></div>
+
+        <div class="extractUser">
+          <span class="sub-tit">提取人:</span>
+          <template v-for="(u, i) in data.extractUser" :key="'eu-'+i">
+            <div class="line">
+              <span>姓名:</span>
+              <el-input class="input" v-model="u.name" placeholder="" style="width: 20%" />
+              <span>工作单位:</span>
+              <el-input class="input" v-model="u.address" placeholder="" style="width: 70%" />
+            </div>
+          </template>
+        </div>
+        <div class="btn-container"><el-button class="btn" @click="addExtractUser">+新增</el-button></div>
+
+        <div>
+          <p style="text-align: left;">证人或当事人:</p>
+          <template v-for="(wit, i) in data.witnessInfo" :key="'w-'+i">
+            <div class="witnessInfo">
+              <div class="line">
+                <span>姓名:</span><el-input class="input" v-model="wit.name" placeholder="" style="width: 180px" />
+                <span style="margin-left: 50px">身份证件号码:</span><el-input class="input" v-model="wit.id" placeholder="" style="width: 280px" />
+                <span style="margin-left: 50px">联系电话:</span><el-input class="input" v-model="wit.phone" placeholder="" style="width: 280px" />
+              </div>
+              <div class="line"><span>单位或住址:</span><el-input class="input" v-model="wit.address" placeholder="" style="width: 100%" /></div>
+            </div>
+          </template>
+        </div>
+        <div class="btn-container"><el-button class="btn" @click="addWitness">+新增</el-button></div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, watch, computed } from 'vue';
+import { useRoute } from 'vue-router';
+import { ElMessage } from 'element-plus';
+import { getCaseDetailInfo, saveCaseDetailInfo } from '@/store/case';
+
+const props = defineProps<{ caseId: number; currentRecord: object; editOrShow: string }>();
+const route = useRoute();
+const visible = computed(() => route.query.editSub === 'editInspection');
+
+const data = reactive<any>({
+  address: '',
+  time: { year: '', month: '', day: '' },
+  location: '',
+  detail: [
+    { name: '', spec: '', num: '', part: '', desc: '' },
+    { name: '', spec: '', num: '', part: '', desc: '' },
+  ],
+  extractUser: [
+    { name: '', address: '', id: '' },
+    { name: '', address: '', id: '' },
+  ],
+  witnessInfo: [
+    { name: '', address: '', phone: '', id: '' },
+    { name: '', address: '', phone: '', id: '' },
+  ],
+});
+
+const resetData = () => {
+  Object.assign(data, {
+    address: '',
+    time: { year: '', month: '', day: '' },
+    location: '',
+    detail: [
+      { name: '', spec: '', num: '', part: '', desc: '' },
+      { name: '', spec: '', num: '', part: '', desc: '' },
+    ],
+    extractUser: [
+      { name: '', address: '', id: '' },
+      { name: '', address: '', id: '' },
+    ],
+    witnessInfo: [
+      { name: '', address: '', phone: '', id: '' },
+      { name: '', address: '', phone: '', id: '' },
+    ],
+  });
+  // 确保新增模式不携带旧的 id
+  delete (data as any).id;
+};
+
+const addItem = () => data.detail.push({ name: '', spec: '', num: '', part: '', desc: '' });
+const addExtractUser = () => data.extractUser.push({ name: '', address: '', id: '' });
+const addWitness = () => data.witnessInfo.push({ name: '', address: '', phone: '', id: '' });
+
+const initInfo = async () => {
+  if (!props.caseId) return;
+  try {
+    // 若为新增模式(无 id 且无 presetKey),跳过预填
+    const idParam = route.query.id ? Number(route.query.id) : undefined;
+    const hasPreset = !!route.query.presetKey;
+    const isNew = idParam === undefined && !hasPreset;
+    if (isNew) { resetData(); return; }
+    const res: any = await getCaseDetailInfo(props.caseId);
+    const payload = res?.data || {};
+    for (const k in data) {
+      if (Object.prototype.hasOwnProperty.call(payload, k)) {
+        (data as any)[k] = payload[k];
+      }
+    }
+    // 读取预填数据(如有)
+    const presetKey = route.query.presetKey as string | undefined;
+    if (presetKey) {
+      try {
+        const txt = sessionStorage.getItem('extractionPreset:' + presetKey);
+        if (txt) {
+          const preset = JSON.parse(txt);
+          for (const k in preset) {
+            if (Object.prototype.hasOwnProperty.call(data, k)) {
+              (data as any)[k] = preset[k];
+            }
+          }
+        }
+      } catch (e) {
+        console.warn('读取预填数据失败', e);
+      }
+    }
+  } catch (e) {
+    console.error('获取提取清单失败', e);
+  }
+};
+
+watch(visible, (v) => { if (v) initInfo(); }, { immediate: true });
+
+const handleSave = async (): Promise<boolean> => {
+  try {
+    const idParam = route.query.id ? Number(route.query.id) : undefined;
+    const payload = idParam !== undefined ? { id: idParam, ...data } : { ...data };
+    await saveCaseDetailInfo(props.caseId, payload);
+    return true;
+  } catch (e) {
+    console.error('保存提取清单失败', e);
+    ElMessage.error('保存失败,请稍后重试');
+    return false;
+  }
+};
+
+defineExpose({ handleSave });
+</script>
+
+<style lang="scss" scoped>
+.edit-inspection-page {
+  position: fixed;
+  display: flex;
+  justify-content: center;
+  width: 100vw;
+  top: 120px;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: #f5f5f5;
+  z-index: 1000;
+  overflow: hidden;
+  padding: 32px 0;
+}
+
+.records {
+  max-width: 1280px;
+  margin: 0 auto;
+  padding: 20px 28px;
+  background: #fff;
+  overflow: auto;
+
+  .input {
+    height: 32px;
+    line-height: 32px;
+    margin: 0 8px;
+  }
+
+  .line {
+    display: inline-flex;
+    width: 100%;
+    flex-direction: row;
+    align-items: center;
+    margin-bottom: 25px;
+    line-height: 38px;
+
+    span {
+      white-space: nowrap;
+    }
+  }
+}
+
+.title { text-align: center; }
+.sub-tit { display: inline-block; padding-bottom: 20px; }
+
+.detail {
+  .con { padding: 20px; background-color: #f5f5f5; }
+  .info {
+    .inner { margin-bottom: 20px; display: flex; flex-direction: row; width: 100%; }
+    .sec { flex: 1; display: inline-flex; align-items: center; justify-content: center; }
+  }
+}
+
+.extractUser {
+  margin-right: 0px;
+  .line { background-color: #f5f5f5; padding: 15px; width: calc(100% - 30px); display: inline-flex; margin-bottom: 15px; }
+}
+
+.witnessInfo { background: #f5f5f5; padding: 15px; margin-top: 20px; }
+.gap { margin: 15px 0; }
+.btn-container { padding: 20px 0; }
+.btn { color: #26559b; width: 100%; }
+.btn:hover { background: #f5f5f5; border-color: #dcdfe6; }
+</style>

+ 201 - 43
src/view/newFireCase/newFireDetails/index.vue

@@ -1,11 +1,14 @@
 <template>
   <div class="new-fire-details">
     <!-- 顶部标题栏 -->
-    <headerTop :caseId="caseId" :currentRecord="currentRecord" :editOrShow="editOrShow" />
+    <headerTop :caseId="caseId" :currentRecord="currentRecord" :editOrShow="editOrShow" :showSave="showSave" @save="saveEditSub" @back="backEditSub" />
+    <editFilePage :caseId="caseId" :currentRecord="currentRecord" :editOrShow="editOrShow" ref="editFilePageRef" />
+    <editInspection :caseId="caseId" :currentRecord="currentRecord" :editOrShow="editOrShow" ref="editInspectionRef" />
+    <photoEdit :caseId="caseId" :title="pageTitle" ref="photoEditRef" />
     <!-- 查看页 -->
-    <showIndex :caseId="caseId" :currentRecord="currentRecord" @playVideo="playVideo" v-if="editOrShow === 'show'" />
+    <showIndex :caseId="caseId" :currentRecord="currentRecord" :fromRoute="fromRoute" :processingIds="processingIds" :recentAddedItem="recentAddedItem" @playVideo="playVideo" v-if="editOrShow === 'show'" />
     <!-- 编辑页 -->
-    <editIndex :caseId="caseId" :currentRecord="currentRecord" @start="startShot" @playVideo="playVideo" v-else />
+    <editIndex :caseId="caseId" :currentRecord="currentRecord" :fromRoute="fromRoute" :processingIds="processingIds" :recentAddedItem="recentAddedItem" @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" />
@@ -17,6 +20,7 @@
     >
       <video
         class="vidoe-play"
+        v-if="previewVisible"
         controls
         autoplay
         playsinline
@@ -28,7 +32,8 @@
 </template>
 
 <script setup lang="ts">
-import { ref, computed, onMounted } from "vue";
+import { ref, computed, onMounted, onUnmounted } from "vue";
+import { ElMessage } from "element-plus";
 import { useRoute, useRouter } from 'vue-router';
 import showIndex from './showIndex.vue';
 import editIndex from './editIndex.vue';
@@ -36,34 +41,53 @@ import { copyCase, getCaseSceneList, getCaseInfo, updateCaseInfo } from "@/store
 import { RouteName, router } from "@/router";
 import shot from './components/shot.vue';
 import headerTop from './components/headerTop.vue';
-import { createRecordFragment } from '@/store/system'
+import editFilePage from './editFilePage.vue';
+import editInspection from './editInspection.vue';
+import photoEdit from './photoEdit.vue';
+import {
+  createRecordFragment,
+  recordFragments,
+  getRecordFragmentBlobs,
+  isTemploraryID,
+  createTemploraryID,
+} from '@/store/system'
+import { uploadRecordFragments, getUploadRecordProgress } from '@/store/editCsae'
 
 // 从路由获取参数
 const route = useRoute();
 const vueRouter = useRouter();
 const caseId = computed(() => Number(route.params.caseId));
+const fromRoute = computed(() => route.query.fromRoute as string || '');
 const editOrShow = computed(() => route.query.editOrShow as string || 'show');
+const showSave = computed(() => {
+  const sub = route.query.editSub as string | undefined;
+  return editOrShow.value === 'edit' && (sub === 'records' || sub === 'editInspection' || sub === 'photoEdit');
+});
+const editFilePageRef = ref<any>(null);
+const editInspectionRef = ref<any>(null);
+const photoEditRef = ref<any>(null);
 const currentRecord = ref<any>({}); // 当前的caseID获取的row
+const pageTitle = computed(() => {
+  const cr: any = currentRecord.value || {};
+  return cr?.caseTitle || cr?.tmProject?.projectName || '';
+});
 onMounted(() => {
   setTimeout(async() => {
     try {
+      console.log(caseId.value, fromRoute.value, 8888)
       const caseInfo = await getCaseInfo(caseId.value!);
-      if (caseInfo) {
+      if (caseInfo && fromRoute.value == 'fire') {
         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 "该案件不存在!";
+      } else if (fromRoute.value == 'criminal') {
+        currentRecord.value = caseInfo;
       }
     } catch (error) {
-      // 跳转到无权限页面
+      console.error(error);
+      // throw "该案件不存在!";
+      //跳转到无权限页面
       vueRouter.replace({
         name: RouteName.noCase,
         query: {}
@@ -71,6 +95,23 @@ onMounted(() => {
     }
   }, 0);
   
+  // 监听标题更新事件:由 basicInfo 自动保存成功派发
+  const titleUpdateHandler = (evt: any) => {
+    const title = evt?.detail?.title || '';
+    if (!title) return;
+    const cr: any = currentRecord.value || {};
+    if (fromRoute.value === 'fire') {
+      if (!cr.tmProject) cr.tmProject = {};
+      cr.tmProject.projectName = title;
+    } else if (fromRoute.value === 'criminal') {
+      cr.caseTitle = title;
+    }
+    currentRecord.value = { ...cr };
+  };
+  window.addEventListener('fireDetails:updateTitle', titleUpdateHandler as any);
+  onUnmounted(() => {
+    window.removeEventListener('fireDetails:updateTitle', titleUpdateHandler as any);
+  });
 });
 
 const emit = defineEmits<{
@@ -78,42 +119,117 @@ const emit = defineEmits<{
   '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>([])
+// 正在合并上传的文件夹 ID 列表,传递到 ScreenShot 控制按钮显示
+const processingIds = ref<(number|string)[]>([]);
+// 最近新增的录制项,传递到 ScreenShot 以便即时加入列表
+const recentAddedItem = ref<any | null>(null);
 const appendFragment = (blobs: Blob[]) => {
-//   recordFragments.value.push(
-//     ...blobs.map((blob) =>
-//       createRecordFragment({ url: blob, recordId: record.id })
-//     )
-//   );
-//  record.value.status = 1
+  const current = record.value;
+  if (!current || !current.id) return;
+  recordFragments.value.push(
+    ...blobs.map((blob) =>
+      createRecordFragment({ url: blob, recordId: current.id })
+    )
+  );
+  console.log(recordFragments.value, 8888)
+  // 触发后端保存与合并
+  const files = getRecordFragmentBlobs(current);
+  if (!files.length) return;
+  if (!current.title || !String(current.title).trim()) {
+    ElMessage.warning('视频名称不可为空');
+    return;
+  }
+  const cId = caseId.value!;
+  (async () => {
+    try {
+      const res = await uploadRecordFragments({ caseId: cId, folderId: current.folderId, files });
+      ElMessage.success('视频已合并并保存');
+      // 跳转到“现场讲解”tab,触发列表刷新
+      vueRouter.replace({ path: route.path, query: { ...route.query, tab: 'screenRecord' } });
+      // 获取待轮询的 folderId(兼容多种返回结构)
+      const folderIdCandidates = [
+        // 常见字段
+        (res as any)?.videoFolderId,
+        (res as any)?.filesId,
+        (res as any)?.folderId,
+        (res as any)?.id,
+        // 某些接口可能包一层 data
+        (res as any)?.data?.videoFolderId,
+        (res as any)?.data?.filesId,
+        (res as any)?.data?.folderId,
+        (res as any)?.data?.id,
+        // 兜底:沿用当前文件夹
+        (current as any)?.folderId,
+      ].filter((v) => v !== undefined && v !== null);
+      const folderId = folderIdCandidates[0];
+      // 组装新增项并传递到子组件列表,uploadStatus 默认为 0
+      recentAddedItem.value = {
+        videoFolderId: folderId,
+        filesId: folderId,
+        videoFolderName: current.title,
+        videoFolderCover: current.cover,
+        videoMergeUrl: res?.videoMergeUrl || current.url || '',
+        uploadStatus: typeof res?.uploadStatus === 'number' ? res.uploadStatus : 0,
+      };
+      if (folderId !== undefined && folderId !== null) {
+        if (!processingIds.value.includes(folderId)) processingIds.value.push(folderId);
+        startProgressPolling(folderId);
+      }
+    } catch (e) {
+      console.error('保存合并失败', e);
+      ElMessage.error('保存失败,请稍后重试');
+    }
+  })();
 };
-const startShot = () => {
-  records.value.push({
-    id: 111,
-    title: '讲解视频' + (records.value.length + 1),
-    cover: '',
-    url: '',
-    status: 1,
-    sort: 0,
-  })
-  isShot.value = true
-  showBottomBar.value = true
-}
+
+// 轮询进度:返回 data == 100 时认为合并上传完成
+const pollingTimers = new Map<any, any>();
+const startProgressPolling = (folderId: number | string) => {
+  if (pollingTimers.has(folderId)) return;
+  const timer = setInterval(async () => {
+    try {
+      const res: any = await getUploadRecordProgress({ folderId });
+      const percent = typeof res?.data === 'number' ? res.data : (res || 0);
+      if (percent >= 100) {
+        clearInterval(timer);
+        pollingTimers.delete(folderId);
+        processingIds.value = processingIds.value.filter(id => id !== folderId);
+      }
+    } catch (e) {
+      console.error('进度轮询失败', e);
+    }
+  }, 2000);
+  pollingTimers.set(folderId, timer);
+};
+
+const startShot = (payload?: any) => {
+  // 创建临时记录,用于拼接录制片段;支持“继续录制”携带已有文件夹信息
+  record.value = {
+    id: createTemploraryID(),
+    title: payload?.videoFolderName || '讲解视频',
+    cover: payload?.videoFolderCover || '',
+    url: payload?.videoMergeUrl || '',
+    folderId: payload?.videoFolderId,
+  };
+  isShot.value = true;
+  showBottomBar.value = true;
+};
+
 const closeHandler = () => {
-  isShot.value = false
-}
+  // 若没有片段且为临时ID,视为撤销本次录制记录
+  const current = record.value;
+  const hasFragments = current && getRecordFragmentBlobs(current).length > 0;
+  if (current && !hasFragments && isTemploraryID(current.id)) {
+    // 清理当前记录
+    record.value = {};
+  }
+  isShot.value = false;
+};
 const playVideo = (url: string | Blob) => {
   console.log(url, 9999)
   palyUrl.value = url;
@@ -123,6 +239,44 @@ const closePreview = () => {
   previewVisible.value = false;
   palyUrl.value = null;
 }
+
+const saveEditSub = async () => {
+  // 根据当前子编辑页选择对应组件保存
+  const sub = route.query.editSub as string | '';
+  let comp: any = null;
+  if (sub === 'editInspection') comp = editInspectionRef.value as any;
+  else if (sub === 'photoEdit') comp = photoEditRef.value as any;
+  else comp = editFilePageRef.value as any;
+  if (!comp || typeof comp.handleSave !== 'function') {
+    ElMessage.warning('当前页面不支持保存');
+    return;
+  }
+  try {
+    const ok = await comp.handleSave();
+    if (ok) {
+      ElMessage.success('保存成功');
+      // const newQuery: any = { ...route.query };
+      // delete newQuery.editSub;
+      // delete newQuery.type;
+      // delete newQuery.presetKey;
+      // vueRouter.replace({ path: route.path, query: newQuery });
+    }
+  } catch (e) {
+    console.error('保存失败', e);
+    ElMessage.error('保存失败,请稍后重试');
+  }
+}
+
+const backEditSub = () => {
+  const newQuery: any = { ...route.query };
+  delete newQuery.editSub;
+  delete newQuery.type;
+  delete newQuery.presetKey;
+  vueRouter.replace({ path: route.path, query: newQuery }).then(() => {
+    // 返回后刷新勘验笔录、提取清单与照片卷列表
+    try { window.dispatchEvent(new CustomEvent('fireDetails:refreshLists')); } catch (e) {}
+  });
+}
 </script>
 
 <style lang="scss">
@@ -139,6 +293,9 @@ const closePreview = () => {
   padding: 0!important;
   background: transparent!important;
   box-shadow: none!important;
+  .el-dialog__body{
+    height: 100%;
+  }
   .el-dialog__headerbtn{
     top: -30px;
     right: -30px;
@@ -158,6 +315,7 @@ const closePreview = () => {
 .new-fire-details{
   width: 100%;
   height: 100%;
+  
   :deep(.el-menu){
     padding-left: 24px;
     .el-menu-item{

+ 104 - 0
src/view/newFireCase/newFireDetails/photoEdit.vue

@@ -0,0 +1,104 @@
+<template>
+  <div v-if="visible" class="photo-edit">
+    <!-- 复用案件照片编纂组件 -->
+    <Photos :caseId="caseId" :title="title" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, ref, watch } from 'vue';
+import { useRoute } from 'vue-router';
+import Photos from '@/view/case/newphotos/index.vue';
+import { getCaseImgTagData, saveCaseImgTagData } from '@/store/case';
+import { ElMessage } from 'element-plus';
+
+const props = defineProps<{ caseId?: number; title?: string }>();
+const route = useRoute();
+
+const caseId = computed(() => props.caseId ?? Number(route.params.caseId));
+const visible = computed(() => route.query.editSub === 'photoEdit');
+const imgId = computed(() => {
+  const val = route.query.imgId;
+  return typeof val === 'string' ? Number(val) : undefined;
+});
+
+// 记录当前照片卷的标签记录ID(有则更新,无则新增)以及横排状态
+const tagId = ref<number | undefined>(undefined);
+const isHorizontal = ref<boolean>(true);
+
+const loadTagInfo = async () => {
+  if (!caseId.value) return;
+  try {
+    const res: any = await getCaseImgTagData(caseId.value!, imgId.value);
+    const data = res?.data || {};
+    tagId.value = (data?.id as number) || undefined;
+    if (typeof data?.isHorizontal === 'boolean') {
+      isHorizontal.value = data.isHorizontal as boolean;
+    }
+  } catch (e) {
+    // 不影响编辑,仅用于获取初始 id/isHorizontal
+    console.warn('获取照片卷标签信息失败', e);
+  }
+};
+
+onMounted(() => {
+  if (visible.value) loadTagInfo();
+});
+watch(visible, (v) => { if (v) loadTagInfo(); });
+
+// 暴露给外层 headerTop 的保存入口:同步标注并触发合并导出(不打开新链接)
+const handleSave = async () => {
+  try {
+    const scene: any = (window as any).scene;
+    if (!scene || !scene.player) {
+      ElMessage.warning('当前页面暂未就绪,稍后再试');
+      return false;
+    }
+
+    // 保存当前标注数据
+    const data = scene.player.getDrawData();
+    scene.player.syncDrawData();
+    await saveCaseImgTagData({
+      caseId: caseId.value!,
+      id: tagId.value,
+      imgId: imgId.value,
+      data,
+      isHorizontal: isHorizontal.value,
+    });
+
+    // 与 index.vue 保持一致:自动导出并保存合并图片,不打开新窗口
+    if ((window as any).scene && !(window as any).isExportScreenshot) {
+      (window as any).scene.exportScreenshot(true);
+    }
+
+    // 交由外层统一提示
+    return true;
+  } catch (e) {
+    console.error('保存照片卷失败', e);
+    ElMessage.error('保存失败,请稍后重试');
+    return false;
+  }
+};
+
+defineExpose({ handleSave });
+</script>
+
+<style scoped lang="scss">
+.photo-edit {
+  width: 100%;
+  height: calc(100% - 0px);
+  :deep(.photo){
+    .left{
+      padding-left: 24px;
+    }
+    .my-photo-upload{
+      .el-button{
+        width: 47%;
+        &:last-child{
+          margin-right: 0;
+        }
+      }
+    }
+  }
+}
+</style>

+ 48 - 7
src/view/newFireCase/newFireDetails/showIndex.vue

@@ -2,7 +2,7 @@
   <div class="new-fire-details">
     <el-menu :default-active="currentMenuKey" mode="horizontal" class="menu-vertical">
       <el-menu-item 
-        v-for="menu in menus" 
+        v-for="menu in preview === 'abstract' ? menusAbstract : menus" 
         :key="menu.key"
         :index="menu.key"
         @click="handleMenuClick(menu)"
@@ -12,10 +12,10 @@
     </el-menu>
 
     <template v-if="currentMenuKey === 'info'">
-      <basicInfo :fire="tempFire" :caseId="caseId" :editOrShow="'show'" />
+      <basicInfo :fire="tempFire" :caseId="caseId" :editOrShow="'show'" :fromRoute="fromRoute" />
     </template>
     <template v-if="currentMenuKey === 'screenRecord'">
-      <ScreenShot :fire="tempFire" :caseId="caseId" :editOrShow="'show'" @start="startShot" @playVideo="(url) => emit('playVideo', url)" />
+      <ScreenShot :fire="tempFire" :caseId="caseId" :editOrShow="'show'" :processingIds="processingIds" :recentAddedItem="recentAddedItem" @start="startShot" @playVideo="(url) => emit('playVideo', url)" />
     </template>
     <template v-if="currentMenuKey === 'scene'">
       <Scene :fire="tempFire" :caseId="caseId" :editOrShow="'show'" />
@@ -45,6 +45,9 @@ import OtherFiles from './components/otherFiles.vue';
 const props = defineProps<{
   caseId: number;
   currentRecord: object;
+  fromRoute: string;
+  processingIds?: (number | string)[];
+  recentAddedItem?: any | null;
 }>();
 const emit = defineEmits<{
   'start': any,
@@ -54,13 +57,17 @@ const emit = defineEmits<{
 const route = useRoute();
 const vueRouter = useRouter();
 const caseId = computed(() => Number(route.params.caseId));
+const preview = computed(() => route.query.preview as string || '');
 let tempFire = ref(null);
 watch(() => props.currentRecord, (newVal, oldVal) => {
-  console.log(newVal, newVal.tmProject, 8888)
-  tempFire.value = newVal.tmProject;
+  if(props.fromRoute === 'fire') {
+    tempFire.value = newVal.tmProject;
+  } else if(props.fromRoute === 'criminal') {
+    tempFire.value = newVal;
+  }
 })
-const startShot = () => {
-  emit("start");
+const startShot = (payload?: any) => {
+  emit("start", payload);
 }
 // 从路由查询参数中获取当前菜单项,如果没有则默认为 'info'
 let currentMenuKey = ref(route.query.tab as string || 'info');
@@ -142,4 +149,38 @@ const menus = computed(() => {
     },
   ];
 });
+const menusAbstract = 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 中设置了
+      },
+    }
+  ];
+});
 </script>

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

@@ -86,6 +86,7 @@ onMounted(() => {
       }
     } catch (error) {
       // 跳转到无权限页面
+      console.log(error)
       vueRouter.replace({
         name: RouteName.noCase,
         query: {}

+ 3 - 3
src/view/newFireCase/newdispatch/index.vue

@@ -17,7 +17,7 @@
       </template>
     </template>
     <template v-slot:appendColumn v-if="!isTeached">
-      <el-table-column label="教学项目" v-slot:default="{ row }: { row: Fire }">
+      <el-table-column label="教学项目" width="90" v-slot:default="{ row }: { row: Fire }">
         {{ row.isTeached ? "是" : "否" }}
       </el-table-column>
     </template>
@@ -25,7 +25,7 @@
     <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" />
+        <MoreMenu :caseId="row.caseId" :title="row.projectSn" :projectName="row.projectName" @copy="copy" />
         <span class="oper-span" @click="pagging.del(row)" style="color: #D8000A;" v-pdpath="['del']">
           删除
         </span>
@@ -105,7 +105,7 @@ const addHandler = async () => {
 const gotoDetails = (row: Fire) => {
   const routeData = router.resolve({
     path: `/fireDetails/${row.caseId}`,
-    query: { editOrShow: 'show' }
+    query: { editOrShow: 'show' , fromRoute: 'fire' }
   });
   window.open(routeData.href, '_blank');
 };

+ 12 - 4
src/view/newFireCase/newdispatch/list.vue

@@ -20,7 +20,7 @@
         width="50"
         :selectable="() => !!operateIsPermissionByPath(checkPerm)"
       />
-      <el-table-column label="序号" width="100" v-slot:default="{ $index }">
+      <el-table-column label="序号" width="60" v-slot:default="{ $index }">
         <span>
           {{ pagging.state.pag.size * (pagging.state.pag.currentPage - 1) + $index + 1 }}
         </span>
@@ -32,7 +32,7 @@
           effect="dark"
           :content="row.projectName"
           placement="bottom-start"
-          v-if="row.projectName && row.projectName.length > 15"
+          v-if="row.projectName && row.projectName.length > 10"
         >
           <p class="tip oper-user clickable" @click="$emit('view-item', row)" v-pdpath="['view']">{{ row.projectName }}</p>
         </el-tooltip>
@@ -70,13 +70,13 @@
           effect="dark"
           :content="row.fireReason"
           placement="bottom-start"
-          v-if="row.fireReason && row.fireReason.length > 15"
+          v-if="row.fireReason && row.fireReason.length > 10"
         >
           <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 }">
+      <el-table-column label="项目状态" width="90" v-slot:default="{ row }">
         {{ fireStatusDesc[row.status as FireStatus] }}
       </el-table-column>
       <slot name="appendColumn" />
@@ -113,4 +113,12 @@ defineEmits<{
   cursor: pointer;
   color: #26559b;
 }
+.el-table th .cell {
+  margin-left: 0;
+}
+.tip {
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+}
 </style>

+ 6 - 0
vite.config.ts

@@ -90,6 +90,12 @@ export default ({ mode }: any) => {
           target: loadEnv(mode, process.cwd()).VITE_SERVICE_URL,
           changeOrigin: true,
         },
+        '/spg': {
+          target: 'https://www.test-4dkankan.com',
+          changeOrigin: true,
+          rewrite: path => path.replace(/^\/spg/, '/spg'),
+          secure: false
+        },
         "/swss": {
           // target: dev
           //   ? "https://uat-laser.4dkankan.com/uat"

+ 5 - 0
yarn.lock

@@ -1556,6 +1556,11 @@ sass@*, sass@^1.64.2:
     immutable "^4.0.0"
     source-map-js ">=0.6.2 <2.0.0"
 
+screenfull@^6.0.2:
+  version "6.0.2"
+  resolved "https://registry.npmmirror.com/screenfull/-/screenfull-6.0.2.tgz"
+  integrity sha512-AQdy8s4WhNvUZ6P8F6PB21tSPIYKniic+Ogx0AacBMjKP1GUHN2E9URxQHtCusiwxudnCKkdy4GrHXPPJSkCCw==
+
 semver@^5.5.0, "semver@2 || 3 || 4 || 5":
   version "5.7.2"
   resolved "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz"