gemercheung 6 месяцев назад
Родитель
Сommit
9b8573a27f

+ 45 - 3
src/api/floder.ts

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

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

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

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

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

+ 59 - 40
src/api/instance.ts

@@ -1,19 +1,21 @@
-import { axiosFactory } from './setup'
-import { Message } from 'bill/index'
-import { showLoad, hideLoad } from '@/utils'
-import * as URL from './constant'
-import { ResCode, ResCodeDesc } from './constant'
-import { appBackRoot, appType, baseURL, params } from '@/env'
+import { axiosFactory } from "./setup";
+import { Dialog, Message } from "bill/index";
+import { showLoad, hideLoad } from "@/utils";
+import * as URL from "./constant";
+import { ResCode, ResCodeDesc } from "./constant";
+import { baseURL, params } from "@/env";
+import GAxios from "axios";
 
-const instance = axiosFactory()
+const instance = axiosFactory();
 
 export const {
-  axios,
+  axios, 
   addUnsetTokenURLS,
   delUnsetTokenURLS,
   addReqErrorHandler,
   addResErrorHandler,
   delReqErrorHandler,
+  addUnsetResErrorURLS,
   delResErrorHandler,
   getToken,
   setToken,
@@ -21,50 +23,67 @@ export const {
   setDefaultURI,
   addHook,
   delHook,
-  setHook
-} = instance
+  setHook,
+} = instance;
 
 const gotoLogin = () => {
-  const loginHref = import.meta.env.DEV ? 'http://localhost:5174' : appBackRoot[params.app]
-  location.href = loginHref + '?redirect=' + escape(location.href) + '#/login'
-}
+  if (import.meta.env.DEV) {
+    GAxios.post("/service/manage/login", {
+      password: "MRinIEn3ExMjM0NTY=Q5Lm39urQWzN7k4oCG",
+      userName: "super-admin",
+      username: "super-admin",
+    }).then((res) => {
+      setToken(res.data.data.token)
+      setTimeout(() => location.reload())
+    });
+  } else {
+    const loginHref = `/admin/#/statistics/scene`
+    location.href = loginHref + '?redirect=' + escape(location.href)
+  }
+};
 
-addReqErrorHandler(err => {
+addReqErrorHandler((err) => {
   // Message.error(err.message)
-  console.error(err)
-  hideLoad()
-  // gotoLogin()
-})
+  console.error(err);
+  hideLoad();
+  gotoLogin();
+});
 
-addResErrorHandler(
-  (response, data) => {
-    if (response && response.status !== 200) {
-      Message.error(response.statusText)
-    } else if (data) {
-      const msg = data.code && ResCodeDesc[data.code] ? ResCodeDesc[data.code] : (data?.message || data?.msg)
-      if (data.code === ResCode.TOKEN_INVALID) {
-        gotoLogin()
-      } else {
-        Message.error(msg)
-      }
+addResErrorHandler((response, data) => {
+  if (response && response.status !== 200) {
+    Message.error(response.statusText);
+  } else if (data) {
+    const msg =
+      data.code && ResCodeDesc[data.code]
+        ? ResCodeDesc[data.code]
+        : data?.message || data?.msg;
+    if (data.code === ResCode.TOKEN_INVALID) {
+      gotoLogin();
+    } else if (data.code === ResCode.UN_AUTH) {
+      Dialog.alert({content: msg, okText: '我知道了'}).then(() => {
+        gotoLogin();
+      })
+      throw msg
+    } else {
+      Message.error(msg || '服务出现异常,请稍后再试');
     }
   }
-)
+});
 
-addHook({ 
+addHook({
   before: (config) => {
     if (config.url !== URL.RECORD_STATUS) {
-      showLoad()
+      showLoad();
     }
-  }, 
+  },
   after: (config) => {
     if (!config || config.url !== URL.RECORD_STATUS) {
-      hideLoad()
+      hideLoad();
     }
-  } 
-})
-
-setDefaultURI(baseURL)
-params.token && setToken(params.token)
+  },
+});
 
-export default axios
+setDefaultURI(baseURL);
+const token = params.token || localStorage.getItem('token')
+token && setToken(token);
+export default axios;

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

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

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

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

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

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

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

@@ -57,7 +57,8 @@ import { computed, ref, watchEffect, watch, onUnmounted } from 'vue'
 import { router, RoutesName } from '@/router'
 import UIBubble from 'bill/components/bubble/index.vue'
 import Images from '@/views/tagging/images.vue'
