@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.
@@ -0,0 +1,628 @@
1
+ /**
2
+ * TaroViz 主题系统
3
+ * 支持 CSS 变量、动态主题切换、自定义主题
4
+ */
5
+ import type { EChartsOption } from 'echarts';
6
+
7
+ // ============================================================================
8
+ // 类型定义
9
+ // ============================================================================
10
+
11
+ /**
12
+ * 主题类型
13
+ */
14
+ export type ThemeType = 'light' | 'dark' | 'custom';
15
+
16
+ /**
17
+ * 主题配置
18
+ */
19
+ export interface ThemeConfig {
20
+ /** 主题名称 */
21
+ name: string;
22
+ /** 主题类型 */
23
+ type: ThemeType;
24
+ /** 主题变量 */
25
+ variables: ThemeVariables;
26
+ /** ECharts 主题配置 */
27
+ echartsTheme?: Record<string, unknown>;
28
+ /** 是否为暗色主题 */
29
+ isDark?: boolean;
30
+ }
31
+
32
+ /**
33
+ * 主题变量 (CSS 变量)
34
+ */
35
+ export interface ThemeVariables {
36
+ /** 背景色 */
37
+ '--tv-bg-color': string;
38
+ '--tv-bg-color-secondary': string;
39
+ /** 文字颜色 */
40
+ '--tv-text-color': string;
41
+ '--tv-text-color-secondary': string;
42
+ /** 主色调 */
43
+ '--tv-primary-color': string;
44
+ '--tv-primary-color-hover': string;
45
+ '--tv-primary-color-active': string;
46
+ /** 成功色 */
47
+ '--tv-success-color': string;
48
+ /** 警告色 */
49
+ '--tv-warning-color': string;
50
+ /** 错误色 */
51
+ '--tv-error-color': string;
52
+ /** 边框颜色 */
53
+ '--tv-border-color': string;
54
+ /** 分割线颜色 */
55
+ '--tv-divider-color': string;
56
+ /** 阴影颜色 */
57
+ '--tv-shadow-color': string;
58
+ /** 图表颜色系列 */
59
+ '--tv-chart-color-1': string;
60
+ '--tv-chart-color-2': string;
61
+ '--tv-chart-color-3': string;
62
+ '--tv-chart-color-4': string;
63
+ '--tv-chart-color-5': string;
64
+ '--tv-chart-color-6': string;
65
+ '--tv-chart-color-7': string;
66
+ '--tv-chart-color-8': string;
67
+ /** 字体 */
68
+ '--tv-font-family': string;
69
+ '--tv-font-size': string;
70
+ '--tv-font-size-small': string;
71
+ '--tv-font-size-large': string;
72
+ /** 圆角 */
73
+ '--tv-border-radius': string;
74
+ '--tv-border-radius-small': string;
75
+ /** 动画 */
76
+ '--tv-transition-duration': string;
77
+ }
78
+
79
+ /**
80
+ * 预设主题
81
+ */
82
+ export type PresetThemeName = 'default' | 'dark' | 'vintage' | 'macarons' | 'infographic' | 'helianthus' | 'blue' | 'red' | 'green' | 'purple';
83
+
84
+ // ============================================================================
85
+ // 预设主题配置
86
+ // ============================================================================
87
+
88
+ const PRESET_THEMES: Record<PresetThemeName, ThemeConfig> = {
89
+ default: {
90
+ name: 'default',
91
+ type: 'light',
92
+ isDark: false,
93
+ variables: {
94
+ '--tv-bg-color': '#ffffff',
95
+ '--tv-bg-color-secondary': '#fafafa',
96
+ '--tv-text-color': '#333333',
97
+ '--tv-text-color-secondary': '#666666',
98
+ '--tv-primary-color': '#1890ff',
99
+ '--tv-primary-color-hover': '#40a9ff',
100
+ '--tv-primary-color-active': '#096dd9',
101
+ '--tv-success-color': '#52c41a',
102
+ '--tv-warning-color': '#faad14',
103
+ '--tv-error-color': '#ff4d4f',
104
+ '--tv-border-color': '#d9d9d9',
105
+ '--tv-divider-color': '#f0f0f0',
106
+ '--tv-shadow-color': 'rgba(0, 0, 0, 0.1)',
107
+ '--tv-chart-color-1': '#5470c6',
108
+ '--tv-chart-color-2': '#91cc75',
109
+ '--tv-chart-color-3': '#fac858',
110
+ '--tv-chart-color-4': '#ee6666',
111
+ '--tv-chart-color-5': '#73c0de',
112
+ '--tv-chart-color-6': '#3ba272',
113
+ '--tv-chart-color-7': '#fc8452',
114
+ '--tv-chart-color-8': '#9a60b4',
115
+ '--tv-font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
116
+ '--tv-font-size': '14px',
117
+ '--tv-font-size-small': '12px',
118
+ '--tv-font-size-large': '16px',
119
+ '--tv-border-radius': '4px',
120
+ '--tv-border-radius-small': '2px',
121
+ '--tv-transition-duration': '0.3s',
122
+ },
123
+ },
124
+ dark: {
125
+ name: 'dark',
126
+ type: 'dark',
127
+ isDark: true,
128
+ variables: {
129
+ '--tv-bg-color': '#1a1a2e',
130
+ '--tv-bg-color-secondary': '#16213e',
131
+ '--tv-text-color': '#e0e0e0',
132
+ '--tv-text-color-secondary': '#a0a0a0',
133
+ '--tv-primary-color': '#1890ff',
134
+ '--tv-primary-color-hover': '#40a9ff',
135
+ '--tv-primary-color-active': '#096dd9',
136
+ '--tv-success-color': '#52c41a',
137
+ '--tv-warning-color': '#faad14',
138
+ '--tv-error-color': '#ff4d4f',
139
+ '--tv-border-color': '#404040',
140
+ '--tv-divider-color': '#303030',
141
+ '--tv-shadow-color': 'rgba(0, 0, 0, 0.3)',
142
+ '--tv-chart-color-1': '#5470c6',
143
+ '--tv-chart-color-2': '#91cc75',
144
+ '--tv-chart-color-3': '#fac858',
145
+ '--tv-chart-color-4': '#ee6666',
146
+ '--tv-chart-color-5': '#73c0de',
147
+ '--tv-chart-color-6': '#3ba272',
148
+ '--tv-chart-color-7': '#fc8452',
149
+ '--tv-chart-color-8': '#9a60b4',
150
+ '--tv-font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
151
+ '--tv-font-size': '14px',
152
+ '--tv-font-size-small': '12px',
153
+ '--tv-font-size-large': '16px',
154
+ '--tv-border-radius': '4px',
155
+ '--tv-border-radius-small': '2px',
156
+ '--tv-transition-duration': '0.3s',
157
+ },
158
+ },
159
+ vintage: {
160
+ name: 'vintage',
161
+ type: 'custom',
162
+ isDark: false,
163
+ variables: {
164
+ '--tv-bg-color': '#fef9ef',
165
+ '--tv-bg-color-secondary': '#fcf5e9',
166
+ '--tv-text-color': '#5c4d3d',
167
+ '--tv-text-color-secondary': '#8b7355',
168
+ '--tv-primary-color': '#d4a574',
169
+ '--tv-primary-color-hover': '#c49566',
170
+ '--tv-primary-color-active': '#b8895a',
171
+ '--tv-success-color': '#8db78e',
172
+ '--tv-warning-color': '#e6c87a',
173
+ '--tv-error-color': '#c97c6d',
174
+ '--tv-border-color': '#e0d5c7',
175
+ '--tv-divider-color': '#f0e8de',
176
+ '--tv-shadow-color': 'rgba(92, 77, 61, 0.1)',
177
+ '--tv-chart-color-1': '#d4a574',
178
+ '--tv-chart-color-2': '#8db78e',
179
+ '--tv-chart-color-3': '#e6c87a',
180
+ '--tv-chart-color-4': '#c97c6d',
181
+ '--tv-chart-color-5': '#9ab5a8',
182
+ '--tv-chart-color-6': '#c9b8d4',
183
+ '--tv-chart-color-7': '#a8c4d4',
184
+ '--tv-chart-color-8': '#d4c49a',
185
+ '--tv-font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
186
+ '--tv-font-size': '14px',
187
+ '--tv-font-size-small': '12px',
188
+ '--tv-font-size-large': '16px',
189
+ '--tv-border-radius': '4px',
190
+ '--tv-border-radius-small': '2px',
191
+ '--tv-transition-duration': '0.3s',
192
+ },
193
+ },
194
+ macarons: {
195
+ name: 'macarons',
196
+ type: 'custom',
197
+ isDark: false,
198
+ variables: {
199
+ '--tv-bg-color': '#fefcf9',
200
+ '--tv-bg-color-secondary': '#f9f6f2',
201
+ '--tv-text-color': '#505050',
202
+ '--tv-text-color-secondary': '#757575',
203
+ '--tv-primary-color': '#60acf2',
204
+ '--tv-primary-color-hover': '#4d9de0',
205
+ '--tv-primary-color-active': '#3d8bd0',
206
+ '--tv-success-color': '#62d17a',
207
+ '--tv-warning-color': '#f7c752',
208
+ '--tv-error-color': '#f4645a',
209
+ '--tv-border-color': '#e8e4e0',
210
+ '--tv-divider-color': '#f0ece8',
211
+ '--tv-shadow-color': 'rgba(80, 80, 80, 0.08)',
212
+ '--tv-chart-color-1': '#60acf2',
213
+ '--tv-chart-color-2': '#62d17a',
214
+ '--tv-chart-color-3': '#f7c752',
215
+ '--tv-chart-color-4': '#f4645a',
216
+ '--tv-chart-color-5': '#95d9f2',
217
+ '--tv-chart-color-6': '#a8e6cf',
218
+ '--tv-chart-color-7': '#ffd3b6',
219
+ '--tv-chart-color-8': '#ffaaa5',
220
+ '--tv-font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
221
+ '--tv-font-size': '14px',
222
+ '--tv-font-size-small': '12px',
223
+ '--tv-font-size-large': '16px',
224
+ '--tv-border-radius': '12px',
225
+ '--tv-border-radius-small': '8px',
226
+ '--tv-transition-duration': '0.3s',
227
+ },
228
+ },
229
+ infographic: {
230
+ name: 'infographic',
231
+ type: 'custom',
232
+ isDark: false,
233
+ variables: {
234
+ '--tv-bg-color': '#ffffff',
235
+ '--tv-bg-color-secondary': '#f5f7fa',
236
+ '--tv-text-color': '#1a1a1a',
237
+ '--tv-text-color-secondary': '#666666',
238
+ '--tv-primary-color': '#277 ace',
239
+ '--tv-primary-color-hover': '#3a8ee6',
240
+ '--tv-primary-color-active': '#146bb3',
241
+ '--tv-success-color': '#2fc25b',
242
+ '--tv-warning-color': '#fbd438',
243
+ '--tv-error-color': '#e8352e',
244
+ '--tv-border-color': '#e0e6ed',
245
+ '--tv-divider-color': '#f0f2f5',
246
+ '--tv-shadow-color': 'rgba(26, 26, 26, 0.06)',
247
+ '--tv-chart-color-1': '#277ace',
248
+ '--tv-chart-color-2': '#31cce8',
249
+ '--tv-chart-color-3': '#23d3a2',
250
+ '--tv-chart-color-4': '#fbd438',
251
+ '--tv-chart-color-5': '#f87f50',
252
+ '--tv-chart-color-6': '#e8352e',
253
+ '--tv-chart-color-7': '#b02ad3',
254
+ '--tv-chart-color-8': '#6475d4',
255
+ '--tv-font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
256
+ '--tv-font-size': '14px',
257
+ '--tv-font-size-small': '12px',
258
+ '--tv-font-size-large': '16px',
259
+ '--tv-border-radius': '2px',
260
+ '--tv-border-radius-small': '1px',
261
+ '--tv-transition-duration': '0.2s',
262
+ },
263
+ },
264
+ helianthus: {
265
+ name: 'helianthus',
266
+ type: 'custom',
267
+ isDark: false,
268
+ variables: {
269
+ '--tv-bg-color': '#fffbf5',
270
+ '--tv-bg-color-secondary': '#fef7f0',
271
+ '--tv-text-color': '#5c4d3d',
272
+ '--tv-text-color-secondary': '#8b7355',
273
+ '--tv-primary-color': '#f5c242',
274
+ '--tv-primary-color-hover': '#e6b53e',
275
+ '--tv-primary-color-active': '#d4a435',
276
+ '--tv-success-color': '#7ec890',
277
+ '--tv-warning-color': '#f5a623',
278
+ '--tv-error-color': '#e74c3c',
279
+ '--tv-border-color': '#e8dccf',
280
+ '--tv-divider-color': '#f0e8de',
281
+ '--tv-shadow-color': 'rgba(92, 77, 61, 0.08)',
282
+ '--tv-chart-color-1': '#f5c242',
283
+ '--tv-chart-color-2': '#7ec890',
284
+ '--tv-chart-color-3': '#5eb8d9',
285
+ '--tv-chart-color-4': '#e74c3c',
286
+ '--tv-chart-color-5': '#9b7ed4',
287
+ '--tv-chart-color-6': '#f5a623',
288
+ '--tv-chart-color-7': '#6dd3ce',
289
+ '--tv-chart-color-8': '#d4778b',
290
+ '--tv-font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
291
+ '--tv-font-size': '14px',
292
+ '--tv-font-size-small': '12px',
293
+ '--tv-font-size-large': '16px',
294
+ '--tv-border-radius': '6px',
295
+ '--tv-border-radius-small': '4px',
296
+ '--tv-transition-duration': '0.3s',
297
+ },
298
+ },
299
+ blue: {
300
+ name: 'blue',
301
+ type: 'custom',
302
+ isDark: false,
303
+ variables: {
304
+ '--tv-bg-color': '#f0f7ff',
305
+ '--tv-bg-color-secondary': '#e6f0ff',
306
+ '--tv-text-color': '#1a3a5c',
307
+ '--tv-text-color-secondary': '#4a6a8c',
308
+ '--tv-primary-color': '#1890ff',
309
+ '--tv-primary-color-hover': '#40a9ff',
310
+ '--tv-primary-color-active': '#096dd9',
311
+ '--tv-success-color': '#52c41a',
312
+ '--tv-warning-color': '#faad14',
313
+ '--tv-error-color': '#ff4d4f',
314
+ '--tv-border-color': '#bfd9f2',
315
+ '--tv-divider-color': '#d9e8fc',
316
+ '--tv-shadow-color': 'rgba(24, 144, 255, 0.15)',
317
+ '--tv-chart-color-1': '#1890ff',
318
+ '--tv-chart-color-2': '#91cc75',
319
+ '--tv-chart-color-3': '#fac858',
320
+ '--tv-chart-color-4': '#ee6666',
321
+ '--tv-chart-color-5': '#73c0de',
322
+ '--tv-chart-color-6': '#3ba272',
323
+ '--tv-chart-color-7': '#fc8452',
324
+ '--tv-chart-color-8': '#9a60b4',
325
+ '--tv-font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
326
+ '--tv-font-size': '14px',
327
+ '--tv-font-size-small': '12px',
328
+ '--tv-font-size-large': '16px',
329
+ '--tv-border-radius': '4px',
330
+ '--tv-border-radius-small': '2px',
331
+ '--tv-transition-duration': '0.3s',
332
+ },
333
+ },
334
+ red: {
335
+ name: 'red',
336
+ type: 'custom',
337
+ isDark: false,
338
+ variables: {
339
+ '--tv-bg-color': '#fff5f5',
340
+ '--tv-bg-color-secondary': '#ffe6e6',
341
+ '--tv-text-color': '#5c1a1a',
342
+ '--tv-text-color-secondary': '#8c4a4a',
343
+ '--tv-primary-color': '#ff4d4f',
344
+ '--tv-primary-color-hover': '#ff7875',
345
+ '--tv-primary-color-active': '#d9363e',
346
+ '--tv-success-color': '#52c41a',
347
+ '--tv-warning-color': '#faad14',
348
+ '--tv-error-color': '#ff4d4f',
349
+ '--tv-border-color': '#f2bfbf',
350
+ '--tv-divider-color': '#fcd9d9',
351
+ '--tv-shadow-color': 'rgba(255, 77, 79, 0.15)',
352
+ '--tv-chart-color-1': '#ff4d4f',
353
+ '--tv-chart-color-2': '#ffd700',
354
+ '--tv-chart-color-3': '#ff7c4d',
355
+ '--tv-chart-color-4': '#9c4dff',
356
+ '--tv-chart-color-5': '#00d0ff',
357
+ '--tv-chart-color-6': '#52c41a',
358
+ '--tv-chart-color-7': '#faad14',
359
+ '--tv-chart-color-8': '#8b5cf6',
360
+ '--tv-font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
361
+ '--tv-font-size': '14px',
362
+ '--tv-font-size-small': '12px',
363
+ '--tv-font-size-large': '16px',
364
+ '--tv-border-radius': '4px',
365
+ '--tv-border-radius-small': '2px',
366
+ '--tv-transition-duration': '0.3s',
367
+ },
368
+ },
369
+ green: {
370
+ name: 'green',
371
+ type: 'custom',
372
+ isDark: false,
373
+ variables: {
374
+ '--tv-bg-color': '#f5fff5',
375
+ '--tv-bg-color-secondary': '#e6ffe6',
376
+ '--tv-text-color': '#1a3d1a',
377
+ '--tv-text-color-secondary': '#4a6c4a',
378
+ '--tv-primary-color': '#52c41a',
379
+ '--tv-primary-color-hover': '#73d13d',
380
+ '--tv-primary-color-active': '#389e0d',
381
+ '--tv-success-color': '#52c41a',
382
+ '--tv-warning-color': '#faad14',
383
+ '--tv-error-color': '#ff4d4f',
384
+ '--tv-border-color': '#bff2bf',
385
+ '--tv-divider-color': '#d9fcd9',
386
+ '--tv-shadow-color': 'rgba(82, 196, 26, 0.15)',
387
+ '--tv-chart-color-1': '#52c41a',
388
+ '--tv-chart-color-2': '#1890ff',
389
+ '--tv-chart-color-3': '#faad14',
390
+ '--tv-chart-color-4': '#ff4d4f',
391
+ '--tv-chart-color-5': '#722ed1',
392
+ '--tv-chart-color-6': '#13c2c2',
393
+ '--tv-chart-color-7': '#fa8c16',
394
+ '--tv-chart-color-8': '#eb2f96',
395
+ '--tv-font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
396
+ '--tv-font-size': '14px',
397
+ '--tv-font-size-small': '12px',
398
+ '--tv-font-size-large': '16px',
399
+ '--tv-border-radius': '4px',
400
+ '--tv-border-radius-small': '2px',
401
+ '--tv-transition-duration': '0.3s',
402
+ },
403
+ },
404
+ purple: {
405
+ name: 'purple',
406
+ type: 'custom',
407
+ isDark: false,
408
+ variables: {
409
+ '--tv-bg-color': '#faf5ff',
410
+ '--tv-bg-color-secondary': '#f0e6ff',
411
+ '--tv-text-color': '#3d1a5c',
412
+ '--tv-text-color-secondary': '#6a4a8c',
413
+ '--tv-primary-color': '#722ed1',
414
+ '--tv-primary-color-hover': '#9254de',
415
+ '--tv-primary-color-active': '#531dab',
416
+ '--tv-success-color': '#52c41a',
417
+ '--tv-warning-color': '#faad14',
418
+ '--tv-error-color': '#ff4d4f',
419
+ '--tv-border-color': '#d9bff2',
420
+ '--tv-divider-color': '#ecd9fc',
421
+ '--tv-shadow-color': 'rgba(114, 46, 209, 0.15)',
422
+ '--tv-chart-color-1': '#722ed1',
423
+ '--tv-chart-color-2': '#eb2f96',
424
+ '--tv-chart-color-3': '#1890ff',
425
+ '--tv-chart-color-4': '#52c41a',
426
+ '--tv-chart-color-5': '#faad14',
427
+ '--tv-chart-color-6': '#ff4d4f',
428
+ '--tv-chart-color-7': '#13c2c2',
429
+ '--tv-chart-color-8': '#fa8c16',
430
+ '--tv-font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
431
+ '--tv-font-size': '14px',
432
+ '--tv-font-size-small': '12px',
433
+ '--tv-font-size-large': '16px',
434
+ '--tv-border-radius': '4px',
435
+ '--tv-border-radius-small': '2px',
436
+ '--tv-transition-duration': '0.3s',
437
+ },
438
+ },
439
+ };
440
+
441
+ // ============================================================================
442
+ // 主题管理器
443
+ // ============================================================================
444
+
445
+ class ThemeManager {
446
+ private static instance: ThemeManager | null = null;
447
+ private currentTheme: ThemeConfig = PRESET_THEMES.default;
448
+ private listeners: Set<(theme: ThemeConfig) => void> = new Set();
449
+ private cssVarPrefix = '--tv-';
450
+
451
+ private constructor() {}
452
+
453
+ /**
454
+ * 获取单例实例
455
+ */
456
+ public static getInstance(): ThemeManager {
457
+ if (!ThemeManager.instance) {
458
+ ThemeManager.instance = new ThemeManager();
459
+ }
460
+ return ThemeManager.instance;
461
+ }
462
+
463
+ /**
464
+ * 获取当前主题
465
+ */
466
+ public getCurrentTheme(): ThemeConfig {
467
+ return this.currentTheme;
468
+ }
469
+
470
+ /**
471
+ * 获取预设主题
472
+ */
473
+ public getPresetTheme(name: PresetThemeName): ThemeConfig | undefined {
474
+ return PRESET_THEMES[name];
475
+ }
476
+
477
+ /**
478
+ * 获取所有预设主题名称
479
+ */
480
+ public getPresetThemeNames(): PresetThemeName[] {
481
+ return Object.keys(PRESET_THEMES) as PresetThemeName[];
482
+ }
483
+
484
+ /**
485
+ * 设置主题
486
+ */
487
+ public setTheme(name: PresetThemeName | ThemeConfig): void {
488
+ const theme = typeof name === 'string' ? PRESET_THEMES[name] : name;
489
+ if (theme) {
490
+ this.currentTheme = theme;
491
+ this.applyThemeVariables(theme);
492
+ this.notifyListeners();
493
+ }
494
+ }
495
+
496
+ /**
497
+ * 应用主题变量到 CSS
498
+ */
499
+ public applyThemeVariables(theme: ThemeConfig): void {
500
+ if (typeof document === 'undefined') return;
501
+
502
+ const root = document.documentElement;
503
+ const variables = theme.variables;
504
+
505
+ Object.entries(variables).forEach(([key, value]) => {
506
+ root.style.setProperty(key, value);
507
+ });
508
+
509
+ // 设置 data 属性用于 JavaScript 检测
510
+ root.setAttribute('data-theme', theme.name);
511
+ root.setAttribute('data-theme-type', theme.type);
512
+
513
+ if (theme.isDark) {
514
+ root.setAttribute('data-theme-dark', 'true');
515
+ } else {
516
+ root.removeAttribute('data-theme-dark');
517
+ }
518
+ }
519
+
520
+ /**
521
+ * 应用 ECharts 主题
522
+ */
523
+ public getEChartsTheme(): Record<string, unknown> {
524
+ const theme = this.currentTheme;
525
+ return {
526
+ color: [
527
+ theme.variables['--tv-chart-color-1'],
528
+ theme.variables['--tv-chart-color-2'],
529
+ theme.variables['--tv-chart-color-3'],
530
+ theme.variables['--tv-chart-color-4'],
531
+ theme.variables['--tv-chart-color-5'],
532
+ theme.variables['--tv-chart-color-6'],
533
+ theme.variables['--tv-chart-color-7'],
534
+ theme.variables['--tv-chart-color-8'],
535
+ ],
536
+ backgroundColor: theme.variables['--tv-bg-color'],
537
+ textStyle: {
538
+ color: theme.variables['--tv-text-color'],
539
+ fontFamily: theme.variables['--tv-font-family'],
540
+ },
541
+ };
542
+ }
543
+
544
+ /**
545
+ * 切换暗色/亮色主题
546
+ */
547
+ public toggleDarkMode(): void {
548
+ if (this.currentTheme.type === 'dark') {
549
+ this.setTheme('default');
550
+ } else {
551
+ this.setTheme('dark');
552
+ }
553
+ }
554
+
555
+ /**
556
+ * 判断是否为暗色主题
557
+ */
558
+ public isDarkMode(): boolean {
559
+ return this.currentTheme.isDark || this.currentTheme.type === 'dark';
560
+ }
561
+
562
+ /**
563
+ * 注册主题变更监听器
564
+ */
565
+ public onThemeChange(listener: (theme: ThemeConfig) => void): () => void {
566
+ this.listeners.add(listener);
567
+ return () => this.listeners.delete(listener);
568
+ }
569
+
570
+ /**
571
+ * 通知所有监听器
572
+ */
573
+ private notifyListeners(): void {
574
+ this.listeners.forEach((listener) => {
575
+ try {
576
+ listener(this.currentTheme);
577
+ } catch (error) {
578
+ console.error('[TaroViz] Theme change listener error:', error);
579
+ }
580
+ });
581
+ }
582
+
583
+ /**
584
+ * 创建自定义主题
585
+ */
586
+ public createCustomTheme(options: Partial<ThemeVariables>, name = 'custom'): ThemeConfig {
587
+ return {
588
+ name,
589
+ type: 'custom',
590
+ isDark: false,
591
+ variables: {
592
+ ...PRESET_THEMES.default.variables,
593
+ ...options,
594
+ },
595
+ };
596
+ }
597
+
598
+ /**
599
+ * 导出主题变量为 CSS 字符串
600
+ */
601
+ public exportThemeAsCSS(theme?: ThemeConfig): string {
602
+ const targetTheme = theme || this.currentTheme;
603
+ const variables = targetTheme.variables;
604
+
605
+ return `:root[data-theme="${targetTheme.name}"] {\n${Object.entries(variables)
606
+ .map(([key, value]) => ` ${key}: ${value};`)
607
+ .join('\n')}\n}`;
608
+ }
609
+
610
+ /**
611
+ * 导出主题变量为 JSON
612
+ */
613
+ public exportThemeAsJSON(theme?: ThemeConfig): string {
614
+ const targetTheme = theme || this.currentTheme;
615
+ return JSON.stringify(targetTheme, null, 2);
616
+ }
617
+ }
618
+
619
+ // 导出单例实例
620
+ export const themeManager = ThemeManager.getInstance();
621
+
622
+ // 导出类型
623
+ export type { ThemeConfig, ThemeVariables, PresetThemeName };
624
+
625
+ // 导出预设主题
626
+ export { PRESET_THEMES };
627
+
628
+ export default themeManager;
@@ -11,6 +11,15 @@ export const CHART_INSTANCES: Record<string, EChartsType> = {};
11
11
  * @param instance 图表实例
