ソースを参照

新增离线包打包命令

wangfumin 2 週間 前
コミット
1f3d50232e

+ 11 - 0
.env.offline

@@ -0,0 +1,11 @@
+VITE_SEVER_URL=http://localhost
+VITE_DEVCODE_URL=http://localhost
+VITE_SWKK_URL=http://localhost
+VITE_SERVICE_URL=http://localhost
+VITE_SWSS_URL=http://localhost
+VITE_LASER_URL=http://localhost
+VITE_FDKK_URL=http://localhost
+
+# Enable offline mode
+VITE_OFFLINE=true
+VITE_OFFLINE_DATA_PATH=/offline-data

+ 23 - 0
offline.html

@@ -0,0 +1,23 @@
+<!doctype html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <link rel="icon" type="image/ico" href="/favicon.ico" id="app-icon" />
+  <title></title>
+</head>
+
+<body>
+  <script type="text/javascript">
+    window._AMapSecurityConfig = {
+      securityJsCode: "fbf0a0f9d5cf8a65b385822dd98536b8",
+    };
+  </script>
+  <script type="text/javascript" src='//webapi.amap.com/maps?v=2.0&key=9282fa28a0363ba8a7b3c6f7d10ee4b1'></script>
+  <script src="//webapi.amap.com/ui/1.1/main.js?v=1.1.1"></script>
+  <div id="app"></div>
+  <script type="module" src="/src/main.ts"></script>
+</body>
+
+</html>

+ 2 - 0
package.json

@@ -7,6 +7,7 @@
     "dev": "vite --mode=fire",
     "devxm": "vite --mode=xmfire",
     "devcjz": "vite --mode=cjzfire",
+    "dev-offline": "vite --mode=offline",
     "build": "npm run build-quisk",
     "build-quisk": "vite build ./ --mode fire && vite build ./ --mode criminal && vite build ./ --mode xmfire && vite build ./ --mode ga",
     "build-fire": "vite build ./ --mode=fire",
@@ -14,6 +15,7 @@
     "build-xmfire": "vite build ./ --mode=xmfire",
     "build-cjzfire": "vite build ./ --mode=cjzfire",
     "build-ga": "vite build ./ --mode=ga",
+    "build-offline": "vite build ./ --mode=offline",
     "build-all": "npm-run-all --parallel build-fire build-criminal build-xmfire build-ga build-cjzfire",
     "preview": "vite preview"
   },

+ 1 - 1
src/app/cjzfire/constant.ts

