tangning 8 月之前
父節點
當前提交
c69acd8d1c
共有 51 個文件被更改,包括 6345 次插入32 次删除
  1. 3 0
      package.json
  2. 4 1
      src/constant/caseFile.ts
  3. 274 0
      src/core/Scene copy.js
  4. 350 0
      src/core/Scene.js
  5. 95 0
      src/core/box/BoxManager.js
  6. 119 0
      src/core/box/HorizontalBox.js
  7. 107 0
      src/core/box/VerticalBox.js
  8. 66 0
      src/core/box/object/CircleTextLabel.js
  9. 47 0
      src/core/box/object/ImgLabel.js
  10. 118 0
      src/core/box/object/ImgLabelBox.js
  11. 139 0
      src/core/box/object/Line.js
  12. 75 0
      src/core/box/object/LinePoints.js
  13. 54 0
      src/core/box/object/PureTextLabel copy.js
  14. 73 0
      src/core/box/object/PureTextLabel.js
  15. 48 0
      src/core/box/object/SimpleLabel.js
  16. 47 0
      src/core/box/object/TextLabel.js
  17. 24 0
      src/core/box/object/TouchEdge.js
  18. 31 0
      src/core/box/object/marker.js
  19. 501 0
      src/core/controls/FloorplanControls.js
  20. 10 0
      src/core/mitt.js
  21. 969 0
      src/core/player/Player.js
  22. 8 0
      src/core/save.json
  23. 0 0
      src/core/settings/style.js
  24. 92 0
      src/core/utils/cap.js
  25. 14 0
      src/core/utils/text.js
  26. 39 0
      src/core/utils/utils.js
  27. 21 1
      src/request/urls.ts
  28. 12 0
      src/router/config.ts
  29. 2 0
      src/router/routeName.ts
  30. 69 4
      src/store/case.ts
  31. 1 0
      src/store/caseFile.ts
  32. 1 1
      src/store/user.ts
  33. 5 5
      src/view/abstract/index.vue
  34. 29 4
      src/view/aiList/index.vue
  35. 151 0
      src/view/case/addPhotoFile.vue
  36. 207 0
      src/view/case/addPhotoFileAll.vue
  37. 1 0
      src/view/case/draw/board/useBoard.ts
  38. 21 7
      src/view/case/draw/index.vue
  39. 2 1
      src/view/case/draw/slider.vue
  40. 209 0
      src/view/case/photos/draggable.vue
  41. 154 0
      src/view/case/photos/edit.vue
  42. 564 0
      src/view/case/photos/index.vue
  43. 12 0
      src/view/case/quisk.ts
  44. 169 0
      src/view/case/records/feild.md
  45. 61 0
      src/view/case/records/formData.ts
  46. 837 0
      src/view/case/records/index.vue
  47. 444 0
      src/view/case/records/manifest.vue
  48. 5 1
      src/view/layout/index.vue
  49. 34 6
      src/view/material/index.vue
  50. 5 1
      src/view/vrmodel/index.vue
  51. 22 0
      yarn.lock

+ 3 - 0
package.json

@@ -22,11 +22,14 @@
     "province-city-china": "^8.5.8",
     "qs": "^6.11.2",
     "sass": "^1.64.2",
+    "swiper": "^11.1.15",
+    "three": "^0.171.0",
     "unplugin-element-plus": "^0.7.2",
     "unplugin-vue-define-options": "^1.3.12",
     "vite-plugin-windicss": "^1.9.4",
     "vue": "^3.3.4",
     "vue-cropper": "^1.0.9",
+    "vue-draggable-plus": "^0.6.0",
     "vue-router": "^4.2.4",
     "windicss": "^3.5.6"
   },

+ 4 - 1
src/constant/caseFile.ts

@@ -4,13 +4,16 @@ export const FileDrawType = 1;
 export const DrawFormats = [".jpg", ".jpeg", ".png"];
 export const OtherFormats = [".pdf", ".jpeg", ".doc", ".docx", ".jpg", ".png"];
 export const BoardTypeDesc = {
-  [BoardType.scene]: "现场图",
+  [BoardType.scene]: "平面图",
   [BoardType.map]: "方位图",
+  [BoardType.aimap]: " AI 平面图 ",
 };
 export const DrawFormatDesc = "jpg、png等格式的文件";
 export const OtherFormatDesc = "pdf、word、jpg、png等格式的文件";
+export const photoFormatDesc = "jpg、jpeg、png等格式的文件";
 
 export const maxFileSize = 100 * 1024 * 1024;
