当前位置: 首页 > news >正文

在 RuoYi 中接入 3D「园区驾驶舱」:Vue2 + Three.js + Nginx

目标:把一个 Vue2 + Three.js 的 3D 可视化大屏接入 RuoYi 后台,前后端解耦,统一经网关访问微服务接口;支持运行时切环境,Nginx 一键上线。
技术:Vue2、Three.js、GSAP、Axios、RuoYi-UI、Nginx。


1. 总体思路

  • 静态站/screen/(Nginx 托管)承载打包后的前端。

  • 接口前缀:前端一律请求 /api/**,由 Nginx 反代到网关(如 /prod-api/**)。

  • 运行时配置:把 API 前缀、设备 IP 写在 app-config.json,前端启动时加载——切环境无需重打包

  • RuoYi 菜单:在“菜单管理”新增菜单,组件路径指向前端页面(或 IFrame 外链)。

2. 目录结构(前端项目)

big-screen-vue-ip
├─ public
│  ├─ app-config.json          # 运行时配置(见下)
│  ├─ glb/                     # 3D 模型(示例:building-a.glb)
│  ├─ data/                    # 数据(routes/object.json 等)
│  └─ index.html
├─ src
│  ├─ views/park3d.vue         # 3D 场景页面(核心)
│  └─ main.js
├─ package.json
└─ vue.config.js               # publicPath = './'(推荐)

3. 运行时配置:public/app-config.json

{"API_BASE": "/api","CAM_IP": "<CAMERA_IP_PLACEHOLDER>"
}

占位说明:

  • <CAMERA_IP_PLACEHOLDER>:设备/摄像头 IP,按需替换;

  • API_BASE:前端统一调用前缀,始终写 /api,由 Nginx 去代理到网关。

4. Axios 初始化与 Token 透传(节选)

// src/views/park3d.vue(或 main.js)
import axios from 'axios';const BASE = process.env.BASE_URL || './'; // 打包后为 /screen/async function loadInit() {const cfg = (await axios.get(BASE + 'app-config.json')).data;// 统一接口前缀axios.defaults.baseURL = cfg.API_BASE || '/api';// (可选)从 ?token= 读取令牌,自动挂到 Authorizationconst token = new URLSearchParams(location.search).get('token');if (token) {axios.interceptors.request.use(c => {c.headers.Authorization = 'Bearer ' + token;return c;});}// 业务自用示例this.cameraIp = cfg.CAM_IP;
}

说明:静态文件(/screen/data/*.json)不要用 axios 默认实例读取,否则会被拼到 /api/...。下文给出用 fetch 的正确方式。

5. 静态 JSON 与模型加载:避免路径踩坑

5.1 用 fetch 读取静态 JSON(不要走 axios.baseURL)

async function loadData() {try {// 正确写法:fetch 走相对路径 /screen/data/*.json,不受 axios.baseURL 影响const roads = await (await fetch(BASE + 'data/routes.json', { cache: 'no-cache' })).json();const objects = await (await fetch(BASE + 'data/object.json', { cache: 'no-cache' })).json();this.parseRoads(roads);this.objects = objects;} catch (e) {console.error('Error loading data:', e);}
}

5.2 统一模型路径:normalizeModelPath + GLTFLoader.setPath

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';const GLB_BASE = BASE + 'glb/';function normalizeModelPath(file) {if (!file) return file;let f = String(file).trim().replace(/\\/g, '/');if (f.includes('://')) return f;                        // 完整 URL 原样返回f = f.replace(/\.gltf(\?.*)?$/i, '.glb$1');             // 统一换 .glbif (/^\/?gltf\//i.test(f)) f = f.replace(/^\/?gltf\//i, 'glb/');if (/^\/?glb\//i.test(f))  f = f.replace(/^\/?glb\//i, '');if (f.startsWith('/'))     return BASE + f.slice(1);    // /xxx → /screen/xxxreturn f; // 文件名交给 setPath(GLB_BASE) 去拼
}// 创建 loader 并设置基础路径
const loader = new GLTFLoader();
loader.setPath(GLB_BASE);// 用法示例:加载单个可点击模型
function loadModel(file, name, position, scale, rotation, userTag = null) {const url = normalizeModelPath(file);loader.load(url, gltf => {const model = gltf.scene;// 可点击标记const clickable = !!userTag;model.userData.clickable = clickable;model.userData.tag = userTag;// 材质统一处理(半透明等)const setOpacity = mesh => {const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material];mats.forEach(m => {m.transparent = true;m.opacity = 0.8;m.depthWrite = m.opacity >= 1.0;m.alphaTest = 0.1;m.needsUpdate = true;});};model.traverse(c => c.isMesh && setOpacity(c));// 位置缩放旋转model.position.set(position.x, position.y, position.z);model.scale.set(scale.x, scale.y, scale.z);model.rotation.set(THREE.MathUtils.degToRad(rotation.x),THREE.MathUtils.degToRad(rotation.y),THREE.MathUtils.degToRad(rotation.z));this.scene.add(model);if (clickable) this.clickableObjects.push(model);}, undefined, err => console.error('GLB load error:', err));
}

6. vue.config.js(推荐)

// vue.config.js
module.exports = {publicPath: './',           // 关键:发布到 /screen/ 子路径时资源可用devServer: {host: '0.0.0.0',port: 8090,proxy: {// 本地开发时,前端请求 /api/** 转发到网关'/api': {target: 'http://localhost:8080', // ← 你的网关changeOrigin: true,pathRewrite: { '^/api': '/prod-api' }}}}
};

7. RuoYi 菜单与路由(避免“两个菜单”)

  • 只用“菜单管理”下发动态路由;不要在 src/router/index.jsconstantRoutes 里再手写 /screen

  • 菜单配置建议:

    • 菜单类型:菜单

    • 路由地址:/screen(顶级菜单加 /

    • 组件路径:monitor/screen/index(你的页面路径)

    • 是否外链:否;显示:是;缓存:按需

如果你要用 IFrame 方式:把是否外链设为“是”,外链地址http://<YOUR_HOST>:8089/screen/(注意尾斜杠),打开方式选“内嵌”。

8. Nginx 配置(server 片段)

文件:C:\nginx\nginx-1.24.0\conf\nginx.conf(Windows 示例)

server {listen 8089;server_name <YOUR_HOST>;# 访问 /screen 会 301 到 /screen/(防止静态相对路径跑偏)location = /screen { return 301 /screen/; }# 大屏静态站(把 dist/ 拷到 C:/nginx/nginx-1.24.0/screen)location /screen/ {root  C:/nginx/nginx-1.24.0;try_files $uri $uri/ /screen/index.html;  # SPA 刷新兜底}# 前端统一 /api → 网关(按需调整)location /api/ {proxy_pass         http://<GATEWAY_HOST>:<GATEWAY_PORT>/prod-api/;proxy_set_header   Host $host;proxy_set_header   X-Real-IP $remote_addr;proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;proxy_http_version 1.1;}
}

<YOUR_HOST><GATEWAY_HOST><GATEWAY_PORT> 改成你的环境;生产建议用域名。

9. Windows 一键启动

文件:C:\nginx\nginx-1.24.0

taskkill //F //IM nginx.exe 2>/dev/null
cd /c/nginx/nginx-1.24.0
./nginx.exe -t && ./nginx.exe

10. 常见报错与快速定位

  1. GLTFLoader 报 “<!doctype ... is not valid JSON

    • .glb 请求 404/被重写 —— 检查 GLTFLoader.setPath(BASE + 'glb/')normalizeModelPath

    • 访问地址缺尾斜杠(/screen)导致相对路径错位 —— 加 location = /screen { return 301 /screen/; },在浏览器保证地址是 /screen/

  2. 读取 JSON 报 iterator/forEach undefined

    • 用了 axios 实例读静态 JSON → 被拼成 /api/screen/data/...。改用 fetch(BASE + 'data/*.json')

  3. 侧栏出现两个“可视化大屏”

    • 你同时在“菜单管理”建了菜单,又在 constantRoutes 手写了一条 /screen。删掉手写常量路由即可。

  4. CORS 跨域

    • 保证前端只访问 /api/**,由 Nginx 反代到网关域名/端口;不要在前端直接写网关的全量域名。

11. 小结

这套方案把前端静态和后端网关彻底解耦

  • 前端只认 /api,Nginx 负责转发到网关;

  • 环境切换只改 app-config.json

  • 发布就是“拷贝 dist → /screen + 重启 Nginx”。


文章转载自:

http://ESkuUuah.yjdqL.cn
http://YBQCZVs2.yjdqL.cn
http://mZzsf6FZ.yjdqL.cn
http://K9R0idCt.yjdqL.cn
http://PXulCHQb.yjdqL.cn
http://ZXB3tQQI.yjdqL.cn
http://V9eXhe9C.yjdqL.cn
http://j4a7Mu89.yjdqL.cn
http://v2Dndut4.yjdqL.cn
http://AeOzzIvl.yjdqL.cn
http://JN6vjbJy.yjdqL.cn
http://Gf5eO01n.yjdqL.cn
http://64tJluJi.yjdqL.cn
http://3ryuIiAS.yjdqL.cn
http://1ORpWd7W.yjdqL.cn
http://bsEx93LY.yjdqL.cn
http://FRbUQUia.yjdqL.cn
http://PFpGHgX7.yjdqL.cn
http://mAt5GtSG.yjdqL.cn
http://U9CAWnBj.yjdqL.cn
http://MGIXbLrf.yjdqL.cn
http://3EkqnP69.yjdqL.cn
http://39UYRlQJ.yjdqL.cn
http://AoeeBEa7.yjdqL.cn
http://AZjcgFVR.yjdqL.cn
http://D9lxaDfz.yjdqL.cn
http://dWwS9nf4.yjdqL.cn
http://18NKdPb7.yjdqL.cn
http://bNnoV9aZ.yjdqL.cn
http://aNQNuzrO.yjdqL.cn
http://www.dtcms.com/a/376276.html

相关文章:

  • tp5的tbmember表闭包查询 openid=‘abc‘ 并且(wx_unionid=null或者wx_unionid=‘‘)
  • PPT转化成PDF脚本
  • 基于 Dockerfile 构建镜像
  • Linux学习记录--消息队列
  • leetcode算法刷题的第三十一天
  • Linux驱动开发(2)进一步理解驱动
  • Linux驱动开发笔记(十)——中断
  • 推荐一款智能三防手机:IP68+天玑6300+PoC对讲+夜视
  • 栈:逆波兰表达式求解
  • nginx中ssl证书的获取与配置
  • 云平台得大模型使用以及调用
  • 手写简单的int类型顺序表
  • Spring Boot 深入剖析:BootstrapRegistry 与 BeanDefinitionRegistry 的对比
  • [rStar] 解决方案节点 | `BaseNode` | `MCTSNode`
  • 鸿蒙:@Builder 和 @BuilderParam正确使用方法
  • 美图云修-一站式AI修图软件
  • 从齿轮到智能:机器人如何重塑我们的世界【科普类】
  • F12中返回的id里preview和response内容不一致的问题
  • 【CSS 3D 交互】实现精美翻牌效果:从原理到实战
  • vue二次封装ant-design-vue的table,识别columns中的自定义插槽
  • vue方法汇总
  • GPU硬件架构和配置的理解
  • C++类和对象初识
  • 笔记:乐鑫 (Espressif) 的生态策略与开发者悖论
  • SELinux策略:域转换与类型继承
  • 【VLMs篇】06:Cosmos-Reason1:从物理常识到具身推理
  • 图漾相机 FM851-E2 相关资料
  • 资产管理什么软件好
  • npm 安装命令中关于 @ 的讲解,如:npm install @vue-office/docx vue-demi
  • PowerBI 没实现的的联动同步下钻,QuickBI 实现了