@agions/taroviz 1.2.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,146 +1,127 @@
1
1
  /**
2
- * TaroViz Error Boundary
3
- * 图表组件错误边界,用于捕获子组件错误并显示备用 UI
2
+ * TaroViz 错误边界组件
3
+ * 捕获图表渲染过程中的错误,防止整个应用崩溃
4
4
  */
5
- import React, { Component, ReactNode } from 'react';
5
+ import React, { Component, ErrorInfo, ReactNode } from 'react';
6
6
 
7
- // ============================================================================
8
- // 类型定义
9
- // ============================================================================
10
-
11
- /** 错误边界属性 */
12
7
  export interface ErrorBoundaryProps {
13
8
  /** 子组件 */
14
9
  children: ReactNode;
15
10
  /** 错误回调 */
16
- onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
17
- /** 错误时显示的备用 UI */
18
- fallback?: ReactNode;
11
+ onError?: (error: Error, errorInfo: ErrorInfo) => void;
12
+ /** 自定义错误展示 */
13
+ fallback?: (error: Error, reset: () => void) => ReactNode;
19
14
  /** 是否显示错误详情 */
20
15
  showDetails?: boolean;
21
- /** 重试回调 */
22
- onRetry?: () => void;
23
16
  }
24
17
 
25
- /** 错误边界状态 */
26
18
  export interface ErrorBoundaryState {
27
- /** 是否发生错误 */
28
19
  hasError: boolean;
29
- /** 错误对象 */
30
20
  error: Error | null;
31
- /** 错误信息 */
32
- errorInfo: React.ErrorInfo | null;
33
21
  }
34
22
 
35
- // ============================================================================
36
- // Error Boundary 组件
37
- // ============================================================================
38
-
39
23
  /**
40
- * 图表错误边界组件
41
- * 用于捕获图表渲染过程中的错误,并显示友好的备用 UI
42
- *
43
- * @example
44
- * ```tsx
45
- * import { ChartErrorBoundary } from '@agions/taroviz'
46
- *
47
- * function App() {
48
- * return (
49
- * <ChartErrorBoundary
50
- * onError={(error) => console.error(error)}
51
- * onRetry={() => console.log('Retrying...')}
52
- * >
53
- * <LineChart data={data} />
54
- * </ChartErrorBoundary>
55
- * )
56
- * }
57
- * ```
24
+ * 错误边界组件
25
+ * 用于捕获子组件树中的 JavaScript 错误
58
26
  */
59
- export class ChartErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
27
+ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
60
28
  constructor(props: ErrorBoundaryProps) {
61
29
  super(props);
62
30
  this.state = {
63
31
  hasError: false,
64
32
  error: null,
65
- errorInfo: null,
66
33
  };
67
34
  }
68
35
 
69
- static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
36
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
70
37
  return {
71
38
  hasError: true,
72
39
  error,
73
40
  };
74
41
  }
75
42
 
76
- componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
77
- const { onError } = this.props;
78
-
79
- console.error('Chart Error:', error, errorInfo);
80
-
81
- this.setState({
82
- error,
83
- errorInfo,
84
- });
43
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
44
+ // 调用错误回调
45
+ this.props.onError?.(error, errorInfo);
85
46
 
86
- if (onError) {
87
- onError(error, errorInfo);
88
- }
47
+ // 打印错误日志
48
+ console.error('[TaroViz] Chart rendering error:', error, errorInfo);
89
49
  }
90
50
 
