简介一套基于 Vue3 TypeScript Three.js 构建的全景家居 VR 看房系统面向前端开发者和三维可视化学习者提供可直接运行和二次开发的室内全景展示与交互方案包含完整的前端工程与场景资源。压缩包共 125 个文件约 75.54MB含 90 张 JPG 全景图、16 张 PNG 贴图、3 个 HDR 环境贴图以及 Vue 组件、JS 逻辑、项目配置文件等场景资源与源码统一打包目录结构清晰便于检索。系统支持拖拽查看 360° 自由视角点击房间名称或热点标注即可在不同房间之间切换快速了解整体户型布局HDR 环境贴图配合 Three.js 渲染呈现接近真实的光照质感。通过 npm install 安装依赖、npm run serve 启动项目可本地运行并在此基础上替换图片素材、调整热点位置完成个性化看房场景搭建。已有 557 人学习下载适合作为毕业设计、作品集案例或 threejs 全景交互入门实践也可在此基础上继续扩展更多房间和交互场景。1. 全景看房还能这么玩vue3 ts threejs 把一个楼盘塞进浏览器第一次打开这个项目的演示页时我以为是某个开发商花钱做的商业站点——鼠标拖拽、视角缩放、户型热点跳转整个看房流程跟实地走一遍差不多。后来发现它其实是一套纯前端的 vue3 ts threejs 全景家居 VR 看房系统所有代码都在本地跑数据源就是几张全景图加一份户型配置。对做房产网站、装修展示、展厅导览这类需求的人来说这个项目最大的价值是它把「全景展示」从依赖第三方平台的黑匣子变成了自己手里可以随意改的工程。你不需要懂复杂的图形学也不用买商用全景服务按它的套路把全景图一换、热点坐标一配一套看房系统就能上线。项目适合两类人一是前端开发想入门 threejs 全景渲染二是产品经理或独立开发者想快速给客户交付看房类 Demo。前者能学到 SphereGeometry 贴图、Raycaster 拾取、场景切换这些关键手法的落地写法后者直接改配置就能换皮。下面从技术栈一路拆到二次开发照着走就能跑起来。2. 技术选型与架构为什么是 vue3 ts threejs而不是纯 threejs 或 unity2.1 技术栈各司其职没有一个是凑数的这个项目的技术选型很克制一共四块Vue3 负责页面结构与状态、TypeScript 约束数据类型、Three.js 做 3D 渲染、Vite 做构建调试。没有引入 Pinia 这类重型状态库也没有上 UI 组件框架因为全景看房的核心交互其实只有「拖拽看房 点热点换房」两件事引入太多依赖反而拖慢首屏。Vue3 在这里最大的作用是组件化全景画布是一个组件、热点标签是一个组件、户型切换按钮是一个组件每个组件只干一件事数据通过 props 和 emit 传递。配合script setup语法代码量比 Options API 少三分之一左右。TypeScript 则用在最关键的两个地方——全景配置的数据结构和 threejs 对象的类型标注。全景配置里每个热点有 id、坐标、跳转目标如果不用类型约束写错字段名要到运行时才报错用 ts 后编辑器里就能提示出来。Three.js 是渲染核心。全景看房的原理说白了就是一个球体 一张全景图把相机放在球心图片贴在球体内表面鼠标拖拽时改变相机朝向看起来就像站在实景里转头环顾。这个项目用的是SphereGeometry球体加纹理贴图的方式而不是BoxGeometry六面贴图因为等距柱状投影的全景图宽高比 2:1直接用一张贴图就能映射素材获取成本最低。2.2 目录结构大文件拆分逻辑清晰改一个功能不用翻全站src/ ├── components/ # 业务组件 │ ├── PanoramaViewer.vue # 全景画布组件初始化 renderer、处理拖拽与缩放 │ ├── HotspotMarker.vue # 热点标记组件渲染可点击的箭头/圆点 │ └── RoomSwitcher.vue # 户型切换面板组件 ├── composables/ │ ├── usePanorama.ts # 组合式函数封装 threejs 场景创建与销毁 │ ├── useHotspots.ts # 热点数据管理坐标转换、射线检测 │ └── useTextureLoader.ts # 纹理加载预加载、进度回调、错误处理 ├── config/ │ └── scenes.ts # 全景场景配置每个房间的图片路径、热点列表、初始视角 ├── types/ │ └── panorama.ts # 全景相关的 ts 类型定义 ├── utils/ │ └── coordinates.ts # 经纬度坐标转 threejs 三维坐标的工具函数 └── App.vue # 入口组件组合 viewer 与 UI 面板这个结构是典型的「业务功能划分」components放界面元素composables放可复用的逻辑config放数据配置。最值得学习的是config/scenes.ts这个文件——所有房间的场景数据都在这里集中管理。想新增一个房间不用改任何 threejs 代码只需在这个文件里加一项配置想调整热点的位置改一个坐标数字就行。composables/usePanorama.ts是 threejs 逻辑的封装层。它对外只暴露三个方法init(container)创建场景、destroy()释放资源、switchScene(sceneId)切换场景。组件层不直接操作 threejs 对象这种解耦让 Vue 组件的代码很干净也让 threejs 逻辑可以脱离 Vue 单独测试。2.3 全景渲染的完整流程从配置数据到屏幕像素整个渲染链路可以简化成六步读取scenes.ts中的场景配置 → 用TextureLoader加载全景图 → 创建SphereGeometry球体并贴图 → 把相机放在球心设置初始朝向 → 监听鼠标事件更新相机旋转角 → 每帧调用renderer.render输出画面。热点交互则是在这个基础上叠加一层射线检测鼠标点击时从相机位置发射一条射线计算射线与热点精灵的包围盒是否相交相交则触发场景切换。这个流程本来需要不少 threejs 功底但项目把它封装成了几个独立模块每个模块的职责都很单纯。useTextureLoader负责加载资源usePanorama负责场景生命周期coordinates.ts负责把配置里的经纬度转成 threejs 的世界坐标。学这个项目本质上就是学这六步的工程化落地方式。3. 核心模块拆解一个全景场景从加载到可交互的完整链路3.1 场景初始化SphereGeometry 贴图的三个关键参数全景渲染的第一步是创建球体并把全景图贴上去。这段代码是usePanorama.ts里的核心片段我把它精简后逐行说明// src/composables/usePanorama.ts import * as THREE from three export function createPanoramaScene(texture: THREE.Texture): THREE.Scene { // 创建场景对象 const scene new THREE.Scene() // 球体半径设 500足够大以保证相机在球心时看不到边界 // 分段数 60 x 40 是平衡性能与平滑度的常见选择过小贴图变形明显 const geometry new THREE.SphereGeometry(500, 60, 40) // 材质使用 MeshBasicMaterial因为全景图自带完整光照信息不需要额外光照 const material new THREE.MeshBasicMaterial({ map: texture, side: THREE.BackSide // BackSide 让贴图显示在球体内表面相机在球心才能看到 }) const sphere new THREE.Mesh(geometry, material) scene.add(sphere) return scene }三个关键参数值得单独说。SphereGeometry的半径设 500 而不是 5是为了让相机在球心时球体远到看不见边缘——如果半径太小移动视口时可能看到球体外部的空白区域。分段数 60 和 40 分别对应经线和纬线的网格密度数值越大球体越平滑但顶点数也越多。对于手机端项目60 x 40 已经够用再大会增加 GPU 负担。side: THREE.BackSide是这个场景里最容易忽略的一行。threejs 默认只渲染材质的外表面而相机在球体内必须设置成BackSide才能让贴图显示在球的内壁上否则打开页面是黑屏。我在第一次调试时就栽在这上面——球体创建成功、贴图加载成功、相机位置也正确但画面上什么都没有最后一查就是少了这一行。MeshBasicMaterial的选择也是有讲究的。全景图本身是用相机拍摄的实景照片光影信息已经固化在图片里不需要 threejs 再模拟光照所以用BasicMaterial而非StandardMaterial。后者会引入环境光、漫反射等计算反而可能导致画面发暗。3.2 相机控制经纬度旋转与缩放阻尼全景看房的核心交互是鼠标拖拽旋转视角和滚轮缩放。这个项目没有用OrbitControls而是自己实现了一套轻量级的旋转控制代码更可控// src/composables/usePanorama.ts // 相机使用透视相机fov 设 75 模拟人眼视角范围 const camera new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 1000) camera.target new THREE.Vector3(0, 0, 0) // 记录当前视角的经纬度角弧度制 let lon 0, lat 0 // 鼠标拖拽时更新角度onPointerDown 记录起始点onPointerMove 计算偏移 function onPointerMove(event: PointerEvent) { // 水平方向移动 1px 约旋转 0.1 度换算成弧度这个灵敏度在大多数设备上手感适中 lon - event.movementX * 0.001 // 垂直方向限制在正负 90 度之间避免翻转到球体下方 lat Math.max(-Math.PI / 2, Math.min(Math.PI / 2, lat - event.movementY * 0.001)) // 经纬度转三维坐标球坐标公式的 threejs 实现 camera.position.x 500 * Math.cos(lat) * Math.sin(lon) camera.position.y 500 * Math.sin(lat) camera.position.z 500 * Math.cos(lat) * Math.cos(lon) camera.lookAt(camera.target) }旋转灵敏度的取值 0.001 是我实际调试时得出的经验值。值太大会导致拖拽时画面「飘」值太小则拖半天转不过身。不同屏幕尺寸下这个值可以微调——大屏显示器建议降到 0.0008手机端可以提高到 0.0012。垂直方向的Math.max / Math.min限制也很重要如果不限制lat 超过 π/2 后画面会翻转倒置这是很多全景 Demo 的常见 bug。缩放用滚轮事件实现本质是修改相机的 fov 值。fov 越小视野越窄画面里的物体显得越大。代码实现是// 滚轮缩放fov 限制在 30 到 90 度之间防止缩放过度导致畸变 function onWheel(event: WheelEvent) { camera.fov event.deltaY * 0.05 camera.fov Math.max(30, Math.min(90, camera.fov)) camera.updateProjectionMatrix() // 修改 fov 后必须更新投影矩阵否则不生效 }updateProjectionMatrix这行也是容易漏掉的。fov 是相机投影矩阵的参数之一直接改数值不会触发矩阵重算不调用这个方法的话滚轮滚了半天画面毫无变化。3.3 热点标记与射线检测从 2D 坐标到 3D 拾取全景看房里最重要的交互是「点热点 → 切换到另一个房间」。这个项目的热点用THREE.Sprite实现每个热点是一个始终朝向相机的平面贴图上面画一个箭头或圆点。热点数据在scenes.ts里配置为经纬度坐标运行时通过工具函数转成三维坐标// src/utils/coordinates.ts // 全景图中某个位置的经纬度lon, lat转为 threejs 世界坐标 // 注意这里 lon/lat 的单位是弧度不是角度 export function lonLatToVector3(lon: number, lat: number, radius: number): THREE.Vector3 { const phi (90 - lat) * Math.PI / 180 const theta (lon 180) * Math.PI / 180 return new THREE.Vector3( -radius * Math.sin(phi) * Math.cos(theta), radius * Math.cos(phi), radius * Math.sin(phi) * Math.sin(theta) ) }经纬度到三维坐标的换算本质上就是球坐标公式。这里最绕的地方是经纬度的坐标系约定全景图的经度 0 度通常对应图片正中央经度正方向向右而 threejs 的世界坐标 X 轴朝右Z 轴朝向屏幕外。所以公式里做了180和取负的处理来对齐坐标系。如果你接入的全景图来自不同的拍摄设备可能同一组坐标在不同设备上方向相反需要对公式里的正负号做微调。点击检测用Raycaster实现threejs 内置的射线检测模块// src/composables/useHotspots.ts import * as THREE from three export function checkHotspotClick( event: MouseEvent, camera: THREE.PerspectiveCamera, hotspots: THREE.Sprite[] ): THREE.Sprite | null { // 鼠标点击的屏幕坐标转标准化设备坐标NDC范围 -1 到 1 const mouse new THREE.Vector2() mouse.x (event.clientX / window.innerWidth) * 2 - 1 mouse.y -(event.clientY / window.innerHeight) * 2 1 // 从相机位置沿鼠标方向发射射线 const raycaster new THREE.Raycaster() raycaster.setFromCamera(mouse, camera) // 检测射线与所有热点精灵的相交情况 const intersects raycaster.intersectObjects(hotspots) if (intersects.length 0) { return intersects[0].object as THREE.Sprite } return null }Raycaster的工作原理是从相机位置发出一条射线穿过鼠标点击的屏幕位置然后检测这条射线与场景中物体的包围盒是否相交。这套机制是 threejs 做拾取Picking的标准做法很多 3D 编辑器、游戏里的点选功能都是这个原理。需要注意的是intersectObjects检测的是 Sprite 的包围盒所以热点贴图边缘的透明区域也可能被判定为命中——如果热点太密集可以考虑改用手工计算的圆形判定按距离过滤掉边缘点击。3.4 场景切换淡入淡出与资源释放从客厅切换到卧室这个项目的实现不是简单地换贴图而是做了一次完整的场景重建// src/composables/usePanorama.ts export function switchScene(sceneId: string) { // 先从配置中读取目标场景的数据 const config scenesConfig[sceneId] // 加载新场景的全景图加载期间显示 loading 遮罩 loading.value true textureLoader.load(config.imageUrl, (texture) { // 创建新场景前先释放旧场景的 GPU 资源 cleanupScene() // 重建场景、相机、热点 scene createPanoramaScene(texture) initHotspots(config.hotspots) // 将相机朝向重置为初始视角 resetCamera(config.initialView) loading.value false }) } // cleanupScene 的关键是遍历场景中的所有对象并释放几何体和材质 function cleanupScene() { scene.traverse((obj) { if (obj instanceof THREE.Mesh) { obj.geometry.dispose() if (obj.material instanceof THREE.Material) { obj.material.dispose() } } }) renderer.dispose() }场景切换最容易出的问题是内存泄漏。threejs 创建的对象几何体、材质、纹理都保存在 GPU 显存里如果不手动释放来回切几次后显存就爆了。cleanupScene里的dispose()调用是必须的而且要注意顺序先释放几何体和材质最后释放 renderer。另外 Vue 组件卸载时也要调用cleanupScene否则页面跳走后 threejs 的动画循环还在跑。淡入淡出的效果是在场景重建时加了一个透明度动画新场景的材质opacity从 0 渐变到 1需要设置transparent: true。这个效果虽然只有一行参数但对用户体验的提升很明显——没有淡入淡出时切场景是一瞬间「跳变」有了过渡感觉柔和很多。4. 运行配置与调试指南从源码到线上可访问4.1 环境要求与依赖安装项目基于 Vite 构建要求 Node.js 版本 16 以上npm 7 以上或者用 pnpm / yarn。工具链就两个关键依赖three^0.160.0 这个区间和 vite-plugin-glb或者直接通过 URL 加载纹理看项目配置。安装依赖# 克隆项目后进入根目录安装依赖 npm install # 启动开发服务器默认端口 5173 npm run dev如果安装过程中遇到node-sass或sass相关的报错基本可以断定是 Node 版本太高导致的。threejs 项目里常见的解决方案是换用官方推荐的 Vite sass组合并确保 Node 版本在 16 到 20 之间。如果不确定本机环境直接用nvm切到 Node 18 LTS 最稳妥目前生态兼容性最好。4.2 开发调试的三个关键入口项目跑起来后改动最频繁的三个地方是config/scenes.ts换全景图和热点、App.vue改页面布局、components/PanoramaViewer.vue改 viewer 的行为。调试时有三件事值得注意第一浏览器控制台如果有「THREE.WebGLRenderer: Context Lost」报错是 GPU 上下文丢失的常见问题。这种时候先强制刷新页面如果频繁出现考虑是否是显卡驱动问题。第二改scenes.ts里的热点坐标时在页面上看不到实时的方位反馈我的调试习惯是先在useHotspots.ts里加一行console.log输出热点的屏幕投影坐标确认热点在画面中的大致位置再微调经纬度数值。第三切换房间后留意显存占用变化——打开任务管理器Windows或活动监视器macOS如果每次切换后显存占用只增不减说明cleanupScene有遗漏。4.3 生产构建与部署要点打包部署的命令是标准的 Vite 流程npm run build产物在dist/目录下。部署到 Nginx 时有一个坑threejs 的纹理加载用的是相对路径项目里如果配置了base: ./Vite 配置项产物可以放在任意子目录如果用默认的/就要保证部署在域名根路径否则全景图 404。我在某次部署时把系统放在example.com/house/子目录下结果图片全挂最后在vite.config.ts里加了一行base: ./就解决了。如果全景图文件比较大单张 8K 图可能 10MB 以上建议用 Nginx 的gzip或brotli压缩纹理图压缩率能到 60% 以上。也可以考虑把全景图转成 WebP 格式——threejs 的TextureLoader原生支持 WebP但要注意 Safari 老版本的兼容性。5. 常见问题与避坑记录全景看房项目最容易翻车的六个细节5.1 全景图出现「上下颠倒」或者「左右镜像」的现象现象贴图加载后画面里的天花板在地面、地板在天花板或者文字方向是反的。原因全景图的坐标系约定不统一。拍摄设备不同无人机、手机、全景相机输出的图片方向和 threejs 的 UV 映射规则可能对不上。解决不要改代码先在图片编辑软件里把图旋转 180 度或水平翻转试试。如果图片动辄 10MB 不好处理可以在代码里改纹理的旋转属性texture.rotation Math.PI并设置texture.center.set(0.5, 0.5)让旋转中心居中。这两个参数是 threejs 纹理的标准处理手段90% 的方向问题都靠它解决。5.2 切换场景后显存持续增长页面越来越卡现象反复切换房间十几次后页面帧率明显下降风扇狂转。原因每次切换场景都创建了新的几何体、材质和纹理但旧对象没有被正确dispose()。解决严格按前面的cleanupScene逻辑释放资源——geometry.dispose()、material.dispose()、texture.dispose()最后renderer.dispose()。还有一个容易漏的是 Sprite 的材质热点精灵的贴图也是纹理需要在traverse里一并处理。我的习惯是每切换一次场景就手动检查一次 GPU 显存养成这个习惯后基本不会出问题。5.3 热点点击「偶尔生效偶尔不生效」现象同一个热点有时候一次点击就跳转有时候要点三四次才触发。原因Raycaster检测的是热点 Sprite 的包围盒Sprite 贴图往往有透明留白。点击透明区域时包围盒有碰撞但视觉上没碰到图标就会产生「没点上」的错觉。解决两个方向。一是缩小 Sprite 的scale让包围盒更贴合图标二是点击判定后再加一层距离过滤——如果射线命中点距离 Sprite 中心的距离大于某个阈值比如图标半径的 80%就忽略这次点击。后者的体验更稳但因为要计算世界坐标距离代码量稍多一些。5.4 滚轮缩放没反应或者画面突然翻转现象滚轮滚动时画面没有缩放效果或者垂直视角转到某个角度后画面整个颠倒。原因缩放失效通常是忘了调updateProjectionMatrix()画面翻转的根源是lat超出了 ±90 度范围。解决滚轮回调里确保camera.updateProjectionMatrix()在赋值 fov 之后调用。翻转问题在onPointerMove里给lat加上Math.max(-Math.PI/2, Math.min(...))的钳制即可。这两个问题都是全景项目的高频 bug代码里写好后基本能一次性通过。5.5 手机上陀螺仪没反应无法「转动手机看房」现象PC 端拖拽正常但手机上打开后转动手机页面没有相应变化。原因iOS 13 和 Chrome Android 都需要用户授权后才能访问DeviceOrientationEvent。项目没有处理授权流程。解决可以做双模式——手机端默认用手指拖拽来旋转视角同时提供一个「陀螺仪」开关按钮。开启时主动向用户申请权限。类似这样的处理// 切换陀螺仪模式 if (typeof DeviceOrientationEvent.requestPermission function) { await DeviceOrientationEvent.requestPermission() }申请权限后设置window.addEventListener(deviceorientation, handler)。这里有个「血泪经验」授权只能由用户点击触发时调用页面加载时直接调会被浏览器拒绝。5.6 全景图跨域导致TextureLoader加载失败现象本地跑没问题部署到服务器后全景图加载报 CORS 错误控制台提示texImage2D: Tainted canvases may not be loaded。原因全景图部署在 CDN 或 OSS 上没有配置Access-Control-Allow-Origin响应头。解决先确认图片所在域名是否开启跨域没有的话在 static 存储服务里给*.jpg加上Access-Control-Allow-Origin: *。如果图片路径和页面同域则检查 Nginx 配置里是否有奇怪的 location 重写规则。这个问题的本质是浏览器的安全策略跟 threejs 无关排查思路要往静态资源配置方向走。6. 二次开发实战把演示改造成你的看房系统6.1 换数据新增一套房源只需要改配置文件项目最有价值的地方是数据和逻辑完全分离。要接入一套新房源只需在src/config/scenes.ts里新增场景配置。每个场景的配置结构是export interface SceneConfig { id: string // 场景唯一标识用于切换 name: string // 显示名用于 UI imageUrl: string // 全景图地址 initialView: { // 进入场景时的默认视角 lon: number // 经度弧度 lat: number // 纬度弧度 } hotspots: Array{ // 热点列表 id: string lon: number // 热点所在的经度弧度 lat: number // 热点所在的纬度弧度 targetSceneId: string // 点击后跳转的场景 id-1 表示无跳转 icon: string // 图标类型arrow 或 circle } }坐标的换算方法把全景图放入看房系统打开控制台用lonLatToVector3逆向计算热点应该显示的方位然后微调。实际项目里我一般先填一个估算坐标跑起来看热点在画面中的位置再用「画面中心对准热点」的方式回读相机当前的经纬度一次校准到位。6.2 改交互给热点加「房间信息弹窗」当前热点的点击行为是直接跳转场景。如果要改成「点击弹窗展示房间信息」只需要在HotspotMarker.vue的点击回调里增加一个分支!-- src/components/HotspotMarker.vue -- template div classhotspot-marker clickhandleClick !-- 图标渲染逻辑 -- /div /template script setup langts const props defineProps{ hotspot: HotspotConfig }() const emit defineEmits{ (e: hotspot-click, hotspot: HotspotConfig): void }() function handleClick() { // 如果热点配置了 targetSceneId 则正常跳转否则抛出信息展示事件 if (props.hotspot.targetSceneId props.hotspot.targetSceneId ! -1) { emit(hotspot-click, props.hotspot) } else { // 处理信息展示逻辑比如通过弹窗组件展示户型图、面积、价格 console.log(show info for hotspot:, props.hotspot.id) } } /script这是一种常见的扩展方式把「跳转」和「展示信息」作为两个动作由配置字段决定走哪个分支。实际项目中我会再加一个action: switch | info字段让配置语义更清晰。这样开发时新增一种交互类型只需要扩展枚举和分支逻辑不需要动渲染层。6.3 加功能接入后端房源数据从静态配置变为动态加载当前项目的房源配置在scenes.ts里是硬编码真正商用肯定要接后端接口。改造思路是页面加载时请求/api/house/{id}/scenes接口拿到场景列表后动态创建渲染器async function loadHouseFromApi(houseId: string) { // 动态加载后端数据替代 import 静态配置 const response await fetch(/api/house/${houseId}/scenes) const scenes: SceneConfig[] await response.json() // 校验数据合法性每个场景必须有 id 和 imageUrl scenes.forEach(scene { if (!scene.id || !scene.imageUrl) { throw new Error(场景 ${JSON.stringify(scene)} 缺少必填字段) } }) // 初始化到第一个场景 initViewer(scenes[0]) }接入后端后有一个额外要注意的点动态加载意味着接口可能返回imageUrl指向不同的 CDN 域名用户隐私合规层面要确保图片域名可跨域访问。逻辑校验这块建议做接口层的字段校验而不是等 threejs 运行时报错——实际项目的教训是后端漏传一个字段前端可能黑屏半天才定位到是数据问题。6.4 性能优化首屏加载速度的取舍全景图本质是高分辨率图片加载速度直接影响用户留存。项目里有一个被忽略的优化点图片分辨率与显示需求的匹配。如果是手机端看房2K 全景图约 2000x1000已经足够清晰PC 端才需要 4K 或 8K。可以分别在assets目录放不同分辨率的图运行时根据window.innerWidth决定加载哪一份// 根据屏幕宽度选择全景图清晰度移动端用低分辨率图加速加载 const imageUrl window.innerWidth 768 ? scene.imageMobileUrl : scene.imagePcUrl另外注意纹理的minFilter设置。默认的LinearMipmapLinear过滤会生成 mipmap 链内存占用较高如果图片本身不过大可以设置texture.minFilter THREE.LinearFilter减少内存消耗。更大胆的取舍是不用 mipmap让缩小采样交给 GPU 实时处理——画面质量几乎看不出差别但内存能省下一大块。做这套二次开发我自己的习惯是先把config/scenes.ts改成从接口加载确认渲染链路不受影响再加「信息弹窗」类的交互分支最后做分辨率适配。每一步都保持「渲染层不动、数据层扩展」的原则。从那以后我每次改这个项目都强制走一遍「配置 → 逻辑 → 渲染」三层纵向验证避免动一处坏另一处的情况。希望这篇拆解能帮你少走一些弯路。本文还有配套的精品资源点击获取