@c2-markdown-ui/react 0.0.0-stage → 0.4.4
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.
- package/README.md +93 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9453 -0
- package/dist/lib/MarkdownUI.d.ts +10 -0
- package/dist/lib/MarkdownUI.d.ts.map +1 -0
- package/dist/lib/index.d.ts +4 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/widgets/Button.d.ts +15 -0
- package/dist/widgets/Button.d.ts.map +1 -0
- package/dist/widgets/ButtonGroup.d.ts +11 -0
- package/dist/widgets/ButtonGroup.d.ts.map +1 -0
- package/dist/widgets/Cascader.d.ts +34 -0
- package/dist/widgets/Cascader.d.ts.map +1 -0
- package/dist/widgets/Chart.d.ts +20 -0
- package/dist/widgets/Chart.d.ts.map +1 -0
- package/dist/widgets/DatePicker.d.ts +28 -0
- package/dist/widgets/DatePicker.d.ts.map +1 -0
- package/dist/widgets/Form.d.ts +47 -0
- package/dist/widgets/Form.d.ts.map +1 -0
- package/dist/widgets/Incomplete.d.ts +3 -0
- package/dist/widgets/Incomplete.d.ts.map +1 -0
- package/dist/widgets/MultipleChoiceQuestion.d.ts +15 -0
- package/dist/widgets/MultipleChoiceQuestion.d.ts.map +1 -0
- package/dist/widgets/NumberInput.d.ts +26 -0
- package/dist/widgets/NumberInput.d.ts.map +1 -0
- package/dist/widgets/Quiz.d.ts +37 -0
- package/dist/widgets/Quiz.d.ts.map +1 -0
- package/dist/widgets/Radio.d.ts +24 -0
- package/dist/widgets/Radio.d.ts.map +1 -0
- package/dist/widgets/Select.d.ts +15 -0
- package/dist/widgets/Select.d.ts.map +1 -0
- package/dist/widgets/SelectMulti.d.ts +20 -0
- package/dist/widgets/SelectMulti.d.ts.map +1 -0
- package/dist/widgets/ShortAnswerQuestion.d.ts +16 -0
- package/dist/widgets/ShortAnswerQuestion.d.ts.map +1 -0
- package/dist/widgets/Slider.d.ts +13 -0
- package/dist/widgets/Slider.d.ts.map +1 -0
- package/dist/widgets/Switch.d.ts +17 -0
- package/dist/widgets/Switch.d.ts.map +1 -0
- package/dist/widgets/TextInput.d.ts +11 -0
- package/dist/widgets/TextInput.d.ts.map +1 -0
- package/dist/widgets/Widget.d.ts +7 -0
- package/dist/widgets/Widget.d.ts.map +1 -0
- package/dist/widgets/chartRemoteData.d.ts +18 -0
- package/dist/widgets/chartRemoteData.d.ts.map +1 -0
- package/dist/widgets/choiceUtils.d.ts +24 -0
- package/dist/widgets/choiceUtils.d.ts.map +1 -0
- package/dist/widgets/index.d.ts +18 -0
- package/dist/widgets/index.d.ts.map +1 -0
- package/package.json +44 -4
- package/src/index.ts +1 -0
- package/src/lib/MarkdownUI.tsx +73 -0
- package/src/lib/index.ts +3 -0
- package/src/lib/widgets.css +1373 -0
- package/src/widgets/Button.tsx +39 -0
- package/src/widgets/ButtonGroup.tsx +47 -0
- package/src/widgets/Cascader.tsx +399 -0
- package/src/widgets/Chart.tsx +238 -0
- package/src/widgets/DatePicker.tsx +434 -0
- package/src/widgets/Form.tsx +372 -0
- package/src/widgets/Incomplete.tsx +55 -0
- package/src/widgets/MultipleChoiceQuestion.tsx +82 -0
- package/src/widgets/NumberInput.tsx +169 -0
- package/src/widgets/Quiz.tsx +320 -0
- package/src/widgets/Radio.tsx +146 -0
- package/src/widgets/Select.tsx +142 -0
- package/src/widgets/SelectMulti.tsx +72 -0
- package/src/widgets/ShortAnswerQuestion.tsx +101 -0
- package/src/widgets/Slider.tsx +57 -0
- package/src/widgets/Switch.tsx +56 -0
- package/src/widgets/TextInput.tsx +38 -0
- package/src/widgets/Widget.tsx +83 -0
- package/src/widgets/chartRemoteData.ts +84 -0
- package/src/widgets/choiceUtils.ts +45 -0
- package/src/widgets/index.ts +17 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ShortAnswerQuestionProps {
|
|
4
|
+
question: string;
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
correctAnswer?: string;
|
|
7
|
+
correctAnswers?: string[];
|
|
8
|
+
showFeedback?: boolean;
|
|
9
|
+
/** 是否禁用:禁用后输入框与提交按钮不可交互 */
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
onchange: (value: { answer: string; isCorrect?: boolean }) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const ShortAnswerQuestion: React.FC<ShortAnswerQuestionProps> = ({
|
|
15
|
+
question,
|
|
16
|
+
placeholder = "Type your answer here...",
|
|
17
|
+
correctAnswer,
|
|
18
|
+
correctAnswers,
|
|
19
|
+
showFeedback = false,
|
|
20
|
+
disabled = false,
|
|
21
|
+
onchange
|
|
22
|
+
}) => {
|
|
23
|
+
const [answer, setAnswer] = useState('');
|
|
24
|
+
const [hasAnswered, setHasAnswered] = useState(false);
|
|
25
|
+
const [isCorrect, setIsCorrect] = useState<boolean | undefined>(undefined);
|
|
26
|
+
|
|
27
|
+
const handleSubmit = () => {
|
|
28
|
+
if (disabled) return;
|
|
29
|
+
if (!answer.trim()) return;
|
|
30
|
+
|
|
31
|
+
setHasAnswered(true);
|
|
32
|
+
|
|
33
|
+
const normalized = (correctAnswers && correctAnswers.length > 0)
|
|
34
|
+
? correctAnswers
|
|
35
|
+
: (typeof correctAnswer === 'string' ? [correctAnswer] : undefined);
|
|
36
|
+
|
|
37
|
+
const correct = normalized
|
|
38
|
+
? normalized.some(a => a.toLowerCase() === answer.trim().toLowerCase())
|
|
39
|
+
: undefined;
|
|
40
|
+
|
|
41
|
+
setIsCorrect(correct);
|
|
42
|
+
onchange({ answer: answer.trim(), isCorrect: correct });
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
46
|
+
if (e.key === 'Enter' && !hasAnswered && !disabled) {
|
|
47
|
+
handleSubmit();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const getFeedbackMessage = () => {
|
|
52
|
+
if (!hasAnswered || !showFeedback) return null;
|
|
53
|
+
|
|
54
|
+
const hasAnswers = (correctAnswers && correctAnswers.length > 0) || typeof correctAnswer !== 'undefined';
|
|
55
|
+
if (!hasAnswers) {
|
|
56
|
+
return `Your answer: ${answer.trim()}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (correctAnswers && correctAnswers.length > 0) {
|
|
60
|
+
return isCorrect
|
|
61
|
+
? '✓ Correct!'
|
|
62
|
+
: `✗ Incorrect. Accepted answers: ${correctAnswers.join(', ')}`;
|
|
63
|
+
}
|
|
64
|
+
return isCorrect ? '✓ Correct!' : `✗ Incorrect. The correct answer is: ${correctAnswer}`;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="widget-saq">
|
|
69
|
+
<div className="saq-question">{question}</div>
|
|
70
|
+
<div className="saq-input-container">
|
|
71
|
+
<input
|
|
72
|
+
type="text"
|
|
73
|
+
value={answer}
|
|
74
|
+
onChange={(e) => setAnswer(e.target.value)}
|
|
75
|
+
onKeyDown={handleKeyDown}
|
|
76
|
+
placeholder={placeholder}
|
|
77
|
+
disabled={disabled || (hasAnswered && showFeedback)}
|
|
78
|
+
className={`saq-input ${
|
|
79
|
+
hasAnswered && showFeedback
|
|
80
|
+
? isCorrect ? 'saq-input-correct' : 'saq-input-incorrect'
|
|
81
|
+
: ''
|
|
82
|
+
}`}
|
|
83
|
+
/>
|
|
84
|
+
{!hasAnswered && (
|
|
85
|
+
<button
|
|
86
|
+
onClick={handleSubmit}
|
|
87
|
+
disabled={disabled || !answer.trim()}
|
|
88
|
+
className="saq-submit"
|
|
89
|
+
>
|
|
90
|
+
Submit
|
|
91
|
+
</button>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
{hasAnswered && showFeedback && getFeedbackMessage() && (
|
|
95
|
+
<div className={`saq-feedback ${isCorrect ? 'correct' : 'incorrect'}`}>
|
|
96
|
+
{getFeedbackMessage()}
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface SliderProps {
|
|
4
|
+
min: number;
|
|
5
|
+
max: number;
|
|
6
|
+
step?: number;
|
|
7
|
+
label?: string;
|
|
8
|
+
default?: number;
|
|
9
|
+
/** 是否禁用 */
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
onchange: (value: number) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const Slider: React.FC<SliderProps> = ({
|
|
15
|
+
min,
|
|
16
|
+
max,
|
|
17
|
+
step = 1,
|
|
18
|
+
label = '',
|
|
19
|
+
default: initial = min,
|
|
20
|
+
disabled = false,
|
|
21
|
+
onchange
|
|
22
|
+
}) => {
|
|
23
|
+
const [value, setValue] = useState(initial);
|
|
24
|
+
|
|
25
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
26
|
+
const newValue = Number(e.target.value);
|
|
27
|
+
setValue(newValue);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const handleComplete = () => {
|
|
31
|
+
onchange(value);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="widget-slider">
|
|
36
|
+
{label && <label>{label}</label>}
|
|
37
|
+
<div className="slider-container">
|
|
38
|
+
<div className="slider-values">
|
|
39
|
+
<span className="min-value">{min}</span>
|
|
40
|
+
<span className="current-value">{value}</span>
|
|
41
|
+
<span className="max-value">{max}</span>
|
|
42
|
+
</div>
|
|
43
|
+
<input
|
|
44
|
+
type="range"
|
|
45
|
+
value={value}
|
|
46
|
+
min={min}
|
|
47
|
+
max={max}
|
|
48
|
+
step={step}
|
|
49
|
+
onChange={handleChange}
|
|
50
|
+
onMouseUp={handleComplete}
|
|
51
|
+
onTouchEnd={handleComplete}
|
|
52
|
+
disabled={disabled}
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface SwitchProps {
|
|
4
|
+
label?: string;
|
|
5
|
+
/** 默认是否选中 */
|
|
6
|
+
default?: boolean;
|
|
7
|
+
/** 选中时展示的文案(antd checkedChildren) */
|
|
8
|
+
checkedText?: string;
|
|
9
|
+
/** 未选中时展示的文案(antd unCheckedChildren) */
|
|
10
|
+
unCheckedText?: string;
|
|
11
|
+
/** 是否禁用 */
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
onchange: (value: boolean) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 开关组件(参考 antd Switch):布尔切换控件,可配置选中/未选中文案。
|
|
17
|
+
label 在上(与表单其他字段同构),表单布局切换时位置自动跟随 */
|
|
18
|
+
export const Switch: React.FC<SwitchProps> = ({
|
|
19
|
+
label = '',
|
|
20
|
+
default: initial = false,
|
|
21
|
+
checkedText = '',
|
|
22
|
+
unCheckedText = '',
|
|
23
|
+
disabled = false,
|
|
24
|
+
onchange
|
|
25
|
+
}) => {
|
|
26
|
+
const [checked, setChecked] = useState(initial);
|
|
27
|
+
|
|
28
|
+
const toggle = () => {
|
|
29
|
+
if (disabled) return;
|
|
30
|
+
const next = !checked;
|
|
31
|
+
setChecked(next);
|
|
32
|
+
onchange(next);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="widget-switch">
|
|
37
|
+
{label && <label>{label}</label>}
|
|
38
|
+
<div className="widget-field-content">
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
role="switch"
|
|
42
|
+
aria-checked={checked}
|
|
43
|
+
aria-label={label}
|
|
44
|
+
className="switch-track"
|
|
45
|
+
data-checked={checked}
|
|
46
|
+
onClick={toggle}
|
|
47
|
+
disabled={disabled}
|
|
48
|
+
>
|
|
49
|
+
<span className="switch-thumb" />
|
|
50
|
+
{checkedText && <span className="switch-text switch-text-checked">{checkedText}</span>}
|
|
51
|
+
{unCheckedText && <span className="switch-text switch-text-unchecked">{unCheckedText}</span>}
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface TextInputProps {
|
|
4
|
+
placeholder?: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
default?: string;
|
|
7
|
+
/** 是否禁用 */
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
onchange: (value: string) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const TextInput: React.FC<TextInputProps> = ({
|
|
13
|
+
placeholder = '',
|
|
14
|
+
label = '',
|
|
15
|
+
default: initial = '',
|
|
16
|
+
disabled = false,
|
|
17
|
+
onchange
|
|
18
|
+
}) => {
|
|
19
|
+
const [value, setValue] = useState(initial);
|
|
20
|
+
|
|
21
|
+
const handleBlur = () => {
|
|
22
|
+
onchange(value);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="widget-text-input">
|
|
27
|
+
{label && <label>{label}</label>}
|
|
28
|
+
<input
|
|
29
|
+
type="text"
|
|
30
|
+
value={value}
|
|
31
|
+
onChange={(e) => setValue(e.target.value)}
|
|
32
|
+
onBlur={handleBlur}
|
|
33
|
+
placeholder={placeholder}
|
|
34
|
+
disabled={disabled}
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import * as widgets from './index';
|
|
3
|
+
|
|
4
|
+
export interface WidgetProps {
|
|
5
|
+
id: string;
|
|
6
|
+
content: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type WidgetType = keyof typeof widgets;
|
|
10
|
+
|
|
11
|
+
// Map camelCase JSON types to PascalCase component names
|
|
12
|
+
const typeMapping: Record<string, WidgetType> = {
|
|
13
|
+
'button': 'Button',
|
|
14
|
+
'button-group': 'ButtonGroup',
|
|
15
|
+
'cascade': 'Cascader',
|
|
16
|
+
'chart-line': 'Chart',
|
|
17
|
+
'chart-bar': 'Chart',
|
|
18
|
+
'chart-pie': 'Chart',
|
|
19
|
+
'chart-scatter': 'Chart',
|
|
20
|
+
'date-picker': 'DatePicker',
|
|
21
|
+
'form': 'Form',
|
|
22
|
+
'incomplete': 'Incomplete',
|
|
23
|
+
'multiple-choice-question': 'MultipleChoiceQuestion',
|
|
24
|
+
'number-input': 'NumberInput',
|
|
25
|
+
'quiz': 'Quiz',
|
|
26
|
+
'radio-group': 'RadioGroup',
|
|
27
|
+
'select': 'Select',
|
|
28
|
+
'select-multi': 'SelectMulti',
|
|
29
|
+
'short-answer-question': 'ShortAnswerQuestion',
|
|
30
|
+
'slider': 'Slider',
|
|
31
|
+
'switch': 'Switch',
|
|
32
|
+
'text-input': 'TextInput'
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const Widget: React.FC<WidgetProps> = ({ id, content }) => {
|
|
36
|
+
const parsed = JSON.parse(decodeURIComponent(content));
|
|
37
|
+
const componentType = typeMapping[parsed.type] || parsed.type;
|
|
38
|
+
const WidgetComponent = widgets[componentType as WidgetType];
|
|
39
|
+
|
|
40
|
+
const dispatch = (detail: any) => {
|
|
41
|
+
console.log('🔥 Widget dispatch called', { id, detail });
|
|
42
|
+
|
|
43
|
+
const event = new CustomEvent('widget-event', {
|
|
44
|
+
detail: detail,
|
|
45
|
+
bubbles: true,
|
|
46
|
+
composed: true
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Find the custom element and bubble up to find the closest widget container
|
|
50
|
+
const customElement = document.querySelector(`markdown-ui-widget[id="${id}"]`);
|
|
51
|
+
console.log('🔍 Custom element found:', customElement);
|
|
52
|
+
|
|
53
|
+
if (customElement) {
|
|
54
|
+
const container = customElement.closest('.widget-container');
|
|
55
|
+
console.log('📦 Container found:', container);
|
|
56
|
+
if (container) {
|
|
57
|
+
console.log('📤 Dispatching event on container');
|
|
58
|
+
container.dispatchEvent(event);
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
console.log('❌ No custom element found for id:', id);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (!WidgetComponent) {
|
|
66
|
+
return (
|
|
67
|
+
<div className="widget">
|
|
68
|
+
<span style={{ color: 'red' }}>
|
|
69
|
+
Unknown widget "{parsed.type}"
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="widget">
|
|
77
|
+
<WidgetComponent
|
|
78
|
+
{...parsed}
|
|
79
|
+
onchange={(value: any) => dispatch({ id, value })}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图表远端数据源解析:
|
|
3
|
+
* - CSV 文本(与 DSL 内嵌 CSV 同格式:首行 header「标签列,系列1,系列2...」,其后数据行)
|
|
4
|
+
* - JSON:支持 { labels, datasets:[{label,data}] } 及常见包装({data|result} 内嵌)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface ChartDataset {
|
|
8
|
+
label: string;
|
|
9
|
+
data: number[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RemoteChartData {
|
|
13
|
+
labels: string[];
|
|
14
|
+
datasets: ChartDataset[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** 解析 CSV 文本为图表数据(header 至少 2 列,数据列须为数字) */
|
|
18
|
+
export function parseChartCsv(text: string): RemoteChartData | null {
|
|
19
|
+
const lines = text
|
|
20
|
+
.split('\n')
|
|
21
|
+
.map(l => l.trim())
|
|
22
|
+
.filter(l => l.length > 0);
|
|
23
|
+
if (lines.length === 0) return null;
|
|
24
|
+
|
|
25
|
+
const headers = lines[0].split(',').map(h => h.trim());
|
|
26
|
+
if (headers.length < 2) return null;
|
|
27
|
+
|
|
28
|
+
const labels: string[] = [];
|
|
29
|
+
const datasets: ChartDataset[] = headers.slice(1).map(label => ({ label, data: [] }));
|
|
30
|
+
|
|
31
|
+
for (let i = 1; i < lines.length; i++) {
|
|
32
|
+
const values = lines[i].split(',').map(v => v.trim());
|
|
33
|
+
if (values.length !== headers.length) return null;
|
|
34
|
+
labels.push(values[0]);
|
|
35
|
+
for (let j = 1; j < values.length; j++) {
|
|
36
|
+
const num = parseFloat(values[j]);
|
|
37
|
+
if (isNaN(num)) return null;
|
|
38
|
+
datasets[j - 1].data.push(num);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { labels, datasets };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 归一化 JSON 响应为图表数据(labels + datasets) */
|
|
45
|
+
export function normalizeRemoteChartData(json: any): RemoteChartData | null {
|
|
46
|
+
// 解开常见包装:{data|result|chart} 内嵌
|
|
47
|
+
let payload = json;
|
|
48
|
+
for (const key of ['data', 'result', 'chart']) {
|
|
49
|
+
if (payload && typeof payload === 'object' && !Array.isArray(payload)
|
|
50
|
+
&& payload[key] && typeof payload[key] === 'object') {
|
|
51
|
+
payload = payload[key];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
56
|
+
|
|
57
|
+
// 直接是 { labels, datasets } 结构
|
|
58
|
+
let labels: any = payload.labels;
|
|
59
|
+
let datasets: any = payload.datasets;
|
|
60
|
+
|
|
61
|
+
// 数组形式:[{ label, value }] → 单系列
|
|
62
|
+
if (Array.isArray(payload) && payload.length > 0 && payload[0]?.label !== undefined && payload[0]?.value !== undefined) {
|
|
63
|
+
labels = payload.map((it: any) => it.label);
|
|
64
|
+
datasets = [{ label: 'Series', data: payload.map((it: any) => Number(it.value)) }];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!Array.isArray(labels) || !Array.isArray(datasets) || labels.length === 0 || datasets.length === 0) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const normLabels = labels.map((l: any) => String(l));
|
|
72
|
+
const normDatasets: ChartDataset[] = [];
|
|
73
|
+
for (const ds of datasets) {
|
|
74
|
+
if (!ds || !Array.isArray(ds.data)) continue;
|
|
75
|
+
const data = ds.data.map((v: any) => {
|
|
76
|
+
const num = Number(v);
|
|
77
|
+
return isNaN(num) ? 0 : num;
|
|
78
|
+
});
|
|
79
|
+
normDatasets.push({ label: String(ds.label ?? 'Series'), data });
|
|
80
|
+
}
|
|
81
|
+
if (normDatasets.length === 0) return null;
|
|
82
|
+
|
|
83
|
+
return { labels: normLabels, datasets: normDatasets };
|
|
84
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** 选项条目:value 是表单实际取值(key),label 是展示文案 */
|
|
2
|
+
export interface ChoiceItem {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 解析本地 choices 条目的 key:label 格式:
|
|
9
|
+
* - "bj:北京" → {value: "bj", label: "北京"}(value 是表单实际取值,label 是展示文案)
|
|
10
|
+
* - "北京" → {value: "北京", label: "北京"}(无冒号或不符合判定规则时视为纯文本)
|
|
11
|
+
* - "09:00" → {value: "09:00", label: "09:00"}(纯数字 key 视为时间/比例值,保持原样)
|
|
12
|
+
* - "http://x" → 原样(label 以 // 开头视为 URL)
|
|
13
|
+
*
|
|
14
|
+
* 判定规则:按首个冒号分割,仅当 key 非空且不含空白、非纯数字,
|
|
15
|
+
* 且 label 非空、不以 // 开头时才视为 key:label。
|
|
16
|
+
*/
|
|
17
|
+
export function parseChoice(c: string): ChoiceItem {
|
|
18
|
+
const s = String(c ?? '');
|
|
19
|
+
const idx = s.indexOf(':');
|
|
20
|
+
if (idx > 0) {
|
|
21
|
+
const key = s.slice(0, idx);
|
|
22
|
+
const label = s.slice(idx + 1);
|
|
23
|
+
if (key && label && !/\s/.test(key) && !/^\d+$/.test(key) && !label.startsWith('//')) {
|
|
24
|
+
return { value: key, label };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { value: s, label: s };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 把本地 choices(string[] 或对象数组)解析为 {value,label} 数组。
|
|
31
|
+
* 对象条目:value > key 取值(表单实际取值),label 缺省回落到 value */
|
|
32
|
+
export function parseChoices(choices: string[] | Array<{ value?: string; key?: string; label?: string }> | undefined | null): ChoiceItem[] {
|
|
33
|
+
return (choices || []).map((c) => {
|
|
34
|
+
if (c !== null && typeof c === 'object') {
|
|
35
|
+
const value = (c as any).value ?? (c as any).key;
|
|
36
|
+
if (value === undefined || value === null || value === '') return null;
|
|
37
|
+
const valueStr = String(value);
|
|
38
|
+
const label = (c as any).label !== undefined && (c as any).label !== null && (c as any).label !== ''
|
|
39
|
+
? String((c as any).label)
|
|
40
|
+
: valueStr;
|
|
41
|
+
return { value: valueStr, label };
|
|
42
|
+
}
|
|
43
|
+
return parseChoice(c as string);
|
|
44
|
+
}).filter((c: ChoiceItem | null): c is ChoiceItem => c !== null);
|
|
45
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { Button } from './Button';
|
|
2
|
+
export { ButtonGroup } from './ButtonGroup';
|
|
3
|
+
export { Cascader } from './Cascader';
|
|
4
|
+
export { Chart } from './Chart';
|
|
5
|
+
export { DatePicker } from './DatePicker';
|
|
6
|
+
export { Form } from './Form';
|
|
7
|
+
export { Incomplete } from './Incomplete';
|
|
8
|
+
export { MultipleChoiceQuestion } from './MultipleChoiceQuestion';
|
|
9
|
+
export { NumberInput } from './NumberInput';
|
|
10
|
+
export { Quiz } from './Quiz';
|
|
11
|
+
export { RadioGroup } from './Radio';
|
|
12
|
+
export { Select } from './Select';
|
|
13
|
+
export { SelectMulti } from './SelectMulti';
|
|
14
|
+
export { ShortAnswerQuestion } from './ShortAnswerQuestion';
|
|
15
|
+
export { Slider } from './Slider';
|
|
16
|
+
export { Switch } from './Switch';
|
|
17
|
+
export { TextInput } from './TextInput';
|