@huibo-ui/react-antd 1.0.11 → 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/Avatar.js +4 -1
- 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 +121 -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/Form.d.ts +25 -14
- package/lib/components/Form.js +307 -95
- 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 +38 -13
- package/lib/components/Menu.js +53 -9
- package/lib/components/Modal.d.ts +4 -1
- package/lib/components/Modal.js +137 -22
- package/lib/components/Radio.d.ts +5 -3
- package/lib/components/Radio.js +24 -13
- package/lib/components/Segmented.d.ts +5 -3
- package/lib/components/Segmented.js +4 -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/Statistic.d.ts +1 -0
- package/lib/components/Statistic.js +5 -1
- package/lib/components/Switch.d.ts +6 -0
- package/lib/components/Switch.js +15 -1
- package/lib/components/Tabs.js +51 -26
- package/lib/components/Tag.d.ts +1 -0
- package/lib/components/Tag.js +6 -2
- package/lib/components/Timeline.js +9 -1
- package/lib/components/TreeSelect.d.ts +2 -0
- package/lib/components/TreeSelect.js +19 -1
- package/lib/components/Typography.js +2 -2
- package/lib/components/Watermark.js +21 -1
- package/package.json +5 -6
package/lib/components/Form.d.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
/**
|
|
3
|
-
* antd Form
|
|
3
|
+
* antd Form 兼容层(纯 div + React 受控实现)。
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* 不再走 <HbForm>/<HbFormItem>(Shadow DOM slot):
|
|
6
|
+
* 1) hb-form-item 的 labelCol/wrapperCol 栅格、labelAlign、必填星号等布局受 Shadow DOM
|
|
7
|
+
* 限制,弹窗/卡片内嵌表单极易错位;
|
|
8
|
+
* 2) hb-form 内部靠 querySelector('hb-input,...') 取子控件 modelValue,在纯 React 子树
|
|
9
|
+
* (兼容层组件已是标准 antd 签名 value/onChange)下完全多余且脆弱。
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* - validateFields / validate 保持异步(antd 本身也返回 Promise)
|
|
11
|
+
* 新方案对齐 antd 的 rc-field-form 思路:Form 持有 values/errors 的 React state,
|
|
12
|
+
* FormItem 通过 cloneElement 给唯一子控件注入 value/onChange/onChange(标准 antd 签名),
|
|
13
|
+
* 子控件作为真实 DOM 参与 flex 文档流。校验在 React 层跑,validateFields 返回 values。
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
-
* useForm 与 ref 共享同一份 createFormInstance 逻辑。
|
|
15
|
+
* FormInstance 保持 antd 语义:getFieldsValue/setFieldsValue/validateFields/resetFields。
|
|
16
16
|
*/
|
|
17
17
|
export interface FormInstance {
|
|
18
18
|
getFieldValue: (name: string) => any;
|
|
@@ -24,11 +24,22 @@ export interface FormInstance {
|
|
|
24
24
|
resetFields: (nameList?: string[]) => void;
|
|
25
25
|
submit: () => void;
|
|
26
26
|
scrollToField: (name: string) => void;
|
|
27
|
-
getFieldsError: () =>
|
|
27
|
+
getFieldsError: () => Array<{
|
|
28
|
+
name: string;
|
|
29
|
+
errors: string[];
|
|
30
|
+
}>;
|
|
28
31
|
getFieldError: (name: string) => string[];
|
|
29
|
-
/**
|
|
32
|
+
/** 内部:注册字段(FormItem 挂载时调用) */
|
|
33
|
+
__registerField?: (name: string, field: FieldControl) => () => void;
|
|
34
|
+
/** scrm 兼容 */
|
|
30
35
|
handleReset?: () => void;
|
|
31
36
|
}
|
|
37
|
+
interface FieldControl {
|
|
38
|
+
setValue: (value: any) => void;
|
|
39
|
+
validate: () => Promise<string[]>;
|
|
40
|
+
setErrors: (errors: string[]) => void;
|
|
41
|
+
props: FormItemProps;
|
|
42
|
+
}
|
|
32
43
|
export interface FormProps {
|
|
33
44
|
form?: FormInstance;
|
|
34
45
|
initialValues?: Record<string, any>;
|
|
@@ -49,10 +60,11 @@ export interface FormProps {
|
|
|
49
60
|
children?: React.ReactNode;
|
|
50
61
|
style?: React.CSSProperties;
|
|
51
62
|
className?: string;
|
|
63
|
+
name?: string;
|
|
52
64
|
[key: string]: any;
|
|
53
65
|
}
|
|
54
66
|
type FormType = React.ForwardRefExoticComponent<FormProps & React.RefAttributes<FormInstance>> & {
|
|
55
|
-
Item:
|
|
67
|
+
Item: React.ComponentType<FormItemProps>;
|
|
56
68
|
useForm: () => [FormInstance];
|
|
57
69
|
useWatch: (namePath: string | string[], form?: FormInstance) => any;
|
|
58
70
|
List: any;
|
|
@@ -75,7 +87,6 @@ export interface FormItemProps {
|
|
|
75
87
|
};
|
|
76
88
|
colon?: boolean;
|
|
77
89
|
tooltip?: React.ReactNode;
|
|
78
|
-
/** antd noStyle:不额外包裹。hb-form-item 无对应概念,这里仅消费该 prop 不报错 */
|
|
79
90
|
noStyle?: boolean;
|
|
80
91
|
style?: React.CSSProperties;
|
|
81
92
|
className?: string;
|
package/lib/components/Form.js
CHANGED
|
@@ -1,25 +1,53 @@
|
|
|
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
|
-
|
|
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],
|
|
22
49
|
getFieldsValue: nameList => {
|
|
50
|
+
const { values } = getState();
|
|
23
51
|
if (nameList && nameList.length > 0) {
|
|
24
52
|
const picked = {};
|
|
25
53
|
nameList.forEach(n => {
|
|
@@ -30,93 +58,190 @@ function createFormInstance() {
|
|
|
30
58
|
}
|
|
31
59
|
return { ...values };
|
|
32
60
|
},
|
|
33
|
-
setFieldValue:
|
|
34
|
-
values[name] = value;
|
|
35
|
-
void run(el => el.setFieldValueMethod(name, value));
|
|
36
|
-
},
|
|
61
|
+
setFieldValue: api.setFieldValue,
|
|
37
62
|
setFieldsValue: vals => {
|
|
38
63
|
if (vals)
|
|
39
|
-
Object.
|
|
40
|
-
void run(el => el.setFieldsValue(vals));
|
|
64
|
+
Object.keys(vals).forEach(n => api.setFieldValue(n, vals[n]));
|
|
41
65
|
},
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
66
|
+
validateFields: api.validateFields,
|
|
67
|
+
validate: async (nameList) => {
|
|
68
|
+
try {
|
|
69
|
+
await api.validateFields(nameList);
|
|
70
|
+
return true;
|
|
47
71
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
delete values[k];
|
|
51
|
-
});
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
52
74
|
}
|
|
53
|
-
void run(el => el.resetFields(nameList));
|
|
54
|
-
},
|
|
55
|
-
validateFields: nameList => run(el => el.validateFields(nameList)).then(() => ({ ...values })),
|
|
56
|
-
validate: nameList => run(el => (nameList ? el.validate(nameList) : el.validate())),
|
|
57
|
-
submit: () => {
|
|
58
|
-
void run(async (el) => {
|
|
59
|
-
try {
|
|
60
|
-
await el.validateFields();
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
/* 校验失败由 FormItem 展示 */
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
75
|
},
|
|
76
|
+
resetFields: api.resetFields,
|
|
77
|
+
submit: api.submit,
|
|
67
78
|
scrollToField: () => { },
|
|
68
|
-
getFieldsError: () => [],
|
|
69
|
-
getFieldError: ()
|
|
70
|
-
|
|
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(),
|
|
71
83
|
};
|
|
72
|
-
// 内部钩子:Form 组件挂
|
|
73
|
-
inst.__setRef = bind;
|
|
74
|
-
inst.__syncValue = (prop, value) => {
|
|
75
|
-
if (prop)
|
|
76
|
-
values[prop] = value;
|
|
77
|
-
};
|
|
78
|
-
return inst;
|
|
79
84
|
}
|
|
80
|
-
|
|
81
|
-
const { form: formProp, initialValues, onFinish, onFinishFailed, onValuesChange, layout, labelCol, children, style, className } = props;
|
|
82
|
-
const [
|
|
83
|
-
const
|
|
84
|
-
React.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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;
|
|
89
132
|
}
|
|
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
|
+
};
|
|
90
160
|
}, []);
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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 重定向到本组件实现。
|
|
101
176
|
React.useEffect(() => {
|
|
102
|
-
|
|
103
|
-
if (!el)
|
|
177
|
+
if (!formProp)
|
|
104
178
|
return;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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 };
|
|
191
|
+
};
|
|
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
|
+
}
|
|
113
203
|
};
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
});
|
|
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);
|
|
117
227
|
Form.useForm = function useForm() {
|
|
118
|
-
const
|
|
119
|
-
|
|
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];
|
|
120
245
|
};
|
|
121
246
|
Form.useWatch = function useWatch(namePath, form) {
|
|
122
247
|
const name = Array.isArray(namePath) ? namePath.join('.') : namePath;
|
|
@@ -131,8 +256,95 @@ Form.useWatch = function useWatch(namePath, form) {
|
|
|
131
256
|
Form.List = ({ children }) => children || null;
|
|
132
257
|
Form.Provider = ({ children }) => children || null;
|
|
133
258
|
export function FormItem(props) {
|
|
134
|
-
const { name, label, rules, required, labelCol,
|
|
135
|
-
const
|
|
136
|
-
|
|
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] })] })] }));
|
|
137
349
|
}
|
|
138
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;
|