基于 Vue 3 + Three.js 的室内 3D 系统开发实战

📅 2026/7/10 2:44:40
基于 Vue 3 + Three.js 的室内 3D 系统开发实战
基于 Vue 3 Three.js 的室内 3D 系统开发实战前言大家好今天给大家分享一个我最近开发的项目——基于 Vue 3 和 Three.js 的室内 3D 导航系统。这个项目主要用于展示文化场馆的室内 3D 地图支持多层楼展示、POI搜索、全景查看等功能。项目简介Indoor3D 是一个现代化的室内 3D 可视化项目采用最新的 Vue 3 和 Three.js 技术栈提供流畅的 3D 地图体验。项目地址GitHub/Gitee: https://gitee.com/unload2018/indoor3-d技术栈Vue 3.5.13 (Composition API Options API)Vite 6.0.5Three.js 0.172.0Element Plus 2.9.3Vue Router 4.5.0tweenjs/tween.js 25.0.0功能特性✨ 核心功能️多层楼 3D 地图展示- 支持多楼层的室内地图可视化POI 搜索与筛选- 按楼层和关键词搜索兴趣点楼层切换- 美观的标签页式楼层选择器点击定位- 点击地图对象自动定位并展示详情️全景视图- 支持查看 360 度全景图片响应式设计- 适配不同屏幕尺寸➕缩放控制- 支持地图缩放操作视图重置- 一键重置视图到初始状态 UI 特色现代化的界面设计毛玻璃效果流畅的过渡动画左侧菜单 底部工具栏 右侧抽屉布局项目结构indoor3-d/ ├── public/ # 公共资源目录 │ ├── glb/ # 3D 模型文件 (GLB 格式) │ ├── icon/ # 图标资源 │ ├── img/ # 图片资源 │ ├── pubicon/ # 公共设施图标 │ ├── data1.json # 地图数据 1 │ └── data2.json # 地图数据 2 ├── src/ # 源代码目录 │ ├── components/ # 组件目录 │ │ ├── environment/ # 3D 环境相关 │ │ │ ├── floor.js # 楼层管理 │ │ │ ├── indoor-manager.js # 室内 3D 主控制器 │ │ │ ├── island.js # 岛屿/区域管理 │ │ │ ├── panorama-viewer.js # 全景视图 │ │ │ └── world-scene.js # 3D 世界管理 │ │ ├── network/ # 网络/数据加载 │ │ │ ├── json-loader.js # JSON 数据加载器 │ │ │ └── model-loader.js # 模型加载器 │ │ ├── utils/ # 工具组件 │ │ │ └── path-to-polygon.js # 路径转多边形 │ │ ├── ThreeJsViewer.vue # Three.js 主组件 │ │ └── Panorama.vue # 全景组件 │ ├── views/ # 页面视图 │ │ ├── Home.vue # 首页 │ │ └── PanoramaView.vue # 全景页面 │ ├── router/ # 路由配置 │ └── main.js # 应用入口 └── README.md # 项目说明核心实现1. Three.js 场景初始化// src/components/environment/indoor-manager.jsimport*asTHREEfromthree;import{OrbitControls}fromthree/examples/jsm/controls/OrbitControls;exportdefaultclassIndoorManager{constructor(container,onLoadData){this.containercontainer;this.scenenull;this.cameranull;this.renderernull;this.controlsnull;this.worldnull;this.onLoadDataonLoadData;}init(width,height){// 创建场景this.scenenewTHREE.Scene();this.scene.backgroundnewTHREE.Color(0xf0f0f0);// 创建相机this.cameranewTHREE.PerspectiveCamera(60,width/height,0.1,1000);this.camera.position.set(0,10,20);// 创建渲染器this.renderernewTHREE.WebGLRenderer({antialias:true});this.renderer.setSize(width,height);this.renderer.shadowMap.enabledtrue;this.container.appendChild(this.renderer.domElement);// 轨道控制器this.controlsnewOrbitControls(this.camera,this.renderer.domElement);this.controls.enableDampingtrue;this.controls.dampingFactor0.05;// 加载数据this.loadData();// 启动动画循环this.animate();}animate(){requestAnimationFrame(()this.animate());this.controls.update();this.renderer.render(this.scene,this.camera);}}2. 楼层切换实现// 显示单个楼层showOneFloor(floorIndex,callback){constfloorListthis.world.getFloors();floorList.forEach((floor,index){floor.visible(indexfloorIndex);});if(callback)callback();}// 显示所有楼层showAll(){constfloorListthis.world.getFloors();floorList.forEach(floor{floor.visibletrue;});}3. POI 点击事件处理// 点击定位到指定 POIfocusOnPOI(position,radius){consttargetPosnewTHREE.Vector3(position[0]-this.worldWidth/2,this.worldHeight/2-position[1],0);constfovthis.camera.fov*(Math.PI/180);constfovh2*Math.atan(Math.tan(fov/2)*this.camera.aspect);consttargetRadiusradius||this.showWidth/4;constdxMath.abs(targetRadius/2/Math.tan(fovh/2));constnewPositionnewTHREE.Vector3(targetPos.x,targetPos.y-dx*Math.sin(Math.PI/4),dx*Math.cos(Math.PI/4));// 使用 tween 动画newTWEEN.Tween(this.camera.position).to(newPosition,500).easing(TWEEN.Easing.Quadratic.Out).start();this.controls.target.copy(targetPos);}4. Vue 组件设计template div classthree-js-viewer div refcontainer classcontainer/div !-- 左侧面板 -- div classleft-panel div classmenu-btn clickopenDrawer svg.../svg /div div classfloor-selector div v-for(floor, index) in floorList :keyindex :class[floor-item, { active: currentFloor index }] clickswitchFloor(index) {{ floor.name }} /div /div /div !-- 底部工具栏 -- div classtoolbar div classtoolbar-btn clickzoomIn svg.../svg /div div classdivider/div div classtoolbar-btn clickzoomOut svg.../svg /div div classdivider/div div classtoolbar-btn clickresetView svg.../svg /div /div !-- 右侧抽屉 -- el-drawer v-modeldrawerVisible directionrtl !-- POI 列表 -- /el-drawer /div /template script export default { name: ThreeJsViewer, data() { return { currentFloor: -1, floorList: [], drawerVisible: false }; }, mounted() { this.initThree(); }, methods: { initThree() { this.indoor new IndoorManager(this.$refs.container, this.onLoadData); this.indoor.init(window.innerWidth, window.innerHeight); }, onLoadData(data) { this.floorList data.data; this.updatePOIList(); }, switchFloor(index) { if (index -1) { this.indoor.showAll(); } else { this.indoor.showOneFloor(index); } this.currentFloor index; }, openDrawer() { this.drawerVisible true; } } }; /script style scoped .three-js-viewer { position: relative; width: 100vw; height: 100vh; } .container { width: 100%; height: 100%; } .left-panel { position: fixed; left: 20px; top: 20px; display: flex; flex-direction: column; gap: 16px; z-index: 100; } .menu-btn { width: 52px; height: 52px; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(12px); border-radius: 14px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.3s; } .menu-btn:hover { transform: translateY(-3px); box-shadow: 0 12px 48px rgba(0, 135, 189, 0.2); } .floor-selector { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(12px); border-radius: 16px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); padding: 16px 12px; min-width: 80px; } .floor-item { padding: 12px 8px; border-radius: 12px; cursor: pointer; transition: all 0.3s; text-align: center; font-weight: 600; } .floor-item:hover { background: rgba(0, 135, 189, 0.1); } .floor-item.active { background: linear-gradient(135deg, #0087bd 0%, #15cbf3 100%); color: white; } .toolbar { position: fixed; right: 20px; bottom: 40px; display: flex; flex-direction: column; gap: 0; z-index: 100; padding: 4px; } .toolbar::before { content: ; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(12px); border-radius: 18px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); z-index: -1; } .toolbar-btn { width: 52px; height: 52px; display: flex; align-items: center; justify-content: center; border-radius: 14px; cursor: pointer; transition: all 0.3s; } .toolbar-btn:hover { background: linear-gradient(135deg, #0087bd 0%, #15cbf3 100%); transform: scale(1.08); } .divider { width: 28px; height: 1px; background: linear-gradient(90deg, transparent, #e2e8f0, transparent); margin: 2px auto; } /style数据格式说明地图数据结构{config:{floor_height:0.01,wall_height:0.01,area_height:100},data:[{name:一楼,floor_shape:[x1,y1,x2,y2,...],walls:[{shape_type:points,wall_shape:[x1,y1,x2,y2,...],wall_color:#e9dacd}],infos:{poi_1:{id:1,name:展厅,position:[x,y],content:展厅描述,cover:cover.jpg,images:[image1.jpg,image2.jpg],pano_url:panorama.jpg}}}]}POI 数据字段说明id: POI 唯一标识name: POI 名称position: 地图上的坐标位置 [x, y]content: POI 详细描述cover: 封面图片images: 详情图片列表pano_url: 全景图片 URL可选快速开始1. 环境准备确保你已经安装了 Node.js推荐 v16 或更高版本。2. 安装依赖# 克隆项目gitclone https://gitee.com/unload2018/indoor3-d.git# 进入项目目录cdindoor3-d# 安装依赖npminstall3. 开发调试# 启动开发服务器支持热重载npmrun dev打开浏览器访问http://localhost:5173即可预览项目。4. 生产构建# 构建生产版本npmrun build# 预览构建结果npmrun preview响应式适配项目采用移动端优先的响应式设计适配三种屏幕尺寸桌面端 1024px标准布局完整功能展示平板端768px - 1024px适当缩小控件尺寸优化触摸体验手机端 768px紧凑布局抽屉全屏展示更大的按钮便于点击/* 平板端 */media(max-width:1024px){.menu-btn{width:48px;height:48px;}.toolbar-btn{width:48px;height:48px;}}/* 手机端 */media(max-width:768px){.menu-btn{width:44px;height:44px;}.marker-card{flex-direction:column;}}/* 小屏手机 */media(max-width:480px){.menu-btn{width:40px;height:40px;}}开发经验分享1. Three.js 性能优化使用requestAnimationFrame实现高效动画循环开启shadowMap提供更好的视觉效果使用OrbitControls的enableDamping提供平滑的操作体验合理设置相机和场景参数避免不必要的渲染2. Vue 3 最佳实践组合使用 Composition API 和 Options API合理组织组件结构分离 UI 和逻辑使用 scoped 样式避免样式污染充分利用 Vue 的响应式特性3. UI/UX 设计要点使用毛玻璃效果提升视觉层次感添加微妙的动画提升交互体验统一的配色方案蓝青色渐变 #0087bd → #15cbf3响应式设计多端适配4. 代码组织规范文件命名Vue 组件用 PascalCaseJS 文件用 kebab-case功能模块化单一职责原则注释清晰便于维护合理使用 Git 提交信息浏览器兼容性✅ Chrome/Edge推荐✅ Firefox✅ Safari⚠️ 需要支持 WebGL 的浏览器总结这个项目展示了如何使用 Vue 3 Three.js 构建一个完整的室内 3D 导航系统。从技术选型、架构设计到实现细节都体现了现代前端开发的最佳实践。希望这篇文章对你有所帮助如果你觉得这个项目有用欢迎 Star 和 Fork。有任何问题也欢迎在评论区交流讨论相关资源Vue 3 官方文档Three.js 官方文档Vite 官方文档Element Plus 组件库