feat: OPC 智能园区数据大屏主页面
- DataScreen.jsx:园区数字运营中心(深色科技风) - 五大 KPI(TOKEN/工具/项目/就业/营收)+ 三栏数据面板 + 实时动态 - 入驻企业信息表真实数据 + 每 2.2s 模拟跳动 - datascreen.css:完整大屏样式(Appica 图标配色/概览卡/导航按钮)
This commit is contained in:
@@ -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 tokens(DeepSeek-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
Reference in New Issue
Block a user