-import Preview, { MediaType } from '../static-preview/index.vue'
+import Preview from '../static-preview/index.vue'
+import { MetaType } from "@/utils";
 import { getTaggingStyle, getFuseModel } from '@/store';
 import { getFileUrl } from '@/utils'
 import { sdk } from '@/sdk'

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

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

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

@@ -1,14 +1,71 @@
-import { ref } from 'vue'
-import { fetchFloderTypes } from '@/api'
+import { computed, ref } from "vue";
+import { fetchFloderTypes } from "@/api";
 
-import type { FloderTypes, FloderType } from '@/api'
+import type { FloderTypes, FloderType, Floder } from "@/api";
+import { getFloderByType } from "./floder";
+import { getUrlType, MetaType } from "@/utils";
 
-export const floderTypes = ref<FloderTypes>([])
-export const getFloderType = (id: FloderType['filesTypeId']) => 
-  floderTypes.value.find(type => type.filesTypeId === id)
+export const floderTypes = ref<FloderTypes>([]);
+export const getFloderType = (id: FloderType["filesTypeId"]) =>
+  floderTypes.value.find((type) => type.filesTypeId === id);
 
 export const initialFloderTypes = async () => {
-  floderTypes.value = await fetchFloderTypes()
+  floderTypes.value = await fetchFloderTypes();
+};
+
+export type FloderRoot = {
+  flat: boolean,
+  modal: boolean
+  id: number;
+  title: string;
+  floders: (ReturnType<typeof getFloderByType>[number] & { metaType: MetaType })[];
+  children?: FloderRoot[];
+};
+const gemerateRoot = (parentId: number | null = null) => {
+  const items: FloderRoot[] = [];
+  for (let i = 0; i < floderTypes.value.length; i++) {
+    const type = floderTypes.value[i];
+    if (type.parentId === parentId || (type.parentId === undefined && parentId === null)) {
+      const item = {
+        id: type.filesTypeId,
+        title: type.filesTypeName,
+        flat: !!type.flatShow,
+        modal: !!type.modalShow,
+        floders: getFloderByType(type).map((floder) => {
+          !floder.filesUrl &&console.log(floder.filesUrl)
+          return{
+          ...floder,
+          metaType: floder.filesUrl && getUrlType(floder.filesUrl),
+        }
+        }),
+        children: gemerateRoot(type.filesTypeId)
+      };
+      items.push(item)
+    }
+  }
+  return items
+};
+export const floderRoots = computed(gemerateRoot);
+
+export const getLevelRoot = (floder: Floder, roots = floderRoots.value): FloderRoot | undefined => {
+  for (const root of roots) {
+    if (root.floders.some(f => f.filesId === floder.filesId)) {
+      return root;
+    } else if (root.children?.length) {
+      const cRoot = getLevelRoot(floder, root.children)
+      if (cRoot) {
+        return cRoot
+      }
+    }
+  }
+}
+
+export const getFlatFloders = (root: FloderRoot, floders: FloderRoot['floders'] = []) => {
+  floders.push(...root.floders)
+  if (root.children?.length) {
+    root.children.forEach(child => getFlatFloders(child, floders))
+  }
+  return floders
 }
 
-export type { FloderType, FloderTypes }
+export type { FloderType, FloderTypes };

+ 5 - 3
src/store/floder.ts

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

+ 21 - 2
src/utils/meta.ts

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

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