+export const maxAppFileSize = 300 * 1024 * 1024;
 
 export const fileOptions  = [
   {

+ 274 - 0
src/core/Scene copy.js

@@ -0,0 +1,274 @@
+import * as THREE from "three";
+import Stats from "three/examples/jsm/libs/stats.module.js";
+import Player from "./player/Player.js";
+import BoxManager from "./box/BoxManager.js";
+import { Mitt } from "./mitt.js";
+import testData from "./save.json";
+import { dataURItoBlob, saveFile } from "./utils/utils.js";
+const stats = new Stats();
+
+function sleep(ms) {
+  return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export default class Scene extends Mitt {
+  constructor(domElement) {
+    super();
+    this.domElement = domElement;
+    this.scene = null;
+    this.renderer = null;
+    this.orthCamera = null;
+    this.player = null;
+    this.sceneType = 1;
+    this.width = 0;
+    this.height = 0;
+    this.defaultZoom = 250;
+    this.initCamPView = new THREE.Vector3();
+    this.initCamRView = new THREE.Vector3();
+    this.blobScreens = [];
+    this.inited = false;
+
+    this.init = () => {
+      this.scene = new THREE.Scene();
+      this.scene.background = new THREE.Color(0xf0f2f5);
+      this.renderer = new THREE.WebGLRenderer({
+        canvas: this.domElement,
+        antialias: true,
+        autoClear: true,
+        preserveDrawingBuffer: true,
+      });
+
+      this.width = this.domElement.clientWidth;
+      this.height = this.domElement.clientHeight;
+      this.renderRes = window.devicePixelRatio;
+
+      this.renderer.setSize(this.width, this.height);
+      this.renderer.setPixelRatio(this.renderRes);
+      console.log(this.width, this.height, this.renderRes);
+
+      this.orthCamera = new THREE.OrthographicCamera(
+        -this.width / 2,
+        this.width / 2,
+        this.height / 2,
+        -this.height / 2,
+        0.1,
+        1000
+      );
+      this.orthCamera.zoom = this.defaultZoom;
+
+      this.orthCamera.position.set(0, 10, 0);
+      this.orthCamera.lookAt(0, 0, 0);
+      // this.orthCamera.setViewOffset(this.width, this.height, 0, 0);
+      this.orthCamera.updateProjectionMatrix();
+
+      //player
+      this.player = new Player(this);
+
+      //stats
+      domElement.parentNode.appendChild(stats.dom);
+      stats.dom.style.pointerEvents = "none";
+      stats.dom.style.left = "15%";
+      stats.dom.style.display = "none";
+
+      this.onBindEvent();
+
+      this.inited = true;
+      this.load();
+      this.animate();
+    };
+  }
+
+  load = (list, type, data) => {
+    if (!list) return;
+    // console.log("scene: ", list, type, data);
+    //axesHeloer
+    this.clearScene();
+    this.sceneType = type;
+    // const axesHelper = new THREE.AxesHelper(1);
+    // this.scene.add(axesHelper);
+    this.boxManager = new BoxManager(this);
+    this.boxManager.load(list, type);
+    //light
+    this.loadLight();
+    this.player.load(type, data || []);
+    this.initCamPView.copy(this.orthCamera.position);
+    this.initCamRView.copy(this.orthCamera.rotation);
+  };
+
+  clearScene() {
+    for (var i = this.scene.children.length - 1; i >= 0; i--) {
+      let obj = this.scene.children[i];
+      this.scene.remove(obj);
+    }
+  }
+  clearDrawScene() {
+    for (var i = this.scene.children.length - 1; i >= 0; i--) {
+      let obj = this.scene.children[i];
+      if (
+        String(obj.name).includes("marker_") ||
+        String(obj.name).includes("line_") ||
+        String(obj.name).includes("line_point_") ||
+        String(obj.name).includes("circle_") ||
+        String(obj.name).includes("pureText_")
+      ) {
+        this.scene.remove(obj);
+      }
+    }
+  }
+
+  deleteItemById(uuid, type) {
+    for (var i = this.scene.children.length - 1; i >= 0; i--) {
+      let obj = this.scene.children[i];
+      if (obj.uuid === uuid) {
+        console.log("deleteItemById-userData", obj.userData);
+        this.player.deleteItemByType(type, obj.userData);
+        this.scene.remove(obj);
+      }
+    }
+  }
+  deleteImageDataByIds(ids) {
+    this.player.deleteImageDataByIds(ids);
+  }
+  loadLight = () => {
+    const light = new THREE.AmbientLight(0xffffff, 1.5); // 柔和的白光
+    this.scene.add(light);
+  };
+
+  setCamera = () => {};
+
+  toHorizontal = () => {};
+
+  toVertical = () => {};
+
+  lockView(open) {
+    if (!open) {
+      this.player.floorplanControls.enablePan = true;
+      this.player.floorplanControls.enableZoom = true;
+    } else {
+      this.player.floorplanControls.enablePan = false;
+      this.player.floorplanControls.enableZoom = false;
+    }
+  }
+  setMode(mode) {
+    this.player.setMode(mode);
+  }
+  onResize = (width, height) => {
+    this.width = width !== undefined ? width : this.domElement.clientWidth;
+    this.height = height !== undefined ? height : this.domElement.clientHeight;
+    console.log("resize", this.width, this.height);
+
+    (this.orthCamera.left = -this.width / 2),
+      (this.orthCamera.right = this.width / 2),
+      (this.orthCamera.bottom = -this.height / 2),
+      (this.orthCamera.top = this.height / 2),
+      this.orthCamera.updateProjectionMatrix();
+
+    this.renderer.setSize(this.width, this.height);
+  };
+  render = () => {
+    if (this.player) {
+      this.player.update();
+      this.renderer.render(this.scene, this.orthCamera);
+    }
+  };
+  animate = () => {
+    stats.begin();
+    this.render();
+    stats.end();
+    requestAnimationFrame(this.animate);
+  };
+
+  resetCameraView() {
+    this.orthCamera.zoom = this.defaultZoom;
+    // this.orthCamera.position.set(0, 0, 0);
+    // this.orthCamera.rotation.set(0, 10, 0);
+    // this.orthCamera.updateMatrixWorld();
+  }
+
+  editing(item) {
+    this.player.editing(item);
+  }
+  endScreenshot() {
+    this.lockView(false);
+    this.blobScreens = [];
+    this.player.floorplanControls.reset();
+    this.onResize();
+    this.renderer.setSize(this.width, this.height);
+  }
+  screenshot(x, zoom) {
+    var imgData, imgNode;
+    const times = 4;
+    this.orthCamera.zoom = zoom || this.defaultZoom;
+    this.scene.position.x = x || 0;
+
+    this.renderer.setSize(this.width * times, this.height * times);
+    this.orthCamera.aspect = this.width / this.height;
+
+    // this.player.floorplanControls.minZoom = 1;
+    // this.orthCamera.zoom = 50;
+    this.orthCamera.updateProjectionMatrix();
+    this.renderer.render(this.scene, this.orthCamera, null, false);
+
+    const dataURL = this.renderer.domElement.toDataURL("image/jpeg");
+    this.blobScreens.push(dataURItoBlob(dataURL));
+    // saveFile(dataURL, `${index}.jpg`);
+    this.onResize(this.width, this.height);
+  }
+
+  exportScreenshot() {
+    if (window.devicePixelRatio !== 1) {
+      this.emit("devicePixelRatio");
+      return;
+    }
+    this.player.floorplanControls.reset();
+    this.lockView(true);
+    this.setMode(0);
+    // await sleep(500);
+    this.orthCamera.zoom = this.defaultZoom;
+    const object = this.boxManager.model;
+    const total = object.children.length;
+    if (total === 0) {
+      return;
+      console.error("没数据");
+    }
+
+    object.updateMatrixWorld();
+    this.orthCamera.updateProjectionMatrix();
+    const boundingBox = new THREE.Box3().setFromObject(object);
+    // 计算宽度、高度和深度
+    const width = boundingBox.max.x - boundingBox.min.x;
+    const one = width / total;
+    let slides = Math.floor(total / 3);
+    // debugger;
+    console.log("slides", slides);
+    if (slides >= 1) {
+      for (var i = 0; i <= slides; i++) {
+        (function (index, that) {
+          setTimeout(function () {
+            const offset = -(one * 3 * index);
+            console.log("Iteration:", offset);
+            that.screenshot(offset);
+            console.log(`Width: ${offset}`);
+            if (index === slides) {
+              console.log("last");
+              that.scene.position.x = 0;
+              that.emit("submitScreenshot");
+            }
+          }, index * 500);
+        })(i, this); // 传递当前迭代的索引i给setTimeout的回调函数
+      }
+    } else {
+      // 只有一个或0个
+      if (total >= 1 && total <= 3) {
+        console.log("total", total);
+        this.player.floorplanControls.reset();
+        this.screenshot(-0.3, 227);
+        this.emit("submitScreenshot");
+      }
+    }
+  }
+
+  onBindEvent = () => {
+    window.addEventListener("resize", this.onResize, false);
+  };
+}

+ 350 - 0
src/core/Scene.js

@@ -0,0 +1,350 @@
+import * as THREE from "three";
+import Stats from "three/examples/jsm/libs/stats.module.js";
+import Player from "./player/Player.js";
+import BoxManager from "./box/BoxManager.js";
+import { Mitt } from "./mitt.js";
+import testData from "./save.json";
+import { dataURItoBlob, saveFile } from "./utils/utils.js";
+import { screenshotObject } from "./utils/cap.js";
+
+const stats = new Stats();
+
+function sleep(ms) {
+  return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export default class Scene extends Mitt {
+  constructor(domElement) {
+    super();
+    this.domElement = domElement;
+    this.scene = null;
+    this.renderer = null;
+    this.orthCamera = null;
+    this.camera = null;
+    this.player = null;
+    this.sceneType = 1;
+    this.width = 0;
+    this.height = 0;
+    this.defaultZoom = 250;
+    this.defaultUseZoom = 250 / window.devicePixelRatio;
+    this.initCamPView = new THREE.Vector3();
+    this.initCamRView = new THREE.Vector3();
+    this.blobScreens = [];
+    this.inited = false;
+
+    this.init = () => {
+      this.scene = new THREE.Scene();
+      this.scene.background = new THREE.Color(0xf0f2f5);
+      this.renderer = new THREE.WebGLRenderer({
+        canvas: this.domElement,
+        antialias: true,
+        autoClear: true,
+        preserveDrawingBuffer: true,
+      });
+
+      this.width = this.domElement.clientWidth;
+      this.height = this.domElement.clientHeight;
+      this.renderRes = window.devicePixelRatio;
+      this.defaultUseZoom = this.defaultZoom / window.devicePixelRatio;
+      this.renderer.setSize(this.width, this.height);
+      this.renderer.setPixelRatio(this.renderRes);
+
+      console.log(
+        "init",
+        this.width,
+        this.height,
+        this.renderRes,
+        this.defaultZoom
+      );
+
+      this.camera = new THREE.PerspectiveCamera(
+        70,
+        this.domElement.clientWidth / this.domElement.clientHeight,
+        0.1,
+        1000
+      );
+
+      this.orthCamera = new THREE.OrthographicCamera(
+        -this.width / 2,
+        this.width / 2,
+        this.height / 2,
+        -this.height / 2,
+        0.1,
+        1000
+      );
+      this.orthCamera.zoom = this.defaultUseZoom;
+      // 影响画线
+      this.orthCamera.position.set(0, 10, 0);
+      this.orthCamera.lookAt(0, 0, 0);
+      // this.orthCamera.setViewOffset(this.width, this.height, 0, 0);
+      this.orthCamera.updateProjectionMatrix();
+      //player
+      this.player = new Player(this);
+
+      domElement.parentNode.appendChild(stats.dom);
+      stats.dom.style.pointerEvents = "none";
+      stats.dom.style.left = "15%";
+      stats.dom.style.display = "none";
+
+      this.onBindEvent();
+
+      this.inited = true;
+      this.load();
+      this.animate();
+    };
+  }
+
+  load = (list, type, data) => {
+    if (!list) return;
+    // console.log("scene: ", list, type, data);
+    //axesHeloer
+    this.clearScene();
+    this.sceneType = type;
+    // const axesHelper = new THREE.AxesHelper(1);
+    // this.scene.add(axesHelper);
+    this.boxManager = new BoxManager(this);
+    this.boxManager.load(list, type);
+    //light
+    this.loadLight();
+    this.player.load(type, data || []);
+    this.initCamPView.copy(this.orthCamera.position);
+    this.initCamRView.copy(this.orthCamera.rotation);
+  };
+
+  clearScene() {
+    for (var i = this.scene.children.length - 1; i >= 0; i--) {
+      let obj = this.scene.children[i];
+      this.scene.remove(obj);
+    }
+  }
+  clearDrawScene() {
+    for (var i = this.scene.children.length - 1; i >= 0; i--) {
+      let obj = this.scene.children[i];
+      if (
+        String(obj.name).includes("marker_") ||
+        String(obj.name).includes("line_") ||
+        String(obj.name).includes("line_point_") ||
+        String(obj.name).includes("circle_") ||
+        String(obj.name).includes("pureText_")
+      ) {
+        this.scene.remove(obj);
+      }
+    }
+  }
+
+  deleteItemById(uuid, type) {
+    for (var i = this.scene.children.length - 1; i >= 0; i--) {
+      let obj = this.scene.children[i];
+      if (obj.uuid === uuid) {
+        console.log("deleteItemById-userData", obj.userData);
+        this.player.deleteItemByType(type, obj.userData);
+        this.scene.remove(obj);
+      }
+    }
+  }
+  deleteImageDataByIds(ids) {
+    this.player.deleteImageDataByIds(ids);
+  }
+  loadLight = () => {
+    const light = new THREE.AmbientLight(0xffffff, 1.5); // 柔和的白光
+    this.scene.add(light);
+  };
+
+  setCamera = () => {
+    // const object = this.boxManager.model;
+    // const boundingBox = new THREE.Box3().setFromObject(object);
+    // let size = boundingBox.getSize(new THREE.Vector3());
+    // const fov = this.camera.fov * (Math.PI / 180);
+    // const fovh = 2 * Math.atan(Math.tan(fov / 2) * this.camera.aspect);
+    // let dx = Math.abs(size.x / 2 / Math.tan(fovh / 2));
+    // let dz = Math.abs(size.z / 2 / Math.tan(fov / 2));
+    // this.orthCamera.zoom = this.floorplanZoom;
+  };
+
+  toHorizontal = () => {};
+
+  toVertical = () => {};
+
+  lockView(open) {
+    if (!open) {
+      this.player.floorplanControls.enablePan = true;
+      this.player.floorplanControls.enableZoom = true;
+    } else {
+      this.player.floorplanControls.enablePan = false;
+      this.player.floorplanControls.enableZoom = false;
+    }
+  }
+  setMode(mode) {
+    this.player.setMode(mode);
+  }
+  onResize = (rWidth, rHeight) => {
+    this.width =
+      typeof rWidth === "number" ? rWidth : this.domElement.clientWidth;
+    this.height =
+      typeof rHeight === "height" ? rHeight : this.domElement.clientHeight;
+
+    const container = document.querySelector("#canvas");
+    const { width, height } = container.getBoundingClientRect();
+
+    if (width !== this.domElement.clientWidth) {
+      this.width = width;
+    }
+    if (height !== this.domElement.clientHeight) {
+      this.height = height;
+    }
+
+    console.warn("onResize", this.width, this.height);
+
+    (this.orthCamera.left = -this.width / 2),
+      (this.orthCamera.right = this.width / 2),
+      (this.orthCamera.bottom = -this.height / 2),
+      (this.orthCamera.top = this.height / 2),
+      this.orthCamera.updateProjectionMatrix();
+
+    this.renderer.setSize(this.width, this.height);
+    this.renderRes = window.devicePixelRatio;
+    this.defaultUseZoom = this.defaultZoom / this.renderRes;
+    this.renderer.setPixelRatio(this.renderRes);
+  };
+  render = () => {
+    if (this.player) {
+      this.player.update();
+      this.renderer.render(this.scene, this.orthCamera);
+    }
+  };
+  animate = () => {
+    stats.begin();
+    this.render();
+    stats.end();
+    requestAnimationFrame(this.animate);
+  };
+
+  resetCameraView() {
+    this.orthCamera.zoom = this.defaultUseZoom;
+    // this.orthCamera.position.set(0, 0, 0);
+    // this.orthCamera.rotation.set(0, 10, 0);
+    this.orthCamera.updateMatrixWorld();
+  }
+
+  editing(item) {
+    this.player.editing(item);
+  }
+  endScreenshot() {
+    this.lockView(false);
+    this.blobScreens = [];
+    this.scene.position.x = 0;
+    this.player.floorplanControls.reset();
+    this.onResize(this.width, this.height);
+  }
+  screenshot(x, zoom, index) {
+    var imgData, imgNode;
+    const times = 4;
+    this.orthCamera.zoom = zoom || this.defaultUseZoom;
+    this.scene.position.x = x || 0;
+
+    this.renderer.setSize(this.width * times, this.height * times);
+    this.renderer.setPixelRatio(this.renderRes);
+    this.orthCamera.aspect = this.width / this.height;
+
+    this.orthCamera.updateProjectionMatrix();
+    this.renderer.render(this.scene, this.orthCamera, null, false);
+
+    const dataURL = this.renderer.domElement.toDataURL("image/jpeg");
+    this.blobScreens.push(dataURItoBlob(dataURL));
+    console.log(this.width, this.height);
+    if (typeof index === "number") {
+      saveFile(dataURL, `${index}.jpg`);
+    }
+
+    this.renderer.setSize(this.width, this.height);
+  }
+
+  exportScreenshot(isSaveJpg = true) {
+    if (window.devicePixelRatio !== 1) {
+      this.emit("devicePixelRatio");
+      return;
+    }
+    window.isExportScreenshot = true
+
+    this.player.floorplanControls.reset();
+    this.onResize();
+    this.lockView(true);
+    this.setMode(0);
+    // await sleep(500);
+
+    const object = this.boxManager.model;
+    const total = object.children.length;
+    if (total === 0) {
+      return;
+      console.error("没数据");
+    }
+
+    object.updateMatrixWorld();
+    this.orthCamera.updateProjectionMatrix();
+    const boundingBox = new THREE.Box3().setFromObject(object);
+    // 计算宽度、高度和深度
+    const width = boundingBox.max.x - boundingBox.min.x;
+    const one = width / total;
+    let slides = Math.floor(total / 3);
+
+    console.log("slides", slides);
+
+    if (slides >= 1 && total > 3) {
+      for (var i = 0; i <= slides; i++) {
+        (function (index, that) {
+          setTimeout(function () {
+            const offset = -(one * 3 * index);
+            console.log("Iteration:", one, index, offset, that.defaultUseZoom);
+            that.screenshot(offset, that.defaultUseZoom);
+            console.log(`Width: ${offset}`);
+            if (index === slides) {
+              console.log("last");
+              that.emit("submitScreenshot", true);
+              window.isExportScreenshot = false
+            }
+          }, index * 500);
+        })(i, this); // 传递当前迭代的索引i给setTimeout的回调函数
+      }
+    } else {
+      // 只有一个
+      if (total >= 1) {
+        console.log("total", total);
+        this.player.floorplanControls.reset();
+        this.screenshot(-0.3, 227);
+        this.emit("submitScreenshot", true);
+        window.isExportScreenshot = false
+      }
+    }
+  }
+  test() {
+    const object = this.boxManager.model;
+    const boundingBox = new THREE.Box3().setFromObject(object);
+    this.lockView(true);
+    // let size = new THREE.Vector3();
+    // boundingBox.getSize(size);
+    const width = boundingBox.max.x - boundingBox.min.x;
+    const height = boundingBox.max.y - boundingBox.min.y;
+    const z = boundingBox.max.z - boundingBox.min.z;
+    const radio = width / height;
+    // const dynamicWidth = this.height * radio;
+
+    // (this.orthCamera.left = -dynamicWidth / 2),
+    //   (this.orthCamera.right = dynamicWidth / 2),
+    //   (this.orthCamera.bottom = -this.height / 2),
+    //   (this.orthCamera.top = this.height / 2),
+    //   this.orthCamera.updateProjectionMatrix();
+
+    // this.renderer.setSize(dynamicWidth, this.height);
+    // this.renderRes = window.devicePixelRatio;
+    // this.defaultUseZoom = this.defaultZoom / this.renderRes;
+    // this.renderer.setPixelRatio(this.renderRes);
+
+    // console.log("dynamicWidth", dynamicWidth);
+
+    // console.log("height", height);
+  }
+
+  onBindEvent = () => {
+    window.addEventListener("resize", this.onResize, false);
+  };
+}

+ 95 - 0
src/core/box/BoxManager.js

@@ -0,0 +1,95 @@
+import * as THREE from "three";
+import HorizontalBox from "./HorizontalBox";
+import VerticalBox from "./VerticalBox";
+import SimpleLabel from "./object/SimpleLabel";
+
+// import { Line2 } from "three/examples/jsm/lines/Line2.js";
+// import { LineGeometry } from "three/examples/jsm/lines/LineGeometry.js";
+// import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
+
+export default class BoxManager {
+  constructor(scene) {
+    this.scene = scene;
+    this.loadingManager = new THREE.LoadingManager();
+    this.loader = new THREE.TextureLoader(this.loadingManager);
+    this.model = new THREE.Group();
+    this.model.name = "boxManager";
+    this.maps = {};
+    this.obb = new THREE.Box3();
+    this.imgList = [];
+    this.opacity = 1;
+
+    this.onBindEvent();
+  }
+
+  load = (list, type) => {
+    console.log("this.model.name", this.model.name);
+    const total = list.length;
+    list.forEach((item, index) => {
+      if (type === 1) {
+        //横排
+        console.log("横排");
+        const box = new HorizontalBox(this, item, index, total);
+        this.model.add(box);
+      }
+      if (type === 2) {
+        //竖排
+        const box = new VerticalBox(this, item, index, total);
+        // console.log("竖排");
+        this.model.add(box);
+      }
+    });
+    // this.model.position.y += 0.3;
+    // this.model.visible =false;
+    this.scene.scene.add(this.model);
+    this.setArea();
+    // console.log("this.scene.scene", this.scene.scene);
+  };
+
+  onBindEvent = () => {
+    const _this = this;
+    this.loadingManager.onStart = (url, itemsLoaded, itemsTotal) => {
+      // console.log( 'Started loading file: ' + url + '.\nLoaded ' + itemsLoaded + ' of ' + itemsTotal + ' files.' );
+      console.log("loading_manager: loading...");
+ 
+    };
+    this.loadingManager.onLoad = () => {
+      console.log("loading_manager: loading complete!");
+      this.scene.emit("loaded");
+
+    };
+    this.loadingManager.onProgress = function (url, itemsLoaded, itemsTotal) {
+      // console.log( 'Loading file: ' + url + '.\nLoaded ' + itemsLoaded + ' of ' + itemsTotal + ' files.' );
+    };
+    this.loadingManager.onError = function (url) {
+      console.error("loading_manager: error loading " + url);
+    };
+  };
+  setArea() {
+    const object = this.model;
+    const boundingBox = new THREE.Box3().setFromObject(object);
+    // boundingBox.expandByScalar(1.3);
+    const size = boundingBox.getSize(new THREE.Vector3());
+    this.obb = boundingBox;
+    console.log("boundingBox-size", size);
+    // const topLine = [
+    //   -size.x,
+    //   0,
+    //   (size.z + 0.3) / -2,
+    //   size.x + 0.1,
+    //   0,
+    //   (size.z + 0.3) / -2,
+    // ];
+
+    // helper.scale.set(1.2, 1.2, 1.2);
+    // helper.update();
+  }
+  setVisible = (val) => {
+    if (!this.model) return;
+    this.model.visible = val;
+  };
+
+  setOpacity = (val) => {
+    this.material.opacity = val;
+  };
+}

+ 119 - 0
src/core/box/HorizontalBox.js

@@ -0,0 +1,119 @@
+import * as THREE from "three";
+import TextLabel from "./object/TextLabel";
+import SimpleLabel from "./object/SimpleLabel";
+import ImgLabel from "./object/ImgLabel";
+import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
+
+export default class HorizontalBox extends THREE.Group {
+  constructor(manager, data, index, total) {
+    super();
+    this.manager = manager;
+    this.name = "horizontal_box";
+    this.total = total;
+    this.getStyle();
+    this.load(data, index);
+  }
+  getStyle() {
+    this.width = 2;
+    this.height = (2 * 710) / 500;
+    this.color = 0xffffff;
+  }
+  cover(texture, aspect) {
+    var imageAspect = texture.image.width / texture.image.height;
+
+    if (aspect < imageAspect) {
+      texture.matrix.setUvTransform(0, 0, aspect / imageAspect, 1, 0, 0.5, 0.5);
+    } else {
+      texture.matrix.setUvTransform(0, 0, 1, imageAspect / aspect, 0, 0.5, 0.5);
+    }
+  }
+  load(data, index) {
+    //box
+    const geometry = new THREE.PlaneGeometry(1, 1);
+    geometry.rotateX(-Math.PI / 2);
+
+    const bm = new THREE.MeshBasicMaterial({
+      color: this.color,
+    });
+
+    const box = new THREE.Mesh(geometry, bm);
+    box.scale.set(this.width, 1, this.height);
+
+    this.add(box);
+    this.position.x = (this.width + 0.125) * index - 1.8;
+
+    const matLine = new LineMaterial({
+      color: 0xe44d54,
+      linewidth: 4, // in world units with size attenuation, pixels otherwise
+      dashed: false,
+      alphaToCoverage: true,
+    });
+    matLine.resolution = new THREE.Vector2(
+      this.manager.scene.width,
+      this.manager.scene.height
+    );
+    //content
+    data.forEach((i, j) => {
+      //img
+      let img;
+      this.manager.loader.load(i.imgUrl, (texture) => {
+        let imgRatio = texture.image.width / texture.image.height;
+        texture.matrixAutoUpdate = false;
+        let planeRatio = 1.5 / 0.85;
+        // let ratio = planeRatio / imgRatio;
+        texture.matrixAutoUpdate = false;
+        if (planeRatio < imgRatio) {
+          texture.matrix.setUvTransform(
+            0,
+            0,
+            planeRatio / imgRatio,
+            1,
+            0,
+            0.5,
+            0.5
+          );
+        } else {
+          texture.matrix.setUvTransform(
+            0,
+            0,
+            1,
+            imgRatio / planeRatio,
+            0,
+            0.5,
+            0.5
+          );
+        }
+        // texture.wrapS = THREE.RepeatWrapping;
+        // texture.wrapS = THREE.RepeatWrapping;
+        // texture.wrapT = THREE.ClampToEdgeWrapping;
+        // texture.repeat.x = ratio;
+        // texture.offset.x = 0.5 * (1 - ratio);
+        // console.log("texture", texture);
+        texture.colorSpace = THREE.SRGBColorSpace;
+
+        img = new ImgLabel(texture, matLine);
+
+        img.userData = i.id;
+        img.position.y += 1;
+        if (j === 0) {
+          img.position.z -= 0.8;
+        } else {
+          img.position.z += 0.43;
+        }
+        this.add(img);
+        this.manager.imgList.push(img);
+        const textlabel = new TextLabel(i.imgInfo, true);
+        this.add(textlabel);
+        textlabel.position.copy(img.position);
+        textlabel.position.z += textlabel.scale.z * 0.5 + 0.1;
+      });
+    });
+    //页脚
+    const f_txt_label = ` 第 ${index + 1} 页  共 ${this.total} 页`;
+    const footlabel = new SimpleLabel(f_txt_label, true);
+    footlabel.renderOrder = 100;
+    footlabel.position.z += 1.26;
+
+    this.add(footlabel);
+  }
+}

+ 107 - 0
src/core/box/VerticalBox.js

@@ -0,0 +1,107 @@
+import * as THREE from "three";
+import TextLabel from "./object/TextLabel";
+import ImgLabel from "./object/ImgLabel";
+import ImgLabelBox from "./object/ImgLabelBox";
+import SimpleLabel from "./object/SimpleLabel";
+import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
+
+export default class VerticalBox extends THREE.Group {
+  constructor(manager, data, index, total) {
+    super();
+    this.manager = manager;
+    this.total = total;
+    this.name = "vertical_box";
+    this.getStyle();
+    this.load(data, index);
+  }
+  getStyle() {
+    this.width = 2;
+    this.height = (2 * 710) / 500;
+    this.color = 0xffffff;
+  }
+
+  load(data, index) {
+    //box
+    const geometry = new THREE.PlaneGeometry(1, 1);
+    geometry.rotateX(-Math.PI / 2);
+
+    const bm = new THREE.MeshBasicMaterial({
+      color: this.color,
+    });
+
+    const box = new THREE.Mesh(geometry, bm);
+    box.scale.set(this.width, 1, this.height);
+
+    this.add(box);
+    this.position.x = (this.width + 0.125) * index - 1.8;
+
+    const matLine = new LineMaterial({
+      color: 0xe44d54,
+      linewidth: 4, // in world units with size attenuation, pixels otherwise
+      dashed: false,
+      alphaToCoverage: true,
+    });
+    matLine.resolution = new THREE.Vector2(
+      this.manager.scene.width,
+      this.manager.scene.height
+    );
+    //content
+    data.forEach((i, j) => {
+      //img
+      let img;
+      this.manager.loader.load(i.imgUrl, (texture) => {
+        let imgRatio = texture.image.width / texture.image.height;
+        let planeRatio = 1.5 / 2;
+        texture.matrixAutoUpdate = false;
+        //放开所有uv的比例
+        if (planeRatio > imgRatio) {
+          // texture.matrix.setUvTransform(
+          //   0,
+          //   0,
+          //   planeRatio / imgRatio,
+          //   1,
+          //   0,
+          //   0.5,
+          //   0.5
+          // );
+        } else {
+          // debugger
+          // texture.matrix.setUvTransform(
+          //   0,
+          //   0,
+          //   1,
+          //   imgRatio / planeRatio,
+          //   0,
+          //   0.5,
+          //   0.5
+          // );
+        }
+
+        texture.colorSpace = THREE.SRGBColorSpace;
+
+        // texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
+        // texture.repeat.set(2, 2); // 纹理平铺两次
+        // texture.offset.set(0.5, 0.5); // 纹理偏移到中心
+        img = new ImgLabelBox(texture, matLine, false);
+
+        img.userData = i.id;
+        img.position.y += 1;
+        img.position.z -= 0.2;
+        this.add(img);
+        this.manager.imgList.push(img);
+        const textlabel = new TextLabel(i.imgInfo, true);
+        this.add(textlabel);
+        textlabel.position.copy(img.position);
+        textlabel.position.z += textlabel.scale.z * 0.5 + 0.7;
+      });
+    });
+
+    //页脚
+    const f_txt_label = ` 第 ${index + 1} 页  共 ${this.total} 页`;
+    const footlabel = new SimpleLabel(f_txt_label, true);
+    footlabel.renderOrder = 100;
+    footlabel.position.z += 1.26;
+
+    this.add(footlabel);
+  }
+}

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

@@ -0,0 +1,66 @@
+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, pos) {
+    let res = 5;
+    let point = new THREE.Vector3().copy(pos);
+    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);
+
+    const geometry = new LineGeometry();
+    geometry.fromEdgesGeometry(edges);
+
+    const line_m = new LineMaterial({
+      color: 0xe44d54,
+      linewidth: 5,
+      dashed: false,
+      dashScale : 0.1,
+      alphaToCoverage: false,
+    });
+
+    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.userData = point.toArray();
+    this.position.copy(point);
+    this.name = "circle_" + text;
+  }
+}

+ 47 - 0
src/core/box/object/ImgLabel.js

@@ -0,0 +1,47 @@
+import * as THREE from "three";
+import TouchEdge from "./TouchEdge";
+
+export default class ImgLabel extends THREE.Mesh {
+  constructor(texture, matLine, isHorizontal = true) {
+    let width, height, p;
+    if (isHorizontal) {
+      width = 1.5;
+      height = 0.85;
+      p = [
+        [-0.75, 0, -0.425, 0.75, 0, -0.425],
+        [-0.75, 0, -0.425, -0.75, 0, 0.425],
+        [-0.75, 0, 0.425, 0.75, 0, 0.425],
+        [0.75, 0, 0.425, 0.75, 0, -0.425],
+      ];
+    } else {
+      width = 1.5;
+      height = 2;
+      p = [
+        [-0.75, 0, -1, 0.75, 0, -1],
+        [-0.75, 0, -1, -0.75, 0, 1],
+        [-0.75, 0, 1, 0.75, 0, 1],
+        [0.75, 0, 1, 0.75, 0, -1],
+      ];
+    }
+    const g = new THREE.PlaneGeometry(width, height);
+    g.rotateX(-Math.PI / 2);
+
+    const m = new THREE.MeshBasicMaterial({
+      map: texture,
+      transparent: true,
+    });
+    super(g, m);
+
+  
+
+    this.width = width;
+    this.height = height;
+    this.touchLines = new TouchEdge(p, matLine);
+
+    this.touchLines.position.y += 0.5;
+    this.add(this.touchLines);
+    // this.touchLines.children.forEach((child) => (child.visible = true));
+
+    this.name = "imglabel";
+  }
+}

+ 118 - 0
src/core/box/object/ImgLabelBox.js

@@ -0,0 +1,118 @@
+import * as THREE from "three";
+import { TriangleBlurShader } from "three/addons/shaders/TriangleBlurShader.js";
+import TouchEdge from "./TouchEdge";
+
+function makeTriangleBlurShader(iterations = 10) {
+  // Remove texture, because texture is a reserved word in WebGL 2
+  const { texture, ...uniforms } = TriangleBlurShader.uniforms;
+
+  const TriangleBlurShader2 = {
+    ...TriangleBlurShader,
+
+    name: "TriangleBlurShader2",
+
+    uniforms: {
+      ...uniforms,
+
+      // Replace texture with blurTexture for WebGL 2
+      blurTexture: { value: null },
+    },
+  };
+
+  // Replace texture with blurTexture for WebGL 2
+  TriangleBlurShader2.fragmentShader =
+    TriangleBlurShader2.fragmentShader.replace(
+      "uniform sampler2D texture;",
+      "uniform sampler2D blurTexture;"
+    );
+  TriangleBlurShader2.fragmentShader =
+    TriangleBlurShader2.fragmentShader.replace(
+      "texture2D( texture",
+      "texture2D( blurTexture"
+    );
+
+  // Make iterations configurable.
+  TriangleBlurShader2.fragmentShader =
+    TriangleBlurShader2.fragmentShader.replace(
+      "#define ITERATIONS 10.0",
+      "#define ITERATIONS " + iterations + ".0"
+    );
+
+  console.log("shader:", TriangleBlurShader2.fragmentShader);
+
+  return TriangleBlurShader2;
+}
+
+export default class ImgLabel extends THREE.Mesh {
+  constructor(texture, matLine, isHorizontal = true) {
+    let width, height, p;
+    if (isHorizontal) {
+      width = 1.5;
+      height = 0.85;
+      p = [
+        [-0.75, 0, -0.425, 0.75, 0, -0.425],
+        [-0.75, 0, -0.425, -0.75, 0, 0.425],
+        [-0.75, 0, 0.425, 0.75, 0, 0.425],
+        [0.75, 0, 0.425, 0.75, 0, -0.425],
+      ];
+    } else {
+      width = 1.5;
+      height = 2;
+      p = [
+        [-0.75, 0, -1, 0.75, 0, -1],
+        [-0.75, 0, -1, -0.75, 0, 1],
+        [-0.75, 0, 1, 0.75, 0, 1],
+        [0.75, 0, 1, 0.75, 0, -1],
+      ];
+    }
+    const g = new THREE.PlaneGeometry(width, height);
+    g.rotateX(-Math.PI / 2);
+
+    // const m = new THREE.MeshBasicMaterial({
+    //   map: texture,
+    //   transparent: true,
+    // });
+
+    // const shader = makeTriangleBlurShader(12);
+
+    // const blurMaterial = new THREE.ShaderMaterial({
+    //   vertexShader: shader.vertexShader,
+    //   fragmentShader: shader.fragmentShader,
+    //   uniforms: THREE.UniformsUtils.clone(shader.uniforms),
+    // });
+    // // console.log("blurMaterial", blurMaterial.uniforms);
+    // blurMaterial.uniforms.blurTexture.value = texture;
+    // blurMaterial.uniforms.delta.value = new THREE.Vector2(0.5, 0.9);
+
+    const bg = new THREE.MeshBasicMaterial({
+      color: 0xf2f2f2,
+      transparent: false,
+    });
+
+    super(g, bg);
+
+    let imgRatio = texture.image.width / texture.image.height;
+    const imgHeight = width / imgRatio >= 2 ? 2 : width / imgRatio;
+    const imageG = new THREE.PlaneGeometry(width, imgHeight);
+
+    imageG.rotateX(-Math.PI / 2);
+
+    const im = new THREE.MeshBasicMaterial({
+      map: texture,
+      transparent: true,
+    });
+    const imageMesh = new THREE.Mesh(imageG, im);
+
+    imageMesh.renderOrder = 10;
+    this.add(imageMesh);
+
+    this.width = width;
+    this.height = height;
+    this.touchLines = new TouchEdge(p, matLine);
+
+    this.touchLines.position.y += 0.5;
+    this.add(this.touchLines);
+    // this.touchLines.children.forEach((child) => (child.visible = true));
+    this.name = "imglabel";
+  }
+}

+ 139 - 0
src/core/box/object/Line.js

@@ -0,0 +1,139 @@
+import * as THREE from "three";
+import { Line2 } from "three/examples/jsm/lines/Line2.js";
+import { LineGeometry } from "three/examples/jsm/lines/LineGeometry.js";
+import gotoPic from "@/assets/image/goto.png";
+const offset = 0.25;
+
+function pointsToArray(arr) {
+  let res = [];
+  arr.forEach((i) => {
+    res = res.concat(i.toArray());
+  });
+  return res;
+}
+let m = new THREE.MeshBasicMaterial({
+  map: new THREE.TextureLoader().load(gotoPic),
+  color: 0x26559b,
+  transparent: true,
+});
+export default class Line extends Line2 {
+  constructor(startPoint, endPoint, endEdge, matLine) {
+    let points;
+
+    let g = new THREE.PlaneGeometry(0.1, 0.1);
+    g.rotateX(-Math.PI / 2);
+    let cross = new THREE.Mesh(g, m);
+
+    if (endEdge.name === 0) {
+      // top
+      let a = startPoint.clone();
+      let b = new THREE.Vector3(
+        startPoint.x,
+        startPoint.y,
+        endEdge.y + endEdge.parent.parent.position.z - offset
+      );
+      let c = new THREE.Vector3(
+        endPoint.x,
+        endPoint.y,
+        endEdge.y + endEdge.parent.parent.position.z - offset
+      );
+      let d = new THREE.Vector3(
+        endPoint.x,
+        endPoint.y,
+        endEdge.y + endEdge.parent.parent.position.z
+      );
+      cross.position.copy(d);
+      cross.rotation.y = -Math.PI / 2;
+      cross.position.z -= 0.02;
+
+      points = pointsToArray([a, b, c, d]);
+    } else if (endEdge.name === 1) {
+      //left
+
+      let a = startPoint.clone();
+      let b = new THREE.Vector3(
+        (startPoint.x + endPoint.x) / 2,
+        startPoint.y,
+        startPoint.z
+      );
+      let c = new THREE.Vector3(
+        (startPoint.x + endPoint.x) / 2,
+        startPoint.y,
+        endPoint.z
+      );
+      let d = new THREE.Vector3(
+        endEdge.x + endEdge.parent.parent.parent.position.x,
+        startPoint.y,
+        endPoint.z
+      );
+      cross.position.copy(d);
+
+      const diff = c.x < d.x;
+      cross.rotation.y = diff ? 0 : -Math.PI;
+      diff ? (cross.position.x -= 0.02) : (cross.position.x += 0.02);
+
+      points = pointsToArray([a, b, c, d]);
+    } else if (endEdge.name === 2) {
+      //bottom
+      let a = startPoint.clone();
+      let b = new THREE.Vector3(
+        startPoint.x,
+        startPoint.y,
+        endEdge.y + endEdge.parent.parent.position.z + offset
+      );
+      let c = new THREE.Vector3(
+        endPoint.x,
+        endPoint.y,
+        endEdge.y + endEdge.parent.parent.position.z + offset
+      );
+      let d = new THREE.Vector3(
+        endPoint.x,
+        endPoint.y,
+        endEdge.y + endEdge.parent.parent.position.z
+      );
+      cross.rotation.y = Math.PI / 2;
+      cross.position.copy(d);
+      cross.position.z += 0.02;
+      points = pointsToArray([a, b, c, d]);
+    } else {
+      //right
+      let a = startPoint.clone();
+      let b = new THREE.Vector3(
+        (startPoint.x + endPoint.x) / 2,
+        startPoint.y,
+        startPoint.z
+      );
+      let c = new THREE.Vector3(
+        (startPoint.x + endPoint.x) / 2,
+        startPoint.y,
+        endPoint.z
+      );
+      let d = new THREE.Vector3(
+        endEdge.x + endEdge.parent.parent.parent.position.x,
+        startPoint.y,
+        endPoint.z
+      );
+      const diff = c.x < d.x;
+      cross.position.copy(d);
+
+      cross.rotation.y = diff ? 0 : Math.PI;
+      diff ? (cross.position.x -= 0.02) : (cross.position.x += 0.02);
+
+      points = pointsToArray([a, b, c, d]);
+    }
+
+    const geometry = new LineGeometry();
+
+    cross.visible = false;
+    geometry.setPositions(points);
+    super(geometry, matLine);
+    this.name = "line_" + this.uuid;
+    this.userData = {
+      dir: endEdge.name,
+      points: points,
+    };
+    this.scale.set(1, 1, 1);
+    this.position.y += 0.5;
+    this.add(cross);
+  }
+}

+ 75 - 0
src/core/box/object/LinePoints.js

@@ -0,0 +1,75 @@
+import * as THREE from "three";
+import { Line2 } from "three/examples/jsm/lines/Line2.js";
+import { LineGeometry } from "three/examples/jsm/lines/LineGeometry.js";
+import gotoPic from "@/assets/image/goto.png";
+const offset = 0.25;
+
+function pointsToArray(arr) {
+  let res = [];
+  arr.forEach((i) => {
+    res = res.concat(i.toArray());
+  });
+  return res;
+}
+let m = new THREE.MeshBasicMaterial({
+  map: new THREE.TextureLoader().load(gotoPic),
+  color: 0x26559b,
+  transparent: true,
+});
+export default class LinePoints extends Line2 {
+  constructor(points, matLine, dir, imgId, id) {
+    let g = new THREE.PlaneGeometry(0.1, 0.1);
+    g.rotateX(-Math.PI / 2);
+    let cross = new THREE.Mesh(g, m);
+    let a = new THREE.Vector3(points[0], points[1], points[2]);
+    let b = new THREE.Vector3(points[3], points[4], points[5]);
+    let c = new THREE.Vector3(points[6], points[7], points[8]);
+    let d = new THREE.Vector3(points[9], points[10], points[11]);
+    let diff;
+    switch (dir) {
+      case 0:
+        //top
+        cross.rotation.y = -Math.PI / 2;
+        cross.position.copy(d);
+        break;
+      case 1:
+        //left
+        diff = c.x < d.x;
+        cross.rotation.y = diff ? 0 : -Math.PI;
+        diff ? (cross.position.x -= 0.02) : (cross.position.x += 0.02);
+
+        break;
+      case 2:
+        //bottom
+        cross.position.copy(d);
+        cross.rotation.y = Math.PI / 2;
+        cross.position.z += 0.02;
+        break;
+      case 3:
+        //right
+        diff = c.x < d.x;
+        cross.position.copy(d);
+        cross.rotation.y = diff ? 0 : Math.PI;
+        diff ? (cross.position.x -= 0.02) : (cross.position.x += 0.02);
+        break;
+    }
+    const geometry = new LineGeometry();
+    cross.visible = false;
+    // console.log("points", points);
+    geometry.setPositions(points);
+    super(geometry, matLine);
+    this.name = "line_point_" + this.uuid;
+    this.scale.set(1, 1, 1);
+    this.position.y += 0.5;
+    if (id) {
+      this.uuid = id;
+    }
+    this.userData = {
+      id: id || this.uuid,
+      dir: dir,
+      points,
+      imgId: imgId || null,
+    };
+    this.add(cross);
+  }
+}

+ 54 - 0
src/core/box/object/PureTextLabel copy.js

@@ -0,0 +1,54 @@
+import * as THREE from "three";
+
+export default class PureTextLabel extends THREE.Mesh {
+  constructor(text, point, fontsize = 12, color = "#000000", id) {
+    let res = 2;
+    const width = 168 * res;
+    const height = 50 * res;
+    var canvas = document.createElement("canvas");
+    canvas.width = width;
+    canvas.height = height;
+
+    let fontFamily = "Arial";
+    let fs = fontsize * res;
+    var context = canvas.getContext("2d");
+    context.fillStyle = "transparent";
+    context.rect(0, 0, width, height);
+    context.fill();
+    let fontStyle = "normal " + fs + "px " + fontFamily;
+    // console.log("fontStyle", fontStyle);
+    context.font = fontStyle;
+    context.fillStyle = color;
+    context.textAlign = "center";
+    context.textBaseline = "middle";
+    context.fillText(text, width / 2, height / 2);
+    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.44);
+    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);
+    if (id) {
+      this.uuid = id;
+    }
+    const p = new THREE.Vector3().copy(point);
+    this.userData = {
+      id: this.uuid,
+      text: text,
+      color: color,
+      pos: p.toArray(),
+      fontsize: fontsize,
+    };
+    this.position.copy(p);
+    this.name = "pureText_" + text;
+  }
+}

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

