Jelajahi Sumber

Merge branch 'xj' of http://192.168.0.115:3000/bill/public-fuse into xj

bill 1 tahun lalu
induk
melakukan
4ed62c4b69

+ 1 - 0
package.json

@@ -13,6 +13,7 @@
     "@amap/amap-jsapi-loader": "^1.0.1",
     "@element-plus/icons-vue": "^2.1.0",
     "@types/qs": "^6.9.7",
+    "@vueuse/core": "^10.11.0",
     "@vueuse/router": "^10.11.0",
     "axios": "^1.4.0",
     "echarts": "^5.4.3",

+ 17 - 4
src/app/map/App.vue

@@ -50,6 +50,7 @@
 
 <script setup lang="ts">
 import { onMounted, ref, computed, onBeforeMount } from "vue";
+// import { useRouteQuery } from "@vueuse/router";
 import AMapLoader from "@amap/amap-jsapi-loader";
 import axios from "axios";
 import { getFuseCodeLink } from "../../view/case/help";
@@ -58,11 +59,17 @@ import { reactive } from "vue";
 import { watch } from "vue";
 import { ElLoading } from "element-plus";
 import linkIco from "@/assets/image/fire.ico";
+import { useUrlSearchParams } from "@vueuse/core";
+
+const params = useUrlSearchParams("history");
+console.log("params", params.caseId);
 
 const current = ref(0);
 const list = ref<any>([]);
+
 const state = reactive({
-  deptId: "",
+  // deptId: "",
+  caseId: params.caseId || "",
 });
 
 const link = document.querySelector<HTMLLinkElement>("#app-icon")!;
@@ -101,7 +108,7 @@ const getDataQuest = () => {
       {
         pageNum: 1,
         pageSize: 10000,
-        deptId: state.deptId,
+        caseId: state.caseId,
       }
     );
     console.log("res.data", res);
@@ -215,7 +222,10 @@ const loadMap = async () => {
   initMakers();
 };
 