91
- handleRetry = (): void => {
92
- const { onRetry } = this.props;
93
-
51
+ handleReset = (): void => {
94
52
  this.setState({
95
53
  hasError: false,
96
54
  error: null,
97
- errorInfo: null,
98
55
  });
99
-
100
- if (onRetry) {
101
- onRetry();
102
- }
103
56
  };
104
57
 
105
58
  render(): ReactNode {
106
- const { children, fallback, showDetails = false, onRetry } = this.props;
107
- const { hasError, error, errorInfo } = this.state;
59
+ const { hasError, error } = this.state;
60
+ const { children, fallback, showDetails = false } = this.props;
108
61
 
109
- if (hasError) {
110
- // 如果提供了自定义 fallback,使用它
62
+ if (hasError && error) {
63
+ // 使用自定义 fallback
111
64
  if (fallback) {
112
- return fallback;
65
+ return fallback(error, this.handleReset);
113
66
  }
114
67
 
115
- // 默认错误 UI
68
+ // 默认错误展示
116
69
  return (
117
- <div style={styles.container}>
118
- <div style={styles.errorBox}>
119
- <div style={styles.icon}>⚠️</div>
120
- <div style={styles.title}>图表渲染出错</div>
121
-
122
- {showDetails && error && (
123
- <div style={styles.errorMessage}>{error.message || '未知错误'}</div>
124
- )}
125
-
126
- {(onRetry || showDetails) && (
127
- <div style={styles.actions}>
128
- {onRetry && (
129
- <button style={styles.retryButton} onClick={this.handleRetry}>
130
- 🔄 重试
131
- </button>
132
- )}
133
- {showDetails && errorInfo && (
134
- <details style={styles.details}>
135
- <summary style={styles.summary}>查看详情</summary>
136
- <pre style={styles.pre}>
137
- {error?.stack || errorInfo.componentStack || '无详细信息'}
138
- </pre>
139
- </details>
140
- )}
141
- </div>
142
- )}
143
- </div>
70
+ <div
71
+ style={{
72
+ display: 'flex',
73
+ flexDirection: 'column',
74
+ alignItems: 'center',
75
+ justifyContent: 'center',
76
+ padding: '20px',
77
+ backgroundColor: '#fff',
78
+ border: '1px solid #ff4d4f',
79
+ borderRadius: '8px',
80
+ color: '#333',
81
+ minHeight: '200px',
82
+ }}
83
+ >
84
+ <div style={{ fontSize: '48px', marginBottom: '16px' }}>⚠️</div>
85
+ <h3 style={{ margin: '0 0 12px', color: '#ff4d4f' }}>图表渲染失败</h3>
86
+ <p style={{ margin: '0 0 16px', color: '#666', textAlign: 'center' }}>
87
+ 图表在渲染过程中遇到错误,请检查数据配置是否正确
88
+ </p>
89
+ {showDetails && (
90
+ <details
91
+ style={{
92
+ width: '100%',
93
+ padding: '12px',
94
+ backgroundColor: '#f5f5f5',
95
+ borderRadius: '4px',
96
+ fontSize: '12px',
97
+ fontFamily: 'monospace',
98
+ overflow: 'auto',
99
+ maxHeight: '150px',
100
+ }}
101
+ >
102
+ <summary style={{ cursor: 'pointer', marginBottom: '8px' }}>错误详情</summary>
103
+ <pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
104
+ {error.message}
105
+ {'\n\n'}
106
+ {error.stack}
107
+ </pre>
108
+ </details>
109
+ )}
110
+ <button
111
+ onClick={this.handleReset}
112
+ style={{
113
+ marginTop: '16px',
114
+ padding: '8px 24px',
115
+ backgroundColor: '#1890ff',
116
+ color: '#fff',
117
+ border: 'none',
118
+ borderRadius: '4px',
119
+ cursor: 'pointer',
120
+ fontSize: '14px',
121
+ }}
122
+ >
123
+ 重试
124
+ </button>
144
125
  </div>
145
126
  );
146
127
  }
@@ -149,310 +130,24 @@ export class ChartErrorBoundary extends Component<ErrorBoundaryProps, ErrorBound
149
130
  }
150
131
  }
151
132
 
152
- // ============================================================================
153
- // 懒加载组件
154
- // ============================================================================
155
-
156
- /** 懒加载属性 */
157
- export interface LazyChartProps {
158
- /** 加载时的占位内容 */
159
- loading?: ReactNode;
160
- /** 懒加载的图表组件 */
161
- children: ReactNode;
162
- /** 加载延迟 (ms),默认 200 */
163
- delay?: number;
164
- }
165
-
166
133
  /**
167
- * 图表懒加载组件
168
- * 用于延迟加载图表组件,优化首屏渲染性能
169
- *
170
- * @example
171
- * ```tsx
172
- * import { LazyChart } from '@agions/taroviz'
173
- *
174
- * function App() {
175
- * return (
176
- * <LazyChart loading={<div>加载中...</div>}>
177
- * <LineChart data={data} />
178
- * </LazyChart>
179
- * )
180
- * }
181
- * ```
134
+ * 创建带错误边界的图表组件
135
+ * @param ChartComponent 图表组件
136
+ * @returns 带错误边界的图表组件
182
137
  */
183
- export function LazyChart({ children, loading = null, delay = 200 }: LazyChartProps) {
184
- const [show, setShow] = React.useState(false);
185
-
186
- React.useEffect(() => {
187
- const timer = setTimeout(() => {
188
- setShow(true);
189
- }, delay);
190
-
191
- return () => clearTimeout(timer);
192
- }, [delay]);
193
-
194
- if (!show) {
195
- return (
196
- <div style={styles.lazyContainer}>
197
- {loading || (
198
- <div style={styles.loading}>
199
- <div style={styles.spinner}></div>
200
- <span>加载图表中...</span>
201
- </div>
202
- )}
203
- </div>
204
- );
205
- }
206
-
207
- return <>{children}</>;
208
- }
209
-
210
- // ============================================================================
211
- // 图表 Suspense
212
- // ============================================================================
213
-
214
- /**
215
- * 图表加载状态组件
216
- * 用于在图表数据加载过程中显示加载动画
217
- *
218
- * @example
219
- * ```tsx
220
- * import { ChartSkeleton } from '@agions/taroviz'
221
- *
222
- * function App() {
223
- * return (
224
- * <ChartSkeleton type="line" />
225
- * )
226
- * }
227
- * ```
228
- */
229
- export interface ChartSkeletonProps {
230
- /** 图表类型 */
231
- type?: 'line' | 'bar' | 'pie' | 'radar' | 'gauge';
232
- /** 宽度 */
233
- width?: string | number;
234
- /** 高度 */
235
- height?: string | number;
236
- }
237
-
238
- /**
239
- * 图表骨架屏组件
240
- * 在图表加载时显示占位动画
241
- */
242
- export function ChartSkeleton({ type = 'line', width = '100%', height = 400 }: ChartSkeletonProps) {
243
- const containerStyle: React.CSSProperties = {
244
- width,
245
- height,
246
- display: 'flex',
247
- flexDirection: 'column',
248
- gap: '12px',
249
- padding: '16px',
250
- };
251
-
252
- const renderSkeleton = () => {
253
- switch (type) {
254
- case 'line':
255
- case 'bar':
256
- return (
257
- <div style={styles.skeletonChart}>
258
- <div style={styles.skeletonBars}>
259
- {[40, 65, 45, 80, 55, 70].map((h, i) => (
260
- <div
261
- key={i}
262
- style={{
263
- ...styles.skeletonBar,
264
- height: `${h}%`,
265
- animationDelay: `${i * 0.1}s`,
266
- }}
267
- />
268
- ))}
269
- </div>
270
- </div>
271
- );
272
- case 'pie':
273
- return (
274
- <div style={styles.skeletonChart}>
275
- <div style={styles.skeletonPie}></div>
276
- </div>
277
- );
278
- case 'radar':
279
- return (
280
- <div style={styles.skeletonChart}>
281
- <div style={styles.skeletonRadar}></div>
282
- </div>
283
- );
284
- case 'gauge':
285
- return (
286
- <div style={styles.skeletonChart}>
287
- <div style={styles.skeletonGauge}></div>
288
- </div>
289
- );
290
- default:
291
- return null;
292
- }
293
- };
294
-
295
- return (
296
- <div style={containerStyle}>
297
- <div style={styles.skeletonTitle}></div>
298
- {renderSkeleton()}
299
- </div>
138
+ export function withErrorBoundary<Props extends object>(
139
+ ChartComponent: React.ComponentType<Props>,
140
+ errorBoundaryProps?: Omit<ErrorBoundaryProps, 'children'>
141
+ ): React.ComponentType<Props> {
142
+ const WrappedChart: React.FC<Props> = (props) => (
143
+ <ErrorBoundary {...errorBoundaryProps}>
144
+ <ChartComponent {...props} />
145
+ </ErrorBoundary>
300
146
  );
301
- }
302
147
 
303
- // ============================================================================
304
- // 样式
305
- // ============================================================================
148
+ WrappedChart.displayName = `withErrorBoundary(${ChartComponent.displayName || ChartComponent.name || 'Chart'})`;
306
149
 
307
- const styles: Record<string, React.CSSProperties> = {
308
- container: {
309
- display: 'flex',
310
- alignItems: 'center',
311
- justifyContent: 'center',
312
- minHeight: '200px',
313
- padding: '20px',
314
- },
315
- errorBox: {
316
- textAlign: 'center',
317
- padding: '24px',
318
- background: 'rgba(239, 68, 68, 0.1)',
319
- border: '1px solid rgba(239, 68, 68, 0.3)',
320
- borderRadius: '8px',
321
- maxWidth: '400px',
322
- },
323
- icon: {
324
- fontSize: '32px',
325
- marginBottom: '8px',
326
- },
327
- title: {
328
- fontSize: '16px',
329
- fontWeight: 600,
330
- color: '#dc2626',
331
- marginBottom: '8px',
332
- },
333
- errorMessage: {
334
- fontSize: '14px',
335
- color: '#991b1b',
336
- marginBottom: '16px',
337
- padding: '8px',
338
- background: 'rgba(239, 68, 68, 0.1)',
339
- borderRadius: '4px',
340
- },
341
- actions: {
342
- display: 'flex',
343
- flexDirection: 'column',
344
- gap: '12px',
345
- alignItems: 'center',
346
- },
347
- retryButton: {
348
- padding: '8px 16px',
349
- background: '#dc2626',
350
- color: 'white',
351
- border: 'none',
352
- borderRadius: '6px',
353
- cursor: 'pointer',
354
- fontSize: '14px',
355
- },
356
- details: {
357
- textAlign: 'left',
358
- width: '100%',
359
- },
360
- summary: {
361
- cursor: 'pointer',
362
- color: '#666',
363
- fontSize: '13px',
364
- marginBottom: '8px',
365
- },
366
- pre: {
367
- fontSize: '12px',
368
- background: '#1a1a1a',
369
- color: '#e0e0e0',
370
- padding: '12px',
371
- borderRadius: '4px',
372
- overflow: 'auto',
373
- maxHeight: '200px',
374
- },
375
- lazyContainer: {
376
- display: 'flex',
377
- alignItems: 'center',
378
- justifyContent: 'center',
379
- minHeight: '200px',
380
- },
381
- loading: {
382
- display: 'flex',
383
- flexDirection: 'column',
384
- alignItems: 'center',
385
- gap: '12px',
386
- color: '#666',
387
- },
388
- spinner: {
389
- width: '32px',
390
- height: '32px',
391
- border: '3px solid #e0e0e0',
392
- borderTopColor: '#2d8a8a',
393
- borderRadius: '50%',
394
- animation: 'spin 1s linear infinite',
395
- },
396
- skeletonChart: {
397
- flex: 1,
398
- display: 'flex',
399
- alignItems: 'flex-end',
400
- justifyContent: 'center',
401
- },
402
- skeletonBars: {
403
- display: 'flex',
404
- alignItems: 'flex-end',
405
- gap: '12px',
406
- height: '100%',
407
- width: '100%',
408
- },
409
- skeletonBar: {
410
- flex: 1,
411
- background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
412
- backgroundSize: '200% 100%',
413
- animation: 'shimmer 1.5s infinite',
414
- borderRadius: '4px 4px 0 0',
415
- },
416
- skeletonTitle: {
417
- width: '40%',
418
- height: '20px',
419
- background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
420
- backgroundSize: '200% 100%',
421
- animation: 'shimmer 1.5s infinite',
422
- borderRadius: '4px',
423
- },
424
- skeletonPie: {
425
- width: '150px',
426
- height: '150px',
427
- borderRadius: '50%',
428
- background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
429
- backgroundSize: '200% 100%',
430
- animation: 'shimmer 1.5s infinite',
431
- },
432
- skeletonRadar: {
433
- width: '150px',
434
- height: '150px',
435
- background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
436
- backgroundSize: '200% 100%',
437
- animation: 'shimmer 1.5s infinite',
438
- clipPath: 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)',
439
- },
440
- skeletonGauge: {
441
- width: '200px',
442
- height: '100px',
443
- background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
444
- backgroundSize: '200% 100%',
445
- animation: 'shimmer 1.5s infinite',
446
- borderRadius: '100px 100px 0 0',
447
- },
448
- };
449
-
450
- // ============================================================================
451
- // 导出
452
- // ============================================================================
150
+ return WrappedChart;
151
+ }
453
152
 
454
- export default {
455
- ChartErrorBoundary,
456
- LazyChart,
457
- ChartSkeleton,
458
- };
153
+ export default ErrorBoundary;