Files
park-desktop/src/pages/DataScreen.jsx
T
Pine 8ee08854cd feat(screen): 园区大屏园区资讯 widget
- ParkFeed 拉取 /park/api/content/feed,滚动展示本园区内容标题;图标用 @appica/icons-react(禁用emoji)
- DataScreen 挂载 ParkFeed

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-27 15:53:38 +08:00

587 lines
26 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useRef } from 'react';
import '../styles/datascreen.css';
import Icon from '../components/Icons';
import ParkOverviewStrip from '../components/ParkOverviewStrip';
import ParkFeed from '../components/ParkFeed';
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) => (Number.isFinite(Number(n)) ? Math.round(Number(n)).toLocaleString('zh-CN') : '--');
/** 万/亿格式化:非有限数值(缺失/不一致口径)安全显示 '--',避免崩溃 */
const fmtWan = (n) => {
const v = Number(n);
if (!Number.isFinite(v)) return '--';
return (v >= 10000 ? (v / 10000).toFixed(2) + '亿' : v >= 100 ? v.toFixed(1) + '万' : v.toFixed(1));
};
/** 安全取数:缺失/非数值回退 0,供直接 .toFixed 调用点使用 */
const num = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0);
/* 数据源:后端 /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={`累计 ${(num(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>{num(d.live.firstToken).toFixed(1)}s</b></span>
<span>吞吐 <b>{d.live.throughput}</b> tok/s</span>
<span>缓存命中 <b>{num(d.live.cacheHit).toFixed(1)}%</b></span>
<span>会话输入 <b>{num(d.live.inputM).toFixed(1)}M</b> / 输出 <b>{num(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>{(num(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>{(num(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>
<ParkFeed />
{/* 园区概览(保持在最底部) */}
<ParkOverviewStrip d={d} />
</div>
);
}