123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- <template>
- <div class="three-preview" :style="getAreaStyle()">
- <SerialNumberPosition v-if="isEnable(data.property.sn_display_mode)" :property="data.property" />
- <div ref="three" class="three">
- <SvgIcon
- :class-name="iconName()"
- :icon-class="icon()"
- :size="isFullScreen ? '32px' : '16px'"
- @click="handleToggleFullScreen"
- />
- </div>
- </div>
- </template>
- <script>
- import PreviewMixin from '../common/PreviewMixin';
- 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';
- import { get3DModelData } from '@/views/book/courseware/data/3dModel';
- import { toggleFullScreen } from '@/utils/common';
- import { GetFileStoreInfo } from '@/api/app';
- export default {
- name: 'ThreeModelPreview',
- mixins: [PreviewMixin],
- data() {
- return {
- data: get3DModelData(),
- scene: null,
- camera: null,
- renderer: null,
- controls: null,
- animationId: null,
- isFullScreen: false, // 是否全屏
- loaded: false, // 是否加载完成
- };
- },
- watch: {
- 'data.model_list': {
- handler(val) {
- if (val && val.length > 0) {
- this.loadModel();
- }
- },
- immediate: true,
- deep: true,
- },
- },
- mounted() {
- this.initThree();
- window.addEventListener('fullscreenchange', this.onWindowResize);
- },
- beforeDestroy() {
- this.cleanup();
- window.removeEventListener('fullscreenchange', this.onWindowResize);
- },
- methods: {
- iconName() {
- if (this.data.model_list.length === 0) return 'fullscreen-btn';
- return this.loaded ? (this.isFullScreen ? 'exit-fullscreen' : 'fullscreen-btn') : 'loading';
- },
- icon() {
- if (this.data.model_list.length === 0) return '';
- return this.loaded ? (this.isFullScreen ? 'exit-fullscreen' : 'fullscreen') : 'loading';
- },
- 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();
- // 监听窗口大小变化
- window.addEventListener('resize', this.onWindowResize);
- },
- async loadModel() {
- if (!this.data.model_list || this.data.model_list.length === 0) {
- return;
- }
- const fileStore = await GetFileStoreInfo({ file_id: this.data.model_list[0].file_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) => {
- console.log('加载进度:', `${(progress.loaded / progress.total) * 100}%`);
- if (progress.loaded >= progress.total) {
- this.loaded = true;
- }
- },
- (error) => {
- console.error('GLTF模型加载失败:', error);
- },
- );
- },
- loadFBX(loader, url) {
- loader.load(
- url,
- (fbx) => {
- this.addModelToScene(fbx);
- },
- (progress) => {
- console.log('加载进度:', `${(progress.loaded / progress.total) * 100}%`);
- if (progress.loaded >= progress.total) {
- this.loaded = true;
- }
- },
- (error) => {
- console.error('FBX模型加载失败:', error);
- },
- );
- },
- loadOBJ(loader, url) {
- loader.load(
- url,
- (obj) => {
- this.addModelToScene(obj);
- },
- (progress) => {
- console.log('加载进度:', `${(progress.loaded / progress.total) * 100}%`);
- 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);
- },
- 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);
- }
- },
- onWindowResize() {
- const container = this.$refs.three;
- this.isFullScreen = document.fullscreenElement !== null;
- if (!container || !this.camera || !this.renderer) return;
- let height = 0;
- let width = 0;
- // 检查是否在全屏状态
- if (document.fullscreenElement) {
- // 全屏状态下使用整个屏幕尺寸
- width = window.innerWidth;
- height = window.innerHeight;
- } else {
- // 非全屏状态下使用容器尺寸
- width = container.clientWidth;
- height = container.clientHeight;
- }
- // 更新相机宽高比
- this.camera.aspect = width / height;
- this.camera.updateProjectionMatrix();
- // 更新渲染器尺寸
- this.renderer.setSize(width, height);
- },
- cleanup() {
- // 停止动画循环
- if (this.animationId) {
- cancelAnimationFrame(this.animationId);
- }
- // 移除事件监听器
- window.removeEventListener('resize', this.onWindowResize);
- // 清理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();
- }
- },
- handleToggleFullScreen() {
- if (!this.$refs.three) return;
- if (!this.loaded) {
- this.$message.warning('请等待模型加载完成后,再切换全屏');
- return;
- }
- toggleFullScreen(this.$refs.three);
- // 延迟一帧来确保DOM已经更新
- this.$nextTick(() => {
- setTimeout(() => {
- this.onWindowResize();
- }, 100);
- });
- },
- },
- };
- </script>
- <style lang="scss" scoped>
- @use '@/styles/mixin.scss' as *;
- .three-preview {
- @include preview-base;
- .three {
- position: relative;
- min-height: 104px;
- overflow: hidden;
- .loading {
- position: absolute;
- top: 10px;
- right: 10px;
- color: #999;
- @include spin;
- }
- .fullscreen-btn {
- position: absolute;
- top: 10px;
- right: 10px;
- z-index: 999;
- cursor: pointer;
- }
- .exit-fullscreen {
- position: fixed;
- top: 24px;
- right: 24px;
- z-index: 999;
- cursor: pointer;
- }
- }
- }
- </style>
|