目标读者 :想从头做一个虚拟仿真大屏的小白或初级开发者
阅读时间 :60-90 分钟
核心收获 :从零到一完成一个能实时显示数据的虚拟仿真大屏
预处理:已完成软件安装(参考02_技术选型),会HTML/CSS/JS基础
🗺️ 全程路线图
阶段一(第1-2周):项目搭建 + 基本页面布局
↓
阶段二(第3-4周):3D场景 + 模型加载
↓
阶段三(第5-6周):后端服务 + 数据库
↓
阶段四(第7-8周):实时数据推送 + 图表
↓
阶段五(第9-10周):数据模拟器 + 联调
↓
阶段六(第11-12周):优化 + 部署
🎯 最终效果预览
┌──────────────────────────────────────────────────────────────┐
│ 🏭 智能工厂虚拟仿真系统 🟢 运行中 时间 │
├───────────┬──────────────────────────────────┬───────────────┤
│ 📦今日产量 │ │ 🚨 实时告警 │
│ 5,247件 │ ╔══════════════════╗ │ ⚡ ROB001温度高 │
│ ↑ +5.2% │ ║ 3D仿真场景 ║ │ ⚡ CON001振动大 │
│ │ ║ 工厂+设备+AGV ║ │ ✅ 系统正常 │
│ ⚡能源消耗│ ║ 可旋转缩放 ║ │ │
│ 8.3 kW │ ╚══════════════════╝ │ 🔌 设备状态 │
│ ↓ -2.1% │ │ ● ROB001 在线 │
│ │ │ ● CON001 在线 │
│ 📊运行效率│ │ ○ AGV001 离线 │
│ 92.4 % │ │ │
├───────────┴──────────────────────────────────┴───────────────┤
│ 📈产量趋势图 │ ⚡能耗分析 │ 📉效率走势图 │
└──────────────────┴─────────────────┴─────────────────────────┘
阶段一:项目搭建 + 基本页面布局
1.1 项目初始化
bash
# 打开终端(Windows:搜索"cmd"或"PowerShell")
# 选一个目录,比如桌面
cd Desktop
# 创建项目
npx create-react-app simulation-screen
cd simulation-screen
# 安装需要的库
npm install three @react-three/fiber @react-three/drei
npm install echarts echarts-for-react
npm install socket.io-client axios dayjs
等待安装完成后,项目文件夹已经好了。
1.2 清理默认文件
删除不需要的文件,保留核心结构:
保留这些:
src/index.js
src/App.js
src/App.css
public/index.html
删除这些:
src/App.test.js
src/reportWebVitals.js
src/setupTests.js
1.3 大屏页面布局
App.css(样式):
css
/* App.css - 大屏基础样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* 避免在大屏上出现滚动条 */
html, body {
width: 100%;
height: 100%;
overflow: hidden;
font-family: 'Microsoft YaHei', '微软雅黑', sans-serif;
}
/* 深色科技风主题 */
body {
background: #080b1a;
color: #ffffff;
}
/* 主容器 */
.app-container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
}
/* ========== 顶部栏 ========== */
.header {
height: 8vh;
background: linear-gradient(90deg, #080b1a 0%, #0d1633 50%, #080b1a 100%);
border-bottom: 1px solid rgba(0, 102, 255, 0.3);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 30px;
}
.header-title {
font-size: 22px;
font-weight: 600;
letter-spacing: 2px;
}
.header-right {
display: flex;
align-items: center;
gap: 25px;
}
.status-badge {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
/* 绿色闪烁的在线点 */
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #26de81;
animation: blink 2s infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
/* ========== 中间主区域 ========== */
.main-content {
flex: 1;
display: flex;
gap: 10px;
padding: 10px;
height: 70vh;
}
/* 左右面板 */
.side-panel {
width: 280px;
display: flex;
flex-direction: column;
gap: 12px;
}
/* 中间3D区域 */
.scene-container {
flex: 1;
background: rgba(8, 11, 26, 0.9);
border-radius: 8px;
border: 1px solid rgba(0, 102, 255, 0.2);
overflow: hidden;
}
/* ========== 底部图表区 ========== */
.bottom-charts {
height: 22vh;
display: flex;
gap: 10px;
padding: 0 10px 10px;
}
.chart-box {
flex: 1;
background: rgba(13, 22, 51, 0.8);
border-radius: 8px;
border: 1px solid rgba(0, 102, 255, 0.15);
padding: 15px;
}
/* ========== 数据卡片 ========== */
.data-card {
background: rgba(13, 22, 51, 0.8);
border-radius: 8px;
border-left: 3px solid #0066ff;
padding: 18px;
transition: transform 0.2s;
}
.data-card:hover {
transform: translateY(-3px);
}
.card-label {
font-size: 13px;
color: #8899aa;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
}
.card-value {
font-size: 32px;
font-weight: 700;
font-family: 'DIN', 'Arial Narrow', sans-serif;
}
.card-trend {
font-size: 12px;
margin-top: 6px;
}
.trend-up { color: #26de81; }
.trend-down { color: #ff4757; }
/* ========== 告警列表 ========== */
.alert-panel {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.alert-title {
font-size: 14px;
color: #8899aa;
padding: 10px 0;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.alert-list {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
padding-top: 8px;
}
.alert-item {
background: rgba(255, 71, 87, 0.1);
border-left: 3px solid #ff4757;
border-radius: 4px;
padding: 10px 12px;
font-size: 13px;
}
.alert-item .alert-device {
font-weight: 600;
color: #ff6b81;
}
.alert-item .alert-msg {
color: #aaa;
margin-top: 3px;
}
.alert-item .alert-time {
color: #666;
font-size: 11px;
margin-top: 3px;
}
/* 无告警状态 */
.no-alert {
color: #26de81;
text-align: center;
padding: 20px;
font-size: 14px;
}
/* 滚动条美化 */
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #0066ff; border-radius: 2px; }
App.js(主组件):
javascript
import React, { useState, useEffect } from 'react';
import './App.css';
import dayjs from 'dayjs';
function App() {
// 状态定义
const [currentTime, setCurrentTime] = useState(dayjs().format('YYYY-MM-DD HH:mm:ss'));
const [alerts, setAlerts] = useState([
{ id: 1, device: 'ROB-001', msg: '温度超过警戒值', time: '10:23:45' },
{ id: 2, device: 'CONV-001', msg: '传送带速度异常', time: '09:58:12' }
]);
const [stats, setStats] = useState({
production: 5247,
energy: 8.3,
efficiency: 92.4,
onlineDevices: 8
});
const [trends, setTrends] = useState({
production: 5.2,
energy: -2.1,
efficiency: 1.5
});
// 每秒更新时间
useEffect(() => {
const timer = setInterval(() => {
setCurrentTime(dayjs().format('YYYY-MM-DD HH:mm:ss'));
}, 1000);
return () => clearInterval(timer);
}, []);
return (
<div className="app-container">
{/* ===== 顶部栏 ===== */}
<header className="header">
<div className="header-title">🏭 智能工厂虚拟仿真系统</div>
<div className="header-right">
<div className="status-badge">
<span className="status-dot"></span>
系统运行中
</div>
<span style={{ color: '#8899aa', fontSize: '14px' }}>{currentTime}</span>
</div>
</header>
{/* ===== 中间主区域 ===== */}
<div className="main-content">
{/* 左侧数据卡片 */}
<div className="side-panel">
<div className="data-card" style={{ borderLeftColor: '#0066ff' }}>
<div className="card-label">📦 今日产量</div>
<div className="card-value">{stats.production.toLocaleString()}<span style={{ fontSize: '16px', color: '#8899aa' }}> 件</span></div>
<div className={`card-trend ${trends.production > 0 ? 'trend-up' : 'trend-down'}`}>
{trends.production > 0 ? '↑' : '↓'} {Math.abs(trends.production)}% 较昨日
</div>
</div>
<div className="data-card" style={{ borderLeftColor: '#ffa502' }}>
<div className="card-label">⚡ 能源消耗</div>
<div className="card-value">{stats.energy}<span style={{ fontSize: '16px', color: '#8899aa' }}> kW</span></div>
<div className={`card-trend ${trends.energy > 0 ? 'trend-down' : 'trend-up'}`}>
{trends.energy > 0 ? '↑' : '↓'} {Math.abs(trends.energy)}% 较昨日
</div>
</div>
<div className="data-card" style={{ borderLeftColor: '#26de81' }}>
<div className="card-label">📊 运行效率</div>
<div className="card-value">{stats.efficiency}<span style={{ fontSize: '16px', color: '#8899aa' }}> %</span></div>
<div className={`card-trend ${trends.efficiency > 0 ? 'trend-up' : 'trend-down'}`}>
{trends.efficiency > 0 ? '↑' : '↓'} {Math.abs(trends.efficiency)}% 较昨日
</div>
</div>
<div className="data-card" style={{ borderLeftColor: '#6c5ce7' }}>
<div className="card-label">🔌 在线设备</div>
<div className="card-value">{stats.onlineDevices}<span style={{ fontSize: '16px', color: '#8899aa' }}> / 10</span></div>
<div className="card-trend trend-up">↑ 全部正常</div>
</div>
</div>
{/* 中间3D场景(暂时放占位) */}
<div className="scene-container">
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#8899aa' }}>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '40px', marginBottom: '10px' }}>🎨</div>
<div>3D仿真场景(下一阶段实现)</div>
</div>
</div>
</div>
{/* 右侧告警面板 */}
<div className="side-panel">
<div className="alert-panel">
<div className="alert-title">🚨 实时告警 ({alerts.length})</div>
<div className="alert-list">
{alerts.length > 0 ? alerts.map(alert => (
<div key={alert.id} className="alert-item">
<div className="alert-device">⚠️ {alert.device}</div>
<div className="alert-msg">{alert.msg}</div>
<div className="alert-time">🕐 {alert.time}</div>
</div>
)) : (
<div className="no-alert">✅ 无告警,系统运行正常</div>
)}
</div>
</div>
</div>
</div>
{/* ===== 底部图表区 ===== */}
<div className="bottom-charts">
<div className="chart-box">📈 产量趋势图(下一阶段实现)</div>
<div className="chart-box">⚡ 能耗分析图(下一阶段实现)</div>
<div className="chart-box">📉 效率走势图(下一阶段实现)</div>
</div>
</div>
);
}
export default App;
运行验证
bash
cd simulation-screen
npm start
# 打开浏览器 http://localhost:3000
# 应该看到基本布局和数据卡片
阶段二:3D场景 + 模型加载
2.1 创建3D场景组件
创建文件 src/Scene3D.js:
javascript
import React from 'react';
import { Canvas } from '@react-three/fiber';
import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
import { useFrame, useRef } from '@react-three/fiber';
import * as THREE from 'three';
// ===== 机械臂组件 =====
function RobotArm({ position, status, temperature }) {
const groupRef = useRef();
const [rotationAngle, setRotationAngle] = React.useState(0);
// 根据状态决定颜色
const statusColor = status === 'online' ? '#0066ff' : '#ff4757';
// 动画:在线时缓慢移动
useFrame((state, delta) => {
if (status === 'online' && groupRef.current) {
const t = state.clock.elapsedTime;
// 手臂上下摆动
groupRef.current.children[0].rotation.z = Math.sin(t * 0.5) * 0.3;
}
});
return (
<group position={position} ref={groupRef}>
{/* 底座 */}
<mesh position={[0, 0.2, 0]}>
<cylinderGeometry args={[0.3, 0.3, 0.4, 16]} />
<meshStandardMaterial color="#444" metalness={0.8} roughness={0.2} />
</mesh>
{/* 第一段手臂 */}
<mesh position={[0, 1, 0]} rotation={[0, 0, 0]}>
<cylinderGeometry args={[0.08, 0.1, 1.5, 12]} />
<meshStandardMaterial color={statusColor} metalness={0.9} roughness={0.1} emissive={statusColor} emissiveIntensity={0.2} />
</mesh>
{/* 第二段手臂 */}
<mesh position={[0, 2, 0]}>
<cylinderGeometry args={[0.06, 0.08, 1, 12]} />
<meshStandardMaterial color={statusColor} metalness={0.9} roughness={0.1} />
</mesh>
{/* 关节球 */}
<mesh position={[0, 0.5, 0]}>
<sphereGeometry args={[0.12, 12, 12]} />
<meshStandardMaterial color="#888" metalness={1} roughness={0.3} />
</mesh>
{/* 状态指示灯 */}
<pointLight position={[0, 2.5, 0]} color={statusColor} intensity={1} distance={3} />
{/* 设备名称标签 */}
<Text position={[0, 3, 0]} fontSize={0.25} color={statusColor} anchorX="center">
{status === 'online' ? '● 在线' : '○ 离线'}
</Text>
{/* 温度标签 */}
<Text position={[0, 2.5, 0]} fontSize={0.2} color="#aaa" anchorX="center">
{`${temperature.toFixed(1)}°C`}
</Text>
</group>
);
}
// ===== 传送带组件 =====
function Conveyor({ position }) {
return (
<group position={position} rotation={[0, Math.PI / 4, 0]}>
{/* 传送带主体 */}
<mesh>
<boxGeometry args={[6, 0.15, 1.5]} />
<meshStandardMaterial color="#555" metalness={0.5} roughness={0.5} />
</mesh>
{/* 两端滚轮 */}
<mesh position={[3, 0, 0]} rotation={[Math.PI / 2, 0, 0]}>
<cylinderGeometry args={[0.2, 0.2, 1.5, 16]} />
<meshStandardMaterial color="#333" metalness={0.8} />
</mesh>
<mesh position={[-3, 0, 0]} rotation={[Math.PI / 2, 0, 0]}>
<cylinderGeometry args={[0.2, 0.2, 1.5, 16]} />
<meshStandardMaterial color="#333" metalness={0.8} />
</mesh>
{/* 支撑架 */}
<mesh position={[2, -0.5, 0]}>
<boxGeometry args={[0.1, 1, 0.1]} />
<meshStandardMaterial color="#666" />
</mesh>
<mesh position={[-2, -0.5, 0]}>
<boxGeometry args={[0.1, 1, 0.1]} />
<meshStandardMaterial color="#666" />
</mesh>
{/* 移动物品(模拟产品) */}
<MovingProduct />
</group>
);
}
// 传送带上移动的产品
function MovingProduct() {
const ref = useRef();
useFrame((state) => {
if (ref.current) {
const t = state.clock.elapsedTime % 4; // 4秒循环
ref.current.position.x = (t / 4) * 6 - 3; // -3到3
}
});
return (
<mesh ref={ref} position={[0, 0.25, 0]}>
<boxGeometry args={[0.5, 0.3, 0.4]} />
<meshStandardMaterial color="#ffa502" />
</mesh>
);
}
// ===== AGV小车组件 =====
function AGV({ position }) {
const ref = useRef();
useFrame((state) => {
if (ref.current) {
const t = state.clock.elapsedTime;
// 沿路径移动
ref.current.position.x = Math.sin(t * 0.3) * 5;
ref.current.position.z = Math.cos(t * 0.3) * 5;
}
});
return (
<group ref={ref}>
{/* 车体 */}
<mesh position={[0, 0.25, 0]}>
<boxGeometry args={[1.2, 0.5, 1.5]} />
<meshStandardMaterial color="#ffa502" metalness={0.3} roughness={0.4} />
</mesh>
{/* 顶部指示灯 */}
<pointLight position={[0, 0.8, 0]} color="#ffa502" intensity={2} distance={5} />
{/* 轮子 */}
{[[-0.5, 0, 0.6], [0.5, 0, 0.6], [-0.5, 0, -0.6], [0.5, 0, -0.6]].map((pos, i) => (
<mesh key={i} position={pos} rotation={[Math.PI / 2, 0, 0]}>
<cylinderGeometry args={[0.15, 0.15, 0.12, 12]} />
<meshStandardMaterial color="#333" />
</mesh>
))}
</group>
);
}
// ===== 地面网格 =====
function FloorGrid() {
return (
<group position={[0, -0.5, 0]}>
{/* 地面 */}
<mesh rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[50, 50]} />
<meshStandardMaterial color="#0a0e1f" />
</mesh>
{/* 网格线 */}
<Grid
args={[50, 50]}
cellSize={2}
cellColor="#1a2040"
sectionColor="#0066ff"
sectionSize={10}
/>
</group>
);
}
// ===== 主场景组件(导出) =====
export default function Scene3D({ deviceData }) {
return (
<Canvas
camera={{ position: [15, 12, 15], fov: 50 }}
style={{ width: '100%', height: '100%' }}
>
{/* 灯光 */}
<ambientLight intensity={0.4} />
<directionalLight position={[10, 20, 10]} intensity={0.8} />
<directionalLight position={[-10, 10, -10]} intensity={0.3} color="#6c5ce7" />
{/* 地面 */}
<FloorGrid />
{/* 机械臂 × 3 */}
<RobotArm position={[-6, 0.5, -3]} status="online" temperature={47.3} />
<RobotArm position={[0, 0.5, -3]} status="online" temperature={45.8} />
<RobotArm position={[6, 0.5, -3]} status="error" temperature={52.1} />
{/* 传送带 */}
<Conveyor position={[0, 0, 3]} />
{/* AGV小车 */}
<AGV position={[0, 0, 0]} />
{/* 鼠标控制 */}
<OrbitControls
enablePan={true}
enableZoom={true}
minPolarAngle={Math.PI / 6}
maxPolarAngle={Math.PI / 2.2}
minDistance={5}
maxDistance={40}
/>
</Canvas>
);
}
2.2 集成到App.js
在 App.js 中替换占位元素:
javascript
import Scene3D from './Scene3D';
// 在 App 函数里,替换 scene-container 的内容:
<div className="scene-container">
<Scene3D deviceData={[]} />
</div>
运行验证
bash
npm start
# 看到:3D工厂场景,有机械臂在动,传送带上有东西移动,AGV在转圈
# 能用鼠标旋转/缩放视角
阶段三:后端服务 + 数据库
3.1 后端项目搭建
bash
# 在项目根目录创建后端
cd Desktop/simulation-screen
mkdir backend
cd backend
npm init -y
npm install express socket.io cors dotenv
3.2 核心后端代码
backend/server.js:
javascript
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const cors = require('cors');
const app = express();
const httpServer = http.createServer(app);
// Socket.IO初始化
const io = new Server(httpServer, {
cors: { origin: '*' }
});
// 中间件
app.use(cors());
app.use(express.json());
// ===== 模拟数据(先不用数据库)=====
let devices = [
{ id: 'ROB-001', name: '机械臂1', type: 'robot_arm', status: 'online', temperature: 47.3, x: -6, y: 0, z: -3 },
{ id: 'ROB-002', name: '机械臂2', type: 'robot_arm', status: 'online', temperature: 45.8, x: 0, y: 0, z: -3 },
{ id: 'ROB-003', name: '机械臂3', type: 'robot_arm', status: 'online', temperature: 44.2, x: 6, y: 0, z: -3 },
{ id: 'CONV-001', name: '传送带1', type: 'conveyor', status: 'online', speed: 1.5, x: 0, y: 0, z: 3 },
{ id: 'AGV-001', name: 'AGV小车', type: 'agv', status: 'online', battery: 85, x: 5, y: 0, z: 5 }
];
// ===== REST API =====
// 获取所有设备
app.get('/api/devices', (req, res) => {
res.json({ success: true, data: devices });
});
// 获取单个设备
app.get('/api/devices/:id', (req, res) => {
const device = devices.find(d => d.id === req.params.id);
if (device) {
res.json({ success: true, data: device });
} else {
res.status(404).json({ success: false, error: '设备未找到' });
}
});
// 获取系统概览
app.get('/api/overview', (req, res) => {
res.json({
success: true,
data: {
totalDevices: devices.length,
onlineDevices: devices.filter(d => d.status === 'online').length,
production: 5000 + Math.floor(Math.random() * 500),
energy: (7.5 + Math.random() * 1.5).toFixed(1),
efficiency: (88 + Math.random() * 10).toFixed(1)
}
});
});
// ===== WebSocket实时推送 =====
io.on('connection', (socket) => {
console.log('📱 客户端连接:', socket.id);
// 连接时发送当前数据
socket.emit('devices-update', devices);
socket.on('disconnect', () => {
console.log('📴 客户端断开:', socket.id);
});
});
// ===== 数据模拟器(每2秒更新一次)=====
setInterval(() => {
devices.forEach(device => {
if (device.type === 'robot_arm') {
device.temperature += (Math.random() - 0.5) * 2; // 温度波动
device.temperature = Math.max(40, Math.min(55, device.temperature)); // 限制范围
}
if (device.type === 'agv') {
device.battery -= 0.1; // 电池缓慢消耗
if (device.battery < 20) device.battery = 100; // 模拟充电
}
});
// 广播给所有客户端
io.emit('devices-update', devices);
// 随机生成告警(5%概率)
if (Math.random() < 0.05) {
const randomDevice = devices[Math.floor(Math.random() * devices.length)];
const alert = {
id: Date.now(),
device: randomDevice.id,
msg: ['温度超过警戒值', '振动幅度异常', '速度偏离标准'][Math.floor(Math.random() * 3)],
time: new Date().toLocaleTimeString()
};
io.emit('new-alert', alert);
}
}, 2000);
// ===== 启动服务器 =====
const PORT = 3001;
httpServer.listen(PORT, () => {
console.log(`🚀 后端服务启动: http://localhost:${PORT}`);
});
backend/package.json 运行命令:
bash
cd backend
node server.js
# 看到: 🚀 后端服务启动: http://localhost:3001
3.3 前端连接WebSocket
在 App.js 中添加实时连接:
javascript
import React, { useState, useEffect } from 'react';
import { io } from 'socket.io-client';
import './App.css';
import Scene3D from './Scene3D';
import dayjs from 'dayjs';
// 连接后端
const socket = io('http://localhost:3001');
function App() {
const [currentTime, setCurrentTime] = useState(dayjs().format('YYYY-MM-DD HH:mm:ss'));
const [devices, setDevices] = useState([]);
const [alerts, setAlerts] = useState([]);
const [stats, setStats] = useState({
production: 5247,
energy: 8.3,
efficiency: 92.4,
onlineDevices: 0
});
// 更新时间
useEffect(() => {
const timer = setInterval(() => {
setCurrentTime(dayjs().format('YYYY-MM-DD HH:mm:ss'));
}, 1000);
return () => clearInterval(timer);
}, []);
// 监听WebSocket事件
useEffect(() => {
// 收到设备数据更新
socket.on('devices-update', (data) => {
setDevices(data);
setStats(prev => ({
...prev,
onlineDevices: data.filter(d => d.status === 'online').length
}));
});
// 收到告警
socket.on('new-alert', (alert) => {
setAlerts(prev => [alert, ...prev].slice(0, 8)); // 最多保留8条
});
return () => {
socket.off('devices-update');
socket.off('new-alert');
};
}, []);
// 渲染部分和之前一样,只是把Scene3D传入devices
return (
<div className="app-container">
<header className="header">
<div className="header-title">🏭 智能工厂虚拟仿真系统</div>
<div className="header-right">
<div className="status-badge">
<span className="status-dot"></span>
系统运行中
</div>
<span style={{ color: '#8899aa', fontSize: '14px' }}>{currentTime}</span>
</div>
</header>
<div className="main-content">
{/* 左侧 - 数据卡片 */}
<div className="side-panel">
<div className="data-card" style={{ borderLeftColor: '#0066ff' }}>
<div className="card-label">📦 今日产量</div>
<div className="card-value">{stats.production.toLocaleString()}<span style={{ fontSize: '16px', color: '#8899aa' }}> 件</span></div>
<div className="card-trend trend-up">↑ +5.2% 较昨日</div>
</div>
<div className="data-card" style={{ borderLeftColor: '#ffa502' }}>
<div className="card-label">⚡ 能源消耗</div>
<div className="card-value">{stats.energy}<span style={{ fontSize: '16px', color: '#8899aa' }}> kW</span></div>
<div className="card-trend trend-up">↓ -2.1% 较昨日</div>
</div>
<div className="data-card" style={{ borderLeftColor: '#26de81' }}>
<div className="card-label">📊 运行效率</div>
<div className="card-value">{stats.efficiency}<span style={{ fontSize: '16px', color: '#8899aa' }}> %</span></div>
<div className="card-trend trend-up">↑ +1.5% 较昨日</div>
</div>
<div className="data-card" style={{ borderLeftColor: '#6c5ce7' }}>
<div className="card-label">🔌 在线设备</div>
<div className="card-value">{stats.onlineDevices}<span style={{ fontSize: '16px', color: '#8899aa' }}> / {devices.length}</span></div>
<div className="card-trend trend-up">↑ 全部正常</div>
</div>
</div>
{/* 中间 - 3D场景 */}
<div className="scene-container">
<Scene3D deviceData={devices} />
</div>
{/* 右侧 - 告警 */}
<div className="side-panel">
<div className="alert-panel">
<div className="alert-title">🚨 实时告警 ({alerts.length})</div>
<div className="alert-list">
{alerts.length > 0 ? alerts.map(alert => (
<div key={alert.id} className="alert-item">
<div className="alert-device">⚠️ {alert.device}</div>
<div className="alert-msg">{alert.msg}</div>
<div className="alert-time">🕐 {alert.time}</div>
</div>
)) : (
<div className="no-alert">✅ 无告警,系统正常</div>
)}
</div>
</div>
</div>
</div>
{/* 底部图表占位 */}
<div className="bottom-charts">
<div className="chart-box" style={{ color: '#8899aa', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>📈 产量趋势(阶段四实现)</div>
<div className="chart-box" style={{ color: '#8899aa', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>⚡ 能耗分析(阶段四实现)</div>
<div className="chart-box" style={{ color: '#8899aa', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>📉 效率走势(阶段四实现)</div>
</div>
</div>
);
}
export default App;
阶段四:实时图表
4.1 创建图表组件
创建 src/Charts.js:
javascript
import React, { useEffect, useRef } from 'react';
import * as echarts from 'echarts';
export function LineChart({ title, data, color = '#0066ff', unit = '' }) {
const ref = useRef(null);
useEffect(() => {
if (!ref.current) return;
const chart = echarts.init(ref.current);
const option = {
backgroundColor: 'transparent',
title: {
text: title,
textStyle: { color: '#fff', fontSize: 14 },
left: 10
},
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(8,11,26,0.9)',
borderColor: color,
textStyle: { color: '#fff', fontSize: 12 }
},
grid: { left: '8%', right: '5%', bottom: '15%', top: '30%' },
xAxis: {
type: 'category',
data: data.map(d => d.time),
axisLine: { lineStyle: { color: '#2a3a5c' } },
axisLabel: { color: '#5a6a8a', fontSize: 10 }
},
yAxis: {
type: 'value',
axisLine: { lineStyle: { color: '#2a3a5c' } },
axisLabel: { color: '#5a6a8a', fontSize: 10 },
splitLine: { lineStyle: { color: '#1a2540', type: 'dashed' } }
},
series: [{
type: 'line',
data: data.map(d => d.value),
smooth: true,
lineStyle: { color: color, width: 2 },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: color + '60' },
{ offset: 1, color: color + '05' }
])
},
itemStyle: { color: color }
}]
};
chart.setOption(option);
const resize = () => chart.resize();
window.addEventListener('resize', resize);
return () => {
window.removeEventListener('resize', resize);
chart.dispose();
};
}, [data, title, color, unit]);
return <div ref={ref} style={{ width: '100%', height: '100%' }} />;
}
4.2 在App.js中使用图表
添加图表数据状态和集成:
javascript
// 在App.js顶部添加import
import { LineChart } from './Charts';
// 在状态声明中添加
const [chartProduction, setChartProduction] = useState([]);
const [chartEnergy, setChartEnergy] = useState([]);
// 在WebSocket监听中,每次收到数据就累积图表数据
socket.on('devices-update', (data) => {
const now = dayjs().format('HH:mm:ss');
setChartProduction(prev => [...prev, { time: now, value: 150 + Math.random() * 50 }].slice(-15));
setChartEnergy(prev => [...prev, { time: now, value: 7 + Math.random() * 2 }].slice(-15));
});
// 替换底部占位元素
<div className="bottom-charts">
<div className="chart-box">
<LineChart title="📈 产量趋势" data={chartProduction} color="#0066ff" />
</div>
<div className="chart-box">
<LineChart title="⚡ 能耗走势" data={chartEnergy} color="#ffa502" />
</div>
<div className="chart-box">
<LineChart title="📊 效率分析" data={chartProduction.map(d => ({ ...d, value: 88 + Math.random() * 8 }))} color="#26de81" />
</div>
</div>
运行最终版本
bash
# 终端1:启动后端
cd backend
node server.js
# 终端2:启动前端
cd ..
npm start
# 浏览器打开 http://localhost:3000
# 看到:完整大屏,3D场景在动,数据实时更新,图表在变
✅ 整体验证清单
□ 页面布局正确(顶栏+左右面板+中间3D+底部图表)
□ 3D场景有机械臂、传送带、AGV
□ 设备在动态运行(机械臂在动,AGV在移动)
□ 左侧数据卡片显示正确
□ 右侧告警偶尔弹出新条目
□ 底部图表随时间滚动更新
□ 时间实时刷新
□ 鼠标可以旋转3D场景