@@ -0,0 +1,73 @@
+import * as THREE from "three";
+import { getWrapText } from "../../utils/text";
+
+export default class PureTextLabel extends THREE.Mesh {
+  constructor(text, point, fontsize = 12, color = "#000000", id) {
+    const radio = fontsize / 12;
+
+    let containerWidth = 1.5 * radio;
+    let containerHeight = 0.12 * radio;
+    const containerRadio = containerWidth / containerHeight;
+    let res = 2;
+    const width = 168 * res * radio;
+    const height = width / containerRadio;
+    var canvas = document.createElement("canvas");
+    canvas.width = width;
+    canvas.height = height;
+
+    let fontFamily = "Arial";
+    let fs = 12 * res * radio;
+    var context = canvas.getContext("2d");
+
+    const lines = getWrapText(context, text, 140);
+    console.log("lines", lines);
+    containerHeight = containerHeight * lines.length;
+    canvas.height = height * lines.length;
+    context.fillStyle = "transparent";
+    // context.fillStyle = "rgba(255,255,255,0.5)";
+    context.rect(0, 0, width, height * lines.length);
+    context.fill();
+    let fontStyle = "normal " + fs + "px " + fontFamily;
+    // console.log("fontStyle", fontStyle);
+    context.font = fontStyle;
+    context.fillStyle = color;
+    context.textAlign = "center";
+    context.textBaseline = "middle";
+    // context.fillText(text, width / 2, height / 2);
+    lines.forEach((txt, index) => {
+      context.fillText(txt, width / 2, height / 2 + height * index);
+    });
+    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(containerWidth, containerHeight);
+
+    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);
+
+    if (id) {
+      this.uuid = id;
+    }
+    const p = new THREE.Vector3().copy(point);
+    this.userData = {
+      id: this.uuid,
+      text: text,
+      color: color,
+      pos: p.toArray(),
+      fontsize: fontsize,
+    };
+    this.position.copy(p);
+
+    this.name = "pureText_" + text;
+  }
+}

+ 48 - 0
src/core/box/object/SimpleLabel.js

@@ -0,0 +1,48 @@
+import * as THREE from "three";
+
+export default class SimpleLabel 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 = 5.2 * res;
+    let offsetX = 75 * res;
+    let offsetY = 10 * res;
+    var context = canvas.getContext("2d");
+
+    context.fillStyle = "#ffffff";
+    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 m = new THREE.MeshBasicMaterial({
+      map: canvas_map,
+    });
+    super(g, m);
+
+    // const edges = new THREE.EdgesGeometry(g);
+    // const line = new THREE.LineSegments(
+    //   edges,
+    //   new THREE.LineBasicMaterial({ color: 0xcccccc })
+    // );
+    // line.position.y += 0.5;
+    // this.add(line);
+
+    this.name = "SimpleLabel_" + text;
+  }
+}

+ 47 - 0
src/core/box/object/TextLabel.js

@@ -0,0 +1,47 @@
+import * as THREE from "three";
+
+export default class TextLabel 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 = "#ffffff";
+    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 m = new THREE.MeshBasicMaterial({
+      map: canvas_map,
+    });
+    super(g, m);
+
+    const edges = new THREE.EdgesGeometry(g);
+    const line = new THREE.LineSegments(
+      edges,
+      new THREE.LineBasicMaterial({ color: 0xcccccc })
+    );
+    line.position.y += 0.5;
+    this.add(line);
+
+    this.name = "textlabel_" + text;
+  }
+}

+ 24 - 0
src/core/box/object/TouchEdge.js

@@ -0,0 +1,24 @@
+import * as THREE from "three";
+import { Line2 } from "three/examples/jsm/lines/Line2.js";
+import { LineGeometry } from "three/examples/jsm/lines/LineGeometry.js";
+
+export default class TouchEdge extends THREE.Group {
+  constructor(positions, matLine) {
+    super();
+    positions.forEach((i, j) => {
+      //top left bottom right
+      const geometry = new LineGeometry();
+      geometry.setPositions(i);
+
+      const line = new Line2(geometry, matLine);
+      line.scale.set(1, 1, 1);
+      line.position.y += 0.5;
+      line.name = j;
+      line.visible = false;
+      line.x = i[0];
+      line.y = i[2];
+      this.add(line);
+    });
+    // console.log(this);
+  }
+}

+ 31 - 0
src/core/box/object/marker.js

@@ -0,0 +1,31 @@
+import * as THREE from "three";
+import gotoPic from "@/assets/image/arrow.svg";
+const m = new THREE.MeshBasicMaterial({
+  map: new THREE.TextureLoader().load(gotoPic),
+  color: 0xe44d54,
+  transparent: true,
+});
+
+export default class Marker extends THREE.Mesh {
+  constructor(startPoint, imageId) {
+    const g = new THREE.PlaneGeometry(0.15, 0.15);
+    g.rotateX(-Math.PI / 2);
+    super(g, m);
+    const a = startPoint.clone();
+    this.position.copy(a);
+
+    this.rotation.y = 0;
+    this.position.y = 5;
+    this.position.z -= 0.02;
+    this.userData = {
+      imageId: imageId || null,
+      point: a.toArray(),
+    };
+    this.visible = true;
+    this.scale.set(1, 1, 1);
+    this.position.y += 0.5;
+    this.name = "marker_" + this.uuid;
+    this.renderOrder = 1000;
+    // console.log(this, this.position);
+  }
+}

+ 501 - 0
src/core/controls/FloorplanControls.js

