bill 8 bulan lalu
induk
melakukan
d09d56f289

+ 1 - 0
package.json

@@ -17,6 +17,7 @@
     "less": "^4.1.3",
     "mitt": "^3.0.0",
     "simaqcore": "^1.2.0",
+    "swiper": "^11.1.15",
     "vite-plugin-mkcert": "^1.10.1",
     "vue": "^3.2.37",
     "vue-cropper": "1.0.2",

+ 7 - 0
pnpm-lock.yaml

@@ -11,6 +11,7 @@ specifiers:
   mitt: ^3.0.0
   sass: ^1.54.3
   simaqcore: ^1.2.0
+  swiper: ^11.1.15
   typescript: ^4.6.4
   vite: ^3.0.0
   vite-plugin-mkcert: ^1.10.1
@@ -28,6 +29,7 @@ dependencies:
   less: 4.1.3
   mitt: 3.0.0
   simaqcore: 1.2.0
+  swiper: 11.1.15
   vite-plugin-mkcert: 1.10.1_vite@3.0.4
   vue: 3.2.37
   vue-cropper: 1.0.2
@@ -1098,6 +1100,11 @@ packages:
     resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
     engines: {node: '>= 0.4'}
 
+  /swiper/11.1.15:
+    resolution: {integrity: sha512-IzWeU34WwC7gbhjKsjkImTuCRf+lRbO6cnxMGs88iVNKDwV+xQpBCJxZ4bNH6gSrIbbyVJ1kuGzo3JTtz//CBw==}
+    engines: {node: '>= 4.7.0'}
+    dev: false
+
   /throttle-debounce/5.0.2:
     resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
     engines: {node: '>=12.22'}

+ 17 - 0
src/api/floder.ts

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

+ 35 - 1
src/api/folder-type.ts

@@ -3,7 +3,8 @@ import axios from './instance'
 
 export interface FloderType {
   filesTypeId: number,
-  filesTypeName: string
+  filesTypeName: string,
+  parentFilesTypeId?: number,
 }
 
 export type FloderTypes = FloderType[]
@@ -15,5 +16,38 @@ export const fetchFloderTypes = async () => {
     filesTypeId: 100,
     filesTypeName: '其他',
   })
+
+  types.push({
+    filesTypeId: 101,
+    filesTypeName: '现场照片',
+  })
+  types.push({
+    parentFilesTypeId: 101,
+    filesTypeId: 102,
+    filesTypeName: '中心现场',
+  })
+
+  types.push({
+    filesTypeId: 1000,
+    parentFilesTypeId: 102,
+    filesTypeName: '方位',
+  }, {
+    filesTypeId: 1001,
+    parentFilesTypeId: 102,
+    filesTypeName: '概貌',
+  }, {
+    filesTypeId: 1002,
+    parentFilesTypeId: 102,
+    filesTypeName: '重点部位',
+  },{
+    filesTypeId: 1001,
+    parentFilesTypeId: 103,
+    filesTypeName: '概貌',
+  },)
+  types.push({
+    parentFilesTypeId: 101,
+    filesTypeId: 103,
+    filesTypeName: '关联现场',
+  })
   return types
 }

+ 34 - 37
src/layout/show/slide-menu.vue

@@ -1,8 +1,8 @@
 <template>
   <div class="slide-menu" id="slide-menu">
-    <div 
-      v-for="item in items" 
-      :class="{active: item.name === activeName}" 
+    <div
+      v-for="item in items"
+      :class="{ active: item.name === activeName }"
       :key="item.name"
       @click="$emit('changeItem', item as any)"
     >
@@ -12,80 +12,78 @@
 </template>
 
 <script lang="ts" setup>
-import { metas, RoutesName, router, getRouteConfig } from '@/router'
-import { views, records, floders } from '@/store';
-import { computed } from 'vue';
+import { metas, RoutesName, router, getRouteConfig } from "@/router";
+import { views, records, floders } from "@/store";
+import { computed } from "vue";
 
