简介:这份资源是一套基于React与ECharts的数据可视化大屏开源项目,主要采用TypeScript开发,并辅以JavaScript、CSS与HTML。项目面向前端开发者、数据可视化工程师及需要快速搭建大屏场景的团队,适用于后台监控、运营展示、综合态势等界面,解决图表集成、组件复用和屏幕适配等常见问题。压缩包共包含620个文件,大小约16.4MB,以385个JSON配置和95个PNG图像为主,同时涵盖tsx/ts组件、字体文件、Markdown文档及样式文件,配置、素材、逻辑与说明文档比较齐全。项目提炼并封装了天气、时间、轮播表格等可复用组件,具备数据动态刷新与屏幕自适应能力,既可作为学习React组件化和ECharts可视化的参考,也能作为实际大屏项目的基础模板二次开发,整体目录结构与工程配置同样具有借鉴价值。已有1266人浏览学习,适合希望快速上手并扩展大屏功能的中高级前端开发者。
1. 拿到 React 和 ECharts 大屏开源项目,第一步不是改代码,是先看懂它的骨架
数据可视化大屏在很长一段时间里被当成“前端门面工程”:背景要炫、数字要飞、地图要亮。但真把这套东西交到你手上,比如从 GitHub 拉下一个带“React + ECharts + 数据大屏”标签的开源项目,问题马上就变了——不是“它怎么这么好看”,而是“它怎么改才能跑我们自己的数”。这类项目最典型的状态是:仓库不大,组件很多,charts 目录一眼望不到头,但 README 里几乎不写清楚哪个组件对应屏幕上哪一块。第一次接触时最容易犯的错误是直接打开一个 .tsx 就改,改完发现别的图表跟着动,或者数字跳动特别假,甚至大屏在不同分辨率的投屏电视上直接破版。
本文就按这个场景展开:一类是你要基于现有的开源项目二次开发,另一类是你准备自己从零搭一个同构的小而美大屏项目。这两件事在 React + ECharts 这个组合下其实是同一条技术路径,区别只在于你有没有那层骨架。你要关注的是四个核心问题:图表组件怎么写才算“规范化”、大屏适配用哪种策略才不抖、数据更新走哪条链路才不会卡死 UI、以及哪些配置项属于“看了源码也调不动、必须自己写”的硬骨头。把这四件事吃透,任何一个开源大屏模板在你手里都只是个壳。
2. ECharts 与 React 的集成方式:从 “mount 里 init” 到组件化封装
2.1 为什么大屏项目普遍选择 ECharts 而不是 D3 或 AntV
先说结论:数据大屏对图表的诉求不是“能做图”,而是“更新快、配置多、地图省事、交互要求低”。ECharts 的 option 描述式 API 恰好满足这几个点,你不需要像 D3 那样主动操作 SVG 节点,也不用像 G2 那样把数据和图形语法概念先讲一遍。React 的声明式渲染模型和 ECharts 的 option 模型很容易“翻译”:把 option 看成 state,setOption 看成 setState,图表的生命周期跟着 React 组件的挂载和卸载走。
从渲染性能上看,ECharts 使用 Canvas 作为默认渲染器,在数据点数几千到几万这个区间内,性能表现明显优于基于 SVG 的方案。大屏的典型数据量级是“每个图表数百到数千点”,Canvas 在这条线上完全够用。AntV 的 G2Plot 在统计图表上体验也不错,但地图投影、动态轮播、特效散点这些大屏高频场景里,ECharts 的生态明显更全。D3 自由度最高,但维护成本也高,不适合“我要快速改出一个大屏”这种目标。所以搜索“数据可视化大屏开源项目”时,React + ECharts 的组合占比最高,是有实践逻辑的。
2.2 手写一个带 resize 监听的 useECharts Hook,替代“复制粘贴 init”
很多开源项目的图表代码是这么写的:在 useEffect 里echarts.init,在另一个 effect 里写setOption,组件卸载时忘了dispose。这种写法在页面单一、只跑一次时没问题,但大屏项目有大量图表在同一个页面上,而且有轮询更新、tab 切换、全屏缩放等动作,不统一管理就会炸出两类 bug:控制台报 “There is a chart instance already initialized on the dom”,以及窗口 resize 后图表被拉变形。
下面给出一个我常用的封装方式,核心思路是把 ECharts 实例放进 ref,把 option 放进 dependencies,任何一次 option 变化只执行setOption(option, true)而不是重新 init:
import * as echarts from 'echarts'; import { useEffect, useRef } from 'react'; type EChartsOption = echarts.EChartsCoreOption; interface UseEChartsOptions { option: EChartsOption; theme?: string; loading?: boolean; onEvents?: Record<string, (params: unknown) => void>; } export function useECharts<T extends HTMLElement>( { option, theme, loading, onEvents }: UseEChartsOptions, deps: unknown[] = [] ) { const containerRef = useRef<T>(null); const chartRef = useRef<echarts.ECharts>(); useEffect(() => { if (!containerRef.current) return; const chart = echarts.init(containerRef.current, theme); chartRef.current = chart; const handleResize = () => chart.resize(); window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); chart.dispose(); }; }, [theme]); useEffect(() => { if (chartRef.current && option) { chartRef.current.setOption(option, true); } }, [option, ...deps]); useEffect(() => { if (!chartRef.current) return; if (loading) { chartRef.current.showLoading('default', { text: '加载中...' }); } else { chartRef.current.hideLoading(); } }, [loading]); return { containerRef, chartRef }; }这段代码做了四件事:第一,组件的containerRef只负责提供一个挂载 DOM;第二,chartRef保存实例,方便后续调用chart.resize()或dispatchAction;第三,setOption(option, true)的第二个参数表示“不合并,整体替换”,这很重要,因为大屏的 option 经常是异步拼接出来的,如果开合并模式,旧数据里的 series 可能会残留;第四,window resize监听统一在这里注册,避免每个图表组件各写一份。
2.2.1 参数和变体说明
上面的封装里,deps参数决定了第二个 effect 什么时候重新执行。常见做法是把“最小必要依赖”传进来,比如地图的geoData,或者某个联动图表的activeKey。如果直接不传 deps,那第二个 effect 依然会被执行,因为每次渲染都会生成新引用,但为了规避 setState 触发无意义的 setOption,还是要传。
主题参数theme可以是 ECharts 内置的'dark',也可以是你自己用echarts.registerTheme注册的主题对象。大屏项目里如果用了开源模板,注意看它的主题是写在init里还是写在配置项里。前者走registerTheme,后者是 option 里的backgroundColor、textStyle等字段。
2.3 图表组件骨架:以“数字翻牌器 + 轮播折线图”为最小可运行样例
光有 Hook 还不够,需要一个实际可复用的组件。以下是一个“基于接口数据呈现轮播折线图”的最小组件结构,大多数开源大屏项目里的统计图都可以按这个模式改造:
import React, { useMemo } from 'react'; import { useECharts } from '@/hooks/useECharts'; import type { EChartsOption, LegendComponentOption } from 'echarts'; interface Props { title: string; timeRange: [string, string]; seriesData: number[][]; } export function TrendChart({ title, timeRange, seriesData }: Props) { const option = useMemo<EChartsOption>(() => { const hours = seriesData.map((item) => item[0]); return { backgroundColor: 'transparent', title: { text: title, left: 20, top: 16, textStyle: { color: '#d3e5ff', fontSize: 16 } }, tooltip: { trigger: 'axis' }, legend: { top: 16, right: 30, data: ['本时段', '昨日同期'], textStyle: { color: '#a8c5e8' } }, grid: { left: 45, right: 20, top: 60, bottom: 40 }, xAxis: { type: 'time', boundaryGap: false, axisLabel: { color: '#7c9bc0' } }, yAxis: { type: 'value', splitLine: { lineStyle: { color: 'rgba(60, 100, 160, 0.25)' } } }, series: [ { name: '本时段', type: 'line', smooth: true, showSymbol: false, areaStyle: { opacity: 0.25 }, data: seriesData.map((it) => [it[0] * 1000, it[1]]) }, { name: '昨日同期', type: 'line', smooth: true, showSymbol: false, lineStyle: { type: 'dashed' }, data: seriesData.map((it) => [it[0] * 1000, it[2]]) } ] }; }, [title, seriesData]); const { containerRef } = useECharts({ option }); return <div ref={containerRef} style={{ width: '100%', height: 280 }} />; }注意这里的useMemo:若不包一层,每次父组件刷新都会重新生成 option 对象,ECharts 内部虽然能处理同值 setOption,但也会做一次 diff 计算,大屏上十几个图表时这个小开销会被放大。代码中 xAxis 使用type: 'time'而不是 category,目的是让轮询上报的新数据点自动落在正确位置上,省去手写刻度对齐的麻烦。
3. 大屏适配策略选择:vw/vh、scale 缩放还是 rem + flex 布局
3.1 三种主流方案的适用边界
“可视化大屏适配”是这类项目里争议最大的一节,因为根本没有银弹。开源项目里常见三种方案:纯 vw/vh、全域 scale 缩放、rem + 弹性布局。很多人把这三种混着用,结果在不同比例的屏幕上要么溢出、要么留黑边。三种方案的对照关系如下:
| 适配方案 | 实现成本 | 文字清晰度 | 比例固定 | 适用场景 |
|---|---|---|---|---|
| vw/vh 布局 | 低 | 清晰,但字号偏差大 | 否,元素会随视口拉伸 | 全屏页,分辨率区间小 |
| scale 缩放 | 中 | 依赖初始设计稿是否高清 | 是,等比缩放 | 只投大屏,不滚动页面 |
| rem + flex | 中高 | 清晰,需配合媒体查询 | 否,长宽比变化时布局会重新流动 | 需要兼顾小屏预览 |
大屏项目通常跑在固定分辨率 LED/拼接屏上,最常见做法是“以 1920x1080 为基准,用 scale 整体缩放”。而开源项目里另一个高频做法是 vw/vh,因为实现最简单——所有组件宽高都写成百分比或 vw 单位,一屏拉满。问题也很明显,当投放屏的长宽比是 16:9 之外的规格时,整个大屏会被拉变形:圆变成椭圆、地图变形、文字加宽。
3.2 一个支持 scale 全局缩放 + onResize 的容器实现
我建议在 React 项目里使用“固定画布 + 容器级缩放”的混合方案。这个思路 Vue 大屏项目里很流行,React 里用一个小工具函数就能实现。基本逻辑是:设计稿固定为 1920x1080,把整个大屏最外层 div 设置成这个宽高,然后通过 CSS transform 的 scale 去适配实际窗口。
import { useEffect, useRef, useState } from 'react'; const DESIGN_WIDTH = 1920; const DESIGN_HEIGHT = 1080; export function useScaleScreen() { const wrapperRef = useRef<HTMLDivElement>(null); const [scale, setScale] = useState(1); useEffect(() => { const updateScale = () => { const wrapper = wrapperRef.current; if (!wrapper) return; const winW = window.innerWidth; const winH = window.innerHeight; const scaleX = winW / DESIGN_WIDTH; const scaleY = winH / DESIGN_HEIGHT; const nextScale = Math.min(scaleX, scaleY); wrapper.style.width = `${DESIGN_WIDTH}px`; wrapper.style.height = `${DESIGN_HEIGHT}px`; wrapper.style.transform = `scale(${nextScale})`; wrapper.style.transformOrigin = 'top left'; setScale(nextScale); }; updateScale(); window.addEventListener('resize', updateScale); return () => window.removeEventListener('resize', updateScale); }, []); return { wrapperRef, scale }; }使用方式:大屏最外层是一个 position fixed 的黑色容器,内部再套一个 wrapperRef 指向的 div。ECharts 实例挂在内部 div 上,所以图表自身只感知 1920x1080 的尺寸,不会触发 resize。需要注意,transform缩放后元素虽然变小,但它在文档流中占用的“逻辑空间”依然占用 1920x1080,因此外层容器应配合position: fixed或overflow: hidden,避免页面出现滚动条。
3.3 适配方案和大屏项目源码阅读的顺序
打开一个开源大屏项目,不要先从图表看起。第一件事是找它的“布局根组件”和样式入口。我一般按这个顺序排查:
- 在 package.json 里看是否有
postcss-px-to-viewport,这代表它可能用了 vw/vh 方案。 - 在全局样式里搜
transform: scale,如果出现在 body 或一级容器样式里,则是缩放方案。 - 看 ECharts 图表组件的 width/height 是固定数字还是百分比,固定数字的通常依赖 scale,百分比则是 vw/vh 流式。
判断完这三点,你就知道这个项目是不是能直接用于你的屏幕。很多开源大屏项目适配目标是“全屏演示用”的 16:9 屏,如果你的使用场景是会议室 1.5 米的横向电视墙,那大概率要把适配方案从 vw/vh 改成 scale,否则地图形状会走样。改的时候不用动每个图表,只需要动根布局组件。
4. 数据处理与轮询方案:从 mock 数据到 WebSocket 推送的切换路径
4.1 大屏的数据链路为什么和普通后台管理系统不一样
普通中后台页面的数据流是“进入页面发请求 -> 拿到数据渲染 -> 操作后再刷新”,关注的是用户交互节点。大屏正好相反,大部分时间无人操作,数据靠自动刷新或推送。因此代码里需要有一个“数据入口”和“展示层”的隔离。开源的 React 大屏项目里,常见的数据来源是本地 mock 文件,也可能是 axios 封装后的 HTTP 请求。接手项目后你要做的,是把这一层替换为公司实际的数据接口,并保留自动刷新机制。
这里有个常见问题:轮询时间设得太短,图表闪个不停;设得太长,“实时”大屏变“T+5 分钟”大屏。根据经验,数值型指标卡(如销售额、在线人数)建议 5~10 秒一次,趋势类图表建议 30 秒~1 分钟一次。折线图如果是一秒一条数据,那不应该用轮询,应该用 WebSocket。
4.2 基于 AbortController 的轮询封装,避免组件卸载后还在请求
很多大屏模板里的轮询是这么写的:setInterval里请求接口,组件卸载时只clearInterval,但上次请求可能还没结束。切到别的页签后,极端情况下会拿到旧数据又 setOption 一次。用 AbortController 能优雅解决请求取消的问题:
import { useEffect, useRef, useState } from 'react'; interface PollingOptions<T> { fetcher: (signal: AbortSignal) => Promise<T>; intervalMs: number; autoStart?: boolean; } export function usePolling<T>({ fetcher, intervalMs, autoStart = true }: PollingOptions<T>) { const [data, setData] = useState<T | null>(null); const [error, setError] = useState<Error | null>(null); const timerRef = useRef<number>(); const abortRef = useRef<AbortController>(); const stop = () => { if (timerRef.current) window.clearInterval(timerRef.current); abortRef.current?.abort(); }; const start = async () => { stop(); const request = async () => { const controller = new AbortController(); abortRef.current = controller; try { const res = await fetcher(controller.signal); setData(res); setError(null); } catch (e) { if (e instanceof DOMException && e.name === 'AbortError') return; setError(e as Error); } }; await request(); timerRef.current = window.setInterval(request, intervalMs); }; useEffect(() => { if (autoStart) { start(); } return stop; }, []); return { data, error, start, stop }; }这个 hook 的关键是stop()里同时做了两件事:清除定时器 + 取消正在进行的请求。React 18 严格模式下 useEffect 会执行两次,若不取消上一次请求,会导致接口重复请求。AbortError在 catch 里单独判断,是因为取消请求属于预期行为,不应该被当成错误上报到监控平台。
4.2.1 连接图表组件的完整写法
拿到数据后,把它转成 ECharts option 的操作写在组件内部,不要写在 usePolling 里。数据层和 UI 层分离是“开源项目能改成企业级数据可视化”的关键。你可以在图表组件里这样组合:
interface FetchDataResponse { timestamps: number[]; values: number[]; } const { data } = usePolling<FetchDataResponse>({ fetcher: async (signal) => { const resp = await fetch('/api/metrics/current', { signal }); return resp.json(); }, intervalMs: 15000, }); const option = useMemo(() => { if (!data) return null; return buildLineOption(data.timestamps, data.values); }, [data]);其中 buildLineOption 是 pure function,它只负责把接口数据映射成 ECharts 识别的结构。这样你把 mock 数据替换成真实接口时,只动 fetcher 函数,图表配置一行不用改。
4.3 图表 key 与卸载策略:轮询高频下如何不卡
当轮询频率变高时,ECharts 的 update 会变得频繁。一个容易忽略的问题是chart.dispose()时机。看开源项目代码时留意里面有没有类似以下这种写法:useEffect(() => {...; return () => chart.clear()}, []),它用的是clear而不是dispose。dispose会销毁实例,重新 init 成本高;clear只是清空画布,实例还在。正确的用法是,组件真正卸载时才dispose,数据更新一律用setOption。
如果你在 react 18 的严格模式下面,会发现图表组件被 mount -> unmount -> mount 两次,如果第一次没有正确 dispose,第二次 init 就会在同一个 dom 上重复初始化。这也是为什么很多开源大屏项目跑到你电脑上控制台会打 “There is a chart instance already initialized on the dom” 的原因——和轮询无关,是严格模式 + 缺少 dispose。
5. 地图类大屏的实现与定制:注册地图数据、背景图叠加与常用交互
5.1 ECharts 地图在开源项目中的常见形态
地图是大屏里最特殊的图表类型,因为其他图表是“数据 + 配置项”,地图必须多一个“地理数据”。开源项目的地图大多基于 GeoJSON,放到 assets/map 目录下,通过echarts.registerMap注册。问题是很多项目并不自带中国的详细 GeoJSON,你得自己去准备。国内地级市或区县级地图精度直接决定效果,地图数据不足时常表现为“区域显示但名字缺失”或“点击区域无响应”。
注册地图的标准流程如下:
import * as echarts from 'echarts'; import chinaJson from '@/assets/map/china.json'; echarts.registerMap('china', chinaJson as any);然后在 option 中使用series: [{ type: 'map', map: 'china' }]就能正常渲染。项目里如果用的是某个市/区的地图,命名建议叫cityName,避免多个地图注册时 key 冲突。地图是否精确到街道,要看业务是“宏观总览”还是“区域下钻”。总览用省市级别即可,下钻则需要多份 GeoJSON 按层级懒加载。
5.2 让 ECharts 地图背景贴合大屏风格的三种做法
热词里提到的“echart 地图加背景图”,本质是把地图和装饰性的背景信息叠在一起。常见做法有三种:
5.2.1 通过 geo 的 itemStyle 设置背景图
在 option 中给 geo 或 series-map 的 itemStyle 写areaColor可以为区域着色,但这不直接支持一张位图。若要背景图显示在地图轮廓内,可以借助backgroundColor或graphic元素。最简单的做法是把背景图铺到整个容器,然后把地图区域颜色设置为半透明:
geo: { map: 'china', roam: false, itemStyle: { areaColor: 'rgba(20, 50, 100, 0.35)', borderColor: '#4f9fff', borderWidth: 1, }, emphasis: { itemStyle: { areaColor: 'rgba(90, 160, 255, 0.6)', }, label: { color: '#fff', fontSize: 14 }, }, label: { show: false }, },这样背景图(比如点阵纹理、渐变光斑)通过 div 背景透过半透明区域显示出来,视觉上就是“地图加背景图”。
5.2.2 使用 graphic 元素定坐标插入背景
如果背景图要跟随地图缩放而缩放(例如地图上有光点跟随城市移动),使用内置graphic.elements插入,保证和地图在同一坐标系下对齐。graphic 里的 x/y 是像素值,如果地图缩放后 alignment 会不准,这是纯 CSS 方案做不到的。实践中可以把图片作为graphic的 type: 'image' 元素,配合left/top百分比定位。
5.2.3 在容器层叠加装饰图
编辑角度最简单:在地图层上方铺一层 pointer-events: none 的装饰 div,直接放地图相关的光效 PNG 或 CSS 动画。多数开源大屏项目都是这么处理的。这种方式会避免你在 ECharts 配置里花大量时间调 graphic 坐标。
5.3 地图右击事件与下钻刷新时的 map 缓存坑
如果项目里响应“echart 框选右击事件”这个诉求,地图区域右击不同于柱状图。常规的events参数写法是chart.on('click', callback),但地图需要区分“点击空白”和“点击区域”。正确写法是监听georoam(缩放事件)以及click事件里读取params.name,同时结合params.componentType判断事件来自 geo 还是 series。
地图下钻最容易踩的坑是重复 registerMap 同名地图。切省级到市级时,不要重新echarts.registerMap('china', newData),应该注册一个新区块名后更新 option:
echarts.registerMap('city', cityGeoJson); chart.setOption({ series: [{ type: 'map', map: 'city' }], });也就是“一套地图数据对应一个唯一名字”。如果反复注册同名地图,ECharts 会保留最后一次注册结果,但旧数据的区域高亮残留,常常会造成“地图更新了,颜色还停留在上一个区域”的错觉。
5.4 设计稿还原与字体比例问题
地图上的标签字号在大屏上需要相对较大,12px 以下的字体在拼接屏上会糊。优化做法是把 label 的 fontSize 写在 option 里,不依赖 CSS。另一个小技巧:地图 Tooltip 在鼠标悬停时容易遮挡相邻区域,可以设置confine: true让提示框不超逸容器边界。具体配置:
tooltip: { trigger: 'item', confine: true, backgroundColor: 'rgba(0, 10, 30, 0.8)', borderColor: '#2563eb', textStyle: { color: '#fff', fontSize: 14 }, }6. 让开源大屏项目能直接上生产:性能优化、字段替换和验收清单
6.1 检查项一:ECharts 是否全量引入
这是带 ECharts 的 React 开源项目最常见的性能隐患。很多模板为了省事在入口写import * as echarts from 'echarts',打包后光图表库就 1MB 以上。按需引入的写法是:
import * as echarts from 'echarts/core'; import { BarChart, LineChart, MapChart, PieChart } from 'echarts/charts'; import { GridComponent, TooltipComponent, LegendComponent, GeoComponent } from 'echarts/components'; import { CanvasRenderer } from 'echarts/renderers'; echarts.use([BarChart, LineChart, MapChart, PieChart, GridComponent, GeoComponent, TooltipComponent, LegendComponent, CanvasRenderer]);做完这一步,查看打包产物体积是不是下降了一大截。该适配在二次改造中的做法是,把这一组 import 收拢到src/plugins/echarts.ts文件,统一注册。之后所有组件里不要再用import * as echarts from 'echarts',改成从这个文件导入echarts。
6.2 检查项二:表结构替换的快速定位法
把开源项目接进自己业务时,最大的成本不是图表样式,而是每一个组件的 mock 数据和服务端返回字段不一致。你可以写一个类型映射工具函数,只暴露转换逻辑,不改动组件:
export function formatSellData(raw: ServerResponse): TrendChartData { return { timeRange: raw.period.map((p) => String(p)), seriesData: raw.records.map((r) => [r.hour, r.amount, r.lastWeekAmount]), }; }这样替换数据源时,你只需要保证 fetcher 返回的 raw 类型正确,组件内部的 series 字段无需知道真实接口长什么样。这也是把“开源项目”变成“企业级数据可视化”的第一步。
6.3 检查项三:大屏有哪些可演进的增量
开一个大屏项目时,验证它是否可用,除了看页面有没有炸,还要做三件事:用 Chrome DevTools 的 performance monitor 观察 CPU 占用;用 Lighthouse/Web Vitals 测加载时长;把窗口拉成非 16:9 长宽比测试布局。在这三项测试里最容易发现问题的集中点在于:
- 布局被拉变形,说明没有用固定比例缩放。
- CPU 长期占用 70% 以上,大概率是 setInterval 里做了纯逻辑操作但没清理。
- 初始化白屏时间超过 2 秒,检查是不是全量引入 ECharts 导致的。
6.4 推进到 SSR 或大屏组态化之前,先解决多图表联动
开源项目给的是单屏静态样例,真正的大屏工程还需要图表联动能力。React 里典型做法是提升 state 到页面级组件中,点击某个柱状图时,修改另一个折线图的 activeKey。ECharts 侧只需要用dispatchAction({ type: 'highlight', seriesIndex: 0 })来高亮对应块。这种联动场景下务必注意每个图表实例都要能通过 ref 访问到,进而在 handler 里调用带业务语义的方法,而不是直接 setOption 整块覆盖。生产环境最后一个建议是保留option.backgroundColor为透明,大屏背景由外层 div 的 CSS 渐变生成,这样在做截图、播放器嵌入或动态更换背景时,不需要重新构建图表库配置。
本文还有配套的精品资源,点击获取