|
@@ -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>
|