C_Form 智能表单组件
🚀 基于 Naive UI 的超强动态表单生成器,让表单开发变得前所未有的简单
✨ 特性
- 🎯 8种布局模式 - 支持默认、行内、网格、卡片、标签页、步骤、动态、自定义等完整布局系统
- 🧩 15+种表单控件 - 内置丰富的表单控件类型,满足各种业务需求
- ⚡ 动态字段管理 - 运行时动态添加、删除、切换字段显示
- 🛡️ 强大的验证体系 - 集成封装的验证工具,支持同步和异步验证
- 🎨 灵活的插槽系统 - 支持自定义操作区、上传区等关键区域
- 📱 响应式设计 - 完美适配各种屏幕尺寸,自动布局优化
- 💪 TypeScript - 完整的类型定义和类型安全
- 🔧 可扩展架构 - 易于扩展新的控件类型和布局模式
- ⚡ 高性能渲染 - 优化的渲染机制,大表单依然流畅
📦 安装
bash
# 基于 Naive UI,确保已安装依赖
npm install naive-ui
1
2
2
🎯 快速开始
基础用法
vue
<template>
<!-- 最简单的表单 -->
<C_Form
:options="basicOptions"
@submit="handleSubmit"
/>
</template>
<script setup>
import { RULE_COMBOS } from '@/utils/v_verify'
const basicOptions = [
{
type: 'input',
prop: 'username',
label: '用户名',
placeholder: '请输入用户名',
rules: RULE_COMBOS.username('用户名')
},
{
type: 'input',
prop: 'email',
label: '邮箱',
placeholder: '请输入邮箱',
rules: RULE_COMBOS.email('邮箱')
}
]
const handleSubmit = ({ model }) => {
console.log('表单数据:', model)
}
</script>
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
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
8种布局类型演示
vue
<template>
<div class="form-demo">
<!-- 布局选择器 -->
<div class="layout-selector">
<button
v-for="layout in layoutOptions"
:key="layout.value"
:class="{ active: currentLayout === layout.value }"
@click="switchLayout(layout.value)"
>
{{ layout.label }}
</button>
</div>
<!-- 动态表单展示 -->
<C_Form
:options="currentOptions"
:layout-type="currentLayout"
:layout-config="currentLayoutConfig"
v-model="formData"
@submit="handleSubmit"
/>
</div>
</template>
<script setup>
import { PRESET_RULES, RULE_COMBOS, customRule } from '@/utils/v_verify'
const currentLayout = ref('default')
const formData = ref({})
const layoutOptions = [
{ label: '默认布局', value: 'default' },
{ label: '内联布局', value: 'inline' },
{ label: '网格布局', value: 'grid' },
{ label: '卡片布局', value: 'card' },
{ label: '标签页布局', value: 'tabs' },
{ label: '步骤布局', value: 'steps' },
{ label: '动态布局', value: 'dynamic' },
{ label: '自定义渲染', value: 'custom' },
]
const baseOptions = [
{
type: 'input',
prop: 'username',
label: '用户名',
placeholder: '请输入用户名',
rules: RULE_COMBOS.username('用户名')
},
{
type: 'input',
prop: 'email',
label: '邮箱',
placeholder: '请输入邮箱',
rules: RULE_COMBOS.email('邮箱')
},
{
type: 'select',
prop: 'gender',
label: '性别',
children: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' }
],
rules: [PRESET_RULES.required('性别')]
},
{
type: 'datePicker',
prop: 'birthday',
label: '生日',
attrs: { type: 'date' },
rules: [PRESET_RULES.required('生日')]
},
{
type: 'textarea',
prop: 'description',
label: '个人描述',
placeholder: '请简单描述一下自己',
attrs: { rows: 4 },
rules: [PRESET_RULES.length('个人描述', 10, 200)]
}
]
const currentOptions = computed(() => {
// 根据不同布局返回对应的字段配置
return baseOptions
})
const currentLayoutConfig = computed(() => {
const configs = {
grid: { cols: 2, gap: 16 },
card: {
groups: [
{ key: 'basic', title: '基础信息' },
{ key: 'contact', title: '联系方式' }
]
},
tabs: {
tabs: [
{ key: 'personal', title: '个人信息' },
{ key: 'contact', title: '联系方式' }
]
}
}
return configs[currentLayout.value] || {}
})
const switchLayout = (layout) => {
currentLayout.value = layout
message.info(`已切换到${layoutOptions.find(opt => opt.value === layout)?.label}`)
}
const handleSubmit = ({ model }) => {
console.log('表单数据:', model)
message.success('表单提交成功')
}
</script>
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
107
108
109
110
111
112
113
114
115
116
117
118
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
107
108
109
110
111
112
113
114
115
116
117
118
📖 API 文档
Props
参数 | 类型 | 默认值 | 说明 |
---|---|---|---|
options | FormOption[] | [] | 表单选项配置数组 |
layoutType | LayoutType | 'default' | 布局类型 |
layoutConfig | LayoutConfig | {} | 布局配置 |
modelValue | FormModel | {} | 表单数据(双向绑定) |
validateOnValueChange | boolean | false | 值变化时是否验证 |
labelPlacement | 'left' | 'top' | 'left' | 标签位置 |
labelWidth | string | number | 'auto' | 标签宽度 |
size | 'small' | 'medium' | 'large' | 'medium' | 表单尺寸 |
disabled | boolean | false | 是否禁用 |
readonly | boolean | false | 是否只读 |
showDefaultActions | boolean | true | 是否显示默认操作按钮 |
Events
事件名 | 参数 | 说明 |
---|---|---|
submit | (payload: SubmitEventPayload) | 表单提交事件 |
update:modelValue | (model: FormModel) | 表单数据更新事件 |
validate-success | (model: FormModel) | 验证成功事件 |
validate-error | (errors: unknown) | 验证失败事件 |
fields-change | (fields: FormOption[]) | 字段变化事件 |
tab-change | (tabKey: string) | 标签页切换事件 |
step-change | (stepIndex: number, stepKey: string) | 步骤切换事件 |
field-add | (fieldConfig: DynamicFieldConfig) | 动态字段添加事件 |
field-remove | (fieldId: string) | 动态字段删除事件 |
Slots
插槽名 | 参数 | 说明 |
---|---|---|
action | { form, model, validate, reset, ... } | 自定义操作按钮区域 |
uploadClick | {} | 自定义上传触发器 |
uploadTip | {} | 自定义上传提示信息 |
类型定义
表单选项接口
typescript
interface FormOption {
type: ComponentType
prop: string
label: string
placeholder?: string
value?: any
rules?: FormItemRule[]
attrs?: Record<string, any>
children?: OptionItem[]
show?: boolean
layout?: FieldLayoutConfig
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
布局类型
typescript
type LayoutType =
| 'default' // 默认布局
| 'inline' // 行内布局
| 'grid' // 网格布局
| 'card' // 卡片布局
| 'tabs' // 标签页布局
| 'steps' // 步骤布局
| 'dynamic' // 动态布局
| 'custom' // 自定义布局
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
组件类型
typescript
type ComponentType =
| 'input' | 'textarea' | 'inputNumber'
| 'select' | 'checkbox' | 'radio'
| 'datePicker' | 'daterange' | 'timePicker'
| 'cascader' | 'colorPicker' | 'switch'
| 'slider' | 'rate' | 'upload' | 'editor'
1
2
3
4
5
6
2
3
4
5
6
🎨 使用示例
场景 1: 用户注册表单(使用验证规则组合)
vue
<template>
<div class="user-registration">
<n-card title="用户注册" style="max-width: 600px; margin: 0 auto;">
<C_Form
ref="registerFormRef"
:options="registerOptions"
layout-type="card"
:layout-config="cardLayoutConfig"
@submit="handleRegister"
@validate-error="handleValidateError"
>
<template #action="{ validate, reset }">
<n-space>
<n-button
type="primary"
size="large"
:loading="registering"
v-debounce="{ delay: 300, immediate: false }"
@click="validate"
>
注册
</n-button>
<n-button size="large" @click="reset">重置</n-button>
</n-space>
</template>
</C_Form>
</n-card>
</div>
</template>
<script setup>
import { RULE_COMBOS, PRESET_RULES, customRule } from '@/utils/v_verify'
const registerFormRef = ref()
const registering = ref(false)
const cardLayoutConfig = {
type: 'card',
groups: [
{ key: 'basic', title: '基础信息' },
{ key: 'contact', title: '联系方式' },
{ key: 'security', title: '安全设置' }
]
}
const registerOptions = [
{
type: 'input',
prop: 'username',
label: '用户名',
placeholder: '请输入用户名(3-20位字符)',
layout: { group: 'basic' },
rules: RULE_COMBOS.username('用户名')
},
{
type: 'input',
prop: 'realName',
label: '真实姓名',
placeholder: '请输入真实姓名',
layout: { group: 'basic' },
rules: [
PRESET_RULES.required('真实姓名'),
PRESET_RULES.length('真实姓名', 2, 20)
]
},
{
type: 'input',
prop: 'email',
label: '邮箱',
placeholder: '请输入邮箱地址',
layout: { group: 'contact' },
rules: RULE_COMBOS.email('邮箱')
},
{
type: 'input',
prop: 'phone',
label: '手机号',
placeholder: '请输入手机号',
layout: { group: 'contact' },
rules: RULE_COMBOS.mobile('手机号')
},
{
type: 'input',
prop: 'password',
label: '密码',
placeholder: '请输入密码(6-20位)',
layout: { group: 'security' },
attrs: { type: 'password', showPasswordOn: 'click' },
rules: RULE_COMBOS.password('密码')
},
{
type: 'input',
prop: 'confirmPassword',
label: '确认密码',
placeholder: '请再次输入密码',
layout: { group: 'security' },
attrs: { type: 'password' },
rules: RULE_COMBOS.confirmPassword('确认密码', () => registerFormRef.value?.getFieldValue('password'))
},
{
type: 'select',
prop: 'gender',
label: '性别',
layout: { group: 'basic' },
children: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' },
{ label: '保密', value: 'secret' }
],
rules: [PRESET_RULES.required('性别')]
},
{
type: 'datePicker',
prop: 'birthday',
label: '生日',
layout: { group: 'basic' },
attrs: { type: 'date' },
rules: [PRESET_RULES.required('生日')]
},
{
type: 'checkbox',
prop: 'agreements',
label: '协议',
layout: { group: 'security' },
children: [
{ label: '我已阅读并同意《用户协议》', value: 'user_agreement' },
{ label: '我已阅读并同意《隐私政策》', value: 'privacy_policy' }
],
rules: [
customRule(
(value) => Array.isArray(value) && value.length === 2,
'请同意所有相关协议'
)
]
}
]
const handleRegister = async ({ model }) => {
registering.value = true
try {
// 模拟注册API调用
await new Promise(resolve => setTimeout(resolve, 2000))
console.log('注册数据:', model)
message.success('注册成功!')
} catch (error) {
message.error('注册失败,请重试')
} finally {
registering.value = false
}
}
const handleValidateError = (errors) => {
console.log('表单验证失败:', errors)
message.error('请检查表单填写是否正确')
}
</script>
<style scoped>
.user-registration {
padding: 40px 20px;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
}
</style>
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
场景 2: 多步骤表单(项目申请流程)
vue
<template>
<div class="multi-step-form">
<n-card title="项目申请流程">
<C_Form
ref="stepFormRef"
:options="stepOptions"
layout-type="steps"
:layout-config="stepLayoutConfig"
@step-change="handleStepChange"
@step-validate="handleStepValidate"
@submit="handleFinalSubmit"
/>
</n-card>
</div>
</template>
<script setup>
import { PRESET_RULES, RULE_COMBOS, customRule, customAsyncRule } from '@/utils/v_verify'
const stepFormRef = ref()
const currentStep = ref(0)
const stepLayoutConfig = {
type: 'steps',
steps: {
steps: [
{ key: 'basic', title: '基本信息', description: '填写项目基本信息' },
{ key: 'detail', title: '详细信息', description: '填写项目详细描述' },
{ key: 'team', title: '团队信息', description: '填写团队成员信息' },
{ key: 'confirm', title: '确认提交', description: '确认所有信息无误' }
],
current: currentStep,
allowJump: false
}
}
const stepOptions = [
// 第一步:基本信息
{
type: 'input',
prop: 'projectName',
label: '项目名称',
placeholder: '请输入项目名称',
layout: { step: 'basic' },
rules: [
PRESET_RULES.required('项目名称'),
PRESET_RULES.length('项目名称', 3, 50)
]
},
{
type: 'select',
prop: 'projectType',
label: '项目类型',
layout: { step: 'basic' },
children: [
{ label: 'Web应用', value: 'web' },
{ label: '移动应用', value: 'mobile' },
{ label: '桌面应用', value: 'desktop' },
{ label: '其他', value: 'other' }
],
rules: [PRESET_RULES.required('项目类型')]
},
{
type: 'daterange',
prop: 'projectDuration',
label: '项目周期',
layout: { step: 'basic' },
attrs: { type: 'daterange' },
rules: [PRESET_RULES.required('项目周期')]
},
// 第二步:详细信息
{
type: 'textarea',
prop: 'projectDescription',
label: '项目描述',
placeholder: '请详细描述项目内容、目标和特色',
layout: { step: 'detail' },
attrs: { rows: 6 },
rules: [
PRESET_RULES.required('项目描述'),
PRESET_RULES.length('项目描述', 50, 1000)
]
},
{
type: 'checkbox',
prop: 'technologies',
label: '技术栈',
layout: { step: 'detail' },
children: [
{ label: 'Vue.js', value: 'vue' },
{ label: 'React', value: 'react' },
{ label: 'Angular', value: 'angular' },
{ label: 'Node.js', value: 'nodejs' },
{ label: 'Python', value: 'python' },
{ label: 'Java', value: 'java' }
],
rules: [
customRule(
(value) => Array.isArray(value) && value.length > 0,
'请选择至少一种技术栈'
)
]
},
{
type: 'upload',
prop: 'projectFiles',
label: '项目文档',
layout: { step: 'detail' },
attrs: {
accept: '.pdf,.doc,.docx',
max: 5,
listType: 'text'
}
},
// 第三步:团队信息
{
type: 'inputNumber',
prop: 'teamSize',
label: '团队规模',
layout: { step: 'team' },
attrs: { min: 1, max: 50 },
rules: [
PRESET_RULES.required('团队规模'),
PRESET_RULES.range('团队规模', 1, 50)
]
},
{
type: 'textarea',
prop: 'teamDescription',
label: '团队介绍',
placeholder: '请介绍团队成员背景和分工',
layout: { step: 'team' },
attrs: { rows: 4 },
rules: [
PRESET_RULES.required('团队介绍'),
PRESET_RULES.length('团队介绍', 20, 500)
]
},
{
type: 'input',
prop: 'contactPerson',
label: '联系人',
layout: { step: 'team' },
rules: [
PRESET_RULES.required('联系人'),
PRESET_RULES.length('联系人', 2, 20)
]
},
{
type: 'input',
prop: 'contactPhone',
label: '联系电话',
layout: { step: 'team' },
rules: RULE_COMBOS.mobile('联系电话')
}
]
const handleStepChange = (stepIndex, stepKey) => {
currentStep.value = stepIndex
console.log(`切换到步骤 ${stepIndex}: ${stepKey}`)
}
const handleStepValidate = async (stepIndex) => {
console.log(`验证步骤 ${stepIndex}`)
// 可以在这里添加自定义验证逻辑
return true
}
const handleFinalSubmit = async ({ model }) => {
console.log('最终提交数据:', model)
try {
// 模拟提交API调用
await new Promise(resolve => setTimeout(resolve, 2000))
message.success('项目申请提交成功!')
} catch (error) {
message.error('提交失败,请重试')
}
}
</script>
<style scoped>
.multi-step-form {
max-width: 800px;
margin: 0 auto;
padding: 24px;
}
</style>
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
场景 3: 动态表单配置器(参考演示页面结构)
vue
<template>
<div class="form-demo">
<n-h1>表单组件场景示例 - 展示所有8种布局类型的完整功能</n-h1>
<!-- 控制面板 -->
<div class="control-panel">
<div class="panel-title">
布局控制中心 <span class="subtitle">/ 实时配置表单布局和行为</span>
</div>
<div class="control-grid">
<!-- 布局选择器 -->
<n-card hoverable class="control-card" :bordered="false">
<div class="card-title">布局类型</div>
<div class="layout-buttons">
<button
v-for="layout in layoutOptions"
:key="layout.value"
:class="['layout-btn', { active: currentLayout === layout.value }]"
@click="switchLayout(layout.value)"
>
{{ layout.label }}
</button>
</div>
</n-card>
<!-- 配置面板 -->
<n-card hoverable class="control-card" :bordered="false">
<div class="card-title">表单配置</div>
<div class="config-section">
<div class="config-item">
<span>标签位置</span>
<div class="button-group">
<button
v-for="item in labelPlacements"
:key="item.value"
:class="{ active: labelPlacement === item.value }"
@click="labelPlacement = item.value"
>
{{ item.label }}
</button>
</div>
</div>
<div class="config-item">
<span>实时验证</span>
<div
:class="['switch', { active: validateOnChange }]"
@click="validateOnChange = !validateOnChange"
/>
</div>
</div>
<div class="action-buttons">
<button
v-for="action in formActions"
:key="action.key"
:class="['action-btn', action.type]"
v-debounce="{ delay: 300, immediate: false }"
@click="handleAction(action.key)"
>
{{ action.label }}
</button>
</div>
</n-card>
<!-- 统计面板 -->
<n-card hoverable class="control-card" :bordered="false">
<div class="card-title">实时统计</div>
<div class="stat-display">
<div class="stat-number">{{ formStats.totalFields }}</div>
<div class="stat-label">当前布局包含的字段总数</div>
</div>
</n-card>
</div>
</div>
<!-- 表单展示 -->
<n-card class="form-section" :bordered="false">
<div class="form-header">
<h3>{{ currentLayoutInfo.title }} - 演示</h3>
<span class="field-badge">{{ formStats.totalFields }} 字段</span>
</div>
<div class="layout-info">
<strong>{{ currentLayoutInfo.title }}</strong> -
{{ currentLayoutInfo.content }}
</div>
<C_Form
ref="formRef"
:options="currentOptions"
:layout-type="currentLayout"
:layout-config="currentLayoutConfig"
v-model="formData"
:label-placement="labelPlacement"
:validate-on-value-change="validateOnChange"
@submit="handleSubmit"
@validate-success="errorCount = 0"
@validate-error="handleValidateError"
@fields-change="currentFields = $event || []"
/>
</n-card>
<!-- 状态卡片 -->
<div class="status-section">
<div class="panel-title">状态监控面板</div>
<div class="status-cards">
<n-card
v-for="(card, index) in statusCards"
:key="index"
:class="['status-card', card.type]"
:bordered="false"
>
<div class="number">{{ card.value }}</div>
<div class="label">{{ card.label }}</div>
</n-card>
</div>
</div>
</div>
</template>
<script setup>
import { PRESET_RULES, RULE_COMBOS, customRule } from '@/utils/v_verify'
const formRef = ref()
const formData = ref({})
const currentLayout = ref('default')
const labelPlacement = ref('left')
const validateOnChange = ref(false)
const currentFields = ref([])
const errorCount = ref(0)
const layoutOptions = [
{ label: '默认布局', value: 'default' },
{ label: '内联布局', value: 'inline' },
{ label: '网格布局', value: 'grid' },
{ label: '卡片布局', value: 'card' },
{ label: '标签页布局', value: 'tabs' },
{ label: '步骤布局', value: 'steps' },
{ label: '动态布局', value: 'dynamic' },
{ label: '自定义渲染', value: 'custom' },
]
const labelPlacements = [
{ value: 'left', label: '左侧' },
{ value: 'top', label: '顶部' },
]
const formActions = [
{ key: 'fill', type: 'fill', label: '填充测试' },
{ key: 'preview', type: 'preview', label: '预览数据' },
{ key: 'clear', type: 'clear', label: '清空数据' },
{ key: 'validate', type: 'validate', label: '验证表单' },
]
// 基础表单选项(使用验证规则)
const baseOptions = [
{
type: 'input',
prop: 'username',
label: '用户名',
placeholder: '请输入用户名',
rules: RULE_COMBOS.username('用户名')
},
{
type: 'input',
prop: 'realName',
label: '真实姓名',
placeholder: '请输入真实姓名',
rules: [
PRESET_RULES.required('真实姓名'),
PRESET_RULES.length('真实姓名', 2, 20)
]
},
{
type: 'input',
prop: 'email',
label: '邮箱',
placeholder: '请输入邮箱',
rules: RULE_COMBOS.email('邮箱')
},
{
type: 'input',
prop: 'phone',
label: '手机号',
placeholder: '请输入手机号',
rules: RULE_COMBOS.mobile('手机号')
},
{
type: 'inputNumber',
prop: 'age',
label: '年龄',
attrs: { min: 1, max: 120 },
rules: [
PRESET_RULES.required('年龄'),
PRESET_RULES.range('年龄', 1, 120)
]
},
{
type: 'select',
prop: 'gender',
label: '性别',
children: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' }
],
rules: [PRESET_RULES.required('性别')]
},
{
type: 'textarea',
prop: 'description',
label: '个人描述',
placeholder: '请简单描述一下自己',
attrs: { rows: 4 },
rules: [PRESET_RULES.length('个人描述', 10, 200)]
},
{
type: 'checkbox',
prop: 'hobbies',
label: '爱好',
children: [
{ label: '阅读', value: 'reading' },
{ label: '运动', value: 'sports' },
{ label: '音乐', value: 'music' },
{ label: '旅行', value: 'travel' }
]
},
{
type: 'switch',
prop: 'newsletter',
label: '订阅新闻'
}
]
// 测试数据配置
const testDataConfig = {
getTestData(layoutType) {
const baseData = {
username: 'cheny_888',
realName: 'CHENY',
age: 28,
gender: 'male',
email: 'demo@cheny-test.com',
phone: '16888888888',
description: '这是一个测试用户的个人描述'
}
const extendedData = {
hobbies: ['reading', 'music'],
newsletter: true,
}
// 根据布局类型返回不同的数据
const needsExtended = ['card', 'tabs', 'steps', 'dynamic', 'custom']
if (needsExtended.includes(layoutType)) {
return { ...baseData, ...extendedData }
}
return baseData
}
}
const currentOptions = computed(() => {
// 根据布局类型过滤和调整选项
return baseOptions
})
const currentLayoutConfig = computed(() => {
const configs = {
grid: { cols: 2, gap: 16 },
card: {
groups: [
{ key: 'basic', title: '基础信息' },
{ key: 'contact', title: '联系方式' }
]
},
tabs: {
tabs: [
{ key: 'personal', title: '个人信息' },
{ key: 'contact', title: '联系方式' }
]
}
}
return configs[currentLayout.value] || {}
})
const currentLayoutInfo = computed(() => {
const descriptions = {
default: { title: '默认布局', content: '标准的垂直表单布局,适用于大多数场景' },
inline: { title: '内联布局', content: '水平排列的表单布局,适用于简单表单' },
grid: { title: '网格布局', content: '基于栅格系统的响应式布局' },
card: { title: '卡片布局', content: '将表单项按功能分组显示' },
tabs: { title: '标签页布局', content: '将表单项分散到不同标签页' },
steps: { title: '步骤布局', content: '引导用户按步骤填写表单' },
dynamic: { title: '动态布局', content: '支持动态添加删除字段' },
custom: { title: '自定义渲染', content: '支持自定义渲染效果' }
}
return descriptions[currentLayout.value] || { title: '', content: '' }
})
const formStats = computed(() => {
const totalFields = currentFields.value.length
const filledCount = currentFields.value.filter(field =>
isValueFilled(formData.value[field.prop])
).length
const pendingCount = Math.max(0, totalFields - filledCount)
const completionPercentage =
totalFields === 0 ? 0 : Math.round((filledCount / totalFields) * 100)
return {
totalFields,
filledCount,
pendingCount,
completionPercentage,
}
})
const statusCards = computed(() => [
{
value: formStats.value.filledCount,
label: '已填写字段',
type: 'completed',
},
{
value: formStats.value.pendingCount,
label: '待填写字段',
type: 'pending',
},
{
value: `${formStats.value.completionPercentage}%`,
label: '完成率',
type: 'completion',
},
{ value: errorCount.value, label: '验证错误', type: 'errors' },
])
const isValueFilled = (value) => {
if (value === null || value === undefined || value === '') return false
if (typeof value === 'string') return value.trim() !== ''
if (Array.isArray(value)) return value.length > 0
if (typeof value === 'number') return value > 0
if (typeof value === 'boolean') return value === true
return false
}
const switchLayout = (layout) => {
currentLayout.value = layout
resetForm()
const layoutName = layoutOptions.find(opt => opt.value === layout)?.label || '未知'
message.info(`已切换到${layoutName}`)
}
const resetForm = () => {
formData.value = {}
errorCount.value = 0
currentFields.value = []
}
const handleAction = (actionKey) => {
const actions = {
fill: () => {
const testData = testDataConfig.getTestData(currentLayout.value)
Object.assign(formData.value, testData)
message.success('已填充测试数据')
},
preview: () => {
console.log('预览数据:', formData.value)
message.success('数据已输出到控制台')
},
clear: () => {
resetForm()
formRef.value?.resetFields?.()
message.info('已清空所有数据')
},
validate: async () => {
try {
if (!formRef.value?.validate) {
message.warning('当前布局不支持验证功能')
return
}
await formRef.value.validate()
errorCount.value = 0
message.success('表单验证通过')
} catch (errors) {
errorCount.value = Array.isArray(errors) ? errors.length : 1
message.error('表单验证失败')
console.error('验证错误:', errors)
}
},
}
actions[actionKey]?.()
}
const handleSubmit = ({ model }) => {
console.log('表单提交:', model)
message.success('表单提交成功')
}
const handleValidateError = (errors) => {
errorCount.value = Array.isArray(errors) ? errors.length : 1
console.error('表单验证失败:', errors)
}
</script>
<style scoped>
.form-demo {
padding: 24px;
}
.control-panel {
margin-bottom: 24px;
}
.panel-title {
font-size: 20px;
font-weight: bold;
margin-bottom: 16px;
}
.subtitle {
font-size: 14px;
color: #666;
font-weight: normal;
}
.control-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
}
.control-card {
padding: 16px;
}
.card-title {
font-weight: bold;
margin-bottom: 12px;
}
.layout-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.layout-btn {
padding: 6px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
background: white;
cursor: pointer;
transition: all 0.3s;
}
.layout-btn.active {
background: #1890ff;
color: white;
border-color: #1890ff;
}
.config-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.config-item {
display: flex;
justify-content: space-between;
align-items: center;
}
.button-group {
display: flex;
gap: 4px;
}
.button-group button {
padding: 4px 8px;
border: 1px solid #d9d9d9;
background: white;
cursor: pointer;
font-size: 12px;
}
.button-group button.active {
background: #1890ff;
color: white;
border-color: #1890ff;
}
.switch {
width: 40px;
height: 20px;
border-radius: 10px;
background: #ccc;
cursor: pointer;
transition: all 0.3s;
position: relative;
}
.switch::after {
content: '';
width: 16px;
height: 16px;
border-radius: 50%;
background: white;
position: absolute;
top: 2px;
left: 2px;
transition: all 0.3s;
}
.switch.active {
background: #1890ff;
}
.switch.active::after {
left: 22px;
}
.action-buttons {
display: flex;
gap: 8px;
margin-top: 12px;
}
.action-btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.3s;
}
.action-btn.fill {
background: #52c41a;
color: white;
}
.action-btn.preview {
background: #1890ff;
color: white;
}
.action-btn.clear {
background: #ff4d4f;
color: white;
}
.action-btn.validate {
background: #faad14;
color: white;
}
.stat-display {
text-align: center;
}
.stat-number {
font-size: 36px;
font-weight: bold;
color: #1890ff;
}
.stat-label {
font-size: 12px;
color: #666;
}
.form-section {
margin-bottom: 24px;
}
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.field-badge {
background: #f0f0f0;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
.layout-info {
margin-bottom: 16px;
padding: 12px;
background: #f9f9f9;
border-radius: 4px;
font-size: 14px;
}
.status-section {
margin-top: 24px;
}
.status-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
}
.status-card {
text-align: center;
padding: 16px;
}
.status-card .number {
font-size: 24px;
font-weight: bold;
}
.status-card .label {
font-size: 12px;
color: #666;
margin-top: 4px;
}
.status-card.completed .number {
color: #52c41a;
}
.status-card.pending .number {
color: #faad14;
}
.status-card.completion .number {
color: #1890ff;
}
.status-card.errors .number {
color: #ff4d4f;
}
</style>
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
🛠️ 高级用法
使用封装的验证工具
vue
<template>
<C_Form
:options="advancedOptions"
@submit="handleSubmit"
/>
</template>
<script setup>
import { PRESET_RULES, RULE_COMBOS, customRule, customAsyncRule } from '@/utils/v_verify'
const advancedOptions = [
{
type: 'input',
prop: 'username',
label: '用户名',
placeholder: '请输入用户名',
rules: [
...RULE_COMBOS.username('用户名'),
// 添加异步验证检查用户名是否已存在
customAsyncRule(
async (value) => {
if (!value) return true
const exists = await checkUsernameExists(value)
return !exists
},
'用户名已存在,请换一个',
'blur'
)
]
},
{
type: 'input',
prop: 'password',
label: '密码',
attrs: { type: 'password' },
rules: RULE_COMBOS.password('密码')
},
{
type: 'input',
prop: 'confirmPassword',
label: '确认密码',
attrs: { type: 'password' },
rules: RULE_COMBOS.confirmPassword('确认密码', () => formRef.value?.getFieldValue('password'))
},
{
type: 'input',
prop: 'email',
label: '邮箱',
rules: RULE_COMBOS.email('邮箱')
},
{
type: 'input',
prop: 'phone',
label: '手机号',
rules: RULE_COMBOS.mobile('手机号')
},
{
type: 'inputNumber',
prop: 'age',
label: '年龄',
rules: [
PRESET_RULES.required('年龄'),
PRESET_RULES.range('年龄', 1, 120)
]
},
{
type: 'input',
prop: 'website',
label: '个人网站',
rules: [PRESET_RULES.url('个人网站')]
},
{
type: 'textarea',
prop: 'bio',
label: '个人简介',
rules: [
PRESET_RULES.required('个人简介'),
PRESET_RULES.length('个人简介', 10, 500)
]
}
]
const formRef = ref()
const checkUsernameExists = async (username) => {
// 模拟异步检查用户名是否存在
await new Promise(resolve => setTimeout(resolve, 500))
return ['admin', 'test', 'user', 'root'].includes(username.toLowerCase())
}
const handleSubmit = ({ model }) => {
console.log('验证通过,提交数据:', model)
message.success('表单提交成功')
}
</script>
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
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
条件显示和字段联动
vue
<template>
<C_Form
:options="conditionalOptions"
v-model="formData"
@submit="handleSubmit"
/>
</template>
<script setup>
import { PRESET_RULES, RULE_COMBOS } from '@/utils/v_verify'
const formData = ref({})
const conditionalOptions = computed(() => [
{
type: 'select',
prop: 'userType',
label: '用户类型',
children: [
{ label: '个人用户', value: 'personal' },
{ label: '企业用户', value: 'business' }
],
rules: [PRESET_RULES.required('用户类型')]
},
// 个人用户字段
{
type: 'input',
prop: 'personalName',
label: '真实姓名',
show: formData.value.userType === 'personal',
rules: formData.value.userType === 'personal' ? [
PRESET_RULES.required('真实姓名'),
PRESET_RULES.length('真实姓名', 2, 20)
] : []
},
{
type: 'input',
prop: 'idCard',
label: '身份证号',
show: formData.value.userType === 'personal',
rules: formData.value.userType === 'personal' ? [
PRESET_RULES.required('身份证号'),
PRESET_RULES.idCard('身份证号')
] : []
},
// 企业用户字段
{
type: 'input',
prop: 'companyName',
label: '公司名称',
show: formData.value.userType === 'business',
rules: formData.value.userType === 'business' ? [
PRESET_RULES.required('公司名称'),
PRESET_RULES.length('公司名称', 2, 50)
] : []
},
{
type: 'input',
prop: 'businessLicense',
label: '营业执照号',
show: formData.value.userType === 'business',
rules: formData.value.userType === 'business' ? [
PRESET_RULES.required('营业执照号'),
PRESET_RULES.length('营业执照号', 10, 30)
] : []
},
// 通用字段
{
type: 'input',
prop: 'email',
label: '邮箱',
rules: RULE_COMBOS.email('邮箱')
},
{
type: 'input',
prop: 'phone',
label: '联系电话',
rules: RULE_COMBOS.mobile('联系电话')
}
])
// 当用户类型改变时,清空相关字段
watch(() => formData.value.userType, (newType, oldType) => {
if (oldType === 'personal') {
delete formData.value.personalName
delete formData.value.idCard
} else if (oldType === 'business') {
delete formData.value.companyName
delete formData.value.businessLicense
}
})
const handleSubmit = ({ model }) => {
console.log('表单数据:', model)
message.success('表单提交成功')
}
</script>
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
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
防抖优化和性能提升
vue
<template>
<C_Form
:options="performanceOptions"
v-model="formData"
@submit="handleSubmit"
>
<!-- 使用防抖指令优化提交按钮 -->
<template #action="{ validate, reset }">
<n-space>
<n-button
type="primary"
size="large"
v-debounce="{ delay: 500, immediate: false, onExecute: handleDebounceExecute }"
@click="validate"
>
提交表单
</n-button>
<n-button size="large" @click="reset">重置</n-button>
</n-space>
</template>
</C_Form>
</template>
<script setup>
import { PRESET_RULES, RULE_COMBOS } from '@/utils/v_verify'
const formData = ref({})
// 使用 shallowRef 优化大型选项数据
const departmentOptions = shallowRef([
{ label: '技术部', value: 'tech' },
{ label: '产品部', value: 'product' },
{ label: '设计部', value: 'design' },
{ label: '运营部', value: 'operation' },
// ... 更多选项
])
const performanceOptions = [
{
type: 'input',
prop: 'username',
label: '用户名',
rules: RULE_COMBOS.username('用户名')
},
{
type: 'select',
prop: 'department',
label: '部门',
children: departmentOptions.value,
rules: [PRESET_RULES.required('部门')]
},
{
type: 'textarea',
prop: 'description',
label: '描述',
attrs: { rows: 4 },
rules: [PRESET_RULES.length('描述', 10, 500)]
}
]
const handleDebounceExecute = () => {
console.log('防抖执行中...')
}
const handleSubmit = ({ model }) => {
console.log('表单提交:', model)
message.success('表单提交成功')
}
</script>
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
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
🎨 自定义样式
CSS 变量定制
scss
.c-form-wrapper {
--form-primary-color: #1890ff;
--form-border-color: #d9d9d9;
--form-hover-color: #40a9ff;
--form-error-color: #ff4d4f;
--form-success-color: #52c41a;
--form-warning-color: #faad14;
--form-label-width: 100px;
--form-item-margin: 16px;
--form-border-radius: 6px;
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
响应式布局
vue
<template>
<C_Form
:options="responsiveOptions"
:layout-config="responsiveLayout"
class="responsive-form"
/>
</template>
<script setup>
const breakpoint = useBreakpoint()
const responsiveLayout = computed(() => ({
type: 'grid',
cols: breakpoint.value.lg ? 3 : breakpoint.value.md ? 2 : 1,
gap: 16
}))
</script>
<style scoped>
.responsive-form {
:deep(.n-form-item-label) {
@media (max-width: 768px) {
margin-bottom: 8px;
}
}
:deep(.n-form-item) {
@media (max-width: 480px) {
margin-bottom: 12px;
}
}
}
</style>
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
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
⚠️ 注意事项
1. 表单数据绑定
vue
<!-- ✅ 推荐:使用双向绑定 -->
<C_Form
v-model="formData"
:options="options"
/>
<!-- ❌ 不推荐:只监听事件 -->
<C_Form
:options="options"
@update:modelValue="handleUpdate"
/>
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2. 验证规则配置
vue
<!-- ✅ 推荐:使用封装的验证规则 -->
<script setup>
import { RULE_COMBOS, PRESET_RULES } from '@/utils/v_verify'
const options = [
{
type: 'input',
prop: 'email',
rules: RULE_COMBOS.email('邮箱') // 完整的验证规则组合
}
]
</script>
<!-- ❌ 不推荐:手写验证规则 -->
<script setup>
const options = [
{
type: 'input',
prop: 'email',
rules: [{ required: true, type: 'email' }] // 缺少错误提示
}
]
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
3. 性能优化
vue
<!-- ✅ 推荐:使用计算属性和防抖 -->
<script setup>
const computedOptions = computed(() => {
return baseOptions.map(option => ({
...option,
show: shouldShowField(option)
}))
})
// 使用防抖指令
// <n-button v-debounce="{ delay: 300 }" @click="submit">提交</n-button>
</script>
<!-- ❌ 不推荐:在模板中计算 -->
<template>
<C_Form :options="baseOptions.filter(shouldShowField)" />
</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
🐛 故障排除
常见问题
Q1: 表单验证不生效?
A1: 检查验证规则配置:
javascript
// 确保使用正确的验证规则
import { RULE_COMBOS, PRESET_RULES } from '@/utils/v_verify'
const rules = RULE_COMBOS.email('邮箱') // ✅ 正确
// 而不是
const rules = [{ required: true }] // ❌ 缺少完整验证
1
2
3
4
5
6
2
3
4
5
6
Q2: 异步验证不工作?
A2: 确保使用 customAsyncRule:
javascript
import { customAsyncRule } from '@/utils/v_verify'
const asyncRule = customAsyncRule(
async (value) => {
const result = await checkValue(value)
return result.isValid
},
'验证失败的错误信息',
'blur'
)
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Q3: 动态字段不显示?
A3: 检查字段配置:
javascript
// 确保show属性设置正确
const option = {
type: 'input',
prop: 'dynamicField',
label: '动态字段',
show: computed(() => someCondition.value) // 使用计算属性
}
1
2
3
4
5
6
7
2
3
4
5
6
7
🎯 最佳实践
1. 验证规则使用
javascript
import { RULE_COMBOS, PRESET_RULES, customRule } from '@/utils/v_verify'
// ✅ 推荐:使用预设规则组合
const goodRules = {
username: RULE_COMBOS.username('用户名'),
email: RULE_COMBOS.email('邮箱'),
phone: RULE_COMBOS.mobile('手机号'),
password: RULE_COMBOS.password('密码')
}
// ✅ 推荐:自定义验证规则
const customValidation = customRule(
(value) => value && value.includes('@company.com'),
'必须使用公司邮箱',
'blur'
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2. 错误处理
javascript
const handleValidateError = (errors) => {
// 处理验证错误
if (Array.isArray(errors) && errors.length > 0) {
const firstError = errors[0]
message.error(firstError.message || '表单验证失败')
}
// 记录详细错误信息用于调试
console.error('Form validation errors:', errors)
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
3. 表单结构设计
javascript
// ✅ 推荐:清晰的表单结构
const formOptions = [
// 基础信息组
{
type: 'input',
prop: 'name',
label: '姓名',
layout: { group: 'basic' },
rules: RULE_COMBOS.username('姓名')
},
// 联系信息组
{
type: 'input',
prop: 'email',
label: '邮箱',
layout: { group: 'contact' },
rules: RULE_COMBOS.email('邮箱')
}
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
4. 类型安全
typescript
// 定义表单数据类型
interface UserForm {
username: string
email: string
age: number
hobbies: string[]
}
// 使用类型约束
const formData = ref<UserForm>({
username: '',
email: '',
age: 0,
hobbies: []
})
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
📝 更新日志
v2.0.0 (2025-07-17)
- ✨ 集成封装的验证工具
v_verify.ts
- ✨ 支持防抖指令优化表单交互
- ✨ 新增8种完整的布局模式
- ✨ 完善的TypeScript类型定义
- 🎨 优化演示页面和文档结构
- ⚡ 提升大表单渲染性能
v1.5.0 (2025-06-15)
- 🆕 新增动态布局和自定义渲染
- 🔧 优化验证机制和错误处理
- 📱 改进移动端响应式适配
v1.0.0 (2025-06-01)
- 🎉 首次发布
- 🎨 支持8种布局类型
- 🧩 支持15+表单控件
- ✅ 完善的验证系统
🤝 贡献指南
- Fork 项目
- 创建功能分支 (
git checkout -b feature/amazing-feature
) - 提交更改 (
git commit -m 'Add amazing feature'
) - 推送到分支 (
git push origin feature/amazing-feature
) - 创建 Pull Request
📄 许可证
Copyright (c) 2025 by ChenYu, All Rights Reserved.
💡 提示: 这个表单组件设计用于快速构建各种复杂表单,支持8种布局模式和丰富的控件类型。集成了封装的验证工具 v_verify.ts
,让表单验证变得简单而强大。结合防抖指令和类型安全设计,无论是简单的登录表单还是复杂的多步骤表单,都能轻松应对。如果遇到问题请先查看文档,或者在团队群里讨论。让我们一起打造更高效的表单开发体验! 🚀