@agions/taroviz 1.7.0 → 1.10.0

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.
Files changed (34) hide show
  1. package/README.md +7 -3
  2. package/dist/cjs/index.js +1 -1
  3. package/dist/esm/index.js +960 -136
  4. package/package.json +1 -1
  5. package/src/adapters/__tests__/index.test.ts +4 -2
  6. package/src/adapters/h5/index.ts +16 -0
  7. package/src/adapters/types.ts +28 -120
  8. package/src/charts/boxplot/types.ts +5 -3
  9. package/src/charts/common/BaseChartWrapper.tsx +193 -32
  10. package/src/charts/liquid/index.tsx +6 -5
  11. package/src/charts/liquid/types.ts +4 -4
  12. package/src/charts/parallel/types.ts +6 -3
  13. package/src/charts/tree/types.ts +4 -4
  14. package/src/charts/types.ts +1 -1
  15. package/src/core/animation/AnimationManager.ts +69 -42
  16. package/src/core/components/Annotation.tsx +12 -10
  17. package/src/core/components/BaseChart.tsx +75 -12
  18. package/src/core/components/ErrorBoundary.tsx +30 -17
  19. package/src/core/components/LazyChart.tsx +14 -9
  20. package/src/core/themes/ThemeManager.ts +33 -0
  21. package/src/core/types/common.ts +21 -110
  22. package/src/core/types/index.ts +4 -135
  23. package/src/core/types/platform.ts +38 -230
  24. package/src/core/utils/chartUtils.ts +8 -3
  25. package/src/core/utils/export/ExportUtils.ts +10 -1
  26. package/src/core/utils/performance/PerformanceAnalyzer.ts +21 -1
  27. package/src/core/utils/performance/types.ts +5 -0
  28. package/src/hooks/__tests__/index.test.tsx +7 -5
  29. package/src/hooks/index.ts +23 -1
  30. package/src/hooks/useAnimation.ts +427 -0
  31. package/src/hooks/useChartHistory.ts +273 -0
  32. package/src/hooks/useChartSelection.ts +350 -0
  33. package/src/hooks/usePerformance.ts +291 -0
  34. package/src/themes/__tests__/index.test.ts +7 -13
@@ -2,17 +2,20 @@
2
2
  * 平行坐标图类型定义
3
3
  */
4
4
 
