开发预览 更新于 2026-05-10

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="you@example.com" />
    </CfFormField>
    <CfButton>提交</CfButton>
  </CfForm>
</template>
<script setup>
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="you@example.com" />
    </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>
<script setup>
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="you@example.com" />
    </CfFormField>
    <div style="display: flex; gap: 8px;">
      <CfButton @click="submit">提交</CfButton>
    </div>
  </CfForm>
</template>
<script setup>
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="you@example.com" />
    </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="you@example.com" />
    </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>
<script setup>
import { reactive, ref } from 'vue';
import {
  CfForm,
  CfFormField,
  CfInput,
  CfButton,
  CfSwitch,
  toast,
} from '@chufix-design/vue';

const model = reactive<SignupModel>({
  name: '',
  email: '',
  password: '',
  confirm: '',
  agree,
});

const rules= {
  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).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="you@example.com" />
    </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>
<script setup>
import { reactive, ref } from 'vue';
import {
  CfForm,
  CfFormField,
  CfInput,
  CfButton,
  toast,
} from '@chufix-design/vue';

const model = reactive({ username: '' });

const TAKEN = new Set(['admin', 'root', 'chufix']);

const rules= {
  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>
<script setup>
import { reactive, ref } from 'vue';
import {
  CfForm,
  CfFormField,
  CfInput,
  CfButton,
  toast,
} from '@chufix-design/vue';

const model = reactive({ project: '', desc: '' });

const rules= {
  project: [{ required: true, min: 2 }],
  desc: [{ max: 200 }],
};

const formRef = ref<{
  validate: () => Promise<{ valid: boolean }>;
  validateField: (n) => 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="you@example.com" />
    </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>
<script setup>
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="you@example.com" />
    </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'默认尺寸
labelWidthnumber | string仅 horizontal 布局生效,固定 label 宽度
disabledbooleanfalse全局禁用
modelRecord<string, unknown>受控的字段值映射,规则校验需要
rulesRecord<string, FieldRule[]>字段名 → 规则数组
validateOn'submit' | 'change' | 'blur''submit'何时跑规则校验
scrollToErrorbooleantrue提交失败时滚动 / focus 到第一个错误

事件:@submit({ valid, values, errors }) / @validate({ valid, errors }) / @reset

API · FormField Props

属性类型说明
namestring字段名,启用规则校验后必填
labelstring | ReactNode标签文案
requiredboolean强制显示必填星号;不传时由规则推断
hintstring | ReactNode控件下方提示文案
errorstring | ReactNode显式错误文案;非空时优先于规则错误
for (Vue) / htmlFor (React)string自定义 input id;省略则自动生成
layoutFormLayout覆盖父级 Form 布局(单字段调整)

FieldRule 字段

字段含义
required不允许 undefined / null / ” / 空数组
min / max字符串/数组的长度范围;number 类型时直接比较数值
pattern正则匹配(仅对字符串生效)
type内置类型校验:'string' | 'number' | 'email' | 'url' | 'array'
validator(value, model)自定义;返回错误字符串或 void。可异步
message覆盖默认错误文案

反馈与讨论

Form 表单 的讨论

0
0 / 600
一键发送
正在加载评论...