From 8c96a1d16d1d95101938670b287e39130fd5e086 Mon Sep 17 00:00:00 2001
From: 闫增涛 <1829501689@qq.com>
Date: 星期四, 12 六月 2025 17:12:51 +0800
Subject: [PATCH] 模型预览

---
 src/components/previewModelDialog.vue         |  134 +++++++++++++++++++
 package-lock.json                             |   68 +++++++++
 package.json                                  |    3 
 src/components/showModel.vue                  |  109 +++++++++++++++
 src/views/model/children/landerModel.vue      |   35 +++--
 src/views/simulation/testSimulation/index.vue |   16 +
 6 files changed, 348 insertions(+), 17 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 902d3c7..e394111 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,9 @@
         "moment": "^2.30.1",
         "pinia": "^3.0.1",
         "spark-md5": "^3.0.2",
+        "three": "^0.177.0",
+        "three-fbx-loader": "^1.0.3",
+        "three-orbitcontrols": "^2.110.3",
         "vue": "^3.4.21",
         "vue-router": "^4.3.0"
       },
@@ -1577,6 +1580,11 @@
       "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
       "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw=="
     },
+    "node_modules/pako": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz",
+      "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
+    },
     "node_modules/parse-node-version": {
       "version": "1.0.1",
       "resolved": "https://registry.npmmirror.com/parse-node-version/-/parse-node-version-1.0.1.tgz",
@@ -1805,6 +1813,34 @@
       },
       "funding": {
         "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/three": {
+      "version": "0.177.0",
+      "resolved": "https://registry.npmmirror.com/three/-/three-0.177.0.tgz",
+      "integrity": "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="
+    },
+    "node_modules/three-fbx-loader": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/three-fbx-loader/-/three-fbx-loader-1.0.3.tgz",
+      "integrity": "sha512-CQZ0IkwqX+ZbIca225mfKCq1feH8XJOf2zqMcC2bugvq20S7alHGsG9QEELbyN9zSL0EvkBXADVDO2lupF0E+A==",
+      "dependencies": {
+        "pako": "^1.0.6",
+        "three": "^0.89.0"
+      }
+    },
+    "node_modules/three-fbx-loader/node_modules/three": {
+      "version": "0.89.0",
+      "resolved": "https://registry.npmmirror.com/three/-/three-0.89.0.tgz",
+      "integrity": "sha512-Evv3JolLQtag/lCWu1o3cgz2E5UYCSZHbh9lfBjeLz7nlK8DMmASID680Odl++ZPYk63rrZjPAMq88IrMP3GCA=="
+    },
+    "node_modules/three-orbitcontrols": {
+      "version": "2.110.3",
+      "resolved": "https://registry.npmmirror.com/three-orbitcontrols/-/three-orbitcontrols-2.110.3.tgz",
+      "integrity": "sha512-BNNbksJwbN3/MmT0X/gjz5ZCchm7bjk26SUdtJYRxfEYjDfkb/0PeUTHE/KuyJ5vb/owK3mojyy3vcqDx99sRA==",
+      "deprecated": "three-js exposes real modules now via three/examples/jsm/...\nfor example to import Orbit, do\nimport { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'\n",
+      "peerDependencies": {
+        "three": ">= 0.110.0"
       }
     },
     "node_modules/tslib": {
@@ -2877,6 +2913,11 @@
       "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
       "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw=="
     },
+    "pako": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz",
+      "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
+    },
     "parse-node-version": {
       "version": "1.0.1",
       "resolved": "https://registry.npmmirror.com/parse-node-version/-/parse-node-version-1.0.1.tgz",
@@ -3040,6 +3081,33 @@
         }
       }
     },
+    "three": {
+      "version": "0.177.0",
+      "resolved": "https://registry.npmmirror.com/three/-/three-0.177.0.tgz",
+      "integrity": "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="
+    },
+    "three-fbx-loader": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/three-fbx-loader/-/three-fbx-loader-1.0.3.tgz",
+      "integrity": "sha512-CQZ0IkwqX+ZbIca225mfKCq1feH8XJOf2zqMcC2bugvq20S7alHGsG9QEELbyN9zSL0EvkBXADVDO2lupF0E+A==",
+      "requires": {
+        "pako": "^1.0.6",
+        "three": "^0.89.0"
+      },
+      "dependencies": {
+        "three": {
+          "version": "0.89.0",
+          "resolved": "https://registry.npmmirror.com/three/-/three-0.89.0.tgz",
+          "integrity": "sha512-Evv3JolLQtag/lCWu1o3cgz2E5UYCSZHbh9lfBjeLz7nlK8DMmASID680Odl++ZPYk63rrZjPAMq88IrMP3GCA=="
+        }
+      }
+    },
+    "three-orbitcontrols": {
+      "version": "2.110.3",
+      "resolved": "https://registry.npmmirror.com/three-orbitcontrols/-/three-orbitcontrols-2.110.3.tgz",
+      "integrity": "sha512-BNNbksJwbN3/MmT0X/gjz5ZCchm7bjk26SUdtJYRxfEYjDfkb/0PeUTHE/KuyJ5vb/owK3mojyy3vcqDx99sRA==",
+      "requires": {}
+    },
     "tslib": {
       "version": "2.8.1",
       "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
diff --git a/package.json b/package.json
index 2cd5334..8c5de90 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,9 @@
     "moment": "^2.30.1",
     "pinia": "^3.0.1",
     "spark-md5": "^3.0.2",
+    "three": "^0.177.0",
+    "three-fbx-loader": "^1.0.3",
+    "three-orbitcontrols": "^2.110.3",
     "vue": "^3.4.21",
     "vue-router": "^4.3.0"
   },
