feat: 数字孪生独立页面与路由切换
- DigitalTwin.jsx:全屏 3D 建筑 + 企业面板(分区切换/房间定位/实时动态) - App.jsx 注册 /twin 路由 - 左右方向键:主大屏 → 数字孪生 → 管理后台 循环切换
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import Admin from './pages/Admin';
|
||||
import Screen from './pages/Screen';
|
||||
import DataScreen from './pages/DataScreen';
|
||||
import DigitalTwin from './pages/DigitalTwin';
|
||||
import './styles/tokens.css';
|
||||
|
||||
export default function App() {
|
||||
@@ -9,6 +11,8 @@ export default function App() {
|
||||
<Routes>
|
||||
<Route path="/admin" element={<Admin />} />
|
||||
<Route path="/screen" element={<Screen />} />
|
||||
<Route path="/screen2" element={<DataScreen />} />
|
||||
<Route path="/twin" element={<DigitalTwin />} />
|
||||
<Route path="*" element={<Navigate to="/screen" replace />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import { useState, useEffect, lazy, Suspense } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import '../styles/datascreen.css';
|
||||
import Icon from '../components/Icons';
|
||||
import PageHeader from '../components/PageHeader';
|
||||
|
||||
/* 3D 园区建筑懒加载 */
|
||||
const Building3D = lazy(() => import('../components/Building3D'));
|
||||
|
||||
/* =========================================================
|
||||
园区数字孪生 · 独立页面(/twin)
|
||||
全屏 3D 建筑模型 + 实时动态 + 企业分布
|
||||
左右方向键:← 返回数据大屏 · → 进入管理后台
|
||||
========================================================= */
|
||||
|
||||
/* ---------- 企业数据(名称 / 区域 / 房间号) ---------- */
|
||||
const COMPANIES = [
|
||||
{ name: '云南派音人工智能科技', zone: '加速区', room: 'A1', color: '#4c8dff' },
|
||||
{ name: '米勒克尔蓝宝石珠宝', zone: '加速区', room: 'A2', color: '#4c8dff' },
|
||||
{ name: '中泰研学合作', zone: '加速区', room: 'A3', color: '#4c8dff' },
|
||||
{ name: '云南宸中低空经济', zone: '加速区', room: 'A4', color: '#4c8dff' },
|
||||
{ name: '昆明智海银高文化科技', zone: '加速区', room: 'A5', color: '#4c8dff' },
|
||||
{ name: 'AI机器人大模型训练', zone: '加速区', room: 'A6', color: '#4c8dff' },
|
||||
{ name: '云南廷秀文旅康养', zone: '加速区', room: 'A7', color: '#4c8dff' },
|
||||
{ name: '瀚颖AI+教育信息咨询', zone: '加速区', room: 'A8', color: '#4c8dff' },
|
||||
{ name: '云南大学AI+创业平台', zone: '加速区', room: 'A9', color: '#4c8dff' },
|
||||
{ name: '仰光客厅', zone: '国际区', room: 'I1', color: '#22d3ee' },
|
||||
{ name: '云南上古绝学文化', zone: '国际区', room: 'I2', color: '#22d3ee' },
|
||||
{ name: '中越生物医疗', zone: '国际区', room: 'I3', color: '#22d3ee' },
|
||||
{ name: '酷享野农AI农业', zone: '国际区', room: 'I4', color: '#22d3ee' },
|
||||
{ name: '滇缅国际设计', zone: '国际区', room: 'I5', color: '#22d3ee' },
|
||||
{ name: '昆明舒诺生物科技', zone: '国际区', room: 'I6', color: '#22d3ee' },
|
||||
{ name: '达岸教育管理', zone: '国际区', room: 'I7', color: '#22d3ee' },
|
||||
{ name: 'Facebook越南跨境电商', zone: '成长区', room: 'G1', color: '#34d399' },
|
||||
{ name: '研X同行者网络', zone: '成长区', room: 'G2', color: '#34d399' },
|
||||
{ name: '朵哈·玫瑰特色产业链', zone: '成长区', room: 'G3', color: '#34d399' },
|
||||
{ name: '昆明云韵体育', zone: '成长区', room: 'G4', color: '#34d399' },
|
||||
{ name: '南菌优培食用菌', zone: '成长区', room: 'G5', color: '#34d399' },
|
||||
{ name: '五华区丽裳文化', zone: '成长区', room: 'G6', color: '#34d399' },
|
||||
{ name: '云南星瑞航空', zone: '成长区', room: 'G7', color: '#34d399' },
|
||||
{ name: '综合直播私域平台', zone: '成长区', room: 'G8', color: '#34d399' },
|
||||
{ name: '园区管理办公室', zone: '园区管理', room: 'M', color: '#fbbf24' },
|
||||
];
|
||||
|
||||
const ZONES = ['加速区', '国际区', '成长区'];
|
||||
|
||||
export default function DigitalTwin() {
|
||||
const navigate = useNavigate();
|
||||
const [feed, setFeed] = useState(initFeed());
|
||||
const [activeZone, setActiveZone] = useState('加速区');
|
||||
const [selected, setSelected] = useState(null);
|
||||
|
||||
// 实时动态滚动
|
||||
useEffect(() => {
|
||||
const iv = setInterval(() => {
|
||||
setFeed((prev) => [genEvent(), ...prev].slice(0, 6));
|
||||
}, 2600);
|
||||
return () => clearInterval(iv);
|
||||
}, []);
|
||||
|
||||
// 左右方向键:← 返回数据大屏 · → 进入管理后台
|
||||
useEffect(() => {
|
||||
const onKey = (e) => {
|
||||
if (e.key === 'ArrowLeft') navigate('/screen2');
|
||||
else if (e.key === 'ArrowRight') navigate('/admin');
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [navigate]);
|
||||
|
||||
const list = COMPANIES.filter((c) => c.zone === activeZone);
|
||||
const zoneColors = { 加速区: '#4c8dff', 国际区: '#22d3ee', 成长区: '#34d399' };
|
||||
|
||||
return (
|
||||
<div className="bd-twin-page">
|
||||
{/* 顶栏(共享组件,与数据大屏风格一致) */}
|
||||
<PageHeader
|
||||
nav={{ to: '/screen2', label: '数据大屏', icon: 'chevron-left', title: '按 ← 返回数据大屏' }}
|
||||
status="3D 实时"
|
||||
/>
|
||||
|
||||
{/* 主体:3D 为主 + 侧边栏 */}
|
||||
<main className="bd-twin-main">
|
||||
{/* 3D 场景 */}
|
||||
<section className="bd-twin-stage">
|
||||
<Suspense fallback={<div className="bd3d-loading"><i />加载三维园区模型…</div>}>
|
||||
<Building3D />
|
||||
</Suspense>
|
||||
{/* 图例 */}
|
||||
<div className="bd-twin-legend">
|
||||
<span><i style={{ background: '#4c8dff' }} />加速区</span>
|
||||
<span><i style={{ background: '#22d3ee' }} />国际区</span>
|
||||
<span><i style={{ background: '#34d399' }} />成长区</span>
|
||||
<span><i style={{ background: '#fbbf24' }} />园区管理</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 右侧企业面板 */}
|
||||
<aside className="bd-twin-side">
|
||||
{/* 分区切换 */}
|
||||
<div className="bd-twin-zones">
|
||||
{ZONES.map((z) => (
|
||||
<button
|
||||
key={z}
|
||||
className={`bd-twin-zone${activeZone === z ? ' active' : ''}`}
|
||||
style={activeZone === z ? { borderColor: zoneColors[z], color: zoneColors[z] } : undefined}
|
||||
onClick={() => { setActiveZone(z); setSelected(null); }}
|
||||
>
|
||||
{z}
|
||||
<em>{COMPANIES.filter((c) => c.zone === z).length} 家</em>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 企业列表 */}
|
||||
<div className="bd-twin-list">
|
||||
{list.map((c) => (
|
||||
<button
|
||||
key={c.room}
|
||||
className={`bd-twin-item${selected && selected.room === c.room ? ' active' : ''}`}
|
||||
onClick={() => setSelected(c)}
|
||||
>
|
||||
<span className="bd-twin-room" style={{ color: c.color, borderColor: c.color }}>{c.room}</span>
|
||||
<span className="bd-twin-name">{c.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 选中企业详情 */}
|
||||
{selected && (
|
||||
<div className="bd-twin-detail">
|
||||
<div className="bd-twin-detail-head">
|
||||
<b>{selected.name}</b>
|
||||
<span style={{ color: zoneColors[activeZone] }}>{selected.zone}</span>
|
||||
</div>
|
||||
<div className="bd-twin-detail-meta">
|
||||
<span>房间 <b>{selected.room}</b></span>
|
||||
<span>状态 <b className="ok">在驻</b></span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 实时动态 */}
|
||||
<div className="bd-card bd-feed-card bd-twin-feed">
|
||||
<div className="bd-card-head">
|
||||
<div className="bd-title">园区实时动态</div>
|
||||
<span className="bd-pill-tag red">● LIVE</span>
|
||||
</div>
|
||||
<div className="bd-feed">
|
||||
{feed.map((e) => (
|
||||
<div className="bd-feed-item" key={e.id}>
|
||||
<span className="bd-feed-ico"><Icon name={e.icon} size={15} /></span>
|
||||
<span className="bd-feed-text">{e.text}</span>
|
||||
<span className="bd-feed-time">{e.time}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- 工具 ---------- */
|
||||
|
||||
const nowTime = () => new Date().toLocaleTimeString('zh-CN', { hour12: false });
|
||||
const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
||||
|
||||
const TOOL_NAMES = ['文档生成', '数据查询', '图像创作', '代码执行', '语音合成'];
|
||||
const MODEL_NAMES = ['DeepSeek-V3', '通义千问', '智谱 GLM-4', '豆包', '讯飞星火'];
|
||||
const COMPANY_NAMES = COMPANIES.map((c) => c.name);
|
||||
|
||||
function genEvent() {
|
||||
const r = Math.random;
|
||||
const pool = [
|
||||
{ icon: 'bolt', text: `AI 推理任务完成 · 消耗 ${Math.round(800 + r() * 9000).toLocaleString()} tokens(${pick(MODEL_NAMES)})` },
|
||||
{ icon: 'wrench', text: `工具「${pick(TOOL_NAMES)}」被调用 ${Math.round(10 + r() * 90)} 次` },
|
||||
{ icon: 'building', text: `「${pick(COMPANY_NAMES)}」提交入驻申请 · 进入评审流程` },
|
||||
{ icon: 'users', text: `「${pick(COMPANY_NAMES)}」新增招聘岗位 ${Math.round(1 + r() * 5)} 个` },
|
||||
{ icon: 'coin', text: `园区企业完成一笔 ¥${(0.5 + r() * 9).toFixed(1)}万 交易` },
|
||||
{ icon: 'robot', text: `「${pick(MODEL_NAMES)}」模型完成一次微调任务` },
|
||||
];
|
||||
const e = pick(pool);
|
||||
return { id: Date.now() + Math.random(), icon: e.icon, text: e.text, time: nowTime() };
|
||||
}
|
||||
|
||||
function initFeed() {
|
||||
return [
|
||||
{ id: 1, icon: 'bolt', text: '云南派音AI 完成音频向量嵌入任务 · 消耗 12,480 tokens', time: nowTime() },
|
||||
{ id: 2, icon: 'building', text: '「启元人工智能科技」通过评审 · 正式入驻 OPC 创业空间', time: nowTime() },
|
||||
{ id: 3, icon: 'users', text: '「昆明舒护安养老服务」新增招聘岗位 2 个', time: nowTime() },
|
||||
{ id: 4, icon: 'coin', text: '园区企业完成一笔 ¥3.6万 交易', time: nowTime() },
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user