12
12
  */
13
13
  export function registerChart(id: string, instance: EChartsType): void {
14
+ // 如果已存在同名ID,先释放旧实例防止内存泄漏
15
+ if (CHART_INSTANCES[id]) {
16
+ try {
17
+ console.warn(`[TaroViz] Chart instance '${id}' already exists, replacing and disposing old instance`);
18
+ CHART_INSTANCES[id].dispose();
19
+ } catch (e) {
20
+ console.warn(`Failed to dispose old chart instance: ${id}`, e);
21
+ }
22
+ }
14
23
  CHART_INSTANCES[id] = instance;
15
24
  }
16
25
 
@@ -79,6 +79,20 @@ export class CodeGenerator {
79
79
  });
80
80
  }
81
81
 
82
+ /**
83
+ * 转义代码生成用的字符串,防止 XSS
84
+ */
85
+ private escapeForCode(str: string): string {
86
+ if (!str) return '';
87
+ // 转义特殊字符防止代码注入
88
+ return str
89
+ .replace(/\\/g, '\\\\')
90
+ .replace(/`/g, '\\`')
91
+ .replace(/\$/g, '\\$')
92
+ .replace(/\{/g, '\\{')
93
+ .replace(/\}/g, '\\}');
94
+ }
95
+
82
96
  /**
83
97
  * 初始化内置模板
84
98
  */
@@ -511,15 +525,15 @@ export default chart;`,
511
525
  // 替换模板变量
512
526
  let code = template.content;
513
527
 
514
- // 替换组件名称
515
- const componentName = options.componentName || 'ChartComponent';
528
+ // 替换组件名称(转义防止XSS)
529
+ const componentName = this.escapeForCode(options.componentName || 'ChartComponent');
516
530
  code = code.replace(/\{componentName\}/g, componentName);
517
531
 
518
- // 替换图表ID
519
- const chartId = options.chartId || 'chart';
532
+ // 替换图表ID(转义防止XSS)
533
+ const chartId = this.escapeForCode(options.chartId || 'chart');
520
534
  code = code.replace(/\{chartId\}/g, chartId);
521
535
 
522
- // 替换选项
536
+ // 替换选项(JSON.stringify已处理转义,但模板变量要转义)
523
537
  const optionStr = JSON.stringify(option, null, 2);
524
538
  code = code.replace(/\{\s*option\s*\}/g, optionStr);
525
539