一、起因:为什么要做这个项目?
最近在做数据可视化需求时,看了太多千篇一律的后台管理界面,总想着能不能做点更酷的东西。正好看到很多政府和企业的智慧城市指挥中心大屏,那些闪烁的数据、3D 地图、实时图表,科技感爆棚!
于是决定自己撸一个,顺便探索一下现代前端可视化技术的边界。
二、效果展示:先看看成品
🎨 整体布局
- 左侧面板:经济指标 + 4 种图表(饼图、柱状图、折线图、面积图)
- 中间地图:3D 网格地图 + 5 个区域标记(西南/中心/西北/东北/东南)
- 右侧面板:人口民生 + 雷达图 + 指标卡片
- 底部导航:城市交通、城市安全、人口民生三大模块切换
✨ 核心亮点
- 炫酷的 3D 地图效果:透视网格 + 发光区域圆圈 + 浮动标记
- 丰富的图表交互:悬停显示详细数据,Tooltip 自定义样式
- 流畅的动画效果:Framer Motion 驱动的进场动画 + 数据轮播
- 完整的数据流:Mock API + 自动刷新 Hook + 数据轮播组件
- 响应式布局:左右面板自适应宽度,图表自动撑满
三、技术栈:用了哪些工具?
| 技术栈 | 用途 | 为什么选它? |
|---|---|---|
| React 19 | 前端框架 | 最新版本,Hooks 更强大 |
| TypeScript | 类型系统 | 代码提示 + 类型安全 |
| Vite | 构建工具 | 启动快,HMR 秒级更新 |
| Tailwind CSS | 样式方案 | 原子化 CSS,开发效率高 |
| Recharts | 图表库 | API 简洁,支持 React 组件化 |
| Framer Motion | 动画库 | 声明式动画,效果丝滑 |
| Lucide React | 图标库 | 轻量级,图标漂亮 |
四、核心功能拆解
📊 1. 自定义 Tooltip(图表交互增强)
痛点:Recharts 默认 Tooltip 样式太朴素,不符合科技大屏的气质。
解决方案:自定义 Tooltip 组件
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-[#0a1628]/95 backdrop-blur-md border border-cyan-500/30 rounded-lg p-3 shadow-[0_0_20px_rgba(0,229,255,0.3)]">
<p className="text-cyan-400 text-xs font-bold mb-2">{label}</p>
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: entry.color }} />
<span className="text-white">{entry.name}: {entry.value}</span>
</div>
))}
</div>
);
}
return null;
};
效果:
- 深色半透明背景 + 发光边框
- 彩色指示点与图表颜色对应
- 平滑的淡入淡出动画
🗺️ 2. 3D 地图效果(视觉核心)
实现思路:
- 透视网格:使用 CSS
transform: perspective() rotateX()创建 3D 感 - 发光圆圈:每个区域用渐变圆圈 + blur 阴影 +
animate-pulse - 浮动标记:自定义 3D 金字塔 SVG + 上下浮动动画
// 透视网格
<div style={{
background: `
linear-gradient(rgba(0, 229, 255, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 229, 255, 0.1) 1px, transparent 1px)
`,
backgroundSize: '60px 60px',
transform: 'perspective(1000px) rotateX(70deg) translateY(-200px) scale(1.5)',
maskImage: 'radial-gradient(circle at center, black 0%, transparent 70%)',
}} />
技巧:
- 用
maskImage实现边缘渐隐效果 - 5 个区域圆圈位置精确对应地图标记
- 标记自带信息卡片,悬停放大
🔄 3. 数据自动刷新(实战 Hooks)
需求:模拟实时数据更新,每 5 秒刷新一次。
自定义 Hook:
export function useDataRefresh<T>(
fetchFunction: () => Promise<T>,
interval: number = 5000,
initialData: T
) {
const [data, setData] = useState<T>(initialData);
const [loading, setLoading] = useState(false);
const fetchData = useCallback(async () => {
try {
setLoading(true);
const result = await fetchFunction();
setData(result);
} catch (err) {
console.error('Data fetch error:', err);
} finally {
setLoading(false);
}
}, [fetchFunction]);
useEffect(() => {
fetchData(); // 初始加载
const timer = setInterval(fetchData, interval); // 定时刷新
return () => clearInterval(timer);
}, [fetchData, interval]);
return { data, loading, refresh: fetchData };
}
使用方式:
const { data, loading, refresh } = useDataRefresh(
fetchMetrics, // Mock API 函数
5000, // 刷新间隔
[] // 初始数据
);
🎠 4. 数据轮播组件(动态展示)
场景:首页需要轮播展示多个重点指标。
实现要点:
- useDataCarousel Hook:管理轮播逻辑
- AnimatePresence:切换时的淡入淡出动画
- 控制按钮:上一个/下一个/播放/暂停
const { currentData, next, prev, pause, play, isPlaying } =
useDataCarousel(dataList, 3000);
<AnimatePresence mode="wait">
<motion.div
key={currentIndex}
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -50 }}
transition={{ duration: 0.5 }}
>
{/* 数据内容 */}
</motion.div>
</AnimatePresence>
亮点:
- 支持手动控制和自动播放
- 轮播指示器实时同步
- 数据切换动画流畅自然
📡 5. Mock API 数据服务
为什么需要 Mock:
- 前端开发阶段后端接口未就绪
- 方便演示和测试
- 模拟随机数据,更真实
API 设计:
// Mock API: 获取指标数据
export const fetchMetrics = async (): Promise<MetricData[]> => {
await delay(500); // 模拟网络延迟
return [
{
title: '公共预算收入',
value: random(500, 600).toString(),
unit: '亿',
trend: 'up',
percentage: random(20, 30)
},
// ... 更多数据
];
};
完整 API 列表:
fetchMetrics()- 顶部指标卡片fetchLineChartData()- 折线图fetchPieData()- 饼图fetchBarChartData()- 柱状图fetchAreaChartData()- 面积图fetchRadarData()- 雷达图fetchMapMarkers()- 地图标记
🎨 6. 响应式布局方案
挑战:大屏通常是固定分辨率,但需要适配不同屏幕。
方案对比:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| autofit.js | 等比缩放,保持比例 | 小屏幕可能有黑边 | 固定比例大屏 |
| Flexbox + 百分比 | 充分利用空间 | 需要精细调整 | 自适应布局 |
| CSS Grid | 布局灵活 | 学习成本高 | 复杂网格 |
我的选择:Flexbox + 百分比宽度
// 左右面板自适应
<div className="w-[22%] min-w-[320px] max-w-[400px]">
{/* 面板内容 */}
</div>
// 图表区域撑满
<div className="flex-1 min-h-0 overflow-y-auto">
{/* 图表列表 */}
</div>
技巧:
flex-1+min-h-0解决 flex 子元素溢出overflow-y-auto让图表区域可滚动ResponsiveContainer让图表自动适应容器
五、踩坑实录
🐛 坑 1:Recharts 图表不撑满容器
现象:图表固定高度,无法充满 flex 容器。
原因:ResponsiveContainer 需要明确的高度。
解决:
// ❌ 错误写法
<div className="flex-1">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data} />
</ResponsiveContainer>
</div>
// ✅ 正确写法
<div className="flex-1 min-h-0"> {/* 关键:min-h-0 */}
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data} />
</ResponsiveContainer>
</div>
🐛 坑 2:Framer Motion 动画闪烁
现象:列表项动画时会闪烁或重复。
原因:没有设置唯一的 key。
解决:
<AnimatePresence mode="wait"> {/* mode="wait" 很重要 */}
<motion.div key={currentIndex}> {/* key 必须唯一 */}
{/* 内容 */}
</motion.div>
</AnimatePresence>
🐛 坑 3:地图标记位置不准
现象:标记偏离预期位置。
原因:绝对定位的基准点是左上角,而不是标记中心。
解决:
<div style={{ left: x, top: y }}> {/* 左上角定位 */}
<div className="transform -translate-x-1/2 -translate-y-1/2"> {/* 居中偏移 */}
{/* 标记内容 */}
</div>
</div>
六、性能优化建议
⚡ 1. 图表按需加载
import { LineChart } from 'recharts'; // ✅ 具名导入
// 而不是 import * as Recharts from 'recharts'; // ❌
⚡ 2. 动画节流
// 使用 Framer Motion 的 layout 模式
<motion.div layout layoutId="card">
⚡ 3. 数据缓存
const [cachedData, setCachedData] = useState({});
// 相同请求返回缓存结果
七、未来计划
- WebSocket 实时数据推送:替换定时轮询
- 可配置主题:支持多种颜色方案
- 其他界面:增加其他tab大屏
八、总结
这个项目最大的收获是:
- Recharts 不只是画图:结合 TypeScript + 自定义组件,可以实现高度定制化
- Hooks 真香:
useDataRefresh和useDataCarousel可以复用到任何项目 - CSS 动画比想象中强大:
perspective+blur+animate-pulse就能做出酷炫效果 - Mock 数据是好习惯:前后端分离开发效率翻倍
如果你也在做数据可视化项目,希望这篇文章能给你一些启发!
九、快速上手
📦 安装依赖
npm install react recharts framer-motion lucide-react
npm install -D tailwindcss @tailwindcss/vite
🚀 启动项目
npm run dev
📂 项目结构
src/
├── api/
│ └── mockData.ts # Mock API 服务
├── hooks/
│ └── useDataRefresh.ts # 数据刷新 Hook
├── components/
│ └── DataCarouselDemo.tsx # 轮播组件
└── App.tsx # 主应用
本文所有代码均可商用,欢迎参考和学习!
我放在公众号(柳杉前端) 回复 智慧城市大屏 获取源码
#前端开发 #数据可视化 #React #智慧城市 #大屏设计