在 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 透传(节选)

javascript 复制代码
// 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= 读取令牌,自动挂到 Authorization
  const 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)

javascript 复制代码
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

javascript 复制代码
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');             // 统一换 .glb
  if (/^\/?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/xxx
  return 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(推荐)

javascript 复制代码
// 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 示例)

javascript 复制代码
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

bash 复制代码
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"。

相关推荐
编码浪子2 小时前
趣味学RUST基础篇(函数式编程闭包)
开发语言·算法·rust
wanhengidc2 小时前
高性价比云手机挑选指南
运维·网络·安全·游戏·智能手机
云枫晖2 小时前
JS核心知识-数据转换
前端·javascript
remaindertime2 小时前
(九)Spring Cloud Alibaba 2023.x:微服务接口文档统一管理与聚合
后端·spring cloud·微服务
凡间客2 小时前
Linux防火墙-Firewalld
linux·运维·服务器
Barcke2 小时前
📘 初识 WebFlux
spring boot·后端·spring
MC皮蛋侠客2 小时前
使用python test测试http接口
开发语言·python·http
JohnYan3 小时前
工作笔记 - 一个浏览器环境适用的类型转换工具
javascript·后端·设计模式
前端Hardy3 小时前
12个被低估的 CSS 特性,让前端开发效率翻倍!
前端·javascript·css