-onMounted(loadMap);
+onMounted(() => {
+  // console.log("caseId", caseId);
+  loadMap();
+});
 const handCardClick = (id: number) => {
   const url = getQuery(id, true);
   console.log("url", url);
@@ -245,10 +255,13 @@ body {
 
 .tabbar .nav {
   display: flex;
-  background: white;
+  /* background: white; */
   justify-content: center;
   align-items: center;
 }
+.el-form-item {
+  margin-bottom: 0px !important;
+}
 
 .tabbar .nav .nav_item {
   padding: 10px;

+ 11 - 1
src/core/Scene.js

@@ -93,12 +93,22 @@ export default class Scene extends Mitt {
       if (
         String(obj.name).includes("marker_") ||
         String(obj.name).includes("line_") ||
-        String(obj.name).includes("line_point_")
+        String(obj.name).includes("line_point_") ||
+        String(obj.name).includes("circle_")
       ) {
         this.scene.remove(obj);
       }
     }
   }
+
+  deleteItemById(uuid) {
+    for (var i = this.scene.children.length - 1; i >= 0; i--) {
+      let obj = this.scene.children[i];
+      if (obj.uuid === uuid) {
+        this.scene.remove(obj);
+      }
+    }
+  }
   loadLight = () => {
     const light = new THREE.AmbientLight(0xffffff, 1.5); // 柔和的白光
     this.scene.add(light);

+ 60 - 0
src/core/box/object/CircleTextLabel.js

@@ -0,0 +1,60 @@
+import * as THREE from "three";
+import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
+import { LineSegments2 } from "three/examples/jsm/lines/LineSegments2.js";
+import { LineGeometry } from "three/examples/jsm/lines/LineGeometry.js";
+
+export default class CircleTextLabel extends THREE.Mesh {
+  constructor(text, outline) {
+    let res = 5;
+    const canvas = document.createElement("canvas");
+    canvas.width = 128 * res;
+    canvas.height = 128 * res;
+    let fontSize = 68 * res;
+    const ctx = canvas.getContext("2d");
+    ctx.font = `800 ${fontSize}px Arial`; // 设置字体大小和类型
+    ctx.textAlign = "center";
+    ctx.textBaseline = "middle";
+    ctx.fillStyle = "#e44d54"; // 设置文字颜色和透明度
+    ctx.fillText(text, canvas.width / 2, canvas.height / 2);
+
+    // 步骤3: 将画布转换为纹理
+    const texture = new THREE.CanvasTexture(canvas);
+
+    // 步骤4: 创建材质并应用纹理
+    const m = new THREE.MeshBasicMaterial({
+      map: texture,
+      transparent: true, // 允许材质透明
+    });
+
+    // const canvas_map = new THREE.Texture(canvas);
+    texture.colorSpace = THREE.SRGBColorSpace;
+    texture.needsUpdate = true;
+    texture.anisotropy = 4;
+
+    const g = new THREE.CircleGeometry(0.08, 128);
+    g.rotateX(-Math.PI / 2);
+
+    super(g, m);
+
+    this.userData = text;
+
+    const edges = new THREE.EdgesGeometry(g, 50);
+
+    const geometry = new LineGeometry();
+    geometry.fromEdgesGeometry(edges);
+
+    const line_m = new LineMaterial({
+      color: 0xe44d54,
+      linewidth: 5,
+    });
+
+    line_m.resolution.set(window.innerWidth, window.innerHeight);
+
+    const line_n = new LineSegments2(geometry, line_m);
+
+    line_n.position.y += 0.5;
+
+    this.add(line_n);
+    this.name = "circle_" + text;
+  }
+}

+ 42 - 0
src/core/box/object/PureTextLabel.js

@@ -0,0 +1,42 @@
+import * as THREE from "three";
+
+export default class PureTextLabel extends THREE.Mesh {
+  constructor(text, outline) {
+    let res = 5;
+    const width = 150 * res;
+    const height = 15 * res;
+    var canvas = document.createElement("canvas");
+    canvas.width = width;
+    canvas.height = height;
+    let fontFamily = "Arial";
+    let fontSize = 7 * res;
+    let offsetX = 75 * res;
+    let offsetY = 10 * res;
+    var context = canvas.getContext("2d");
+
+    context.fillStyle = "transparent";
+    context.rect(0, 0, width, height);
+    context.fill();
+    context.font = "normal " + fontSize + "px " + fontFamily;
+    context.fillStyle = "#000000";
+    context.textAlign = "center";
+    context.fillText(text, offsetX, offsetY);
+    const canvas_map = new THREE.Texture(canvas);
+    canvas_map.colorSpace = THREE.SRGBColorSpace;
+    canvas_map.needsUpdate = true;
+    canvas_map.anisotropy = 4;
+
+    const g = new THREE.PlaneGeometry(1.5, 0.15);
+    g.rotateX(-Math.PI / 2);
+
+    // const texture = new THREE.CanvasTexture(canvas_map);
+
+    const m = new THREE.MeshBasicMaterial({
+      map: canvas_map,
+      transparent: true, // 允许材质透明
+    });
+    super(g, m);
+
+    this.name = "textlabel_" + text;
+  }
+}

+ 1 - 1
src/core/box/object/marker.js

@@ -17,7 +17,7 @@ export default class Marker extends THREE.Mesh {
     this.rotation.y = 0;
     this.position.y = 5;
     this.position.z -= 0.02;
-
+    this.userData = startPoint;
     this.visible = true;
     this.scale.set(1, 1, 1);
     this.position.y += 0.5;

+ 135 - 18
src/core/player/Player.js

@@ -2,10 +2,13 @@ import * as THREE from "three";
 
 import FloorplanControls from "../controls/FloorplanControls.js";
 import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
+
 import { TrackballControls } from "three/examples/jsm/controls/TrackballControls.js";
 import Line from "../box/object/Line";
 import LinePoints from "../box/object/LinePoints.js";
 import Marker from "../box/object/marker.js";
+import CircleTextLabel from "../box/object/CircleTextLabel.js";
+import PureTextLabel from "../box/object/PureTextLabel.js";
 import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
 
 const convertScreenToNDC = function (event, domElement) {
@@ -33,16 +36,21 @@ export default class Player {
     this.drawLine = null;
     this.startObj = null;
     this.marker = null;
-    this.allowDrawing = false;
+    this.symbol = null;
+    this.symbolIndex = 0;
+    this.text = null;
+    this.showText = "文本";
+    this.selectItem = null;
 
     this.drawing = false;
     this.inited = false;
     this.renderLines = [];
     this.renderMarkers = [];
     this.activeEdges = [];
+    this.renderSymbols = [];
     this.matLine = null;
     this.lineColor = 0xe44d54;
-    // 1是画线,2是标方向
+    // 1是画线,2是标方向, 3符号, 4文本
     this.mode = 0;
     this.init();
   }
@@ -53,6 +61,40 @@ export default class Player {
       this.reset();
       this.setEditMode();
     }
+    // 2方向
+    if (mode === 2) {
+      let pos = new THREE.Vector3(0, 0, -1);
+      pos.unproject(this.orthCamera);
+      pos.y = 5;
+
+      this.marker = new Marker(pos);
+      this.marker.visible = false;
+      this.scene.scene.add(this.marker);
+      this.drawing = true;
+    }
+    //符号
+    if (mode === 3) {
+      let pos = new THREE.Vector3(0, 0, -1);
+      pos.unproject(this.orthCamera);
+      pos.y = 5;
+      this.symbolIndex += 1;
+      this.symbol = new CircleTextLabel(this.symbolIndex, true);
+      this.symbol.visible = false;
+      this.scene.scene.add(this.symbol);
+      console.log("this.symbol", this.symbol);
+      this.drawing = true;
+    }
+
+    if (mode === 4) {
+      let pos = new THREE.Vector3(0, 0, -1);
+      pos.unproject(this.orthCamera);
+      pos.y = 5;
+      this.text = new PureTextLabel(this.showText, true);
+      this.text.visible = false;
+      this.scene.scene.add(this.text);
+      this.drawing = true;
+    }
+
     if (mode === 0) {
       this.setFreeMode();
     }
@@ -129,10 +171,25 @@ export default class Player {
   };
 
   onPointerMove = (e) => {
-    if (!this.drawing) return;
+    // console.log("intersects", intersects);
+    // if (this.mode === 0) {
+    //   const intersects = this.raycaster.intersectObjects(
+    //     this.scene.scene.children,
+    //     true
+    //   );
+    //   intersects.forEach((i) => {
+    //     if (String(i.object.name).includes("marker")) {
+    //       // console.log("i.object.name", i.object);
+    //       // debugger
+    //     }
+    //   });
+    // }
     this.pointermove = convertScreenToNDC(e, this.scene.domElement);
+    this.raycaster.setFromCamera(this.pointermove, this.orthCamera);
+
+    if (!this.drawing) return;
+
     if (this.mode === 1) {
-      this.raycaster.setFromCamera(this.pointermove, this.orthCamera);
       let intersectArr = this.scene.boxManager.imgList;
       // if(this.startObj) {
       //   let i = intersectArr.indexOf(this.startObj)
@@ -146,18 +203,55 @@ export default class Player {
     }
     if (this.mode === 2) {
       if (this.marker) {
+        this.marker.visible = true;
         let pos = new THREE.Vector3(this.pointermove.x, this.pointermove.y, -1);
         pos.unproject(this.orthCamera);
         pos.y = 5;
-        // console.log("pos", pos);
         this.marker.position.copy(pos);
       }
     }
+
+    if (this.mode === 3) {
+      if (this.symbol) {
+        this.symbol.visible = true;
+        let pos = new THREE.Vector3(this.pointermove.x, this.pointermove.y, -1);
+        pos.unproject(this.orthCamera);
+        pos.y = 5;
+        this.symbol.position.copy(pos);
+      }
+    }
+
+    if (this.mode === 4) {
+      if (this.text) {
+        this.text.visible = true;
+        let pos = new THREE.Vector3(this.pointermove.x, this.pointermove.y, -1);
+        pos.unproject(this.orthCamera);
+        pos.y = 5;
+        this.text.position.copy(pos);
+      }
+    }
   };
 
   onPointerDown = (e) => {
     console.log("start draw");
     this.pointerdown = convertScreenToNDC(e, this.scene.domElement);
+
+    if (this.mode === 0) {
+      const intersects = this.raycaster.intersectObjects(
+        this.scene.scene.children,
+        true
+      );
+      intersects.forEach((i) => {
+        if (
+          String(i.object.name).includes("marker") ||
+          String(i.object.name).includes("line")
+        ) {
+          this.selectItem = i.object;
+          this.scene.emit("confirmDelete", i.object.uuid);
+        }
+      });
+    }
+
     if (this.mode === 1) {
       this.raycaster.setFromCamera(this.pointerdown, this.orthCamera);
       let intersectArr = this.scene.boxManager.imgList;
@@ -173,15 +267,7 @@ export default class Player {
     }
 
     if (this.mode === 2) {
-      if (!this.marker) {
-        let pos = new THREE.Vector3(this.pointerdown.x, this.pointerdown.y, -1);
-        pos.unproject(this.orthCamera);
-        pos.y = 5;
-        this.marker = new Marker(pos);
-        this.scene.scene.add(this.marker);
-        this.drawing = true;
-      } else {
-        // this.drawing = false;
+      if (this.marker) {
         this.raycaster.setFromCamera(this.pointerdown, this.orthCamera);
         let intersectArr = this.scene.boxManager.imgList;
         const intersects = this.raycaster.intersectObjects(intersectArr, false);
@@ -214,15 +300,47 @@ export default class Player {
           }
 
           console.log("activeMarkeritem", activeMarkeritem);
+          this.setMode(0);
         }
       }
     }
-    // this.floorplanControls.enabled = false;
+
+    if (this.mode === 3) {
+      if (this.symbol) {
+        let lasPos = new THREE.Vector3(
+          this.pointerdown.x,
+          this.pointerdown.y,
+          -1
+        );
+        const activeSymbolItem = {
+          id: this.symbolIndex,
+          point: lasPos.toArray(),
+        };
+        this.renderSymbols.push(activeSymbolItem);
+        this.setMode(0);
+      }
+    }
+    if (this.mode === 4) {
+      if (this.text) {
+        let lasPos = new THREE.Vector3(
+          this.pointerdown.x,
+          this.pointerdown.y,
+          -1
+        );
+        this.scene.emit("lockText");
+        this.drawing = false;
+        // const activeSymbolItem = {
+        //   id: this.symbolIndex,
+        //   point: lasPos.toArray(),
+        // };
+
+        // this.setMode(0);
+      }
+    }
   };
   onPointerUp = (e) => {
     this.pointerup = convertScreenToNDC(e, this.scene.domElement);
-    console.log("onPointerUp", this.pointerup);
-
+    // console.log("onPointerUp", this.pointerup);
     if (this.mode === 1) {
       this.drawing = false;
       this.floorplanControls.enabled = true;
@@ -425,7 +543,6 @@ export default class Player {
     this.scene.emit("data", data);
   }
   load(type, data) {
-
     if (type === 1) {
       console.log("data1", data);
       const { hor_activeEdges, hor_lines, hor_markers } = data;

+ 1 - 0
src/request/urls.ts

@@ -239,6 +239,7 @@ export const onLineCheck = "/web/fireProject/onLineCheck";
 // 照片制卷
 export const caseApiList = "/fusion-xj/caseImg/list";
 export const saveApiOrUpdate = "/fusion-xj/caseImg/saveOrUpdate";
+export const uploadImagesAndSave = "/fusion-xj/caseImg/addBatch";
 export const caseApiDel = "/fusion-xj/caseImg/delete";
 export const caseApiUpdateSort = "/fusion-xj/caseImg/updateSort";
 

+ 16 - 0
src/store/case.ts

@@ -9,6 +9,7 @@ import {
   updateCaseFile,
   caseApiList,
   saveApiOrUpdate,
+  uploadImagesAndSave,
   caseApiDel,
   caseApiUpdateSort,
   caseInquestInfo,
@@ -25,6 +26,7 @@ import { ModelScene, QuoteScene, Scene, SceneType } from "./scene";
 import { CaseFile } from "./caseFile";
 
 export type Case = {
+  value: Case;
   caseId: number;
   caseTitle: string;
   createTime: string;
@@ -42,6 +44,17 @@ export type CaseImg = {
   sort: number | null;
 };
 
+export type filesItem = {
+  caseId: number;
+  imgInfo: string | null;
+  imgUrl: string | null;
+};
+
+export type AllSaveFile = {
+  imgUrls: filesItem[];
+};
+
+
 export const setCaseSharePWD = (params: { caseId: number; randCode: string }) =>
   axios.post(setCasePsw, params);
 
@@ -100,6 +113,9 @@ export const caseImgList = (caseId: number, orderBy: string | null) =>
 export const saveOrUpdate = (params: CaseImg) =>
   axios.post(saveApiOrUpdate, { ...params });
 
+export const saveOrAndSave = (params: AllSaveFile) =>
+  axios.post(uploadImagesAndSave, { ...params });
+
 export const caseDel = (id: number) => axios.post(caseApiDel, { id });
 
 export const caseUpdateSort = (list: [CaseImg]) =>

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

@@ -0,0 +1,190 @@
+<template>
+  <el-form
+    ref="form"
+    :model="caseFile"
+    label-width="90px"
+    class="camera-from dispatch-file-from"
+  >
+    <el-form-item label="照片:" class="mandatory">
+      <el-upload
+        class="upload-demo"
+        :multiple="true"
+        :limit="10"
+        :before-upload="upload"
+        v-model:file-list="fileList"
+        :http-request="httpsApi"
+        :on-preview="previewFile"
+        :accept="photoFormats"
+        :on-remove="handleRemove"
+      >
+        <el-button type="primary" :disabled="!!file">
+          <el-icon><Upload /></el-icon>上传
+        </el-button>
+        <template v-slot:tip>
+          <div class="el-upload__tip">注:可上传{{ size }}以内的{{ photoFormatDesc }}</div>
+        </template>
+        <template v-slot:file="{ file }">
+          <div class="file" @click.stop="previewFile()">
+            <div>
+              <el-icon><Document /></el-icon>
+              <span class="name">{{ file.name }}</span>
+            </div>
+            <el-icon @click.stop="removeFile(file)"><Close /></el-icon>
+          </div>
+        </template>
+      </el-upload>
+    </el-form-item>
+    <!-- <el-form-item label="附件标题:" class="mandatory">
+      <el-input
+        v-model="caseFile.imgInfo"
+        placeholder="请输入最多不能超过50字"
+        maxlength="50"
+        show-word-limit
+      ></el-input>
+    </el-form-item> -->
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import {
+  DrawFormatDesc,
+  DrawFormats,
+  photoFormats,
+  photoFormatDesc,
+  FileDrawType,
+  OtherFormatDesc,
+  OtherFormats,
+} from "@/constant/caseFile";
+import { uploadFile } from "@/store/system";
+import { maxFileSize } from "@/constant/caseFile";
+import { useUpload } from "@/hook/upload";
+import { saveOrAndSave, CaseImg } from "@/store/case";
+import { ElMessage } from "element-plus";
+import { computed, ref, watch, watchEffect } from "vue";
+import { QuiskExpose } from "@/helper/mount";
+import type { UploadProps } from 'element-plus'
+
+const props = defineProps<{
+  caseId: number;
+  data: CaseImg;
+}>();
+
+const defaultUpload = (
+  file: File,
+  onPercentage: (percentage: number) => void
+) => {
+  onPercentage(100);
+};
+
+const caseFile = ref<CaseImg>({
+  caseId: props.caseId,
+  id: props.data?.id,
+  imgUrl: props.data.imgUrl,
+  imgInfo: props.data.imgInfo,
+  sort: props.data?.sort || '',
+} as any);
+
+const { size, previewFile, file, accept, percentage, format } = useUpload({
+  maxSize: maxFileSize,
+  formats: DrawFormats,
+});
+
+const upload = async (file: File) => {
+    const fileType = file.name
+      .substring(file.name.lastIndexOf("."))
+      .toUpperCase();
+
+    if (!DrawFormats.some((type) => type.toUpperCase() === fileType)) {
+      ElMessage.error(`请上传${format.value}`);
+      return false;
+    } else if (file.size > maxFileSize) {
+      ElMessage.error(`请上传${size.value}以内的文件`);
+      return false;
+    } else {
+      console.log('file', file)
+      fileList.value.push(file);
+      await defaultUpload(
+        file,
+        (val) => (percentage.value = val)
+      );
+      if (fileType === ".RAW") {
+      }
+      percentage.value = undefined;
+      return true;
+    }
+  };
+const fileList = ref([])
+const formatDesc = computed(() =>
+DrawFormatDesc
+);
+watch(props, newValue => {
+  caseFile.value.id = newValue.data.id;
+  caseFile.value.imgInfo = newValue.data.imgInfo;
+  caseFile.value.imgUrl = newValue.data.imgUrl;
+  caseFile.value.sort = newValue.data.sort;
+  if(newValue.data.imgUrl){
+    file.value = {
+    name: newValue.data.imgInfo || '',
+    url: newValue.data.imgUrl || '',
+  }
+  }
+},{ immediate: true })
+watchEffect(() => {
+  if (file.value?.name) {
+    caseFile.value.imgInfo = file.value?.name.substring(0, 50);
+  }
+});
+const handleRemove: UploadProps['onRemove'] = (uploadFile, uploadFiles) => {
+  console.log(uploadFile, uploadFiles)
+}
+const removeFile = (file) => {
+  fileList.value = fileList.value.filter((item) => item.raw.url !== file.raw.url);
+};
+const httpsApi = async ({file})=> {
+  console.log('httpsApi', file)
+  let fileUrl = await uploadFile(file);
+
+  file.url = fileUrl
+  console.log('httpsApi', file, fileUrl)
+}
+
+defineExpose<QuiskExpose>({
+  async submit() {
+    console.log('defineExpose', fileList.value)
+    if (!fileList.value.length) {
+      ElMessage.error("请上传照片");
+      throw "请上传照片";
+    }
+    console.log('defineExpose', caseFile.value, file.value)
+    let imgUrls = fileList.value.map(item => {
+      return {
+        imgUrl: item.raw && item.raw.url,
+        imgInfo: item.name.replace(/\.[^/.]+$/, ""),
+        caseId: props.caseId,
+      }
+    })
+    await saveOrAndSave({ imgUrls });
+    return caseFile.value;
+  },
+});
+</script>
+
+<style scoped lang="scss">
+.upload-demo {
+  overflow: hidden;
+}
+
+.file {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  > div {
+    display: flex;
+    align-items: center;
+  }
+
+  .name {
+    margin-left: 10px;
+  }
+}
+</style>

+ 37 - 10
src/view/case/caseFile.vue

@@ -1,15 +1,20 @@
 <template>
-  <com-head :options="options" v-model="currentTypeId" notContent v-if="options.length" />
+  <com-head
+    :options="options"
+    v-model="currentTypeId"
+    notContent
+    v-if="options.length"
+  />
 
   <div class="body-layer">
     <template v-if="currentTypeId === 2">
       <Photos :caseId="caseId" />
     </template>
     <template v-else-if="currentTypeId === 3">
-      <Records :caseId="caseId" />
+      <Records :caseId="caseId" :title="caseInfoData.caseTitle" />
     </template>
     <template v-else-if="currentTypeId === 5">
-      <Manifest :caseId="caseId" />
+      <Manifest :caseId="caseId" :title="caseInfoData.caseTitle" />
     </template>
     <template v-else>
       <div class="body-head">
@@ -23,17 +28,27 @@
               创建{{ BoardTypeDesc[BoardType.scene] }}
             </el-button>
           </template>
-          <el-button type="primary" @click="addCaseFileHandler"> 上传 </el-button>
+          <el-button type="primary" @click="addCaseFileHandler">
+            上传
+          </el-button>
         </div>
       </div>
 
-      <el-table :data="files" tooltip-effect="dark" style="width: 100%" size="large">
+      <el-table
+        :data="files"
+        tooltip-effect="dark"
+        style="width: 100%"
+        size="large"
+      >
         <el-table-column label="序号" width="70" v-slot:default="{ $index }">
           <div style="text-align: center">
             {{ $index + 1 }}
           </div>
         </el-table-column>
-        <el-table-column label="名称" v-slot:default="{ row }: { row: CaseFile }">
+        <el-table-column
+          label="名称"
+          v-slot:default="{ row }: { row: CaseFile }"
+        >
           <span v-if="!inputCaseTitles.includes(row)">
             {{ row.filesTitle }}
             <el-icon class="edit-title" @click="inputCaseTitles.push(row)">
@@ -57,7 +72,10 @@
           </template>
         </el-table-column>
         <el-table-column label="创建时间" prop="createTime"></el-table-column>
-        <el-table-column label="操作" v-slot:default="{ row }: { row: CaseFile }">
+        <el-table-column
+          label="操作"
+          v-slot:default="{ row }: { row: CaseFile }"
+        >
           <span class="oper-span" @click="query(row)"> 查看 </span>
           <span
             class="oper-span"
@@ -102,6 +120,7 @@ const caseId = computed(() => {
     return Number(caseId);
   }
 });
+const caseInfoData = ref<any>();
 
 const inputCaseTitles = ref<CaseFile[]>([]);
 
@@ -110,13 +129,18 @@ const updateFileTitle = async (caseFile: CaseFile) => {
     return ElMessage.error("卷宗标题不能为空!");
   }
   await updateCaseInfo(caseFile);
-  inputCaseTitles.value = inputCaseTitles.value.filter((item) => item !== caseFile);
+  inputCaseTitles.value = inputCaseTitles.value.filter(
+    (item) => item !== caseFile
+  );
 };
 
 const currentTypeId = ref<number>();
 const types = ref<CaseFileType[]>([]);
 const options = computed(() =>
-  types.value.map((item) => ({ name: item.filesTypeName, value: item.filesTypeId }))
+  types.value.map((item) => ({
+    name: item.filesTypeName,
+    value: item.filesTypeId,
+  }))
 );
 const isDraw = computed(() => currentTypeId.value === FileDrawType);
 
@@ -130,7 +154,9 @@ const refresh = async () => {
 watchEffect(() => caseId.value && currentTypeId.value && refresh());
 
 const query = (file: CaseFile) => {
-  const ext = file.filesUrl.substring(file.filesUrl.lastIndexOf(".")).toLocaleLowerCase();
+  const ext = file.filesUrl
+    .substring(file.filesUrl.lastIndexOf("."))
+    .toLocaleLowerCase();
   if ([".raw", ".dcm"].includes(ext)) {
     window.open(
       `/xfile-viewer/index.html?file=${file.filesUrl}&name=${file.filesTitle}&time=` +
@@ -164,6 +190,7 @@ onMounted(async () => {
   currentTypeId.value = types.value[0].filesTypeId;
   const caseInfo = await getCaseInfo(caseId.value!);
   if (caseInfo) {
+    caseInfoData.value = caseInfo;
     title.value = (await getCaseInfo(caseId.value!)).caseTitle + " | 卷宗管理";
     desc.value = "";
   } else {

+ 84 - 10
src/view/case/photos/index.vue

@@ -1,8 +1,20 @@
 <template>
-  <div class="photo">
+  <div class="photo ">
     <div class="left">
-      <div class="upload">
-        <el-button type="primary" @click="addCaseFileHandler">
+      <div class="upload my-photo-upload">
+          <!-- <el-upload
+          v-model:file-list="fileList"
+          class="upload-demo"
+          multiple
+          :show-file-list="false"
+          :http-request="handleRequest"
+          :on-change="handleChange"
+          :before-upload="handleUpload"
+          :limit="10"
+        >
+          <el-button type="primary">上传照片</el-button>
+        </el-upload> -->
+        <el-button type="primary" @click="addCaseFileHandlerAll">
           上传照片
         </el-button>
         <el-button
@@ -22,14 +34,15 @@
     </div>
     <div class="right">
       <div class="tools">
-        <el-button @click="handleMark">标注方向</el-button>
-        <el-button @click="handleLine">标注连线</el-button>
+        <el-button @click="handleMark">箭头</el-button>
+        <el-button @click="handleLine">标引</el-button>
+        <el-button @click="handleSymbol">符号</el-button>
+        <el-button @click="handleText">文本</el-button>
         <el-button @click="handleSave" type="success">保存</el-button>
 
         <el-button @click="handleClear" v-if="hasDrawData" type="warning"
           >清空</el-button
         >
-
         <el-button @click="handleFree" v-if="isShowExitEdit" type="warning"
           >退出编辑</el-button
         >
@@ -77,13 +90,15 @@ import { Menu, FullScreen } from "@element-plus/icons-vue";
 import { Swiper, SwiperSlide } from "swiper/vue";
 import "swiper/css";
 // import { addCaseFile } from "@/store/caseFile";
-import { addCaseImgFile } from "../quisk";
+import { addCaseImgFile, addCaseImgFileAll } from "../quisk";
 import { saveCaseImgTagData, getCaseImgTagData } from "@/store/case";
 import Scene from "@/core/Scene.js";
 import draggable from "./draggable.vue";
-import { ElMessage } from "element-plus";
+import { ElMessage, ElMessageBox } from "element-plus";
+
 const props = defineProps({ caseId: Number });
 const newlist = ref([]);
+const fileList = ref([]);
 const swiperRef = ref(null);
 const childRef = ref(null);
 const caseId = ref(props.caseId);
@@ -106,6 +121,20 @@ const addCaseFileHandler = async () => {
   });
   refresh();
 };
+
+const addCaseFileHandlerAll = async () => {
+  await addCaseImgFileAll({
+    caseId: caseId.value,
+    data: {
+      imgUrl: "",
+      imgInfo: "",
+      id: "",
+      sort: "",
+    },
+  });
+  refresh();
+};
+
 function refresh() {
   console.log("changeList", childRef.value);
 
@@ -165,6 +194,16 @@ const renderCanvas = () => {
   scene.on("markerExist", () => {
     ElMessage.error("该案件已有方向标注!");
   });
+  scene.on("confirmDelete", async (uuid) => {
+    const res = await ElMessageBox.confirm("是否删除该部件?", "温馨提示", {
+      confirmButtonText: "确定",
+      cancelButtonText: "取消",
+      type: "default",
+    });
+    if (res) {
+      window.scene.deleteItemById(uuid);
+    }
+  });
   scene.on("data", (data) => {
     let hasData = false;
     Object.keys(data).forEach((key) => {
@@ -185,6 +224,16 @@ const onSwiper = (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);
@@ -199,14 +248,25 @@ const handleDetele = async (item) => {
     refresh();
   }
 };
+const handleLine = () => {
+  if (window.scene) {
+    window.scene.player.setMode(1);
+  }
+};
 const handleMark = () => {
   if (window.scene) {
     window.scene.player.setMode(2);
   }
 };
-const handleLine = () => {
+
+const handleSymbol = () => {
   if (window.scene) {
-    window.scene.player.setMode(1);
+    window.scene.player.setMode(3);
+  }
+};
+const handleText = () => {
+  if (window.scene) {
+    window.scene.player.setMode(4);
   }
 };
 const handleSave = async () => {
@@ -246,6 +306,19 @@ onMounted(() => {
   // } catch (error) {}
 });
 </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%;
@@ -264,6 +337,7 @@ onMounted(() => {
     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 {

+ 7 - 1
src/view/case/quisk.ts

@@ -1,4 +1,5 @@
 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";
@@ -17,7 +18,12 @@ export const addCaseFile = quiskMountFactory(AddCaseFile, {
 });
 
 export const addCaseImgFile = quiskMountFactory(addPhotoFile, {
-  title: "上传附件",
+  title: "上传照片",
+  width: 500,
+});
+
+export const addCaseImgFileAll = quiskMountFactory(addPhotoFileAll, {
+  title: "上传照片",
   width: 500,
 });
 

+ 2 - 2
src/view/case/records/index.vue

@@ -320,7 +320,7 @@ import {
 } from "@/store/case";
 import { ElMessage } from "element-plus";
 import saveAs from "@/util/file-serve";
-const props = defineProps({ caseId: Number });
+const props = defineProps({ caseId: Number, title: String });
 
 console.log(props);
 const isDisableExport = ref(false);
@@ -449,7 +449,7 @@ const handleSave = async () => {
 const handleExport = async () => {
   const res = await exportCaseInquestInfo(props.caseId);
   console.log("res", res);
-  saveAs(res, `勘验笔录-${props.caseId}.docx`);
+  saveAs(res, `${props.title}_勘验笔录.docx`);
 };
 </script>
 

+ 160 - 124
src/view/case/records/manifest.vue

@@ -1,37 +1,59 @@
 <template>
-
   <div class="records">
     <div class="header">
       <el-button type="primary" @click="handleSave">保存</el-button>
-      <el-button :disabled="isDisableExport" @click="handleExport">导出</el-button>
+      <el-button :disabled="isDisableExport" @click="handleExport"
+        >导出</el-button
+      >
     </div>
 
     <div class="content">
       <div class="line">
         <span>起火单位/地址:</span>
-        <el-input class="input" v-model="data.address" placeholder="" style="width: 100%;" />
-
+        <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;" />
+          <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;" />
+          <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;" />
+          <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">
+        <template v-for="(item, index) in data.detail">
           <div class="con">
             <span class="sub-tit">编号 {{ index + 1 }}: </span>
             <div class="info">
@@ -56,23 +78,16 @@
                   <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>
@@ -84,11 +99,20 @@
         <template v-for="extractUser in data.extractUser">
           <div class="line">
             <span>姓名:</span>
-            <el-input class="input" v-model="extractUser.name" placeholder="" style="width: 20%" />
+            <el-input
+              class="input"
+              v-model="extractUser.name"
+              placeholder=""
+              style="width: 20%"
+            />
             <span>工作单位:</span>
-            <el-input class="input" v-model="extractUser.address" placeholder="" style="width: 70%" />
+            <el-input
+              class="input"
+              v-model="extractUser.address"
+              placeholder=""
+              style="width: 70%"
+            />
           </div>
-
         </template>
       </div>
       <div class="btn-container">
@@ -102,126 +126,147 @@
             <!-- <span class="sub-tit">证人信息:</span> -->
             <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;" />
+              <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%;" />
+              <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="addwitnessInfo">+新增</el-button>
       </div>
-      <div>
-      </div>
+      <div></div>
     </div>
   </div>
-
 </template>
 <script setup>
-import { onMounted, ref, watch } from 'vue';
-import { reactive } from 'vue'
+import { onMounted, ref, watch } from "vue";
+import { reactive } from "vue";
 import {
   getCaseDetailInfo,
   saveCaseDetailInfo,
-  exportCaseDetailInfo
+  exportCaseDetailInfo,
 } from "@/store/case";
 import saveAs from "@/util/file-serve";
-import { ElMessage } from 'element-plus';
+import { ElMessage } from "element-plus";
 
-const props = defineProps({ caseId: Number })
+const props = defineProps({ caseId: Number, title: String });
 
-const isDisableExport = ref(false)
+const isDisableExport = ref(false);
 
-console.log(props)
+console.log(props);
 
 const data = reactive({
   address: "",
   time: {
     year: "",
     month: "",
-    day: ""
+    day: "",
   },
 
-  location: '',
-  detail: [{
-    // id: "1",
-    name: "",
-    spec: "",
-    num: "",
-    part: "",
-    desc: "",
-  },
-  {
-    // id: "2",
-    name: "",
-    spec: "",
-    num: "",
-    part: "",
-    desc: "",
-  }],
+  location: "",
+  detail: [
+    {
+      // id: "1",
+      name: "",
+      spec: "",
+      num: "",
+      part: "",
+      desc: "",
+    },
+    {
+      // id: "2",
+      name: "",
+      spec: "",
+      num: "",
+      part: "",
+      desc: "",
+    },
+  ],
   extractUser: [
     {
       name: "",
       workplace: "",
-      id: ""
+      id: "",
     },
 
     {
       name: "",
       address: "",
-      id: ""
+      id: "",
     },
   ],
 
-  witnessInfo: [{
-    name: "",
-    address: "",
-    phone: '',
-    id: "",
-
-  }, {
-    name: "",
-    address: "",
-    phone: '',
-    id: ""
-  }]
-})
-
-watch(data, newValue => {
-  // data.userName = newValue.userName.replace(/[^0-9]/g, '');
-  const sMonth = newValue.time.month.replace(/[^0-9]/g, '');
-  const sDay = newValue.time.day.replace(/[^0-9]/g, '');
-
-  data.time.year = newValue.time.year.replace(/[^0-9]/g, '');
-  data.time.month = Number(sMonth) > 12 ? '12' : sMonth;
-  data.time.day = Number(sDay) > 31 ? '31' : sDay;
-
-
-}, {
-  immediate: true,
-  deep: true
-})
-
+  witnessInfo: [
+    {
+      name: "",
+      address: "",
+      phone: "",
+      id: "",
+    },
+    {
+      name: "",
+      address: "",
+      phone: "",
+      id: "",
+    },
+  ],
+});
+
+watch(
+  data,
+  (newValue) => {
+    // data.userName = newValue.userName.replace(/[^0-9]/g, '');
+    const sMonth = newValue.time.month.replace(/[^0-9]/g, "");
+    const sDay = newValue.time.day.replace(/[^0-9]/g, "");
+
+    data.time.year = newValue.time.year.replace(/[^0-9]/g, "");
+    data.time.month = Number(sMonth) > 12 ? "12" : sMonth;
+    data.time.day = Number(sDay) > 31 ? "31" : sDay;
+  },
+  {
+    immediate: true,
+    deep: true,
+  }
+);
 
-onMounted(() => {
-})
+onMounted(() => {});
 
 const addwitnessInfo = () => {
   data.witnessInfo.push({
     name: "",
     address: "",
-    phone: '',
-    id: ""
-  })
-}
+    phone: "",
+    id: "",
+  });
+};
 const addItem = () => {
   data.detail.push({
     // id: "1",
@@ -230,43 +275,41 @@ const addItem = () => {
     num: "",
     part: "",
     desc: "",
-  })
-}
+  });
+};
 const addextractUser = () => {
   data.extractUser.push({
     name: "",
     address: "",
-    id: ""
-  })
-}
+    id: "",
+  });
+};
 const handleSave = async () => {
-  console.log('data', data)
+  console.log("data", data);
   const res = await saveCaseDetailInfo(props.caseId, data);
   if (res.code === 0) {
-    ElMessage.success('保存成功!')
+    ElMessage.success("保存成功!");
   }
-}
+};
 const handleExport = async () => {
   const res = await exportCaseDetailInfo(props.caseId);
-  console.log('res', res)
-  saveAs(res, `提取清单-${props.caseId}.docx`)
-}
+  console.log("res", res);
+  saveAs(res, `${props.title}_提取清单.docx`);
+};
 onMounted(async () => {
   const res = await getCaseDetailInfo(props.caseId);
 
-  console.log('res', res)
+  console.log("res", res);
   for (var k in data) {
     if (!res.data) {
-      isDisableExport.value = true
+      isDisableExport.value = true;
     }
     if (res.data && res.data.hasOwnProperty(k)) {
       // console.log("Key is " + k)
-      data[k] = res.data[k]
+      data[k] = res.data[k];
     }
   }
-
-})
-
+});
 </script>
 
 <style lang="scss">
@@ -340,15 +383,11 @@ onMounted(async () => {
       align-items: center;
       justify-content: center;
     }
-
-
   }
-
-
 }
 
 .witnessInfo {
-  background: #F5F5F5;
+  background: #f5f5f5;
   padding: 15px;
   margin-top: 20px;
   // margin-right: 8px;
@@ -362,22 +401,20 @@ onMounted(async () => {
   padding: 20px 0;
 
   .btn {
-    color: #26559B;
+    color: #26559b;
     width: 100%;
 
     &:hover {
-      background: #F5F5F5;
+      background: #f5f5f5;
       border-color: #dcdfe6;
     }
   }
 }
 
 .detail {
-
-
   .con {
     padding: 20px;
-    background-color: #F5F5F5;
+    background-color: #f5f5f5;
   }
 
   .info {
@@ -385,18 +422,17 @@ onMounted(async () => {
       margin-bottom: 20px;
     }
   }
-
 }
 
 .extractUser {
   margin-right: 0px;
 
   .line {
-    background-color: #F5F5F5;
+    background-color: #f5f5f5;
     padding: 15px;
     width: calc(100% - 30px);
     display: inline-flex;
     margin-bottom: 15px;
   }
 }
-</style>
+</style>

+ 20 - 0
yarn.lock

@@ -229,6 +229,11 @@
   resolved "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz"
   integrity sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==
 
+"@types/web-bluetooth@^0.0.20":
+  version "0.0.20"
+  resolved "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz#f066abfcd1cbe66267cdbbf0de010d8a41b41597"
+  integrity sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==
+
 "@vitejs/plugin-vue@^4.2.3":
   version "4.6.2"
   resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz"
@@ -368,6 +373,16 @@
   resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.4.31.tgz"
   integrity sha512-Yp3wtJk//8cO4NItOPpi3QkLExAr/aLBGZMmTtW9WpdwBCJpRM6zj9WgWktXAl8IDIozwNMByT45JP3tO3ACWA==
 
+"@vueuse/core@^10.11.0":
+  version "10.11.0"
+  resolved "https://registry.npmjs.org/@vueuse/core/-/core-10.11.0.tgz#b042585a8bf98bb29c177b33999bd0e3fcd9e65d"
+  integrity sha512-x3sD4Mkm7PJ+pcq3HX8PLPBadXCAlSDR/waK87dz0gQE+qJnaaFhc/dZVfJz+IUYzTMVGum2QlR7ImiJQN4s6g==
+  dependencies:
+    "@types/web-bluetooth" "^0.0.20"
+    "@vueuse/metadata" "10.11.0"
+    "@vueuse/shared" "10.11.0"
+    vue-demi ">=0.14.8"
+
 "@vueuse/core@^9.1.0":
   version "9.13.0"
   resolved "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz"
@@ -378,6 +393,11 @@
     "@vueuse/shared" "9.13.0"
     vue-demi "*"
 
+"@vueuse/metadata@10.11.0":
+  version "10.11.0"
+  resolved "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.0.tgz#27be47cf115ee98e947a1bfcd0b1b5b35d785fb6"
+  integrity sha512-kQX7l6l8dVWNqlqyN3ePW3KmjCQO3ZMgXuBMddIu83CmucrsBfXlH+JoviYyRBws/yLTQO8g3Pbw+bdIoVm4oQ==
+
 "@vueuse/metadata@9.13.0":
   version "9.13.0"
   resolved "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz"