@c2-markdown-ui/react 0.4.4 → 0.4.5

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 (73) hide show
  1. package/README.md +24 -6
  2. package/dist/index.js +4365 -3733
  3. package/package.json +2 -2
  4. package/src/lib/MarkdownUI.tsx +47 -9
  5. package/src/lib/widgets.css +2188 -1372
  6. package/src/widgets/Button.tsx +39 -39
  7. package/src/widgets/ButtonGroup.tsx +47 -47
  8. package/src/widgets/Cascader.tsx +3 -2
  9. package/src/widgets/Chart.tsx +237 -237
  10. package/src/widgets/DatePicker.tsx +451 -434
  11. package/src/widgets/Form.tsx +512 -372
  12. package/src/widgets/Incomplete.tsx +3 -3
  13. package/src/widgets/MultipleChoiceQuestion.tsx +82 -82
  14. package/src/widgets/Quiz.tsx +320 -320
  15. package/src/widgets/Radio.tsx +57 -26
  16. package/src/widgets/Select.tsx +178 -142
  17. package/src/widgets/SelectMulti.tsx +105 -72
  18. package/src/widgets/ShortAnswerQuestion.tsx +101 -101
  19. package/src/widgets/Slider.tsx +70 -56
  20. package/src/widgets/Switch.tsx +14 -2
  21. package/src/widgets/TextInput.tsx +76 -38
  22. package/src/widgets/TreeSelect.tsx +649 -0
  23. package/src/widgets/Widget.tsx +130 -82
  24. package/src/widgets/choiceUtils.ts +90 -45
  25. package/src/widgets/index.ts +17 -16
  26. package/dist/index.d.ts +0 -2
  27. package/dist/index.d.ts.map +0 -1
  28. package/dist/lib/MarkdownUI.d.ts +0 -10
  29. package/dist/lib/MarkdownUI.d.ts.map +0 -1
  30. package/dist/lib/index.d.ts +0 -4
  31. package/dist/lib/index.d.ts.map +0 -1
  32. package/dist/widgets/Button.d.ts +0 -15
  33. package/dist/widgets/Button.d.ts.map +0 -1
  34. package/dist/widgets/ButtonGroup.d.ts +0 -11
  35. package/dist/widgets/ButtonGroup.d.ts.map +0 -1
  36. package/dist/widgets/Cascader.d.ts +0 -34
  37. package/dist/widgets/Cascader.d.ts.map +0 -1
  38. package/dist/widgets/Chart.d.ts +0 -20
  39. package/dist/widgets/Chart.d.ts.map +0 -1
  40. package/dist/widgets/DatePicker.d.ts +0 -28
  41. package/dist/widgets/DatePicker.d.ts.map +0 -1
  42. package/dist/widgets/Form.d.ts +0 -47
  43. package/dist/widgets/Form.d.ts.map +0 -1
  44. package/dist/widgets/Incomplete.d.ts +0 -3
  45. package/dist/widgets/Incomplete.d.ts.map +0 -1
  46. package/dist/widgets/MultipleChoiceQuestion.d.ts +0 -15
  47. package/dist/widgets/MultipleChoiceQuestion.d.ts.map +0 -1
  48. package/dist/widgets/NumberInput.d.ts +0 -26
  49. package/dist/widgets/NumberInput.d.ts.map +0 -1
  50. package/dist/widgets/Quiz.d.ts +0 -37
  51. package/dist/widgets/Quiz.d.ts.map +0 -1
  52. package/dist/widgets/Radio.d.ts +0 -24
  53. package/dist/widgets/Radio.d.ts.map +0 -1
  54. package/dist/widgets/Select.d.ts +0 -15
  55. package/dist/widgets/Select.d.ts.map +0 -1
  56. package/dist/widgets/SelectMulti.d.ts +0 -20
  57. package/dist/widgets/SelectMulti.d.ts.map +0 -1
  58. package/dist/widgets/ShortAnswerQuestion.d.ts +0 -16
  59. package/dist/widgets/ShortAnswerQuestion.d.ts.map +0 -1
  60. package/dist/widgets/Slider.d.ts +0 -13
  61. package/dist/widgets/Slider.d.ts.map +0 -1
  62. package/dist/widgets/Switch.d.ts +0 -17
  63. package/dist/widgets/Switch.d.ts.map +0 -1
  64. package/dist/widgets/TextInput.d.ts +0 -11
  65. package/dist/widgets/TextInput.d.ts.map +0 -1
  66. package/dist/widgets/Widget.d.ts +0 -7
  67. package/dist/widgets/Widget.d.ts.map +0 -1
  68. package/dist/widgets/chartRemoteData.d.ts +0 -18
  69. package/dist/widgets/chartRemoteData.d.ts.map +0 -1
  70. package/dist/widgets/choiceUtils.d.ts +0 -24
  71. package/dist/widgets/choiceUtils.d.ts.map +0 -1
  72. package/dist/widgets/index.d.ts +0 -18
  73. package/dist/widgets/index.d.ts.map +0 -1
@@ -1,39 +1,39 @@
1
- import React from 'react';
2
-
3
- export interface ButtonProps {
4
- label?: string;
5
- /** 背景色(十六进制或 CSS 颜色名) */
6
- color?: string;
7
- /** 字体颜色(十六进制或 CSS 颜色名) */
8
- textColor?: string;
9
- /** 尺寸:small / medium / large,默认 medium */
10
- size?: 'small' | 'medium' | 'large';
11
- /** 是否禁用 */
12
- disabled?: boolean;
13
- onchange: (value: string) => void;
14
- }
15
-
16
- export const Button: React.FC<ButtonProps> = ({
17
- label = '按钮',
18
- color,
19
- textColor,
20
- size = 'medium',
21
- disabled = false,
22
- onchange
23
- }) => {
24
- const style: React.CSSProperties = {};
25
- if (color) style.backgroundColor = color;
26
- if (textColor) style.color = textColor;
27
-
28
- return (
29
- <button
30
- type="button"
31
- className={`widget-button size-${size}`}
32
- style={Object.keys(style).length > 0 ? style : undefined}
33
- onClick={() => onchange(label)}
34
- disabled={disabled}
35
- >
36
- {label}
37
- </button>
38
- );
39
- };
1
+ import React from 'react';
2
+
3
+ export interface ButtonProps {
4
+ label?: string;
5
+ /** 背景色(十六进制或 CSS 颜色名) */
6
+ color?: string;
7
+ /** 字体颜色(十六进制或 CSS 颜色名) */
8
+ textColor?: string;
9
+ /** 尺寸:small / medium / large,默认 medium */
10
+ size?: 'small' | 'medium' | 'large';
11
+ /** 是否禁用 */
12
+ disabled?: boolean;
13
+ onchange: (value: string) => void;
14
+ }
15
+
16
+ export const Button: React.FC<ButtonProps> = ({
17
+ label = '按钮',
18
+ color,
19
+ textColor,
20
+ size = 'medium',
21
+ disabled = false,
22
+ onchange
23
+ }) => {
24
+ const style: React.CSSProperties = {};
25
+ if (color) style.backgroundColor = color;
26
+ if (textColor) style.color = textColor;
27
+
28
+ return (
29
+ <button
30
+ type="button"
31
+ className={`widget-button size-${size}`}
32
+ style={Object.keys(style).length > 0 ? style : undefined}
33
+ onClick={() => onchange(label)}
34
+ disabled={disabled}
35
+ >
36
+ {label}
37
+ </button>
38
+ );
39
+ };
@@ -1,47 +1,47 @@
1
- import React, { useState } from 'react';
2
- import { parseChoices, type ChoiceItem } from './choiceUtils';
3
-
4
- export interface ButtonGroupProps {
5
- choices: string[];
6
- label?: string;
7
- default?: string;
8
- /** 是否禁用 */
9
- disabled?: boolean;
10
- onchange: (value: string) => void;
11
- }
12
-
13
- export const ButtonGroup: React.FC<ButtonGroupProps> = ({
14
- choices,
15
- label = '',
16
- default: initial,
17
- disabled = false,
18
- onchange
19
- }) => {
20
- const items = parseChoices(choices);
21
- const [selected, setSelected] = useState(initial ?? items[0]?.value ?? '');
22
-
23
- const handleClick = (item: ChoiceItem) => {
24
- if (disabled) return;
25
- setSelected(item.value);
26
- onchange(item.value);
27
- };
28
-
29
- return (
30
- <div className="widget-button-group">
31
- {label && <label>{label}</label>}
32
- <div role="group" aria-label={label}>
33
- {items.map((item) => (
34
- <button
35
- key={item.value}
36
- type="button"
37
- aria-pressed={selected === item.value}
38
- onClick={() => handleClick(item)}
39
- disabled={disabled}
40
- >
41
- {item.label}
42
- </button>
43
- ))}
44
- </div>
45
- </div>
46
- );
47
- };
1
+ import React, { useState } from 'react';
2
+ import { parseChoices, type ChoiceItem } from './choiceUtils';
3
+
4
+ export interface ButtonGroupProps {
5
+ choices: string[];
6
+ label?: string;
7
+ default?: string;
8
+ /** 是否禁用 */
9
+ disabled?: boolean;
10
+ onchange: (value: string) => void;
11
+ }
12
+
13
+ export const ButtonGroup: React.FC<ButtonGroupProps> = ({
14
+ choices,
15
+ label = '',
16
+ default: initial,
17
+ disabled = false,
18
+ onchange
19
+ }) => {
20
+ const items = parseChoices(choices);
21
+ const [selected, setSelected] = useState(initial ?? items[0]?.value ?? '');
22
+
23
+ const handleClick = (item: ChoiceItem) => {
24
+ if (disabled) return;
25
+ setSelected(item.value);
26
+ onchange(item.value);
27
+ };
28
+
29
+ return (
30
+ <div className="widget-button-group">
31
+ {label && <label>{label}</label>}
32
+ <div role="group" aria-label={label}>
33
+ {items.map((item) => (
34
+ <button
35
+ key={item.value}
36
+ type="button"
37
+ aria-pressed={selected === item.value}
38
+ onClick={() => handleClick(item)}
39
+ disabled={disabled}
40
+ >
41
+ {item.label}
42
+ </button>
43
+ ))}
44
+ </div>
45
+ </div>
46
+ );
47
+ };
@@ -56,14 +56,15 @@ function parseSeg(seg: string): { value: string; label: string } {
56
56
  return { value: seg.trim(), label: seg.trim() };
57
57
  }
58
58
 
59
- /** 默认路径(value 串,/ 分隔)→ 逐级 value 数组;支持 label 串回退(无 key:label 时 value=label 等价) */
59
+ /** 默认路径(value 串,/ 分隔)→ 逐级 value 数组;支持 key:label 段与 label 串回退 */
60
60
  function pathToValues(tree: CasNode[], path: string | undefined): string[] {
61
61
  if (!path) return [];
62
62
  const segs = path.split('/').map(s => s.trim()).filter(Boolean);
63
63
  const values: string[] = [];
64
64
  let level = tree;
65
65
  for (const seg of segs) {
66
- const node = level.find(n => n.value === seg) ?? level.find(n => n.label === seg);
66
+ const { value: segVal, label: segLabel } = parseSeg(seg);
67
+ const node = level.find(n => n.value === segVal || n.value === seg || n.label === segLabel || n.label === seg);
67
68
  if (!node) return values;
68
69
  values.push(node.value);
69
70
  level = node.children;
@@ -1,238 +1,238 @@
1
- import { useEffect, useRef, useState } from 'react';
2
- import {
3
- Chart as ChartJS,
4
- CategoryScale,
5
- LinearScale,
6
- PointElement,
7
- LineElement,
8
- BarElement,
9
- Title,
10
- Tooltip,
11
- Legend,
12
- ArcElement,
13
- LineController,
14
- BarController,
15
- PieController,
16
- ScatterController
17
- } from 'chart.js';
18
- import { parseChartCsv, normalizeRemoteChartData, RemoteChartData } from './chartRemoteData';
19
-
20
- // Register Chart.js components
21
- ChartJS.register(
22
- CategoryScale,
23
- LinearScale,
24
- PointElement,
25
- LineElement,
26
- BarElement,
27
- Title,
28
- Tooltip,
29
- Legend,
30
- ArcElement,
31
- LineController,
32
- BarController,
33
- PieController,
34
- ScatterController
35
- );
36
-
37
- interface Dataset {
38
- label: string;
39
- data: number[];
40
- }
41
-
42
- interface ChartProps {
43
- type: "chart-line" | "chart-bar" | "chart-pie" | "chart-scatter";
44
- title?: string;
45
- height?: number;
46
- /** 远端数据源 URL:设置后运行时 fetch 拉取(CSV 文本或 JSON),本地 CSV 作为初始/后备数据 */
47
- url?: string;
48
- /** 是否展示图例:未设置(undefined)时自动判断(多数据集或饼图时展示) */
49
- legend?: boolean;
50
- labels: string[];
51
- datasets: Dataset[];
52
- options?: Record<string, any>;
53
- onchange: (data: any) => void;
54
- }
55
-
56
- export function Chart({ type, title, height, url = '', legend, labels, datasets, options = {}, onchange }: ChartProps) {
57
- const canvasRef = useRef<HTMLCanvasElement>(null);
58
- const chartRef = useRef<ChartJS | null>(null);
59
-
60
- // 远端数据:拉取成功后覆盖本地 CSV 数据
61
- // 加载失败静默处理:回落本地数据,均无数据时图表区域显示灰色"暂无数据"
62
- const [remoteData, setRemoteData] = useState<RemoteChartData | null>(null);
63
- const [loading, setLoading] = useState(false);
64
-
65
- // 实际渲染数据:远端优先,本地 CSV 作为初始/后备
66
- const effectiveLabels = remoteData ? remoteData.labels : labels;
67
- const effectiveDatasets = remoteData ? remoteData.datasets : datasets;
68
- /** 无任何可渲染数据(本地与远端均为空) */
69
- const isEmpty = !loading && effectiveLabels.length === 0 && effectiveDatasets.length === 0;
70
-
71
- // 远端数据源:fetch 拉取 CSV 文本或 JSON,成功后覆盖本地数据
72
- useEffect(() => {
73
- if (!url) return;
74
- let cancelled = false;
75
- setLoading(true);
76
- (async () => {
77
- try {
78
- const res = await fetch(url);
79
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
80
- const text = await res.text();
81
- if (cancelled) return;
82
- let parsed: RemoteChartData | null = null;
83
- const trimmed = text.trim();
84
- if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
85
- try {
86
- parsed = normalizeRemoteChartData(JSON.parse(trimmed));
87
- } catch {
88
- parsed = null;
89
- }
90
- }
91
- if (!parsed) parsed = parseChartCsv(text);
92
- if (cancelled) return;
93
- if (!parsed) throw new Error('响应无法解析为图表数据(CSV 或 JSON)');
94
- setRemoteData(parsed);
95
- } catch {
96
- // 加载失败:静默回落本地数据
97
- } finally {
98
- if (!cancelled) setLoading(false);
99
- }
100
- })();
101
- return () => { cancelled = true; };
102
- }, [url]);
103
-
104
- // Clamp height to safe bounds: 200px min, 800px max, 400px default
105
- const safeHeight = Math.max(200, Math.min(800, height || 400));
106
-
107
- const getChartType = (widgetType: string) => {
108
- switch (widgetType) {
109
- case 'chart-line': return 'line';
110
- case 'chart-bar': return 'bar';
111
- case 'chart-pie': return 'pie';
112
- case 'chart-scatter': return 'scatter';
113
- default: return 'line';
114
- }
115
- };
116
-
117
- const getDefaultColors = () => [
118
- 'rgb(75, 192, 192)',
119
- 'rgb(255, 99, 132)',
120
- 'rgb(54, 162, 235)',
121
- 'rgb(255, 206, 86)',
122
- 'rgb(153, 102, 255)',
123
- 'rgb(255, 159, 64)'
124
- ];
125
-
126
- const prepareDatasets = (datasets: Dataset[], chartType: string, chartLabels: string[]) => {
127
- const colors = getDefaultColors();
128
-
129
- return datasets.map((dataset, index) => {
130
- const color = colors[index % colors.length];
131
- let processedData: any = dataset.data;
132
-
133
- // For scatter plots, convert data to {x, y} format
134
- if (chartType === 'scatter') {
135
- processedData = dataset.data.map((value, dataIndex) => ({
136
- x: dataIndex,
137
- y: value
138
- }));
139
- }
140
-
141
- if (chartType === 'pie') {
142
- return {
143
- ...dataset,
144
- data: processedData,
145
- backgroundColor: colors.slice(0, dataset.data.length),
146
- borderColor: colors.slice(0, dataset.data.length),
147
- borderWidth: 1
148
- };
149
- } else {
150
- return {
151
- ...dataset,
152
- data: processedData,
153
- borderColor: color,
154
- backgroundColor: chartType === 'line' ? color + '20' : color,
155
- borderWidth: 2,
156
- fill: chartType === 'line' ? false : true
157
- };
158
- }
159
- });
160
- };
161
-
162
- useEffect(() => {
163
- if (!canvasRef.current) return;
164
-
165
- // Destroy existing chart
166
- if (chartRef.current) {
167
- chartRef.current.destroy();
168
- }
169
-
170
- const chartType = getChartType(type);
171
- const preparedDatasets = prepareDatasets(effectiveDatasets, chartType, effectiveLabels);
172
-
173
- const defaultOptions = {
174
- responsive: true,
175
- maintainAspectRatio: false,
176
- plugins: {
177
- title: {
178
- display: !!title,
179
- text: title || ''
180
- },
181
- legend: {
182
- display: legend !== undefined ? legend : (effectiveDatasets.length > 1 || chartType === 'pie')
183
- }
184
- },
185
- onClick: (_event: any, elements: any[]) => {
186
- if (elements.length > 0) {
187
- const element = elements[0];
188
- const dataIndex = element.index;
189
- const datasetIndex = element.datasetIndex;
190
-
191
- onchange({
192
- label: effectiveLabels[dataIndex],
193
- value: effectiveDatasets[datasetIndex]?.data[dataIndex],
194
- datasetLabel: effectiveDatasets[datasetIndex]?.label,
195
- dataIndex,
196
- datasetIndex
197
- });
198
- }
199
- }
200
- };
201
-
202
- const mergedOptions = { ...defaultOptions, ...options };
203
-
204
- const chartData = chartType === 'scatter'
205
- ? { datasets: preparedDatasets }
206
- : { labels: effectiveLabels, datasets: preparedDatasets };
207
-
208
- chartRef.current = new ChartJS(canvasRef.current, {
209
- type: chartType as any,
210
- data: chartData,
211
- options: mergedOptions
212
- });
213
-
214
- // Cleanup function
215
- return () => {
216
- if (chartRef.current) {
217
- chartRef.current.destroy();
218
- chartRef.current = null;
219
- }
220
- };
221
- }, [type, title, height, url, legend, effectiveLabels, effectiveDatasets, options, onchange]);
222
-
223
- return (
224
- <div className="widget-chart" style={{ height: `${safeHeight}px`, position: 'relative', width: '100%' }}>
225
- {isEmpty && (
226
- <div style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, zIndex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#9ca3af', fontSize: 13, background: '#ffffff' }}>
227
- 暂无数据
228
- </div>
229
- )}
230
- {loading && (
231
- <div style={{ position: 'absolute', top: 8, right: 8, zIndex: 1, padding: '2px 8px', borderRadius: 4, background: 'rgba(0, 0, 0, 0.05)', fontSize: 12 }}>
232
- 数据加载中...
233
- </div>
234
- )}
235
- <canvas ref={canvasRef} style={{ maxWidth: '100%', height: '100%' }}></canvas>
236
- </div>
237
- );
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import {
3
+ Chart as ChartJS,
4
+ CategoryScale,
5
+ LinearScale,
6
+ PointElement,
7
+ LineElement,
8
+ BarElement,
9
+ Title,
10
+ Tooltip,
11
+ Legend,
12
+ ArcElement,
13
+ LineController,
14
+ BarController,
15
+ PieController,
16
+ ScatterController
17
+ } from 'chart.js';
18
+ import { parseChartCsv, normalizeRemoteChartData, RemoteChartData } from './chartRemoteData';
19
+
20
+ // Register Chart.js components
21
+ ChartJS.register(
22
+ CategoryScale,
23
+ LinearScale,
24
+ PointElement,
25
+ LineElement,
26
+ BarElement,
27
+ Title,
28
+ Tooltip,
29
+ Legend,
30
+ ArcElement,
31
+ LineController,
32
+ BarController,
33
+ PieController,
34
+ ScatterController
35
+ );
36
+
37
+ interface Dataset {
38
+ label: string;
39
+ data: number[];
40
+ }
41
+
42
+ interface ChartProps {
43
+ type: "chart-line" | "chart-bar" | "chart-pie" | "chart-scatter";
44
+ title?: string;
45
+ height?: number;
46
+ /** 远端数据源 URL:设置后运行时 fetch 拉取(CSV 文本或 JSON),本地 CSV 作为初始/后备数据 */
47
+ url?: string;
48
+ /** 是否展示图例:未设置(undefined)时自动判断(多数据集或饼图时展示) */
49
+ legend?: boolean;
50
+ labels: string[];
51
+ datasets: Dataset[];
52
+ options?: Record<string, any>;
53
+ onchange: (data: any) => void;
54
+ }
55
+
56
+ export function Chart({ type, title, height, url = '', legend, labels, datasets, options = {}, onchange }: ChartProps) {
57
+ const canvasRef = useRef<HTMLCanvasElement>(null);
58
+ const chartRef = useRef<ChartJS | null>(null);
59
+
60
+ // 远端数据:拉取成功后覆盖本地 CSV 数据
61
+ // 加载失败静默处理:回落本地数据,均无数据时图表区域显示灰色"暂无数据"
62
+ const [remoteData, setRemoteData] = useState<RemoteChartData | null>(null);
63
+ const [loading, setLoading] = useState(false);
64
+
65
+ // 实际渲染数据:远端优先,本地 CSV 作为初始/后备
66
+ const effectiveLabels = remoteData ? remoteData.labels : labels;
67
+ const effectiveDatasets = remoteData ? remoteData.datasets : datasets;
68
+ /** 无任何可渲染数据(本地与远端均为空) */
69
+ const isEmpty = !loading && effectiveLabels.length === 0 && effectiveDatasets.length === 0;
70
+
71
+ // 远端数据源:fetch 拉取 CSV 文本或 JSON,成功后覆盖本地数据
72
+ useEffect(() => {
73
+ if (!url) return;
74
+ let cancelled = false;
75
+ setLoading(true);
76
+ (async () => {
77
+ try {
78
+ const res = await fetch(url);
79
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
80
+ const text = await res.text();
81
+ if (cancelled) return;
82
+ let parsed: RemoteChartData | null = null;
83
+ const trimmed = text.trim();
84
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
85
+ try {
86
+ parsed = normalizeRemoteChartData(JSON.parse(trimmed));
87
+ } catch {
88
+ parsed = null;
89
+ }
90
+ }
91
+ if (!parsed) parsed = parseChartCsv(text);
92
+ if (cancelled) return;
93
+ if (!parsed) throw new Error('响应无法解析为图表数据(CSV 或 JSON)');
94
+ setRemoteData(parsed);
95
+ } catch {
96
+ // 加载失败:静默回落本地数据
97
+ } finally {
98
+ if (!cancelled) setLoading(false);
99
+ }
100
+ })();
101
+ return () => { cancelled = true; };
102
+ }, [url]);
103
+
104
+ // Clamp height to safe bounds: 200px min, 800px max, 400px default
105
+ const safeHeight = Math.max(200, Math.min(800, height || 400));
106
+
107
+ const getChartType = (widgetType: string) => {
108
+ switch (widgetType) {
109
+ case 'chart-line': return 'line';
110
+ case 'chart-bar': return 'bar';
111
+ case 'chart-pie': return 'pie';
112
+ case 'chart-scatter': return 'scatter';
113
+ default: return 'line';
114
+ }
115
+ };
116
+
117
+ const getDefaultColors = () => [
118
+ '#1677ff',
119
+ '#52c41a',
120
+ '#faad14',
121
+ '#ff4d4f',
122
+ '#722ed1',
123
+ '#13c2c2'
124
+ ];
125
+
126
+ const prepareDatasets = (datasets: Dataset[], chartType: string, chartLabels: string[]) => {
127
+ const colors = getDefaultColors();
128
+
129
+ return datasets.map((dataset, index) => {
130
+ const color = colors[index % colors.length];
131
+ let processedData: any = dataset.data;
132
+
133
+ // For scatter plots, convert data to {x, y} format
134
+ if (chartType === 'scatter') {
135
+ processedData = dataset.data.map((value, dataIndex) => ({
136
+ x: dataIndex,
137
+ y: value
138
+ }));
139
+ }
140
+
141
+ if (chartType === 'pie') {
142
+ return {
143
+ ...dataset,
144
+ data: processedData,
145
+ backgroundColor: colors.slice(0, dataset.data.length),
146
+ borderColor: colors.slice(0, dataset.data.length),
147
+ borderWidth: 1
148
+ };
149
+ } else {
150
+ return {
151
+ ...dataset,
152
+ data: processedData,
153
+ borderColor: color,
154
+ backgroundColor: chartType === 'line' ? color + '20' : color,
155
+ borderWidth: 2,
156
+ fill: chartType === 'line' ? false : true
157
+ };
158
+ }
159
+ });
160
+ };
161
+
162
+ useEffect(() => {
163
+ if (!canvasRef.current) return;
164
+
165
+ // Destroy existing chart
166
+ if (chartRef.current) {
167
+ chartRef.current.destroy();
168
+ }
169
+
170
+ const chartType = getChartType(type);
171
+ const preparedDatasets = prepareDatasets(effectiveDatasets, chartType, effectiveLabels);
172
+
173
+ const defaultOptions = {
174
+ responsive: true,
175
+ maintainAspectRatio: false,
176
+ plugins: {
177
+ title: {
178
+ display: !!title,
179
+ text: title || ''
180
+ },
181
+ legend: {
182
+ display: legend !== undefined ? legend : (effectiveDatasets.length > 1 || chartType === 'pie')
183
+ }
184
+ },
185
+ onClick: (_event: any, elements: any[]) => {
186
+ if (elements.length > 0) {
187
+ const element = elements[0];
188
+ const dataIndex = element.index;
189
+ const datasetIndex = element.datasetIndex;
190
+
191
+ onchange({
192
+ label: effectiveLabels[dataIndex],
193
+ value: effectiveDatasets[datasetIndex]?.data[dataIndex],
194
+ datasetLabel: effectiveDatasets[datasetIndex]?.label,
195
+ dataIndex,
196
+ datasetIndex
197
+ });
198
+ }
199
+ }
200
+ };
201
+
202
+ const mergedOptions = { ...defaultOptions, ...options };
203
+
204
+ const chartData = chartType === 'scatter'
205
+ ? { datasets: preparedDatasets }
206
+ : { labels: effectiveLabels, datasets: preparedDatasets };
207
+
208
+ chartRef.current = new ChartJS(canvasRef.current, {
209
+ type: chartType as any,
210
+ data: chartData,
211
+ options: mergedOptions
212
+ });
213
+
214
+ // Cleanup function
215
+ return () => {
216
+ if (chartRef.current) {
217
+ chartRef.current.destroy();
218
+ chartRef.current = null;
219
+ }
220
+ };
221
+ }, [type, title, height, url, legend, effectiveLabels, effectiveDatasets, options, onchange]);
222
+
223
+ return (
224
+ <div className="widget-chart" style={{ height: `${safeHeight}px`, position: 'relative', width: '100%' }}>
225
+ {isEmpty && (
226
+ <div style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, zIndex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'rgba(0, 0, 0, 0.45)', fontSize: 13, background: '#ffffff' }}>
227
+ 暂无数据
228
+ </div>
229
+ )}
230
+ {loading && (
231
+ <div style={{ position: 'absolute', top: 8, right: 8, zIndex: 1, padding: '2px 8px', borderRadius: 4, background: 'rgba(0, 0, 0, 0.05)', fontSize: 12 }}>
232
+ 数据加载中...
233
+ </div>
234
+ )}
235
+ <canvas ref={canvasRef} style={{ maxWidth: '100%', height: '100%' }}></canvas>
236
+ </div>
237
+ );
238
238
  }