@@ -0,0 +1,501 @@
+import { Vector2, Vector3 } from "three";
+
+const STATE = {
+  NONE: -1,
+  ROTATE: 0,
+  PAN: 1,
+  ZOOM: 2,
+  ZOOM_PAN: 3,
+  ZOOM_ROTATE: 4,
+};
+const HANDLE = {
+  ROTATE: 0,
+  PAN: 1,
+  ZOOM: 2,
+  ZOOM_PAN: 3,
+  ZOOM_ROTATE: 4,
+};
+
+const pointers = [];
+const pointerPositions = {};
+
+export default class FloorplanControls {
+  constructor(camera, dom, player) {
+    this.camera = camera;
+    this.domElement = dom;
+    this.domElement.style.touchAction = "none"; // disable touch scroll
+    this.player = player;
+
+    this.panSpeed = 1;
+    this.zoomSpeed = 1;
+    this.rotateSpeed = 1;
+
+    this.maxDistance = 100;
+    this.minDistance = 0.1;
+    this.maxZoom = 500;
+    this.minZoom = 5;
+
+    this.target = new Vector3();
+
+    this.state = STATE.NONE;
+
+    this.rotateStart = new Vector2();
+    this.rotateEnd = new Vector2();
+
+    this.panStart = new Vector2();
+    this.panEnd = new Vector2();
+
+    this.zoomStart = new Vector2();
+
+    this.locked = false; //禁止用户操作
+    this.enabled = true; //禁止update
+    this.enablePan = true;
+    this.enableRotate = true;
+    this.enableZoom = true;
+
+    this.touchesEvent = {
+      ONE: HANDLE.PAN,
+      TWO: HANDLE.ZOOM,
+    };
+    this.mouseEvent = {
+      LEFT: HANDLE.PAN,
+      RIGHT: HANDLE.ROTATE,
+      WHEEL: HANDLE.ZOOM,
+    };
+
+    this.onBindEvent();
+  }
+
+  onBindEvent = () => {
+    this.domElement.addEventListener(
+      "pointerdown",
+      this.onPointerDown.bind(this)
+    );
+    this.domElement.addEventListener("pointerup", this.onPointerUp.bind(this));
+    this.domElement.addEventListener(
+      "pointermove",
+      this.onPointerMove.bind(this)
+    );
+    this.domElement.addEventListener(
+      "pointercancel",
+      this.onPointerUp.bind(this)
+    );
+
+    this.domElement.addEventListener(
+      "mousewheel",
+      this.onMouseWheel.bind(this),
+      { passive: false }
+    );
+
+    this.domElement.addEventListener("contextmenu", this.onPreventDefault);
+  };
+
+  addPointer = (event) => {
+    pointers.push(event);
+  };
+  removePointer = (event) => {
+    for (let i = 0; i < pointers.length; i++) {
+      if (pointers[i].pointerId == event.pointerId) {
+        pointers.splice(i, 1);
+        return;
+      }
+    }
+  };
+  isTrackingPointer = (event) => {
+    for (let i = 0; i < pointers.length; i++) {
+      if (pointers[i] == event.pointerId) return true;
+    }
+    return false;
+  };
+  trackPointer = (event) => {
+    let position = pointerPositions[event.pointerId];
+    if (position === undefined) {
+      position = new Vector2();
+      pointerPositions[event.pointerId] = position;
+    }
+    position.set(event.pageX, event.pageY);
+  };
+  getSecondPointerPosition = (event) => {
+    const pointerId =
+      event.pointerId === pointers[0].pointerId
+        ? pointers[1].pointerId
+        : pointers[0].pointerId;
+    return pointerPositions[pointerId];
+  };
+
+  // pointer event
+  onPointerDown = (event) => {
+    if (this.locked) return;
+    if (pointers.length === 0) {
+      this.domElement.setPointerCapture(event.pointerId);
+    }
+    if (this.isTrackingPointer(event)) return;
+    this.addPointer(event);
+    if (event.pointerType === "touch") {
+      this.onTouchStart(event);
+    } else {
+      this.onMouseDown(event);
+    }
+  };
+  onPointerUp = (event) => {
+    if (this.locked) return;
+    this.removePointer(event);
+    if (pointers.length === 0) {
+      this.domElement.releasePointerCapture(event.pointerId);
+      this.state = STATE.NONE;
+    } else if (pointers.length === 1) {
+      const pointerId = pointers[0].pointerId;
+      const position = pointerPositions[pointerId];
+      this.onTouchStart({
+        pointerId: pointerId,
+        pageX: position.x,
+        pageY: position.y,
+      });
+    }
+  };
+  onPointerMove = (event) => {
+    if (this.locked) return;
+    if (event.pointerType === "touch") {
+      this.onTouchMove(event);
+    } else {
+      this.onMouseMove(event);
+    }
+  };
+
+  //touch event
+  onTouchStart = (event) => {
+    this.trackPointer(event);
+    switch (pointers.length) {
+      case 1:
+        switch (this.touchesEvent.ONE) {
+          case HANDLE.ROTATE: //rotate
+            if (this.enableRotate === false) return;
+            this.handleTouchStartRotate();
+            this.state = STATE.ROTATE;
+            break;
+          case HANDLE.PAN: //pan
+            if (this.enablePan === false) return;
+            this.handleTouchStartPan();
+            this.state = STATE.PAN;
+            break;
+          default:
+            state = STATE.NONE;
+        }
+        break;
+      case 2:
+        switch (this.touchesEvent.TWO) {
+          case HANDLE.ZOOM: //zoom
+            if (this.enableZoom === false) return;
+            this.handleTouchStartZoom();
+            this.state = STATE.ZOOM;
+            break;
+          case HANDLE.ZOOM_PAN: //zoom_pan
+            if (this.enableZoom === false && this.enablePan === false) return;
+            this.handleTouchStartZoom();
+            this.handleTouchStartPan();
+            this.state = STATE.ZOOM_PAN;
+            break;
+          //todo case HANDLE.ZOOM_ROTATE:
+          default:
+            state = STATE.NONE;
+        }
+        break;
+      default:
+        this.state = STATE.NONE;
+    }
+  };
+  onTouchMove = (event) => {
+    this.trackPointer(event);
+    switch (this.state) {
+      case STATE.ROTATE:
+        if (this.enableRotate === false) return;
+        this.handleTouchMoveRotate(event);
+        break;
+      case STATE.PAN:
+        if (this.enablePan === false) return;
+        this.handleTouchMovePan(event);
+        break;
+      case STATE.ZOOM:
+        if (this.enableZoom === false) return;
+        this.handleTouchMoveZoom(event);
+        break;
+      case STATE.ZOOM_PAN:
+        if (this.enableZoom) this.handleTouchMoveZoom(event);
+        if (this.enablePan) this.handleTouchMovePan(event);
+        break;
+      //todo case STATE.ZOOM_ROTATE:
+      default:
+        this.state = STATE.NONE;
+    }
+  };
+
+  //mouse event
+  onMouseDown = (event) => {
+    if (this.locked) return;
+    switch (event.button) {
+      case 0: //left
+        switch (this.mouseEvent.LEFT) {
+          case HANDLE.PAN:
+            if (this.enablePan === false) return;
+            this.handleMouseDownPan(event);
+            this.state = STATE.PAN;
+            break;
+          case HANDLE.ROTATE:
+            if (this.enablePan === false) return;
+            this.handleMouseDownRotate(event);
+            this.state = STATE.ROTATE;
+            break;
+          default:
+            this.state = STATE.NONE;
+        }
+        break;
+      case 2: //right
+        switch (this.mouseEvent.RIGHT) {
+          case HANDLE.PAN:
+            if (this.enablePan === false) return;
+            this.handleMouseDownPan(event);
+            this.state = STATE.PAN;
+            break;
+          case HANDLE.ROTATE:
+            if (this.enablePan === false) return;
+            this.handleMouseDownRotate(event);
+            this.state = STATE.ROTATE;
+            break;
+          default:
+            this.state = STATE.NONE;
+        }
+        break;
+      default:
+        this.state = STATE.NONE;
+    }
+  };
+  onMouseMove = (event) => {
+    if (this.locked) return;
+    switch (this.state) {
+      case STATE.PAN:
+        if (this.enablePan === false) return;
+        this.handleMouseMovePan(event);
+        break;
+      case STATE.ROTATE:
+        if (this.enableRotate === false) return;
+        this.handleMouseMoveRotate(event);
+        break;
+      default:
+        this.state = STATE.NONE;
+    }
+  };
+  onMouseWheel = (event) => {
+    // console.log("this", this);
+    if (this.locked) return;
+    if (this.enableZoom === false) return;
+    event.preventDefault();
+    this.handleMouseWheelZoom(event);
+  };
+  onPreventDefault = (event) => {
+    event.preventDefault();
+  };
+
+  //================================handle================================
+  //-------------------------rotate-------------------------
+  handleTouchStartRotate = () => {
+    const position = pointerPositions[pointers[0].pointerId];
+    this.rotateStart.set(position.x, position.y);
+  };
+  handleTouchMoveRotate = (event) => {
+    this.rotateEnd.set(event.pageX, event.pageY);
+    let rotateDelta = this.rotateEnd
+      .clone()
+      .sub(this.rotateStart)
+      .multiplyScalar(this.rotateSpeed);
+    let element = this.domElement;
+    let rotateX = (2 * Math.PI * rotateDelta.x) / element.clientHeight;
+    let rotateY = (2 * Math.PI * rotateDelta.y) / element.clientHeight;
+
+    this.rotate(rotateX, rotateY);
+
+    this.rotateStart.copy(this.rotateEnd);
+  };
+  handleMouseDownRotate = (event) => {
+    this.rotateStart.set(event.pageX, event.pageY);
+  };
+  handleMouseMoveRotate = (event) => {
+    this.rotateEnd.set(event.pageX, event.pageY);
+    let rotateDelta = this.rotateEnd
+      .clone()
+      .sub(this.rotateStart)
+      .multiplyScalar(this.rotateSpeed);
+    let element = this.domElement;
+    let rotateX = (2 * Math.PI * rotateDelta.x) / element.clientHeight;
+    let rotateY = (2 * Math.PI * rotateDelta.y) / element.clientHeight;
+
+    this.rotate(rotateX, rotateY);
+
+    this.rotateStart.copy(this.rotateEnd);
+  };
+  //-------------------------zoom-------------------------
+  handleTouchStartZoom = () => {
+    const dx = pointers[0].pageX - pointers[1].pageX;
+    const dy = pointers[0].pageY - pointers[1].pageY;
+    const distance = Math.sqrt(dx * dx + dy * dy);
+    this.zoomStart.set(0, distance);
+  };
+  handleTouchMoveZoom = (event) => {
+    const position = this.getSecondPointerPosition(event);
+    const dx = event.pageX - position.x;
+    const dy = event.pageY - position.y;
+    const distance = Math.sqrt(dx * dx + dy * dy);
+    let delta = Math.pow(distance / this.zoomStart.y, this.zoomSpeed);
+
+    this.zoom(1 / delta);
+
+    this.zoomStart.set(0, distance);
+  };
+  handleMouseWheelZoom = (event) => {
+    if (event.deltaY > 0) {
+      //zoom out
+      this.zoom(1.05 * this.zoomSpeed);
+    } else {
+      //zoom in
+      this.zoom(0.95 * this.zoomSpeed);
+    }
+  };
+  //-------------------------pan-------------------------
+  handleTouchStartPan = () => {
+    if (pointers.length === 1) {
+      const position = pointerPositions[pointers[0].pointerId];
+      this.panStart.set(position.x, position.y);
+    } else {
+      const x = 0.5 * (pointers[0].pageX + pointers[1].pageX);
+      const y = 0.5 * (pointers[0].pageY + pointers[1].pageY);
+      this.panStart.set(x, y);
+    }
+  };
+  handleTouchMovePan = (event) => {
+    if (pointers.length === 1) {
+      this.panEnd.set(event.pageX, event.pageY);
+    } else {
+      const position = this.getSecondPointerPosition(event);
+      const x = 0.5 * (event.pageX + position.x);
+      const y = 0.5 * (event.pageY + position.y);
+      this.panEnd.set(x, y);
+    }
+    let panDelta = this.panEnd.clone().sub(this.panStart);
+
+    this.pan(panDelta);
+
+    this.panStart.copy(this.panEnd);
+  };
+  handleMouseDownPan = (event) => {
+    this.panStart.set(event.pageX, event.pageY);
+  };
+  handleMouseMovePan = (event) => {
+    this.panEnd.set(event.pageX, event.pageY);
+    let panDelta = this.panEnd.clone().sub(this.panStart);
+
+    this.pan(panDelta);
+
+    this.panStart.copy(this.panEnd);
+  };
+
+  rotate(x, y) {
+    let r = y;
+    if (Math.abs(x) > Math.abs(y)) r = x;
+
+    let cameraRZ = this.camera.rotation.z;
+    cameraRZ += r;
+    if (Math.abs(cameraRZ) >= Math.PI * 2) {
+      cameraRZ -= Math.sign(cameraRZ) * Math.PI * 2;
+    }
+    this.camera.rotation.z = cameraRZ;
+    this.cameraUpdate();
+  }
+  zoom(delta) {
+    // if(this.camera.isPerspectiveCamera) {
+    //   let cameraY = this.camera.position.y
+    //   cameraY *= delta
+    //   cameraY = Math.max(cameraY, this.minDistance)
+    //   cameraY = Math.min(cameraY, this.maxDistance)
+    //   this.camera.position.y = cameraY //handle
+    // } else if(this.camera.isOrthographicCamera) {
+    //   let zoom = this.camera.zoom
+    //   zoom *= 1/delta
+    //   console.log(zoom)
+    //   this.camera.zoom = zoom
+    //   this.camera.updateProjectionMatrix()
+    // }
+
+    let cameraY = this.camera.position.y;
+    cameraY *= delta;
+    cameraY = Math.max(cameraY, this.minDistance);
+    cameraY = Math.min(cameraY, this.maxDistance);
+    this.camera.position.y = cameraY; //handle
+    if (this.camera.isOrthographicCamera) {
+      let zoom = this.camera.zoom;
+      zoom *= 1 / delta;
+      zoom = Math.max(zoom, this.minZoom);
+      zoom = Math.min(zoom, this.maxZoom);
+      this.camera.zoom = zoom;
+      this.camera.updateProjectionMatrix();
+    }
+
+    this.cameraUpdate();
+  }
+  pan(delta) {
+    const element = this.domElement;
+    const matrix = this.camera.matrix.clone();
+    const left = new Vector3();
+    const up = new Vector3();
+    let panDelta = delta.multiplyScalar(this.panSpeed);
+
+    if (this.camera.isPerspectiveCamera) {
+      let scalar =
+        (2 *
+          this.camera.position.y *
+          Math.tan(((this.camera.fov / 2) * Math.PI) / 180.0)) /
+        element.clientHeight;
+      panDelta.multiplyScalar(scalar);
+
+      left.setFromMatrixColumn(matrix, 0);
+      left.multiplyScalar(-panDelta.x);
+      up.setFromMatrixColumn(matrix, 1);
+      up.multiplyScalar(panDelta.y);
+    } else if (this.camera.isOrthographicCamera) {
+      (panDelta.x =
+        (panDelta.x * (this.camera.right - this.camera.left)) /
+        this.camera.zoom /
+        element.clientWidth),
+        this.camera.matrix;
+      (panDelta.y =
+        (panDelta.y * (this.camera.top - this.camera.bottom)) /
+        this.camera.zoom /
+        element.clientHeight),
+        this.camera.matrix;
+      left.setFromMatrixColumn(matrix, 0);
+      left.multiplyScalar(-panDelta.x);
+      up.setFromMatrixColumn(matrix, 1);
+      up.multiplyScalar(panDelta.y);
+    } else {
+      return;
+    }
+    this.camera.position.add(left).add(up);
+    this.target.set(this.camera.position.x, 0, this.camera.position.z);
+    this.cameraUpdate();
+  }
+
+  lookAt(target, height) {
+    if (!target) return;
+    height = height !== undefined ? height : this.camera.position.y;
+    this.camera.position.set(target.x, height, target.z);
+    this.target.set(target.x, 0, target.z);
+    this.camera.lookAt(this.target);
+  }
+
+  cameraUpdate = () => {
+    this.camera.updateMatrix();
+    this.camera.updateProjectionMatrix();
+  };
+
+  update = () => {
+    if (!this.enabled) return;
+  };
+}

+ 10 - 0
src/core/mitt.js

@@ -0,0 +1,10 @@
+import mitt from "mitt";
+export class Mitt {
+  constructor() {
+    const emitter = mitt();
+
+    Object.keys(emitter).forEach((method) => {
+      this[method] = emitter[method];
+    });
+  }
+}

+ 969 - 0
src/core/player/Player.js