@@ -5,7 +5,7 @@ import { cjzcriminalDeptId } from "@/constant/appDeptId";
 
 export const appConstant: AppConstant = {
   title: "火灾调查三维远程勘验平台",
-  desc: "Three-dimensional remote prospecting platform for fire scenes",
+  desc: "",
   ico,
   name: "cjzfire",
   banner,

+ 1 - 1
src/app/fire/constant.ts

@@ -5,7 +5,7 @@ import { fireDeptId } from "@/constant/appDeptId";
 
 export const appConstant: AppConstant = {
   title: "消防火调三维远程勘验平台",
-  desc: "Three-dimensional remote prospecting platform for fire scenes",
+  desc: "",
   ico,
   banner,
   name: "fire",

+ 1 - 1
src/app/xmfire/constant.ts

@@ -5,7 +5,7 @@ import { xmfireDeptId } from "@/constant/appDeptId";
 
 export const appConstant: AppConstant = {
   title: "火灾调查三维远程勘验平台",
-  desc: "Three-dimensional remote prospecting platform for fire scenes",
+  desc: "",
   ico,
   name: "fire",
   banner,

+ 211 - 2
src/store/editCsae.ts

@@ -1,5 +1,6 @@
 import {
   axios,
+  PaggingRes,
   recordCaseVideo,
   getFusionAndScene,
   addFusionIds,
@@ -10,15 +11,69 @@ import {
   updateRecord,
   deleteRecord,
   caseFilesTypeGetByTree,
+  // 文件相关列表接口
+  caseFileTypes,
+  caseFiles,
+  // 案件基础信息与场景列表接口
+  caseInfo,
+  caseSceneList,
+  // 场景与融合分页列表接口
+  getSceneList,
+  getMix3dList,
+  // 照片制卷列表接口
+  getFfmpegImage,
   // 多元融合离线包相关接口
   downloadOfflineFusion,
   getOfflineFusionProcess,
 } from "@/request";
 import { uploadFile } from '@/store/system';
+import { isOfflineMode, getOfflineDataPath, fetchOfflineJson } from '@/util/offline';
 
-export const getRecordCaseVideo = async (props) => (await axios.get(recordCaseVideo, { params: { ...props, ingoreRes: true } })).data;
+export const getRecordCaseVideo = async (props) => {
+  if (isOfflineMode()) {
+    const base = getOfflineDataPath();
+    const caseId = props?.caseId ?? props?.id;
+    const candidates = [
+      `${base}/record/${caseId}.json`,
+      `${base}/record-case-${caseId}.json`,
+      `${base}/case-${caseId}-record.json`,
+    ];
+    try {
+      const data: any = await fetchOfflineJson<any>(candidates);
+      return Array.isArray(data) ? data : (data?.list ?? data?.data ?? []);
+    } catch (e) {
+      return [];
+    }
+  }
+  return (await axios.get(recordCaseVideo, { params: { ...props, ingoreRes: true } })).data;
+};
 
-export const getFusionAndSceneList = async (props) => (await axios.get(getFusionAndScene, { params: props })).data;
+export const getFusionAndSceneList = async (props) => {
+  // 离线模式:优先从本地静态 JSON 读取
+  if (isOfflineMode()) {
+    const base = getOfflineDataPath();
+    const caseId = props?.caseId ?? props?.id;
+    const type = props?.type || 'scene';
+    const candidates = [
+      `${base}/case-${caseId}-${type}.json`,
+      `${base}/case-${caseId}.json`,
+      `${base}/${type}/${caseId}.json`,
+    ];
+    try {
+      const data: any = await fetchOfflineJson<any>(candidates);
+      if (Array.isArray(data)) return data;
+      if (data && Array.isArray(data[type])) return data[type];
+      if (data && Array.isArray(data.list)) return data.list;
+      if (data && Array.isArray(data.data)) return data.data;
+      return [];
+    } catch (e) {
+      // 未找到离线数据时返回空数组,避免页面阻塞
+      return [];
+    }
+  }
+  // 在线模式:走后端接口
+  return (await axios.get(getFusionAndScene, { params: props })).data;
+};
 
 export const addMix3dFusionIds = async (props) => (await axios.post(addFusionIds, props)).data;
 
@@ -91,6 +146,86 @@ export const updateRecordInfo = async (props: {
 // 获取全部文件类型
 export const caseFilesTypeGetTree = async (caseId: number) => (await axios.get(caseFilesTypeGetByTree, { params: { caseId, ingoreRes: true } })).data;
 
+// =============== 其他资料 & 勘验:列表接口(离线支持) ===============
+export const getCaseFileTypesOffline = async (): Promise<any[]> => {
+  if (isOfflineMode()) {
+    const base = getOfflineDataPath();
+    const candidates = [
+      `${base}/case-file-types.json`,
+      `${base}/files/types.json`,
+    ];
+    try {
+      const data: any = await fetchOfflineJson<any>(candidates);
+      return Array.isArray(data) ? data : (data?.list ?? data?.data ?? []);
+    } catch (e) {
+      return [];
+    }
+  }
+  return (await axios.get(caseFileTypes)).data as any[];
+};
+
+export const getCaseFilesOffline = async (props: { caseId: number; filesTypeId?: number; }): Promise<any[]> => {
+  if (isOfflineMode()) {
+    const base = getOfflineDataPath();
+    const caseId = props?.caseId;
+    const typeId = props?.filesTypeId ?? 'all';
+    const candidates = [
+      `${base}/case-files/${caseId}-${typeId}.json`,
+      `${base}/case-${caseId}-files-${typeId}.json`,
+      `${base}/files/${caseId}/${typeId}.json`,
+    ];
+    try {
+      const data: any = await fetchOfflineJson<any>(candidates);
+      return Array.isArray(data) ? data : (data?.list ?? data?.data ?? []);
+    } catch (e) {
+      return [];
+    }
+  }
+  return (await axios.get(caseFiles, { params: props })).data as any[];
+};
+
+// =============== 基础信息:案件信息(离线支持) ===============
+export const getCaseInfoOffline = async (caseId: number): Promise<any> => {
+  if (isOfflineMode()) {
+    const base = getOfflineDataPath();
+    const candidates = [
+      `${base}/case-info/${caseId}.json`,
+      `${base}/case-${caseId}-info.json`,
+      `${base}/case/${caseId}.json`,
+    ];
+    try {
+      const data: any = await fetchOfflineJson<any>(candidates);
+      if (data && typeof data === 'object') {
+        if (data.caseId) return data;
+        return data.info ?? data.data ?? data.detail ?? data.value ?? data;
+      }
+      return {};
+    } catch (e) {
+      return {};
+    }
+  }
+  return (await axios.get(caseInfo, { params: { caseId } })).data;
+};
+
+// =============== 场景:案件场景列表(离线支持) ===============
+export const getCaseSceneListOffline = async (caseId: number): Promise<any[]> => {
+  if (isOfflineMode()) {
+    const base = getOfflineDataPath();
+    const candidates = [
+      `${base}/case-scene-list/${caseId}.json`,
+      `${base}/case-${caseId}-scene-list.json`,
+      `${base}/scene/case-${caseId}.json`,
+    ];
+    try {
+      const data: any = await fetchOfflineJson<any>(candidates);
+      return Array.isArray(data) ? data : (data?.list ?? data?.data ?? []);
+    } catch (e) {
+      return [];
+    }
+  }
+  return (await axios.get(caseSceneList, { params: { caseId } })).data;
+};
+
 // =============== 多元融合离线包下载 =================
 // 触发生成/下载任务(POST,请求参数:fusionId)
 export const startOfflineFusionDownload = async (fusionId: number | string) =>
@@ -103,3 +238,77 @@ export const getOfflineFusionDownloadProcess = async (fusionId: number | string)
 // 进度完成后拉取文件(GET,返回 Blob)
 export const fetchOfflineFusionBlob = async (fusionId: number | string) =>
   (await axios.get<Blob>(downloadOfflineFusion, { params: { fusionId, ingoreRes: true }, responseType: 'blob' })).data;
+
+// =============== 场景/融合分页列表(离线支持) ===============
+type ScenePaggingParams = {
+  pageNum: number;
+  pageSize: number;
+  sceneName?: string;
+  status?: number;
+  caseId?: number;
+  deptId?: string;
+  snCode?: string;
+  cameraType?: string;
+  searchType?: any;
+  isObj?: number;
+  fusionTitle?: any;
+};
+
+export const getScenePaggingOffline = async (params: ScenePaggingParams): Promise<PaggingRes<any>> => {
+  if (isOfflineMode()) {
+    const base = getOfflineDataPath();
+    const candidates = [
+      `${base}/scene-pagging.json`,
+      `${base}/scene/pagging.json`,
+      `${base}/scene-list.json`,
+    ];
+    try {
+      const data: any = await fetchOfflineJson<any>(candidates);
+      const list = Array.isArray(data) ? data : (data?.list ?? data?.data ?? []);
+      const total = typeof data?.total === 'number' ? data.total : list.length;
+      return { list, total } as PaggingRes<any>;
+    } catch (e) {
+      return { list: [], total: 0 } as PaggingRes<any>;
+    }
+  }
+  return (await axios.get(getSceneList, { params })).data as PaggingRes<any>;
+};
+
+export const getMix3dPaggingOffline = async (params: ScenePaggingParams): Promise<PaggingRes<any>> => {
+  if (isOfflineMode()) {
+    const base = getOfflineDataPath();
+    const candidates = [
+      `${base}/mix3d-pagging.json`,
+      `${base}/fusion/pagging.json`,
+      `${base}/fusion-list.json`,
+    ];
+    try {
+      const data: any = await fetchOfflineJson<any>(candidates);
+      const list = Array.isArray(data) ? data : (data?.list ?? data?.data ?? []);
+      const total = typeof data?.total === 'number' ? data.total : list.length;
+      return { list, total } as PaggingRes<any>;
+    } catch (e) {
+      return { list: [], total: 0 } as PaggingRes<any>;
+    }
+  }
+  return (await axios.get(getMix3dList, { params })).data as PaggingRes<any>;
+};
+
+// =============== 现场勘验:照片制卷列表(离线支持) ===============
+export const getFfmpegImageListOffline = async (caseId: number): Promise<any[]> => {
+  if (isOfflineMode()) {
+    const base = getOfflineDataPath();
+    const candidates = [
+      `${base}/photos/${caseId}.json`,
+      `${base}/ffmpeg-images-${caseId}.json`,
+      `${base}/case-${caseId}-photos.json`,
+    ];
+    try {
+      const data: any = await fetchOfflineJson<any>(candidates);
+      return Array.isArray(data) ? data : (data?.list ?? data?.data ?? []);
+    } catch (e) {
+      return [];
+    }
+  }
+  return (await axios.get(getFfmpegImage, { params: { caseId } })).data as any[];
+};

+ 35 - 0
src/util/offline.ts

@@ -0,0 +1,35 @@
+export const isOfflineMode = (): boolean => {
+  try {
+    const envFlag = (import.meta as any)?.env?.VITE_OFFLINE;
+    const winFlag = (typeof window !== 'undefined') && (window as any).__OFFLINE_MODE__;
+    return String(envFlag).toLowerCase() === 'true' || !!winFlag;
+  } catch (e) {
+    return false;
+  }
+};
+
+export const getOfflineDataPath = (): string => {
+  try {
+    const p = (import.meta as any)?.env?.VITE_OFFLINE_DATA_PATH;
+    return p || '/offline-data';
+  } catch (e) {
+    return '/offline-data';
+  }
+};
+
+export const fetchOfflineJson = async <T = any>(candidatePaths: string[]): Promise<T> => {
+  const errors: any[] = [];
+  for (const p of candidatePaths) {
+    try {
+      const res = await fetch(p, { cache: 'no-store' });
+      if (!res.ok) {
+        errors.push(`${p}: ${res.status}`);
+        continue;
+      }
+      return (await res.json()) as T;
+    } catch (e) {
+      errors.push(`${p}: ${(e as Error)?.message || 'fetch failed'}`);
+    }
+  }
+  throw new Error(`offline json not found. tried: ${errors.join('; ')}`);
+};

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

@@ -131,7 +131,7 @@ import { user } from "@/store/user";
 import { Search } from "@element-plus/icons-vue";
 // 旧的选择地图逻辑已统一为 creatMap 弹窗,移除 selectMapImage
 import { Example, setExample as setCriminalExample, addExample as addCriminalExample } from "@/app/criminal/store/example";
-import { getCaseInfo } from "@/store/case";
+import { getCaseInfoOffline as getCaseInfo } from "@/store/editCsae";
 import creatMap from "./creatMap.vue";
 
 const props = defineProps<{ fire?: Fire, caseId?: number, editOrShow?: string, fromRoute?: string }>();

+ 1 - 2
src/view/newFireCase/newFireDetails/components/mix3d.vue

@@ -124,8 +124,7 @@ import { useRoute, useRouter } from 'vue-router';
 import { DocumentAdd, Edit, Delete, FullScreen } from '@element-plus/icons-vue';
 import { Scene } from '@/store/scene';
 import { SceneTypeDesc } from '@/constant/scene';
-import { getFusionAndSceneList, addMix3dFusionIds } from '@/store/editCsae';
-import { getMix3dPagging } from '@/store/scene';
+import { getFusionAndSceneList, addMix3dFusionIds, getMix3dPaggingOffline as getMix3dPagging } from '@/store/editCsae';
 import { confirm } from '@/helper/message';
 import { ElMessage } from 'element-plus';
 import { user } from "@/store/user";

+ 2 - 2
src/view/newFireCase/newFireDetails/components/otherFiles.vue

@@ -77,9 +77,9 @@ import { ref, computed, onMounted } from 'vue';
 import RelName from './relName.vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox } from 'element-plus';
-import { Upload, View, Download, Close } from '@element-plus/icons-vue';
 import { useUpload } from '@/hook/upload';
-import { getCaseFileTypes, getCaseFiles, addCaseFile, setCaseFile, delCaseFile, type CaseFile } from '@/store/caseFile';
+import { addCaseFile, setCaseFile, delCaseFile, type CaseFile } from '@/store/caseFile';
+import { getCaseFileTypesOffline as getCaseFileTypes, getCaseFilesOffline as getCaseFiles } from '@/store/editCsae';
 import { confirm } from '@/helper/message';
 
 const props = defineProps<{ caseId?: number; fire?: any, editOrShow?: string }>();

+ 2 - 2
src/view/newFireCase/newFireDetails/components/scene.vue

@@ -152,10 +152,10 @@ import screenfull from 'screenfull';
 import { useRoute, useRouter } from 'vue-router';
 import { DocumentAdd, Edit, Delete, FullScreen } from '@element-plus/icons-vue';
 import comPagination from "@/components/pagination/index.vue";
-import { Scene, SceneType, getScenePagging, getSWKKSyncLink } from '@/store/scene';
+import { Scene, SceneType, getSWKKSyncLink } from '@/store/scene';
+import { getScenePaggingOffline as getScenePagging, getFusionAndSceneList } from '@/store/editCsae';
 import { SceneTypeDesc } from '@/constant/scene';
 import { getCaseScenes, replaceCaseScenes, getSceneKey, getCaseScenesBySceneType } from '@/store/case';
-import { getFusionAndSceneList } from '@/store/editCsae';
 import { confirm } from '@/helper/message';
 import { ElMessage } from 'element-plus';
 

+ 4 - 4
src/view/newFireCase/newFireDetails/components/siteInspection.vue

@@ -350,16 +350,16 @@ import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import RelName from './relName.vue';
 // 弹窗已移除,直接跳转到绘制页面
-import { getCaseFiles, CaseFile, BoardType, setCaseFile, delCaseFile, getCaseFileImageInfo } from '@/store/caseFile';
+import { CaseFile, BoardType, setCaseFile, delCaseFile, getCaseFileImageInfo } 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 { getFfmpegImageList, exportCaseInquestInfo, exportCaseDetailInfo, caseDel } from '@/store/case';
+import { exportCaseInquestInfo, exportCaseDetailInfo, caseDel } from '@/store/case';
+import { getFfmpegImageListOffline as getFfmpegImageList, caseFilesTypeGetTree, getCaseFilesOffline as getCaseFiles } from '@/store/editCsae';
 import { getCaseInquestInfo, getCaseDetailInfo } from '@/store/case';
 import { axios } from '@/request';
 import { saveAs } from '@/util/file-serve';
-import { caseFilesTypeGetTree } from '@/store/editCsae';
 
 const appId = import.meta.env.VITE_APP_APP || 'fire';
 const url = import.meta.env.VITE_DRAW_URL || 'http://mix3d.4dkankan.com';
@@ -953,7 +953,7 @@ 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 : [];
+    const list: any[] = Array.isArray(res) ? res : [];
     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}`;

+ 2 - 2
src/view/newFireCase/newFireDetails/index.vue

@@ -52,7 +52,8 @@ import { useRoute, useRouter } from 'vue-router';
 import md5 from 'js-md5';
 import showIndex from './showIndex.vue';
 import editIndex from './editIndex.vue';
-import { copyCase, getCaseSceneList, getCaseInfo, updateCaseInfo } from "@/store/case";
+import { copyCase, updateCaseInfo } from "@/store/case";
+import { getCaseInfoOffline as getCaseInfo, getCaseSceneListOffline as getCaseSceneList, uploadRecordFragments, getUploadRecordProgress } from "@/store/editCsae";
 import { RouteName, router } from "@/router";
 import shot from './components/shot.vue';
 import headerTop from './components/headerTop.vue';
@@ -66,7 +67,6 @@ import {
   isTemploraryID,
   createTemploraryID,
 } from '@/store/system'
-import { uploadRecordFragments, getUploadRecordProgress } from '@/store/editCsae'
 
 // 从路由获取参数
 const appId = import.meta.env.VITE_APP_APP === 'criminal' ? 'criminal' : 'fire'

+ 1 - 0
vite.config.ts

@@ -17,6 +17,7 @@ export default ({ mode }: any) => {
         input: {
           index: resolve(__dirname, "index.html"),
           map: resolve(__dirname, "map.html"),
+          offline: resolve(__dirname, "offline.html"),
           // 在这里继续添加更多页面
         },
       },