@huibo-ui/react-antd 1.0.10 → 1.0.12
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/LICENSE +21 -0
- package/lib/components/Affix.js +1 -1
- package/lib/components/Alert.js +1 -1
- package/lib/components/Avatar.js +4 -1
- package/lib/components/BackTop.js +1 -1
- package/lib/components/Badge.js +1 -1
- package/lib/components/Breadcrumb.js +4 -2
- package/lib/components/Button.js +3 -2
- package/lib/components/Card.d.ts +14 -1
- package/lib/components/Card.js +30 -3
- package/lib/components/Checkbox.d.ts +2 -0
- package/lib/components/Checkbox.js +14 -2
- package/lib/components/Collapse.d.ts +34 -4
- package/lib/components/Collapse.js +122 -4
- package/lib/components/Descriptions.js +3 -5
- package/lib/components/Divider.d.ts +8 -0
- package/lib/components/Divider.js +38 -3
- package/lib/components/Drawer.d.ts +7 -1
- package/lib/components/Drawer.js +167 -4
- package/lib/components/Dropdown.js +1 -1
- package/lib/components/FloatButton.js +1 -1
- package/lib/components/Form.d.ts +25 -14
- package/lib/components/Form.js +315 -92
- package/lib/components/Image.js +1 -1
- package/lib/components/Input.js +25 -4
- package/lib/components/InputNumber.d.ts +1 -1
- package/lib/components/InputNumber.js +15 -1
- package/lib/components/Layout.d.ts +1 -1
- package/lib/components/Layout.js +41 -16
- package/lib/components/Menu.js +55 -11
- package/lib/components/Modal.d.ts +4 -1
- package/lib/components/Modal.js +137 -20
- package/lib/components/PageHeader.js +1 -1
- package/lib/components/Pagination.js +1 -1
- package/lib/components/Popconfirm.js +1 -1
- package/lib/components/Popover.js +1 -1
- package/lib/components/Progress.js +1 -1
- package/lib/components/Radio.d.ts +5 -3
- package/lib/components/Radio.js +24 -13
- package/lib/components/Rate.js +1 -1
- package/lib/components/Result.js +1 -1
- package/lib/components/Segmented.d.ts +5 -3
- package/lib/components/Segmented.js +4 -1
- package/lib/components/Select.js +1 -1
- package/lib/components/Slider.d.ts +9 -2
- package/lib/components/Slider.js +8 -1
- package/lib/components/Space.d.ts +2 -1
- package/lib/components/Space.js +47 -4
- package/lib/components/Spin.js +1 -1
- package/lib/components/Statistic.d.ts +1 -0
- package/lib/components/Statistic.js +5 -1
- package/lib/components/Steps.js +4 -2
- package/lib/components/Switch.d.ts +6 -0
- package/lib/components/Switch.js +15 -1
- package/lib/components/Table.js +68 -12
- package/lib/components/Tabs.js +52 -27
- package/lib/components/Tag.d.ts +1 -0
- package/lib/components/Tag.js +16 -2
- package/lib/components/TimePicker.js +1 -1
- package/lib/components/Timeline.js +9 -1
- package/lib/components/Tooltip.js +1 -1
- package/lib/components/Tree.js +2 -4
- package/lib/components/TreeSelect.d.ts +2 -0
- package/lib/components/TreeSelect.js +19 -1
- package/lib/components/Typography.js +7 -3
- package/lib/components/Upload.js +1 -1
- package/lib/components/Watermark.js +21 -1
- package/lib/components/message.js +4 -3
- package/lib/components/notification.js +3 -1
- package/package.json +5 -6
package/lib/components/Form.js
CHANGED
|
@@ -1,111 +1,247 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
|
-
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
3
|
+
// === 校验规则执行 ===
|
|
4
|
+
async function runRules(value, rules = []) {
|
|
5
|
+
const errors = [];
|
|
6
|
+
for (const rule of rules) {
|
|
7
|
+
const label = rule.label || '该字段';
|
|
8
|
+
if (rule.required && (value === undefined || value === null || value === '' || (Array.isArray(value) && value.length === 0))) {
|
|
9
|
+
errors.push(rule.message ?? `${label}不能为空`);
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
if (value === undefined || value === null || value === '')
|
|
13
|
+
continue;
|
|
14
|
+
if (rule.type === 'email' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value))) {
|
|
15
|
+
errors.push(rule.message ?? `${label}格式不正确`);
|
|
16
|
+
}
|
|
17
|
+
else if (rule.type === 'url' && !/^https?:\/\//.test(String(value))) {
|
|
18
|
+
errors.push(rule.message ?? `${label}必须是合法 URL`);
|
|
19
|
+
}
|
|
20
|
+
else if (rule.pattern) {
|
|
21
|
+
const re = typeof rule.pattern === 'string' ? new RegExp(rule.pattern) : rule.pattern;
|
|
22
|
+
if (!re.test(String(value)))
|
|
23
|
+
errors.push(rule.message ?? `${label}格式不正确`);
|
|
24
|
+
}
|
|
25
|
+
else if (rule.min !== undefined && typeof value === 'number' && value < rule.min) {
|
|
26
|
+
errors.push(rule.message ?? `${label}不能小于 ${rule.min}`);
|
|
27
|
+
}
|
|
28
|
+
else if (rule.max !== undefined && typeof value === 'number' && value > rule.max) {
|
|
29
|
+
errors.push(rule.message ?? `${label}不能大于 ${rule.max}`);
|
|
30
|
+
}
|
|
31
|
+
else if (rule.len !== undefined && String(value).length !== rule.len) {
|
|
32
|
+
errors.push(rule.message ?? `${label}长度必须为 ${rule.len}`);
|
|
33
|
+
}
|
|
34
|
+
else if (rule.validator) {
|
|
35
|
+
try {
|
|
36
|
+
await rule.validator(value, rules);
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
errors.push(e?.message || rule.message || `${label}校验失败`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return errors;
|
|
44
|
+
}
|
|
45
|
+
const FormContext = React.createContext(null);
|
|
46
|
+
function createFormInstance(getState, api) {
|
|
47
|
+
return {
|
|
48
|
+
getFieldValue: name => getState().values[name],
|
|
49
|
+
getFieldsValue: nameList => {
|
|
50
|
+
const { values } = getState();
|
|
23
51
|
if (nameList && nameList.length > 0) {
|
|
24
52
|
const picked = {};
|
|
25
|
-
nameList.forEach(n => {
|
|
26
|
-
|
|
53
|
+
nameList.forEach(n => {
|
|
54
|
+
if (n in values)
|
|
55
|
+
picked[n] = values[n];
|
|
56
|
+
});
|
|
27
57
|
return picked;
|
|
28
58
|
}
|
|
29
59
|
return { ...values };
|
|
30
60
|
},
|
|
31
|
-
setFieldValue:
|
|
32
|
-
|
|
33
|
-
void run(el => el.setFieldValueMethod(name, value));
|
|
34
|
-
},
|
|
35
|
-
setFieldsValue: (vals) => {
|
|
61
|
+
setFieldValue: api.setFieldValue,
|
|
62
|
+
setFieldsValue: vals => {
|
|
36
63
|
if (vals)
|
|
37
|
-
Object.
|
|
38
|
-
void run(el => el.setFieldsValue(vals));
|
|
64
|
+
Object.keys(vals).forEach(n => api.setFieldValue(n, vals[n]));
|
|
39
65
|
},
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
66
|
+
validateFields: api.validateFields,
|
|
67
|
+
validate: async (nameList) => {
|
|
68
|
+
try {
|
|
69
|
+
await api.validateFields(nameList);
|
|
70
|
+
return true;
|
|
43
71
|
}
|
|
44
|
-
|
|
45
|
-
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
46
74
|
}
|
|
47
|
-
void run(el => el.resetFields(nameList));
|
|
48
75
|
},
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
submit: () => { void run(async (el) => { try {
|
|
52
|
-
await el.validateFields();
|
|
53
|
-
}
|
|
54
|
-
catch { /* 校验失败由 FormItem 展示 */ } }); },
|
|
76
|
+
resetFields: api.resetFields,
|
|
77
|
+
submit: api.submit,
|
|
55
78
|
scrollToField: () => { },
|
|
56
|
-
getFieldsError: () => [],
|
|
57
|
-
getFieldError: ()
|
|
58
|
-
|
|
79
|
+
getFieldsError: () => Object.entries(getState().errors).map(([name, errors]) => ({ name, errors })),
|
|
80
|
+
getFieldError: name => getState().errors[name] || [],
|
|
81
|
+
__registerField: api.registerField,
|
|
82
|
+
handleReset: () => api.resetFields(),
|
|
59
83
|
};
|
|
60
|
-
// 内部钩子:Form 组件挂
|
|
61
|
-
inst.__setRef = bind;
|
|
62
|
-
inst.__syncValue = (prop, value) => { if (prop)
|
|
63
|
-
values[prop] = value; };
|
|
64
|
-
return inst;
|
|
65
84
|
}
|
|
66
|
-
|
|
67
|
-
const { form: formProp, initialValues, onFinish, onFinishFailed, onValuesChange, layout, labelCol, children, style, className, } = props;
|
|
68
|
-
const [
|
|
69
|
-
const
|
|
70
|
-
React.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
85
|
+
const FormInner = (props, ref) => {
|
|
86
|
+
const { form: formProp, initialValues, onFinish, onFinishFailed, onValuesChange, layout = 'horizontal', labelCol, wrapperCol, labelAlign = 'right', requiredMark = true, children, style, className, } = props;
|
|
87
|
+
const [values, setValues] = React.useState(() => ({ ...(initialValues || {}) }));
|
|
88
|
+
const [errors, setErrors] = React.useState({});
|
|
89
|
+
const fieldsRef = React.useRef(new Map());
|
|
90
|
+
const stateRef = React.useRef({ values, errors });
|
|
91
|
+
stateRef.current = { values, errors };
|
|
92
|
+
const setFieldValueInternal = React.useCallback((name, value) => {
|
|
93
|
+
setValues(prev => {
|
|
94
|
+
const next = { ...prev, [name]: value };
|
|
95
|
+
if (onValuesChange)
|
|
96
|
+
onValuesChange({ [name]: value }, next);
|
|
97
|
+
return next;
|
|
98
|
+
});
|
|
99
|
+
// 有错误时尽早重校以清除
|
|
100
|
+
fieldsRef.current
|
|
101
|
+
.get(name)
|
|
102
|
+
?.validate()
|
|
103
|
+
.then(ers => {
|
|
104
|
+
setErrors(prev => (prev[name] && prev[name].length ? { ...prev, [name]: ers } : prev));
|
|
105
|
+
});
|
|
106
|
+
}, [onValuesChange]);
|
|
107
|
+
const validateFieldsInternal = React.useCallback(async (nameList) => {
|
|
108
|
+
const names = nameList && nameList.length > 0 ? nameList : Array.from(fieldsRef.current.keys());
|
|
109
|
+
const newErrors = {};
|
|
110
|
+
await Promise.all(names.map(async (name) => {
|
|
111
|
+
const field = fieldsRef.current.get(name);
|
|
112
|
+
if (!field)
|
|
113
|
+
return;
|
|
114
|
+
const ers = await runRules(values[name], field.props.rules);
|
|
115
|
+
if (ers.length)
|
|
116
|
+
newErrors[name] = ers;
|
|
117
|
+
field.setErrors(ers);
|
|
118
|
+
}));
|
|
119
|
+
setErrors(prev => {
|
|
120
|
+
const merged = { ...prev };
|
|
121
|
+
names.forEach(n => {
|
|
122
|
+
if (n in newErrors)
|
|
123
|
+
merged[n] = newErrors[n];
|
|
124
|
+
else
|
|
125
|
+
delete merged[n];
|
|
126
|
+
});
|
|
127
|
+
return merged;
|
|
128
|
+
});
|
|
129
|
+
if (Object.keys(newErrors).length > 0) {
|
|
130
|
+
const err = { values, errorFields: Object.entries(newErrors).map(([name, errors]) => ({ name, errors })) };
|
|
131
|
+
throw err;
|
|
75
132
|
}
|
|
76
|
-
|
|
133
|
+
const picked = {};
|
|
134
|
+
names.forEach(n => {
|
|
135
|
+
picked[n] = values[n];
|
|
136
|
+
});
|
|
137
|
+
return picked;
|
|
138
|
+
}, [values]);
|
|
139
|
+
const resetFieldsInternal = React.useCallback((nameList) => {
|
|
140
|
+
const names = nameList && nameList.length > 0 ? nameList : Array.from(fieldsRef.current.keys());
|
|
141
|
+
setValues(prev => {
|
|
142
|
+
const next = { ...prev };
|
|
143
|
+
names.forEach(n => {
|
|
144
|
+
next[n] = initialValues?.[n];
|
|
145
|
+
});
|
|
146
|
+
return next;
|
|
147
|
+
});
|
|
148
|
+
setErrors(prev => {
|
|
149
|
+
const next = { ...prev };
|
|
150
|
+
names.forEach(n => delete next[n]);
|
|
151
|
+
return next;
|
|
152
|
+
});
|
|
153
|
+
names.forEach(n => fieldsRef.current.get(n)?.setErrors([]));
|
|
154
|
+
}, [initialValues]);
|
|
155
|
+
const registerField = React.useCallback((name, field) => {
|
|
156
|
+
fieldsRef.current.set(name, field);
|
|
157
|
+
return () => {
|
|
158
|
+
fieldsRef.current.delete(name);
|
|
159
|
+
};
|
|
77
160
|
}, []);
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
161
|
+
const api = React.useMemo(() => ({
|
|
162
|
+
setFieldValue: setFieldValueInternal,
|
|
163
|
+
validateFields: validateFieldsInternal,
|
|
164
|
+
resetFields: resetFieldsInternal,
|
|
165
|
+
registerField,
|
|
166
|
+
submit: () => {
|
|
167
|
+
validateFieldsInternal()
|
|
168
|
+
.then(vals => onFinish?.(vals))
|
|
169
|
+
.catch(errInfo => onFinishFailed?.(errInfo));
|
|
170
|
+
},
|
|
171
|
+
}), [setFieldValueInternal, validateFieldsInternal, resetFieldsInternal, registerField, onFinish, onFinishFailed]);
|
|
172
|
+
const instance = React.useMemo(() => createFormInstance(() => stateRef.current, api), [api]);
|
|
173
|
+
// 受控 form(外部 useForm 传入)需要把内部 api 桥接上去:让外部 instance 也能驱动本组件 state。
|
|
174
|
+
React.useImperativeHandle(ref, () => formProp || instance, [formProp, instance]);
|
|
175
|
+
// 外部 formProp 同步:把它的 get/set 重定向到本组件实现。
|
|
88
176
|
React.useEffect(() => {
|
|
89
|
-
|
|
90
|
-
if (!el)
|
|
177
|
+
if (!formProp)
|
|
91
178
|
return;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
179
|
+
formProp.getFieldValue = (n) => stateRef.current.values[n];
|
|
180
|
+
formProp.getFieldsValue = (nl) => {
|
|
181
|
+
const v = stateRef.current.values;
|
|
182
|
+
if (nl && nl.length) {
|
|
183
|
+
const p = {};
|
|
184
|
+
nl.forEach(n => {
|
|
185
|
+
if (n in v)
|
|
186
|
+
p[n] = v[n];
|
|
187
|
+
});
|
|
188
|
+
return p;
|
|
189
|
+
}
|
|
190
|
+
return { ...v };
|
|
102
191
|
};
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
192
|
+
formProp.setFieldValue = setFieldValueInternal;
|
|
193
|
+
formProp.setFieldsValue = (vals) => vals && Object.keys(vals).forEach(n => setFieldValueInternal(n, vals[n]));
|
|
194
|
+
formProp.validateFields = validateFieldsInternal;
|
|
195
|
+
formProp.validate = async (nl) => {
|
|
196
|
+
try {
|
|
197
|
+
await validateFieldsInternal(nl);
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
formProp.resetFields = resetFieldsInternal;
|
|
205
|
+
formProp.submit = api.submit;
|
|
206
|
+
formProp.getFieldsError = () => Object.entries(stateRef.current.errors).map(([name, errors]) => ({ name, errors }));
|
|
207
|
+
formProp.getFieldError = (n) => stateRef.current.errors[n] || [];
|
|
208
|
+
formProp.handleReset = () => resetFieldsInternal();
|
|
209
|
+
}, [formProp, setFieldValueInternal, validateFieldsInternal, resetFieldsInternal, api.submit]);
|
|
210
|
+
const ctxValue = React.useMemo(() => ({
|
|
211
|
+
values,
|
|
212
|
+
errors,
|
|
213
|
+
setFieldValue: setFieldValueInternal,
|
|
214
|
+
registerField,
|
|
215
|
+
layout,
|
|
216
|
+
labelColSpan: labelCol?.span,
|
|
217
|
+
wrapperColSpan: wrapperCol?.span,
|
|
218
|
+
labelAlign,
|
|
219
|
+
requiredMark,
|
|
220
|
+
}), [values, errors, setFieldValueInternal, registerField, layout, labelCol, wrapperCol, labelAlign, requiredMark]);
|
|
221
|
+
return (_jsx(FormContext.Provider, { value: ctxValue, children: _jsx("form", { onSubmit: e => {
|
|
222
|
+
e.preventDefault();
|
|
223
|
+
api.submit();
|
|
224
|
+
}, style: style, className: className, children: children }) }));
|
|
225
|
+
};
|
|
226
|
+
export const Form = React.forwardRef(FormInner);
|
|
106
227
|
Form.useForm = function useForm() {
|
|
107
|
-
const
|
|
108
|
-
|
|
228
|
+
const ref = React.useRef(null);
|
|
229
|
+
if (!ref.current) {
|
|
230
|
+
ref.current = {
|
|
231
|
+
getFieldValue: () => undefined,
|
|
232
|
+
getFieldsValue: () => ({}),
|
|
233
|
+
setFieldValue: () => { },
|
|
234
|
+
setFieldsValue: () => { },
|
|
235
|
+
validateFields: () => Promise.resolve({}),
|
|
236
|
+
validate: () => Promise.resolve(true),
|
|
237
|
+
resetFields: () => { },
|
|
238
|
+
submit: () => { },
|
|
239
|
+
scrollToField: () => { },
|
|
240
|
+
getFieldsError: () => [],
|
|
241
|
+
getFieldError: () => [],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return [ref.current];
|
|
109
245
|
};
|
|
110
246
|
Form.useWatch = function useWatch(namePath, form) {
|
|
111
247
|
const name = Array.isArray(namePath) ? namePath.join('.') : namePath;
|
|
@@ -117,11 +253,98 @@ Form.useWatch = function useWatch(namePath, form) {
|
|
|
117
253
|
}, [form, name]);
|
|
118
254
|
return val;
|
|
119
255
|
};
|
|
120
|
-
Form.List = (
|
|
121
|
-
Form.Provider = (
|
|
256
|
+
Form.List = ({ children }) => children || null;
|
|
257
|
+
Form.Provider = ({ children }) => children || null;
|
|
122
258
|
export function FormItem(props) {
|
|
123
|
-
const { name, label, rules, required, labelCol,
|
|
124
|
-
const
|
|
125
|
-
|
|
259
|
+
const { name, label, rules, required, valuePropName = 'value', initialValue, labelCol, wrapperCol, colon = true, noStyle, style, className, children } = props;
|
|
260
|
+
const ctx = React.useContext(FormContext);
|
|
261
|
+
const propName = Array.isArray(name) ? name.join('.') : name || '';
|
|
262
|
+
const [localErrors, setLocalErrors] = React.useState([]);
|
|
263
|
+
const errors = (ctx?.errors[propName] ?? localErrors) || [];
|
|
264
|
+
const isRequired = required || rules?.some((r) => r.required);
|
|
265
|
+
const child = React.Children.only(children);
|
|
266
|
+
const labelColSpan = labelCol?.span ?? ctx?.labelColSpan;
|
|
267
|
+
// wrapperCol 支持 { span, offset }:offset 在 wrapper 左侧留出与 labelCol 等比的空间。
|
|
268
|
+
// 注意:|| 与 ?? 不可直接混用(babel parser 报错),用显式括号 + 层级判断。
|
|
269
|
+
const wrapperColObjSpan = typeof wrapperCol === 'object' ? wrapperCol.span : undefined;
|
|
270
|
+
const wrapperColNumSpan = typeof wrapperCol === 'number' ? wrapperCol : undefined;
|
|
271
|
+
const wrapperColSpan = wrapperColObjSpan ?? wrapperColNumSpan ?? ctx?.wrapperColSpan;
|
|
272
|
+
const wrapperColOffset = typeof wrapperCol === 'object' ? wrapperCol.offset : undefined;
|
|
273
|
+
const isHorizontalLayout = ctx?.layout === 'horizontal';
|
|
274
|
+
const hasLabel = label !== undefined;
|
|
275
|
+
// 注册字段(供 Form.validateFields 跑 rules)
|
|
276
|
+
React.useEffect(() => {
|
|
277
|
+
if (!ctx || !propName)
|
|
278
|
+
return;
|
|
279
|
+
const control = {
|
|
280
|
+
setValue: v => ctx.setFieldValue(propName, v),
|
|
281
|
+
validate: async () => {
|
|
282
|
+
const ers = await runRules(ctx.values[propName], rules);
|
|
283
|
+
setLocalErrors(ers);
|
|
284
|
+
return ers;
|
|
285
|
+
},
|
|
286
|
+
setErrors: ers => setLocalErrors(ers),
|
|
287
|
+
props,
|
|
288
|
+
};
|
|
289
|
+
return ctx.registerField(propName, control);
|
|
290
|
+
}, [propName, rules, ctx]);
|
|
291
|
+
// 给子控件注入 value/onChange(标准 antd 签名)
|
|
292
|
+
let clonedChild = child;
|
|
293
|
+
if (child && ctx && propName) {
|
|
294
|
+
const currentValue = ctx.values[propName] ?? initialValue;
|
|
295
|
+
clonedChild = React.cloneElement(child, {
|
|
296
|
+
[valuePropName]: currentValue,
|
|
297
|
+
onChange: (e) => {
|
|
298
|
+
// 兼容 React.ChangeEvent(e.target.value)、CustomEvent(e.detail)、裸值
|
|
299
|
+
let v;
|
|
300
|
+
if (e && e.target && valuePropName in e.target)
|
|
301
|
+
v = e.target[valuePropName];
|
|
302
|
+
else if (e && 'detail' in e)
|
|
303
|
+
v = e.detail;
|
|
304
|
+
else
|
|
305
|
+
v = e;
|
|
306
|
+
ctx.setFieldValue(propName, v);
|
|
307
|
+
// 透传业务方自己写的 onChange
|
|
308
|
+
child.props.onChange?.(e);
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
if (noStyle) {
|
|
313
|
+
return _jsx(_Fragment, { children: clonedChild });
|
|
314
|
+
}
|
|
315
|
+
const isVertical = ctx?.layout === 'vertical';
|
|
316
|
+
// 水平布局:label 占 labelCol 宽度(右对齐),wrapper 占 wrapperCol 宽度。
|
|
317
|
+
// 关键:即使本 Item 无 label,只要 Form 设置了 labelCol,wrapper 仍要左移 label 宽度,
|
|
318
|
+
// 保持与有 label 的行齐平(antd 行为)。wrapperCol.offset 额外再左移。
|
|
319
|
+
const labelWidthPct = labelColSpan != null ? `${(labelColSpan / 24) * 100}%` : undefined;
|
|
320
|
+
const wrapperWidthPct = wrapperColSpan != null ? `${(wrapperColSpan / 24) * 100}%` : undefined;
|
|
321
|
+
// 无 label 时:用空 label 占位(保持缩进);有 offset 时叠加
|
|
322
|
+
const offsetPct = wrapperColOffset != null ? `${(wrapperColOffset / 24) * 100}%` : undefined;
|
|
323
|
+
const showLabelPlaceholder = isHorizontalLayout && !hasLabel && labelWidthPct != null;
|
|
324
|
+
if (isVertical) {
|
|
325
|
+
return (_jsxs("div", { style: { marginBottom: 8, ...style }, className: className, children: [hasLabel && (_jsxs("div", { style: { fontSize: 14, color: 'var(--hb-color-text, rgba(0,0,0,0.88))', marginBottom: 8 }, children: [isRequired && ctx?.requiredMark !== false && _jsx("span", { style: { color: '#ff4d4f', marginRight: 4 }, children: "*" }), label] })), _jsx("div", { children: clonedChild }), errors.length > 0 && _jsx("div", { style: { color: '#ff4d4f', fontSize: 12, lineHeight: '20px', marginTop: 4 }, children: errors[0] })] }));
|
|
326
|
+
}
|
|
327
|
+
if (!isHorizontalLayout) {
|
|
328
|
+
// inline / 默认:纯堆叠
|
|
329
|
+
return (_jsxs("div", { style: { display: 'inline-flex', alignItems: 'center', marginRight: 16, marginBottom: 8, ...style }, className: className, children: [hasLabel && (_jsxs("span", { style: { fontSize: 14, color: 'var(--hb-color-text, rgba(0,0,0,0.88))', marginRight: 8 }, children: [isRequired && ctx?.requiredMark !== false && _jsx("span", { style: { color: '#ff4d4f', marginRight: 4 }, children: "*" }), label] })), _jsx("div", { children: clonedChild }), errors.length > 0 && _jsx("div", { style: { color: '#ff4d4f', fontSize: 12, marginLeft: 8 }, children: errors[0] })] }));
|
|
330
|
+
}
|
|
331
|
+
// 水平布局:label 列 + wrapper 列(flex,align-items 顶部对齐,label 用 line-height 与控件首行齐平)
|
|
332
|
+
return (_jsxs("div", { style: {
|
|
333
|
+
display: 'flex',
|
|
334
|
+
flexWrap: 'wrap',
|
|
335
|
+
alignItems: 'flex-start',
|
|
336
|
+
marginBottom: 24,
|
|
337
|
+
...style,
|
|
338
|
+
}, className: className, children: [(hasLabel || showLabelPlaceholder) && (_jsx("div", { style: {
|
|
339
|
+
flexGrow: 0,
|
|
340
|
+
flexShrink: 0,
|
|
341
|
+
width: labelWidthPct,
|
|
342
|
+
textAlign: ctx?.labelAlign === 'left' ? 'left' : 'right',
|
|
343
|
+
paddingRight: 8,
|
|
344
|
+
fontSize: 14,
|
|
345
|
+
color: 'var(--hb-color-text, rgba(0,0,0,0.88))',
|
|
346
|
+
lineHeight: '32px',
|
|
347
|
+
minHeight: 32,
|
|
348
|
+
}, children: hasLabel && (_jsxs(_Fragment, { children: [isRequired && ctx?.requiredMark !== false && _jsx("span", { style: { color: '#ff4d4f', fontFamily: 'SimSun, sans-serif', marginRight: 4 }, children: "*" }), label, colon && typeof label === 'string' ? ':' : ''] })) })), _jsxs("div", { style: { flex: 1, minWidth: 0, width: wrapperWidthPct, marginLeft: offsetPct }, children: [clonedChild, errors.length > 0 && _jsx("div", { style: { color: '#ff4d4f', fontSize: 12, lineHeight: '20px', marginTop: 4 }, children: errors[0] })] })] }));
|
|
126
349
|
}
|
|
127
350
|
Form.Item = FormItem;
|
package/lib/components/Image.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { HbImage } from '@huibo-ui/react';
|
|
3
3
|
export function Image(props) {
|
|
4
|
-
return _jsx(HbImage, { src: props.src || '', width: props.width ? String(props.width) : undefined, height: props.height ? String(props.height) : undefined, preview: props.preview, alt: props.alt, style: props.style });
|
|
4
|
+
return (_jsx(HbImage, { src: props.src || '', width: props.width ? String(props.width) : undefined, height: props.height ? String(props.height) : undefined, preview: props.preview ?? true, alt: props.alt, style: props.style }));
|
|
5
5
|
}
|
package/lib/components/Input.js
CHANGED
|
@@ -2,7 +2,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { HbInput } from '@huibo-ui/react';
|
|
3
3
|
export function Input(props) {
|
|
4
4
|
const { value, onChange, onInput, placeholder, disabled, readOnly, size, allowClear, showCount, maxLength, prefix, suffix, addonBefore, addonAfter, type, style, className, id, name, } = props;
|
|
5
|
-
|
|
5
|
+
// type 透传:textarea / password 都要保留,其余按 text。
|
|
6
|
+
const resolvedType = type === 'password' ? 'password' : type === 'textarea' ? 'textarea' : 'text';
|
|
7
|
+
return (_jsx(HbInput, { modelValue: value ?? '', placeholder: placeholder, disabled: disabled, readonly: readOnly, size: size === 'middle' ? 'default' : size, clearable: allowClear, showWordLimit: showCount, maxlength: maxLength, type: resolvedType, rows: props.rows, resize: props.resize, autosize: props.autoSize,
|
|
8
|
+
// antd prefix/suffix 是 ReactNode,hb-input 的 prefixIcon/suffixIcon 只接受 string,
|
|
9
|
+
// 取文本内容映射(图标类 prefix 暂不支持,文字 prefix 如 ¥ 生效)。
|
|
10
|
+
prefixIcon: typeof prefix === 'string' ? prefix : '', suffixIcon: typeof suffix === 'string' ? suffix : '', addonBefore: typeof addonBefore === 'string' ? addonBefore : '', addonAfter: typeof addonAfter === 'string' ? addonAfter : '', inputId: id, name: name, showPassword: props.showPassword, style: style,
|
|
6
11
|
// antd onChange 接收 React.ChangeEvent,huibo hbChange 接收 CustomEvent<string>
|
|
7
12
|
onHbChange: (e) => {
|
|
8
13
|
if (onChange)
|
|
@@ -13,13 +18,29 @@ export function Input(props) {
|
|
|
13
18
|
} }));
|
|
14
19
|
}
|
|
15
20
|
Input.TextArea = function TextArea(props) {
|
|
16
|
-
|
|
21
|
+
// antd TextArea:autoSize 时锁定高度(不可拖拽),否则默认允许垂直拖拽(resize:vertical)。
|
|
22
|
+
const resize = props.autoSize ? 'none' : 'vertical';
|
|
23
|
+
return _jsx(Input, { ...props, type: "textarea", resize: resize });
|
|
17
24
|
};
|
|
18
25
|
/**
|
|
19
|
-
* Input.Password ——
|
|
26
|
+
* Input.Password —— 带眼睛图标切换显示/隐藏(对齐 antd)。
|
|
27
|
+
*
|
|
28
|
+
* 修复前:用 suffix={emoji} 自渲染眼睛图标,但 hb-input 的 suffixIcon 不支持 onClick,
|
|
29
|
+
* 图标只能看不能点,无法切换显示/隐藏。
|
|
30
|
+
*
|
|
31
|
+
* 现在:hb-input 原生支持 type="password" + showPassword,内部已实现可点击的眼睛切换
|
|
32
|
+
* (见 Input.tsx 的 .hb-input__password,onClick→togglePasswordVisible,图标随状态切换
|
|
33
|
+
* 睁眼/闭眼 SVG)。这里只需透传 showPassword=true 即可获得与 antd 一致的切换交互,
|
|
34
|
+
* 不再自渲染 emoji suffix。
|
|
35
|
+
*
|
|
36
|
+
* visibilityToggle=false 时关闭切换(对齐 antd visibilityToggle 属性)。
|
|
20
37
|
*/
|
|
21
38
|
Input.Password = function Password(props) {
|
|
22
|
-
|
|
39
|
+
const visibilityToggle = props.visibilityToggle !== false;
|
|
40
|
+
// 透传给 Input:type 固定 password;当 visibilityToggle 为 false 时不开启底层切换。
|
|
41
|
+
// 注意 Input.Password 不应接受外部 suffix(否则会与眼睛图标冲突),这里不透传 suffix。
|
|
42
|
+
const { suffix: _omitSuffix, ...rest } = props;
|
|
43
|
+
return _jsx(Input, { ...rest, type: "password", showPassword: visibilityToggle });
|
|
23
44
|
};
|
|
24
45
|
Input.Search = function Search(props) {
|
|
25
46
|
const { onSearch, enterButton, ...rest } = props;
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { HbInputNumber } from '@huibo-ui/react';
|
|
3
3
|
export function InputNumber(props) {
|
|
4
|
-
|
|
4
|
+
// value 可能是 null/undefined/NaN(Form 初始或 InputNumber 内部 bug)。
|
|
5
|
+
// 传给 hb-input-number 的 modelValue 必须是合法 number 或 undefined,
|
|
6
|
+
// 传 null/NaN 会让内部 currentValue 变 NaN,点击加减按钮又 emit NaN 污染 Form。
|
|
7
|
+
const safeValue = typeof props.value === 'number' && !Number.isNaN(props.value) ? props.value : props.defaultValue;
|
|
8
|
+
// ⚠️ min/max/step/precision 不能透传 undefined:React 兼容层 attachProps 会执行
|
|
9
|
+
// node.step = undefined,覆盖 hb-input-number 的 @Prop 默认值(step 默认 1),
|
|
10
|
+
// 导致 increase 里 base + this.step = 3 + undefined = NaN,点加减号 emit NaN。
|
|
11
|
+
// 故这些数值 prop 必须给 antd 语义的默认值兜底。
|
|
12
|
+
return (_jsx(HbInputNumber, { modelValue: safeValue, min: props.min ?? -Infinity, max: props.max ?? Infinity, step: props.step ?? 1, precision: props.precision, disabled: props.disabled, size: props.size === 'middle' ? 'default' : props.size, controlsPosition: "right", placeholder: props.placeholder, style: props.style, onHbChange: (e) => {
|
|
13
|
+
// 兜底:hb-input-number 在点击加减按钮等路径下会 emit NaN,
|
|
14
|
+
// 这里转成 null(antd 语义:空值),避免污染 Form 字段。
|
|
15
|
+
const v = e.detail;
|
|
16
|
+
const out = typeof v === 'number' && !Number.isNaN(v) ? v : null;
|
|
17
|
+
props.onChange?.(out);
|
|
18
|
+
} }));
|
|
5
19
|
}
|
|
@@ -26,7 +26,7 @@ export declare namespace Layout {
|
|
|
26
26
|
var Footer: ({ children, style, className }: any) => React.JSX.Element;
|
|
27
27
|
}
|
|
28
28
|
export declare function Row(props: {
|
|
29
|
-
gutter?: number | number
|
|
29
|
+
gutter?: number | [number, number];
|
|
30
30
|
justify?: string;
|
|
31
31
|
align?: string;
|
|
32
32
|
children?: React.ReactNode;
|
package/lib/components/Layout.js
CHANGED
|
@@ -31,9 +31,13 @@ Layout.Sider = function Sider({ children, style, className, collapsed, theme, wi
|
|
|
31
31
|
};
|
|
32
32
|
/**
|
|
33
33
|
* Content —— 主内容区。flex:1 撑满剩余空间。
|
|
34
|
+
*
|
|
35
|
+
* 对齐 antd .ant-layout-content(flex: auto + min-height: 0):
|
|
36
|
+
* 当消费方传入 height:'auto'(常见脆弱写法)时,flex 容器仍会基于剩余
|
|
37
|
+
* 空间分配高度,避免子元素 height:100% 因父级塌缩而失效(如全屏背景图)。
|
|
34
38
|
*/
|
|
35
39
|
Layout.Content = function Content({ children, style, className }) {
|
|
36
|
-
return (_jsx("div", { style: { flex: 1, minWidth: 0, minHeight: 0, ...style }, className: className, children: children }));
|
|
40
|
+
return (_jsx("div", { style: { flex: '1 1 auto', minWidth: 0, minHeight: 0, ...style }, className: className, children: children }));
|
|
37
41
|
};
|
|
38
42
|
/**
|
|
39
43
|
* Footer —— 页脚。固定高度。
|
|
@@ -41,27 +45,38 @@ Layout.Content = function Content({ children, style, className }) {
|
|
|
41
45
|
Layout.Footer = function Footer({ children, style, className }) {
|
|
42
46
|
return (_jsx("div", { style: { flexShrink: 0, ...style }, className: className, children: children }));
|
|
43
47
|
};
|
|
48
|
+
// gutter 通过 React context 下发给每个 Col(antd 模型:gutter = Col 的 padding,而非 Row 的 gap)。
|
|
49
|
+
// 关键:用 gap + 百分比宽度会溢出换行(3×33.33% + 2×gap > 100%),导致栅格错位。
|
|
50
|
+
// 改用 antd 的「Row 负 margin + Col padding」模型:Col 宽度是百分比且 box-sizing:border-box,
|
|
51
|
+
// gutter 由 Col 两侧 padding 提供,Row 用等量负 margin 抵消两端 padding,宽度永远精确。
|
|
52
|
+
const RowGutterContext = React.createContext({});
|
|
44
53
|
export function Row(props) {
|
|
45
|
-
const
|
|
46
|
-
const
|
|
54
|
+
const rawGutter = props.gutter;
|
|
55
|
+
const horizontal = Array.isArray(rawGutter) ? rawGutter[0] : typeof rawGutter === 'number' ? rawGutter : 0;
|
|
56
|
+
const vertical = Array.isArray(rawGutter) ? rawGutter[1] : typeof rawGutter === 'number' ? rawGutter : 0;
|
|
57
|
+
const gutterCtx = React.useMemo(() => ({ horizontal, vertical }), [horizontal, vertical]);
|
|
47
58
|
const justifyMap = {
|
|
48
|
-
start: 'flex-start',
|
|
49
|
-
end: 'flex-end',
|
|
50
|
-
center: 'center',
|
|
59
|
+
'start': 'flex-start',
|
|
60
|
+
'end': 'flex-end',
|
|
61
|
+
'center': 'center',
|
|
51
62
|
'space-between': 'space-between',
|
|
52
63
|
'space-around': 'space-around',
|
|
53
64
|
};
|
|
54
|
-
return (_jsx("div", { style: {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
return (_jsx(RowGutterContext.Provider, { value: gutterCtx, children: _jsx("div", { style: {
|
|
66
|
+
display: 'flex',
|
|
67
|
+
flexWrap: 'wrap',
|
|
68
|
+
// 负 margin 抵消首尾 Col 的 padding(antd 行为),让 Row 仍贴齐父容器两边。
|
|
69
|
+
marginLeft: horizontal ? `${-horizontal / 2}px` : undefined,
|
|
70
|
+
marginRight: horizontal ? `${-horizontal / 2}px` : undefined,
|
|
71
|
+
rowGap: vertical ? `${vertical}px` : undefined,
|
|
72
|
+
justifyContent: props.justify ? justifyMap[props.justify] || props.justify : undefined,
|
|
73
|
+
alignItems: props.align,
|
|
74
|
+
...props.style,
|
|
75
|
+
}, className: props.className, children: props.children }) }));
|
|
62
76
|
}
|
|
63
|
-
// Col
|
|
77
|
+
// Col:百分比宽度(box-sizing:border-box),两侧 padding 提供 gutter;支持响应式 + offset。
|
|
64
78
|
export function Col(props) {
|
|
79
|
+
const gutter = React.useContext(RowGutterContext);
|
|
65
80
|
// 响应式:取当前视口匹配的最大断点(xl>lg>md>sm>xs),都没有则用 span(默认 24)
|
|
66
81
|
const resolveSpan = () => {
|
|
67
82
|
if (typeof window !== 'undefined') {
|
|
@@ -84,5 +99,15 @@ export function Col(props) {
|
|
|
84
99
|
const span = resolveSpan();
|
|
85
100
|
const pct = (span / 24) * 100;
|
|
86
101
|
const offsetPct = props.offset ? (props.offset / 24) * 100 : 0;
|
|
87
|
-
|
|
102
|
+
const padH = gutter.horizontal ? `${gutter.horizontal / 2}px` : undefined;
|
|
103
|
+
// 垂直 gutter 由 Row 的 rowGap 提供,Col 不再加垂直 padding(否则与 rowGap 双重叠加,
|
|
104
|
+
// 导致 Row 整体偏高,且首/末行出现多余上下空白)。
|
|
105
|
+
return (_jsx("div", { style: {
|
|
106
|
+
width: `${pct}%`,
|
|
107
|
+
paddingLeft: padH,
|
|
108
|
+
paddingRight: padH,
|
|
109
|
+
marginLeft: offsetPct ? `${offsetPct}%` : undefined,
|
|
110
|
+
boxSizing: 'border-box',
|
|
111
|
+
...props.style,
|
|
112
|
+
}, className: props.className, children: props.children }));
|
|
88
113
|
}
|