diff --git a/src/components/previewModelDialog.vue b/src/components/previewModelDialog.vue
new file mode 100644
index 0000000..34792c1
--- /dev/null
+++ b/src/components/previewModelDialog.vue
@@ -0,0 +1,134 @@
+<template>
+  <el-dialog
+    v-model="dialogVisible"
+    @open="openDialog"
+    destroy-on-close
+    class="prviewModel"
+  >
+    <div  v-loading="loading">
+      <div
+        id="container"
+        class="viewer-container"
+        v-if="selectData.md5"
+      ></div>
+      <el-empty v-else description="鏆傛棤鏁版嵁" />
+    </div>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import * as THREE from "three";
+import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader";
+import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
+import {  ref, defineExpose, inject } from "vue";
+const config: any = inject("config");
+const dialogVisible = ref<boolean>(false);
+const selectData = ref<any>(null);
+const loading = ref<boolean>(false);
+const openDialog = () => {
+  loading.value = true;
+  if (selectData.value.md5) {
+    initThree();
+  } else {
+    loading.value = false
+  }
+};
+const initThree = () => {
+  const container = document.getElementById("container");
+  const scene = new THREE.Scene();
+  const camera = new THREE.PerspectiveCamera(
+    75,
+    container.clientWidth / container.clientHeight,
+    0.1,
+    10000 // 澧炲ぇ杩滃钩闈㈣窛绂�
+  );
+  const renderer = new THREE.WebGLRenderer({ antialias: true });
+  renderer.setSize(container.clientWidth, container.clientHeight);
+  container.appendChild(renderer.domElement);
+
+  // 娣诲姞鍏夋簮
+  const light = new THREE.DirectionalLight(0xffffff, 2);
+  light.position.set(5, 10, 7.5);
+  scene.add(light);
+
+  // 鍔犺浇妯″瀷
+  const loader = new FBXLoader();
+  const token = localStorage.getItem(config.tokenKey);
+  loader.load(
+    config.requestCtx +
+      "/file/FileDownload/Download?md5=" +
+      selectData.value.md5 +
+      "&token=" +
+      token,
+    (object) => {
+      scene.add(object);
+
+      // 鍒涘缓鎺т欢
+      const controls = new OrbitControls(camera, renderer.domElement);
+      controls.enableDamping = true;
+      controls.dampingFactor = 0.25;
+      controls.screenSpacePanning = false;
+
+      // 1. 璁$畻妯″瀷灏哄鍜屼腑蹇冪偣
+      const box = new THREE.Box3().setFromObject(object);
+      const center = box.getCenter(new THREE.Vector3());
+      const size = box.getSize(new THREE.Vector3());
+
+      // 2. 璁$畻鍚堥�傜殑鐩告満璺濈
+      const maxDim = Math.max(size.x, size.y, size.z);
+      const fov = camera.fov * (Math.PI / 180);
+      let cameraDistance = Math.abs(maxDim / (2 * Math.tan(fov / 2)));
+
+      // 3. 璁剧疆鐩告満浣嶇疆鍜屾柟鍚�
+      camera.position.copy(center);
+      camera.position.z += cameraDistance * 1.5; // 澧炲姞50%鐨勯澶栫┖闂�
+      camera.lookAt(center);
+
+      // 4. 鏇存柊鎺т欢鐩爣鐐�
+      controls.target.copy(center);
+      controls.update();
+
+      // 5. 璋冩暣鐩告満鍓骞抽潰
+      const minDistance = cameraDistance * 0.1;
+      const maxDistance = cameraDistance * 10;
+      camera.near = minDistance > 0.1 ? minDistance : 0.1;
+      camera.far = maxDistance < 10000 ? maxDistance : 10000;
+      camera.updateProjectionMatrix();
+
+      function animate() {
+        requestAnimationFrame(animate);
+        controls.update();
+        renderer.render(scene, camera);
+      }
+      animate();
+      loading.value = false
+    },
+    undefined,
+    (error) => {
+      console.error("鍔犺浇妯″瀷澶辫触:", error);
+      loading.value = false
+    },
+    
+  );
+};
+const handleDialogVisible = (value: boolean, data: any) => {
+  dialogVisible.value = value;
+  selectData.value = data;
+  console.log("select", selectData.value);
+};
+defineExpose({ handleDialogVisible });
+</script>
+
+<style lang="less">
+.prviewModel {
+  width: 60vw;
+  min-height: 580px;
+  .viewer-container {
+    width: 100%;
+    height: 60vh;
+    .el-empty {
+      margin-top: 100px !important;
+    }
+  }
+}
+</style>
diff --git a/src/components/showModel.vue b/src/components/showModel.vue
new file mode 100644
index 0000000..0357ee8
--- /dev/null
+++ b/src/components/showModel.vue
@@ -0,0 +1,109 @@
+<template>
+  <div class="showModel" v-loading="loading">
+    <div :id="'container' + props.md5 + props.index" class="viewer-container" v-if="props.md5"></div>
+    <el-empty v-else description="鏆傛棤鏁版嵁" image-size="80" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, defineProps,inject } from "vue";
+import * as THREE from "three";
+import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader";
+import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
+const props = defineProps<{md5:string,index:number}>();
+const config: any = inject("config");
+const loading = ref<boolean>(false);
+onMounted(() => {
+  loading.value = true;
+  if(props.md5) {
+     initThree();
+  } else {
+    loading.value = false;
+  }
+});
+const initThree = () => {
+  const container = document.getElementById("container" + props.md5 + props.index);
+  const scene = new THREE.Scene();
+  const camera = new THREE.PerspectiveCamera(
+    75,
+    container.clientWidth / container.clientHeight,
+    0.1,
+    10000 // 澧炲ぇ杩滃钩闈㈣窛绂�
+  );
+  const renderer = new THREE.WebGLRenderer({ antialias: true });
+  renderer.setSize(container.clientWidth, container.clientHeight);
+  container.appendChild(renderer.domElement);
+
+  // 娣诲姞鍏夋簮
+  const light = new THREE.DirectionalLight(0xffffff, 2);
+  light.position.set(5, 10, 7.5);
+  scene.add(light);
+
+  // 鍔犺浇妯″瀷
+  const loader = new FBXLoader();
+  const token = localStorage.getItem(config.tokenKey);
+  loader.load(
+    config.requestCtx +
+      "/file/FileDownload/Download?md5=" +
+      props.md5 +
+      "&token=" +
+      token,
+    (object) => {
+      scene.add(object);
+
+      // 鍒涘缓鎺т欢
+      const controls = new OrbitControls(camera, renderer.domElement);
+      controls.enableDamping = true;
+      controls.dampingFactor = 0.25;
+      controls.screenSpacePanning = false;
+
+      // 1. 璁$畻妯″瀷灏哄鍜屼腑蹇冪偣
+      const box = new THREE.Box3().setFromObject(object);
+      const center = box.getCenter(new THREE.Vector3());
+      const size = box.getSize(new THREE.Vector3());
+
+      // 2. 璁$畻鍚堥�傜殑鐩告満璺濈
+      const maxDim = Math.max(size.x, size.y, size.z);
+      const fov = camera.fov * (Math.PI / 180);
+      let cameraDistance = Math.abs(maxDim / (2 * Math.tan(fov / 2)));
+
+      // 3. 璁剧疆鐩告満浣嶇疆鍜屾柟鍚�
+      camera.position.copy(center);
+      camera.position.z += cameraDistance * 1.5; // 澧炲姞50%鐨勯澶栫┖闂�
+      camera.lookAt(center);
+
+      // 4. 鏇存柊鎺т欢鐩爣鐐�
+      controls.target.copy(center);
+      controls.update();
+
+      // 5. 璋冩暣鐩告満鍓骞抽潰
+      const minDistance = cameraDistance * 0.1;
+      const maxDistance = cameraDistance * 10;
+      camera.near = minDistance > 0.1 ? minDistance : 0.1;
+      camera.far = maxDistance < 10000 ? maxDistance : 10000;
+      camera.updateProjectionMatrix();
+
+      function animate() {
+        requestAnimationFrame(animate);
+        controls.update();
+        renderer.render(scene, camera);
+      }
+      animate();
+      loading.value = false;
+    },
+    undefined,
+    (error) => {
+      console.error("鍔犺浇妯″瀷澶辫触:", error);
+      loading.value = false;
+    }
+  );
+};
+</script>
+
+<style lang="less" scoped>
+.showModel,
+.viewer-container {
+  width: 100%;
+  height: 100%;
+}
+</style>
diff --git a/src/views/model/children/landerModel.vue b/src/views/model/children/landerModel.vue
index 5fc6331..136b11c 100644
--- a/src/views/model/children/landerModel.vue
+++ b/src/views/model/children/landerModel.vue
@@ -92,16 +92,8 @@
         />
       </div>
     </div>