-import type { RouteRaw } from '@/router'
-import { appType, params, routeIncludeFire } from '@/env';
+import type { RouteRaw } from "@/router";
+import { params, routeIncludeFire } from "@/env";
 
 export type MenuItem = {
-  name: RoutesName,
-  config: RouteRaw,
-} & (typeof metas)[keyof typeof metas]
-
-defineProps<{ activeName: RoutesName }>()
-defineEmits<{ (e: 'changeItem', item: MenuItem): void }>()
+  name: RoutesName;
+  config: RouteRaw;
+} & typeof metas[keyof typeof metas];
 
+defineProps<{ activeName: RoutesName }>();
+defineEmits<{ (e: "changeItem", item: MenuItem): void }>();
 
 const items = computed(() => {
   const items = [
     {
       name: RoutesName.summaryShow,
       config: getRouteConfig(RoutesName.summaryShow),
-      ...metas[RoutesName.summaryShow]
-    }
-  ]
+      ...metas[RoutesName.summaryShow],
+    },
+  ];
   if (routeIncludeFire(params.app)) {
     items.unshift({
       name: RoutesName.fireInfo,
       config: getRouteConfig(RoutesName.fireInfo),
-      ...metas[RoutesName.fireInfo]
-    }) 
+      ...metas[RoutesName.fireInfo],
+    });
   }
 
   if (views.value.length) {
     items.push({
       name: RoutesName.viewShow,
       config: getRouteConfig(RoutesName.viewShow),
-      ...metas[RoutesName.viewShow]
-    }) 
+      ...metas[RoutesName.viewShow],
+    });
   }
 
   if (records.value.length) {
     items.push({
       name: RoutesName.recordShow,
       config: getRouteConfig(RoutesName.recordShow),
-      ...metas[RoutesName.recordShow]
-    }) 
+      ...metas[RoutesName.recordShow],
+    });
   }
 
   if (floders.value.length) {
     items.push({
       name: RoutesName.folderShow,
       config: getRouteConfig(RoutesName.folderShow),
-      ...metas[RoutesName.folderShow]
-    }) 
+      ...metas[RoutesName.folderShow],
+    });
   }
 
-  return items
-})
+  return items;
+});
 </script>
 
 <style lang="scss" scoped>
