refactor: 页眉由 App 统一注入(ScreenLayout),三页共用一致页眉

- 新增 ScreenLayout 布局路由:注入共享 PageHeader + 全局左右键循环导航
- PageHeader 无 props,按路由自动识别状态(数据实时/3D 实时/媒体播放)
- DataScreen / DigitalTwin / MediaScreen 移除各自页眉,避免重复
- 页脚按页面按需保留(数据大屏 + 媒体轮播,数字孪生无)
- 管理后台不参与导航循环;旧 /screen2 路由移除
- 补齐 MediaScreen 音量/播放/翻页图标与浅色主题样式
This commit is contained in:
Pine
2026-08-17 20:08:45 +08:00
parent b08055f8a5
commit 84b3f12146
11 changed files with 1131 additions and 47 deletions
+47
View File
@@ -0,0 +1,47 @@
import { useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
/* =========================================================
页面循环导航 —— 左右方向键切换大屏页面
顺序:数据大屏 → 数字孪生 → 媒体轮播(循环)
管理后台不参与循环切换,仅在登录后进入
← 向左切换 · → 向右切换
========================================================= */
export const PAGE_ORDER = [
{ path: '/', label: '数据大屏' },
{ path: '/twin', label: '数字孪生' },
{ path: '/screen', label: '媒体轮播' },
];
/**
* 左右方向键循环切换页面。
* 在任意页面使用;当前路径不在 PAGE_ORDER 中时不响应。
*/
export function usePageNav() {
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
const onKey = (e) => {
const idx = PAGE_ORDER.findIndex((p) => p.path === location.pathname);
if (idx === -1) return;
const n = PAGE_ORDER.length;
if (e.key === 'ArrowRight') {
navigate(PAGE_ORDER[(idx + 1) % n].path);
} else if (e.key === 'ArrowLeft') {
navigate(PAGE_ORDER[(idx - 1 + n) % n].path);
}
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [navigate, location.pathname]);
return null;
}
/** 获取当前页在循环中的位置(-1 表示不在循环中) */
export function usePageIndex() {
const location = useLocation();
return PAGE_ORDER.findIndex((p) => p.path === location.pathname);
}
+132
View File
@@ -0,0 +1,132 @@
import { useState, useEffect } from 'react';
/* =========================================================
园区模拟数据引擎(共享)—— 供数据大屏 / 媒体播放页复用
每 2.2s 跳动一次,保持页眉与园区概览页脚数据一致
========================================================= */
export const fmtInt = (n) => Math.round(n).toLocaleString('zh-CN');
export const fmtWan = (n) => (n >= 10000 ? (n / 10000).toFixed(2) + '亿' : n >= 100 ? n.toFixed(1) + '万' : n.toFixed(1));
const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
const nowTime = () => new Date().toLocaleTimeString('zh-CN', { hour12: false });
const TOOL_NAMES = ['文档生成', '数据查询', '图像创作', '代码执行', '语音合成'];
const MODEL_NAMES = ['DeepSeek-V3', '通义千问', '智谱 GLM-4', '豆包', '讯飞星火'];
const COMPANY_NAMES = [
'云南派音人工智能科技', '米勒克尔蓝宝石珠宝', '中泰研学合作', '云南宸中低空经济',
'昆明智海银高文化科技', '云南廷秀文旅康养', '瀚颖AI+教育信息咨询',
'仰光客厅', '云南上古绝学文化', '中越生物医疗', '酷享野农AI农业',
'滇缅国际设计', '昆明舒诺生物科技', '达岸教育管理', '花仙子园艺肥料',
'研X同行者网络', '鬼才明AI创意工作室', '朵哈·玫瑰特色产业链', '昆明云韵体育',
'南菌优培食用菌', '五华区丽裳文化', '云南星瑞航空', '综合直播私域平台',
'昆明屿澈电商', '蓝智科技', '云品出滇·纸享万家', '启元人工智能科技',
];
function makeSeries(base, growth, noise, n = 30) {
const arr = [];
let v = base;
for (let i = 0; i < n; i++) {
v = v * (1 + growth) + (Math.random() - 0.5) * noise;
arr.push(Math.max(1, Math.round(v * 100) / 100));
}
return arr;
}
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() },
];
}
function initSnapshot() {
return {
t: 0,
token: { today: 128.64, total: 12840, rate: 84.6, series: makeSeries(82, 0.012, 9) },
tools: { today: 3568, total: 365204, success: 98.7 },
projects: { inPark: 158, cum: 208, todayNew: 2 },
jobs: { total: 2186, todayNew: 3 },
revenue: { today: 38.6, total: 20800, growth: 8.2, series: makeSeries(30, 0.006, 4) },
park: { devices: 98.6, energy: 386, people: 127, desk: 76, meeting: 3, nodes: 12 },
feed: initFeed(),
};
}
function nextSnapshot(s) {
const r = Math.random;
const tDeltaWan = +(0.26 + r() * 0.34).toFixed(2);
const token = {
today: +(s.token.today + tDeltaWan).toFixed(2),
total: +(s.token.total + tDeltaWan).toFixed(2),
rate: +(76 + r() * 20).toFixed(1),
};
const toolDelta = Math.round(13 + r() * 22);
const tools = {
today: s.tools.today + toolDelta,
total: s.tools.total + toolDelta,
success: +(98.1 + r() * 1.2).toFixed(1),
};
const revDelta = +(0.6 + r() * 1.7).toFixed(1);
const revenue = {
...s.revenue,
today: +(s.revenue.today + revDelta).toFixed(1),
total: +(s.revenue.total + revDelta).toFixed(1),
growth: +(7.2 + r() * 2.2).toFixed(1),
};
const park = {
devices: +(97.6 + r() * 1.6).toFixed(1),
energy: Math.round(320 + r() * 130),
people: Math.round(80 + r() * 95),
desk: Math.round(62 + r() * 24),
meeting: Math.round(2 + r() * 4),
nodes: 12,
};
const projects = { ...s.projects };
if (r() < 0.055) projects.todayNew += 1;
if (r() < 0.035) { projects.inPark += 1; projects.cum += 1; }
const jobs = { ...s.jobs };
if (r() < 0.08) jobs.todayNew += 1;
if (r() < 0.05) jobs.total += 1;
let tSeries = s.token.series.slice();
tSeries[tSeries.length - 1] = +(tSeries[tSeries.length - 1] + tDeltaWan).toFixed(2);
if (s.t % 12 === 11) tSeries = [...tSeries.slice(1), +(72 + r() * 30).toFixed(2)];
token.series = tSeries;
let rSeries = s.revenue.series.slice();
rSeries[rSeries.length - 1] = +(rSeries[rSeries.length - 1] + revDelta).toFixed(1);
if (s.t % 12 === 11) rSeries = [...rSeries.slice(1), +(26 + r() * 9).toFixed(1)];
revenue.series = rSeries;
const feed = s.t % 3 === 2 ? [genEvent(), ...s.feed].slice(0, 7) : s.feed;
return { ...s, t: s.t + 1, token, tools, revenue, park, projects, jobs, feed };
}
export function useParkSim() {
const [snap, setSnap] = useState(initSnapshot);
useEffect(() => {
const iv = setInterval(() => setSnap((p) => nextSnapshot(p)), 2200);
return () => clearInterval(iv);
}, []);
return snap;
}