虚拟仿真大屏系统——完整实施手册

目标读者 :想从头做一个虚拟仿真大屏的小白或初级开发者
阅读时间 :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场景
相关推荐
阿斯加德D11 小时前
天国:拯救 2风灵月影修改器下载(已汉化)2026最新版下载分享
测试工具·游戏·3d·游戏程序
Hi2024021714 小时前
Apollo CUDA-BEVFusion 高性能 3D 目标检测
人工智能·目标检测·3d
AZaLEan__14 小时前
CSS3:从 2D 变换到 3D 翻转
前端·3d·css3
CG_MAGIC18 小时前
3ds Max FloorGenerator 插件:快速生成地板木纹
3d·贴图·uv·建模教程·渲云渲染
亿源通科技1 天前
MPO 端面 3D 几何核心参数解读
3d
元让_vincent1 天前
AutoDL 上配置远程桌面运行 3DGS / SLAM 可视化:TurboVNC + XFCE + SSH 隧道完整可行流程
运维·3d·ssh
●VON1 天前
纯ArkUI实现7层拟物3D环形进度图:零依赖的视觉革命
服务器·3d·app·鸿蒙·von
阿斯加德D1 天前
《霍格沃茨之遗》风灵月影修改器下载(已汉化)2026最新版
人工智能·测试工具·游戏·3d·游戏程序
在下胡三汉1 天前
3dmax直接导入加载glb,gltf格式模型插件,支持导入动画材质贴图纹理,可以批量导入
3d·材质·贴图·gltf·glb