feat: OPC 智能园区数据大屏主页面

- DataScreen.jsx:园区数字运营中心(深色科技风)
- 五大 KPI(TOKEN/工具/项目/就业/营收)+ 三栏数据面板 + 实时动态
- 入驻企业信息表真实数据 + 每 2.2s 模拟跳动
- datascreen.css:完整大屏样式(Appica 图标配色/概览卡/导航按钮)
This commit is contained in:
Pine
2026-08-17 18:37:31 +08:00
parent 2a7f51a202
commit ef6ed76478
2 changed files with 2374 additions and 0 deletions
+748
View File
@@ -0,0 +1,748 @@
import { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import '../styles/datascreen.css';
import Icon from '../components/Icons';
import PageHeader from '../components/PageHeader';
/* =========================================================
昆明市大学生创业园 · 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>
);
}
/* ---------- 园区概览(页面最底部 · 综合指标条) ---------- */
function ParkOverviewStrip({ d }) {
const chips = [
{ icon: 'building', label: '在园项目', value: `${d.projects.inPark}` },
{ icon: 'users', label: '带动就业', value: `${fmtInt(d.jobs.total)}` },
{ icon: 'coin', label: '累计', value: `${fmtWan(d.revenue.total)}` },
{ icon: 'factory', label: '高新技术企业', value: '32 家' },
{ icon: 'flask', label: '省级以上重点研发', value: '18 项' },
{ icon: 'trophy', label: '创业大赛奖项', value: '47 项' },
{ icon: 'diamond', label: '孵化企业估值', value: '5.2 亿元' },
{ icon: 'map-pin', label: '园区面积', value: '1.2 万㎡' },
{ icon: 'graduation', label: '创业导师', value: '58 位' },
{ icon: 'chart-line', label: '累计孵化企业', value: `${d.projects.cum}` },
];
return (
<footer className="bd-strip">
<div className="bd-strip-title"><Icon name="map-pin" size={14} /> 园区概览</div>
<div className="bd-strip-chips">
{chips.map((c, i) => (
<div className="bd-chip" key={i}>
<span className="bd-chip-ico"><Icon name={c.icon} size={15} /></span>
<span className="bd-chip-label">{c.label}</span>
<b>{c.value}</b>
</div>
))}
</div>
</footer>
);
}
/* ---------- TOKEN 消耗 ---------- */
function TokenPanel({ d }) {
return (
<div className="bd-card bd-token-panel">
{/* 概括数据内联在标题右侧 */}
<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-split bd-split-tri">
<div className="bd-donut-block">
<Donut items={MODELS} size={96} thickness={13} 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-chart-block bd-chart-mid">
<AreaChart series={d.token.series} color="#4cc2ff" height={72}
lastLabel={`近30日 ${d.token.series[d.token.series.length - 1].toFixed(1)}万/日`} />
</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>
);
}
/* ---------- 工具调用 ---------- */
function ToolsPanel({ d }) {
return (
<div className="bd-card bd-tools-panel">
{/* 概括数据内联在标题右侧 */}
<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-hbar-title">热门工具调用占比</div>
<HBar items={TOOL_RANK} color="#2f6bff" />
<div className="bd-tools-foot">
<span>· 文本创作 34%</span>
<span>· 智能分析 27%</span>
<span>· 多模态生成 16%</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.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={92} 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>
);
}
/* ---------- 实时动态(纯列表,外层自行包裹卡片) ---------- */
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();
const navigate = useNavigate();
// 左右方向键:→ 进入数字孪生页,← 返回(管理页为左端)
useEffect(() => {
const onKey = (e) => {
if (e.key === 'ArrowRight') navigate('/twin');
else if (e.key === 'ArrowLeft') navigate('/admin');
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [navigate]);
return (
<div className="bd-page">
{/* 顶栏(共享组件,与数字孪生页风格一致) */}
<PageHeader
nav={{ to: '/twin', label: '数字孪生', icon: 'chevron-right', title: '按 → 进入数字孪生' }}
status="数据实时"
/>
{/* 园区今日概览(五个指标一行 + 标题右侧运行状态) */}
<TodayOverview d={d} />
{/* 主体:下方内容三列显示 */}
<main className="bd-main bd-main-3col">
{/* 第 1 列:TOKEN 消耗 · 工具调用 */}
<section className="bd-col bd-col-left">
<TokenPanel d={d} />
<ToolsPanel d={d} />
</section>
{/* 第 2 列:入园项目 · 带动就业 */}
<section className="bd-col bd-col-mid">
<ProjectsPanel d={d} />
<JobsPanel d={d} />
</section>
{/* 第 3 列:营收总数 */}
<section className="bd-col bd-col-right">
<RevenuePanel d={d} />
</section>
</main>
{/* 园区实时动态(底部整行 · 园区概览条上方 · 内容三列) */}
<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>
);
}
File diff suppressed because it is too large Load Diff