@@ -0,0 +1,69 @@
+<template>
+  <Modal
+    width="1200px"
+    :title="title"
+    @cancel="$emit('update:open', false)"
+    :open="open"
+    :footer="null"
+  >
+    <Info
+      title="案件信息"
+      :data="caseProject"
+      :label-map="tmLabelMap1"
+      v-if="caseProject"
+    />
+    <Info
+      title="勘验信息"
+      :data="caseProject.tmProject"
+      :label-map="tmLabelMap2"
+      v-if="caseProject?.tmProject"
+    />
+  </Modal>
+</template>
+
+<script setup lang="ts">
+import { Modal } from "ant-design-vue";
+import Info from "./info.vue";
+import { showRightPanoStack } from "@/env";
+import { useViewStack } from "@/hook";
+import router, { RoutesName } from "@/router";
+import { title } from "@/store";
+import { caseProject } from "@/store/case";
+import { ref } from "vue";
+
+defineProps<{ open: boolean }>();
+defineEmits<{ (e: "update:open", v: boolean): void }>();
+
+type LabelMap = Record<string, string | [string, (v: any) => any]>;
+
+const tmLabelMap1 = {
+  caseTitle: "案件名称",
+  caseNum: "立案编号",
+  caseCategory: "案件类别",
+  crimeTime: "案发时间",
+  homicideCase: ["是否命案", (v: any) => (v === null ? "" : v ? "是" : "否")],
+  criminalCase: [
+    "是否刑案",
+    (v: any) => {
+      console.error(v);
+      return v === null ? "" : v ? "是" : "否";
+    },
+  ],
+  caseRegion: ["案发区域", (v: string[]) => v.join("-")],
+  caseAddress: "案发地点",
+  latAndLong: ["经纬度", (v: any) => (v ? v.split(",").reverse().join(",") : "")],
+} as LabelMap;
+
+const tmLabelMap2 = {
+  commandTime: "指挥中心电话时间",
+  alarmTime: "报警时间",
+  alarmName: "报警人",
+  assignDept: "指派/报告单位",
+  inquestDept: "现场勘验单位",
+  assignType: "指派方式",
+  inquestAddress: "勘验地点",
+  times: ["勘验时间", (v: string[]) => v.join("到")],
+} as LabelMap;
+</script>
+
+<style lang="scss" scoped></style>

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

@@ -0,0 +1,59 @@
+<template>
+  <div class="info" v-if="data">
+    <h2>{{ title }}</h2>
+    <div>
+      <p v-for="(label, key) in labelMap">
+        <span>{{ typeof label === "string" ? label : label[0] }}:</span>
+        {{ typeof label === "string" ? data[key] : label[1](data[key]) }}
+      </p>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+defineProps<{
+  title: string;
+  data: Record<string, any>;
+  labelMap: Record<string, string | [string, (v: any) => any]>;
+}>();
+</script>
+
+<style lang="scss" scoped>
+.info {
+  margin-bottom: 30px;
+  h2 {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 10px;
+    font-size: 16px;
+  }
+  > div {
+    display: flex;
+    flex-wrap: wrap;
+
+    p {
+      width: 33.33%;
+    }
+  }
+
+  p {
+    margin: 10px 0;
+    padding: 0 7px;
+    color: rgba(255, 255, 255, 1);
+    font-size: 14px;
+    display: flex;
+    word-break: break-all;
+
+    span {
+      flex: none;
+      display: inline-block;
+      width: 70px;
+      text-align: right;
+      height: 100%;
+      margin-right: 10px;
+      color: rgba(255, 255, 255, 0.7);
+    }
+  }
+}
+</style>

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

