refactor: 页眉由 App 统一注入(ScreenLayout),三页共用一致页眉
- 新增 ScreenLayout 布局路由:注入共享 PageHeader + 全局左右键循环导航 - PageHeader 无 props,按路由自动识别状态(数据实时/3D 实时/媒体播放) - DataScreen / DigitalTwin / MediaScreen 移除各自页眉,避免重复 - 页脚按页面按需保留(数据大屏 + 媒体轮播,数字孪生无) - 管理后台不参与导航循环;旧 /screen2 路由移除 - 补齐 MediaScreen 音量/播放/翻页图标与浅色主题样式
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user