@@ -0,0 +1,969 @@
+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) {
+  let x = (event.offsetX / domElement.clientWidth) * 2 - 1;
+  let y = -(event.offsetY / domElement.clientHeight) * 2 + 1;
+  return new THREE.Vector2(x, y);
+};
+export default class Player {
+  constructor(scene) {
+    this.scene = scene;
+    this.orthCamera = scene.orthCamera;
+
+    this.floorplanControls = null;
+    this.raycaster = null;
+
+    this.position = new THREE.Vector3();
+
+    this.pointerdown = new THREE.Vector2();
+    this.pointerup = new THREE.Vector2();
+    this.pointer = new THREE.Vector2();
+    this.markPosition = new THREE.Vector3();
+
+    this.touchImg = null;
+    this.activeEdge = null;
+    this.drawLine = null;
+    this.startObj = null;
+    this.marker = null;
+    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.renderTexts = [];
+
+    this.matLine = null;
+    this.lineColor = 0xe44d54;
+    // 1是画线,2是标方向, 3符号, 4文本
+    this.mode = 0;
+    this.init();
+  }
+
+  setMode(mode) {
+    this.mode = mode;
+
+    if (mode !== 0) {
+      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;
+      const lastIndex = this.getLaSybIndex();
+      this.symbolIndex = lastIndex + 1;
+
+      this.symbol = new CircleTextLabel(this.symbolIndex, pos);
+      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, pos);
+      this.text.visible = false;
+      this.showText = "文本";
+      this.scene.scene.add(this.text);
+      this.drawing = true;
+    }
+
+    if (mode === 0) {
+      this.setFreeMode();
+    }
+    this.scene.emit("mode", this.mode);
+  }
+  getLaSybIndex() {
+    const maxIndexObject = this.renderSymbols.reduce(
+      (max, current) => {
+        return current.index > max.index ? current : max;
+      },
+      { index: 0, point: [] }
+    );
+    return maxIndexObject.index;
+  }
+
+  setFreeMode() {
+    this.floorplanControls.enablePan = true;
+    this.floorplanControls.mouseButtons = {
+      LEFT: THREE.MOUSE.PAN,
+      MIDDLE: THREE.MOUSE.DOLLY,
+      RIGHT: THREE.MOUSE.PAN,
+    };
+    this.reset();
+  }
+
+  setEditMode() {
+    this.floorplanControls.enablePan = true;
+    this.floorplanControls.mouseButtons = {
+      LEFT: THREE.MOUSE.ROTATE,
+      MIDDLE: THREE.MOUSE.DOLLY,
+      RIGHT: THREE.MOUSE.PAN,
+    };
+  }
+
+  setRotateMode() {
+    this.floorplanControls.enableRotate = true;
+    this.floorplanControls.mouseButtons = {
+      LEFT: THREE.MOUSE.ROTATE,
+      MIDDLE: THREE.MOUSE.DOLLY,
+      RIGHT: THREE.MOUSE.PAN,
+    };
+  }
+  init = () => {
+    // //floorplanControls
+    // this.floorplanControls = new FloorplanControls(
+    //   this.orthCamera,
+    //   this.scene.domElement,
+    //   this
+    // );
+    this.floorplanControls = new OrbitControls(
+      this.orthCamera,
+      this.scene.domElement
+    );
+
+    this.floorplanControls.enablePan = true;
+    // this.floorplanControls.target.set(0, 1, 0);
+    // this.floorplanControls.rotateSpeed = 0.5;
+    // this.floorplanControls.panSpeed = 0.75
+
+    this.floorplanControls.maxDistance = 100;
+    this.floorplanControls.minDistance = 3.5;
+    this.floorplanControls.maxZoom = 500;
+    this.floorplanControls.minZoom = 100;
+
+    // this.floorplanControls.mouseButtons = {
+    //   LEFT: THREE.MOUSE.PAN,
+    //   MIDDLE: THREE.MOUSE.DOLLY,
+    //   RIGHT: THREE.MOUSE.PAN
+    // }
+    this.setMode(0);
+    this.floorplanControls.enableRotate = false;
+    this.raycaster = new THREE.Raycaster();
+    this.onBindEvent();
+    this.inited = true;
+    this.matLine = new LineMaterial({
+      color: this.lineColor,
+      linewidth: 4, // in world units with size attenuation, pixels otherwise
+      dashed: false,
+      alphaToCoverage: true,
+    });
+    this.matLine.resolution = new THREE.Vector2(
+      this.scene.width,
+      this.scene.height
+    );
+  };
+
+  onPointerMove = (e) => {
+    // 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) {
+      let intersectArr = this.scene.boxManager.imgList;
+      // if(this.startObj) {
+      //   let i = intersectArr.indexOf(this.startObj)
+      //   intersectArr.splice(i, 1)
+      // }
+      const intersects = this.raycaster.intersectObjects(intersectArr, false);
+      if (intersects[0] && intersects[0].object !== this.startObj) {
+        this.touchImg = intersects[0];
+        this.setActiveLine(this.touchImg);
+      }
+    }
+    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;
+        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;
+        const clamp = new THREE.Vector3();
+        this.scene.boxManager.obb.clampPoint(pos, clamp);
+        clamp.y = 5;
+        this.symbol.position.copy(clamp);
+      }
+    }
+
+    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;
+
+        const clamp = new THREE.Vector3();
+        this.scene.boxManager.obb.clampPoint(pos, clamp);
+        clamp.y = 5;
+        this.text.position.copy(clamp);
+        this.text.userData.pos = clamp.toArray();
+      }
+    }
+  };
+
+  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") ||
+          String(i.object.name).includes("circle") ||
+          String(i.object.name).includes("pureText")
+        ) {
+          let type;
+          switch (true) {
+            case String(i.object.name).includes("marker"):
+              type = 1;
+              break;
+            case String(i.object.name).includes("line"):
+              type = 2;
+              break;
+            case String(i.object.name).includes("circle"):
+              type = 3;
+              break;
+            case String(i.object.name).includes("pureText"):
+              type = 4;
+              break;
+          }
+
+          this.selectItem = i.object;
+          this.scene.emit("confirmDelete", {
+            id: i.object.uuid,
+            type,
+          });
+        }
+      });
+    }
+
+    if (this.mode === 1) {
+      this.raycaster.setFromCamera(this.pointerdown, this.orthCamera);
+      let intersectArr = this.scene.boxManager.imgList;
+      const intersects = this.raycaster.intersectObjects(intersectArr, false);
+      console.log("intersects", intersects);
+      if (intersects[0]) {
+        this.startObj = intersects[0].object;
+        this.drawing = true;
+      } else {
+        this.startObj = null;
+        this.drawing = false;
+      }
+    }
+
+    if (this.mode === 2) {
+      if (this.marker) {
+        this.raycaster.setFromCamera(this.pointerdown, this.orthCamera);
+        let intersectArr = this.scene.boxManager.imgList;
+        const intersects = this.raycaster.intersectObjects(intersectArr, false);
+
+        if (intersects[0]) {
+          // this.drawing = false;
+          const imageId = intersects[0].object.userData;
+          let lasPos = new THREE.Vector3(
+            this.pointerdown.x,
+            this.pointerdown.y,
+            -1
+          );
+          lasPos.unproject(this.orthCamera);
+          lasPos.y = 5;
+          const marker = new Marker(lasPos, imageId);
+
+          const activeMarkeritem = {
+            id: imageId,
+            point: lasPos.toArray(),
+          };
+          const exist = this.renderMarkers.find((item) => item.id === imageId);
+
+          if (!exist) {
+            this.scene.scene.add(marker);
+            this.renderMarkers.push(activeMarkeritem);
+            this.scene.scene.remove(this.marker);
+            this.marker = null;
+          } else {
+            this.scene.emit("markerExist");
+          }
+
+          console.log("activeMarkeritem", activeMarkeritem);
+          this.setMode(0);
+        }
+      }
+    }
+
+    if (this.mode === 3) {
+      if (this.symbol) {
+        let lasPos = new THREE.Vector3(
+          this.pointerdown.x,
+          this.pointerdown.y,
+          -1
+        );
+        lasPos.unproject(this.orthCamera);
+        lasPos.y = 5;
+
+        const activeSymbolItem = {
+          index: this.symbolIndex,
+          point: lasPos.toArray(),
+        };
+        this.renderSymbols.push(activeSymbolItem);
+        console.log("activeSymbolItem", 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("edit", {
+          type: 4,
+          text: this.showText,
+          id: this.text.uuid,
+          ...this.text.userData,
+        });
+        this.drawing = true;
+        // 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);
+    if (this.mode === 1) {
+      this.drawing = false;
+      this.floorplanControls.enabled = true;
+
+      if (this.drawLine) {
+        const points = this.drawLine.userData.points;
+        const dir = this.drawLine.userData.dir;
+        const imageId = this.touchImg.object.userData;
+        const finishLine = new LinePoints(points, this.matLine, dir, imageId);
+        // console.log("startObj", this.startObj);
+        this.scene.scene.add(finishLine);
+        const activeLineItem = {
+          id: finishLine.uuid,
+          imgId: imageId,
+          startId: this.startObj.userData,
+          dir: dir,
+          points: points,
+        };
+        const activeEdgeItem = {
+          id: imageId,
+          startId: this.startObj.userData,
+          dir: [dir],
+        };
+
+        this.renderLines.push(activeLineItem);
+        // console.log("this.touchImg", activeLineItem, points);
+        this.insertActiveEdge(activeEdgeItem);
+        this.scene.scene.remove(this.drawLine);
+        this.drawLine = null;
+        this.startObj = null;
+      }
+    }
+    if (this.mode === 2) {
+      // this.drawing = false;
+    }
+    if (this.mode === 4) {
+      if (this.text) {
+        let pos = new THREE.Vector3(this.pointerup.x, this.pointerup.y, -1);
+        pos.unproject(this.orthCamera);
+        pos.y = 5;
+        const clamp = new THREE.Vector3();
+        this.scene.boxManager.obb.clampPoint(pos, clamp);
+        clamp.y = 5;
+        console.log("pos", pos);
+        console.log("clamp", clamp);
+        this.text.position.copy(clamp);
+        this.text.userData.pos = clamp.toArray();
+        const activeTextItem = this.text.userData;
+        this.drawing = false;
+        console.log("activeTextItem", activeTextItem);
+        this.insertrenderTexts(activeTextItem);
+      }
+    }
+    this.syncDrawData();
+  };
+
+  Listener = {
+    onPointerDown: this.onPointerDown.bind(this),
+    onPointerMove: this.onPointerMove.bind(this),
+    onPointerUp: this.onPointerUp.bind(this),
+  };
+
+  onBindEvent = () => {
+    this.scene.domElement.addEventListener(
+      "pointerdown",
+      this.Listener.onPointerDown
+    );
+    this.scene.domElement.addEventListener(
+      "pointermove",
+      this.Listener.onPointerMove,
+      false
+    );
+    this.scene.domElement.addEventListener(
+      "pointerup",
+      this.Listener.onPointerUp
+    );
+  };
+  unbindEvent = () => {
+    this.scene.domElement.removeEventListener(
+      "pointerdown",
+      this.Listener.onPointerDown
+    );
+    this.scene.domElement.removeEventListener(
+      "pointermove",
+      this.Listener.onPointerMove
+    );
+    this.scene.domElement.removeEventListener(
+      "pointerup",
+      this.Listener.onPointerUp
+    );
+  };
+
+  buildLine = () => {
+    if (this.drawLine) {
+      this.drawLine.removeFromParent();
+    }
+    let s = new THREE.Vector3(this.pointerdown.x, this.pointerdown.y, -1);
+    let e = new THREE.Vector3(this.pointermove.x, this.pointermove.y, -1);
+    s.unproject(this.orthCamera);
+    e.unproject(this.orthCamera);
+    s.y = 5;
+    e.y = 5;
+    const matLine = new LineMaterial({
+      color: this.lineColor,
+      linewidth: 4, // in world units with size attenuation, pixels otherwise
+      dashed: false,
+      alphaToCoverage: true,
+    });
+    matLine.resolution = new THREE.Vector2(this.scene.width, this.scene.height);
+    this.drawLine = new Line(s, e, this.activeEdge, matLine);
+    this.scene.scene.add(this.drawLine);
+  };
+
+  setActiveLine = (obj) => {
+    function getTouchLine(x, y) {
+      // [0 - 1]
+      x -= 0.5;
+      y -= 0.5;
+      // console.log(x, y);
+      if (x >= 0 && y >= 0) {
+        if (x > y) {
+          return 3;
+        } else {
+          return 0;
+        }
+      } else if (x >= 0 && y <= 0) {
+        if (x > Math.abs(y)) {
+          return 3;
+        } else {
+          return 2;
+        }
+      } else if (x <= 0 && y >= 0) {
+        if (Math.abs(x) > y) {
+          return 1;
+        } else {
+          return 0;
+        }
+      } else if (x <= 0 && y <= 0) {
+        if (-x > -y) {
+          return 1;
+        } else {
+          return 2;
+        }
+      }
+    }
+
+    if (this.activeEdge) {
+      this.activeEdge.visible = false;
+      this.activeEdge = null;
+    }
+
+    let num = getTouchLine(obj.uv.x, obj.uv.y);
+    this.activeEdge = obj.object.touchLines.getObjectByName(num);
+    this.activeEdge.visible = true;
+    this.buildLine();
+  };
+
+  insertActiveEdge(item) {
+    const exist = this.activeEdges.find((s) => item.id === s.id);
+    if (exist) {
+      exist.dir = [...new Set([...exist.dir, ...item.dir])];
+    } else {
+      this.activeEdges.push(item);
+    }
+  }
+
+  // insertActiveMarker(item) {
+  //   const exist = this.activeEdges.find((s) => item.id === s.id);
+  //   if (exist) {
+  //     exist.dir = [...new Set([...exist.dir, ...item.dir])];
+  //   } else {
+  //     this.activeEdges.push(item);
+  //   }
+  // }
+
+  insertrenderTexts(item) {
+    const index = this.renderTexts.findIndex((s) => item.id === s.id);
+    if (index > -1) {
+      this.renderTexts[index] = item;
+    } else {
+      this.renderTexts.push(item);
+    }
+  }
+
+  showAllActiveEdges() {
+    if (this.inited) {
+      let imgList = this.scene.boxManager.imgList;
+      if (this.activeEdges.length > 0) {
+        this.activeEdges.forEach((edge) => {
+          const exist = imgList.find((item) => item.userData === edge.id);
+          if (exist) {
+            let others = [0, 1, 2, 3].filter((x) => !edge.dir.includes(x));
+            // console.log("others", others);
+            edge.dir.forEach((dir) => {
+              exist.touchLines.children[dir].visible = true;
+            });
+            // console.log("others", others);
+            others.forEach((dir) => {
+              exist.touchLines.children[dir].visible = false;
+            });
+          }
+        });
+      } else {
+        imgList.forEach((img) => {
+          // console.log("img", img);
+          img.touchLines.children.forEach((line) => {
+            if (line.visible) {
+              line.visible = false;
+            }
+          });
+        });
+      }
+    }
+  }
+  deleteItemByType(type, data) {
+    if (type === 1) {
+      const index = this.renderMarkers.findIndex((mk) => {
+        const p = new THREE.Vector3().fromArray(mk.point);
+        const v = new THREE.Vector3().fromArray(data.point);
+        return p.equals(v);
+      });
+      this.renderMarkers.splice(index, 1);
+    }
+    if (type === 2) {
+      const { imgId, id, dir, points } = data;
+      const index = this.renderLines.findIndex((item) => item.id === id);
+      index > -1 && this.renderLines.splice(index, 1);
+      //线段处理完成
+      const egIndex = this.activeEdges.findIndex((eg) => eg.id === imgId);
+      if (egIndex > -1) {
+        //存在activeEdge 再找renderLines的sibling
+        const cluEgArr = this.renderLines
+          .filter((l) => l.imgId === this.activeEdges[egIndex].id)
+          .reduce((pre, curr) => pre.concat(curr["dir"]), []);
+        const uni_dir = [...new Set(cluEgArr)];
+        console.log("uni_dir", uni_dir);
+        if (uni_dir.length > 0) {
+          this.activeEdges[egIndex].dir = uni_dir;
+        } else {
+          console.log("全空", this.activeEdges[egIndex].id);
+          let imgList = this.scene.boxManager.imgList;
+          const image = imgList.find(
+            (item) => item.userData === this.activeEdges[egIndex].id
+          );
+          image.touchLines.children.forEach((line) => {
+            line.visible = false;
+          });
+          this.activeEdges.splice(egIndex, 1);
+          this.update();
+        }
+      }
+    }
+    if (type === 3) {
+      const index = this.renderSymbols.findIndex((syb) => {
+        const p = new THREE.Vector3().fromArray(syb.point);
+        const v = new THREE.Vector3().fromArray(data);
+        return p.equals(v);
+      });
+      this.renderSymbols.splice(index, 1);
+    }
+    if (type === 4) {
+      const { id } = data;
+      const index = this.renderTexts.findIndex((item) => item.id === id);
+      index > -1 && this.renderTexts.splice(index, 1);
+    }
+  }
+  editing(item) {
+    if (item.type === 4) {
+      if (this.text) {
+        const { pos } = this.text.userData;
+        const newP = new THREE.Vector3().fromArray(pos);
+        this.scene.scene.remove(this.text);
+        this.text = null;
+        this.showText = item.text;
+        console.log("editing", item, newP);
+        // // console.log("this.text", lastPos, newP, item);
+        this.text = new PureTextLabel(
+          item.text,
+          newP,
+          item.fontsize,
+          item.color,
+          item.id
+        );
+        this.scene.scene.add(this.text);
+        const activeTextItem = this.text.userData;
+        console.log("activeTextItem", activeTextItem);
+        this.insertrenderTexts(activeTextItem);
+      }
+    }
+  }
+  getDrawData() {
+    let data;
+    if (this.scene.sceneType === 1) {
+      data = {
+        hor_lines: this.renderLines,
+        hor_activeEdges: this.activeEdges,
+        hor_markers: this.renderMarkers,
+        hor_symbols: this.renderSymbols,
+        hor_texts: this.renderTexts,
+        vir_lines: [],
+        vir_activeEdges: [],
+        vir_markers: [],
+        vir_symbols: [],
+        vir_texts: [],
+      };
+    } else {
+      data = {
+        hor_lines: [],
+        hor_activeEdges: [],
+        hor_markers: [],
+        hor_symbols: [],
+        hor_texts: [],
+        vir_lines: this.renderLines,
+        vir_activeEdges: this.activeEdges,
+        vir_markers: this.renderMarkers,
+        vir_symbols: this.renderSymbols,
+        vir_texts: this.renderTexts,
+      };
+    }
+
+    // console.log("sceneType", this.scene.sceneType);
+    return data;
+  }
+
+  syncDrawData() {
+    const data = this.getDrawData();
+    this.scene.emit("data", data);
+  }
+  load(type, data) {
+    if (type === 1) {
+      console.log("data1", data);
+      const {
+        hor_activeEdges,
+        hor_lines,
+        hor_markers,
+        hor_symbols,
+        hor_texts,
+      } = data;
+      hor_activeEdges && (this.activeEdges = hor_activeEdges);
+      if (hor_lines && Array.isArray(hor_lines)) {
+        this.renderLines = hor_lines;
+        hor_lines.forEach((line) => {
+          const finishLine = new LinePoints(
+            line.points,
+            this.matLine,
+            line.dir,
+            line.imgId,
+            line.id
+          );
+          this.scene.scene.add(finishLine);
+        });
+      }
+      if (hor_markers && Array.isArray(hor_markers)) {
+        this.renderMarkers = hor_markers;
+        hor_markers.forEach((pos) => {
+          console.log("pos");
+          const p = new THREE.Vector3().fromArray(pos.point);
+          const marker = new Marker(p, pos.id);
+          this.scene.scene.add(marker);
+        });
+      }
+
+      if (hor_symbols && Array.isArray(hor_symbols)) {
+        this.renderSymbols = hor_symbols;
+        hor_symbols.forEach((syb) => {
+          console.log("pos");
+          const p = new THREE.Vector3().fromArray(syb.point);
+          const symbol = new CircleTextLabel(syb.index, p);
+          this.scene.scene.add(symbol);
+        });
+      }
+      if (hor_texts && Array.isArray(hor_texts)) {
+        this.renderTexts = hor_texts;
+        hor_texts.forEach((txt) => {
+          console.log("pos");
+          const p = new THREE.Vector3().fromArray(txt.pos);
+          const text = new PureTextLabel(
+            txt.text,
+            p,
+            txt.fontsize,
+            txt.color,
+            txt.id
+          );
+          this.scene.scene.add(text);
+        });
+      }
+    }
+
+    if (type === 2) {
+      const {
+        vir_activeEdges,
+        vir_lines,
+        vir_markers,
+        vir_symbols,
+        vir_texts,
+      } = data;
+      vir_activeEdges && (this.activeEdges = vir_activeEdges);
+      if (vir_lines && Array.isArray(vir_lines)) {
+        this.renderLines = vir_lines;
+        vir_lines.forEach((line) => {
+          const finishLine = new LinePoints(
+            line.points,
+            this.matLine,
+            line.dir,
+            line.imgId,
+            line.id
+          );
+          this.scene.scene.add(finishLine);
+        });
+      }
+      if (vir_markers && Array.isArray(vir_markers)) {
+        this.renderMarkers = vir_markers;
+        vir_markers.forEach((pos) => {
+          const p = new THREE.Vector3().fromArray(pos.point);
+          const marker = new Marker(p);
+          this.scene.scene.add(marker);
+        });
+      }
+      if (vir_symbols && Array.isArray(vir_symbols)) {
+        this.renderSymbols = vir_symbols;
+        vir_symbols.forEach((syb) => {
+          // console.log("pos", syb);
+          const p = new THREE.Vector3().fromArray(syb.point);
+          const symbol = new CircleTextLabel(syb.index, p);
+          this.scene.scene.add(symbol);
+        });
+      }
+      if (vir_texts && Array.isArray(vir_texts)) {
+        this.renderTexts = vir_texts;
+        vir_texts.forEach((txt) => {
+          console.log("pos");
+          const p = new THREE.Vector3().fromArray(txt.pos);
+          const text = new PureTextLabel(
+            txt.text,
+            p,
+            txt.fontsize,
+            txt.color,
+            txt.id
+          );
+          this.scene.scene.add(text);
+        });
+      }
+    }
+    this.syncDrawData();
+  }
+  reset() {
+    if (this.marker) {
+      this.scene.scene.remove(this.marker);
+      this.marker = null;
+    }
+    if (this.drawLine) {
+      this.scene.scene.remove(this.drawLine);
+      this.drawLine = null;
+    }
+    if (this.touchImg) {
+      this.touchImg = null;
+    }
+    if (this.activeEdge) {
+      this.activeEdge = null;
+    }
+    if (this.text) {
+      this.text = null;
+      this.scene.scene.remove(this.text);
+    }
+    this.showText = "文本";
+    this.drawing = false;
+  }
+
+  clear() {
+    this.activeEdges = [];
+    this.renderLines = [];
+    this.renderMarkers = [];
+    this.renderSymbols = [];
+    this.renderTexts = [];
+    this.reset();
+    this.scene.clearDrawScene();
+    this.syncDrawData();
+  }
+
+  //单多张图片删除时要删除相关数据
+  deleteImageDataByIds(ids) {
+    setTimeout(() => {
+      console.warn("单多张图片删除时要删除相关数据", ids);
+      this.clear();
+      this.scene.emit("autoSave");
+      // this.t_deleteImageDataByIds(ids);
+    }, 500);
+  }
+  t_deleteImageDataByIds(ids) {
+    ids.forEach((id) => {
+      const makerIndex = this.renderMarkers.findIndex((item) => item.id === id);
+      if (makerIndex > -1) {
+        this.renderMarkers.splice(makerIndex, 1);
+      }
+      const lines = this.renderLines.filter(
+        (item) => item.imgId === id || item.startId === id
+      );
+
+      lines.forEach((line) => {
+        const edge = this.activeEdges.find(
+          (item) => item.id === line.imgId || item.startId === line.imgId
+        );
+        if (edge) {
+          if (edge.dir.length > 0) {
+            const dirIndex = Array.from(edge.dir).findIndex(
+              (d) => d === line.dir
+            );
+            edge.dir.splice(dirIndex, 1);
+          } else {
+            const indexEdge = this.activeEdges.findIndex(
+              (a) => a.id === edge.id
+            );
+            console.warn("edge没dir", indexEdge);
+            indexEdge > -1 && this.activeEdges.splice(indexEdge, 1);
+          }
+        }
+
+        const lineIndex = this.renderLines.findIndex(
+          (item) => item.imageId === line.imageId
+        );
+        console.log("lineIndex", lineIndex);
+        if (lineIndex > -1) {
+          this.renderLines.splice(lineIndex, 1);
+        }
+      });
+      console.log("lines", lines);
+    });
+
+    setTimeout(() => {
+      this.syncDrawData();
+      this.scene.emit("autoSave");
+    }, 2500);
+  }
+
+  checkDeleteing() {
+    const makers = this.scene.scene.children.filter((obj) =>
+      String(obj.name).includes("marker_")
+    );
+    Array.from(makers).forEach((marker) => {
+      const { imageId, point } = marker.userData;
+      const image = this.scene.boxManager.imgList.find(
+        (item) => item.userData === imageId
+      );
+      if (image) {
+        const nPoint = new THREE.Vector3().fromArray(point);
+        nPoint.project(this.orthCamera);
+        console.log("nPoint", nPoint, point);
+
+        this.raycaster.setFromCamera(nPoint, this.orthCamera);
+        const intersects = this.raycaster.intersectObjects(
+          this.scene.boxManager.imgList,
+          false
+        );
+
+        if (image.userData !== intersects[0].object.userData) {
+          console.log("相交不正确");
+          this.scene.scene.remove(marker);
+        }
+      }
+      // const
+    });
+  }
+
+  update = () => {
+    if (this.floorplanControls.enabled) {
+      this.floorplanControls && this.floorplanControls.update();
+      this.scene.boxManager && this.showAllActiveEdges();
+    }
+  };
+}

+ 8 - 0
src/core/save.json

@@ -0,0 +1,8 @@
+{
+    "hor_lines": [],
+    "vir_lines": [],
+    "hor_markers": [],
+    "vir_markers": [],
+    "hor_activeEdges": [],
+    "vir_activeEdges": []
+}

+ 0 - 0
src/core/settings/style.js


+ 92 - 0
src/core/utils/cap.js

@@ -0,0 +1,92 @@
+import * as THREE from "three";
+
+export function screenshotObject(obj, camera, renderer) {
+  this.obj = obj;
+  this.camera = camera;
+  this.renderer = renderer;
+  this.box = new THREE.Box3().setFromObject(obj);
+  this.size = { w: 0, h: 0 };
+  this.pos = { x: 0, y: 0 };
+
+  var distance = this.distance();
+  this.size = this.getSizeInPixel(distance);
+  this.pos = this.getPositionInPixel();
+  debugger;
+  this.getImage(this.size.w, this.size.h, this.pos.x, this.pos.y);
+}
+
+screenshotObject.prototype.distance = function () {
+  var self = this;
+  var size = new THREE.Vector3();
+  self.box.getSize(size);
+  var z = self.camera.position.z - self.obj.position.z - size.z / 2;
+  // or use self.camera.position.distanceTo( self.obj.position );
+  return z;
+};
+screenshotObject.prototype.getSizeInPixel = function (distance) {
+  var self = this;
+  var size = new THREE.Vector3();
+  self.box.getSize(size);
+
+  // Calc visible height and width
+  var vFOV = THREE.MathUtils.degToRad(self.camera.far); // convert vertical fov to radians
+  var height = 2 * Math.tan(vFOV / 2) * Math.abs(distance); // visible height
+  var width =
+    height * (self.renderer.domElement.width / self.renderer.domElement.height); // visible width
+  // Calc ratio between pixel and visible z-unit of threejs
+  var ratio = self.renderer.domElement.height / height;
+
+  var width = size.x * ratio;
+  var height = size.y * ratio;
+  return { w: width, h: height };
+};
+screenshotObject.prototype.getPositionInPixel = function () {
+  var self = this;
+  var vector = new THREE.Vector3();
+  var viewProjectionMatrix = new THREE.Matrix4();
+  var viewMatrix = new THREE.Matrix4();
+  viewMatrix.copy(self.camera.matrixWorldInverse);
+  viewProjectionMatrix.multiplyMatrices(
+    self.camera.projectionMatrix,
+    viewMatrix
+  );
+  var widthHalf = 0.5 * self.renderer.domElement.width;
+  var heightHalf = 0.5 * self.renderer.domElement.height;
+  self.obj.updateMatrixWorld();
+  vector.setFromMatrixPosition(self.obj.matrixWorld);
+  //vector.project(camera);
+  vector.applyMatrix4(viewProjectionMatrix);
+
+  vector.x = vector.x * widthHalf + widthHalf;
+  vector.y = -(vector.y * heightHalf) + heightHalf;
+
+  var x = vector.x - self.size.w / 2;
+  var y = vector.y - self.size.h / 2;
+  return { x: x, y: y };
+};
+screenshotObject.prototype.getImage = function (w, h, x, y) {
+  var self = this;
+  var oldCanvas = self.renderer.domElement;
+  var newCanvas = document.createElement("canvas");
+  newCanvas.width = w;
+  newCanvas.height = h;
+  var newContext = newCanvas.getContext("2d");
+  newContext.drawImage(oldCanvas, x, y, w, h, 0, 0, w, h);
+
+  var fileName = "test.png";
+  var strMime = "image/png";
+  var strDownloadMime = "image/octet-stream";
+  var imgData = newCanvas.toDataURL(strMime);
+  var base64str = imgData.replace(strMime, strDownloadMime);
+
+  var link = document.createElement("a");
+  if (typeof link.download === "string") {
+    document.body.appendChild(link); //Firefox requires the link to be in the body
+    link.download = fileName;
+    link.href = base64str;
+    link.click();
+    document.body.removeChild(link); //remove the link when done
+  } else {
+    window.location.replace(uri);
+  }
+};

+ 14 - 0
src/core/utils/text.js

@@ -0,0 +1,14 @@
+export const getWrapText = (ctx, text = "", maxWidth = 200) => {
+  let txtList = [];
+  let str = "";
+  for (let i = 0, len = text.length; i < len; i++) {
+    str += text.charAt(i);
+    if (ctx.measureText(str).width > maxWidth) {
+      txtList.push(str.substring(0, str.length - 1));
+      str = "";
+      i--;
+    }
+  }
+  txtList.push(str);
+  return txtList;
+};

+ 39 - 0
src/core/utils/utils.js

@@ -0,0 +1,39 @@
+
+
+export function dataURItoBlob(dataURI) {
+  // convert base64 to raw binary data held in a string
+  // doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this
+  var byteString = atob(dataURI.split(",")[1]);
+
+  // separate out the mime component
+  var mimeString = dataURI.split(",")[0].split(":")[1].split(";")[0];
+
+  // write the bytes of the string to an ArrayBuffer
+  var ab = new ArrayBuffer(byteString.length);
+
+  // create a view into the buffer
+  var ia = new Uint8Array(ab);
+
+  // set the bytes of the buffer to the correct values
+  for (var i = 0; i < byteString.length; i++) {
+    ia[i] = byteString.charCodeAt(i);
+  }
+
+  // write the ArrayBuffer to a blob, and you're done
+  var blob = new Blob([ab], { type: mimeString });
+  return blob;
+}
+
+export const saveFile = function (strData, filename) {
+  var link = document.createElement("a");
+  if (typeof link.download === "string") {
+    document.body.appendChild(link); //Firefox requires the link to be in the body
+    link.download = filename;
+    link.href = strData;
+    link.click();
+    document.body.removeChild(link); //remove the link when done
+  } else {
+    location.replace(uri);
+  }
+};
+

+ 21 - 1
src/request/urls.ts

@@ -178,7 +178,10 @@ export const insertCaseFile = "/fusion/caseFiles/add";
 export const deleteCaseFile = "/fusion/caseFiles/delete";
 export const updateCaseFile = "/fusion/caseFiles/updateTitle";
 export const newFileupload = "/service/manage/common/upload/fileNew";
-
+//勘验笔录信息
+export const caseInquestInfoOld = "/fusion/caseInquest/info";
+export const caseInquestOpt = "/fusion/caseInquest/saveOrUpdate";
+export const caseInquestExport = "/fusion/caseInquest/downDocx";
 // 火调链接地址设置密码
 export const setCasePsw = "/fusion/web/fireProject/updateRandomCode";
 export const getCasePsw = "/fusion/web/fireProject/getRandCode";
@@ -205,6 +208,12 @@ export const uploadAttachFile = "/web/fireProject/uploadFile";
 // 火调上传jpg,png文件
 export const uploadAttachImage = "/web/fireProject/uploadImage";
 /** ------------------------------------------ */
+//标注
+export const getCaseImgTag = "/fusion/caseImgTag/info";
+export const saveCaseImgTag = "/fusion/caseImgTag/saveOrUpdate";
+
+//相片合成
+export const ffmpegMergeImage = `/fusion/caseImg/ffmpegImage`;
 
 // 下载校验
 export const checkHasDownload = "/fusion/sceneDownLog/checkDownLoad";
@@ -217,10 +226,21 @@ export const downloadSceneList = "/fusion/sceneDownLog/list";
 export const offLine = "/web/fireProject/offLine"; //{roomId}
 export const onLine = "/web/fireProject/onLine"; //{roomId}
 export const onLineCheck = "/web/fireProject/onLineCheck";
+// 照片制卷
+export const caseApiList = "/fusion/caseImg/list";
+export const saveApiOrUpdate = "/fusion/caseImg/saveOrUpdate";
+export const uploadImagesAndSave = "/fusion/caseImg/addBatch";
+export const caseApiDel = "/fusion/caseImg/delete";
+export const caseApiUpdateSort = "/fusion/caseImg/updateSort";
+
+export const getSysSetting = `/fusion/systemSetting/info`;
+export const updateSysSetting = `/fusion/systemSetting/save`;
+
 // 案件相关接口
 export const sceneList = "/service/manage/case/sceneList"; //{roomId}
 export const getCaseByNum = "/service/manage/case/getCaseByNum"; //{roomId}
 export const newCaseInfo = "/fusion/caseInquestInfo/info";
+export const casesaveOrUpdate = "/fusion/caseInquestInfo/saveOrUpdate";
 export const getByTree = "/fusion/caseFilesType/getByTree";
 export const updateFileType = "/fusion/caseFiles/updateFileType";
 export const addByMediaLibrary = "/fusion/caseFiles/addByMediaLibrary";

+ 12 - 0
src/router/config.ts

@@ -128,6 +128,18 @@ export const routes: Routes = [
         component: () => import("@/view/case/caseFile.vue"),
         meta: { title: "卷宗管理" },
       },
+      {
+        name: RouteName.drawCasePhotos,
+        path: "/case/photos/:caseId",
+        component: () => import("@/view/case/photos/index.vue"),
+        meta: { title: "照片制卷" },
+      },
+      {
+        name: RouteName.records,
+        path: "/case/records/:caseId",
+        component: () => import("@/view/case/records/index.vue"),
+        meta: { title: "现场勘验笔录" },
+      },
     ],
   },
   {

+ 2 - 0
src/router/routeName.ts

@@ -9,6 +9,7 @@ export const RouteName = {
   camera: "camera",
   caseFile: "caseFile",
   drawCaseFile: "drawCaseFile",
+  drawCasePhotos: "drawCasePhotos",
   user: "user",
   scene: "scene",
   organization: "dept",
@@ -19,6 +20,7 @@ export const RouteName = {
   originalPhoto: "originalPhoto",
   material: "material",
   other: "other",
+  records: "records",
   aiList: "aiList",
   dossier: "dossier",
 } as const;

+ 69 - 4
src/store/case.ts

@@ -16,6 +16,20 @@ import {
   updateFileType,
   saveOrUpdate,
   criminalInfo,
+  getSceneList,
+  newCaseInfo,
+  casesaveOrUpdate,
+  saveApiOrUpdate,
+  uploadImagesAndSave,
+  caseApiDel,
+  caseApiList,
+  caseApiUpdateSort,
+  saveCaseImgTag,
+  getCaseImgTag,
+  ffmpegMergeImage,
+  caseInquestInfoOld,
+  caseInquestOpt,
+  caseInquestExport,
 } from "@/request";
 import { router } from "@/router";
 import { ModelScene, QuoteScene, Scene, SceneType } from "./scene";
@@ -46,14 +60,21 @@ export const AddsaveOrUpdate = async (params) =>
   (await axios.post<string>(saveOrUpdate, params)).data;
 
 export const getcaseInDate = async (params) =>
-  (await axios.post<string>(criminalInfo, params)).data;
+  (await axios.get<string>(criminalInfo, {params})).data;
   // (await axios.get<string>(info, { params:params })).data;
 
-export const getCaseInfo = async (params) =>
-  (await axios.post<Case>(caseInfo, params)).data;
+export const getSceneListData = async (params) =>
+  (await axios.get<string>(getSceneList, { params })).data;
+  // (await axios.get<string>(info, { params:params })).data;
+
+export const getCaseInfo = async (caseId) =>
+  (await axios.get<Case>(caseInfo, { params: { caseId } })).data;
 
 export const getCaseInquestInfo = async (caseId: number) =>
-  (await axios.get(caseInquestInfo, { params: { caseId } })).data;
+  (await axios.get(newCaseInfo, { params: { caseId } })).data;
+
+export const casesaveOrUpDate = async (params) =>
+  (await axios.post<string>(casesaveOrUpdate, params)).data;
 
 
 export const updateCaseInfo = async (caseFile: CaseFile) =>
@@ -100,11 +121,36 @@ export const updateSelectByTreeFileLists = async (): Promise<Scene[]> => {
   }
   return getTreeList((await axios.get(getByTree, { params: { caseId: '' } })).data)
 };
+export const caseImgList = (caseId: number, orderBy: string | null) =>
+  axios.post(caseApiList, { orderBy: orderBy || "", caseId });
+
+export const caseDel = (id: number) => axios.post(caseApiDel, { id });
+
+export const saveOrUpDate = (params: CaseImg) =>
+  axios.post(saveApiOrUpdate, { ...params });
+
+export const caseUpdateSort = (list: [CaseImg]) =>
+  axios.post(caseApiUpdateSort, { paramList: list });
+
+export const saveOrAndSave = (params) =>
+  axios.post(uploadImagesAndSave, { ...params });
 
 export const getByTreeFileLists = async () => {
   return treeList.value
 };
 
+export const getCaseInquestInfoOld = (caseId: number) =>
+  axios.get(caseInquestInfoOld, { params: { caseId } });
+
+export const saveCaseInquestInfo = (caseId: number, data) =>
+  axios.post(caseInquestOpt, { caseId, ...data });
+
+export const exportCaseInquestInfo = (caseId: number) =>
+  axios.get(caseInquestExport, {
+    params: { caseId, ingoreRes: true },
+    responseType: "blob",
+  });
+
 export const getSyncSceneInfo = async (caseId: number) => {
   return (await axios.post<string>(syncInfo, { caseId })).data;
 };
@@ -148,3 +194,22 @@ export const uploadNewFile = (data) => {
   });
 }
 