-
 .slide-menu {
   width: var(--editor-menu-width);
-  filter: var(--editor-menu-filter); 
+  filter: var(--editor-menu-filter);
   background-color: var(--editor-menu-back);
   position: fixed;
   left: var(--editor-menu-left);
   top: calc(var(--editor-head-height) + var(--header-top));
   bottom: 0;
-  z-index: 2000;
+  z-index: 200;
   overflow: hidden;
   backdrop-filter: blur(4px);
-  transition: all .3s ease;
+  transition: all 0.3s ease;
 
   > div {
     height: 70px;
@@ -94,23 +92,23 @@ const items = computed(() => {
     justify-content: center;
     position: relative;
     color: rgba(255, 255, 255, 0.6);
-    transition: color .3s ease;
+    transition: color 0.3s ease;
     cursor: pointer;
 
     &::before {
-      content: '';
+      content: "";
       position: absolute;
       left: 0;
       top: 0;
       bottom: 0;
       width: 0;
       background: currentColor;
-      transition: width .3s ease;
+      transition: width 0.3s ease;
     }
 
     &.active,
     &:hover {
-      color: #00C8AF;
+      color: #00c8af;
     }
 
     &.active::before {
@@ -121,7 +119,6 @@ const items = computed(() => {
       font-size: 24px;
       color: currentColor;
     }
-    
   }
 }
-</style>
+</style>

+ 38 - 9
src/store/floder-type.ts

@@ -1,14 +1,43 @@
-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 } 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 { FloderType, FloderTypes }
+export type FloderRoot = {
+  id: number;
+  title: string;
+  floders: (ReturnType<typeof getFloderByType>[number] & { metaType: MetaType })[];
+  children?: FloderRoot[];
+};
+const gemerateRoot = (parentId?: number) => {
+  const items: FloderRoot[] = [];
+  for (let i = 0; i < floderTypes.value.length; i++) {
+    const type = floderTypes.value[i];
+    if (type.parentFilesTypeId === parentId) {
+      const item = {
+        id: type.filesTypeId,
+        title: type.filesTypeName,
+        floders: getFloderByType(type).map((floder) => ({
+          ...floder,
+          metaType: getUrlType(floder.filesUrl),
+        })),
+        children: gemerateRoot(type.filesTypeId)
+      };
+      items.push(item)
+    }
+  }
+  return items
+};
+export const floderRoots = computed(gemerateRoot);
+
+export type { FloderType, FloderTypes };

+ 3 - 1
src/store/floder.ts

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

+ 5 - 0
src/style.scss

@@ -146,4 +146,9 @@ input::-ms-clear,input::-ms-reveal {
   content:attr(placeholder);
   color:grey;
   // font-style:italic;
+}
+
+
+.ant-modal-mask {
+  background-color: rgba(0, 0, 0, 0.6) !important;
 }

+ 54 - 66
src/views/fire/index.vue

@@ -1,79 +1,67 @@
 <template>
-  <LeftPano>
-    <div class="info" v-if="caseProject?.tmProject">
-      <h2>案件信息</h2>
-      <p v-for="(label, key) in labelMap">
-        <span>{{ typeof label === "string" ? label : label[0] }}:</span>
-        {{
-          typeof label === "string"
-            ? caseProject.tmProject[key]
-            : label[1](caseProject.tmProject[key])
-        }}
-      </p>
-    </div>
-  </LeftPano>
+  <Modal
+    width="1200px"
+    :title="title"
+    @cancel="router.push({ name: RoutesName.show })"
+    :open="router.currentRoute.value.name === RoutesName.fireInfo"
+    :footer="null"
+  >
+    <Info
+      title="案件信息"
+      :data="caseProject.tmProject"
+      :label-map="tmLabelMap1"
+      v-if="caseProject?.tmProject"
+    />
+    <Info
+      title="勘验信息"
+      :data="caseProject.tmProject"
+      :label-map="tmLabelMap2"
+      v-if="caseProject?.tmProject"
+    />
+  </Modal>
 </template>
 
 <script setup lang="ts">
-import { FireProject } from "@/api";
+import { Modal } from "ant-design-vue";
+import Info from "./info.vue";
 import { showRightPanoStack } from "@/env";
 import { useViewStack } from "@/hook";
-import { LeftPano } from "@/layout";
+import router, { RoutesName } from "@/router";
+import { title } from "@/store";
 import { caseProject } from "@/store/case";
 import { ref } from "vue";
 
-type LabelMap<T extends object> = {
-  [k in keyof Required<T>]: string | [string, (v: any) => any];
-};
+type LabelMap = Record<string, string | [string, (v: any) => any]>;
 
-const labelMap = {
-  projectSn: "项目编号",
-  projectAddress: "起火地址",
-  projectSite: "起火场所",
-  organizerDeptName: "承办单位",
-  projectName: "起火对象",
-  organizerUsers: "承办人员",
-  accidentDate: "事故日期",
-  fireReason: "火灾原因",
-  statusDesc: "项目状态",
-  isTeached: ["教学项目", (v: any) => (v ? "是" : "否")],
-  creatorName: "创建人",
-  editorName: "编辑人",
-  createTime: "创建时间",
-  editTime: "最新编辑",
-} as LabelMap<FireProject>;
+const tmLabelMap1 = {
+  projectSn: "案件名称",
+  projectAddress: "立案编号",
+  projectSite: "案件类别",
+  organizerDeptName: "案发时间",
+  projectName: ["是否命案", (v: any) => (v ? "是" : "否")],
+  organizerUsers: ["是否刑件", (v: any) => (v ? "是" : "否")],
+  accidentDate: "案发区域",
+  fireReason: "案发地点",
+  statusDesc: "经纬度",
+} as LabelMap;
 
-useViewStack(() => showRightPanoStack.push(ref(false)));
-</script>
-
-<style lang="scss" scoped>
-.info {
-  h2 {
-    padding: 20px;
-    font-weight: bold;
-    display: flex;
-    justify-content: space-between;
-    border-bottom: 1px solid rgba(255, 255, 255, 0.16);
-    align-items: center;
-    margin-bottom: 0;
-  }
+const tmLabelMap2 = {
+  creatorName: "指挥中心电话时间",
+  editorName: "报警时间",
+  createTime: "现场勘验单位",
+  editTime: "指派方式",
+  statusDesc: "勘验地点",
+  fireReason: "勘验时间",
+} as LabelMap;
 
-  p {
-    padding: 0 30px;
-    margin: 20px 0;
-    color: rgba(255, 255, 255, 1);
-    font-size: 14px;
-    display: flex;
-    word-break: break-all;
+useViewStack(() => {
+  const f1 = showRightPanoStack.push(ref(false));
+  // const f2 = showLeftPanoStack.push(ref(false));
+  return () => {
+    f1();
+    // f2();
+  };
+});
+</script>
 
-    span {
-      flex: none;
-      display: inline-block;
-      width: 70px;
-      height: 100%;
-      margin-right: 20px;
-      color: rgba(255, 255, 255, 0.7);
-    }
-  }
-}
-</style>
+<style lang="scss" scoped></style>

+ 58 - 0
src/views/fire/info.vue

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

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

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

+ 39 - 15
src/views/folder/index.vue

@@ -1,8 +1,8 @@
 <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">
+    <template v-for="item in types" :key="item.id">
+      <div class="types" v-if="item.floders.length || item.children?.length">
+        <h2 @click="showHanlder(item)">
           {{ item.title }}
           <ui-icon :type="`pull-${item.show.value ? 'up' : 'down'}`" class="icon" ctrl />
         </h2>
@@ -23,29 +23,44 @@
   </LeftPano>
 
   <Preview :items="[currentFile]" v-if="currentFile" @close="currentFile = null" />
+  <Modal
+    width="800px"
+    :title="showModalRoot?.title"
+    @cancel="showModalRoot = void 0"
+    :open="!!showModalRoot"
+    :footer="null"
+  >
+    <ShowFloderRoot :root="showModalRoot" v-if="showModalRoot" @preview="preview" />
+  </Modal>
 </template>
 
 <script lang="ts" setup>
 import { LeftPano } from "@/layout";
-import { computed, ref } from "vue";
+import { computed, Ref, ref } from "vue";
 import { getUrlType, MetaType, saveAs } from "@/utils";
 import { Preview, MediaItem, MediaType } from "@/components/static-preview/index.vue";
-import { floderTypes, getFloderByType } from "@/store";
+import { floderRoots, floderTypes, getFloderByType } from "@/store";
+import { Modal } from "ant-design-vue";
+import ShowFloderRoot from "./floder-root-view.vue";
 
-import type { Floder } from "@/store";
+import type { Floder, FloderRoot } from "@/store";
 import { useViewStack } from "@/hook";
 import { showRightPanoStack } from "@/env";
 
+const showModalRoot = ref<FloderRoot>();
 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),
-    })),
-  }))
+  floderRoots.value.map((type) => {
+    let show: Ref<boolean>;
+    if (type.children?.length) {
+      show = computed(() => showModalRoot.value?.id === type.id);
+    } else {
+      show = ref(true);
+    }
+    return {
+      show,
+      ...type,
+    };
+  })
 );
 
 const typeIcons = {
@@ -55,8 +70,17 @@ const typeIcons = {
   [MetaType.audio]: "nav-edit",
 };
 
+const showHanlder = (item: FloderRoot & { show: Ref<boolean> }) => {
+  if (item.children?.length) {
+    showModalRoot.value = item;
+  } else {
+    item.show.value = !item.show.value;
+  }
+};
+
 const currentFile = ref<MediaItem | null>(null);
 const preview = async (floder: Floder) => {
+  console.log("???");
   const ext = floder.filesUrl
     .substring(floder.filesUrl.lastIndexOf("."))
     .toLocaleLowerCase();