@@ -0,0 +1,178 @@
+<template>
+  <div
+    :class="{ root: index === 1 }"
+    class="tree"
+    v-if="getFlatFloders(root).length !== 0"
+  >
+    <div
+      class="solid header"
+      :class="{ ['root-header']: index === 1 }"
+      :style="{ '--index': index }"
+      @click="showChildren = !showChildren"
+    >
+      <span> {{ root.title }} </span>
+      <ui-icon
+        :type="`pull-${showChildren ? 'up' : 'down'}`"
+        class="icon"
+        ctrl
+        v-if="floders.length || root.children?.length"
+      />
+    </div>
+
+    <template v-if="!root.modal && showChildren && (floders.length || children?.length)">
+      <div class="items" :class="{ ['root-items']: index === 1 }">
+        <template v-if="floders.length">
+          <div
+            :style="{ '--index': index }"
+            v-for="floder in floders"
+            :key="floder.filesId"
+            class="fun-ctrl solid item"
+            @click="$emit('preview', [floder, root])"
+          >
+            <ui-icon :type="typeIcons[floder.metaType]" v-if="floder.metaType" />
+            <p>{{ floder.filesTitle }}</p>
+          </div>
+        </template>
+        <template v-if="children?.length">
+          <FloderView
+            v-for="item in children"
+            @preview="(v: any) => emit('preview', v)"
+            :root="item"
+            :index="index + 1"
+          />
+        </template>
+      </div>
+    </template>
+  </div>
+
+  <Modal
+    v-if="root.modal"
+    width="1200px"
+    :title="root.title"
+    @cancel="showChildren = false"
+    :open="showChildren"
+    :footer="null"
+  >
+    <div class="modal-root-content">
+      <ModalFloderView :root="root" @preview="(v) => emit('preview', v)" />
+    </div>
+  </Modal>
+</template>
+<script lang="ts" setup>
+import { Floder, FloderRoot, getFlatFloders } from "@/store";
+import { computed, ref, toRaw, watchEffect } from "vue";
+import { MetaType } from "@/utils";
+import ModalFloderView from "./modal-floder-view.vue";
+import { Modal } from "ant-design-vue";
+
+const props = defineProps<{ root: FloderRoot; index?: number }>();
+const emit = defineEmits<{ (e: "preview", v: [Floder, FloderRoot]): void }>();
+
+const index = props.index || 1;
+const typeIcons = {
+  [MetaType.image]: "pic",
+  [MetaType.video]: "a-film",
+  [MetaType.other]: "nav-edit",
+  [MetaType.audio]: "nav-edit",
+  [MetaType.xfile]: "nav-edit",
+};
+
+const floders = computed(() => {
+  if (props.root.flat) {
+    return getFlatFloders(props.root);
+  } else {
+    return props.root.floders;
+  }
+});
+const children = computed(() => {
+  if (props.root.flat || props.root.modal) {
+    return [];
+  } else {
+    return props.root.children;
+  }
+});
+const showChildren = ref(props.root.modal ? false : true);
+watchEffect(() => {
+  if (showChildren.value) {
+    console.log(toRaw(props.root));
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.tree {
+  margin-bottom: 0;
+}
+.modal-root-content {
+  max-height: 700px;
+  overflow-y: auto;
+}
+.root-items {
+  background: rgba(0, 0, 0, 0.5);
+}
+
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 0;
+  cursor: pointer;
+  padding: 20px 0;
+  position: relative;
+  padding-left: calc(var(--index) * 20px);
+  padding-right: 20px;
+
+  span {
+    font-weight: normal;
+    font-size: 14px;
+  }
+
+  &.root-header {
+    padding: 20px;
+    span {
+      font-weight: bold;
+      font-size: 16px;
+    }
+  }
+
+  .icon {
+    font-size: 14px;
+  }
+}
+
+.solid {
+  &::after {
+    content: "";
+    position: absolute;
+    left: 20px;
+    right: 20px;
+    height: 1px;
+    background: rgba(255, 255, 255, 0.16);
+    bottom: 0;
+  }
+}
+
+.item {
+  margin-right: 20px;
+  padding: 20px 0;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  position: relative;
+  padding-left: calc(var(--index) * 20px);
+  padding-right: 0;
+
+  &.solid::after {
+    right: 0;
+  }
+
+  p {
+    margin-left: 10px;
+    font-size: 12px;
+    color: currentColor;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    overflow: hidden;
+  }
+}
+</style>

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

@@ -1,90 +1,74 @@
 <template>
   <LeftPano>
-    <template v-for="item in types">
-      <div :key="item.id" class="types" v-if="item.floders.length">
-        <h2 @click="item.show.value = !item.show.value">
-          {{item.title}}
-          <ui-icon :type="`pull-${item.show.value ? 'up' : 'down'}`" class="icon" ctrl />
-        </h2>
-
-        <div class="floders"  v-if="item.show.value">
-          <div 
-            v-for="floder in item.floders" 
-            :key="floder.filesId" 
-            class="fun-ctrl" 
-            @click="preview(floder)"
-          >
-            <ui-icon :type="typeIcons[floder.metaType]" v-if="floder.metaType" />
-            <p>{{ floder.filesTitle }}</p>
-          </div>
-        </div>
-      </div>
+    <div class="types">
+      <h2 @click="showInfo = true">
+        案件概要
+        <ui-icon :type="`pull-${showInfo ? 'up' : 'down'}`" class="icon" ctrl />
+      </h2>
+    </div>
+    <template v-for="item in floderRoots" :key="item.id">
+      <FloderView :root="item" @preview="(v) => preview(v)" />
     </template>
   </LeftPano>
 
-  <Preview :items="[currentFile]" v-if="currentFile" @close="currentFile = null" />
+  <Preview
+    :items="currentItems"
+    :current="currentNdx"
+    v-if="~currentNdx"
+    @close="currentNdx = -1"
+  />
+  <Fire v-model:open="showInfo" />
 </template>
 
 <script lang="ts" setup>
-import { LeftPano } from '@/layout'
-import { computed, ref } from 'vue';
-import { getUrlType, MetaType } from '@/utils'
-import { Preview, MediaItem, MediaType } from '@/components/static-preview/index.vue'
-import { floderTypes, getFloderByType } from '@/store'
-
-import type { Floder } from '@/store'
-import { useViewStack } from '@/hook';
-import { showRightPanoStack } from '@/env';
+import { LeftPano } from "@/layout";
+import { ref } from "vue";
+import { getUrlType, MetaType, saveAs } from "@/utils";
+import { Preview, MediaItem } from "@/components/static-preview/index.vue";
+import { floderRoots, getFlatFloders } from "@/store";
+import FloderView from "./floder-view.vue";
+import Fire from "./fire/index.vue";
 
-const types = computed(() => 
-  floderTypes.value.map(type => ({
-    show: ref(true),
-    id: type.filesTypeId,
-    title: type.filesTypeName,
-    floders: getFloderByType(type)
-      .map(floder => ({
-        ...floder,
-        metaType: getUrlType(floder.filesUrl)
-      })),
-  }))
-)
+import type { Floder, FloderRoot } from "@/store";
+import { useViewStack } from "@/hook";
+import { showRightPanoStack } from "@/env";
 
-const typeIcons = {
-  [MetaType.image]: 'pic',
-  [MetaType.video]: 'a-film',
-  [MetaType.other]: 'nav-edit'
-}
-
-const currentFile = ref<MediaItem | null>(null)
-const preview = (floder: Floder) => {
-  const type = getUrlType(floder.filesUrl)
-  const mediaType = type === MetaType.image 
-      ? MediaType.img 
-      : type === MetaType.video
-        ? MediaType.video
-        : null
-  
-  if (!mediaType) {
-    window.open(floder.filesUrl)
-  } else {
-    currentFile.value = {
-      type: mediaType,
-      url: floder.filesUrl
+const showInfo = ref(false);
+const currentNdx = ref(-1);
+const currentItems = ref<MediaItem[]>([]);
+const preview = async ([floder, root]: [Floder, FloderRoot]) => {
+  const metaType = getUrlType(floder.filesUrl);
+  if (metaType === MetaType.other) {
+    const isBlob = floder.filesUrl.includes("blob");
+    if (floder.filesTypeId === 100) {
+      saveAs(floder.filesUrl, floder.filesTitle + ".doc");
+    } else {
+      window.open(floder.filesUrl + (!isBlob ? "?time=" + Date.now() : ""));
     }
+  } else {
+    const floders = root.flat ? getFlatFloders(root) : root.floders;
+    const items = floders.map((item) => ({
+      type: getUrlType(item.filesUrl),
+      id: item.filesId,
+      url: item.filesUrl,
+    }));
+    currentNdx.value = items.findIndex((item) => item.id === floder.filesId);
+    currentItems.value = items;
   }
-}
-useViewStack(() => showRightPanoStack.push(ref(false)))
-
+};
+useViewStack(() => showRightPanoStack.push(ref(false)));
 </script>
 
 <style lang="scss" scoped>
 .types {
   h2 {
-    padding: 20px;
+    padding: 20px 0;
+    margin: 0 20px;
+    font-size: 16px;
     font-weight: bold;
     display: flex;
     justify-content: space-between;
-    border-bottom: 1px solid rgba(255,255,255,0.16);
+    border-bottom: 1px solid rgba(255, 255, 255, 0.16);
     align-items: center;
     margin-bottom: 0;
     cursor: pointer;
@@ -96,12 +80,12 @@ useViewStack(() => showRightPanoStack.push(ref(false)))
 }
 
 .floders {
-  background: rgba(0,0,0,0.5);
+  background: rgba(0, 0, 0, 0.5);
 
   > div {
     margin: 0 20px;
     padding: 20px 10px;
-    border-bottom: 1px solid rgba(255,255,255,0.16);
+    border-bottom: 1px solid rgba(255, 255, 255, 0.16);
     cursor: pointer;
     display: flex;
     align-items: center;
@@ -110,8 +94,8 @@ useViewStack(() => showRightPanoStack.push(ref(false)))
       margin-left: 10px;
       font-size: 12px;
       color: currentColor;
-      word-break:break-all;
+      word-break: break-all;
     }
   }
 }
-</style>
+</style>

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

@@ -0,0 +1,136 @@
+<template>
+  <ui-group v-if="floders.length">
+    <ui-group-option>
+      <span @click="canAll && (showAll = !showAll)" :class="{ ['fun-ctrl']: canAll }">
+        <template v-if="canAll">
+          <UpOutlined v-if="showAll" />
+          <DownOutlined v-else />
+        </template>
+        {{ root.title }}
+      </span>
+    </ui-group-option>
+    <ui-group-option>
+      <div class="items">
+        <div
+          class="img-item"
+          v-for="(_, i) in showLen"
+          :key="floders[i].filesId"
+          :style="{ '--rawLen': samLen }"
+        >
+          <div class="img-item-content">
+            <div>
+              <Sign
+                focus
+                :media="{ url: floders[i].filesUrl }"
+                @click="clickHandler(floders[i])"
+              />
+            </div>
+            <!-- <img :src="floders[i].filesUrl"  /> -->
+          </div>
+        </div>
+      </div>
+    </ui-group-option>
+  </ui-group>
+
+  <Tabs v-if="!emptyTabs" v-model:activeKey="activeTab" class="f-tabs">
+    <template v-for="children in root.children">
+      <TabPane
+        :tab="children.title"
+        :key="children.id"
+        v-if="getFlatFloders(children).length"
+      >
+        <ModalFloderView :root="children" @preview="(v: any) => emit('preview', v)" />
+      </TabPane>
+    </template>
+  </Tabs>
+  <template v-for="c in children" :key="c.id" v-else>
+    <ModalFloderView
+      :root="c"
+      v-if="isLastLevel(c)"
+      @preview="(v: any) => emit('preview', v)"
+    />
+  </template>
+</template>
+<script lang="ts" setup>
+import { Floder, FloderRoot, getFlatFloders } from "@/store";
+import { computed, ref } from "vue";
+import { TabPane, Tabs } from "ant-design-vue";
+import { DownOutlined, UpOutlined } from "@ant-design/icons-vue";
+import Sign from "@/components/static-preview/sign.vue";
+
+const props = defineProps<{ root: FloderRoot }>();
+const emit = defineEmits<{ (e: "preview", v: [Floder, FloderRoot]): void }>();
+const isLastLevel = (root: FloderRoot) => {
+  return !root.children?.length;
+};
+const emptyTabs = computed(
+  () => props.root.children?.every((r) => isLastLevel(r)) && !props.root.flat
+);
+const oneTabs = computed(() => {
+  if (!emptyTabs.value) return null;
+  return props.root.children!.find((i) => !isLastLevel(i));
+});
+const clickHandler = (floder: Floder) => {
+  emit("preview", [floder, props.root]);
+};
+const activeTab = ref(oneTabs.value?.id);
+const floders = computed(() => {
+  if (props.root.flat) {
+    return getFlatFloders(props.root);
+  } else {
+    return props.root.floders;
+  }
+});
+const children = computed(() => {
+  if (props.root.flat) {
+    return [];
+  } else {
+    return props.root.children;
+  }
+});
+const len = computed(() => floders.value.length);
+const showAll = ref(false);
+const samLen = 6;
+const showLen = computed(() => (showAll.value ? len.value : Math.min(samLen, len.value)));
+const canAll = computed(() => len.value > samLen);
+</script>
+
+<style lang="scss" scoped>
+.items {
+  display: flex;
+  flex-wrap: wrap;
+}
+.img-item {
+  width: calc(100% / var(--rawLen));
+  padding-right: 5px;
+  .img-item-content {
+    padding-top: 56.25%;
+    position: relative;
+    cursor: pointer;
+
+    > div {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      left: 0;
+      top: 0;
+      object-fit: cover;
+    }
+  }
+}
+
+.mySwiper {
+  --swiper-pagination-fraction-color: #000;
+  --swiper-theme-color: #03ad98;
+  --swiper-navigation-size: 30px;
+}
+</style>
+
+<style>
+.f-tabs.ant-tabs-top > .ant-tabs-nav {
+  margin-bottom: 30px;
+}
+.f-tabs.ant-tabs-top > .ant-tabs-nav::before {
+  display: none !important;
+}
+</style>