feat: 园区大屏前端(React+Vite+Tailwind+three.js+MQTT)

- 路由:数据大屏/数字孪生/AI/媒体轮播/语音/企业墙
- 多园区通用,园区账号登录显示本园区
This commit is contained in:
2026-08-23 22:34:45 +08:00
commit 28e233520b
60 changed files with 23189 additions and 0 deletions
+576
View File
@@ -0,0 +1,576 @@
import { useState, useEffect, useRef } from 'react';
import '../styles/datascreen.css';
import Icon from '../components/Icons';
import ParkOverviewStrip from '../components/ParkOverviewStrip';
import { useParkSim } from '../utils/parkData';
/* =========================================================
昆明市大学生创业园 · OPC 智能园区数字运营中心
—— 数据大屏(真实数据展示)
---------------------------------------------------------
数据口径(标准资料,多个权威来源):
· 《昆明市大学生创业园运行情况统计表(2026年7月).xls》(官方统计)
实际入驻 38 家 / 可容纳 49 个 / 累计投入 560 万 /
当年累计带动就业 213 人 / 当年生产经营总额 252.15 万 / 当年税利 1.03 万 / 面积 3000㎡
· 《附件1.省级创业孵化载体相关情况简介材料》(省级绩效累计口径,截至2026年5月)
累计孵化 181 家 / 在孵 31 家 / 累计就业 1,529 人 / 累计经营收入 21,911.14 万
· 《昆明市创业园宣传册.pdf》
2009 年成立 / 2012 年省级创业园 / 民航路 229 号 / 四大孵化区域
· 《入驻企业信息表》39 家名录(含创始人/简介/场地安排)
测算口径(AI 平台业务锚点,非政府统计口径):
· TOKEN:月均 200 亿 tokens → 日均 ≈ 6.67 亿,实时 ≈ 7,700 t/s,近30日累计 200 亿
· 工具调用:按每次调用约 15 万 tokens 折算
TOKEN/工具为基于真实锚点的测算演示,其余为真实统计值
========================================================= */
/* ---------- 模型占比(TOKEN 构成演示分布) ---------- */
const MODELS = [
{ name: 'DeepSeek', value: 38, color: '#4c8dff' },
{ name: '通义千问', value: 21, color: '#22d3ee' },
{ name: '智谱 GLM-4', value: 16, color: '#34d399' },
{ name: '豆包', value: 12, color: '#fbbf24' },
{ name: '讯飞星火', value: 8, color: '#a78bfa' },
{ name: '其他', value: 5, color: '#64748b' },
];
/* 工具调用占比(演示分布) */
const TOOL_RANK = [
{ name: '文档生成', value: 34 },
{ name: '数据查询', value: 27 },
{ name: '图像创作', value: 16 },
{ name: '代码执行', value: 13 },
{ name: '语音合成', value: 10 },
];
/* ---------- 工具函数 ---------- */
const fmtInt = (n) => Math.round(n).toLocaleString('zh-CN');
const fmtWan = (n) => (n >= 10000 ? (n / 10000).toFixed(2) + '亿' : n >= 100 ? n.toFixed(1) + '万' : n.toFixed(1));
/* 数据源:后端 /api/dashboard/snapshot(离线时本地模拟兜底),见 utils/parkData.js */
/* ---------- 通用 Hooks ---------- */
/* 数字平滑滚动(rAF 缓动;进度钳制 [0,1],避免边缘环境时间戳异常导致发散) */
function useAnimatedNumber(target, duration = 900) {
const [cur, setCur] = useState(target);
const curRef = useRef(target);
const raf = useRef();
useEffect(() => {
const from = curRef.current;
if (from === target) return;
const t0 = performance.now();
cancelAnimationFrame(raf.current);
const tick = (now) => {
const p = Math.min(1, Math.max(0, (now - t0) / duration));
const e = 1 - Math.pow(1 - p, 3);
const v = from + (target - from) * e;
curRef.current = v;
setCur(v);
if (p < 1) raf.current = requestAnimationFrame(tick);
};
raf.current = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf.current);
}, [target, duration]);
return cur;
}
/* ---------- 通用小组件 ---------- */
function CardHead({ title, right, icon }) {
return (
<div className="bd-card-head">
<div className="bd-title">
{icon && <span className="bd-title-ico"><Icon name={icon} size={16} /></span>}
<span>{title}</span>
</div>
{right && <div className="bd-head-right">{right}</div>}
</div>
);
}
function Pill({ children, tone = 'blue' }) {
return <span className={`bd-pill-tag ${tone}`}>{children}</span>;
}
/* 面积图 + 折线(viewBox 高度动态匹配容器,满宽绘制、无留白错位) */
function AreaChart({ series, color, height = 100, unit = '', lastLabel }) {
const W = 300, PL = 8, PR = 8, PT = 26, PB = 26;
const ref = useRef(null);
const [vh, setVh] = useState(height); // viewBox 高度(按容器宽高比计算)
useEffect(() => {
const el = ref.current;
if (!el) return;
const update = () => {
const w = el.clientWidth, h = el.clientHeight;
if (w > 0 && h > 0) setVh(Math.max(40, Math.round((W * h) / w)));
};
update();
const ro = new ResizeObserver(update);
ro.observe(el);
return () => ro.disconnect();
}, []);
const H = vh;
const min = Math.min(...series), max = Math.max(...series);
const lo = min * 0.9, hi = max * 1.08, span = hi - lo || 1;
const px = (i) => PL + (i / (series.length - 1)) * (W - PL - PR);
const py = (v) => H - PB - ((v - lo) / span) * (H - PT - PB);
const line = series.map((v, i) => `${px(i).toFixed(1)},${py(v).toFixed(1)}`).join(' ');
const area = `${line} ${px(series.length - 1).toFixed(1)},${(H - PB).toFixed(1)} ${px(0).toFixed(1)},${(H - PB).toFixed(1)}`;
const gid = useRef('g' + Math.random().toString(36).slice(2, 8)).current;
return (
<svg ref={ref} viewBox={`0 0 ${W} ${H}`} className="bd-chart-svg">
{[0.25, 0.5, 0.75].map((t) => (
<line key={t} x1={PL} x2={W - PR} y1={PT + t * (H - PT - PB)} y2={PT + t * (H - PT - PB)}
stroke="rgba(130,160,205,0.10)" strokeWidth="1" strokeDasharray="3 5" />
))}
<defs>
<linearGradient id={gid} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={color} stopOpacity="0.34" />
<stop offset="100%" stopColor={color} stopOpacity="0.02" />
</linearGradient>
</defs>
<polygon points={area} fill={`url(#${gid})`} />
<polyline points={line} fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
style={{ filter: `drop-shadow(0 0 5px ${color}88)` }} />
<circle cx={px(series.length - 1)} cy={py(series[series.length - 1])} r="3" fill={color} />
<text x={px(0)} y={H - 7} fontSize="8.5" fill="#8fa1ba">30 天前</text>
<text x={px(series.length - 1)} y={H - 7} textAnchor="end" fontSize="8.5" fill="#8fa1ba">今日</text>
{lastLabel && (
<text x={W - PR} y={PT - 9} textAnchor="end" fontSize="11" fontWeight="700" fill="#1d2c44">{lastLabel}</text>
)}
</svg>
);
}
/* 环形图 */
function Donut({ items, size = 148, thickness = 18, centerTop, centerBottom }) {
const total = items.reduce((a, b) => a + b.value, 0);
const R = (size - thickness) / 2;
const C = 2 * Math.PI * R;
const cx = size / 2;
let acc = 0;
return (
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} className="bd-donut">
<circle cx={cx} cy={cx} r={R} fill="none" stroke="rgba(130,160,205,0.10)" strokeWidth={thickness} />
{items.map((it, i) => {
const frac = it.value / total;
const dash = Math.max(0.5, frac * C - 1.5);
const off = -acc * C;
acc += frac;
return (
<circle key={i} cx={cx} cy={cx} r={R} fill="none" stroke={it.color} strokeWidth={thickness}
strokeDasharray={`${dash} ${C - dash}`} strokeDashoffset={off}
transform={`rotate(-90 ${cx} ${cx})`}
strokeLinecap="round"
style={{ filter: `drop-shadow(0 0 4px ${it.color}66)` }} />
);
})}
<text x={cx} y={cx - 3} textAnchor="middle" className="bd-donut-total">{centerTop}</text>
<text x={cx} y={cx + 15} textAnchor="middle" className="bd-donut-label">{centerBottom}</text>
</svg>
);
}
/* 横向排行条 */
function HBar({ items, color = '#4cc2ff', unit = '%' }) {
const max = Math.max(...items.map((i) => i.value));
return (
<div className="bd-hbars">
{items.map((it, i) => (
<div className="bd-hbar" key={i}>
<span className="bd-hbar-name">{it.name}</span>
<div className="bd-hbar-track">
<div className="bd-hbar-fill" style={{ width: `${(it.value / max) * 100}%`, background: `linear-gradient(90deg, ${color}44, ${color})` }} />
</div>
<span className="bd-hbar-val">{it.value}{unit}</span>
</div>
))}
</div>
);
}
/* =========================================================
面板组件
========================================================= */
/* ---------- KPI 核心指标带(5 大必须指标) ---------- */
function KpiCard({ icon, label, value, unit, sub, tone }) {
const v = useAnimatedNumber(value);
return (
<div className={`bd-kpi ${tone}`}>
<div className="bd-kpi-ico"><Icon name={icon} size={22} /></div>
<div className="bd-kpi-body">
<div className="bd-kpi-label">{label}</div>
<div className="bd-kpi-value">
{tone === 'revenue'
? <span>{fmtWan(v)}</span>
: <span>{fmtInt(v)}</span>}
{unit && <em>{unit}</em>}
</div>
<div className="bd-kpi-sub">{sub}</div>
</div>
<span className="bd-kpi-glow" />
</div>
);
}
/* =========================================================
园区今日概览(标题右侧内联运行状态 + 五个指标一行)
┌──────────────────────────────────────────────┐
│ 园区今日概览 · [6 项运行状态内联] · 实时刷新 │
├──────┬──────┬──────┬──────┬──────┤
│ TOKEN│ 营收 │ 入园 │ 工具 │ 就业 │ ← 五个卡片一行
└──────┴──────┴──────┴──────┴──────┘
(园区实时动态 → 整体页面右下方)
========================================================= */
function TodayOverview({ d }) {
/* 园区运行状态 —— 内联在标题右侧,不作为独立卡片(真实数据) */
const status = [
{ icon: 'gauge', label: '成立时间', value: `${d.park.founded} 年`, color: '#17b26a' },
{ icon: 'trophy', label: '省级创业园', value: `${d.park.provinceLevel} 年认定`, color: '#2f6bff' },
{ icon: 'building', label: '可容纳企业', value: `${d.projects.capacity} 个`, color: '#f79009' },
{ icon: 'coin', label: '累计税利', value: `${fmtWan(d.revenue.tax)}万元`, color: '#8b5cf6' },
{ icon: 'headset', label: '报名电话', value: d.park.phone, color: '#f04438' },
{ icon: 'map-pin', label: '园区地址', value: d.park.address, color: '#0ea5e9' },
];
return (
<section className="bd-overview">
<div className="bd-overview-head">
<div className="bd-title">
<span className="bd-title-ico"><Icon name="chart-area" size={15} /></span>
<span>园区概览</span>
</div>
<div className="bd-ov-status">
{status.map((s) => (
<span className="bd-ost-chip" key={s.label}>
<Icon name={s.icon} size={12} />
<i>{s.label}</i>
<b style={{ color: s.color }}>{s.value}</b>
</span>
))}
</div>
</div>
{/* 五个指标卡片:必须一行展示 */}
<div className="bd-overview-grid">
<div className="bd-cell bd-cell-token">
<KpiCard tone="token" icon="bolt" label="TOKEN 消耗" value={d.token.today} unit="万 tokens"
sub={`实时 ${d.token.rate} t/s · 近30日 ${fmtWan(d.token.total)}`} />
</div>
<div className="bd-cell bd-cell-revenue">
<KpiCard tone="revenue" icon="coin" label="营收总额" value={d.revenue.total} unit="元"
sub={`累计生产经营总额 · 税利 ${fmtWan(d.revenue.tax)}元`} />
</div>
<div className="bd-cell bd-cell-projects">
<KpiCard tone="project" icon="building" label="入园项目" value={d.projects.inPark} unit="家"
sub={`可容纳 ${d.projects.capacity} 个 · 累计投入 ${d.projects.invested} 万`} />
</div>
<div className="bd-cell bd-cell-tools">
<KpiCard tone="tool" icon="wrench" label="工具调用" value={d.tools.today} unit="次"
sub={`累计 ${(d.tools.total / 10000).toFixed(1)}万次 · 成功率 ${d.tools.success}%`} />
</div>
<div className="bd-cell bd-cell-jobs">
<KpiCard tone="job" icon="users" label="带动就业" value={d.jobs.total} unit="人"
sub={`入驻企业累计带动就业 · 2026年7月统计`} />
</div>
</div>
</section>
);
}
/* ---------- 园区概览页脚:使用共享组件(与媒体播放页完全一致) ---------- */
/* =========================================================
中部轮换大屏(一次展示一个面板,自动轮换,降低信息密度)
========================================================= */
/* ---------- TOKEN 消耗(大屏版) ---------- */
function TokenBig({ d }) {
return (
<div className="bd-card bd-big bd-big-token">
<CardHead title="TOKEN 消耗" icon="bolt" right={
<div className="bd-head-stats">
<span className="bd-ost-chip"><i>今日消耗</i><b>{fmtInt(d.token.today)} 万 tokens</b></span>
<span className="bd-ost-chip"><i>近30日</i><b>{fmtWan(d.token.total)} tokens</b></span>
<span className="bd-ost-chip"><i>实时速率</i><b>{d.token.rate} t/s</b></span>
</div>
} />
<div className="bd-real-metrics">
<span>首 token 平均 <b>{d.live.firstToken.toFixed(1)}s</b></span>
<span>吞吐 <b>{d.live.throughput}</b> tok/s</span>
<span>缓存命中 <b>{d.live.cacheHit.toFixed(1)}%</b></span>
<span>会话输入 <b>{d.live.inputM.toFixed(1)}M</b> / 输出 <b>{d.live.outputM.toFixed(1)}M</b> tok</span>
<span><b>{d.live.rounds}</b> 轮 · <b>{d.live.steps}</b> 步</span>
</div>
<div className="bd-big-body">
<div className="bd-big-left bd-big-chart">
<AreaChart series={d.token.series} color="#4cc2ff" height={110}
lastLabel={`近30日 ${fmtWan(d.token.series[d.token.series.length - 1])}/日`} />
</div>
<div className="bd-big-right">
<div className="bd-donut-block">
<Donut items={MODELS} size={150} thickness={16} centerTop="200亿" centerBottom="月均 tokens" />
</div>
<div className="bd-model-list">
{MODELS.map((m) => (
<div className="bd-legend-row" key={m.name}>
<span className="bd-legend-dot" style={{ background: m.color }} />
<span className="bd-legend-name">{m.name}</span>
</div>
))}
</div>
<div className="bd-value-list">
{MODELS.map((m) => (
<div className="bd-legend-row" key={m.name}>
<span className="bd-legend-dot" style={{ background: m.color }} />
<b className="bd-value-num">{m.value}%</b>
</div>
))}
</div>
</div>
</div>
</div>
);
}
/* ---------- 工具调用(大屏版) ---------- */
function ToolsBig({ d }) {
return (
<div className="bd-card bd-big bd-big-tools">
<CardHead title="工具调用" icon="wrench" right={
<div className="bd-head-stats">
<span className="bd-ost-chip"><i>今日调用</i><b>{fmtInt(d.tools.today)} 次</b></span>
<span className="bd-ost-chip"><i>累计调用</i><b>{(d.tools.total / 10000).toFixed(1)}万 次</b></span>
<span className="bd-ost-chip"><i>成功率</i><b>{d.tools.success}%</b></span>
<span className="bd-ost-chip"><i>服务节点</i><b>{d.tools.nodes} / {d.tools.nodes} 在线</b></span>
</div>
} />
<div className="bd-big-body">
<div className="bd-big-right bd-big-stats">
<div className="bd-big-stat"><span>今日调用</span><b>{fmtInt(d.tools.today)}<em>次</em></b></div>
<div className="bd-big-stat"><span>累计调用</span><b>{(d.tools.total / 10000).toFixed(1)}<em>万次</em></b></div>
<div className="bd-big-stat"><span>成功率</span><b>{d.tools.success}<em>%</em></b></div>
<div className="bd-big-stat"><span>AI 服务节点</span><b>12<em>/12 在线</em></b></div>
</div>
<div className="bd-big-left bd-big-bars">
<div className="bd-hbar-title">热门工具调用占比</div>
<HBar items={TOOL_RANK} color="#2f6bff" />
</div>
</div>
<div className="bd-real-metrics">
<span>LLM 推理 <b>15m27s</b></span>
<span>工具调用 <b>9m56s</b></span>
<span><b>81</b> 步 / <b>2</b> 轮会话</span>
<span>每次调用 ≈ <b>15万</b> tokens</span>
</div>
</div>
);
}
/* ---------- 入园项目(组视图 · 中密度) ---------- */
function ProjectsPanel({ d }) {
return (
<div className="bd-card bd-projects-panel">
<CardHead title="入园项目" icon="building" right={
<div className="bd-head-stats">
<span className="bd-ost-chip"><i>实际入驻</i><b>{d.projects.inPark} 家</b></span>
<span className="bd-ost-chip"><i>可容纳</i><b>{d.projects.capacity} 个</b></span>
<span className="bd-ost-chip"><i>累计投入</i><b>{d.projects.invested} 万</b></span>
</div>
} />
<div className="bd-zone-wrap">
<div className="bd-hbar-title">四大孵化区域(宣传册)</div>
<div className="bd-zone-legend">
{d.zones.map((z) => (
<div className="bd-zone-row" key={z.name}>
<span className="bd-legend-dot" style={{ background: '#4c8dff' }} />
<span className="bd-zone-name" title={z.desc}>{z.name}</span>
<span className="bd-zone-val">{z.period}</span>
</div>
))}
</div>
</div>
<div className="bd-ticker-title">入驻企业名录 <span>({d.companies.length} 家)</span></div>
<div className="bd-ticker">
<div className="bd-ticker-inner">
{[...d.companies, ...d.companies].map((c, i) => (
<div className="bd-ticker-item" key={i}>
<span className="bd-ticker-dot" />
{c}
</div>
))}
</div>
</div>
</div>
);
}
/* ---------- 带动就业(组视图 · 中密度) ---------- */
function JobsPanel({ d }) {
const mixTotal = d.industryMix.reduce((a, b) => a + b.value, 0);
return (
<div className="bd-card bd-jobs-panel">
<CardHead title="带动就业" icon="users" right={
<div className="bd-head-stats">
<span className="bd-ost-chip"><i>累计带动就业</i><b>{fmtInt(d.jobs.total)} 人</b></span>
<span className="bd-ost-chip"><i>统计时间</i><b>2026年7月</b></span>
<span className="bd-ost-chip"><i>来源</i><b>运行情况统计表</b></span>
</div>
} />
<div className="bd-hbar-title">园区企业行业比例 <span>({mixTotal} 家)</span></div>
<HBar items={d.industryMix} color="#2f6bff" unit="家" />
<div className="bd-big-stats bd-jobs-grid">
<div className="bd-big-stat"><span>累计带动就业</span><b>{fmtInt(d.jobs.total)}<em>人</em></b></div>
<div className="bd-big-stat"><span>累计生产经营总额</span><b>{d.revenue.total}<em>万元</em></b></div>
<div className="bd-big-stat"><span>累计上缴税利</span><b>{d.revenue.tax}<em>万元</em></b></div>
<div className="bd-big-stat"><span>可容纳企业</span><b>{d.projects.capacity}<em>个</em></b></div>
</div>
</div>
);
}
/* ---------- 营收总数(组视图 · 中密度) ---------- */
function RevenuePanel({ d }) {
return (
<div className="bd-card bd-revenue-panel">
<CardHead title="营收总额" icon="coin" right={
<div className="bd-head-stats">
<span className="bd-ost-chip"><i>累计生产经营总额</i><b>{fmtWan(d.revenue.total)}元</b></span>
<span className="bd-ost-chip"><i>累计税利</i><b>{fmtWan(d.revenue.tax)}元</b></span>
<span className="bd-ost-chip"><i>统计时间</i><b>2026年7月</b></span>
</div>
} />
<div className="bd-big-stats bd-revenue-nums">
<div className="bd-big-stat"><span>累计生产经营总额</span><b>{d.revenue.total}<em>万元</em></b></div>
<div className="bd-big-stat"><span>累计上缴税利总额</span><b>{d.revenue.tax}<em>万元</em></b></div>
<div className="bd-big-stat"><span>带动就业</span><b>{fmtInt(d.jobs.total)}<em>人</em></b></div>
<div className="bd-big-stat"><span>实际入驻企业</span><b>{d.projects.inPark}<em>家</em></b></div>
<div className="bd-big-stat"><span>累计投入运营资金</span><b>{d.projects.invested}<em>万元</em></b></div>
<div className="bd-big-stat"><span>可容纳企业</span><b>{d.projects.capacity}<em>个</em></b></div>
<div className="bd-big-stat"><span>园区面积</span><b>{d.park.area}<em>㎡</em></b></div>
<div className="bd-big-stat"><span>成立时间</span><b>{d.park.founded}<em>年</em></b></div>
</div>
<div className="bd-revenue-tags">
<span><Icon name="building" size={14} /> 免费办公空间 <b>≥20</b> ㎡</span>
<span><Icon name="coin" size={14} /> 一次性创业补贴 <b>最高10万</b></span>
<span><Icon name="wallet" size={14} /> 担保贷款贴息 <b>最高50万</b></span>
<span><Icon name="flask" size={14} /> 科技计划经费 <b>最高30万</b></span>
</div>
<div className="bd-real-metrics">
<span>单位负责人 <b>李云鹏</b></span>
{/* <span>填表人 <b>李禹贤</b></span>
<span>填报时间 <b>2026-07-31</b></span> */}
<span>报名电话 <b>{d.park.phone}</b></span>
</div>
</div>
);
}
/* ---------- 中部轮换器:五项面板自由组织成两组,组间轮换 ---------- */
const ROTATE_MS = 10000;
function MiddleRotator({ d }) {
const views = [
{
key: 'ai',
label: 'OPC 智能资源',
icon: 'cpu',
node: (
<div className="bd-view bd-view-2">
<TokenBig d={d} />
<ToolsBig d={d} />
</div>
),
},
{
key: 'park',
label: '园区经营概况',
icon: 'building',
node: (
<div className="bd-view bd-view-3">
<ProjectsPanel d={d} />
<JobsPanel d={d} />
<RevenuePanel d={d} />
</div>
),
},
];
const [idx, setIdx] = useState(0);
// 组间自动轮换:每次切换后重新计时;点击 Tab 也可切换
useEffect(() => {
const iv = setInterval(() => setIdx((i) => (i + 1) % views.length), ROTATE_MS);
return () => clearInterval(iv);
}, [idx, views.length]);
return (
<section className="bd-rotator">
<div className="bd-rotator-tabs">
{views.map((p, i) => (
<button key={p.key} className={`bd-rot-tab${i === idx ? ' active' : ''}`} onClick={() => setIdx(i)}>
<Icon name={p.icon} size={13} />
<span>{p.label}</span>
<i key={`${p.key}-${i === idx ? 'on' : 'off'}`} className="bd-rot-progress" />
</button>
))}
</div>
<div className="bd-rotator-stage">
<div className="bd-rotator-slide" key={views[idx].key}>
{views[idx].node}
</div>
</div>
</section>
);
}
/* ---------- 实时动态(3 列 × 2 行,每几秒整体轮换加载) ---------- */
function LiveFeed({ feed, limit = 6 }) {
const [page, setPage] = useState(0);
const chunks = [];
for (let i = 0; i < feed.length; i += limit) chunks.push(feed.slice(i, i + limit));
useEffect(() => {
if (chunks.length <= 1) return;
const iv = setInterval(() => setPage((p) => (p + 1) % chunks.length), 5000);
return () => clearInterval(iv);
}, [chunks.length]);
const chunk = chunks[page % chunks.length] || [];
return (
<div className="bd-feed">
<div className="bd-feed-grid" key={page}>
{chunk.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>
);
}
/* =========================================================
页面
========================================================= */
export default function DataScreen() {
const d = useParkSim();
return (
<div className="bd-page">
{/* 园区今日概览(五个指标一行 + 标题右侧运行状态) */}
<TodayOverview d={d} />
{/* 中部大屏:五项内容自动轮换展示(降低信息密度) */}
<MiddleRotator d={d} />
{/* 园区实时动态(底部整行 · 园区概览条上方 · 内容三列) */}
<section className="bd-feed-row">
<div className="bd-card bd-feed-card">
<CardHead title="园区实时动态" icon="radio" right={<Pill tone="red">● LIVE</Pill>} />
<LiveFeed feed={d.feed} limit={6} />
</div>
</section>
{/* 园区概览(保持在最底部) */}
<ParkOverviewStrip d={d} />
</div>
);
}