|  | @@ -109,9 +109,7 @@
 | 
	
		
			
				|  |  |                    <p class="name">
 | 
	
		
			
				|  |  |                      {{ item.name }}
 | 
	
		
			
				|  |  |                      <SvgIcon
 | 
	
		
			
				|  |  | -                      v-show="
 | 
	
		
			
				|  |  | -                        item.file_id && (type_index === 5 || type_index === 3 || type_index === 2 || type_index === 1)
 | 
	
		
			
				|  |  | -                      "
 | 
	
		
			
				|  |  | +                      v-show="item.file_id && type_index !== 0"
 | 
	
		
			
				|  |  |                        icon-class="uploadPreview"
 | 
	
		
			
				|  |  |                        size="16"
 | 
	
		
			
				|  |  |                        @click="viewDialog(item)"
 | 
	
	
		
			
				|  | @@ -213,6 +211,20 @@
 | 
	
		
			
				|  |  |      >
 | 
	
		
			
				|  |  |        <iframe v-if="visible" :src="newpath" width="100%" :height="iframeHeight" frameborder="0"></iframe>
 | 
	
		
			
				|  |  |      </el-dialog>
 | 
	
		
			
				|  |  | +    <!-- 3D模型预览 -->
 | 
	
		
			
				|  |  | +    <el-dialog
 | 
	
		
			
				|  |  | +      v-if="visible3D"
 | 
	
		
			
				|  |  | +      :visible.sync="visible3D"
 | 
	
		
			
				|  |  | +      :show-close="true"
 | 
	
		
			
				|  |  | +      :close-on-click-modal="true"
 | 
	
		
			
				|  |  | +      :modal-append-to-body="true"
 | 
	
		
			
				|  |  | +      :append-to-body="true"
 | 
	
		
			
				|  |  | +      :lock-scroll="true"
 | 
	
		
			
				|  |  | +      width="80%"
 | 
	
		
			
				|  |  | +      top="0"
 | 
	
		
			
				|  |  | +    >
 | 
	
		
			
				|  |  | +      <div style="height: 85vh" ref="three" class="three"></div>
 | 
	
		
			
				|  |  | +    </el-dialog>
 | 
	
		
			
				|  |  |    </div>
 | 
	
		
			
				|  |  |  </template>
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -235,6 +247,13 @@ import AudioLine from './components/AudioLine.vue';
 | 
	
		
			
				|  |  |  const Base64 = require('js-base64').Base64;
 | 
	
		
			
				|  |  |  import { getConfig } from '@/utils/auth';
 | 
	
		
			
				|  |  |  import { H5StartupFile, GetFileURLMap } from '@/api/app';
 | 
	
		
			
				|  |  | +import { GetFileStoreInfo } from '@/api/app';
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +import * as THREE from 'three';
 | 
	
		
			
				|  |  | +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
 | 
	
		
			
				|  |  | +import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js';
 | 
	
		
			
				|  |  | +import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
 | 
	
		
			
				|  |  | +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  export default {
 | 
	
		
			
				|  |  |    name: 'ProjectResourceManager',
 | 
	
	
		
			
				|  | @@ -322,6 +341,13 @@ export default {
 | 
	
		
			
				|  |  |        visible: false,
 | 
	
		
			
				|  |  |        newpath: '',
 | 
	
		
			
				|  |  |        iframeHeight: `${window.innerHeight - 100}px`,
 | 
	
		
			
				|  |  | +      visible3D: false,
 | 
	
		
			
				|  |  | +      scene: null,
 | 
	
		
			
				|  |  | +      camera: null,
 | 
	
		
			
				|  |  | +      renderer: null,
 | 
	
		
			
				|  |  | +      controls: null,
 | 
	
		
			
				|  |  | +      animationId: null,
 | 
	
		
			
				|  |  | +      loaded: false, // 是否加载完成
 | 
	
		
			
				|  |  |      };
 | 
	
		
			
				|  |  |    },
 | 
	
		
			
				|  |  |    created() {
 | 
	
	
		
			
				|  | @@ -627,6 +653,10 @@ export default {
 | 
	
		
			
				|  |  |            this.newpath = res.file_url;
 | 
	
		
			
				|  |  |            this.visible = true;
 | 
	
		
			
				|  |  |          });
 | 
	
		
			
				|  |  | +      } else if (this.type_index === 4) {
 | 
	
		
			
				|  |  | +        this.visible3D = true;
 | 
	
		
			
				|  |  | +        this.loadModel(file.file_id);
 | 
	
		
			
				|  |  | +        this.initThree();
 | 
	
		
			
				|  |  |        } else {
 | 
	
		
			
				|  |  |          GetFileURLMap({ file_id_list: [file.file_id] }).then(({ url_map }) => {
 | 
	
		
			
				|  |  |            this.newpath = `${this.file_preview_url}onlinePreview?url=${Base64.encode(url_map[file.file_id])}`;
 | 
	
	
		
			
				|  | @@ -634,6 +664,193 @@ export default {
 | 
	
		
			
				|  |  |          });
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  |      },
 | 
	
		
			
				|  |  | +    initThree() {
 | 
	
		
			
				|  |  | +      const container = this.$refs.three;
 | 
	
		
			
				|  |  | +      if (!container) return;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 创建场景
 | 
	
		
			
				|  |  | +      this.scene = new THREE.Scene();
 | 
	
		
			
				|  |  | +      this.scene.background = new THREE.Color(0xf0f0f0);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 创建相机
 | 
	
		
			
				|  |  | +      const width = container.clientWidth;
 | 
	
		
			
				|  |  | +      const height = container.clientHeight;
 | 
	
		
			
				|  |  | +      this.camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
 | 
	
		
			
				|  |  | +      this.camera.position.set(5, 5, 5);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 创建渲染器
 | 
	
		
			
				|  |  | +      this.renderer = new THREE.WebGLRenderer({ antialias: true });
 | 
	
		
			
				|  |  | +      this.renderer.setSize(width, height);
 | 
	
		
			
				|  |  | +      this.renderer.shadowMap.enabled = true;
 | 
	
		
			
				|  |  | +      this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
 | 
	
		
			
				|  |  | +      container.appendChild(this.renderer.domElement);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 添加光源
 | 
	
		
			
				|  |  | +      const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
 | 
	
		
			
				|  |  | +      this.scene.add(ambientLight);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
 | 
	
		
			
				|  |  | +      directionalLight.position.set(10, 10, 5);
 | 
	
		
			
				|  |  | +      directionalLight.castShadow = true;
 | 
	
		
			
				|  |  | +      this.scene.add(directionalLight);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 添加控制器
 | 
	
		
			
				|  |  | +      this.controls = new OrbitControls(this.camera, this.renderer.domElement);
 | 
	
		
			
				|  |  | +      this.controls.enableDamping = true;
 | 
	
		
			
				|  |  | +      this.controls.dampingFactor = 0.25;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 开始渲染循环
 | 
	
		
			
				|  |  | +      this.animate();
 | 
	
		
			
				|  |  | +    },
 | 
	
		
			
				|  |  | +    animate() {
 | 
	
		
			
				|  |  | +      this.animationId = requestAnimationFrame(this.animate);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 更新控制器
 | 
	
		
			
				|  |  | +      if (this.controls) {
 | 
	
		
			
				|  |  | +        this.controls.update();
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 更新动画混合器
 | 
	
		
			
				|  |  | +      if (this.mixer) {
 | 
	
		
			
				|  |  | +        this.mixer.update(0.016); // 假设60fps
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 渲染场景
 | 
	
		
			
				|  |  | +      if (this.renderer && this.scene && this.camera) {
 | 
	
		
			
				|  |  | +        this.renderer.render(this.scene, this.camera);
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    },
 | 
	
		
			
				|  |  | +    cleanup() {
 | 
	
		
			
				|  |  | +      // 停止动画循环
 | 
	
		
			
				|  |  | +      if (this.animationId) {
 | 
	
		
			
				|  |  | +        cancelAnimationFrame(this.animationId);
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 清理Three.js资源
 | 
	
		
			
				|  |  | +      if (this.renderer) {
 | 
	
		
			
				|  |  | +        this.renderer.dispose();
 | 
	
		
			
				|  |  | +        const container = this.$refs.three;
 | 
	
		
			
				|  |  | +        if (container && container.contains(this.renderer.domElement)) {
 | 
	
		
			
				|  |  | +          container.removeChild(this.renderer.domElement);
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      if (this.scene) {
 | 
	
		
			
				|  |  | +        this.scene.clear();
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    },
 | 
	
		
			
				|  |  | +    async loadModel(id) {
 | 
	
		
			
				|  |  | +      const fileStore = await GetFileStoreInfo({ file_id: id });
 | 
	
		
			
				|  |  | +      if (!fileStore || fileStore.error) {
 | 
	
		
			
				|  |  | +        console.error('模型文件加载失败:', fileStore);
 | 
	
		
			
				|  |  | +        return;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +      const modelUrl = fileStore.file_url;
 | 
	
		
			
				|  |  | +      // 根据文件扩展名选择合适的加载器
 | 
	
		
			
				|  |  | +      const extension = modelUrl.split('.').pop().toLowerCase();
 | 
	
		
			
				|  |  | +      let loader = null;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      switch (extension) {
 | 
	
		
			
				|  |  | +        case 'gltf':
 | 
	
		
			
				|  |  | +        case 'glb':
 | 
	
		
			
				|  |  | +          loader = new GLTFLoader();
 | 
	
		
			
				|  |  | +          this.loadGLTF(loader, modelUrl);
 | 
	
		
			
				|  |  | +          break;
 | 
	
		
			
				|  |  | +        case 'fbx':
 | 
	
		
			
				|  |  | +          loader = new FBXLoader();
 | 
	
		
			
				|  |  | +          this.loadFBX(loader, modelUrl);
 | 
	
		
			
				|  |  | +          break;
 | 
	
		
			
				|  |  | +        case 'obj':
 | 
	
		
			
				|  |  | +          loader = new OBJLoader();
 | 
	
		
			
				|  |  | +          this.loadOBJ(loader, modelUrl);
 | 
	
		
			
				|  |  | +          break;
 | 
	
		
			
				|  |  | +        default:
 | 
	
		
			
				|  |  | +          console.error('不支持的模型格式:', extension);
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    },
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    loadGLTF(loader, url) {
 | 
	
		
			
				|  |  | +      loader.load(
 | 
	
		
			
				|  |  | +        url,
 | 
	
		
			
				|  |  | +        (gltf) => {
 | 
	
		
			
				|  |  | +          const model = gltf.scene;
 | 
	
		
			
				|  |  | +          this.addModelToScene(model);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +          // 如果模型有动画,播放第一个动画
 | 
	
		
			
				|  |  | +          if (gltf.animations && gltf.animations.length > 0) {
 | 
	
		
			
				|  |  | +            this.mixer = new THREE.AnimationMixer(model);
 | 
	
		
			
				|  |  | +            const action = this.mixer.clipAction(gltf.animations[0]);
 | 
	
		
			
				|  |  | +            action.play();
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  | +        },
 | 
	
		
			
				|  |  | +        (progress) => {
 | 
	
		
			
				|  |  | +          if (progress.loaded >= progress.total) {
 | 
	
		
			
				|  |  | +            this.loaded = true;
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  | +        },
 | 
	
		
			
				|  |  | +        (error) => {
 | 
	
		
			
				|  |  | +          console.error('GLTF模型加载失败:', error);
 | 
	
		
			
				|  |  | +        },
 | 
	
		
			
				|  |  | +      );
 | 
	
		
			
				|  |  | +    },
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    loadFBX(loader, url) {
 | 
	
		
			
				|  |  | +      loader.load(
 | 
	
		
			
				|  |  | +        url,
 | 
	
		
			
				|  |  | +        (fbx) => {
 | 
	
		
			
				|  |  | +          this.addModelToScene(fbx);
 | 
	
		
			
				|  |  | +        },
 | 
	
		
			
				|  |  | +        (progress) => {
 | 
	
		
			
				|  |  | +          if (progress.loaded >= progress.total) {
 | 
	
		
			
				|  |  | +            this.loaded = true;
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  | +        },
 | 
	
		
			
				|  |  | +        (error) => {
 | 
	
		
			
				|  |  | +          console.error('FBX模型加载失败:', error);
 | 
	
		
			
				|  |  | +        },
 | 
	
		
			
				|  |  | +      );
 | 
	
		
			
				|  |  | +    },
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    loadOBJ(loader, url) {
 | 
	
		
			
				|  |  | +      loader.load(
 | 
	
		
			
				|  |  | +        url,
 | 
	
		
			
				|  |  | +        (obj) => {
 | 
	
		
			
				|  |  | +          this.addModelToScene(obj);
 | 
	
		
			
				|  |  | +        },
 | 
	
		
			
				|  |  | +        (progress) => {
 | 
	
		
			
				|  |  | +          if (progress.loaded >= progress.total) {
 | 
	
		
			
				|  |  | +            this.loaded = true;
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  | +        },
 | 
	
		
			
				|  |  | +        (error) => {
 | 
	
		
			
				|  |  | +          console.error('OBJ模型加载失败:', error);
 | 
	
		
			
				|  |  | +        },
 | 
	
		
			
				|  |  | +      );
 | 
	
		
			
				|  |  | +    },
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    addModelToScene(model) {
 | 
	
		
			
				|  |  | +      // 计算模型的包围盒,用于自动调整相机位置
 | 
	
		
			
				|  |  | +      const box = new THREE.Box3().setFromObject(model);
 | 
	
		
			
				|  |  | +      const center = box.getCenter(new THREE.Vector3());
 | 
	
		
			
				|  |  | +      const size = box.getSize(new THREE.Vector3());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 将模型移到原点
 | 
	
		
			
				|  |  | +      model.position.sub(center);
 | 
	
		
			
				|  |  | +      this.scene.add(model);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 自动调整相机位置
 | 
	
		
			
				|  |  | +      const maxDim = Math.max(size.x, size.y, size.z);
 | 
	
		
			
				|  |  | +      const fov = this.camera.fov * (Math.PI / 180);
 | 
	
		
			
				|  |  | +      let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));
 | 
	
		
			
				|  |  | +      cameraZ *= 1.5; // 稍微远一点以便观察
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      this.camera.position.set(cameraZ, cameraZ, cameraZ);
 | 
	
		
			
				|  |  | +      this.camera.lookAt(0, 0, 0);
 | 
	
		
			
				|  |  | +      this.controls.target.set(0, 0, 0);
 | 
	
		
			
				|  |  | +    },
 | 
	
		
			
				|  |  | +  },
 | 
	
		
			
				|  |  | +  beforeDestroy() {
 | 
	
		
			
				|  |  | +    this.cleanup();
 | 
	
		
			
				|  |  |    },
 | 
	
		
			
				|  |  |  };
 | 
	
		
			
				|  |  |  </script>
 |