5
+ import type { EChartsType, ECElementEvent } from 'echarts';
6
+ import type { LoadingOptions } from '../types';
7
+
5
8
  export type ParallelChartProps = {
6
9
  option?: ParallelOption;
7
10
  width?: string | number;
8
11
  height?: string | number;
9
12
  className?: string;
10
13
  style?: React.CSSProperties;
11
- onEvents?: Record<string, (params: any) => void>;
14
+ onEvents?: Record<string, (params: ECElementEvent) => void>;
12
15
  loading?: boolean;
13
- loadingOption?: any;
16
+ loadingOption?: LoadingOptions;
14
17
  theme?: string;
15
- onChartReady?: (chart: any) => void;
18
+ onChartReady?: (chart: EChartsType) => void;
16
19
  opts?: {
17
20
  devicePixelRatio?: number;
18
21
  renderer?: 'canvas' | 'svg';
@@ -2,7 +2,7 @@
2
2
  * 树图类型定义
3
3
  * ECharts 内置 tree 类型
4
4
  */
5
- import type { EChartsOption } from 'echarts';
5
+ import type { EChartsOption, EChartsType, ECElementEvent } from 'echarts';
6
6
 
7
7
  // ============================================================================
8
8
  // 树图数据节点
@@ -166,9 +166,9 @@ export interface TreeChartProps {
166
166
  /** 加载配置 */
167
167
  loadingOption?: Record<string, unknown>;
168
168
  /** 图表初始化回调 */
169
- onChartInit?: (chart: any) => void;
169
+ onChartInit?: (chart: EChartsType) => void;
170
170
  /** 图表就绪回调 */
171
- onChartReady?: (chart: any) => void;
171
+ onChartReady?: (chart: EChartsType) => void;
172
172
  /** 事件回调 */
173
- onEvents?: Record<string, (params: any) => void>;
173
+ onEvents?: Record<string, (params: ECElementEvent) => void>;
174
174
  }
@@ -681,7 +681,7 @@ export interface SankeyChartProps extends BaseChartProps {
681
681
  nodeWidth?: number;
682
682
 
683
683
  /** 节点排序方式 */
684
- nodeSort?: 'ascending' | 'descending' | 'none' | ((a: any, b: any) => number);
684
+ nodeSort?: 'ascending' | 'descending' | 'none' | ((a: unknown, b: unknown) => number);
685
685
 
686
686
  /** 边的曲度 */
687
687
  linkCurveness?: number;
@@ -13,21 +13,33 @@ import {
13
13
  } from './types';
14
14
 
15
15
  /**
16
- * 动画预设集合
16
+ * Checks the OS/browser prefers-reduced-motion setting.
17
+ * Returns true if the user has requested reduced motion.
18
+ */
19
+ function prefersReducedMotion(): boolean {
20
+ if (typeof window === 'undefined') return false;
21
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
22
+ }
23
+
24
+ /**
25
+ * Professional animation presets following frontend-design-pro skill guidelines:
26
+ * - Easing: cubic-bezier(0.16, 1, 0.3, 1) ("cubicOut") for natural deceleration
27
+ * - Durations: 100-200ms micro, 300-500ms transitions, never >600ms
28
+ * - NO bounce/elastic — anti-patterns that feel廉价 (cheap)
17
29
  */
18
30
  const DEFAULT_ANIMATION_PRESETS: AnimationPreset[] = [
19
31
  {
20
32
  name: 'default',
21
- description: '默认动画配置',
33
+ description: '默认动画配置 — 专业级 (300-500ms, cubicOut)',
22
34
  config: {
23
35
  enabled: true,
24
- duration: 1000,
36
+ duration: 400,
25
37
  easing: 'cubicOut',
26
- appearDuration: 1200,
38
+ appearDuration: 450,
27
39
  appearEasing: 'cubicOut',
28
- updateDuration: 800,
40
+ updateDuration: 300,
29
41
  updateEasing: 'cubicOut',
30
- disappearDuration: 600,
42
+ disappearDuration: 250,
31
43
  disappearEasing: 'cubicIn',
32
44
  threshold: 1000,
33
45
  progressive: true,
@@ -36,17 +48,17 @@ const DEFAULT_ANIMATION_PRESETS: AnimationPreset[] = [
36
48
  },
37
49
  {
38
50
  name: 'fast',
39
- description: '快速动画配置',
51
+ description: '快速动画配置 — 微交互 (150-200ms)',
40
52
  config: {
41
53
  enabled: true,
42
- duration: 500,
43
- easing: 'linear',
44
- appearDuration: 600,
45
- appearEasing: 'linear',
46
- updateDuration: 400,
47
- updateEasing: 'linear',
48
- disappearDuration: 300,
49
- disappearEasing: 'linear',
54
+ duration: 150,
55
+ easing: 'cubicOut',
56
+ appearDuration: 200,
57
+ appearEasing: 'cubicOut',
58
+ updateDuration: 150,
59
+ updateEasing: 'cubicOut',
60
+ disappearDuration: 100,
61
+ disappearEasing: 'cubicIn',
50
62
  threshold: 2000,
51
63
  progressive: true,
52
64
  progressiveStep: 1000,
@@ -54,55 +66,57 @@ const DEFAULT_ANIMATION_PRESETS: AnimationPreset[] = [
54
66
  },
55
67
  {
56
68
  name: 'slow',
57
- description: '慢速动画配置',
69
+ description: '慢速动画配置 — 页面过渡 (500-600ms, capped)',
58
70
  config: {
59
71
  enabled: true,
60
- duration: 2000,
72
+ duration: 500,
61
73
  easing: 'cubicInOut',
62
- appearDuration: 2400,
74
+ appearDuration: 600,
63
75
  appearEasing: 'cubicInOut',
64
- updateDuration: 1600,
76
+ updateDuration: 400,
65
77
  updateEasing: 'cubicInOut',
66
- disappearDuration: 1200,
78
+ disappearDuration: 300,
67
79
  disappearEasing: 'cubicInOut',
68
80
  threshold: 500,
69
81
  progressive: true,
70
82
  progressiveStep: 250,
71
83
  },
72
84
  },
85
+ // DEPRECATED — bounce is an anti-pattern per frontend-design-pro skill
73
86
  {
74
87
  name: 'bounce',
75
- description: '弹跳动画配置',
88
+ description: '[已废弃] 弹跳动画 — 请使用 default 或 fast',
76
89
  config: {
77
- enabled: true,
78
- duration: 1500,
79
- easing: 'bounceOut',
80
- appearDuration: 1800,
81
- appearEasing: 'bounceOut',
82
- updateDuration: 1200,
83
- updateEasing: 'bounceOut',
84
- disappearDuration: 900,
85
- disappearEasing: 'bounceIn',
90
+ enabled: false, // disabled by default — anti-pattern
91
+ duration: 0,
92
+ easing: 'cubicOut',
93
+ appearDuration: 0,
94
+ appearEasing: 'cubicOut',
95
+ updateDuration: 0,
96
+ updateEasing: 'cubicOut',
97
+ disappearDuration: 0,
98
+ disappearEasing: 'cubicIn',
86
99
  threshold: 500,
87
- progressive: true,
100
+ progressive: false,
88
101
  progressiveStep: 250,
89
102
  },
90
103
  },
104
+ // DEPRECATED — elastic is an anti-pattern per frontend-design-pro skill
91
105
  {
92
106
  name: 'elastic',
93
- description: '弹性动画配置',
107
+ description: '[已废弃] 弹性动画 — 请使用 default 或 fast',
94
108
  config: {
95
- enabled: true,
96
- duration: 1500,
97
- easing: 'elasticOut',
98
- appearDuration: 1800,
99
- appearEasing: 'elasticOut',
100
- updateDuration: 1200,
101
- updateEasing: 'elasticOut',
102
- disappearDuration: 900,
103
- disappearEasing: 'elasticIn',
109
+ enabled: false, // disabled by default — anti-pattern
110
+ duration: 0,
111
+ easing: 'cubicOut',
112
+ appearDuration: 0,
113
+ appearEasing: 'cubicOut',
114
+ updateDuration: 0,
115
+ updateEasing: 'cubicOut',
116
+ disappearDuration: 0,
117
+ disappearEasing: 'cubicIn',
104
118
  threshold: 500,
105
- progressive: true,
119
+ progressive: false,
106
120
  progressiveStep: 250,
107
121
  },
108
122
  },
@@ -213,6 +227,7 @@ export class AnimationManager {
213
227
 
214
228
  /**
215
229
  * 根据数据量和动画类型获取优化后的动画配置
230
+ * 尊重 prefers-reduced-motion 无障碍设置
216
231
  */
217
232
  public getOptimizedConfig(
218
233
  config: Partial<AnimationConfig> = {},
@@ -224,6 +239,18 @@ export class AnimationManager {
224
239
  ...config,
225
240
  };
226
241
 
242
+ // Respect OS/browser prefers-reduced-motion setting (WCAG)
243
+ if (prefersReducedMotion()) {
244
+ return {
245
+ ...mergedConfig,
246
+ enabled: false,
247
+ duration: 0,
248
+ appearDuration: 0,
249
+ updateDuration: 0,
250
+ disappearDuration: 0,
251
+ };
252
+ }
253
+
227
254
  // 根据数据量优化动画
228
255
  if (mergedConfig.threshold && dataLength > mergedConfig.threshold) {
229
256
  mergedConfig.enabled = false;
@@ -32,7 +32,7 @@ export interface MarkAreaStyle {
32
32
  }
33
33
 
34
34
  /**
35
- * 标记线数据点
35
+ * 标记线数据点 — 支持常规坐标和 ECharts 统计类型
36
36
  */
37
37
  export interface MarkLineDataPoint {
38
38
  /** X轴值 */
@@ -41,6 +41,8 @@ export interface MarkLineDataPoint {
41
41
  yAxis?: string | number;
42
42
  /** 数据点名称 */
43
43
  name?: string;
44
+ /** ECharts 统计/特殊类型('average' | 'max' | 'min' | 'median') */
45
+ type?: string;
44
46
  }
45
47
 
46
48
  /**
@@ -97,7 +99,7 @@ export interface ScatterAnnotationConfig {
97
99
  data: Array<{
98
100
  coord: [number | string, number];
99
101
  value?: number;
100
- name?: string;
102
+ name: string;
101
103
  }>;
102
104
  /** 符号类型 */
103
105
  symbol?: string;
@@ -109,7 +111,7 @@ export interface ScatterAnnotationConfig {
109
111
  label?: {
110
112
  show?: boolean;
111
113
  position?: string;
112
- formatter?: string | ((value: any) => string);
114
+ formatter?: string | ((value: unknown) => string);
113
115
  color?: string;
114
116
  };
115
117
  }
@@ -223,14 +225,14 @@ export function convertAnnotationToScatter(
223
225
  itemStyle,
224
226
  label: {
225
227
  show: label?.show !== false,
226
- position: label?.position || 'top',
228
+ position: label?.position || ('top' as const),
227
229
  formatter: label?.formatter,
228
230
  color: label?.color || '#333',
229
231
  },
230
232
  data,
231
233
  },
232
- } as any,
233
- ];
234
+ },
235
+ ] as unknown as EChartsOption['series'];
234
236
  }
235
237
 
236
238
  /**
@@ -269,28 +271,28 @@ export function useAnnotation(props: AnnotationProps): EChartsOption {
269
271
  export const AnnotationPresets = {
270
272
  /** 平均线 */
271
273
  averageLine: (color = '#1890ff'): MarkLineConfig => ({
272
- data: [{ type: 'average', name: '平均值' }] as any,
274
+ data: [{ type: 'average', name: '平均值' }],
273
275
  lineStyle: { color, type: 'dashed', width: 2 },
274
276
  label: { show: true, position: 'end', color },
275
277
  }),
276
278
 
277
279
  /** 最大值线 */
278
280
  maxLine: (color = '#f5222d'): MarkLineConfig => ({
279
- data: [{ type: 'max', name: '最大值' }] as any,
281
+ data: [{ type: 'max', name: '最大值' }],
280
282
  lineStyle: { color, type: 'dashed', width: 2 },
281
283
  label: { show: true, position: 'end', color },
282
284
  }),
283
285
 
284
286
  /** 最小值线 */
285
287
  minLine: (color = '#52c41a'): MarkLineConfig => ({
286
- data: [{ type: 'min', name: '最小值' }] as any,
288
+ data: [{ type: 'min', name: '最小值' }],
287
289
  lineStyle: { color, type: 'dashed', width: 2 },
288
290
  label: { show: true, position: 'end', color },
289
291
  }),
290
292
 
291
293
  /** 警戒线 */
292
294
  thresholdLine: (value: number, color = '#faad14'): MarkLineConfig => ({
293
- data: [{ yAxis: value, name: '警戒线' }] as any,
295
+ data: [{ yAxis: value, name: '警戒线' }],
294
296
  lineStyle: { color, type: 'solid', width: 2 },
295
297
  label: { show: true, position: 'start', color },
296
298
  }),
@@ -9,6 +9,7 @@ import React, { useEffect, useRef, useMemo, useCallback } from 'react';
9
9
 
10
10
  import { generateEChartsAnimationConfig } from '../animation';
11
11
  import { EChartsOption, EChartsType, AnimationConfig } from '../types';
12
+ import type { DataZoomComponentOption } from 'echarts';
12
13
  import { registerChart, removeChart, getChart } from '../utils/chartInstances';
13
14
  import { DebugPanel, DebugPanelOptions, updateDebugInfo } from '../utils/debug';
14
15
  import { PerformanceAnalyzer } from '../utils/performance';
@@ -21,8 +22,27 @@ import type { BaseChartProps } from '../../charts/types';
21
22
  // ============================================================================
22
23
 
23
24
  /** 图表事件参数类型 */
24
- export interface ChartEventParams {
25
- [key: string]: any;
25
+ export interface ChartEventParams extends Record<string, unknown> {
26
+ componentType?: string;
27
+ componentSubType?: string;
28
+ componentIndex?: number;
29
+ seriesType?: string;
30
+ seriesIndex?: number;
31
+ seriesId?: string;
32
+ seriesName?: string;
33
+ name?: string;
34
+ dataIndex?: number;
35
+ data?: unknown;
36
+ dataType?: string;
37
+ value?: unknown;
38
+ color?: string;
39
+ borderColor?: string;
40
+ dimensionNames?: string[];
41
+ encode?: Record<string, number[]>;
42
+ marker?: string;
43
+ status?: string;
44
+ dimensionIndex?: number;
45
+ percent?: number;
26
46
  }
27
47
 
28
48
  /** 图表导出选项 */
@@ -75,8 +95,8 @@ export interface ChartProps {
75
95
  enableZoom?: boolean;
76
96
  onZoom?: (data: { start: number; end: number; dataZoomIndex: number }) => void;
77
97
  enableDataFiltering?: boolean;
78
- filters?: Record<string, any>;
79
- onDataFiltered?: (filteredData: any[], filters: Record<string, any>) => void;
98
+ filters?: Record<string, string | number | boolean | string[] | null>;
99
+ onDataFiltered?: (filteredData: unknown[], filters: Record<string, unknown>) => void;
80
100
  enableLegendInteraction?: boolean;
81
101
  legendInteractionMode?: 'single' | 'multiple' | 'all';
82
102
  onLegendSelect?: (params: { name: string; selected: Record<string, boolean> }) => void;
@@ -146,7 +166,7 @@ const BaseChart: React.FC<ChartProps> = (props) => {
146
166
  } = props;
147
167
 
148
168
  // Refs
149
- const chartInstanceRef = useRef<any>(null);
169
+ const chartInstanceRef = useRef<EChartsType | null>(null);
150
170
  const performanceRef = useRef({
151
171
  initStartTime: 0,
152
172
  initEndTime: 0,
@@ -162,7 +182,7 @@ const BaseChart: React.FC<ChartProps> = (props) => {
162
182
  isScrolling: false,
163
183
  });
164
184
  const oldOptionRef = useRef<EChartsOption | undefined>(option);
165
- const adapterRef = useRef<any>(null);
185
+ const adapterRef = useRef<unknown>(null);
166
186
  const debugConfigRef = useRef<DebugPanelOptions | null>(null);
167
187
  const performanceAnalyzerRef = useRef<PerformanceAnalyzer | null>(null);
168
188
 
@@ -206,6 +226,22 @@ const BaseChart: React.FC<ChartProps> = (props) => {
206
226
  }
207
227
  }
208
228
 
229
+ // Inject dataZoom when enableZoom is true (keyboard-accessible zoom)
230
+ if (_enableZoom) {
231
+ processed = JSON.parse(JSON.stringify(processed));
232
+ // Avoid duplicate dataZoom entries
233
+ const existingDzArr = Array.isArray(processed.dataZoom)
234
+ ? processed.dataZoom as DataZoomComponentOption[]
235
+ : processed.dataZoom ? [processed.dataZoom as DataZoomComponentOption] : [];
236
+ if (!existingDzArr.some((dz) => dz?.type === 'inside')) {
237
+ processed.dataZoom = [
238
+ ...(existingDzArr || []),
239
+ // Inside (mouse wheel + keyboard) — wired to keyboard nav in BaseChartWrapper
240
+ { type: 'inside', start: 0, end: 100, zoomOnMouseWheel: true, moveOnMouseMove: false },
241
+ ];
242
+ }
243
+ }
244
+
209
245
  // Apply animation config
210
246
  const dataLength = calculateDataLength(processed);
211
247
  const animConfig = generateEChartsAnimationConfig(animation, dataLength);
@@ -213,6 +249,7 @@ const BaseChart: React.FC<ChartProps> = (props) => {
213
249
  }, [
214
250
  option,
215
251
  animation,
252
+ _enableZoom,
216
253
  enableDataFiltering,
217
254
  filters,
218
255
  virtualScroll,
@@ -223,13 +260,14 @@ const BaseChart: React.FC<ChartProps> = (props) => {
223
260
 
224
261
  // Internal chartInit that wraps the user's callback
225
262
  const handleChartInit = useCallback(
226
- (instance: any) => {
263
+ (instance: EChartsType) => {
227
264
  chartInstanceRef.current = instance;
228
- adapterRef.current = instance;
265
+ adapterRef.current = instance as unknown;
229
266
 
230
267
  // Performance monitoring init
231
268
  if (enablePerformanceMonitoring) {
232
269
  performanceAnalyzerRef.current = PerformanceAnalyzer.getInstance({
270
+ chartId,
233
271
  enabled: true,
234
272
  metrics: ['initTime', 'renderTime', 'updateTime', 'dataSize', 'frameRate'],
235
273
  sampleInterval: 1000,
@@ -394,15 +432,40 @@ const BaseChart: React.FC<ChartProps> = (props) => {
394
432
  }
395
433
  }, [option, onPerformance]);
396
434
 
397
- // Data update callback
435
+ // Data update callback — supports debounce
436
+ const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
437
+
398
438
  useEffect(() => {
399
- if (onDataUpdate && dataUpdateOptions?.enabled !== false) {
439
+ if (!onDataUpdate || dataUpdateOptions?.enabled === false) return;
440
+
441
+ const delay = dataUpdateOptions?.debounceDelay ?? 0;
442
+
443
+ if (debounceTimerRef.current) {
444
+ clearTimeout(debounceTimerRef.current);
445
+ }
446
+
447
+ if (delay > 0) {
448
+ debounceTimerRef.current = setTimeout(() => {
449
+ const oldOpt = oldOptionRef.current;
450
+ if (oldOpt !== option) {
451
+ onDataUpdate(oldOpt, option);
452
+ oldOptionRef.current = option;
453
+ }
454
+ }, delay);
455
+ } else {
400
456
  const oldOpt = oldOptionRef.current;
401
457
  if (oldOpt !== option) {
402
458
  onDataUpdate(oldOpt, option);
403
459
  oldOptionRef.current = option;
404
460
  }
405
461
  }
462
+
463
+ return () => {
464
+ if (debounceTimerRef.current) {
465
+ clearTimeout(debounceTimerRef.current);
466
+ debounceTimerRef.current = null;
467
+ }
468
+ };
406
469
  }, [option, onDataUpdate, dataUpdateOptions]);
407
470
 
408
471
  // Cleanup on unmount
@@ -413,7 +476,7 @@ const BaseChart: React.FC<ChartProps> = (props) => {
413
476
  performanceAnalyzerRef.current.dispose();
414
477
  performanceAnalyzerRef.current = null;
415
478
  }
416
- if (adapterRef.current) adapterRef.current.dispose();
479
+ if (adapterRef.current) (adapterRef.current as EChartsType).dispose();
417
480
  };
418
481
  }, [chartId]);
419
482
 
@@ -422,7 +485,7 @@ const BaseChart: React.FC<ChartProps> = (props) => {
422
485
  };
423
486
 
424
487
  const wrapperProps: BaseChartProps & { chartType: string } = {
425
- option: wrappedOption as any,
488
+ option: wrappedOption as unknown as Record<string, unknown>,
426
489
  width,
427
490
  height,
428
491
  theme: typeof theme === 'string' ? theme : (theme as Record<string, unknown>),
@@ -65,25 +65,30 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
65
65
  return fallback(error, this.handleReset);
66
66
  }
67
67
 
68
- // 默认错误展示
68
+ // 默认错误展示(使用 CSS 变量,与 ThemeManager 对齐)
69
69
  return (
70
70
  <div
71
+ role="alert"
72
+ aria-live="assertive"
71
73
  style={{
72
74
  display: 'flex',
73
75
  flexDirection: 'column',
74
76
  alignItems: 'center',
75
77
  justifyContent: 'center',
76
- padding: '20px',
77
- backgroundColor: '#fff',
78
- border: '1px solid #ff4d4f',
79
- borderRadius: '8px',
80
- color: '#333',
78
+ padding: 'var(--tv-border-radius, 16px)',
79
+ backgroundColor: 'var(--tv-bg-color, #fff)',
80
+ border: '1px solid var(--tv-error-color, #ff4d4f)',
81
+ borderRadius: 'var(--tv-border-radius, 8px)',
82
+ color: 'var(--tv-text-color, #333)',
81
83
  minHeight: '200px',
84
+ fontFamily: 'var(--tv-font-family, sans-serif)',
82
85
  }}
83
86
  >
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
+ <div style={{ fontSize: '48px', marginBottom: '16px' }} aria-hidden="true">⚠️</div>
88
+ <h3 style={{ margin: '0 0 12px', color: 'var(--tv-error-color, #ff4d4f)', fontWeight: 700 }}>
89
+ 图表渲染失败
90
+ </h3>
91
+ <p style={{ margin: '0 0 16px', color: 'var(--tv-text-color-secondary, #666)', textAlign: 'center', maxWidth: '320px' }}>
87
92
  图表在渲染过程中遇到错误,请检查数据配置是否正确
88
93
  </p>
89
94
  {showDetails && (
@@ -91,15 +96,15 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
91
96
  style={{
92
97
  width: '100%',
93
98
  padding: '12px',
94
- backgroundColor: '#f5f5f5',
95
- borderRadius: '4px',
96
- fontSize: '12px',
97
- fontFamily: 'monospace',
99
+ backgroundColor: 'var(--tv-bg-color-secondary, #f5f5f5)',
100
+ borderRadius: 'var(--tv-border-radius-small, 4px)',
101
+ fontSize: 'var(--tv-font-size-small, 12px)',
102
+ fontFamily: 'var(--tv-font-family, monospace)',
98
103
  overflow: 'auto',
99
104
  maxHeight: '150px',
100
105
  }}
101
106
  >
102
- <summary style={{ cursor: 'pointer', marginBottom: '8px' }}>错误详情</summary>
107
+ <summary style={{ cursor: 'pointer', marginBottom: '8px', fontWeight: 600 }}>错误详情</summary>
103
108
  <pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
104
109
  {error.message}
105
110
  {'\n\n'}
@@ -112,12 +117,20 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
112
117
  style={{
113
118
  marginTop: '16px',
114
119
  padding: '8px 24px',
115
- backgroundColor: '#1890ff',
120
+ backgroundColor: 'var(--tv-primary-color, #1890ff)',
116
121
  color: '#fff',
117
122
  border: 'none',
118
- borderRadius: '4px',
123
+ borderRadius: 'var(--tv-border-radius-small, 4px)',
119
124
  cursor: 'pointer',
120
- fontSize: '14px',
125
+ fontSize: 'var(--tv-font-size, 14px)',
126
+ fontWeight: 600,
127
+ transition: 'background-color var(--tv-transition-duration, 0.3s)',
128
+ }}
129
+ onMouseEnter={(e) => {
130
+ e.currentTarget.style.backgroundColor = 'var(--tv-primary-color-hover, #40a9ff)';
131
+ }}
132
+ onMouseLeave={(e) => {
133
+ e.currentTarget.style.backgroundColor = 'var(--tv-primary-color, #1890ff)';
121
134
  }}
122
135
  >
123
136
  重试
@@ -18,7 +18,7 @@ const LazySunburstChart = lazy(() => import('../../charts/sunburst'));
18
18
  const LazySankeyChart = lazy(() => import('../../charts/sankey'));
19
19
 
20
20
  // 统一的图表类型到懒加载组件映射
21
- const LAZY_CHART_MODULES: Record<string, () => Promise<{ default: ComponentType<any> }>> = {
21
+ const LAZY_CHART_MODULES: Record<string, () => Promise<{ default: ComponentType<Record<string, unknown>> }>> = {
22
22
  line: () => import('../../charts/line'),
23
23
  bar: () => import('../../charts/bar'),
24
24
  pie: () => import('../../charts/pie'),
@@ -35,10 +35,12 @@ const LAZY_CHART_MODULES: Record<string, () => Promise<{ default: ComponentType<
35
35
  export const LAZY_CHART_TYPES = Object.keys(LAZY_CHART_MODULES);
36
36
 
37
37
  /**
38
- * 默认加载状态组件
38
+ * 默认加载状态组件(使用 CSS 变量,与 ThemeManager 对齐)
39
39
  */
40
40
  const DefaultLoadingFallback: React.FC<{ text?: string }> = ({ text = '加载中...' }) => (
41
41
  <div
42
+ role="status"
43
+ aria-label={text}
42
44
  style={{
43
45
  display: 'flex',
44
46
  alignItems: 'center',
@@ -46,8 +48,8 @@ const DefaultLoadingFallback: React.FC<{ text?: string }> = ({ text = '加载中
46
48
  width: '100%',
47
49
  height: '100%',
48
50
  minHeight: '200px',
49
- backgroundColor: '#f5f5f5',
50
- borderRadius: '8px',
51
+ backgroundColor: 'var(--tv-bg-color-secondary, #f5f5f5)',
52
+ borderRadius: 'var(--tv-border-radius, 8px)',
51
53
  }}
52
54
  >
53
55
  <div style={{ textAlign: 'center' }}>
@@ -55,12 +57,13 @@ const DefaultLoadingFallback: React.FC<{ text?: string }> = ({ text = '加载中
55
57
  style={{
56
58
  width: '40px',
57
59
  height: '40px',
58
- border: '3px solid #1890ff',
60
+ border: '3px solid var(--tv-primary-color, #1890ff)',
59
61
  borderTopColor: 'transparent',
60
62
  borderRadius: '50%',
61
63
  animation: 'taroviz-spin 1s linear infinite',
62
64
  margin: '0 auto 12px',
63
65
  }}
66
+ aria-hidden="true"
64
67
  />
65
68
  <style>
66
69
  {`
@@ -69,7 +72,9 @@ const DefaultLoadingFallback: React.FC<{ text?: string }> = ({ text = '加载中
69
72
  }
70
73
  `}
71
74
  </style>
72
- <span style={{ color: '#666', fontSize: '14px' }}>{text}</span>
75
+ <span style={{ color: 'var(--tv-text-color-secondary, #666)', fontSize: 'var(--tv-font-size, 14px)' }}>
76
+ {text}
77
+ </span>
73
78
  </div>
74
79
  </div>
75
80
  );
@@ -136,8 +141,8 @@ export function preloadAllCharts(): Promise<void[]> {
136
141
  * 创建懒加载图表映射
137
142
  * 用于动态导入图表
138
143
  */
139
- export function createLazyChart(chartType: string): ComponentType<any> | null {
140
- const lazyCharts: Record<string, ComponentType<any>> = {
144
+ export function createLazyChart(chartType: string): ComponentType<Record<string, unknown>> | null {
145
+ const lazyCharts: Record<string, ComponentType<Record<string, unknown>>> = {
141
146
  line: LazyLineChart,
142
147
  bar: LazyBarChart,
143
148
  pie: LazyPieChart,
@@ -159,7 +164,7 @@ export function createLazyChart(chartType: string): ComponentType<any> | null {
159
164
  * 用于按名称动态获取懒加载图表组件
160
165
  */
161
166
  export const LazyChartRegistry = {
162
- get(chartType: string): ComponentType<any> | null {
167
+ get(chartType: string): ComponentType<Record<string, unknown>> | null {
163
168
  return createLazyChart(chartType);
164
169
  },
165
170