Form 表单
表单封装层 —— 三种布局、规则校验、异步 validator、命令式 validate / reset / submit、自动滚动到第一个错误。
基础用法
<CfForm> 提供布局上下文,<CfFormField> 包裹每一个表单项,统一渲染 label、必填星号、提示 / 错误文案。最简形式只是个布局封装。
背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfForm, CfFormField, CfInput, CfButton } from '@chufix-design/vue';
const name = ref('');
const email = ref('');
</script>
<template>
<CfForm layout="vertical">
<CfFormField label="姓名">
<CfInput v-model="name" placeholder="张三" />
</CfFormField>
<CfFormField label="邮箱" hint="用于登录">
<CfInput v-model="email" type="email" placeholder="[email protected]" />
</CfFormField>
<CfButton>提交</CfButton>
</CfForm>
</template>
三种布局
layout 决定 label 和控件的排列方式:
vertical(默认)—— label 在控件上方,最常见的填表样式horizontal—— label 与控件同行,配合labelWidth对齐inline—— 所有字段挤在一行,常用于搜索条 / 工具栏
背景
layout = vertical(默认)
layout = horizontal
layout = inline
<script setup lang="ts">
import { ref } from 'vue';
import { CfForm, CfFormField, CfInput, CfButton } from '@chufix-design/vue';
const a = ref('');
const b = ref('');
const c = ref('');
const d = ref('');
const e = ref('');
const f = ref('');
</script>
<template>
<div class="demo-stack">
<div>
<div style="font-size: 12px; color: var(--fg-3); margin-bottom: 4px;">layout = vertical(默认)</div>
<CfForm layout="vertical">
<CfFormField label="姓名"><CfInput v-model="a" /></CfFormField>
<CfFormField label="邮箱"><CfInput v-model="b" /></CfFormField>
</CfForm>
</div>
<div>
<div style="font-size: 12px; color: var(--fg-3); margin-bottom: 4px;">layout = horizontal</div>
<CfForm layout="horizontal" :label-width="80">
<CfFormField label="姓名"><CfInput v-model="c" /></CfFormField>
<CfFormField label="邮箱"><CfInput v-model="d" /></CfFormField>
</CfForm>
</div>
<div>
<div style="font-size: 12px; color: var(--fg-3); margin-bottom: 4px;">layout = inline</div>
<CfForm layout="inline">
<CfFormField label="姓名"><CfInput v-model="e" /></CfFormField>
<CfFormField label="邮箱"><CfInput v-model="f" /></CfFormField>
<CfButton>搜索</CfButton>
</CfForm>
</div>
</div>
</template>
手动 error 模式
最直接的用法:父组件自己写校验逻辑,把错误文案塞进每个字段的 error 属性。这种模式不依赖 Form 的内置 validator,适合你已经在用 zod / valibot / yup 等库的项目。
背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfForm, CfFormField, CfInput, CfButton } from '@chufix-design/vue';
const name = ref('');
const email = ref('');
const errors = ref<Record<string, string>>({});
function submit() {
errors.value = {};
if (!name.value.trim()) errors.value.name = '姓名不能为空';
if (!email.value.includes('@')) errors.value.email = '邮箱格式不正确';
}
</script>
<template>
<CfForm layout="vertical">
<CfFormField label="姓名" required :error="errors.name">
<CfInput v-model="name" placeholder="张三" />
</CfFormField>
<CfFormField label="邮箱" required hint="用于登录与接收通知" :error="errors.email">
<CfInput v-model="email" type="email" placeholder="[email protected]" />
</CfFormField>
<div style="display: flex; gap: 8px;">
<CfButton @click="submit">提交</CfButton>
</div>
</CfForm>
</template>
规则校验
给 Form 传 model + rules + name,组件内置 validator 就接管了:
required/min/max/pattern/type: 'email' | 'url' | 'string' | 'number' | 'array'—— 内置规则validator: async (value, model) => string | void—— 任何自定义判断validateOn="submit" | "change" | "blur"—— 触发时机- 必填星号自动从
required规则推断,不再需要手动写required
const rules: Record<string, FieldRules> = {
email: [{ required: true, type: 'email' }],
password: [{ required: true, min: 8, message: '密码至少 8 位' }],
confirm: [
{ required: true },
{ validator: (v, m) => (v !== (m as any).password ? '两次输入不一致' : undefined) },
],
};
背景
<script setup lang="ts">
import { reactive, ref } from 'vue';
import {
CfForm,
CfFormField,
CfInput,
CfButton,
CfSwitch,
toast,
type FieldRules,
} from '@chufix-design/vue';
interface SignupModel {
name: string;
email: string;
password: string;
confirm: string;
agree: boolean;
}
const model = reactive<SignupModel>({
name: '',
email: '',
password: '',
confirm: '',
agree: false,
});
const rules: Record<string, FieldRules> = {
name: [{ required: true, min: 2, max: 24, message: '姓名 2~24 个字符' }],
email: [{ required: true, type: 'email' }],
password: [{ required: true, min: 8, message: '密码至少 8 位' }],
confirm: [
{ required: true },
{
validator: (v, m) => (v !== (m as SignupModel).password ? '两次输入的密码不一致' : undefined),
},
],
agree: [{ validator: (v) => (v === true ? undefined : '请阅读并同意条款') }],
};
const formRef = ref<{ validate: () => Promise<{ valid: boolean }> ; resetFields: () => void } | null>(null);
async function onSubmit({ valid }: { valid: boolean }) {
if (valid) toast({ type: 'success', message: '注册成功' });
}
function reset() {
formRef.value?.resetFields();
}
</script>
<template>
<CfForm
ref="formRef"
layout="vertical"
:model="model"
:rules="rules"
validate-on="blur"
@submit="onSubmit"
>
<CfFormField label="姓名" name="name">
<CfInput v-model="model.name" placeholder="2~24 字符" />
</CfFormField>
<CfFormField label="邮箱" name="email" hint="用于登录与接收通知">
<CfInput v-model="model.email" placeholder="[email protected]" />
</CfFormField>
<CfFormField label="密码" name="password">
<CfInput v-model="model.password" type="password" />
</CfFormField>
<CfFormField label="确认密码" name="confirm">
<CfInput v-model="model.confirm" type="password" />
</CfFormField>
<CfFormField name="agree" :label="undefined">
<label style="display: inline-flex; gap: 8px; align-items: center; font-size: 13px;">
<CfSwitch v-model="model.agree" />
我已阅读并同意《用户协议》
</label>
</CfFormField>
<div style="display: flex; gap: 8px;">
<CfButton type="submit">注册</CfButton>
<CfButton variant="tertiary" @click="reset">重置</CfButton>
</div>
</CfForm>
</template>
异步 validator
validator 可以返回 Promise — 典型场景是”用户名是否被占用”这种需要查后端的校验。
背景
<script setup lang="ts">
import { reactive, ref } from 'vue';
import {
CfForm,
CfFormField,
CfInput,
CfButton,
toast,
type FieldRules,
} from '@chufix-design/vue';
const model = reactive({ username: '' });
const TAKEN = new Set(['admin', 'root', 'chufix']);
const rules: Record<string, FieldRules> = {
username: [
{ required: true, min: 3, max: 16 },
{ pattern: /^[a-z][a-z0-9_]*$/, message: '只能小写字母 / 数字 / 下划线,且需小写字母开头' },
{
validator: (v) =>
new Promise<string | void>((resolve) => {
setTimeout(() => {
resolve(TAKEN.has(String(v)) ? '该用户名已被占用' : undefined);
}, 700);
}),
},
],
};
const formRef = ref<{ validate: () => Promise<{ valid: boolean }> } | null>(null);
const checking = ref(false);
async function onSubmit({ valid }: { valid: boolean }) {
if (valid) toast({ type: 'success', message: '用户名可用' });
}
async function check() {
checking.value = true;
await formRef.value?.validate();
checking.value = false;
}
</script>
<template>
<CfForm
ref="formRef"
layout="vertical"
:model="model"
:rules="rules"
validate-on="blur"
@submit="onSubmit"
>
<CfFormField
label="用户名"
name="username"
hint="`admin` / `root` / `chufix` 已被占用,可触发异步验证"
>
<CfInput v-model="model.username" placeholder="3~16 字符" />
</CfFormField>
<div style="display: flex; gap: 8px;">
<CfButton type="submit" :loading="checking">提交</CfButton>
<CfButton variant="tertiary" @click="check">手动校验</CfButton>
</div>
</CfForm>
</template>
命令式方法
通过 ref 拿到 Form 实例后,可以调用:
| 方法 | 说明 |
|---|---|
submit() | 跑一遍 validate 然后触发 @submit |
validate() | 只跑 validate,返回 { valid, errors } |
validateField(name) | 只校验单个字段 |
clearValidate(name?) | 清空错误信息(不动数据) |
resetFields() | 把 model 还原到初始值,并清空错误 |
提交时如果有错,组件会自动滚动到第一个出错字段并 focus,可通过 :scroll-to-error="false" 关闭。
背景
<script setup lang="ts">
import { reactive, ref } from 'vue';
import {
CfForm,
CfFormField,
CfInput,
CfButton,
toast,
type FieldRules,
} from '@chufix-design/vue';
const model = reactive({ project: '', desc: '' });
const rules: Record<string, FieldRules> = {
project: [{ required: true, min: 2 }],
desc: [{ max: 200 }],
};
const formRef = ref<{
validate: () => Promise<{ valid: boolean }>;
validateField: (n: string) => Promise<unknown>;
clearValidate: (n?: string) => void;
resetFields: () => void;
submit: () => Promise<void>;
} | null>(null);
async function onlyProject() {
await formRef.value?.validateField('project');
}
function clear() {
formRef.value?.clearValidate();
toast({ type: 'info', message: '已清空错误信息(数据不变)' });
}
function reset() {
formRef.value?.resetFields();
toast({ type: 'info', message: '已重置到初始值' });
}
</script>
<template>
<CfForm
ref="formRef"
layout="vertical"
:model="model"
:rules="rules"
@submit="(p: { valid: boolean }) => p.valid && toast({ type: 'success', message: '提交成功' })"
>
<CfFormField label="项目名" name="project">
<CfInput v-model="model.project" />
</CfFormField>
<CfFormField label="描述" name="desc" hint="最多 200 字">
<CfInput v-model="model.desc" />
</CfFormField>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
<CfButton type="submit">submit()</CfButton>
<CfButton variant="tertiary" @click="onlyProject">validateField('project')</CfButton>
<CfButton variant="tertiary" @click="clear">clearValidate()</CfButton>
<CfButton variant="tertiary" @click="reset">resetFields()</CfButton>
</div>
</CfForm>
</template>
复杂表单
混合 Input / Select / Textarea / Button 的真实表单模板。
背景
<script setup lang="ts">
import { ref } from 'vue';
import {
CfForm,
CfFormField,
CfInput,
CfTextarea,
CfSelect,
CfButton,
} from '@chufix-design/vue';
const name = ref('');
const email = ref('');
const role = ref('user');
const bio = ref('');
const roles = [
{ label: '普通用户', value: 'user' },
{ label: '管理员', value: 'admin' },
{ label: '只读', value: 'viewer' },
];
</script>
<template>
<CfForm layout="vertical">
<CfFormField label="姓名" required>
<CfInput v-model="name" placeholder="张三" />
</CfFormField>
<CfFormField label="邮箱" required hint="用于登录与接收通知">
<CfInput v-model="email" type="email" placeholder="[email protected]" />
</CfFormField>
<CfFormField label="角色">
<CfSelect v-model="role" :options="roles" />
</CfFormField>
<CfFormField label="简介">
<CfTextarea v-model="bio" :rows="3" placeholder="一句话介绍自己" />
</CfFormField>
<div style="display: flex; gap: 8px;">
<CfButton>提交</CfButton>
<CfButton variant="ghost" type="reset">重置</CfButton>
</div>
</CfForm>
</template>
API · Form Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
layout | 'vertical' | 'horizontal' | 'inline' | 'vertical' | 整体布局 |
size | 'sm' | 'md' | 'lg' | 'md' | 默认尺寸 |
labelWidth | number | string | — | 仅 horizontal 布局生效,固定 label 宽度 |
disabled | boolean | false | 全局禁用 |
model | Record<string, unknown> | — | 受控的字段值映射,规则校验需要 |
rules | Record<string, FieldRule[]> | — | 字段名 → 规则数组 |
validateOn | 'submit' | 'change' | 'blur' | 'submit' | 何时跑规则校验 |
scrollToError | boolean | true | 提交失败时滚动 / focus 到第一个错误 |
事件:@submit({ valid, values, errors }) / @validate({ valid, errors }) / @reset。
API · FormField Props
| 属性 | 类型 | 说明 |
|---|---|---|
name | string | 字段名,启用规则校验后必填 |
label | string | ReactNode | 标签文案 |
required | boolean | 强制显示必填星号;不传时由规则推断 |
hint | string | ReactNode | 控件下方提示文案 |
error | string | ReactNode | 显式错误文案;非空时优先于规则错误 |
for (Vue) / htmlFor (React) | string | 自定义 input id;省略则自动生成 |
layout | FormLayout | 覆盖父级 Form 布局(单字段调整) |
FieldRule 字段
| 字段 | 含义 |
|---|---|
required | 不允许 undefined / null / ” / 空数组 |
min / max | 字符串/数组的长度范围;number 类型时直接比较数值 |
pattern | 正则匹配(仅对字符串生效) |
type | 内置类型校验:'string' | 'number' | 'email' | 'url' | 'array' |
validator(value, model) | 自定义;返回错误字符串或 void。可异步 |
message | 覆盖默认错误文案 |
反馈与讨论
Form 表单 的讨论