+export type CaseImg = {
+  id: number;
+  caseId: number;
+  imgInfo: string | null;
+  imgUrl: string | null;
+  status: number | null;
+  sort: number | null;
+};
+
+
+
+export const saveCaseImgTagData = (params: any) =>
+  axios.post(saveCaseImgTag, { ...params });
+
+export const getCaseImgTagData = (caseId: number) =>
+  axios.get(getCaseImgTag, { params: { caseId } });
+
+export const submitMergePhotos = (data) => axios.post(ffmpegMergeImage, { ...data })
+

+ 1 - 0
src/store/caseFile.ts

@@ -13,6 +13,7 @@ import {
 export enum BoardType {
   map = "0",
   scene = "1",
+  aimap = "2",
 }
 
 export interface CaseFileType {

+ 1 - 1
src/store/user.ts

@@ -50,7 +50,7 @@ export const getUsers = async (deptId?: string) =>
 // 当前用户的信息
 
 export const user = ref({
-  token: getLocal(`token`, "") || 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOjEsImRldmljZSI6ImRlZmF1bHQtZGV2aWNlIiwiZWZmIjotMSwicm5TdHIiOiJ2U1FBRVF6UVpIZXhqS1ZMOFo3UmxSSVpUMDhTajdQZyIsImlkIjoxLCJ1c2VySWQiOjgxNywiaXNBZG1pbiI6MSwicGxhdGZvcm1JZCI6MSwidXNlck5hbWUiOiJzdXBlci1hZG1pbiIsIm5pY2tOYW1lIjoi6LaF57qn566h55CG5ZGYIn0.pdpmpIaUf5IKMEMI_3aKot-4ZS7YfOggUIC6jn3W290',
+  token: getLocal(`token`, "") || 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOjEsImRldmljZSI6ImRlZmF1bHQtZGV2aWNlIiwiZWZmIjotMSwicm5TdHIiOiJZS2Vsb2pleTZYQTBQNjU1VXRkeEdzQk95Wlpoc2JqTCIsImlkIjoxLCJ1c2VySWQiOjgxNywiaXNBZG1pbiI6MSwicGxhdGZvcm1JZCI6MSwidXNlck5hbWUiOiJzdXBlci1hZG1pbiIsIm5pY2tOYW1lIjoi6LaF57qn566h55CG5ZGYIn0.kFEPN80qudXaxdxqV4nbueKeRhEAjZSLobpbCsqPqTU',
   info: getLocal("info", {} as UserInfo),
 });
 

+ 5 - 5
src/view/abstract/index.vue

@@ -394,10 +394,10 @@
 import { computed, ref, reactive, onMounted } from "vue";
 import { Search } from "@element-plus/icons-vue";
 import { ElMessage, CascaderOption, CascaderProps } from "element-plus";
-import { Example, setExample, addExample } from "@/app/criminal/store/example";
+import { Example, setExample, getExamplePagging } from "@/app/criminal/store/example";
 import { selectMapImage } from "@/view/case/quisk";
 import { geoData } from "./getGeo";
-import { getCaseInfo, getCaseInquestInfo, AddsaveOrUpdate, getcaseInDate } from "@/store/case";
+import { getSceneListData, getCaseInquestInfo, casesaveOrUpDate, getcaseInDate, getCaseInfo  } from "@/store/case";
 const selectSetting = ref<CascaderProps>({
   value: "name",
   label: "name",
@@ -455,11 +455,11 @@ const criminalType = [
 ];
 onMounted(async () => {
   console.log("caseInfo", caseId.value);
-  const caseInfo = await getcaseInDate(caseId.value);
+  const caseInfo = await getCaseInfo(caseId.value)
   const caseData = await getCaseInquestInfo(caseId.value)
   console.log("caseInfo", caseInfo, caseData);
   if (caseInfo) {
-    ruleForm.value = caseInfo;
+    ruleForm.value = caseData;
     bindExample.value = caseInfo;
 
  }
@@ -503,7 +503,7 @@ const submit = async () => {
     }
     setExample({...bindExample.value, caseId :caseId.value })
   }else{
-    AddsaveOrUpdate({...ruleForm.value, caseId :caseId.value })
+    casesaveOrUpDate({...ruleForm.value, caseId :caseId.value })
   }
 };
 const submitForm = async (formEl) => {

+ 29 - 4
src/view/aiList/index.vue

@@ -3,8 +3,14 @@
     <div>场景列表</div>
     <div>仅 Mesh 场景支持 AI 现勘,请在列表中选择一个场景</div>
     <div class="blList">
-      <div class="listItem p-2 cursor-pointer" v-for="item in 3" :key="itme" :class="{active: active == item}" @click="handleItem(item)">
-        <div class="title1">场景{{ item }}</div>
+      <div
+        class="listItem p-2 cursor-pointer"
+        v-for="(item, index) in list"
+        :key="index"
+        :class="{ active: active == item }"
+        @click="handleItem(item)"
+      >
+        <div class="title1">{{ item.sceneName }}</div>
       </div>
     </div>
   </div>
@@ -15,12 +21,14 @@ import { computed, ref, reactive } from "vue";
 import { addCaseFile } from "../originalPhoto/quisk";
 import { ElMessage, ElMessageBox } from "element-plus";
 import { useUpload } from "@/hook/upload";
+import { getSceneListData } from "@/store/case";
 import { Delete, Edit } from "@element-plus/icons-vue";
 const srcList = [
   "https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg",
   "https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg",
 ];
 const active = ref(null);
+const list = ref([]);
 const ruleFormRef = ref(null);
 const activeName = ref("1");
 const showModal = ref(false);
@@ -30,9 +38,21 @@ const { size, fileList, upload, removeFile, previewFile, file, accept } =
     formats: [".doc", ".docx", , ".pdf"],
   });
 const handleItem = (item) => {
-  console.log('handleItem', item);
+  console.log("handleItem", item);
   active.value = item;
 };
+const getList = async () => {
+  getSceneListData({
+    pageNum: 1,
+    pageSize: 99,
+    sceneType: "1",
+    type: "1",
+  }).then((res) => {
+    list.value = res.list;
+    console.log("getSceneListData", res);
+  });
+};
+getList();
 const submitForm = async (formEl) => {
   if (!formEl) return;
   await formEl.validate((valid, fields) => {
@@ -83,8 +103,13 @@ function handleDel() {
       line-height: 32px;
     }
   }
-  .active{
+  .active {
     background-color: #f0f2f5;
   }
+  .title1 {
+    overflow: hidden; //超出的文本隐藏
+    text-overflow: ellipsis; //溢出用省略号显示
+    white-space: nowrap; //溢出不换行
+  }
 }
 </style>

+ 151 - 0
src/view/case/addPhotoFile.vue

@@ -0,0 +1,151 @@
+<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="false"
+        :limit="1"
+        :disabled="!!file"
+        :before-upload="upload"
+        :file-list="fileList"
+        :http-request="httpsApi"
+        :on-preview="previewFile"
+        :accept="DrawFormats"
+        :before-remove="removeFile"
+      >
+        <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 v-if="!caseFile.id" @click.stop="removeFile()"><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,
+  photoFormatDesc,
+  FileDrawType,
+  OtherFormatDesc,
+  OtherFormats,
+} from "@/constant/caseFile";
+import { uploadFile } from "@/store/system";
+import { maxFileSize } from "@/constant/caseFile";
+import { useUpload } from "@/hook/upload";
+import { saveOrUpDate, CaseImg } from "@/store/case";
+import { ElMessage } from "element-plus";
+import { computed, ref, watch, watchEffect } from "vue";
+import { QuiskExpose } from "@/helper/mount";
+
+const props = defineProps<{
+  caseId: number;
+  data: CaseImg;
+}>();
+
+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, fileList, upload, removeFile, previewFile, file, accept } = useUpload({
+  maxSize: maxFileSize,
+  formats: DrawFormats,
+});
+
+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 httpsApi = async ({file})=> {
+  console.log('httpsApi', file)
+  let fileUrl = await uploadFile(file);
+
+  file.value = {
+    name: file.name,
+    url: fileUrl,
+  }
+  console.log('httpsApi', file, fileUrl)
+}
+
+defineExpose<QuiskExpose>({
+  async submit() {
+    if (!file.value) {
+      ElMessage.error("请上传附件");
+      throw "请上传附件";
+    } else if (!caseFile.value.imgInfo.trim()) {
+      ElMessage.error("附件标题不能为空!");
+      throw "附件标题不能为空!";
+    }
+    console.log('defineExpose', caseFile.value, file.value)
+    let imgUrl = file.value && file.value.value ? file.value.value?.url : file.value?.url
+    await saveOrUpDate({ ...caseFile.value, imgUrl });
+    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>

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

@@ -0,0 +1,207 @@
+<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"
+        :on-exceed="handleExceed"
+        :accept="DrawFormats.join(',')"
+        :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,
+  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 handleExceed = () => {
+  return ElMessage.error(`最大上传数量为10个`);
+};
+const upload = async (file: File) => {
+  const fileType = file.name
+    .substring(file.name.lastIndexOf("."))
+    .toUpperCase();
+
+  if (!DrawFormats.some((type) => type.toUpperCase() === fileType)) {
+    ElMessage.error(`请上传${photoFormatDesc}`);
+    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);
+    const af = fileList.value.filter(
+      (item) => item.raw && item.raw?.url && item.raw?.url.length > 0
+    );
+
+    const isAllupload = af.length === fileList.value.length;
+
+    if (isAllupload) {
+      let imgUrls = fileList.value.map((item) => {
+        return {
+          imgUrl: item.raw && item.raw.url,
+          imgInfo: item.name.replace(/\.[^/.]+$/, ""),
+          caseId: props.caseId,
+        };
+      });
+      await saveOrAndSave({ imgUrls });
+    } else {
+      ElMessage.warning("上传中,请等候!");
+      throw "上传中,请等候!";
+    }
+
+    // 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>

+ 1 - 0
src/view/case/draw/board/useBoard.ts

@@ -68,6 +68,7 @@ const getStore = async (caseId: number, fileId: number, type: BoardType) => {
     }
   } else {
     const fileInfo = await getCaseFileImageInfo(fileId);
+    console.log('getCaseFileImageInfo', fileInfo);
     if (fileInfo) {
       console.log(fileInfo);
       data = {

+ 21 - 7
src/view/case/draw/index.vue

@@ -49,6 +49,7 @@ import { selectFuseImage, selectMapImage } from "@/view/case/quisk";
 import { CaseTagging } from "@/store/caseTagging";
 import saveAs from "@/util/file-serve";
 import { BoardTypeDesc } from "@/constant/caseFile";
+import { addByMediaLiBrary, updateByTreeFileLists, uploadNewFile } from "@/store/case";
 import {
   BoardType,
   SaveCaseFileImageInfo,
@@ -58,6 +59,9 @@ import {
 import { uploadFile } from "@/store/system";
 
 const dom = ref<HTMLCanvasElement>();
+
+const fmtId = ref(0);
+const pmtId = ref(0);
 const props = computed(() => {
   const route = router.currentRoute.value;
   if (route.name !== RouteName.drawCaseFile || !dom.value) {
@@ -120,7 +124,16 @@ const getStore = async () => {
   ) as TitleShapeData;
   return { store, titleShape };
 };
-
+function getList() {
+  let caseId = props.value?.caseId;
+  updateByTreeFileLists(caseId).then(res => {
+    let newlist =  res.find(ele => ele.filesTypeName == '三录材料')?.childrenList || [];
+    let slclList = newlist.find(ele => ele.filesTypeName == '现场图')?.childrenList || [];
+    fmtId.value = slclList.find(ele => ele.filesTypeName == '平面图').filesTypeId
+    pmtId.value = slclList.find(ele => ele.filesTypeName == '方面图').filesTypeId
+  })
+}
+getList();
 // 保存数据
 const saveHandler = async () => {
   const { store, titleShape } = await getStore();
@@ -132,21 +145,22 @@ const saveHandler = async () => {
     imgType: args.type,
     file: new File([blob], `${args.type}_${args.fileId}.jpg`),
     filesTitle: titleShape?.text || `${args.caseId}_${BoardTypeDesc[args.type]}`,
-    content: store,
+    content: store && JSON.stringify(store),
   };
   args.inAdd || (body.filesId = props.value!.fileId);
-
-  const data = await saveCaseFileImageInfo(body);
+  console.log('args1', args, body);
+  const { data } = await uploadNewFile(body);
+  const rse = await addByMediaLiBrary({ ...body, caseId: args.caseId,filesTypeId: args.type == 1 ? pmtId.value:fmtId.value, uploadId: data.id });
+  console.log('args2',rse, { ...body,caseId: args.caseId, type: args.type, id: data.id });
   if (args.inAdd) {
     router.replace({
       name: RouteName.drawCaseFile,
-      params: { caseId: args.caseId, type: args.type, id: data.filesId },
+      params: { caseId: args.caseId, type: args.type, id: rse.filesId },
     });
   }
-
   await nextTick();
   setTimeout(() => {
-    location.reload();
+    // location.reload();
   }, 100);
 };
 

+ 2 - 1
src/view/case/draw/slider.vue

@@ -95,8 +95,9 @@ const props = defineProps<{
 }>();
 
 const fileDesc = {
-  [BoardType.scene]: "户型图",
+  [BoardType.scene]: "平面图",
   [BoardType.map]: "方位图",
+  [BoardType.aimap]: " AI 平面图 ",
 };
 const emit = defineEmits<{
   (e: "update:addShape", val: MetaShapeType | null, data?: any): void;

+ 209 - 0
src/view/case/photos/draggable.vue

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

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

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

+ 564 - 0
src/view/case/photos/index.vue

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

+ 12 - 0
src/view/case/quisk.ts

@@ -1,3 +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";
@@ -14,6 +16,16 @@ export const addCaseFile = quiskMountFactory(AddCaseFile, {
   title: "上传附件",
   width: 500,
 });
+export const addCaseImgFile = quiskMountFactory(addPhotoFile, {
+  title: "上传照片",
+  width: 500,
+});
+
+export const addCaseImgFileAll = quiskMountFactory(addPhotoFileAll, {
+  title: "上传照片",
+  width: 500,
+});
+
 
 export const addCaseScenes = quiskMountFactory(AddScenes, {
   title: "添加场景",

+ 169 - 0
src/view/case/records/feild.md

@@ -0,0 +1,169 @@
+    /**
+     * 案件id
+     */
+    private Integer caseId;
+
+    /**
+     * 现场勘验号
+     */
+    private String inquestNum;
+
+    /**
+     * 单位名称
+     */
+    private String deptName;
+
+    /**
+     * 标题
+     */
+    private String title;
+
+    /**
+     * 发送单位
+     */
+    private String sendDept;
+
+    /**
+     * 笔录人
+     */
+    private String recorder;
+
+    /**
+     * 绘图人
+     */
+    private String painter;
+
+    /**
+     * 照相人
+     */
+    private String photographer;
+
+    /**
+     * 份数
+     */
+    private Integer issuanceCount;
+
+    /**
+     * 制作时间
+     */
+    private Date makeTime;
+
+    /**
+     * 签发意见
+     */
+    private String issuanceOpinion;
+
+    /**
+     * 签名
+     */
+    private String signature;
+
+    /**
+     * 签名时间
+     */
+    private Date signatureTime;
+
+    /**
+     * 报告单位
+     */
+    private String reportDept;
+
+    /**
+     * 时间
+     */
+    private Date inquestTime;
+
+    /**
+     * 勘验开始时间
+     */
+    private Date startTime;
+
+    /**
+     * 勘验结束时间
+     */
+    private Date endTime;
+
+    /**
+     * 勘验地址
+     */
+    private String address;
+
+    /**
+     * 现场保护情况
+     */
+    private String protectionSituation;
+
+    /**
+     * 现场保护人
+     */
+    private String protector;
+
+    /**
+     * 现场保护措施
+     */
+    private String protectionMeasures;
+
+    /**
+     * 现场情况
+     */
+    private String situation;
+
+    /**
+     * 变动原因
+     */
+    private String changeReason;
+
+    /**
+     * 天气情况
+     */
+    private JSONObject weatherInfo;
+
+    /**
+     * 光线
+     */
+    private String light;
+
+    /**
+     * 勘验指挥人
+     */
+    private String inquestCommander;
+
+    /**
+     * 勘验情况
+     */
+    private String inquestSituation;
+
+    /**
+     * 现场勘验制图数量
+     */
+    private Integer imageNum;
+
+    /**
+     * 照相数量
+     */
+    private Integer photographNum;
+
+    /**
+     * 摄影数量
+     */
+    private Integer photographyNum;
+
+    /**
+     * 现场勘验纪录人员数组
+     */
+    private JSONArray recorderInfo;
+
+    /**
+     * 现场勘验人员签名数组
+     */
+    private JSONArray signatureInfo;
+
+    /**
+     * 现场勘验见证人数组
+     */
+    private JSONArray witnessInfo;
+
+    /**
+     * 备注
+     */
+    private String remark;

+ 61 - 0
src/view/case/records/formData.ts

@@ -0,0 +1,61 @@
+export const recorderInfoType =
+    [
+        {
+            type: 0,
+            typeLabel: "笔录人",
+            name: "",
+            unit: "",
+            job: "",
+        },
+        {
+            type: 1,
+            typeLabel: "制图人",
+            name: "",
+            unit: "",
+            job: "",
+        },
+        {
+            type: 2,
+            typeLabel: "照相人",
+            name: "",
+            unit: "",
+            job: "",
+        },
+        {
+            type: 3,
+            typeLabel: "摄像人",
+            name: "",
+            unit: "",
+            job: "",
+        },
+        {
+            type: 4,
+            typeLabel: "法 医",
+            name: "",
+            unit: "",
+            job: "",
+        },
+
+    ]
+
+
+export const ChangeReasonType = [
+    {
+        id: 0,
+        name: "事主进入",
+        flag: "true",
+        value: ""
+    },
+    {
+        id: 1,
+        name: "报案人进入",
+        flag: "true",
+        value: ""
+    },
+    {
+        id: 2,
+        name: "其他",
+        flag: "true",
+        value: ""
+    }
+]

+ 837 - 0
src/view/case/records/index.vue

@@ -0,0 +1,837 @@
+<template>
+  <!-- 勘验笔录{{ props.caseId }} -->
+  <div class="records">
+    <div class="header" style="flex-direction: row-reverse;">
+      <el-button style="margin-left: 10px" type="primary" @click="handleSave">保存</el-button>
+      <el-button :disabled="isDisableExport" @click="handleExport"
+        >导出</el-button
+      >
+    </div>
+    <!-- <h3 class="title">现场勘验笔录</h3>
+    <div class="content">
+      <div class="line" style="flex-direction: row-reverse;">
+        <el-input
+          class="input"
+          v-model="data.inquestNum"
+          placeholder=""
+          style="width: 280px"
+        />
+        <span>勘查号:</span>
+      </div>
+      <div class="line">
+        <span>发送单位:</span>
+        <el-input
+          class="input"
+          v-model="data.sendDept"
+          placeholder=""
+          style="width: 100%"
+        />
+      </div>
+
+      <div class="line">
+        <span>笔 录 人 :</span>
+        <el-input
+          class="input"
+          v-model="data.recorder"
+          placeholder=""
+          style="width: 120px"
+        />
+        <span>绘 图 人 :</span>
+        <el-input
+          class="input"
+          v-model="data.painter"
+          placeholder=""
+          style="width: 120px"
+        />
+        <span>照 相 人 :</span>
+        <el-input
+          class="input"
+          v-model="data.photographer"
+          placeholder=""
+          style="width: 120px"
+        />
+      </div>
+
+      <div class="line">
+        <span>份数: </span>
+        <el-input
+          class="input"
+          type="number"
+          v-model="data.issuanceCount"
+          placeholder=""
+          style="width: 120px"
+        />
+      </div>
+      <div class="line">
+        <span>制作时间: &nbsp; </span>
+        <div>
+          <el-date-picker
+            class="input"
+            v-model="data.makeTime"
+            type="datetime"
+            placeholder="制作时间"
+            style="width: 180px"
+          />
+        </div>
+      </div>
+      <div class="textarea">
+        <span>签发意见 :</span>
+        <el-input
+          type="textarea"
+          :rows="6"
+          v-model="data.issuanceOpinion"
+          placeholder=""
+          style="width: 100%"
+        />
+      </div>
+    </div> -->
+
+    <h3 class="title">现 场 勘 验 笔 录</h3>
+
+    <div class="content">
+        <div class="line" style="flex-direction: row-reverse;">
+        <!-- <span>第</span> -->
+        <el-input
+          class="input"
+          v-model="data.inquestNum"
+          placeholder=""
+          style="width: 280px"
+        />
+        <span>现场勘验号:</span>
+      </div>
+
+      <div class="line">
+        <span>现场勘验单位:</span>
+        <!-- <span>第</span> -->
+        <el-input
+          class="input"
+          v-model="data.deptName"
+          placeholder=""
+          style="width: 100%"
+        />
+      </div>
+
+      <div class="line">
+        <span>指派/报告单位:</span>
+        <!-- <span>第</span> -->
+        <el-input
+          class="input"
+          v-model="data.reportDept"
+          placeholder=""
+          style="width: 100%"
+        />
+      </div>
+
+      <div class="line">
+        <span>勘验时间: &nbsp; </span>
+        <div>
+          <el-date-picker
+            class="input"
+            v-model="data.inquestTime"
+            type="datetime"
+            placeholder="勘验时间"
+            style="width: 180px"
+          />
+        </div>
+      </div>
+      <div class="textarea">
+        <span>勘验事由: </span>
+        <el-input
+          type="textarea"
+          :rows="6"
+          v-model="data.klsy"
+          placeholder=""
+          style="width: 100%"
+        />
+      </div>
+      <div class="line">
+        <span>现场勘验时间: &nbsp; </span>
+        <div>
+          <el-date-picker
+            class="input"
+            v-model="data.times"
+            type="datetimerange"
+            range-separator="至"
+            start-placeholder="现场勘验开始时间"
+            end-placeholder="现场勘验结束时间"
+          />
+        </div>
+      </div>
+
+      <div class="line">
+        <span>现场地点:</span>
+        <el-input
+          class="input"
+          type="text"
+          v-model="data.address"
+          placeholder=""
+          style="width: 100%"
+        />
+      </div>
+
+      <div class="line">
+        <span>现场保护情况:</span>
+        <el-input
+          class="input"
+          type="text"
+          v-model="data.protectionSituation"
+          placeholder=""
+          style="width: 100%"
+        />
+      </div>
+
+      <div class="line">
+        <span>现场保护人: &nbsp;&nbsp; &nbsp;</span>
+        <span>姓名</span>
+        <!-- 单位 XX派出所 职务 一级警长 -->
+        <el-input
+          class="input"
+          type="text"
+          v-model="data.protector.name"
+          placeholder=""
+          style="width: 180px"
+        />
+        <span>单位</span>
+        <el-input
+          class="input"
+          type="text"
+          v-model="data.protector.unit"
+          placeholder=""
+          style="width: 200px"
+        />
+        <span>职务</span>
+        <el-input
+          class="input"
+          type="text"
+          v-model="data.protector.job"
+          placeholder=""
+          style="width: 200px"
+        />
+      </div>
+      <!-- protectionMeasures -->
+      <div class="line">
+        <span>保护措施:&nbsp; </span>
+        <el-checkbox-group v-model="data.protectionMeasures">
+          <el-checkbox :value="0" label="专人看护现场,防止他人进入" />
+          <el-checkbox :value="1" checked label="设立警戒带,划定禁行区域" />
+          <el-checkbox :value="2" label="其他措施" />
+        </el-checkbox-group>
+      </div>
+      <div class="line">
+        <span>现场情况: &nbsp; </span>
+        <el-checkbox-group v-model="data.situation">
+          <el-checkbox :value="0" label="原始现场" />
+          <el-checkbox :value="1" checked label="变动现场" />
+        </el-checkbox-group>
+      </div>
+      <!-- changeReason -->
+      <div class="line">
+        <span>变动原因: &nbsp; </span>
+        <el-checkbox-group v-model="data.changeReason">
+          <el-checkbox
+            v-for="reason in ChangeReasonType"
+            :value="reason.id"
+            :label="reason.name"
+          >
+          </el-checkbox>
+          <!-- <el-checkbox :value="0" label="事主进入" />
+          <el-checkbox :value="1" label="报案人进入" />
+          <el-checkbox :value="2" label="其他" /> -->
+        </el-checkbox-group>
+        <el-input
+          class="input"
+          v-model="data.changeReasonOtherValue"
+          :disabled="!data.changeReason.includes(2)"
+          style="margin-left: 20px; width: 200px"
+        />
+      </div>
+
+      <div class="line">
+        <span>天气: &nbsp; </span>
+        <el-checkbox-group v-model="data.weatherInfo.type">
+          <el-checkbox :value="0" label="阴" />
+          <el-checkbox :value="1" label="晴" />
+          <el-checkbox :value="2" label="雨" />
+          <el-checkbox :value="3" label="雾" />
+        </el-checkbox-group>
+        <span style="margin-left: 20px; font-size: 12px">温度: &nbsp;</span>
+        <el-input
+          class="input"
+          v-model="data.weatherInfo.temperature"
+          style="width: 80px"
+        />
+        <span style="margin-left: 20px; font-size: 12px">湿度: &nbsp;</span>
+        <el-input
+          class="input"
+          v-model="data.weatherInfo.humidity"
+          style="width: 80px"
+        />
+        <span style="margin-left: 20px; font-size: 12px">风向: &nbsp;</span>
+        <el-input
+          class="input"
+          v-model="data.weatherInfo.windDirection"
+          style="width: 80px"
+        />
+      </div>
+
+      <div class="line">
+        <span>现场勘验利用的光线: &nbsp; </span>
+        <el-checkbox-group v-model="data.light">
+          <el-checkbox :value="0" checked label="自然光" />
+          <el-checkbox :value="1" checked label="灯光" />
+          <el-checkbox :value="2" label="特种光" />
+        </el-checkbox-group>
+      </div>
+
+      <div class="line">
+        <span>现场勘验指挥人: &nbsp;&nbsp; </span>
+        <span>姓名</span>
+        <!-- 单位 XX派出所 职务 一级警长 -->
+        <el-input
+          class="input"
+          type="text"
+          v-model="data.inquestCommander.name"
+          placeholder=""
+          style="width: 180px"
+        />
+        <span>单位</span>
+        <el-input
+          class="input"
+          type="text"
+          v-model="data.inquestCommander.unit"
+          placeholder=""
+          style="width: 200px"
+        />
+        <span>职务</span>
+        <el-input
+          class="input"
+          type="text"
+          v-model="data.inquestCommander.job"
+          placeholder=""
+          style="width: 200px"
+        />
+      </div>
+
+      <div class="textarea">
+        <span>现场勘验情况 :</span>
+        <el-input
+          type="textarea"
+          :rows="6"
+          v-model="data.inquestSituation"
+          placeholder=""
+          style="width: 100%"
+        />
+      </div>
+
+      <div class="line">
+        <span>现场勘验制图,&nbsp; </span>
+        <el-input-number
+          class="input"
+          controls-position="right"
+          v-model="data.imageNum"
+          style="width: 130px"
+        />
+        <span>张; &nbsp;&nbsp;</span>
+        <span>照相</span>
+        <el-input-number
+          class="input"
+          controls-position="right"
+          v-model="data.photographNum"
+          style="width: 130px"
+        />
+        <span>张;&nbsp;&nbsp;</span>
+        <span>摄像</span>
+        <el-input-number
+          class="input"
+          controls-position="right"
+          v-model="data.photographyMinNum"
+          style="width: 130px"
+        />
+        <span>分</span>
+        <el-input-number
+          class="input"
+          controls-position="right"
+          v-model="data.photographySecNum"
+          style="width: 130px"
+        />
+        <span>秒</span>
+      </div>
+      <div class="line">
+        现场勘验记录人员:
+        <el-button link @click="recorderInfoDialogShow = true"
+          ><el-icon :size="18"><CirclePlus /> </el-icon>
+        </el-button>
+      </div>
+
+      <!-- 现场勘验记录人员 -->
+      <template v-for="(info, index) in data.recorderInfo">
+        <div class="line info">
+          <span>{{ info.typeLabel }}: &nbsp;&nbsp; </span>
+          <span>姓名</span>
+          <!-- 单位 XX派出所 职务 一级警长 -->
+          <el-input
+            class="input"
+            type="text"
+            v-model="info.name"
+            placeholder=""
+            style="width: 180px"
+          />
+          <span>单位</span>
+          <el-input
+            class="input"
+            type="text"
+            v-model="info.unit"
+            placeholder=""
+            style="width: 200px"
+          />
+          <span>职务</span>
+          <el-input
+            class="input"
+            type="text"
+            v-model="info.job"
+            placeholder=""
+            style="width: 200px"
+          />
+          <el-button
+            link
+            class="del-btn"
+            type="danger"
+            @click="removeRecorderInfo(index)"
+            ><el-icon :size="18"><CircleClose /> </el-icon>
+          </el-button>
+        </div>
+      </template>
+
+      <div class="line">
+        现场勘验人员:
+        <el-button link @click="addSignatureInfo"
+          ><el-icon :size="18"><CirclePlus /> </el-icon>
+        </el-button>
+      </div>
+      <template v-for="(sign, index) of data.signatureInfo">
+        <div class="line info">
+          <span>本人签名: &nbsp;&nbsp; </span>
+          <span>_______________ &nbsp;&nbsp;</span>
+
+          <span>单位</span>
+          <el-input
+            class="input"
+            type="text"
+            v-model="sign.unit"
+            placeholder=""
+            style="width: 200px"
+          />
+          <span>职务</span>
+          <el-input
+            class="input"
+            type="text"
+            v-model="sign.job"
+            placeholder=""
+            style="width: 200px"
+          />
+          <el-button
+            class="del-btn"
+            link
+            type="danger"
+            @click="removeSignatureInfo(index)"
+            ><el-icon :size="18"><CircleClose /> </el-icon>
+          </el-button>
+        </div>
+      </template>
+
+      <div class="line">
+        现场勘验见证人:
+
+        <el-button link @click="addWitnessInfo"
+          ><el-icon :size="18"><CirclePlus /> </el-icon>
+        </el-button>
+      </div>
+      <template v-for="(witness, index) of data.witnessInfo">
+        <div class="line info">
+          <span>本人签名: &nbsp;&nbsp; </span>
+          <span>_______________ &nbsp;&nbsp;</span>
+          <span>性别</span>
+          <el-select
+            class="input"
+            v-model="witness.sex"
+            placeholder="性别"
+            style="width: 140px"
+          >
+            <el-option :value="0" label="男" />
+            <el-option :value="1" label="女" />
+          </el-select>
+          <span>出生日期</span>
+          <el-date-picker
+            class="input"
+            v-model="witness.birthday"
+            type="date"
+            placeholder="出生日期"
+            style="width: 180px"
+          />
+          <!-- <el-input
+            class="input"
+            type="text"
+            v-model="witness.birthday"
+            placeholder=""
+            style="width: 200px"
+          /> -->
+          <span>住址</span>
+          <el-input
+            class="input"
+            type="text"
+            v-model="witness.address"
+            placeholder=""
+            style="width: 260px"
+          />
+          <el-button
+            link
+            class="del-btn"
+            type="danger"
+            @click="removeWitnessInfo(index)"
+            ><el-icon :size="18"><CircleClose /> </el-icon>
+          </el-button>
+        </div>
+      </template>
+
+      <div class="textarea">
+        <span>备注:</span>
+        <el-input
+          type="textarea"
+          :rows="6"
+          v-model="data.remark"
+          placeholder=""
+          style="width: 100%"
+        />
+      </div>
+
+      <div></div>
+    </div>
+    <el-dialog
+      v-model="recorderInfoDialogShow"
+      title="增加现场勘验记录人员"
+      width="500"
+      align-center
+      @close="recorderInfoDialogSelect = null"
+    >
+      <div style="width: 80%; margin: 30px auto">
+        <el-select
+          v-model="recorderInfoDialogSelect"
+          placeholder="请选择现场勘验记录人员"
+          size="large"
+        >
+          <el-option
+            v-for="item in recorderInfoType"
+            :key="item.type"
+            :label="item.typeLabel"
+            :value="item.type"
+          />
+        </el-select>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="addRecorderInfo"> 确定 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+<script setup>
+import { onMounted, ref, watch, h } from "vue";
+import { reactive } from "vue";
+import {
+  getCaseInquestInfo,
+  saveCaseInquestInfo,
+  exportCaseInquestInfo,
+} from "@/store/case";
+import { ElMessage, ElMessageBox } from "element-plus";
+import saveAs from "@/util/file-serve";
+import { CirclePlus, CircleClose } from "@element-plus/icons-vue";
+import { recorderInfoType, ChangeReasonType } from "./formData.ts";
+import { confirm } from "@/helper/message";
+const props = defineProps({ caseId: Number, title: String });
+
+console.log(props);
+const isDisableExport = ref(false);
+const data = reactive({
+  title: "",
+  inquestNum: "", //现场勘验号
+  deptName: "", //单位名称
+  sendDept: "", //发送单位
+  issuanceCount: "", // 份数
+  makeTime: "", //制作时间
+  recorder: "", // 笔录人
+  issuanceOpinion: "", //签发意见
+  reportDept: "", //报告单位
+  times: [], // startTime ,endTime
+  address: "", // 勘验地址
+  inquestTime: "", //勘验时间
+  klsy: "报警人:xxx(性别:xxx 电话:xxx)报警称:xxx发生一起:xxx案,现场需勘察。接报后技术科科长:xxx带领技术员:xxx、法医:xxx立即赶赴现场进行勘察.", //勘验范围
+  protector: {
+    name: "",
+    unit: "",
+    job: "",
+  }, // 现场保护人
+  protectionSituation: "", // 现场保护情况
+  situation: [], //现场情况
+  changeReason: [],
+  changeReasonOtherValue: "",
+  protectionMeasures: [], //保护措施
+  weatherInfo: {
+    type: [],
+    temperature: "",
+    humidity: "",
+    windDirection: "",
+  }, //天气情况
+  light: [], //光线
+  painter: "",
+  photographer: "",
+  inquestCommander: {
+    name: "",
+    unit: "",
+    job: "",
+  }, //现场勘验指挥人
+  inquestSituation: "", //现场勘验情况
+  imageNum: 0, //现场勘验制图数量
+  photographNum: 0, //照相数量
+  photographyMinNum: 0, //摄影数量
+  photographySecNum: 0, //摄影数量
+  recorderInfo: [...recorderInfoType, recorderInfoType[4]],
+  signatureInfo: [], //现场勘验人员
+  witnessInfo: [],
+  remark: "",
+});
+const signatureInfoNum = 4;
+const witnessInfoNum = 2;
+const recorderInfoDialogShow = ref(false);
+const recorderInfoDialogSelect = ref();
+const initInfo = async () => {
+  const res = await getCaseInquestInfo(props.caseId);
+
+  if (!res.data) {
+    isDisableExport.value = true;
+  } else {
+    isDisableExport.value = false;
+  }
+  for (var k in data) {
+    if (res.data && res.data.hasOwnProperty(k)) {
+      // console.log("Key is " + k);
+      if (res.data[k]) {
+        data[k] = res.data[k];
+      }
+    }
+  }
+  setTimeout(() => {
+    initSignatureAndWitInfo();
+  }, 500);
+};
+const initSignatureAndWitInfo = () => {
+  (data.recorderInfo.length === 0 || !data.recorderInfo) &&
+    (data.recorderInfo = [...recorderInfoType, recorderInfoType[4]]);
+
+  (data.signatureInfo.length === 0 || !data.signatureInfo) &&
+    Array.from(new Array(signatureInfoNum)).forEach(() => {
+      data.signatureInfo.push({
+        unit: "",
+        job: "",
+      });
+    });
+
+  (data.witnessInfo.length === 0 || !data.witnessInfo) &&
+    Array.from(new Array(witnessInfoNum)).forEach(() => {
+      data.witnessInfo.push({
+        sex: 0,
+        birthday: "",
+        address: "",
+      });
+    });
+};
+
+onMounted(() => {
+  initInfo();
+});
+
+const addRecorderInfo = () => {
+  recorderInfoDialogShow.value = false;
+  if (recorderInfoDialogSelect.value) {
+    const newCrew = recorderInfoType.find(
+      (i) => i.type === recorderInfoDialogSelect.value
+    );
+    data.recorderInfo.push(newCrew);
+    // debugger;
+  }
+};
+const removeRecorderInfo = async (index) => {
+  if (await confirm("确定要删除此数据?")) {
+    data.recorderInfo.splice(index, 1);
+  }
+};
+const addSignatureInfo = async () => {
+  if (await confirm("确定要添加新的现场勘验人员?")) {
+    data.signatureInfo.push({
+      unit: "",
+      job: "",
+    });
+  }
+};
+const removeSignatureInfo = async (index) => {
+  if (await confirm("确定要删除此数据?")) {
+    data.signatureInfo.splice(index, 1);
+  }
+};
+const addWitnessInfo = async () => {
+  if (await confirm("确定要添加新的现场勘验见证人?")) {
+    data.witnessInfo.push({
+      sex: 0,
+      birthday: "",
+      address: "",
+    });
+  }
+};
+
+const removeWitnessInfo = async (index) => {
+  if (await confirm("确定要删除此数据?")) {
+    data.witnessInfo.splice(index, 1);
+  }
+};
+
+const handleSave = async () => {
+  console.log("data", data);
+
+  for (var k in data) {
+    if (data && data.hasOwnProperty(k)) {
+      // console.log("Key is " + k);
+      if (Array.isArray(data[k])) {
+        data[k] = data[k].sort((a, b) => a - b);
+      }
+    }
+  }
+  console.log("saveCaseInquestInfo", data);
+  const res = await saveCaseInquestInfo(props.caseId, data);
+  if (res.code === 0) {
+    ElMessage.success("保存成功!");
+    initInfo();
+  }
+};
+const handleExport = async () => {
+  await saveCaseInquestInfo(props.caseId, data);
+  const res = await exportCaseInquestInfo(props.caseId);
+  console.log("res", res);
+  saveAs(res, `${props.title}_勘验笔录.docx`);
+};
+</script>
+
+<style lang="scss">
+.records {
+  max-width: 1280px;
+  margin: 0 auto;
+  padding: 20px;
+  position: relative;
+  background: #fff;
+  .header {
+    display: flex;
+    justify-content: flex-endTime;
+    position: sticky;
+    top: 10px;
+    z-index: 1000;
+    background-color: white;
+
+  }
+
+  .input {
+    height: 32px;
+    line-height: 32px;
+    margin: 0 8px;
+  }
+
+  .textarea {
+    margin-right: 8px;
+    margin-bottom: 20px;
+
+    span {
+      padding: 10px 0;
+      display: inline-block;
+    }
+
+    // margin: 0 8px;
+  }
+
+  .line {
+    display: inline-flex;
+    width: 100%;
+    flex-direction: row;
+    align-items: center;
+    margin-bottom: 25px;
+    line-height: 38px;
+
+    span {
+      white-space: nowrap;
+    }
+  }
+}
+
+.title {
+  text-align: center;
+  margin-bottom: 30px;
+}
+
+.sub-tit {
+  display: inline-block;
+  padding-bottom: 20px;
+}
+
+.info {
+  display: block;
+
+  .inner {
+    display: flex;
+    flex-direction: row;
+    width: 100%;
+
+    .input {
+      flex: 1;
+    }
+
+    .sec {
+      flex: 1;
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+    }
+  }
+}
+
+.witnessInfo {
+  background: #f5f5f5;
+  padding: 15px;
+  margin-top: 20px;
+  margin-right: 8px;
+}
+
+.gap {
+  margin: 15px 0;
+}
+
+.btn-container {
+  padding: 20px 0;
+
+  .btn {
+    color: #26559b;
+    width: 100%;
+
+    &:hover {
+      background: #f5f5f5;
+      border-color: #dcdfe6;
+    }
+  }
+}
+.info {
+  .del-btn {
+    display: none;
+  }
+  &:hover {
+    .del-btn {
+      display: block;
+    }
+  }
+}
+</style>

+ 444 - 0
src/view/case/records/manifest.vue

@@ -0,0 +1,444 @@
+<template>
+  <div class="records">
+    <div class="header">
+      <el-button type="primary" @click="handleSave">保存</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%"
+        />
+      </div>
+
+      <div class="line">
+        <span>提取日期:</span>
+        <div>
+          <el-input
+            class="input"
+            :maxlength="4"
+            type="text"
+            v-model="data.time.year"
+            placeholder=""
+            style="width: 80px"
+          />
+          <span>年</span>
+          <el-input
+            class="input"
+            :maxlength="2"
+            type="text"
+            v-model="data.time.month"
+            placeholder=""
+            style="width: 80px"
+          />
+          <span>月</span>
+          <el-input
+            class="input"
+            :maxlength="2"
+            type="text"
+            v-model="data.time.day"
+            placeholder=""
+            style="width: 80px"
+          />
+          <span>日</span>
+        </div>
+      </div>
+
+      <div class="detail">
+        <span class="sub-tit">提取清单:</span>
+        <template v-for="(item, index) in data.detail">
+          <div class="con">
+            <span class="sub-tit">编号 {{ index + 1 }}: </span>
+            <div class="info">
+              <div class="inner">
+                <div class="sec">
+                  <span>名称: </span>
+                  <el-input class="input" v-model="item.name" placeholder="" />
+                </div>
+
+                <div class="sec">
+                  <span>规格: </span>
+                  <el-input class="input" v-model="item.spec" placeholder="" />
+                </div>
+
+                <div class="sec">
+                  <span>数量: </span>
+                  <el-input class="input" v-model="item.num" placeholder="" />
+                </div>
+              </div>
+              <div class="inner">
+                <div class="sec">
+                  <span>提取部位: </span>
+                  <el-input class="input" v-model="item.part" placeholder="" />
+                </div>
+              </div>
+              <div class="inner">
+                <div class="sec">
+                  <span>特征: </span>
+                  <el-input class="input" v-model="item.desc" placeholder="" />
+                </div>
+              </div>
+            </div>
+          </div>
+        </template>
+      </div>
+      <div class="btn-container">
+        <el-button class="btn" @click="addItem">+新增</el-button>
+      </div>
+      <div class="gap"></div>
+
+      <div class="extractUser">
+        <span class="sub-tit">提取人:</span>
+        <template v-for="extractUser in data.extractUser">
+          <div class="line">
+            <span>姓名:</span>
+            <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%"
+            />
+          </div>
+        </template>
+      </div>
+      <div class="btn-container">
+        <el-button class="btn" @click="addextractUser">+新增</el-button>
+      </div>
+      <!-- 证人 -->
+      <div>
+        <span>证人或当事人:</span>
+        <template v-for="wit in data.witnessInfo">
+          <div class="witnessInfo">
+            <!-- <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"
+              />
+            </div>
+            <div class="line">
+              <span>单位或住址:</span>
+              <el-input
+                class="input"
+                v-model="wit.address"
+                placeholder=""
+                style="width: 100%"
+              />
+            </div>
+          </div>
+        </template>
+      </div>
+      <div class="btn-container">
+        <el-button class="btn" @click="addwitnessInfo">+新增</el-button>
+      </div>
+      <div></div>
+    </div>
+  </div>
+</template>
+<script setup>
+import { onMounted, ref, watch } from "vue";
+import { reactive } from "vue";
+import {
+  getCaseDetailInfo,
+  saveCaseDetailInfo,
+  exportCaseDetailInfo,
+} from "@/store/case";
+import saveAs from "@/util/file-serve";
+import { ElMessage } from "element-plus";
+
+const props = defineProps({ caseId: Number, title: String });
+
+const isDisableExport = ref(false);
+
+console.log(props);
+
+const data = reactive({
+  address: "",
+  time: {
+    year: "",
+    month: "",
+    day: "",
+  },
+
+  location: "",
+  detail: [
+    {
+      // id: "1",
+      name: "",
+      spec: "",
+      num: "",
+      part: "",
+      desc: "",
+    },
+    {
+      // id: "2",
+      name: "",
+      spec: "",
+      num: "",
+      part: "",
+      desc: "",
+    },
+  ],
+  extractUser: [
+    {
+      name: "",
+      workplace: "",
+      id: "",
+    },
+
+    {
+      name: "",
+      address: "",
+      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,
+  }
+);
+
+onMounted(() => {});
+
+const addwitnessInfo = () => {
+  data.witnessInfo.push({
+    name: "",
+    address: "",
+    phone: "",
+    id: "",
+  });
+};
+const addItem = () => {
+  data.detail.push({
+    // id: "1",
+    name: "",
+    spec: "",
+    num: "",
+    part: "",
+    desc: "",
+  });
+};
+const addextractUser = () => {
+  data.extractUser.push({
+    name: "",
+    address: "",
+    id: "",
+  });
+};
+const handleSave = async () => {
+  console.log("data", data);
+  const res = await saveCaseDetailInfo(props.caseId, data);
+  if (res.code === 0) {
+    ElMessage.success("保存成功!");
+    initInfo();
+  }
+};
+const handleExport = async () => {
+  await saveCaseDetailInfo(props.caseId, data);
+  const res = await exportCaseDetailInfo(props.caseId);
+  console.log("res", res);
+  saveAs(res, `${props.title}_提取清单.docx`);
+};
+const initInfo = async () => {
+  const res = await getCaseDetailInfo(props.caseId);
+
+  console.log("res", res);
+  for (var k in data) {
+    if (!res.data) {
+      isDisableExport.value = true;
+    } else {
+      isDisableExport.value = false;
+    }
+    if (res.data && res.data.hasOwnProperty(k)) {
+      // console.log("Key is " + k)
+      data[k] = res.data[k];
+    }
+  }
+};
+onMounted(() => {
+  initInfo();
+});
+</script>
+
+<style lang="scss">
+.records {
+  max-width: 1280px;
+  margin: 0 auto;
+  padding: 20px 0;
+
+  .header {
+    display: flex;
+    justify-content: flex-end;
+    margin-bottom: 50px;
+  }
+
+  .input {
+    height: 32px;
+    line-height: 32px;
+    margin: 0 8px;
+  }
+
+  .textarea {
+    margin-right: 8px;
+    margin-bottom: 20px;
+
+    span {
+      padding: 10px 0;
+      display: inline-block;
+    }
+
+    // margin: 0 8px;
+  }
+
+  .line {
+    display: inline-flex;
+    width: 100%;
+    flex-direction: row;
+    align-items: center;
+    margin-bottom: 25px;
+    line-height: 38px;
+
+    span {
+      white-space: nowrap;
+    }
+  }
+}
+
+.title {
+  text-align: center;
+}
+
+.sub-tit {
+  display: inline-block;
+  padding-bottom: 20px;
+}
+
+.info {
+  display: block;
+
+  .inner {
+    display: flex;
+    flex-direction: row;
+    width: 100%;
+
+    .input {
+      flex: 1;
+    }
+
+    .sec {
+      flex: 1;
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+    }
+  }
+}
+.witnessInfo {
+  background: #f5f5f5;
+  padding: 15px;
+  margin-top: 20px;
+  // margin-right: 8px;
+}
+
+.gap {
+  margin: 15px 0;
+}
+
+.btn-container {
+  padding: 20px 0;
+
+  .btn {
+    color: #26559b;
+    width: 100%;
+
+    &:hover {
+      background: #f5f5f5;
+      border-color: #dcdfe6;
+    }
+  }
+}
+
+.detail {
+  .con {
+    padding: 20px;
+    background-color: #f5f5f5;
+  }
+
+  .info {
+    .inner {
+      margin-bottom: 20px;
+    }
+  }
+}
+
+.extractUser {
+  margin-right: 0px;
+
+  .line {
+    background-color: #f5f5f5;
+    padding: 15px;
+    width: calc(100% - 30px);
+    display: inline-flex;
+    margin-bottom: 15px;
+  }
+}
+</style>

+ 5 - 1
src/view/layout/index.vue

@@ -27,7 +27,7 @@
               ></iframe>
             </div>
           </div>
-          <div class="main p-4">
+          <div class="main p-4" :class="{ fullmain: hiddenSlide }">
             <router-view v-slot="{ Component }">
               <component :is="Component" />
             </router-view>
@@ -105,6 +105,10 @@ const hiddenSlide = computed(
         flex: 1;
         background: #fff;
       }
+      .fullmain{
+        background-color: var(--bgColor);
+        overflow-y: scroll;
+      }
     }
   }
 }

+ 34 - 6
src/view/material/index.vue

@@ -6,14 +6,14 @@
           <el-button
             style="width: calc(50% - 7px)"
             type="primary"
-            @click="submitForm(ruleFormRef)"
+            @click="gotoDraw(BoardType.scene, -1)"
           >
             绘制平面图
           </el-button>
           <el-button
             style="width: calc(50% - 6px)"
             type="primary"
-            @click="submitForm(ruleFormRef)"
+            @click="gotoDraw(BoardType.aimap, -1)"
           >
             AI 平面图
           </el-button>
@@ -21,7 +21,7 @@
             class="w-full mt-4"
             style="margin-left: 0"
             type="primary"
-            @click="submitForm(ruleFormRef)"
+            @click="gotoDraw(BoardType.map, -1)"
           >
             绘制方位图
           </el-button>
@@ -48,7 +48,7 @@
               </div>
             </div>
           </div>
-          <el-button class="w-full" @click="handleAdd()">照片制卷</el-button>
+          <el-button class="w-full" @click="handledrawCasePhotos">照片制卷</el-button>
         </div>
       </el-tab-pane>
       <el-tab-pane label="勘验笔录" name="3">
@@ -72,7 +72,7 @@
               <div class="">支持 pdf、word 格式图片上传</div>
             </div>
           </el-upload>
-          <el-button class="w-full mt-2" @click="handleAdd">在线填写</el-button>
+          <el-button class="w-full mt-2" @click="handleRecords">在线填写</el-button>
         </div>
         <div class="blList">
           <div class="bllistItem flex justify-between items-center mt-2" v-for="item,index in list.klbj" :key="index">
@@ -102,7 +102,8 @@ import { computed, ref, reactive } from "vue";
 import { addCaseFile } from "../originalPhoto/quisk";
 import { ElMessage, ElMessageBox } from "element-plus";
 import { useUpload } from "@/hook/upload";
-import { router } from "@/router";
+import { RouteName, router } from "@/router";
+import { BoardType } from "@/store/caseFile";
 import { updateByTreeFileLists, getByTreeFileLists } from "@/store/case";
 import { Delete, Edit } from "@element-plus/icons-vue";
 import viewImg from "@/components/viewImg/index.vue"
@@ -122,6 +123,8 @@ const srcList = [
 const settype = ref(false);
 const ruleFormRef = ref(null);
 const klblId = ref(0);
+const fmtId = ref(0);
+const pmtId = ref(0);
 const activeName = ref('1');
 const showModal = ref(false);
 const { size, fileList, upload, removeFile, previewFile, file, accept } =
@@ -132,6 +135,12 @@ const { size, fileList, upload, removeFile, previewFile, file, accept } =
 const handleClick = (tab) => {
   console.log(tab);
 };
+const gotoDraw = (type: BoardType, id: number) => {
+  router.push({
+    name: RouteName.drawCaseFile,
+    params: { caseId: caseId.value!, type, id },
+  });
+};
 function getList() {
   updateByTreeFileLists(caseId.value).then(res => {
     let newlist =  res.find(ele => ele.filesTypeName == '三录材料')?.childrenList || [];
@@ -139,6 +148,8 @@ function getList() {
     list.value.xczp = newlist.find(ele => ele.filesTypeName == '现场照片')?.childrenList || [];
     list.value.klbj = newlist.find(ele => ele.filesTypeName == '勘验笔录')?.caseFilesList || [];
     klblId.value = newlist.find(ele => ele.filesTypeName == '勘验笔录').filesTypeId
+    fmtId.value = list.value.xct.find(ele => ele.filesTypeName == '平面图').filesTypeId
+    pmtId.value = list.value.xct.find(ele => ele.filesTypeName == '方面图').filesTypeId
     console.log('list.value', list.value)
   })
 }
@@ -161,6 +172,10 @@ function handleActive(params) {
 }
 function handleItem(type, item) {
   console.log("handleItem", type, item);
+  if('edit' == type) {
+    gotoDraw(item.filesTypeId == pmtId.value? BoardType.scene : BoardType.map, item.filesId)
+  }
+  getList()
 }
 function handleSuccess(item) {
   let uploadId = item?.data.id;
@@ -168,6 +183,19 @@ function handleSuccess(item) {
     getList();
   });
 }
+function handleRecords() {
+  router.push({
+    name: RouteName.records,
+    params: { caseId: caseId.value! },
+  });
+}
+async function handledrawCasePhotos() {
+  router.push({
+    name: RouteName.drawCasePhotos,
+    params: { caseId: caseId.value! },
+  });
+  console.log("handleAdd");
+}
 async function handleAdd(filesTypeName) {
   await addCaseFile({ caseId: caseId.value, fileType: 1, filesTypeName });
   getList()

+ 5 - 1
src/view/vrmodel/index.vue

@@ -14,7 +14,7 @@
             :key="index"
           >
             <span>{{ item.name || item.modelTitle }}</span>
-            <div class="cursor-pointer" quaternary type="primary">编辑</div>
+            <div @click="handlegotoEdit(item)" class="cursor-pointer" quaternary type="primary">编辑</div>
           </div>
         </div>
       </div>
@@ -44,6 +44,10 @@ async function geiList() {
   list.value = await getCaseSceneList(caseId.value);
   console.log("res", list.value);
 }
+function handlegotoEdit(record) {
+  let url = `/mega/index.html?m=${record.num}`
+  window.open(url);
+}
 function handleActive(params) {
   console.log("handleActive", params);
 }

+ 22 - 0
yarn.lock

@@ -340,6 +340,11 @@
   resolved "https://mirrors.cloud.tencent.com/npm/@types/qs/-/qs-6.9.17.tgz#fc560f60946d0aeff2f914eb41679659d3310e1a"
   integrity sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==
 
+"@types/sortablejs@^1.15.8":
+  version "1.15.8"
+  resolved "https://mirrors.cloud.tencent.com/npm/@types/sortablejs/-/sortablejs-1.15.8.tgz#11ed555076046e00869a5ef85d1e7651e7a66ef6"
+  integrity sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==
+
 "@types/web-bluetooth@^0.0.16":
   version "0.0.16"
   resolved "https://mirrors.cloud.tencent.com/npm/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz#1d12873a8e49567371f2a75fe3e7f7edca6662d8"
@@ -1167,6 +1172,16 @@ side-channel@^1.0.6:
   resolved "https://mirrors.cloud.tencent.com/npm/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
   integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
 
+swiper@^11.1.15:
+  version "11.1.15"
+  resolved "https://mirrors.cloud.tencent.com/npm/swiper/-/swiper-11.1.15.tgz#e2258c8d38282e2f115ca463d6e8c5b84cdcf1ca"
+  integrity sha512-IzWeU34WwC7gbhjKsjkImTuCRf+lRbO6cnxMGs88iVNKDwV+xQpBCJxZ4bNH6gSrIbbyVJ1kuGzo3JTtz//CBw==
+
+three@^0.171.0:
+  version "0.171.0"
+  resolved "https://mirrors.cloud.tencent.com/npm/three/-/three-0.171.0.tgz#3c0dd3f8fa14e78a7f8db6e416b98f264f1185c0"
+  integrity sha512-Y/lAXPaKZPcEdkKjh0JOAHVv8OOnv/NDJqm0wjfCzyQmfKxV7zvkwsnBgPBKTzJHToSOhRGQAGbPJObT59B/PQ==
+
 to-regex-range@^5.0.1:
   version "5.0.1"
   resolved "https://mirrors.cloud.tencent.com/npm/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
@@ -1252,6 +1267,13 @@ vue-demi@*:
   resolved "https://mirrors.cloud.tencent.com/npm/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04"
   integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==
 
+vue-draggable-plus@^0.6.0:
+  version "0.6.0"
+  resolved "https://mirrors.cloud.tencent.com/npm/vue-draggable-plus/-/vue-draggable-plus-0.6.0.tgz#e4da6917e94ca73402f34113073f44771691fa5e"
+  integrity sha512-G5TSfHrt9tX9EjdG49InoFJbt2NYk0h3kgjgKxkFWr3ulIUays0oFObr5KZ8qzD4+QnhtALiRwIqY6qul4egqw==
+  dependencies:
+    "@types/sortablejs" "^1.15.8"
+
 vue-router@^4.2.4:
   version "4.5.0"
   resolved "https://mirrors.cloud.tencent.com/npm/vue-router/-/vue-router-4.5.0.tgz#58fc5fe374e10b6018f910328f756c3dae081f14"