-    <el-dialog v-model="dialogTableVisible" title="棰勮">
-      <div>
-        <iframe
-          style="width: 100%; height: 500px"
-          src="../static/modelView/index.html?md5=62d4eadc420b7403fce2be993baa095d&name=椋炶妫�&domain=https://www.jlstp.cn&target=iframe"
-          frameborder="0"
-        ></iframe>
-      </div>
-    </el-dialog>
-
+    <!-- 棰勮 -->
+  <previewModule ref="previewModelRef"></previewModule>
     <el-dialog v-model="dialogFormVisible" title="鏂板缓妯″瀷">
       <el-form :rules="rules" :model="form" ref="formRef">
         <el-form-item
@@ -122,7 +114,11 @@
           />
         </el-form-item>
         <el-form-item label="妯″瀷鏂囦欢" :label-width="formLabelWidth">
-          <el-upload :before-upload="beforeUpload" :limit="1" v-if="!form.ModelFile">
+          <el-upload
+            :before-upload="beforeUpload"
+            :limit="1"
+            v-if="!form.ModelFile"
+          >
             <template #trigger>
               <el-button type="primary">涓婁紶妯″瀷</el-button>
             </template>
@@ -156,6 +152,7 @@
 import { Plus } from "@element-plus/icons-vue";
 import { curStoreInfo } from "@/store/index";
 import SparkMD5 from "spark-md5";
