@c2-markdown-ui/react 0.0.0-stage → 0.4.3
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 +111 -2
- package/dist/index.js +10085 -0
- package/package.json +44 -4
- package/src/index.ts +1 -0
- package/src/lib/MarkdownUI.tsx +111 -0
- package/src/lib/index.ts +3 -0
- package/src/lib/widgets.css +2189 -0
- package/src/widgets/Button.tsx +39 -0
- package/src/widgets/ButtonGroup.tsx +47 -0
- package/src/widgets/Cascader.tsx +400 -0
- package/src/widgets/Chart.tsx +238 -0
- package/src/widgets/DatePicker.tsx +451 -0
- package/src/widgets/Form.tsx +512 -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 +177 -0
- package/src/widgets/Select.tsx +178 -0
- package/src/widgets/SelectMulti.tsx +105 -0
- package/src/widgets/ShortAnswerQuestion.tsx +101 -0
- package/src/widgets/Slider.tsx +71 -0
- package/src/widgets/Switch.tsx +68 -0
- package/src/widgets/TextInput.tsx +76 -0
- package/src/widgets/TreeSelect.tsx +649 -0
- package/src/widgets/Widget.tsx +131 -0
- package/src/widgets/chartRemoteData.ts +84 -0
- package/src/widgets/choiceUtils.ts +90 -0
- package/src/widgets/index.ts +18 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import * as widgets from './index';
|
|
3
|
+
|
|
4
|
+
export interface WidgetProps {
|
|
5
|
+
id: string;
|
|
6
|
+
content: string;
|
|
7
|
+
/** 外部传入的表单初始值(encodeURIComponent 后的 JSON 字符串),由 MarkdownUI 注入 */
|
|
8
|
+
data?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type WidgetType = keyof typeof widgets;
|
|
12
|
+
|
|
13
|
+
// Map camelCase JSON types to PascalCase component names
|
|
14
|
+
const typeMapping: Record<string, WidgetType> = {
|
|
15
|
+
'button': 'Button',
|
|
16
|
+
'button-group': 'ButtonGroup',
|
|
17
|
+
'cascade': 'Cascader',
|
|
18
|
+
'chart-line': 'Chart',
|
|
19
|
+
'chart-bar': 'Chart',
|
|
20
|
+
'chart-pie': 'Chart',
|
|
21
|
+
'chart-scatter': 'Chart',
|
|
22
|
+
'date-picker': 'DatePicker',
|
|
23
|
+
'form': 'Form',
|
|
24
|
+
'incomplete': 'Incomplete',
|
|
25
|
+
'multiple-choice-question': 'MultipleChoiceQuestion',
|
|
26
|
+
'number-input': 'NumberInput',
|
|
27
|
+
'quiz': 'Quiz',
|
|
28
|
+
'radio-group': 'RadioGroup',
|
|
29
|
+
'select': 'Select',
|
|
30
|
+
'select-multi': 'SelectMulti',
|
|
31
|
+
'tree-select': 'TreeSelect',
|
|
32
|
+
'short-answer-question': 'ShortAnswerQuestion',
|
|
33
|
+
'slider': 'Slider',
|
|
34
|
+
'switch': 'Switch',
|
|
35
|
+
'text-input': 'TextInput'
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const Widget: React.FC<WidgetProps> = ({ id, content, data }) => {
|
|
39
|
+
const parsed = JSON.parse(decodeURIComponent(content));
|
|
40
|
+
const componentType = typeMapping[parsed.type] || parsed.type;
|
|
41
|
+
const WidgetComponent = widgets[componentType as WidgetType];
|
|
42
|
+
|
|
43
|
+
// 解析外部注入的表单初始值(容错:非法 JSON 时忽略)
|
|
44
|
+
let initialData: Record<string, unknown> | undefined;
|
|
45
|
+
if (data) {
|
|
46
|
+
try {
|
|
47
|
+
initialData = JSON.parse(decodeURIComponent(data));
|
|
48
|
+
} catch {
|
|
49
|
+
initialData = undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const dispatch = (detail: any) => {
|
|
54
|
+
console.log('🔥 Widget dispatch called', { id, detail });
|
|
55
|
+
|
|
56
|
+
const event = new CustomEvent('widget-event', {
|
|
57
|
+
detail: detail,
|
|
58
|
+
bubbles: true,
|
|
59
|
+
composed: true
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Find the custom element and bubble up to find the closest widget container
|
|
63
|
+
const customElement = document.querySelector(`markdown-ui-widget[id="${id}"]`);
|
|
64
|
+
console.log('🔍 Custom element found:', customElement);
|
|
65
|
+
|
|
66
|
+
if (customElement) {
|
|
67
|
+
const container = customElement.closest('.widget-container');
|
|
68
|
+
console.log('📦 Container found:', container);
|
|
69
|
+
if (container) {
|
|
70
|
+
console.log('📤 Dispatching event on container');
|
|
71
|
+
container.dispatchEvent(event);
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
console.log('❌ No custom element found for id:', id);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** form 提交事件:独立于 widget-event,通过 widget-submit 派发 */
|
|
79
|
+
const dispatchSubmit = (value: any) => {
|
|
80
|
+
const customElement = document.querySelector(`markdown-ui-widget[id="${id}"]`);
|
|
81
|
+
if (customElement) {
|
|
82
|
+
const container = customElement.closest('.widget-container');
|
|
83
|
+
if (container) {
|
|
84
|
+
container.dispatchEvent(new CustomEvent('widget-submit', {
|
|
85
|
+
detail: { id, value },
|
|
86
|
+
bubbles: true,
|
|
87
|
+
composed: true
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** form 字段级统计事件:任一表单项被填写时通过 widget-form-change 派发 */
|
|
94
|
+
const dispatchFieldChange = (stats: any) => {
|
|
95
|
+
const customElement = document.querySelector(`markdown-ui-widget[id="${id}"]`);
|
|
96
|
+
if (customElement) {
|
|
97
|
+
const container = customElement.closest('.widget-container');
|
|
98
|
+
if (container) {
|
|
99
|
+
container.dispatchEvent(new CustomEvent('widget-form-change', {
|
|
100
|
+
detail: { id, stats },
|
|
101
|
+
bubbles: true,
|
|
102
|
+
composed: true
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (!WidgetComponent) {
|
|
109
|
+
return (
|
|
110
|
+
<div className="widget">
|
|
111
|
+
<span style={{ color: 'red' }}>
|
|
112
|
+
Unknown widget "{parsed.type}"
|
|
113
|
+
</span>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div className="widget">
|
|
120
|
+
<WidgetComponent
|
|
121
|
+
{...parsed}
|
|
122
|
+
initialData={initialData}
|
|
123
|
+
onchange={(value: any) => dispatch({ id, value })}
|
|
124
|
+
{...(parsed.type === 'form' ? {
|
|
125
|
+
onsubmit: (values: any) => dispatchSubmit(values),
|
|
126
|
+
onfieldchange: (stats: any) => dispatchFieldChange(stats)
|
|
127
|
+
} : {})}
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
@@ -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,90 @@
|
|
|
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
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 兼容常见远端响应格式并归一化为 {value,label}:
|
|
49
|
+
* - ["测试"] → value=label="测试"
|
|
50
|
+
* - [{key:"test",label:"测试"}] → value="test"(表单取值),label="测试"(展示)
|
|
51
|
+
* - [{value:"test",label:"测试"}] → value="test",label="测试"
|
|
52
|
+
* - {data:[...]} / {options:[...]} / {choices:[...]} / {items:[...]} → 取内嵌数组
|
|
53
|
+
*
|
|
54
|
+
* 对象条目取值优先级:value > key > id > name;展示优先级:label > name > text > value。
|
|
55
|
+
*/
|
|
56
|
+
export function normalizeRemoteChoices(json: any): ChoiceItem[] {
|
|
57
|
+
let list = json;
|
|
58
|
+
if (!Array.isArray(json)) {
|
|
59
|
+
list = Array.isArray(json?.data) ? json.data
|
|
60
|
+
: Array.isArray(json?.options) ? json.options
|
|
61
|
+
: Array.isArray(json?.choices) ? json.choices
|
|
62
|
+
: Array.isArray(json?.items) ? json.items
|
|
63
|
+
: [];
|
|
64
|
+
}
|
|
65
|
+
return (list || [])
|
|
66
|
+
.map((item: any): ChoiceItem | null => {
|
|
67
|
+
if (typeof item === 'string' || typeof item === 'number') {
|
|
68
|
+
const s = String(item);
|
|
69
|
+
return s ? { value: s, label: s } : null;
|
|
70
|
+
}
|
|
71
|
+
if (item && typeof item === 'object') {
|
|
72
|
+
const value = item.value ?? item.key ?? item.id ?? item.name;
|
|
73
|
+
if (value === undefined || value === null || value === '') return null;
|
|
74
|
+
const label = item.label ?? item.name ?? item.text ?? String(value);
|
|
75
|
+
return { value: String(value), label: String(label) };
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
})
|
|
79
|
+
.filter((c: ChoiceItem | null): c is ChoiceItem => c !== null);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 把本地 choices / default 值(可能写的是展示文案或 key:label 原文)映射到远端 value 体系:
|
|
84
|
+
* 条目本身已是 value 优先,字符串则用 label 匹配远端条目回查 value。
|
|
85
|
+
*/
|
|
86
|
+
export function resolveChoiceValue(c: string, remote: ChoiceItem[]): string {
|
|
87
|
+
const parsed = parseChoice(c).value;
|
|
88
|
+
const hit = remote.find(r => r.value === c || r.label === c || r.value === parsed);
|
|
89
|
+
return hit ? hit.value : c;
|
|
90
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
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 { TreeSelect } from './TreeSelect';
|
|
15
|
+
export { ShortAnswerQuestion } from './ShortAnswerQuestion';
|
|
16
|
+
export { Slider } from './Slider';
|
|
17
|
+
export { Switch } from './Switch';
|
|
18
|
+
export { TextInput } from './TextInput';
|