@c2-markdown-ui/react 0.4.3 → 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 +6 -24
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3733 -4365
- 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 +2 -2
- package/src/lib/MarkdownUI.tsx +9 -47
- package/src/lib/widgets.css +1372 -2188
- package/src/widgets/Button.tsx +39 -39
- package/src/widgets/ButtonGroup.tsx +47 -47
- package/src/widgets/Cascader.tsx +2 -3
- package/src/widgets/Chart.tsx +237 -237
- package/src/widgets/DatePicker.tsx +434 -451
- package/src/widgets/Form.tsx +372 -512
- package/src/widgets/Incomplete.tsx +3 -3
- package/src/widgets/MultipleChoiceQuestion.tsx +82 -82
- package/src/widgets/Quiz.tsx +320 -320
- package/src/widgets/Radio.tsx +26 -57
- package/src/widgets/Select.tsx +142 -178
- package/src/widgets/SelectMulti.tsx +72 -105
- package/src/widgets/ShortAnswerQuestion.tsx +101 -101
- package/src/widgets/Slider.tsx +56 -70
- package/src/widgets/Switch.tsx +2 -14
- package/src/widgets/TextInput.tsx +38 -76
- package/src/widgets/Widget.tsx +82 -130
- package/src/widgets/choiceUtils.ts +45 -90
- package/src/widgets/index.ts +16 -17
- package/src/widgets/TreeSelect.tsx +0 -649
package/src/widgets/Radio.tsx
CHANGED
|
@@ -14,13 +14,6 @@ export interface RadioGroupProps {
|
|
|
14
14
|
direction?: 'vertical' | 'horizontal';
|
|
15
15
|
/** 自定义布局:每行列数(设置后按 N 列网格排列,优先于 direction) */
|
|
16
16
|
columns?: number;
|
|
17
|
-
hint?: string;
|
|
18
|
-
/** 是否必填 */
|
|
19
|
-
required?: boolean;
|
|
20
|
-
/** 校验失败提示文案 */
|
|
21
|
-
message?: string;
|
|
22
|
-
/** 外部(form 提交时)传入的错误信息,优先于组件内部即时校验 */
|
|
23
|
-
error?: string;
|
|
24
17
|
/** 是否禁用 */
|
|
25
18
|
disabled?: boolean;
|
|
26
19
|
onchange: (value: string) => void;
|
|
@@ -47,10 +40,6 @@ export const RadioGroup: React.FC<RadioGroupProps> = ({
|
|
|
47
40
|
url,
|
|
48
41
|
direction,
|
|
49
42
|
columns,
|
|
50
|
-
hint = '',
|
|
51
|
-
required = false,
|
|
52
|
-
message = '',
|
|
53
|
-
error = '',
|
|
54
43
|
disabled = false,
|
|
55
44
|
onchange
|
|
56
45
|
}) => {
|
|
@@ -68,7 +57,6 @@ export const RadioGroup: React.FC<RadioGroupProps> = ({
|
|
|
68
57
|
if (initial !== undefined && initial !== null && initial !== '') return mapInitial(String(initial));
|
|
69
58
|
return items[0]?.value ?? '';
|
|
70
59
|
});
|
|
71
|
-
const [localError, setLocalError] = useState('');
|
|
72
60
|
|
|
73
61
|
useEffect(() => {
|
|
74
62
|
if (!url) { setRemoteItems(null); return; }
|
|
@@ -92,22 +80,13 @@ export const RadioGroup: React.FC<RadioGroupProps> = ({
|
|
|
92
80
|
const handleChange = (value: string) => {
|
|
93
81
|
if (disabled) return;
|
|
94
82
|
setSelected(value);
|
|
95
|
-
setLocalError(validate(value));
|
|
96
83
|
onchange(value);
|
|
97
84
|
};
|
|
98
85
|
|
|
99
|
-
/** 校验当前值:返回错误文案或空串 */
|
|
100
|
-
const validate = (v: string): string => {
|
|
101
|
-
if (required && v === '') return message || (label ? `请填写${label}` : '请至少选择一项');
|
|
102
|
-
return '';
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const shownError = error || localError;
|
|
106
|
-
|
|
107
86
|
// ---------- 按钮样式(antd RadioButton):复用 widget-button-group 视觉 ----------
|
|
108
87
|
if (button) {
|
|
109
88
|
return (
|
|
110
|
-
<div className=
|
|
89
|
+
<div className="widget-button-group">
|
|
111
90
|
{label && <label>{label}</label>}
|
|
112
91
|
<div
|
|
113
92
|
role="group"
|
|
@@ -117,7 +96,7 @@ export const RadioGroup: React.FC<RadioGroupProps> = ({
|
|
|
117
96
|
style={columns && columns >= 1 ? { gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` } : undefined}
|
|
118
97
|
>
|
|
119
98
|
{items.length === 0 && (
|
|
120
|
-
<div style={{ color: '
|
|
99
|
+
<div style={{ color: '#9ca3af', fontSize: 13 }}>暂无数据</div>
|
|
121
100
|
)}
|
|
122
101
|
{items.map((item) => (
|
|
123
102
|
<button
|
|
@@ -131,46 +110,36 @@ export const RadioGroup: React.FC<RadioGroupProps> = ({
|
|
|
131
110
|
</button>
|
|
132
111
|
))}
|
|
133
112
|
</div>
|
|
134
|
-
{shownError ? <div className="widget-error-text" role="alert">{shownError}</div> : null}
|
|
135
113
|
</div>
|
|
136
114
|
);
|
|
137
115
|
}
|
|
138
116
|
|
|
139
117
|
// ---------- 默认圆点单选样式(antd Radio) ----------
|
|
140
118
|
return (
|
|
141
|
-
<div className=
|
|
142
|
-
{label
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
checked={selected === item.value}
|
|
166
|
-
onChange={() => handleChange(item.value)}
|
|
167
|
-
disabled={disabled}
|
|
168
|
-
/>
|
|
169
|
-
<span>{item.label}</span>
|
|
170
|
-
</label>
|
|
171
|
-
))}
|
|
172
|
-
</div>
|
|
173
|
-
{shownError ? <div className="widget-error-text" role="alert">{shownError}</div> : null}
|
|
119
|
+
<div className="selector">
|
|
120
|
+
{label && <div className="selector-multi-label">{label}</div>}
|
|
121
|
+
<div
|
|
122
|
+
className="radio-group"
|
|
123
|
+
role="radiogroup"
|
|
124
|
+
aria-label={label}
|
|
125
|
+
data-direction={columns ? 'grid' : (direction || 'vertical')}
|
|
126
|
+
style={columns && columns >= 1 ? { gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` } : undefined}
|
|
127
|
+
>
|
|
128
|
+
{items.length === 0 && (
|
|
129
|
+
<div style={{ color: '#9ca3af', fontSize: 13 }}>暂无数据</div>
|
|
130
|
+
)}
|
|
131
|
+
{items.map((item) => (
|
|
132
|
+
<label key={item.value} className="radio-item">
|
|
133
|
+
<input
|
|
134
|
+
type="radio"
|
|
135
|
+
value={item.value}
|
|
136
|
+
checked={selected === item.value}
|
|
137
|
+
onChange={() => handleChange(item.value)}
|
|
138
|
+
disabled={disabled}
|
|
139
|
+
/>
|
|
140
|
+
<span>{item.label}</span>
|
|
141
|
+
</label>
|
|
142
|
+
))}
|
|
174
143
|
</div>
|
|
175
144
|
</div>
|
|
176
145
|
);
|
package/src/widgets/Select.tsx
CHANGED
|
@@ -1,178 +1,142 @@
|
|
|
1
|
-
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
export interface SelectProps {
|
|
5
|
-
choices: string[];
|
|
6
|
-
label?: string;
|
|
7
|
-
default?: string;
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
const [
|
|
39
|
-
/**
|
|
40
|
-
const [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
</button>
|
|
144
|
-
{open && (
|
|
145
|
-
<div className="cas-panel cas-panel-single">
|
|
146
|
-
{searchable && (
|
|
147
|
-
<div className="cas-search">
|
|
148
|
-
<input
|
|
149
|
-
ref={searchInputRef}
|
|
150
|
-
type="text"
|
|
151
|
-
className="cas-search-input"
|
|
152
|
-
value={query}
|
|
153
|
-
placeholder="搜索选项…"
|
|
154
|
-
onChange={e => setQuery(e.target.value)}
|
|
155
|
-
/>
|
|
156
|
-
</div>
|
|
157
|
-
)}
|
|
158
|
-
<div className="cas-menu cas-menu-full" role="menu">
|
|
159
|
-
{filteredOptions.map(it => (
|
|
160
|
-
<button
|
|
161
|
-
type="button"
|
|
162
|
-
key={it.value}
|
|
163
|
-
className={`cas-option${value === it.value ? ' cas-option-active' : ''}`}
|
|
164
|
-
onClick={() => pick(it.value)}
|
|
165
|
-
>
|
|
166
|
-
<span className="cas-option-label">{it.label}</span>
|
|
167
|
-
</button>
|
|
168
|
-
))}
|
|
169
|
-
{filteredOptions.length === 0 && (
|
|
170
|
-
<div className="cas-empty">{loading ? '加载中…' : query.trim() ? '无匹配选项' : '暂无数据'}</div>
|
|
171
|
-
)}
|
|
172
|
-
</div>
|
|
173
|
-
</div>
|
|
174
|
-
)}
|
|
175
|
-
</div>
|
|
176
|
-
</div>
|
|
177
|
-
);
|
|
178
|
-
};
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { parseChoices } from './choiceUtils';
|
|
3
|
+
|
|
4
|
+
export interface SelectProps {
|
|
5
|
+
choices: string[];
|
|
6
|
+
label?: string;
|
|
7
|
+
default?: string;
|
|
8
|
+
/** 是否可搜索:开启后下拉面板内置搜索框,按 key/value 与 label 过滤选项 */
|
|
9
|
+
searchable?: boolean;
|
|
10
|
+
/** 搜索框占位文案 */
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
/** 是否禁用 */
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
onchange: (value: string | string[]) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const Select: React.FC<SelectProps> = ({
|
|
18
|
+
choices,
|
|
19
|
+
label = '',
|
|
20
|
+
default: initial,
|
|
21
|
+
searchable = false,
|
|
22
|
+
placeholder = '',
|
|
23
|
+
disabled = false,
|
|
24
|
+
onchange
|
|
25
|
+
}) => {
|
|
26
|
+
const items = parseChoices(choices);
|
|
27
|
+
const [value, setValue] = useState(initial ?? items[0]?.value ?? '');
|
|
28
|
+
|
|
29
|
+
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
30
|
+
const newValue = e.target.value;
|
|
31
|
+
setValue(newValue);
|
|
32
|
+
onchange(newValue);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ---------- searchable 模式:自定义下拉(触发按钮 + 搜索框 + 滚动选项列表) ----------
|
|
36
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
37
|
+
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
38
|
+
const [open, setOpen] = useState(false);
|
|
39
|
+
/** 搜索关键字(按 value/key 与 label 过滤,大小写不敏感) */
|
|
40
|
+
const [query, setQuery] = useState('');
|
|
41
|
+
/** 当前选中项展示文案(value 与 label 不同时优先 label) */
|
|
42
|
+
const display = useMemo(() => {
|
|
43
|
+
const hit = items.find(it => it.value === value);
|
|
44
|
+
return hit ? hit.label : '';
|
|
45
|
+
}, [items, value]);
|
|
46
|
+
|
|
47
|
+
const filtered = useMemo(() => {
|
|
48
|
+
const q = query.trim().toLowerCase();
|
|
49
|
+
if (!q) return items;
|
|
50
|
+
return items.filter(it => it.value.toLowerCase().includes(q) || it.label.toLowerCase().includes(q));
|
|
51
|
+
}, [items, query]);
|
|
52
|
+
|
|
53
|
+
// 外部点击关闭面板
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
function onDocClick(e: MouseEvent) {
|
|
56
|
+
if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false);
|
|
57
|
+
}
|
|
58
|
+
document.addEventListener('mousedown', onDocClick);
|
|
59
|
+
return () => document.removeEventListener('mousedown', onDocClick);
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
// 打开面板时聚焦搜索框并清空关键字
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (open && searchInputRef.current) {
|
|
65
|
+
setQuery('');
|
|
66
|
+
searchInputRef.current.focus();
|
|
67
|
+
}
|
|
68
|
+
}, [open]);
|
|
69
|
+
|
|
70
|
+
function toggleOpen() {
|
|
71
|
+
if (disabled) return;
|
|
72
|
+
setOpen(prev => !prev);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 选中某选项:回填 value 并关闭面板 */
|
|
76
|
+
function pick(v: string) {
|
|
77
|
+
if (disabled) return;
|
|
78
|
+
setValue(v);
|
|
79
|
+
setOpen(false);
|
|
80
|
+
onchange(v);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (searchable) {
|
|
84
|
+
return (
|
|
85
|
+
<div className="selector" ref={rootRef}>
|
|
86
|
+
{label && <label>{label}</label>}
|
|
87
|
+
<div className="widget-field-content widget-select-content">
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
className="cas-input"
|
|
91
|
+
onClick={toggleOpen}
|
|
92
|
+
aria-expanded={open}
|
|
93
|
+
disabled={disabled}
|
|
94
|
+
>
|
|
95
|
+
<span className={`cas-input-text${display ? '' : ' cas-placeholder'}`}>{display || placeholder || '请选择'}</span>
|
|
96
|
+
<span className={`cas-arrow${open ? ' cas-arrow-open' : ''}`} aria-hidden="true">▾</span>
|
|
97
|
+
</button>
|
|
98
|
+
{open && (
|
|
99
|
+
<div className="cas-panel cas-panel-single">
|
|
100
|
+
<div className="cas-search">
|
|
101
|
+
<input
|
|
102
|
+
ref={searchInputRef}
|
|
103
|
+
type="text"
|
|
104
|
+
className="cas-search-input"
|
|
105
|
+
value={query}
|
|
106
|
+
placeholder="搜索选项…"
|
|
107
|
+
onChange={e => setQuery(e.target.value)}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="cas-menu cas-menu-full" role="menu">
|
|
111
|
+
{filtered.map(it => (
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
key={it.value}
|
|
115
|
+
className={`cas-option${value === it.value ? ' cas-option-active' : ''}`}
|
|
116
|
+
onClick={() => pick(it.value)}
|
|
117
|
+
>
|
|
118
|
+
<span className="cas-option-label">{it.label}</span>
|
|
119
|
+
</button>
|
|
120
|
+
))}
|
|
121
|
+
{filtered.length === 0 && <div className="cas-empty">无匹配选项</div>}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div className="selector">
|
|
132
|
+
{label && <label>{label}</label>}
|
|
133
|
+
<select value={value} onChange={handleChange} disabled={disabled}>
|
|
134
|
+
{items.map((item) => (
|
|
135
|
+
<option key={item.value} value={item.value}>
|
|
136
|
+
{item.label}
|
|
137
|
+
</option>
|
|
138
|
+
))}
|
|
139
|
+
</select>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
};
|
|
@@ -1,105 +1,72 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { parseChoice, parseChoices } from './choiceUtils';
|
|
3
|
-
|
|
4
|
-
export interface SelectMultiProps {
|
|
5
|
-
/** 选项:字符串数组(每项支持 key:label 格式)或对象数组({key|value,label},取值为 key/value,展示 label) */
|
|
6
|
-
choices: string[] | Array<{ value?: string; key?: string; label?: string }>;
|
|
7
|
-
label?: string;
|
|
8
|
-
default?: string | string[];
|
|
9
|
-
/** 选项排列方向:vertical 纵向(默认)/ horizontal 横向 */
|
|
10
|
-
direction?: 'vertical' | 'horizontal';
|
|
11
|
-
/** 自定义布局:每行列数(设置后按 N 列网格排列,优先于 direction) */
|
|
12
|
-
columns?: number;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
default:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
<div className="widget-label-row">
|
|
74
|
-
{required && <span className="widget-required-mark" aria-hidden="true">*</span>}
|
|
75
|
-
{label && <div className="selector-multi-label">{label}</div>}
|
|
76
|
-
{hint && <span className="widget-hint" data-hint={hint}>ⓘ</span>}
|
|
77
|
-
</div>
|
|
78
|
-
) : null}
|
|
79
|
-
<div className="widget-field-content">
|
|
80
|
-
<div
|
|
81
|
-
className="checkbox-group"
|
|
82
|
-
data-direction={columns ? 'grid' : (direction || 'vertical')}
|
|
83
|
-
style={columns && columns >= 1 ? { gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` } : undefined}
|
|
84
|
-
>
|
|
85
|
-
{items.length === 0 && (
|
|
86
|
-
<div style={{ color: 'rgba(0, 0, 0, 0.45)', fontSize: '13px' }}>暂无数据</div>
|
|
87
|
-
)}
|
|
88
|
-
{items.map((item) => (
|
|
89
|
-
<label key={item.value} className="checkbox-item">
|
|
90
|
-
<input
|
|
91
|
-
type="checkbox"
|
|
92
|
-
value={item.value}
|
|
93
|
-
checked={value.includes(item.value)}
|
|
94
|
-
onChange={handleChange}
|
|
95
|
-
disabled={disabled}
|
|
96
|
-
/>
|
|
97
|
-
<span>{item.label}</span>
|
|
98
|
-
</label>
|
|
99
|
-
))}
|
|
100
|
-
</div>
|
|
101
|
-
{shownError ? <div className="widget-error-text" role="alert">{shownError}</div> : null}
|
|
102
|
-
</div>
|
|
103
|
-
</div>
|
|
104
|
-
);
|
|
105
|
-
};
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { parseChoice, parseChoices } from './choiceUtils';
|
|
3
|
+
|
|
4
|
+
export interface SelectMultiProps {
|
|
5
|
+
/** 选项:字符串数组(每项支持 key:label 格式)或对象数组({key|value,label},取值为 key/value,展示 label) */
|
|
6
|
+
choices: string[] | Array<{ value?: string; key?: string; label?: string }>;
|
|
7
|
+
label?: string;
|
|
8
|
+
default?: string | string[];
|
|
9
|
+
/** 选项排列方向:vertical 纵向(默认)/ horizontal 横向 */
|
|
10
|
+
direction?: 'vertical' | 'horizontal';
|
|
11
|
+
/** 自定义布局:每行列数(设置后按 N 列网格排列,优先于 direction) */
|
|
12
|
+
columns?: number;
|
|
13
|
+
/** 是否禁用 */
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
onchange: (value: string | string[]) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const SelectMulti: React.FC<SelectMultiProps> = ({
|
|
19
|
+
choices,
|
|
20
|
+
label = '',
|
|
21
|
+
default: initial,
|
|
22
|
+
direction,
|
|
23
|
+
columns,
|
|
24
|
+
disabled = false,
|
|
25
|
+
onchange
|
|
26
|
+
}) => {
|
|
27
|
+
const items = parseChoices(choices);
|
|
28
|
+
// 初始勾选值:default 可能是 key、label 或 key:label 原文,统一解析为 key 体系
|
|
29
|
+
const [value, setValue] = useState<string[]>(
|
|
30
|
+
Array.isArray(initial)
|
|
31
|
+
? initial.map(v => parseChoice(String(v)).value)
|
|
32
|
+
: (initial ? [parseChoice(String(initial)).value] : [])
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
36
|
+
const choice = e.target.value;
|
|
37
|
+
let newValue: string[];
|
|
38
|
+
|
|
39
|
+
if (e.target.checked) {
|
|
40
|
+
newValue = [...value, choice];
|
|
41
|
+
} else {
|
|
42
|
+
newValue = value.filter(v => v !== choice);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setValue(newValue);
|
|
46
|
+
onchange(newValue);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="selector-multi">
|
|
51
|
+
{label && <div className="selector-multi-label">{label}</div>}
|
|
52
|
+
<div
|
|
53
|
+
className="checkbox-group"
|
|
54
|
+
data-direction={columns ? 'grid' : (direction || 'vertical')}
|
|
55
|
+
style={columns && columns >= 1 ? { gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` } : undefined}
|
|
56
|
+
>
|
|
57
|
+
{items.map((item) => (
|
|
58
|
+
<label key={item.value} className="checkbox-item">
|
|
59
|
+
<input
|
|
60
|
+
type="checkbox"
|
|
61
|
+
value={item.value}
|
|
62
|
+
checked={value.includes(item.value)}
|
|
63
|
+
onChange={handleChange}
|
|
64
|
+
disabled={disabled}
|
|
65
|
+
/>
|
|
66
|
+
<span>{item.label}</span>
|
|
67
|
+
</label>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
};
|