+import previewModule from "@/components/previewModelDialog.vue";
 import {
   ComponentSize,
   ElMessage,
@@ -192,7 +189,7 @@
   JointData: "",
   IsSimulation: false,
   ModelRemarks: "",
-  ModelFile: null
+  ModelFile: null,
 });
 
 const progress = ref(0);
@@ -348,8 +345,18 @@
   getTableData();
 };
 //棰勮鎿嶄綔
-const handlePreview = (row) => {
-  dialogTableVisible.value = true;
+const previewModelRef = ref<any>();
+const showModelData = ref<any>(null);
+const handlePreview = (row: any) => {
+  let md5: any = null;
+  try {
+    const fileData = row.fieldList.find((item: any) => item.FileList.length);
+    md5 = fileData.FileList[0].File.Md5;
+  } catch (error) {}
+  previewModelRef.value.handleDialogVisible(true, {
+    name: row.name,
+    md5,
+  });
 };
 //涓婄Щ鎿嶄綔
 const handleMoveUp = (row) => {};
diff --git a/src/views/simulation/testSimulation/index.vue b/src/views/simulation/testSimulation/index.vue
index dae9cec..2303f82 100644
--- a/src/views/simulation/testSimulation/index.vue
+++ b/src/views/simulation/testSimulation/index.vue
@@ -46,11 +46,12 @@
             >
               <div class="model-body-box">
                 <div class="model-img">
-                  <iframe
+                  <!-- <iframe
                     style="width: 100%; height: 100%"
                     src="./static/modelView/index.html?md5=62d4eadc420b7403fce2be993baa095d&name=椋炶妫�&domain=https://www.jlstp.cn&target=iframe"
                     frameborder="0"
-                  ></iframe>
+                  ></iframe> -->
+                  <showModel :md5="item.md5" :index="index"></showModel>
                 </div>
                 <div class="model-info">
                   <h1 class="model-title" :title="item.name">
@@ -94,6 +95,7 @@
 <script setup lang="ts">
 import { ref, onMounted, watch, inject } from "vue";
 import { useRouter, useRoute } from "vue-router";
+import showModel from "../../../components/showModel.vue";
 const router = useRouter();
 const route = useRoute();
 import { curStoreInfo } from "@/store/index";
@@ -205,12 +207,21 @@
     },
     {
       ModelName: [],
+      ModelFile: [],
       JointData: [],
       IsSimulation: [],
       ModelRemarks: [],
       ChildrenCount: [],
     }
   );
+  for (let index = 0; index < treeData.datas.length; index++) {
+    const item = treeData.datas[index];
+    item.md5 = null;
+    try {
+      const fileData = item.fieldList.find((citem: any) => citem.FileList.length);
+      item.md5 = fileData.FileList[0].File.Md5;
+    } catch (error) {}
+  }
   console.log(treeData, "getModelList");
   modelDataList.value = treeData.datas;
   listLoading.value = false;
@@ -262,7 +273,6 @@
 const gotoReport = (item) => {
   router.push({
     name: "simulation-testReport",
-
   });
 };
 //鎵撳紑浠跨湡

--
Gitblit v1.9.1