Skip to content

Commit 90f4f0b

Browse files
committed
feat: echarts update
1 parent ed95fae commit 90f4f0b

16 files changed

Lines changed: 842 additions & 1085 deletions

File tree

docs/ECHARTS_BIGSCREEN_REINIT.md

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# bigscreen-charts-reinit 事件说明与升级指南
2+
3+
## 这是什么?
4+
`bigscreen-charts-reinit` 是一个 **BigScreen(大屏)页面专用** 的全局浏览器事件:
5+
6+
```js
7+
window.dispatchEvent(new CustomEvent('bigscreen-charts-reinit'))
8+
```
9+
10+
它的语义不是“重新 init ECharts 实例”,而是:
11+
12+
> **告诉所有大屏里的图表:页面缩放/布局已基本稳定,请做一次“安全的自适应”(优先 `chart.resize()`)。**
13+
14+
目前该事件不携带 payload(没有 detail),属于纯信号型事件。
15+
16+
---
17+
18+
## 为什么需要它?(BigScreen 场景的特殊性)
19+
BigScreen 里存在这些会影响 ECharts 容器尺寸/可见性的因素:
20+
21+
1) **previewFitScale 缩放逻辑**
22+
- BigScreen 会调用 `previewFitScale(...)`,内部会计算比例并对容器做 transform / 尺寸调整。
23+
- transform/尺寸变化发生在时间线上是“先渲染 DOM,再计算/再缩放”。
24+
25+
2) **KeepAlive 激活/失活**
26+
- 页面被 KeepAlive 缓存时,重新进入页面会触发 `useActivate`
27+
- 激活时容器可能刚从不可见/旧缩放状态切回可见状态。
28+
29+
3) **路由切换到 /big-screen**
30+
- 路由进入时容器尺寸、字体、布局往往还在抖动/稳定中。
31+
32+
ECharts 的已知行为:
33+
- **初始化时容器尺寸为 0 或尺寸不稳定**,容易出现空白、布局错位。
34+
- **容器尺寸发生变化后**(尤其是缩放/激活带来的变化),需要显式 `chart.resize()` 才能正确重排。
35+
36+
你们目前的通用封装 `EChart` 已经做了两件关键事:
37+
- 如果容器宽高为 0,会通过 `ResizeObserver` **延迟 init**,直到可测量。
38+
- 支持 `ResizeObserver` **自动 resize**
39+
40+
但在 BigScreen 里,仍然可能出现:
41+
- 容器“看起来变了”(transform/激活),但 `ResizeObserver` 触发时机不够可靠/不够晚。
42+
- 所以 BigScreen 需要一个“布局稳定点”的信号,让图表再做一次最终 `resize()`
43+
44+
---
45+
46+
## 目前谁在触发它?(触发方)
47+
触发方是 BigScreen 页面:[src/pages/bigScreen/index.jsx](../src/pages/bigScreen/index.jsx)
48+
49+
关键点:
50+
- `scheduleChartsReinit(delayMs)`**去重**:同一时间窗口内多次调用只触发最后一次。
51+
- 触发前会先 `calcRate()`,尽量保证缩放计算完成。
52+
53+
触发时机(当前三类):
54+
1) 初次挂载:`calcRate()` + `windowResize()`(此处不 dispatch event)
55+
2) KeepAlive 激活:将容器 transform 设为 `scale(1, 1)`,再延迟 dispatch
56+
3) 路由进入 `/big-screen`:延迟 dispatch
57+
58+
---
59+
60+
## 目前谁在监听它?(消费方)
61+
### 1) 通用兼容组件(推荐使用)
62+
`EChartsCommon`[src/components/stateless/EChartsCommon/index.tsx](../src/components/stateless/EChartsCommon/index.tsx)
63+
64+
当前策略(很关键):
65+
- **优先 `resize()`**(避免 dispose/remount 导致的闪烁)
66+
- 只有在没有实例(极端情况)才会通过 `key` 变化触发 remount
67+
68+
### 2) 个别图表组件(已改为 resize-only)
69+
- `PieNestCharts`[src/components/stateless/PieNestCharts/index.jsx](../src/components/stateless/PieNestCharts/index.jsx)
70+
- `DonutCharts`[src/components/stateless/DonutCharts/index.jsx](../src/components/stateless/DonutCharts/index.jsx)
71+
72+
当前都是:监听事件 -> `chartHandleRef.current?.resize?.()`,不再 remount。
73+
74+
---
75+
76+
## 这几个“场景”的关系是什么?(时序视角)
77+
可以把 BigScreen 里图表状态变化分成三个阶段:
78+
79+
1) **页面首次进入(mount)**
80+
- React 渲染 DOM
81+
- `previewFitScale` 初始化并计算缩放
82+
- 图表可能会 init(或延迟 init)
83+
84+
2) **页面激活(KeepAlive activate)**
85+
- DOM 已存在,但可能刚从隐藏/旧缩放状态恢复
86+
- BigScreen 会触发一次 `bigscreen-charts-reinit`
87+
- 图表收到信号后执行一次 `resize()`,让布局贴合当前缩放
88+
89+
3) **路由切回 /big-screen**
90+
- 类似激活:容器尺寸/缩放在短时间内稳定
91+
- BigScreen 会延迟触发一次 `bigscreen-charts-reinit`
92+
- 图表执行 `resize()` 做最终对齐
93+
94+
一句话:
95+
- `bigscreen-charts-reinit` 是 BigScreen 的“布局稳定点广播”。
96+
97+
---
98+
99+
## 以后所有 ECharts 图表都要基于它再次封装吗?
100+
不建议把它当作“全局通用机制”。建议分层:
101+
102+
### A. 普通页面(非大屏)
103+
- **只用 `EChart` 封装**即可:它已经负责 init/resize/option 应用。
104+
- 不应依赖 `bigscreen-charts-reinit`,避免隐式耦合 BigScreen。
105+
106+
### B. BigScreen 页面内的图表
107+
- 推荐两种方式之一:
108+
1)`EChartsCommon`(它已经内置监听 + resize 优先策略)
109+
2) 直接用 `EChart`,并在组件内监听 `bigscreen-charts-reinit` -> 调 `resize()`
110+
111+
### C. “再封装”的正确方向
112+
更好的再封装点通常是:
113+
- 统一的 `EChart`(生命周期、ResizeObserver、setOption、事件绑定)
114+
- BigScreen 专用的“激活/缩放同步策略”(监听该事件并调用 resize)
115+
116+
也就是说:
117+
- `EChart` 是通用底座
118+
- `bigscreen-charts-reinit` 是 BigScreen 的“外部环境信号”
119+
120+
---
121+
122+
## 升级/迁移规范(强约束)
123+
1) **禁止**`bigscreen-charts-reinit` 上做 dispose/remount
124+
- 目标是:只 `resize()`,不销毁实例。
125+
- remount 会导致画布闪烁、tooltip 状态丢失、动画重复。
126+
127+
2) 新增图表组件优先:
128+
- 普通页面:直接用 `EChart`
129+
- BigScreen:用 `EChartsCommon` 或在组件内监听事件并 `resize()`
130+
131+
3) 事件只解决“布局稳定点”问题
132+
- 不要把数据刷新、主题切换等逻辑塞进该事件。
133+
134+
---
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { useEffect } from 'react'
2+
3+
const DEFAULT_BIGSCREEN_CHARTS_REINIT_EVENT = 'bigscreen-charts-reinit'
4+
const DEFAULT_REINIT_DELAY_MS = 50
5+
6+
export type ResizeCapableHandle = {
7+
getInstance?: () => unknown
8+
resize?: () => void
9+
}
10+
11+
export type UseBigScreenChartsReinitOptions = {
12+
enabled?: boolean
13+
/** 仅在特殊场景需要覆盖,默认使用 50ms */
14+
delayMs?: number
15+
/** 仅在特殊场景需要覆盖,默认使用 `bigscreen-charts-reinit` */
16+
eventName?: string
17+
onMissingInstance?: () => void
18+
}
19+
20+
/**
21+
* BigScreen 专用:监听 `bigscreen-charts-reinit`,在布局稳定后触发一次图表自适应(优先 resize)。
22+
*
23+
* 约定:
24+
* - 不做 dispose/remount(除非调用方通过 onMissingInstance 自己兜底)
25+
* - 允许延迟执行,避免容器仍在缩放/渲染中
26+
*/
27+
export default function useBigScreenChartsReinit(
28+
chartHandleRef: React.RefObject<ResizeCapableHandle | null>,
29+
options: UseBigScreenChartsReinitOptions = {}
30+
) {
31+
const {
32+
enabled = true,
33+
delayMs = DEFAULT_REINIT_DELAY_MS,
34+
eventName = DEFAULT_BIGSCREEN_CHARTS_REINIT_EVENT,
35+
onMissingInstance,
36+
} = options
37+
38+
useEffect(() => {
39+
if (!enabled) return
40+
41+
const handleReinit = () => {
42+
window.setTimeout(() => {
43+
const handle = chartHandleRef.current
44+
if (!handle) {
45+
onMissingInstance?.()
46+
return
47+
}
48+
49+
const hasInstance = typeof handle.getInstance === 'function' ? Boolean(handle.getInstance()) : true
50+
if (hasInstance && typeof handle.resize === 'function') {
51+
handle.resize()
52+
return
53+
}
54+
55+
onMissingInstance?.()
56+
}, delayMs)
57+
}
58+
59+
window.addEventListener(eventName, handleReinit)
60+
return () => window.removeEventListener(eventName, handleReinit)
61+
}, [chartHandleRef, delayMs, enabled, eventName, onMissingInstance])
62+
}

