8ee08854cd
- ParkFeed 拉取 /park/api/content/feed,滚动展示本园区内容标题;图标用 @appica/icons-react(禁用emoji) - DataScreen 挂载 ParkFeed Co-Authored-By: Claude <noreply@anthropic.com>
587 lines
26 KiB
React
587 lines
26 KiB
React
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>
|
||
);
|
||
}
|