ProForm 高级表单
基于 NForm + NGrid 封装,通过 columns 驱动表单渲染,支持多种字段类型、表单验证、嵌套路径。
基础用法
基础字段
从 input、input-number、switch、checkbox-group 开始配置声明式表单。
vue
<script setup lang="ts">
import type { ProFormColumn } from '@naive-ui-pro/pro-form'
import { ProForm } from '@naive-ui-pro/pro-form'
import { ref } from 'vue'
const model = ref({ name: '', age: undefined, status: true, hobbies: [] as string[] })
const columns: ProFormColumn[] = [
{
key: 'name',
label: '姓名',
component: 'input',
componentProps: { placeholder: '请输入姓名' },
rule: { required: true, message: '请输入姓名', trigger: 'blur' },
},
{
key: 'age',
label: '年龄',
component: 'input-number',
componentProps: { min: 0, max: 150, placeholder: '请输入年龄' },
},
{
key: 'status',
label: '状态',
component: 'switch',
componentProps: { checkedText: '启用', uncheckedText: '禁用' },
},
{
key: 'hobbies',
label: '爱好',
component: 'checkbox-group',
componentProps: {
options: [
{ label: '阅读', value: 'reading' },
{ label: '运动', value: 'sports' },
{ label: '音乐', value: 'music' },
],
},
},
]
</script>
<template>
<div>
<ProForm v-model:value="model" :columns="columns" />
<div style="margin-top: 12px; padding: 12px; background: var(--vp-code-block-bg); border-radius: 8px;">
<pre style="margin: 0; font-size: 13px;">{{ JSON.stringify(model, null, 2) }}</pre>
</div>
</div>
</template>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
全部字段组件
字段组件实验台
按类别展示 ProForm 支持的全部 23 种字段组件,并实时显示模型变化。
vue
<script setup lang="ts">
import type { ProFormColumn } from '@naive-ui-pro/pro-form'
import { ProForm } from '@naive-ui-pro/pro-form'
import { NButton, NTabPane, NTabs } from 'naive-ui'
import { h, ref } from 'vue'
const model = ref<Record<string, unknown>>({
autoComplete: '',
cascader: null,
checkbox: false,
checkboxGroup: ['vue'],
color: '#18a058',
date: null,
dynamicInput: ['第一项'],
dynamicTags: ['Vue', 'TypeScript'],
input: '',
inputNumber: 18,
inputOtp: [],
mention: '',
radio: false,
radioButton: false,
radioGroup: 'frontend',
rate: 4,
select: null,
slider: 40,
switch: true,
time: null,
transfer: ['read'],
treeSelect: null,
upload: [],
})
const cityOptions = [
{ label: '北京', value: 'beijing' },
{ label: '上海', value: 'shanghai' },
{ label: '深圳', value: 'shenzhen' },
]
const treeOptions = [
{ label: '华北', key: 'north', children: [{ label: '北京', key: 'beijing' }] },
{ label: '华东', key: 'east', children: [{ label: '上海', key: 'shanghai' }] },
]
const permissionOptions = [
{ label: '查看', value: 'read' },
{ label: '编辑', value: 'write' },
{ label: '发布', value: 'publish' },
]
const basicColumns: ProFormColumn[] = [
{ key: 'input', label: 'Input', component: 'input', span: 12, componentProps: { placeholder: '请输入项目名称' } },
{ key: 'inputNumber', label: 'InputNumber', component: 'input-number', span: 12, componentProps: { min: 0, max: 100, style: { width: '100%' } } },
{ key: 'autoComplete', label: 'AutoComplete', component: 'auto-complete', span: 12, componentProps: { options: ['Vue', 'Vite', 'Vitest'].map(value => ({ label: value, value })) } },
{ key: 'select', label: 'Select', component: 'select', span: 12, componentProps: { options: cityOptions, placeholder: '请选择城市' } },
{ key: 'date', label: 'DatePicker', component: 'date-picker', span: 12, componentProps: { type: 'date', clearable: true, style: { width: '100%' } } },
{ key: 'time', label: 'TimePicker', component: 'time-picker', span: 12, componentProps: { clearable: true, style: { width: '100%' } } },
{ key: 'color', label: 'ColorPicker', component: 'color-picker', span: 12 },
{ key: 'inputOtp', label: 'InputOtp', component: 'input-otp', span: 12, componentProps: { length: 6 } },
]
const choiceColumns: ProFormColumn[] = [
{ key: 'switch', label: 'Switch', component: 'switch', span: 12, componentProps: { checkedText: '启用', uncheckedText: '停用' } },
{ key: 'checkbox', label: 'Checkbox', component: 'checkbox', span: 12, componentProps: { label: '接受服务条款' } },
{ key: 'checkboxGroup', label: 'CheckboxGroup', component: 'checkbox-group', span: 12, componentProps: { options: [{ label: 'Vue', value: 'vue' }, { label: 'React', value: 'react' }] } },
{ key: 'radioGroup', label: 'RadioGroup', component: 'radio-group', span: 12, componentProps: { optionType: 'button', buttonStyle: 'solid', options: [{ label: '前端', value: 'frontend' }, { label: '后端', value: 'backend' }] } },
{ key: 'radio', label: 'Radio', component: 'radio', span: 12, componentProps: { label: '普通单选项' } },
{ key: 'radioButton', label: 'RadioButton', component: 'radio-button', span: 12, componentProps: { label: '按钮单选项' } },
{ key: 'rate', label: 'Rate', component: 'rate', span: 12, componentProps: { allowHalf: true } },
{ key: 'slider', label: 'Slider', component: 'slider', span: 12, componentProps: { step: 10, marks: { 0: '0', 50: '50', 100: '100' } } },
]
const advancedColumns: ProFormColumn[] = [
{ key: 'cascader', label: 'Cascader', component: 'cascader', span: 12, componentProps: { options: [{ label: '华北', value: 'north', children: cityOptions.slice(0, 1) }], clearable: true } },
{ key: 'treeSelect', label: 'TreeSelect', component: 'tree-select', span: 12, componentProps: { options: treeOptions, clearable: true } },
{ key: 'dynamicTags', label: 'DynamicTags', component: 'dynamic-tags', span: 12 },
{ key: 'mention', label: 'Mention', component: 'mention', span: 12, componentProps: { options: [{ label: '张三', value: '张三' }, { label: '李四', value: '李四' }], placeholder: '输入 @ 提及成员' } },
{ key: 'dynamicInput', label: 'DynamicInput', component: 'dynamic-input', span: 24, componentProps: { placeholder: '请输入列表项' } },
{ key: 'transfer', label: 'Transfer', component: 'transfer', span: 24, componentProps: { options: permissionOptions, sourceFilterable: true } },
{
key: 'upload',
label: 'Upload',
component: 'upload',
span: 24,
componentProps: { defaultUpload: false, max: 3 },
slots: { default: () => h(NButton, null, { default: () => '选择文件' }) },
},
]
</script>
<template>
<div class="component-lab">
<NTabs type="line" animated>
<NTabPane name="basic" tab="基础输入">
<ProForm v-model:value="model" :columns="basicColumns" :x-gap="16" />
</NTabPane>
<NTabPane name="choice" tab="选择与评分">
<ProForm v-model:value="model" :columns="choiceColumns" :x-gap="16" />
</NTabPane>
<NTabPane name="advanced" tab="高级输入">
<ProForm v-model:value="model" :columns="advancedColumns" :x-gap="16" />
</NTabPane>
</NTabs>
<div class="component-lab__state">
<span>实时模型</span>
<pre><code>{{ JSON.stringify(model, null, 2) }}</code></pre>
</div>
</div>
</template>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
表单验证
表单验证
通过 rule 配置验证规则,支持正则、自定义校验器和下拉框 change 校验。
vue
<script setup lang="ts">
import type { ProFormColumn } from '@naive-ui-pro/pro-form'
import { ProForm } from '@naive-ui-pro/pro-form'
import { ref } from 'vue'
const model = ref({
username: '',
password: '',
confirmPassword: '',
phone: '',
role: null as string | null,
idCard: '',
})
const columns: ProFormColumn[] = [
{
key: 'username',
label: '用户名',
component: 'input',
componentProps: { placeholder: '4-16位字母数字' },
rule: [
{ required: true, message: '请输入用户名', trigger: 'input' },
{ min: 4, max: 16, message: '长度4-16位', trigger: 'input' },
],
span: 12,
},
{
key: 'password',
label: '密码',
component: 'input',
componentProps: { type: 'password', placeholder: '至少6位', showPasswordOn: 'click' },
rule: [
{ required: true, message: '请输入密码', trigger: 'input' },
{ min: 6, message: '至少6位', trigger: 'input' },
],
span: 12,
},
{
key: 'confirmPassword',
label: '确认密码',
component: 'input',
componentProps: { type: 'password', placeholder: '再次输入密码' },
rule: {
required: true,
validator: (_rule: any, value: string) => {
if (!value)
return new Error('请确认密码')
if (value !== model.value.password)
return new Error('两次密码不一致')
return true
},
trigger: 'blur',
},
span: 12,
},
{
key: 'phone',
label: '手机号',
component: 'input',
componentProps: { placeholder: '11位手机号' },
rule: { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'input' },
span: 12,
},
{
key: 'role',
label: '用户角色',
component: 'select',
componentProps: {
clearable: true,
placeholder: '请选择用户角色',
options: [
{ label: '管理员', value: 'admin' },
{ label: '普通用户', value: 'user' },
],
},
rule: { required: true, message: '请选择用户角色', trigger: 'change' },
span: 12,
},
{
key: 'idCard',
label: '身份证',
component: 'input',
componentProps: { placeholder: '18位身份证号' },
rule: { pattern: /^\d{17}[\dX]$/i, message: '身份证格式不正确', trigger: 'input' },
span: 12,
},
]
</script>
<template>
<ProForm v-model:value="model" :columns="columns" />
</template>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
自定义渲染
自定义渲染
使用 render 函数完全自定义字段渲染。
vue
<script setup lang="ts">
import type { ProFormColumn } from '@naive-ui-pro/pro-form'
import { ProForm } from '@naive-ui-pro/pro-form'
import { NButton, NSpace, useMessage } from 'naive-ui'
import { h, ref } from 'vue'
const model = ref({ actions: '' })
const message = useMessage()
const columns: ProFormColumn[] = [
{
key: 'name',
label: '姓名',
component: 'input',
componentProps: { placeholder: '请输入姓名' },
span: 12,
},
{
key: 'email',
label: '邮箱',
component: 'input',
componentProps: { placeholder: 'xxx@xxx.com' },
span: 12,
},
{
key: 'actions',
label: '操作',
component: 'none',
render(ctx) {
return h(NSpace, {}, {
default: () => [
h(NButton, { type: 'primary', onClick: () => message.success(JSON.stringify(ctx.model)) }, { default: () => '提交' }),
h(NButton, { onClick: () => message.info('已取消') }, { default: () => '取消' }),
],
})
},
},
]
</script>
<template>
<div>
<ProForm v-model:value="model" :columns="columns" />
<div style="margin-top: 12px; padding: 12px; background: var(--vp-code-block-bg); border-radius: 8px; font-size: 13px;">
<pre style="margin: 0;">{{ JSON.stringify(model, null, 2) }}</pre>
</div>
</div>
</template>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
嵌套路径
嵌套路径
通过 path 读写深层表单字段。
vue
<script setup lang="ts">
import type { ProFormColumn } from '@naive-ui-pro/pro-form'
import { ProForm } from '@naive-ui-pro/pro-form'
import { ref } from 'vue'
const model = ref({ user: { profile: { nickname: '' } } })
const columns: ProFormColumn[] = [
{ key: 'nickname', path: 'user.profile.nickname', label: '昵称', component: 'input' },
]
</script>
<template>
<ProForm v-model:value="model" :columns="columns" />
<pre>{{ JSON.stringify(model, null, 2) }}</pre>
</template>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
插槽覆盖
插槽覆盖
按字段 key 覆盖默认控件。
vue
<script setup lang="ts">
import type { ProFormColumn } from '@naive-ui-pro/pro-form'
import { ProForm } from '@naive-ui-pro/pro-form'
import { NInput } from 'naive-ui'
import { ref } from 'vue'
const model = ref({ name: '' })
const columns: ProFormColumn[] = [{ key: 'name', label: '姓名', component: 'input' }]
</script>
<template>
<ProForm v-model:value="model" :columns="columns">
<template #name="{ value, updateValue }">
<NInput :value="value" placeholder="插槽渲染的输入框" @update:value="updateValue" />
</template>
</ProForm>
</template>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
实例方法
调用 NForm 方法
通过 ProFormInst 调用 validate、restoreValidation 和 invalidateLabelWidth。
vue
<script setup lang="ts">
import type { ProFormColumn, ProFormInst } from '@naive-ui-pro/pro-form'
import { ProForm } from '@naive-ui-pro/pro-form'
import { NAlert, NButton, NSpace } from 'naive-ui'
import { ref } from 'vue'
const formRef = ref<ProFormInst | null>(null)
const model = ref({ name: '', email: '' })
const result = ref('点击按钮调用 NForm 实例方法')
const columns: ProFormColumn[] = [
{
key: 'name',
label: '姓名',
component: 'input',
rule: { required: true, message: '请输入姓名', trigger: ['input', 'blur'] },
span: 12,
},
{
key: 'email',
label: '邮箱',
component: 'input',
rule: { type: 'email', message: '请输入正确的邮箱', trigger: ['input', 'blur'] },
span: 12,
},
]
async function validate(): Promise<void> {
try {
await formRef.value?.validate()
result.value = '校验通过'
}
catch {
result.value = '校验未通过,请检查表单字段'
}
}
function restoreValidation(): void {
formRef.value?.restoreValidation()
result.value = '已清除校验状态'
}
function invalidateLabelWidth(): void {
formRef.value?.invalidateLabelWidth()
result.value = '已重新计算标签宽度'
}
</script>
<template>
<ProForm ref="formRef" v-model:value="model" :columns="columns" />
<NSpace style="margin-bottom: 12px;">
<NButton type="primary" @click="validate">
validate
</NButton>
<NButton @click="restoreValidation">
restoreValidation
</NButton>
<NButton @click="invalidateLabelWidth">
invalidateLabelWidth
</NButton>
</NSpace>
<NAlert type="info" :show-icon="false">
{{ result }}
</NAlert>
</template>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
API
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| columns | ProFormColumn[] | [] | 表单字段配置 |
| value | object | {} | 表单数据(v-model) |
| itemResponsive | boolean | true | 响应式布局 |
| xGap | number | 12 | 水平间距 |
继承 Naive UI NForm 和 NGrid 全部 Props。
Methods
通过组件 ref 调用,完整继承 Naive UI FormInst:
| 方法 | 说明 |
|---|---|
validate(callback?, shouldRuleBeApplied?) | 校验表单 |
restoreValidation() | 恢复表单校验状态 |
invalidateLabelWidth() | 重新计算标签宽度 |
ProFormColumn
| 属性 | 类型 | 说明 |
|---|---|---|
| key | string | 字段唯一标识 |
| path | string | 模型路径,支持嵌套 |
| label | string | 标签文本 |
| component | ProFormFieldComponent | 组件类型或自定义组件 |
| componentProps | object | 组件属性 |
| rule | FormItemRule | FormItemRule[] | 验证规则 |
| enabled | boolean | 是否启用 |
| sort | number | 排序权重 |
| span | number | 栅格占位(默认24) |
| render | (ctx) => VNodeChild | 自定义渲染 |
支持的组件类型:input、input-number、select、date-picker、time-picker、switch、checkbox、checkbox-group、radio、radio-button、radio-group、cascader、tree-select、upload、rate、slider、color-picker、dynamic-input、dynamic-tags、mention、transfer、auto-complete、input-otp