src/components/stateless/DonutCharts/index.jsx

Lines changed: 20 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import React, { useEffect, useRef, useCallback } from 'react'
2-
import * as echarts from 'echarts'
3-
import { normalizeEChartsOption } from '@utils/echarts/normalizeOption'
1+
import React, { useMemo, useRef, useCallback } from 'react'
2+
import EChart from '@stateless/EChart'
43
import PropTypes from 'prop-types'
4+
import useBigScreenChartsReinit from '@/components/hooks/useBigScreenChartsReinit'
55

66
const DonutChart = ({ height = '100%', eOptions }) => {
7-
const chartRef = useRef(null)
87
const chartInstance = useRef(null)
8+
const chartHandleRef = useRef(null)
99

1010
const colors = [
1111
['#2878FF', '#73B9FF'],
@@ -33,25 +33,11 @@ const DonutChart = ({ height = '100%', eOptions }) => {
3333
['#F8C106', '#FFEAA4'],
3434
]
3535

36-
// 处理窗口大小变化
37-
const handleResize = () => {
38-
if (chartInstance.current) {
39-
chartInstance.current.resize()
40-
}
41-
}
42-
43-
// 初始化图表
44-
const initChart = () => {
45-
if (!chartRef.current) return
46-
47-
// 销毁旧实例
48-
if (chartInstance.current) {
49-
chartInstance.current.dispose()
50-
}
51-
52-
// 创建新实例
53-
chartInstance.current = echarts.init(chartRef.current)
36+
const handleChartInit = useCallback((chart) => {
37+
chartInstance.current = chart
38+
}, [])
5439

40+
const option = useMemo(() => {
5541
const data = eOptions?.data?.map((item, index) => ({
5642
...item,
5743
tooltip: {
@@ -152,16 +138,20 @@ const DonutChart = ({ height = '100%', eOptions }) => {
152138
},
153139
},
154140
labelLayout: (params) => {
155-
const isLeft = params.labelRect.x < chartInstance.current.getWidth() / 2
141+
const width = chartInstance.current?.getWidth?.() ?? 0
142+
const isLeft = width ? params.labelRect.x < width / 2 : params.labelRect.x < 0
156143
const points = params.labelLinePoints
144+
if (!points?.[2]) {
145+
return { hideOverlap: true }
146+
}
157147
points[2][0] = isLeft ? params.labelRect.x : params.labelRect.x + params.labelRect.width
158148
return {
159149
labelLinePoints: points,
160150
hideOverlap: true,
161151
}
162152
},
163153
animationEasing: 'elasticOut',
164-
animationDelay: () => Math.random() * 200,
154+
animationDelay: (idx) => idx * 10,
165155
}
166156

167157
// 配置项
@@ -190,47 +180,14 @@ const DonutChart = ({ height = '100%', eOptions }) => {
190180
}))
191181
: [optionSerie],
192182
}
183+
return defaultOption
184+
}, [colors, eOptions])
193185

194-
normalizeEChartsOption(defaultOption)
195-
chartInstance.current.setOption(defaultOption)
196-
}
197-
198-
// 重新初始化图表
199-
const reinitChart = useCallback(() => {
200-
if (chartInstance.current) {
201-
chartInstance.current.dispose()
202-
}
203-
initChart()
204-
}, [initChart])
205-
206-
// 组件挂载和卸载时的处理
207-
useEffect(() => {
208-
initChart()
209-
window.addEventListener('resize', handleResize)
210-
211-
// 监听 BigScreen 页面重新初始化事件
212-
const handleReinit = () => {
213-
reinitChart()
214-
}
215-
window.addEventListener('bigscreen-charts-reinit', handleReinit)
216-
217-
return () => {
218-
window.removeEventListener('resize', handleResize)
219-
window.removeEventListener('bigscreen-charts-reinit', handleReinit)
220-
if (chartInstance.current) {
221-
chartInstance.current.dispose()
222-
}
223-
}
224-
}, [reinitChart])
225-
226-
// 当props变化时重新初始化图表
227-
useEffect(() => {
228-
if (eOptions) {
229-
initChart()
230-
}
231-
}, [eOptions])
186+
useBigScreenChartsReinit(chartHandleRef)
232187

233-
return <div ref={chartRef} style={{ height, width: '100%' }} />
188+
return (
189+
<EChart ref={chartHandleRef} option={option} onInit={handleChartInit} notMerge style={{ height, width: '100%' }} />
190+
)
234191
}
235192

236193
DonutChart.propTypes = {

0 commit comments

Comments
 (0)