Files
DPM/src/pages/DataScreen.jsx
T
Pine ef009dad05 fix: 园区概览页脚颜色统一(设计变量提升至 :root)
- --bd-* 设计变量从 .bd-page 提升到 :root,页眉/数字孪生/媒体页/页脚全部生效
  修复媒体播放页页脚图标与数值失去蓝色(回退为继承深色)的问题
- 数据大屏改用共享 ParkOverviewStrip 组件,与媒体播放页页脚完全一致
  (标签统一为「累计营收」)
- PageHeader 头像 Logo 路径修正 /public/logo.png → /logo.png,修复 Logo 不显示
2026-08-17 20:15:18 +08:00

728 lines
30 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';
/* =========================================================
昆明市大学生创业园 · OPC 智能园区数字运营中心
—— 数据大屏(深色科技风 · 实时模拟数据)
---------------------------------------------------------
基础数据来源:
· 《昆明市创业园宣传册》:在园项目 150+ / 累计孵化 200+ /
带动就业 2000+ / 累计营收 2 亿+ / 企业估值 5 亿+ /
高企 32 家 / 重点研发项目 18 项 / 大赛奖项 47 项
· 《昆明市大学生创业园入驻企业信息表》:39 家本期入驻企业
(区域、行业、名称)
大屏实时数据为演示模拟(每 2.2s 跳动一次)
========================================================= */
/* ---------- 真实基础数据(入驻企业信息表) ---------- */
// [企业名, 区域, 行业]
const PROJECTS = [
['云南派音人工智能科技', '加速区', 'AI 科技'],
['米勒克尔蓝宝石珠宝', '加速区', '新材料'],
['中泰研学合作', '加速区', '教育培训'],
['云南宸中低空经济', '加速区', '低空经济'],
['昆明智海银高文化科技', '加速区', 'AI 科技'],
['AI 机器人大模型训练', '加速区', 'AI 科技'],
['云南廷秀文旅康养', '加速区', '文旅康养'],
['瀚颖 AI+ 教育咨询', '加速区', '教育培训'],
['云南大学 AI+ 创业平台', '加速区', 'AI 科技'],
['仰光客厅', '国际区', '文旅康养'],
['云南上古绝学文化', '国际区', '文化创意'],
['千楣之路', '国际区', '跨境电商'],
['中越生物医疗', '国际区', '生物医药'],
['中越文旅交流', '国际区', '文旅康养'],
['绿态环保科技', '国际区', '绿色环保'],
['联翼航空低空经济', '国际区', '低空经济'],
['酷享野农 AI 农业', '国际区', '现代农业'],
['滇缅国际设计', '国际区', '企业服务'],
['昆明舒诺生物科技', '国际区', '生物医药'],
['达岸教育管理', '国际区', '教育培训'],
['昆明滇油石化', '成长区', '绿色环保'],
['花仙子园艺肥料', '成长区', '现代农业'],
['Facebook 越商茶器', '成长区', '跨境电商'],
['研X 同行者网络', '成长区', 'AI 科技'],
['鬼才明 AI 创意工作室', '成长区', 'AI 科技'],
['朵哈·玫瑰特色产业链', '成长区', '文化创意'],
['昆明云韵体育', '成长区', '文化创意'],
['昆明舒护安养老服务', '成长区', '生物医药'],
['南菌优培食用菌', '成长区', '生物医药'],
['五华区丽裳文化', '成长区', '文化创意'],
['云南星瑞航空', '成长区', '低空经济'],
['云南直播私域平台', '成长区', '跨境电商'],
['昆明屿澈电商', 'OPC 空间', '跨境电商'],
['中外青少年研学', 'OPC 空间', '教育培训'],
['星杭商务信息咨询', 'OPC 空间', '企业服务'],
['蓝智科技', 'OPC 空间', '现代农业'],
['云品出滇·纸享万家', 'OPC 空间', '跨境电商'],
['云迹轻旅', 'OPC 空间', '文旅康养'],
['启元人工智能科技', 'OPC 空间', 'AI 科技'],
];
/* 区域分布(39 家本期企业) */
const ZONE_DIST = [
{ name: '孵化加速区', value: 9, color: '#4c8dff' },
{ name: '国际创客区', value: 7, color: '#22d3ee' },
{ name: '成长苗圃区', value: 8, color: '#34d399' },
{ name: 'OPC 创业空间', value: 6, color: '#fbbf24' },
{ name: '待分配', value: 9, color: '#7c8db0' },
];
/* 就业行业分布(累计 2186 人) */
const INDUSTRY = [
{ name: 'AI 与数字经济', value: 520, color: '#4cc2ff' },
{ name: '电商与贸易', value: 310, color: '#7c9bff' },
{ name: '生物医药与大健康', value: 240, color: '#34d399' },
{ name: '文创与农业', value: 260, color: '#f472b6' },
{ name: '文旅与康养', value: 190, color: '#fbbf24' },
{ name: '教育培训', value: 180, color: '#a78bfa' },
{ name: '低空经济', value: 160, color: '#22d3ee' },
{ name: '绿色新材料与企业服务', value: 326, color: '#9aa7c4' },
];
/* 模型占比 */
const MODELS = [
{ name: 'DeepSeek-V3', 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 TOOL_NAMES = TOOL_RANK.map((t) => t.name);
const MODEL_NAMES = MODELS.map((m) => m.name);
const COMPANY_NAMES = PROJECTS.map((p) => p[0]);
/* ---------- 工具函数 ---------- */
const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
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));
const nowTime = () => new Date().toLocaleTimeString('zh-CN', { hour12: false });
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: 'broadcast', text: `创业沙龙「AI 一人公司实战课」报名开启` },
{ icon: 'robot', text: `${pick(MODEL_NAMES)}」模型完成一次微调任务` },
{ icon: 'flask', text: `在孵项目完成实验验证 · 进入加速评审` },
];
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 tokensDeepSeek-V3', time: '09:02:14' },
{ id: 2, icon: 'building', text: '「启元人工智能科技」通过评审 · 正式入驻 OPC 创业空间', time: '09:05:41' },
{ id: 3, icon: 'wrench', text: '工具「文档生成」被调用 36 次', time: '09:11:03' },
{ id: 4, icon: 'coin', text: '园区企业完成一笔 ¥3.6万 交易', time: '09:17:26' },
{ id: 5, icon: 'users', text: '「昆明舒护安养老服务」新增招聘岗位 2 个', time: '09:23:58' },
];
}
/* ---------- 模拟数据引擎(每 2.2s 跳动) ---------- */
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); // 万 tokens
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;
// 30 日曲线:今日点持续爬升,每 12 个 tick 滚动一天
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 };
}
function useParkSim() {
const [snap, setSnap] = useState(initSnapshot);
useEffect(() => {
const iv = setInterval(() => setSnap((p) => nextSnapshot(p)), 2200);
return () => clearInterval(iv);
}, []);
return snap;
}
/* ---------- 通用 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 = 16, PB = 18;
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 - 5} fontSize="8.5" fill="#8fa1ba">30 天前</text>
<text x={px(series.length - 1)} y={H - 5} textAnchor="end" fontSize="8.5" fill="#8fa1ba">今日</text>
{lastLabel && (
<text x={W - PR} y={PT - 6} textAnchor="end" fontSize="10.5" 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' || tone === 'token'
? <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.devices}%`, color: '#17b26a' },
{ icon: 'flame', label: '能耗', value: `${d.park.energy} kWh`, color: '#2f6bff' },
{ icon: 'users', label: '人流量', value: `${d.park.people}`, color: '#f79009' },
{ icon: 'briefcase', label: '工位', value: `${d.park.desk}%`, color: '#8b5cf6' },
{ icon: 'radio', label: '会议室', value: `${d.park.meeting}/6`, color: '#f04438' },
{ icon: 'cpu', label: '云桌面', value: `${Math.round(d.park.people * 0.42)}`, 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 className="bd-head-right">
<Pill tone="green">实时刷新 · 2s</Pill>
</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}
sub={`累计 ${fmtWan(d.token.total)} · 实时 ${d.token.rate} t/s`} />
</div>
<div className="bd-cell bd-cell-revenue">
<KpiCard tone="revenue" icon="coin" label="营收总数" value={d.revenue.total}
sub={`今日 +${d.revenue.today}万 · 环比 +${d.revenue.growth}%`} />
</div>
<div className="bd-cell bd-cell-projects">
<KpiCard tone="project" icon="building" label="入园项目" value={d.projects.inPark} unit="家"
sub={`累计孵化 ${d.projects.cum} 家 · 今日新增 ${d.projects.todayNew}`} />
</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={`今日新增 ${d.jobs.todayNew} 人 · 高企 32 家`} />
</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>{fmtWan(d.token.today)} tokens</b></span>
<span className="bd-ost-chip"><i>累计消耗</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-big-body">
<div className="bd-big-left bd-big-chart">
<AreaChart series={d.token.series} color="#4cc2ff" height={110}
lastLabel={`近30日 ${d.token.series[d.token.series.length - 1].toFixed(1)}万/日`} />
</div>
<div className="bd-big-right">
<div className="bd-donut-block">
<Donut items={MODELS} size={150} thickness={16} centerTop="1.28亿" 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>12 / 12 在线</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>
);
}
/* ---------- 入园项目(组视图 · 中密度) ---------- */
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.cum} </b></span>
<span className="bd-ost-chip"><i>今日新增</i><b>{d.projects.todayNew} </b></span>
{/* <span className="bd-ost-chip"><i>高企 / 研发</i><b>32 / 18</b></span> */}
</div>
} />
<div className="bd-zone-wrap">
<Donut items={ZONE_DIST} size={96} thickness={13} centerTop="39" centerBottom="本期企业" />
<div className="bd-zone-legend">
{ZONE_DIST.map((z) => (
<div className="bd-zone-row" key={z.name}>
<span className="bd-legend-dot" style={{ background: z.color }} />
<span className="bd-zone-name">{z.name}</span>
<span className="bd-zone-val">{z.value} </span>
</div>
))}
</div>
</div>
<div className="bd-ticker-title">入驻企业名录 <span>(39 )</span></div>
<div className="bd-ticker">
<div className="bd-ticker-inner">
{[...COMPANY_NAMES, ...COMPANY_NAMES].map((c, i) => (
<div className="bd-ticker-item" key={i}>
<span className="bd-ticker-dot" />
{c}
</div>
))}
</div>
</div>
</div>
);
}
/* ---------- 带动就业(组视图 · 中密度) ---------- */
function JobsPanel({ d }) {
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>1,680 </b></span>
<span className="bd-ost-chip"><i>实习</i><b>506 </b></span>
{/* <span className="bd-ost-chip"><i>今日新增</i><b>{d.jobs.todayNew} 人</b></span> */}
</div>
} />
<div className="bd-hbar-title">行业就业分布</div>
<div className="bd-jobs-bars">
{INDUSTRY.slice(0, 6).map((it, i) => {
const max = Math.max(...INDUSTRY.map((x) => x.value));
return (
<div className="bd-vbar" key={i}>
<div className="bd-vbar-track">
<div className="bd-vbar-fill" style={{ height: `${(it.value / max) * 100}%`, background: `linear-gradient(180deg, ${it.color}, ${it.color}33)` }} />
</div>
<span className="bd-vbar-val">{it.value}</span>
<span className="bd-vbar-name">{it.name}</span>
</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>+{d.revenue.today} </b></span>
<span className="bd-ost-chip"><i>企业估值</i><b>5.2 亿元</b></span>
<span className="bd-ost-chip"><i>环比</i><b>+{d.revenue.growth}%</b></span>
</div>
} />
<div className="bd-chart-block">
<AreaChart series={d.revenue.series} color="#17b26a" height={76}
lastLabel={`近30日 ${d.revenue.series[d.revenue.series.length - 1].toFixed(1)}万/日`} />
</div>
<div className="bd-revenue-tags">
<span><Icon name="trophy" size={14} /> 大赛奖项 <b>47</b> </span>
<span><Icon name="flask" size={14} /> 重点研发 <b>18</b> </span>
<span><Icon name="factory" size={14} /> 高新技术企业 <b>32</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>
);
}
/* ---------- 实时动态(纯列表,外层自行包裹卡片) ---------- */
function LiveFeed({ feed, limit = 7 }) {
return (
<div className="bd-feed">
{feed.slice(0, limit).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>
);
}
/* =========================================================
页面
========================================================= */
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>
);
}