@fastspace/schema-form 0.0.1
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 +1124 -0
- package/dist/index.d.ts +539 -0
- package/dist/schema-form-lib.js +37784 -0
- package/dist/schema-form-lib.umd.cjs +180 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,1124 @@
|
|
|
1
|
+
# SchemaForm 组件文档
|
|
2
|
+
|
|
3
|
+
基于 JSON Schema 驱动的动态表单组件,支持验证、联动、计算字段等功能。
|
|
4
|
+
|
|
5
|
+
## 目录
|
|
6
|
+
|
|
7
|
+
- [快速开始](#快速开始)
|
|
8
|
+
- [组件类型](#组件类型)
|
|
9
|
+
- [验证规则](#验证规则)
|
|
10
|
+
- [条件控制](#条件控制)
|
|
11
|
+
- [布局配置](#布局配置)
|
|
12
|
+
- [FormList 动态列表](#formlist-动态列表)
|
|
13
|
+
- [Group 字段分组](#group-字段分组)
|
|
14
|
+
- [Custom 自定义组件](#custom-自定义组件)
|
|
15
|
+
- [计算字段](#计算字段)
|
|
16
|
+
- [API 参考](#api-参考)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 快速开始
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { SchemaForm, type SchemaInput, type SchemaFormInstance } from './share/Schema';
|
|
24
|
+
|
|
25
|
+
const schema: SchemaInput = {
|
|
26
|
+
fields: [
|
|
27
|
+
{
|
|
28
|
+
name: 'username',
|
|
29
|
+
component: 'Text',
|
|
30
|
+
ui: { label: '用户名', placeholder: '请输入用户名' },
|
|
31
|
+
rules: [
|
|
32
|
+
{ type: 'required', message: '用户名必填' },
|
|
33
|
+
{ type: 'minLength', value: 3, message: '至少3个字符' },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'email',
|
|
38
|
+
component: 'Text',
|
|
39
|
+
ui: { label: '邮箱' },
|
|
40
|
+
rules: [
|
|
41
|
+
{ type: 'required', message: '邮箱必填' },
|
|
42
|
+
{ type: 'email', message: '请输入有效的邮箱' },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function App() {
|
|
49
|
+
const formRef = useRef<SchemaFormInstance>(null);
|
|
50
|
+
|
|
51
|
+
const handleSubmit = (values) => {
|
|
52
|
+
console.log('提交数据:', values);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<SchemaForm
|
|
57
|
+
ref={formRef}
|
|
58
|
+
schema={schema}
|
|
59
|
+
onSubmit={handleSubmit}
|
|
60
|
+
spacing={2}
|
|
61
|
+
>
|
|
62
|
+
<Button onClick={() => formRef.current?.submit()}>提交</Button>
|
|
63
|
+
</SchemaForm>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 组件类型
|
|
71
|
+
|
|
72
|
+
### 基础输入组件
|
|
73
|
+
|
|
74
|
+
| 组件名 | 说明 | 数据类型 |
|
|
75
|
+
|--------|------|----------|
|
|
76
|
+
| `Text` | 单行文本输入 | `string` |
|
|
77
|
+
| `Password` | 密码输入 | `string` |
|
|
78
|
+
| `Number` | 数字输入 | `number` |
|
|
79
|
+
| `Textarea` | 多行文本 | `string` |
|
|
80
|
+
|
|
81
|
+
### 选择组件
|
|
82
|
+
|
|
83
|
+
| 组件名 | 说明 | 数据类型 |
|
|
84
|
+
|--------|------|----------|
|
|
85
|
+
| `Select` | 下拉选择(单选) | `string \| number` |
|
|
86
|
+
| `Autocomplete` | 自动完成(支持多选) | `string \| number \| array` |
|
|
87
|
+
| `Checkbox` | 复选框 | `boolean` |
|
|
88
|
+
| `Switch` | 开关 | `boolean` |
|
|
89
|
+
| `Radio` | 单选组 | `string \| number` |
|
|
90
|
+
|
|
91
|
+
### 日期时间组件
|
|
92
|
+
|
|
93
|
+
| 组件名 | 说明 | 数据类型 |
|
|
94
|
+
|--------|------|----------|
|
|
95
|
+
| `Date` | 日期选择 | `string` (YYYY-MM-DD) |
|
|
96
|
+
| `Time` | 时间选择 | `string` |
|
|
97
|
+
| `DateTime` | 日期时间选择 | `string` (YYYY-MM-DD HH:mm) |
|
|
98
|
+
|
|
99
|
+
### 其他组件
|
|
100
|
+
|
|
101
|
+
| 组件名 | 说明 | 数据类型 |
|
|
102
|
+
|--------|------|----------|
|
|
103
|
+
| `Slider` | 滑块 | `number` |
|
|
104
|
+
| `Rating` | 评分 | `number` |
|
|
105
|
+
| `Hidden` | 隐藏字段 | `any` |
|
|
106
|
+
| `Custom` | 自定义组件 | `any` |
|
|
107
|
+
|
|
108
|
+
### 复合组件
|
|
109
|
+
|
|
110
|
+
| 组件名 | 说明 |
|
|
111
|
+
|--------|------|
|
|
112
|
+
| `FormList` | 动态列表(可增删行) |
|
|
113
|
+
| `Group` | 字段分组(多字段一行) |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 验证规则
|
|
118
|
+
|
|
119
|
+
### 规则类型一览
|
|
120
|
+
|
|
121
|
+
| 规则类型 | 说明 | 适用组件 | 参数 |
|
|
122
|
+
|----------|------|----------|------|
|
|
123
|
+
| `required` | 必填 | 所有 | `message?: string` |
|
|
124
|
+
| `minLength` | 最小长度 | Text, Password, Textarea | `value: number, message?: string` |
|
|
125
|
+
| `maxLength` | 最大长度 | Text, Password, Textarea | `value: number, message?: string` |
|
|
126
|
+
| `min` | 最小值 | Number, Slider, Rating | `value: number, message?: string` |
|
|
127
|
+
| `max` | 最大值 | Number, Slider, Rating | `value: number, message?: string` |
|
|
128
|
+
| `pattern` | 正则匹配 | Text, Password, Textarea | `value: string \| RegExp, message?: string` |
|
|
129
|
+
| `email` | 邮箱格式 | Text | `message?: string` |
|
|
130
|
+
| `url` | URL格式 | Text | `message?: string` |
|
|
131
|
+
| `custom` | 自定义验证 | 所有 | `validate: (value, values) => boolean \| string` |
|
|
132
|
+
| `array` | 数组验证 | Upload, FormList | `minItems?: number, maxItems?: number, message?: string` |
|
|
133
|
+
|
|
134
|
+
### 使用示例
|
|
135
|
+
|
|
136
|
+
#### 必填验证
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
{
|
|
140
|
+
name: 'username',
|
|
141
|
+
component: 'Text',
|
|
142
|
+
rules: [
|
|
143
|
+
{ type: 'required', message: '用户名不能为空' }
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### 长度验证
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
{
|
|
152
|
+
name: 'password',
|
|
153
|
+
component: 'Password',
|
|
154
|
+
rules: [
|
|
155
|
+
{ type: 'required', message: '密码必填' },
|
|
156
|
+
{ type: 'minLength', value: 6, message: '密码至少6个字符' },
|
|
157
|
+
{ type: 'maxLength', value: 20, message: '密码最多20个字符' }
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### 数值范围验证
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
{
|
|
166
|
+
name: 'age',
|
|
167
|
+
component: 'Number',
|
|
168
|
+
rules: [
|
|
169
|
+
{ type: 'required', message: '年龄必填' },
|
|
170
|
+
{ type: 'min', value: 0, message: '年龄不能为负数' },
|
|
171
|
+
{ type: 'max', value: 150, message: '年龄不能超过150' }
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### 正则验证
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
{
|
|
180
|
+
name: 'phone',
|
|
181
|
+
component: 'Text',
|
|
182
|
+
rules: [
|
|
183
|
+
{ type: 'required', message: '手机号必填' },
|
|
184
|
+
{
|
|
185
|
+
type: 'pattern',
|
|
186
|
+
value: '^1[3-9]\\d{9}$', // 注意转义
|
|
187
|
+
message: '请输入有效的手机号'
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
#### 邮箱验证
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
{
|
|
197
|
+
name: 'email',
|
|
198
|
+
component: 'Text',
|
|
199
|
+
rules: [
|
|
200
|
+
{ type: 'required', message: '邮箱必填' },
|
|
201
|
+
{ type: 'email', message: '请输入有效的邮箱地址' }
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### URL 验证
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
{
|
|
210
|
+
name: 'website',
|
|
211
|
+
component: 'Text',
|
|
212
|
+
rules: [
|
|
213
|
+
{ type: 'url', message: '请输入有效的网址' }
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
#### 自定义验证
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
{
|
|
222
|
+
name: 'confirmPassword',
|
|
223
|
+
component: 'Password',
|
|
224
|
+
rules: [
|
|
225
|
+
{ type: 'required', message: '请确认密码' },
|
|
226
|
+
{
|
|
227
|
+
type: 'custom',
|
|
228
|
+
validate: (value, values) => {
|
|
229
|
+
if (value !== values.password) {
|
|
230
|
+
return '两次密码输入不一致'; // 返回错误消息
|
|
231
|
+
}
|
|
232
|
+
return true; // 返回 true 表示验证通过
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
#### Checkbox 必须勾选
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
{
|
|
243
|
+
name: 'agreeTerms',
|
|
244
|
+
component: 'Checkbox',
|
|
245
|
+
ui: { label: '我已阅读并同意服务条款' },
|
|
246
|
+
rules: [
|
|
247
|
+
{ type: 'required', message: '请同意服务条款' }
|
|
248
|
+
]
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## 条件控制
|
|
255
|
+
|
|
256
|
+
### visibleWhen - 条件显示
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
{
|
|
260
|
+
name: 'companyName',
|
|
261
|
+
component: 'Text',
|
|
262
|
+
ui: { label: '公司名称' },
|
|
263
|
+
// 当 accountType 等于 'business' 时显示
|
|
264
|
+
visibleWhen: { field: 'accountType', eq: 'business' }
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### disabledWhen - 条件禁用
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
{
|
|
272
|
+
name: 'discount',
|
|
273
|
+
component: 'Number',
|
|
274
|
+
// 当 isVip 为 false 时禁用
|
|
275
|
+
disabledWhen: { field: 'isVip', eq: false }
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### requiredWhen - 条件必填
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
{
|
|
283
|
+
name: 'taxId',
|
|
284
|
+
component: 'Text',
|
|
285
|
+
// 当 accountType 等于 'business' 时必填
|
|
286
|
+
requiredWhen: { field: 'accountType', eq: 'business' }
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### 条件表达式
|
|
291
|
+
|
|
292
|
+
#### 简单条件
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
// 等于
|
|
296
|
+
{ field: 'status', eq: 'active' }
|
|
297
|
+
|
|
298
|
+
// 不等于
|
|
299
|
+
{ field: 'status', ne: 'inactive' }
|
|
300
|
+
|
|
301
|
+
// 大于
|
|
302
|
+
{ field: 'age', gt: 18 }
|
|
303
|
+
|
|
304
|
+
// 大于等于
|
|
305
|
+
{ field: 'age', gte: 18 }
|
|
306
|
+
|
|
307
|
+
// 小于
|
|
308
|
+
{ field: 'price', lt: 100 }
|
|
309
|
+
|
|
310
|
+
// 小于等于
|
|
311
|
+
{ field: 'price', lte: 100 }
|
|
312
|
+
|
|
313
|
+
// 在数组中
|
|
314
|
+
{ field: 'type', in: ['a', 'b', 'c'] }
|
|
315
|
+
|
|
316
|
+
// 不在数组中
|
|
317
|
+
{ field: 'type', notIn: ['x', 'y'] }
|
|
318
|
+
|
|
319
|
+
// 为空
|
|
320
|
+
{ field: 'name', empty: true }
|
|
321
|
+
|
|
322
|
+
// 非空
|
|
323
|
+
{ field: 'name', notEmpty: true }
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
#### 复合条件
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
// AND 逻辑
|
|
330
|
+
{
|
|
331
|
+
and: [
|
|
332
|
+
{ field: 'type', eq: 'business' },
|
|
333
|
+
{ field: 'country', eq: 'CN' }
|
|
334
|
+
]
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// OR 逻辑
|
|
338
|
+
{
|
|
339
|
+
or: [
|
|
340
|
+
{ field: 'type', eq: 'vip' },
|
|
341
|
+
{ field: 'points', gte: 1000 }
|
|
342
|
+
]
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// NOT 逻辑
|
|
346
|
+
{
|
|
347
|
+
not: { field: 'status', eq: 'disabled' }
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## 布局配置
|
|
354
|
+
|
|
355
|
+
### colSpan - 响应式布局
|
|
356
|
+
|
|
357
|
+
通过 `colSpan` 控制字段宽度(基于 12 栅格):
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
// 简单值 - 固定宽度
|
|
361
|
+
{ colSpan: 6 } // 占 50%
|
|
362
|
+
|
|
363
|
+
// 响应式
|
|
364
|
+
{
|
|
365
|
+
colSpan: {
|
|
366
|
+
xs: 12, // 手机:100%
|
|
367
|
+
sm: 6, // 平板:50%
|
|
368
|
+
md: 4, // 桌面:33%
|
|
369
|
+
lg: 3, // 大屏:25%
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### spacing - 间距
|
|
375
|
+
|
|
376
|
+
```tsx
|
|
377
|
+
<SchemaForm schema={schema} spacing={3} />
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### newLine - 强制换行
|
|
381
|
+
|
|
382
|
+
在 `FieldSchema` 中添加 `newLine: true` 可以强制字段从新的一行开始。
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
{
|
|
386
|
+
name: 'is_super_admin',
|
|
387
|
+
component: 'Radio',
|
|
388
|
+
newLine: true, // 👈 强制从新行开始
|
|
389
|
+
ui: { ... },
|
|
390
|
+
colSpan: { xs: 12, md: 6 }, // 即使宽度只有一半,也会另起一行
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Radio 行内布局 (inline)
|
|
395
|
+
|
|
396
|
+
通过在 `ui.props` 中设置 `inline: true`,可以让 Radio 组件的 Label 和选项在同一行显示。
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
{
|
|
400
|
+
name: 'is_super_admin',
|
|
401
|
+
component: 'Radio',
|
|
402
|
+
ui: {
|
|
403
|
+
label: '超管状态',
|
|
404
|
+
props: {
|
|
405
|
+
inline: true, // 👈 label 和 radio 选项在同一行显示
|
|
406
|
+
},
|
|
407
|
+
options: [
|
|
408
|
+
{ label: '普通用户', value: 0 },
|
|
409
|
+
{ label: '超级管理员', value: 1 },
|
|
410
|
+
],
|
|
411
|
+
},
|
|
412
|
+
colSpan: { xs: 12, md: 6 },
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## FormList 动态列表
|
|
419
|
+
|
|
420
|
+
用于动态添加/删除表单行。
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
{
|
|
424
|
+
name: 'contacts',
|
|
425
|
+
component: 'FormList',
|
|
426
|
+
ui: { label: '联系人列表' },
|
|
427
|
+
// 默认值:初始化时显示的行 (支持多行)
|
|
428
|
+
defaultValue: [
|
|
429
|
+
{ name: '张三', phone: '13800000001' },
|
|
430
|
+
{ name: '李四', phone: '13800000002' }
|
|
431
|
+
],
|
|
432
|
+
minItems: 1, // 最少1行
|
|
433
|
+
maxItems: 5, // 最多5行
|
|
434
|
+
addText: '添加联系人', // 添加按钮文字
|
|
435
|
+
copyable: true, // 允许复制行
|
|
436
|
+
columns: [
|
|
437
|
+
{
|
|
438
|
+
name: 'name',
|
|
439
|
+
component: 'Text',
|
|
440
|
+
ui: { label: '姓名' },
|
|
441
|
+
rules: [{ type: 'required', message: '姓名必填' }],
|
|
442
|
+
colSpan: { xs: 12, sm: 6 }
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
name: 'phone',
|
|
446
|
+
component: 'Text',
|
|
447
|
+
ui: { label: '电话' },
|
|
448
|
+
rules: [
|
|
449
|
+
{ type: 'required', message: '电话必填' },
|
|
450
|
+
{ type: 'pattern', value: '^1[3-9]\\d{9}$', message: '请输入有效的手机号' }
|
|
451
|
+
],
|
|
452
|
+
colSpan: { xs: 12, sm: 6 }
|
|
453
|
+
}
|
|
454
|
+
]
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### FormList 配置项
|
|
459
|
+
|
|
460
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
461
|
+
|------|------|--------|------|
|
|
462
|
+
| `columns` | `FieldSchema[]` | - | 子字段定义 |
|
|
463
|
+
| `defaultValue` | `array` | `[]` | 初始值 |
|
|
464
|
+
| `minItems` | `number` | `0` | 最少行数 |
|
|
465
|
+
| `maxItems` | `number` | `Infinity` | 最多行数 |
|
|
466
|
+
| `addText` | `string` | `'添加一行'` | 添加按钮文字 |
|
|
467
|
+
| `copyable` | `boolean` | `false` | 是否可复制行 |
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
## Group 字段分组
|
|
472
|
+
|
|
473
|
+
将多个字段组合在一行显示。
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
{
|
|
477
|
+
name: 'addressGroup',
|
|
478
|
+
component: 'Group',
|
|
479
|
+
colSpan: { xs: 12 },
|
|
480
|
+
columns: [
|
|
481
|
+
{
|
|
482
|
+
name: 'province',
|
|
483
|
+
component: 'Select',
|
|
484
|
+
ui: { label: '省份', options: [...] },
|
|
485
|
+
colSpan: { xs: 12, sm: 4 }
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
name: 'city',
|
|
489
|
+
component: 'Select',
|
|
490
|
+
ui: { label: '城市', options: [...] },
|
|
491
|
+
colSpan: { xs: 12, sm: 4 }
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
name: 'district',
|
|
495
|
+
component: 'Select',
|
|
496
|
+
ui: { label: '区县', options: [...] },
|
|
497
|
+
colSpan: { xs: 12, sm: 4 }
|
|
498
|
+
}
|
|
499
|
+
]
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
## Custom 自定义组件
|
|
506
|
+
|
|
507
|
+
用于完全自定义渲染的场景。
|
|
508
|
+
|
|
509
|
+
### 方式1:children 函数(推荐)
|
|
510
|
+
|
|
511
|
+
可以访问 `field`、`form`、`error` 等,实现完全自定义的表单控件:
|
|
512
|
+
|
|
513
|
+
```typescript
|
|
514
|
+
{
|
|
515
|
+
name: 'customInput',
|
|
516
|
+
component: 'Custom',
|
|
517
|
+
colSpan: { xs: 12 },
|
|
518
|
+
ui: {
|
|
519
|
+
label: '自定义输入',
|
|
520
|
+
props: {
|
|
521
|
+
// children 是一个函数,接收 field、form、label、error、helperText 等参数
|
|
522
|
+
children: ({ field, form, label, error, helperText, fieldProps }) => {
|
|
523
|
+
return (
|
|
524
|
+
<div>
|
|
525
|
+
<label>{label}</label>
|
|
526
|
+
<input
|
|
527
|
+
type="text"
|
|
528
|
+
value={field.value ?? ''}
|
|
529
|
+
onChange={(e) => field.onChange(e.target.value)}
|
|
530
|
+
onBlur={field.onBlur}
|
|
531
|
+
disabled={fieldProps?.disabled}
|
|
532
|
+
style={{
|
|
533
|
+
border: error ? '1px solid red' : '1px solid #ccc',
|
|
534
|
+
padding: '8px',
|
|
535
|
+
width: '100%',
|
|
536
|
+
}}
|
|
537
|
+
/>
|
|
538
|
+
{helperText && <span style={{ color: error ? 'red' : '#666' }}>{helperText}</span>}
|
|
539
|
+
</div>
|
|
540
|
+
);
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
rules: [{ type: 'required', message: '此字段必填' }],
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
#### children 函数参数
|
|
549
|
+
|
|
550
|
+
| 参数 | 类型 | 说明 |
|
|
551
|
+
|------|------|------|
|
|
552
|
+
| `field` | `ControllerRenderProps` | RHF 字段对象,包含 `value`、`onChange`、`onBlur`、`name`、`ref` |
|
|
553
|
+
| `form` | `UseFormReturn` | 表单实例,可调用 `form.setValue()`、`form.trigger()` 等 |
|
|
554
|
+
| `values` | `Record<string, any>` | 当前表单值(通常仅包含被依赖的字段,如需全量可用 `form.getValues()`) |
|
|
555
|
+
| `label` | `string` | 标签文本 |
|
|
556
|
+
| `error` | `boolean` | 是否有错误 |
|
|
557
|
+
| `helperText` | `ReactNode` | 帮助/错误文本 |
|
|
558
|
+
| `fieldProps` | `object` | 包含 `disabled`、`required`、`readOnly` 等状态 |
|
|
559
|
+
|
|
560
|
+
### 方式2:children 静态内容
|
|
561
|
+
|
|
562
|
+
用于纯展示,不需要表单交互:
|
|
563
|
+
|
|
564
|
+
```typescript
|
|
565
|
+
{
|
|
566
|
+
name: 'notice',
|
|
567
|
+
component: 'Custom',
|
|
568
|
+
colSpan: { xs: 12 },
|
|
569
|
+
noSubmit: true, // 不参与表单提交
|
|
570
|
+
ui: {
|
|
571
|
+
props: {
|
|
572
|
+
children: (
|
|
573
|
+
<div style={{ padding: 16, background: '#f5f5f5', borderRadius: 8 }}>
|
|
574
|
+
<strong>💡 提示:</strong>
|
|
575
|
+
<span>这是一段提示信息</span>
|
|
576
|
+
</div>
|
|
577
|
+
),
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### 方式3:传入自定义组件
|
|
584
|
+
|
|
585
|
+
```typescript
|
|
586
|
+
// 先定义自定义组件
|
|
587
|
+
const MyCustomInput = ({ field, form, values, label, error, helperText }) => {
|
|
588
|
+
return (
|
|
589
|
+
<div>
|
|
590
|
+
<label>{label}</label>
|
|
591
|
+
<input
|
|
592
|
+
value={field.value ?? ''}
|
|
593
|
+
onChange={(e) => field.onChange(e.target.value)}
|
|
594
|
+
/>
|
|
595
|
+
{error && <span style={{ color: 'red' }}>{helperText}</span>}
|
|
596
|
+
{/* 可以在这里使用 values 或 form 做更多逻辑 */}
|
|
597
|
+
</div>
|
|
598
|
+
);
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
// 在 schema 中使用
|
|
602
|
+
{
|
|
603
|
+
name: 'myField',
|
|
604
|
+
component: 'Custom',
|
|
605
|
+
ui: {
|
|
606
|
+
label: '我的字段',
|
|
607
|
+
props: {
|
|
608
|
+
component: MyCustomInput, // 传入组件
|
|
609
|
+
// 其他 props 会透传给 MyCustomInput
|
|
610
|
+
customProp: 'value',
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
### 完整示例
|
|
617
|
+
|
|
618
|
+
```typescript
|
|
619
|
+
{
|
|
620
|
+
name: 'paymentAccount',
|
|
621
|
+
component: 'Custom',
|
|
622
|
+
colSpan: { xs: 12 },
|
|
623
|
+
ui: {
|
|
624
|
+
label: '付款账户',
|
|
625
|
+
helperText: '请选择或输入付款账户',
|
|
626
|
+
props: {
|
|
627
|
+
children: ({ field, form, values, label, error, helperText, fieldProps }) => {
|
|
628
|
+
const [accounts] = useState([
|
|
629
|
+
{ id: '1', name: '工商银行 **** 1234' },
|
|
630
|
+
{ id: '2', name: '建设银行 **** 5678' },
|
|
631
|
+
]);
|
|
632
|
+
|
|
633
|
+
return (
|
|
634
|
+
<div>
|
|
635
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
|
636
|
+
<span style={{ fontWeight: 500 }}>{label}</span>
|
|
637
|
+
<a href="#" style={{ fontSize: 12 }}>添加账户</a>
|
|
638
|
+
</div>
|
|
639
|
+
|
|
640
|
+
<select
|
|
641
|
+
value={field.value ?? ''}
|
|
642
|
+
onChange={(e) => field.onChange(e.target.value)}
|
|
643
|
+
onBlur={field.onBlur}
|
|
644
|
+
disabled={fieldProps?.disabled}
|
|
645
|
+
style={{
|
|
646
|
+
width: '100%',
|
|
647
|
+
padding: '8px 12px',
|
|
648
|
+
border: error ? '1px solid red' : '1px solid #ddd',
|
|
649
|
+
borderRadius: 4,
|
|
650
|
+
}}
|
|
651
|
+
>
|
|
652
|
+
<option value="">请选择账户</option>
|
|
653
|
+
{accounts.map((acc) => (
|
|
654
|
+
<option key={acc.id} value={acc.id}>{acc.name}</option>
|
|
655
|
+
))}
|
|
656
|
+
</select>
|
|
657
|
+
|
|
658
|
+
{helperText && (
|
|
659
|
+
<div style={{ fontSize: 12, color: error ? 'red' : '#999', marginTop: 4 }}>
|
|
660
|
+
{helperText}
|
|
661
|
+
</div>
|
|
662
|
+
)}
|
|
663
|
+
</div>
|
|
664
|
+
);
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
rules: [{ type: 'required', message: '请选择付款账户' }],
|
|
669
|
+
}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
## 计算字段
|
|
675
|
+
|
|
676
|
+
自动计算字段值,支持简单的数学运算表达式,以及精度和舍入控制。
|
|
677
|
+
|
|
678
|
+
### 基础用法
|
|
679
|
+
|
|
680
|
+
```typescript
|
|
681
|
+
{
|
|
682
|
+
name: 'total',
|
|
683
|
+
component: 'Number',
|
|
684
|
+
readonly: true,
|
|
685
|
+
ui: {
|
|
686
|
+
label: '总价',
|
|
687
|
+
helperText: '自动计算: 单价 × 数量'
|
|
688
|
+
},
|
|
689
|
+
compute: {
|
|
690
|
+
expr: 'price * quantity', // 表达式,支持 + - * / % 等
|
|
691
|
+
dependencies: ['price', 'quantity'] // 可选,指定依赖字段(默认自动分析)
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
### 精度与舍入控制
|
|
697
|
+
|
|
698
|
+
通过 `precision` 和 `roundMode` 属性控制计算结果的精度。
|
|
699
|
+
|
|
700
|
+
* `precision`: 保留的小数位数(整数)。
|
|
701
|
+
* `roundMode`: 舍入模式,可选值:
|
|
702
|
+
* `'round'`: 四舍五入(默认)。
|
|
703
|
+
* `'ceil'`: 向上取整。
|
|
704
|
+
* `'floor'`: 向下取整。
|
|
705
|
+
|
|
706
|
+
```typescript
|
|
707
|
+
{
|
|
708
|
+
name: 'discountedPrice',
|
|
709
|
+
component: 'Number',
|
|
710
|
+
readonly: true,
|
|
711
|
+
ui: { label: '折后价(保留2位小数,向下取整)' },
|
|
712
|
+
compute: {
|
|
713
|
+
expr: 'price * discount',
|
|
714
|
+
precision: 2, // 保留两位小数
|
|
715
|
+
roundMode: 'floor' // 向下取整 (例如 10.559 -> 10.55)
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
### 舍入模式示例
|
|
721
|
+
|
|
722
|
+
假设计算结果为 `12.3456`,`precision` 为 `2`:
|
|
723
|
+
|
|
724
|
+
| roundMode | 结果 | 说明 |
|
|
725
|
+
|-----------|------|------|
|
|
726
|
+
| `round` (默认) | `12.35` | 四舍五入 |
|
|
727
|
+
| `ceil` | `12.35` | 向上取整 (12.3456 -> 12.35) |
|
|
728
|
+
| `floor` | `12.34` | 向下取整 (12.3456 -> 12.34) |
|
|
729
|
+
|
|
730
|
+
### 高级示例:互斥计算(含税/不含税)
|
|
731
|
+
|
|
732
|
+
这是一个复杂的业务场景示例:根据用户选择的"是否含税",自动计算"含税金额"或"不含税金额"。
|
|
733
|
+
|
|
734
|
+
* 如果选择"含税",用户输入"含税金额",系统自动计算"不含税金额"。
|
|
735
|
+
* 如果选择"不含税",用户输入"不含税金额",系统自动计算"含税金额"。
|
|
736
|
+
* "增值税"始终由两者差值计算得出。
|
|
737
|
+
|
|
738
|
+
```typescript
|
|
739
|
+
{
|
|
740
|
+
name: 'is_include_tax',
|
|
741
|
+
component: 'Radio',
|
|
742
|
+
ui: {
|
|
743
|
+
label: '是否含税',
|
|
744
|
+
options: [{ label: '含税', value: 1 }, { label: '不含税', value: 2 }]
|
|
745
|
+
},
|
|
746
|
+
defaultValue: 1,
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
name: 'contract_amount',
|
|
750
|
+
component: 'Number',
|
|
751
|
+
ui: { label: '合同额(含税)' },
|
|
752
|
+
// 当选择不含税(2)时,该字段禁用且由计算得出
|
|
753
|
+
disabledWhen: { field: 'is_include_tax', eq: 2 },
|
|
754
|
+
compute: {
|
|
755
|
+
// 仅当 is_include_tax === 2 (不含税) 时才执行计算
|
|
756
|
+
// 否则保持原值 (contract_amount)
|
|
757
|
+
expr: 'is_include_tax === 2 ? exclud_tax_amount * (1 + tax_rate / 100) : contract_amount',
|
|
758
|
+
dependencies: ['exclud_tax_amount', 'tax_rate', 'is_include_tax'],
|
|
759
|
+
precision: 2,
|
|
760
|
+
roundMode: 'round',
|
|
761
|
+
},
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
name: 'exclud_tax_amount',
|
|
765
|
+
component: 'Number',
|
|
766
|
+
ui: { label: '合同额(不含税)' },
|
|
767
|
+
// 当选择含税(1)时,该字段禁用且由计算得出
|
|
768
|
+
disabledWhen: { field: 'is_include_tax', eq: 1 },
|
|
769
|
+
compute: {
|
|
770
|
+
// 仅当 is_include_tax === 1 (含税) 时才执行计算
|
|
771
|
+
// 否则保持原值 (exclud_tax_amount)
|
|
772
|
+
expr: 'is_include_tax === 1 ? contract_amount / (1 + tax_rate / 100) : exclud_tax_amount',
|
|
773
|
+
dependencies: ['contract_amount', 'tax_rate', 'is_include_tax'],
|
|
774
|
+
precision: 2,
|
|
775
|
+
roundMode: 'round',
|
|
776
|
+
},
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
name: 'tax_rate',
|
|
780
|
+
component: 'Number',
|
|
781
|
+
ui: { label: '税率(%)' },
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
name: 'tax_amount',
|
|
785
|
+
component: 'Number',
|
|
786
|
+
ui: { label: '增值税额' },
|
|
787
|
+
disabled: true,
|
|
788
|
+
compute: {
|
|
789
|
+
expr: 'contract_amount - exclud_tax_amount',
|
|
790
|
+
dependencies: ['contract_amount', 'exclud_tax_amount'],
|
|
791
|
+
precision: 2,
|
|
792
|
+
},
|
|
793
|
+
}
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
798
|
+
## 远程搜索与分页 (Autocomplete)
|
|
799
|
+
|
|
800
|
+
`Autocomplete` 组件支持远程搜索、分页加载、防抖搜索以及回显。
|
|
801
|
+
|
|
802
|
+
配置 `ui.remoteConfig` 即可开启远程模式。
|
|
803
|
+
|
|
804
|
+
```typescript
|
|
805
|
+
{
|
|
806
|
+
name: 'userId',
|
|
807
|
+
component: 'Autocomplete',
|
|
808
|
+
ui: {
|
|
809
|
+
label: '搜索用户',
|
|
810
|
+
remoteConfig: {
|
|
811
|
+
// 远程搜索函数
|
|
812
|
+
fetchOptions: async (keyword, page, pageSize) => {
|
|
813
|
+
const res = await api.searchUsers({ keyword, page, pageSize });
|
|
814
|
+
return {
|
|
815
|
+
data: res.list.map(u => ({ label: u.name, value: u.id })),
|
|
816
|
+
total: res.total,
|
|
817
|
+
hasMore: res.hasMore
|
|
818
|
+
};
|
|
819
|
+
},
|
|
820
|
+
// 回显函数(用于处理默认值不在当前列表中的情况)
|
|
821
|
+
fetchById: async (value) => {
|
|
822
|
+
const user = await api.getUserById(value);
|
|
823
|
+
return user ? { label: user.name, value: user.id } : null;
|
|
824
|
+
},
|
|
825
|
+
// 加载状态回调(可选,用于外部显示 loading)
|
|
826
|
+
onLoadingChange: (loading) => {
|
|
827
|
+
console.log('Loading:', loading);
|
|
828
|
+
},
|
|
829
|
+
pageSize: 20, // 每页条数 (默认 20)
|
|
830
|
+
debounceTimeout: 800 // 防抖时间 (默认 500ms)
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
### RemoteConfig 类型定义
|
|
837
|
+
|
|
838
|
+
```typescript
|
|
839
|
+
type RemoteConfig = {
|
|
840
|
+
/**
|
|
841
|
+
* 远程获取选项列表
|
|
842
|
+
* @param keyword 搜索关键词
|
|
843
|
+
* @param page 当前页码 (从1开始)
|
|
844
|
+
* @param pageSize 每页条数
|
|
845
|
+
*/
|
|
846
|
+
fetchOptions: (
|
|
847
|
+
keyword: string,
|
|
848
|
+
page: number,
|
|
849
|
+
pageSize: number
|
|
850
|
+
) => Promise<{
|
|
851
|
+
data: OptionItem[];
|
|
852
|
+
total: number;
|
|
853
|
+
hasMore: boolean;
|
|
854
|
+
}>;
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* 根据 ID 获取单个选项(用于回显)
|
|
858
|
+
* 当 field.value 有值但 options 中没有对应项时触发
|
|
859
|
+
*/
|
|
860
|
+
fetchById?: (value: string | number) => Promise<OptionItem | null>;
|
|
861
|
+
|
|
862
|
+
/** 加载状态变更回调 */
|
|
863
|
+
onLoadingChange?: (loading: boolean) => void;
|
|
864
|
+
|
|
865
|
+
/** 每页条数 (默认 20) */
|
|
866
|
+
pageSize?: number;
|
|
867
|
+
|
|
868
|
+
/** 搜索防抖时间 (ms, 默认 500) */
|
|
869
|
+
debounceTimeout?: number;
|
|
870
|
+
|
|
871
|
+
/** 最小搜索字符长度 (暂未实现) */
|
|
872
|
+
minSearchLength?: number;
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
---
|
|
876
|
+
|
|
877
|
+
## 数据转换
|
|
878
|
+
|
|
879
|
+
### transform - 提交前转换
|
|
880
|
+
|
|
881
|
+
在表单提交前对数据进行转换,例如将数组转换为字符串,或格式化日期。
|
|
882
|
+
|
|
883
|
+
`transform` 函数接收两个参数:
|
|
884
|
+
1. `value`: 当前字段的值
|
|
885
|
+
2. `values`: 当前表单的所有值 (T)
|
|
886
|
+
|
|
887
|
+
```typescript
|
|
888
|
+
{
|
|
889
|
+
name: 'tags',
|
|
890
|
+
component: 'Select',
|
|
891
|
+
ui: {
|
|
892
|
+
label: '标签',
|
|
893
|
+
options: [
|
|
894
|
+
{ label: '技术', value: 'tech' },
|
|
895
|
+
{ label: '生活', value: 'life' }
|
|
896
|
+
],
|
|
897
|
+
props: { multiple: true } // 多选
|
|
898
|
+
},
|
|
899
|
+
// 提交时将数组转换为逗号分隔字符串: ['tech', 'life'] -> "tech,life"
|
|
900
|
+
transform: (value) => Array.isArray(value) ? value.join(',') : value
|
|
901
|
+
}
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
### noSubmit - 不参与提交
|
|
905
|
+
|
|
906
|
+
用于纯 UI 展示字段,或仅用于计算的中间变量,不包含在最终提交的数据中。
|
|
907
|
+
|
|
908
|
+
```typescript
|
|
909
|
+
{
|
|
910
|
+
name: 'tips',
|
|
911
|
+
component: 'Custom',
|
|
912
|
+
noSubmit: true, // 👈 提交时会自动过滤掉此字段
|
|
913
|
+
ui: {
|
|
914
|
+
props: {
|
|
915
|
+
children: <div style={{ color: '#666' }}>请务必填写真实信息</div>
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
---
|
|
922
|
+
|
|
923
|
+
## API 参考
|
|
924
|
+
|
|
925
|
+
### SchemaFormProps
|
|
926
|
+
|
|
927
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
928
|
+
|------|------|--------|------|
|
|
929
|
+
| `schema` | `SchemaInput` | - | Schema 定义(必填) |
|
|
930
|
+
| `defaultValues` | `object` | - | 默认值 |
|
|
931
|
+
| `onSubmit` | `(values) => void` | - | 提交回调 |
|
|
932
|
+
| `onValuesChange` | `(values) => void` | - | 值变化回调 |
|
|
933
|
+
| `grid` | `boolean` | `true` | 是否使用 Grid 布局 |
|
|
934
|
+
| `spacing` | `number` | `2` | 间距 |
|
|
935
|
+
| `disabled` | `boolean` | `false` | 全局禁用 |
|
|
936
|
+
| `readOnly` | `boolean` | `false` | 全局只读 |
|
|
937
|
+
| `widgets` | `Record<string, WidgetComponent>` | - | 自定义组件 |
|
|
938
|
+
| `children` | `ReactNode` | - | 子元素(如提交按钮) |
|
|
939
|
+
|
|
940
|
+
### SchemaFormInstance (ref)
|
|
941
|
+
|
|
942
|
+
| 方法 | 说明 |
|
|
943
|
+
|------|------|
|
|
944
|
+
| `submit()` | 提交表单 |
|
|
945
|
+
| `reset()` | 重置表单 |
|
|
946
|
+
| `getValues()` | 获取所有值 |
|
|
947
|
+
| `getFormValues()` | 获取表单值(排除 noSubmit 字段) |
|
|
948
|
+
| `setValue(name, value)` | 设置单个值 |
|
|
949
|
+
| `setValues(values)` | 批量设置值 |
|
|
950
|
+
| `trigger(name?)` | 触发验证 |
|
|
951
|
+
| `clearErrors(name?)` | 清除错误 |
|
|
952
|
+
|
|
953
|
+
### FieldSchema
|
|
954
|
+
|
|
955
|
+
| 属性 | 类型 | 说明 |
|
|
956
|
+
|------|------|------|
|
|
957
|
+
| **基础配置** | | |
|
|
958
|
+
| `name` | `string` | 字段名(必填,对应表单数据 key) |
|
|
959
|
+
| `component` | `ComponentType` | 组件类型(Text, Number, Select, ...) |
|
|
960
|
+
| `defaultValue` | `any` | 默认值 |
|
|
961
|
+
| **UI 配置** | | |
|
|
962
|
+
| `ui.label` | `string` | 字段标签 |
|
|
963
|
+
| `ui.placeholder` | `string` | 占位符 |
|
|
964
|
+
| `ui.helperText` | `ReactNode` | 帮助/错误提示文本 |
|
|
965
|
+
| `ui.tooltip` | `string` | 悬浮提示信息 |
|
|
966
|
+
| `ui.options` | `OptionItem[]` | 静态选项列表 (Select/Radio/Checkbox) |
|
|
967
|
+
| `ui.optionRequest` | `(values) => Promise` | 异步加载选项函数 |
|
|
968
|
+
| `ui.remoteConfig` | `RemoteConfig` | 远程搜索配置 (Autocomplete) |
|
|
969
|
+
| `ui.props` | `object` | 透传给底层组件的 props |
|
|
970
|
+
| **布局配置** | | |
|
|
971
|
+
| `colSpan` | `GridSize \| object` | 栅格列宽 (1-12),支持响应式对象 `{ xs, md }` |
|
|
972
|
+
| `newLine` | `boolean` | 是否强制换行 |
|
|
973
|
+
| **验证与状态** | | |
|
|
974
|
+
| `rules` | `ValidationRule[]` | 验证规则数组 |
|
|
975
|
+
| `readonly` | `boolean` | 是否只读 |
|
|
976
|
+
| `disabled` | `boolean` | 是否禁用 |
|
|
977
|
+
| `hidden` | `boolean` | 是否隐藏 (不渲染但保留数据) |
|
|
978
|
+
| **条件控制** | | |
|
|
979
|
+
| `visibleWhen` | `ConditionExpression` | 条件显示表达式 |
|
|
980
|
+
| `disabledWhen` | `ConditionExpression` | 条件禁用表达式 |
|
|
981
|
+
| `requiredWhen` | `ConditionExpression` | 条件必填表达式 |
|
|
982
|
+
| **计算与数据** | | |
|
|
983
|
+
| `compute` | `ComputeConfig` | 自动计算配置 `{ expr, dependencies, precision }` |
|
|
984
|
+
| `dependencies` | `string[]` | 显式声明依赖字段 (触发重置或副作用) |
|
|
985
|
+
| `transform` | `(val, vals) => any` | 提交前的数据转换函数 |
|
|
986
|
+
| `noSubmit` | `boolean` | 是否从提交数据中排除 |
|
|
987
|
+
| **FormList/Group** | | |
|
|
988
|
+
| `columns` | `FieldSchema[]` | 子字段定义 (用于 Group/FormList) |
|
|
989
|
+
| `minItems` | `number` | 最小行数 (FormList) |
|
|
990
|
+
| `maxItems` | `number` | 最大行数 (FormList) |
|
|
991
|
+
| `addText` | `string` | 添加按钮文案 (FormList) |
|
|
992
|
+
| `sortable` | `boolean` | 是否可拖拽排序 (FormList) |
|
|
993
|
+
| `copyable` | `boolean` | 是否可复制行 (FormList) |
|
|
994
|
+
|
|
995
|
+
---
|
|
996
|
+
|
|
997
|
+
## 完整示例
|
|
998
|
+
|
|
999
|
+
```typescript
|
|
1000
|
+
import { SchemaForm, type SchemaInput } from './share/Schema';
|
|
1001
|
+
|
|
1002
|
+
const schema: SchemaInput = {
|
|
1003
|
+
fields: [
|
|
1004
|
+
// 基础字段
|
|
1005
|
+
{
|
|
1006
|
+
name: 'username',
|
|
1007
|
+
component: 'Text',
|
|
1008
|
+
ui: { label: '用户名', placeholder: '请输入用户名' },
|
|
1009
|
+
colSpan: { xs: 12, md: 6 },
|
|
1010
|
+
rules: [
|
|
1011
|
+
{ type: 'required', message: '用户名必填' },
|
|
1012
|
+
{ type: 'minLength', value: 3, message: '用户名至少3个字符' },
|
|
1013
|
+
],
|
|
1014
|
+
},
|
|
1015
|
+
|
|
1016
|
+
// 邮箱验证
|
|
1017
|
+
{
|
|
1018
|
+
name: 'email',
|
|
1019
|
+
component: 'Text',
|
|
1020
|
+
ui: { label: '邮箱', placeholder: '请输入邮箱' },
|
|
1021
|
+
colSpan: { xs: 12, md: 6 },
|
|
1022
|
+
rules: [
|
|
1023
|
+
{ type: 'required', message: '邮箱必填' },
|
|
1024
|
+
{ type: 'email', message: '请输入有效的邮箱' },
|
|
1025
|
+
],
|
|
1026
|
+
},
|
|
1027
|
+
|
|
1028
|
+
// 下拉选择 + 条件显示
|
|
1029
|
+
{
|
|
1030
|
+
name: 'accountType',
|
|
1031
|
+
component: 'Select',
|
|
1032
|
+
defaultValue: 'personal',
|
|
1033
|
+
ui: {
|
|
1034
|
+
label: '账户类型',
|
|
1035
|
+
options: [
|
|
1036
|
+
{ label: '个人账户', value: 'personal' },
|
|
1037
|
+
{ label: '企业账户', value: 'business' },
|
|
1038
|
+
],
|
|
1039
|
+
},
|
|
1040
|
+
colSpan: { xs: 12, md: 6 },
|
|
1041
|
+
},
|
|
1042
|
+
|
|
1043
|
+
// 条件显示字段
|
|
1044
|
+
{
|
|
1045
|
+
name: 'companyName',
|
|
1046
|
+
component: 'Text',
|
|
1047
|
+
ui: { label: '公司名称' },
|
|
1048
|
+
colSpan: { xs: 12, md: 6 },
|
|
1049
|
+
visibleWhen: { field: 'accountType', eq: 'business' },
|
|
1050
|
+
requiredWhen: { field: 'accountType', eq: 'business' },
|
|
1051
|
+
},
|
|
1052
|
+
|
|
1053
|
+
// 动态列表
|
|
1054
|
+
{
|
|
1055
|
+
name: 'contacts',
|
|
1056
|
+
component: 'FormList',
|
|
1057
|
+
ui: { label: '联系人' },
|
|
1058
|
+
colSpan: { xs: 12 },
|
|
1059
|
+
defaultValue: [{ name: '', phone: '' }],
|
|
1060
|
+
minItems: 1,
|
|
1061
|
+
maxItems: 3,
|
|
1062
|
+
columns: [
|
|
1063
|
+
{
|
|
1064
|
+
name: 'group',
|
|
1065
|
+
component: 'Group',
|
|
1066
|
+
colSpan: { xs: 12 },
|
|
1067
|
+
columns: [
|
|
1068
|
+
{
|
|
1069
|
+
name: 'name',
|
|
1070
|
+
component: 'Text',
|
|
1071
|
+
ui: { label: '姓名' },
|
|
1072
|
+
colSpan: { xs: 12, sm: 6 },
|
|
1073
|
+
rules: [{ type: 'required', message: '姓名必填' }],
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
name: 'phone',
|
|
1077
|
+
component: 'Text',
|
|
1078
|
+
ui: { label: '电话' },
|
|
1079
|
+
colSpan: { xs: 12, sm: 6 },
|
|
1080
|
+
rules: [{ type: 'required', message: '电话必填' }],
|
|
1081
|
+
},
|
|
1082
|
+
],
|
|
1083
|
+
},
|
|
1084
|
+
],
|
|
1085
|
+
},
|
|
1086
|
+
|
|
1087
|
+
// 计算字段
|
|
1088
|
+
{
|
|
1089
|
+
name: 'price',
|
|
1090
|
+
component: 'Number',
|
|
1091
|
+
defaultValue: 100,
|
|
1092
|
+
ui: { label: '单价' },
|
|
1093
|
+
colSpan: { xs: 12, md: 4 },
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
name: 'quantity',
|
|
1097
|
+
component: 'Number',
|
|
1098
|
+
defaultValue: 1,
|
|
1099
|
+
ui: { label: '数量' },
|
|
1100
|
+
colSpan: { xs: 12, md: 4 },
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
name: 'total',
|
|
1104
|
+
component: 'Number',
|
|
1105
|
+
readonly: true,
|
|
1106
|
+
ui: { label: '总价' },
|
|
1107
|
+
colSpan: { xs: 12, md: 4 },
|
|
1108
|
+
compute: { expr: 'price * quantity' },
|
|
1109
|
+
},
|
|
1110
|
+
|
|
1111
|
+
// 协议勾选
|
|
1112
|
+
{
|
|
1113
|
+
name: 'agree',
|
|
1114
|
+
component: 'Checkbox',
|
|
1115
|
+
ui: { label: '我已阅读并同意服务条款' },
|
|
1116
|
+
colSpan: { xs: 12 },
|
|
1117
|
+
rules: [{ type: 'required', message: '请同意服务条款' }],
|
|
1118
|
+
},
|
|
1119
|
+
],
|
|
1120
|
+
};
|
|
1121
|
+
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
|