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> <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="[email protected]" />
</CfFormField>
<CfButton>提交</CfButton>
</CfForm>
</template> import { useState } from 'react';
import { CfButton, CfForm, CfFormField, CfInput } from '@chufix-design/react';
export default function Demo() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
return (
<>
<CfForm layout="vertical">
<CfFormField label="姓名">
<CfInput value={name} onChange={setName} placeholder="张三" />
</CfFormField>
<CfFormField label="邮箱" hint="用于登录">
<CfInput value={email} onChange={setEmail} type="email" placeholder="[email protected]" />
</CfFormField>
<CfButton>提交</CfButton>
</CfForm>
</>
);
} import { useState } from 'react';
import { CfButton, CfForm, CfFormField, CfInput } from '@chufix-design/react';
export default function Demo() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
return (
<>
<CfForm layout="vertical">
<CfFormField label="姓名">
<CfInput value={name} onChange={setName} placeholder="张三" />
</CfFormField>
<CfFormField label="邮箱" hint="用于登录">
<CfInput value={email} onChange={setEmail} type="email" placeholder="[email protected]" />
</CfFormField>
<CfButton>提交</CfButton>
</CfForm>
</>
);
} 三种布局
layout 决定 label 和控件的排列方式:
vertical(默认)—— label 在控件上方,最常见的填表样式horizontal—— label 与控件同行,配合labelWidth对齐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>
<CfForm layout="vertical">
<CfFormField label="姓名"><CfInput v-model="a" /></CfFormField>
<CfFormField label="邮箱"><CfInput v-model="b" /></CfFormField>
</CfForm>
</div>
<div>
<CfForm layout="horizontal" :label-width="80">
<CfFormField label="姓名"><CfInput v-model="c" /></CfFormField>
<CfFormField label="邮箱"><CfInput v-model="d" /></CfFormField>
</CfForm>
</div>
<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>
<CfForm layout="vertical">
<CfFormField label="姓名"><CfInput v-model="a" /></CfFormField>
<CfFormField label="邮箱"><CfInput v-model="b" /></CfFormField>
</CfForm>
</div>
<div>
<CfForm layout="horizontal" :label-width="80">
<CfFormField label="姓名"><CfInput v-model="c" /></CfFormField>
<CfFormField label="邮箱"><CfInput v-model="d" /></CfFormField>
</CfForm>
</div>
<div>
<CfForm layout="inline">
<CfFormField label="姓名"><CfInput v-model="e" /></CfFormField>
<CfFormField label="邮箱"><CfInput v-model="f" /></CfFormField>
<CfButton>搜索</CfButton>
</CfForm>
</div>
</div>
</template> import { useState } from 'react';
import { CfButton, CfForm, CfFormField, CfInput } from '@chufix-design/react';
export default function Demo() {
const [a, setA] = useState('');
const [b, setB] = useState('');
const [c, setC] = useState('');
const [d, setD] = useState('');
const [e, setE] = useState('');
const [f, setF] = useState('');
return (
<>
<div className="demo-stack">
<div>
<CfForm layout="vertical">
<CfFormField label="姓名"><CfInput value={a} onChange={setA} /></CfFormField>
<CfFormField label="邮箱"><CfInput value={b} onChange={setB} /></CfFormField>
</CfForm>
</div>
<div>
<CfForm layout="horizontal" labelWidth={80}>
<CfFormField label="姓名"><CfInput value={c} onChange={setC} /></CfFormField>
<CfFormField label="邮箱"><CfInput value={d} onChange={setD} /></CfFormField>
</CfForm>
</div>
<div>
<CfForm layout="inline">
<CfFormField label="姓名"><CfInput value={e} onChange={setE} /></CfFormField>
<CfFormField label="邮箱"><CfInput value={f} onChange={setF} /></CfFormField>
<CfButton>搜索</CfButton>
</CfForm>
</div>
</div>
</>
);
} import { useState } from 'react';
import { CfButton, CfForm, CfFormField, CfInput } from '@chufix-design/react';
export default function Demo() {
const [a, setA] = useState('');
const [b, setB] = useState('');
const [c, setC] = useState('');
const [d, setD] = useState('');
const [e, setE] = useState('');
const [f, setF] = useState('');
return (
<>
<div className="demo-stack">
<div>
<CfForm layout="vertical">
<CfFormField label="姓名"><CfInput value={a} onChange={setA} /></CfFormField>
<CfFormField label="邮箱"><CfInput value={b} onChange={setB} /></CfFormField>
</CfForm>
</div>
<div>
<CfForm layout="horizontal" labelWidth={80}>
<CfFormField label="姓名"><CfInput value={c} onChange={setC} /></CfFormField>
<CfFormField label="邮箱"><CfInput value={d} onChange={setD} /></CfFormField>
</CfForm>
</div>
<div>
<CfForm layout="inline">
<CfFormField label="姓名"><CfInput value={e} onChange={setE} /></CfFormField>
<CfFormField label="邮箱"><CfInput value={f} onChange={setF} /></CfFormField>
<CfButton>搜索</CfButton>
</CfForm>
</div>
</div>
</>
);
} 手动 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> <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="[email protected]" />
</CfFormField>
<div style="display: flex; gap: 8px;">
<CfButton @click="submit">提交</CfButton>
</div>
</CfForm>
</template> import { useState } from 'react';
import { CfButton, CfForm, CfFormField, CfInput } from '@chufix-design/react';
export default function Demo() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [errors, setErrors] = useState<Record<string, string>>({});
function submit() {
setErrors({});
if (!name.trim()) errors.name = '姓名不能为空';
if (!email.includes('@')) errors.email = '邮箱格式不正确';
}
return (
<>
<CfForm layout="vertical">
<CfFormField label="姓名" required error={errors.name}>
<CfInput value={name} onChange={setName} placeholder="张三" />
</CfFormField>
<CfFormField label="邮箱" required hint="用于登录与接收通知" error={errors.email}>
<CfInput value={email} onChange={setEmail} type="email" placeholder="[email protected]" />
</CfFormField>
<div style={{ display: "flex", gap: 8 }}>
<CfButton onClick={submit}>提交</CfButton>
</div>
</CfForm>
</>
);
} import { useState } from 'react';
import { CfButton, CfForm, CfFormField, CfInput } from '@chufix-design/react';
export default function Demo() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [errors, setErrors] = useState<Record<string, string>>({});
function submit() {
setErrors({});
if (!name.trim()) errors.name = '姓名不能为空';
if (!email.includes('@')) errors.email = '邮箱格式不正确';
}
return (
<>
<CfForm layout="vertical">
<CfFormField label="姓名" required error={errors.name}>
<CfInput value={name} onChange={setName} placeholder="张三" />
</CfFormField>
<CfFormField label="邮箱" required hint="用于登录与接收通知" error={errors.email}>
<CfInput value={email} onChange={setEmail} type="email" placeholder="[email protected]" />
</CfFormField>
<div style={{ display: "flex", gap: 8 }}>
<CfButton onClick={submit}>提交</CfButton>
</div>
</CfForm>
</>
);
} 规则校验
给 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> <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: false,
});
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="[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> import { useState } from 'react';
import { CfButton, CfForm, CfFormField, CfInput, CfSwitch } from '@chufix-design/react';
export default function Demo() {
interface SignupModel {
name: string;
email: string;
password: string;
confirm: string;
agree: boolean;
}
const model = {
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, setFormRef] = useState<{ validate: () => Promise<{ valid: boolean }> ; resetFields: () => void } | null>(null);
async function onSubmit({ valid }: { valid: boolean }) {
if (valid) toast({ type: 'success', message: '注册成功' });
}
function reset() {
formRef?.resetFields();
}
return (
<>
<CfForm ref="formRef" layout="vertical" model={model} rules={rules} validate-on="blur" onSubmit={onSubmit} >
<CfFormField label="姓名" name="name">
<CfInput value={model.name} placeholder="2~24 字符" />
</CfFormField>
<CfFormField label="邮箱" name="email" hint="用于登录与接收通知">
<CfInput value={model.email} placeholder="[email protected]" />
</CfFormField>
<CfFormField label="密码" name="password">
<CfInput value={model.password} type="password" />
</CfFormField>
<CfFormField label="确认密码" name="confirm">
<CfInput value={model.confirm} type="password" />
</CfFormField>
<CfFormField name="agree" label={undefined}>
<label style={{ display: "inline-flex", gap: 8, alignItems: "center", fontSize: 13 }}>
<CfSwitch value={model.agree} />
我已阅读并同意《用户协议》
</label>
</CfFormField>
<div style={{ display: "flex", gap: 8 }}>
<CfButton type="submit">注册</CfButton>
<CfButton variant="tertiary" onClick={reset}>重置</CfButton>
</div>
</CfForm>
</>
);
} import { useState } from 'react';
import { CfButton, CfForm, CfFormField, CfInput, CfSwitch } from '@chufix-design/react';
export default function Demo() {
const model = {
name: '',
email: '',
password: '',
confirm: '',
agree: false,
};
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, setFormRef] = useState<{ validate: () => Promise<{ valid: boolean }> ; resetFields: () => void } | null>(null);
async function onSubmit({ valid }: { valid: boolean }) {
if (valid) toast({ type: 'success', message: '注册成功' });
}
function reset() {
formRef?.resetFields();
}
return (
<>
<CfForm ref="formRef" layout="vertical" model={model} rules={rules} validate-on="blur" onSubmit={onSubmit} >
<CfFormField label="姓名" name="name">
<CfInput value={model.name} placeholder="2~24 字符" />
</CfFormField>
<CfFormField label="邮箱" name="email" hint="用于登录与接收通知">
<CfInput value={model.email} placeholder="[email protected]" />
</CfFormField>
<CfFormField label="密码" name="password">
<CfInput value={model.password} type="password" />
</CfFormField>
<CfFormField label="确认密码" name="confirm">
<CfInput value={model.confirm} type="password" />
</CfFormField>
<CfFormField name="agree" label={undefined}>
<label style={{ display: "inline-flex", gap: 8, alignItems: "center", fontSize: 13 }}>
<CfSwitch value={model.agree} />
我已阅读并同意《用户协议》
</label>
</CfFormField>
<div style={{ display: "flex", gap: 8 }}>
<CfButton type="submit">注册</CfButton>
<CfButton variant="tertiary" onClick={reset}>重置</CfButton>
</div>
</CfForm>
</>
);
} 异步 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> import { useState } from 'react';
import { CfButton, CfForm, CfFormField, CfInput } from '@chufix-design/react';
export default function Demo() {
const model = { 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, setFormRef] = useState<{ validate: () => Promise<{ valid: boolean }> } | null>(null);
const [checking, setChecking] = useState(false);
async function onSubmit({ valid }: { valid: boolean }) {
if (valid) toast({ type: 'success', message: '用户名可用' });
}
async function check() {
setChecking(true);
await formRef?.validate();
setChecking(false);
}
return (
<>
<CfForm ref="formRef" layout="vertical" model={model} rules={rules} validate-on="blur" onSubmit={onSubmit} >
<CfFormField label="用户名" name="username" hint="`admin` / `root` / `chufix` 已被占用,可触发异步验证" >
<CfInput value={model.username} placeholder="3~16 字符" />
</CfFormField>
<div style={{ display: "flex", gap: 8 }}>
<CfButton type="submit" loading={checking}>提交</CfButton>
<CfButton variant="tertiary" onClick={check}>手动校验</CfButton>
</div>
</CfForm>
</>
);
} import { useState } from 'react';
import { CfButton, CfForm, CfFormField, CfInput } from '@chufix-design/react';
export default function Demo() {
const model = { 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, setFormRef] = useState<{ validate: () => Promise<{ valid: boolean }> } | null>(null);
const [checking, setChecking] = useState(false);
async function onSubmit({ valid }: { valid: boolean }) {
if (valid) toast({ type: 'success', message: '用户名可用' });
}
async function check() {
setChecking(true);
await formRef?.validate();
setChecking(false);
}
return (
<>
<CfForm ref="formRef" layout="vertical" model={model} rules={rules} validate-on="blur" onSubmit={onSubmit} >
<CfFormField label="用户名" name="username" hint="`admin` / `root` / `chufix` 已被占用,可触发异步验证" >
<CfInput value={model.username} placeholder="3~16 字符" />
</CfFormField>
<div style={{ display: "flex", gap: 8 }}>
<CfButton type="submit" loading={checking}>提交</CfButton>
<CfButton variant="tertiary" onClick={check}>手动校验</CfButton>
</div>
</CfForm>
</>
);
} 命令式方法
通过 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> import { useState } from 'react';
import { CfButton, CfForm, CfFormField, CfInput } from '@chufix-design/react';
export default function Demo() {
const model = { project: '', desc: '' };
const rules: Record<string, FieldRules> = {
project: [{ required: true, min: 2 }],
desc: [{ max: 200 }],
};
const [formRef, setFormRef] = useState<{
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?.validateField('project');
}
function clear() {
formRef?.clearValidate();
toast({ type: 'info', message: '已清空错误信息(数据不变)' });
}
function reset() {
formRef?.resetFields();
toast({ type: 'info', message: '已重置到初始值' });
}
return (
<>
<CfForm ref="formRef" layout="vertical" model={model} rules={rules} onSubmit={(p: { valid: boolean }) => p.valid && toast({ type: 'success', message: '提交成功' })}
>
<CfFormField label="项目名" name="project">
<CfInput value={model.project} />
</CfFormField>
<CfFormField label="描述" name="desc" hint="最多 200 字">
<CfInput value={model.desc} />
</CfFormField>
<div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
<CfButton type="submit">submit()</CfButton>
<CfButton variant="tertiary" onClick={onlyProject}>validateField('project')</CfButton>
<CfButton variant="tertiary" onClick={clear}>clearValidate()</CfButton>
<CfButton variant="tertiary" onClick={reset}>resetFields()</CfButton>
</div>
</CfForm>
</>
);
} import { useState } from 'react';
import { CfButton, CfForm, CfFormField, CfInput } from '@chufix-design/react';
export default function Demo() {
const model = { project: '', desc: '' };
const rules= {
project: [{ required: true, min: 2 }],
desc: [{ max: 200 }],
};
const [formRef, setFormRef] = useState<{
validate: () => Promise<{ valid: boolean }>;
validateField: (n) => Promise<unknown>;
clearValidate: (n?: string) => void;
resetFields: () => void;
submit: () => Promise<void>;
} | null>(null);
async function onlyProject() {
await formRef?.validateField('project');
}
function clear() {
formRef?.clearValidate();
toast({ type: 'info', message: '已清空错误信息(数据不变)' });
}
function reset() {
formRef?.resetFields();
toast({ type: 'info', message: '已重置到初始值' });
}
return (
<>
<CfForm ref="formRef" layout="vertical" model={model} rules={rules} onSubmit={(p: { valid: boolean }) => p.valid && toast({ type: 'success', message: '提交成功' })}
>
<CfFormField label="项目名" name="project">
<CfInput value={model.project} />
</CfFormField>
<CfFormField label="描述" name="desc" hint="最多 200 字">
<CfInput value={model.desc} />
</CfFormField>
<div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
<CfButton type="submit">submit()</CfButton>
<CfButton variant="tertiary" onClick={onlyProject}>validateField('project')</CfButton>
<CfButton variant="tertiary" onClick={clear}>clearValidate()</CfButton>
<CfButton variant="tertiary" onClick={reset}>resetFields()</CfButton>
</div>
</CfForm>
</>
);
} 复杂表单
混合 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> <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="[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> import { useState } from 'react';
import { CfButton, CfForm, CfFormField, CfInput, CfSelect, CfTextarea } from '@chufix-design/react';
export default function Demo() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [role, setRole] = useState('user');
const [bio, setBio] = useState('');
const roles = [
{ label: '普通用户', value: 'user' },
{ label: '管理员', value: 'admin' },
{ label: '只读', value: 'viewer' },
];
return (
<>
<CfForm layout="vertical">
<CfFormField label="姓名" required>
<CfInput value={name} onChange={setName} placeholder="张三" />
</CfFormField>
<CfFormField label="邮箱" required hint="用于登录与接收通知">
<CfInput value={email} onChange={setEmail} type="email" placeholder="[email protected]" />
</CfFormField>
<CfFormField label="角色">
<CfSelect value={role} onChange={setRole} options={roles} />
</CfFormField>
<CfFormField label="简介">
<CfTextarea value={bio} onChange={setBio} rows={3} placeholder="一句话介绍自己" />
</CfFormField>
<div style={{ display: "flex", gap: 8 }}>
<CfButton>提交</CfButton>
<CfButton variant="ghost" type="reset">重置</CfButton>
</div>
</CfForm>
</>
);
} import { useState } from 'react';
import { CfButton, CfForm, CfFormField, CfInput, CfSelect, CfTextarea } from '@chufix-design/react';
export default function Demo() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [role, setRole] = useState('user');
const [bio, setBio] = useState('');
const roles = [
{ label: '普通用户', value: 'user' },
{ label: '管理员', value: 'admin' },
{ label: '只读', value: 'viewer' },
];
return (
<>
<CfForm layout="vertical">
<CfFormField label="姓名" required>
<CfInput value={name} onChange={setName} placeholder="张三" />
</CfFormField>
<CfFormField label="邮箱" required hint="用于登录与接收通知">
<CfInput value={email} onChange={setEmail} type="email" placeholder="[email protected]" />
</CfFormField>
<CfFormField label="角色">
<CfSelect value={role} onChange={setRole} options={roles} />
</CfFormField>
<CfFormField label="简介">
<CfTextarea value={bio} onChange={setBio} rows={3} placeholder="一句话介绍自己" />
</CfFormField>
<div style={{ display: "flex", gap: 8 }}>
<CfButton>提交</CfButton>
<CfButton variant="ghost" type="reset">重置</CfButton>
</div>
</CfForm>
</>
);
} 全局禁用
Form 顶层的 disabled 会沿上下文向下传,让所有 FormField 内部的控件统一进入 disabled 视觉态。
常用于 “查看模式 / 编辑模式” 的切换 —— 不需要逐个给 Input / Select 加 disabled。
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { CfForm, CfFormField, CfInput, CfTextarea, CfSelect, CfButton, CfCheckbox } from '@chufix-design/vue';
const locked = ref<boolean>(true);
const model = reactive({
name: '陈奇',
role: 'admin',
bio: '管理员',
agree: true,
});
const roles = [
{ label: '管理员', value: 'admin' },
{ label: '普通用户', value: 'user' },
];
</script>
<template>
<div class="demo-stack">
<CfButton size="sm" variant="tertiary" @click="locked = !locked">
{{ locked ? '解锁表单' : '锁定表单' }}
</CfButton>
<CfForm :model="model" :disabled="locked" layout="vertical">
<CfFormField label="姓名" name="name">
<CfInput v-model="model.name" />
</CfFormField>
<CfFormField label="角色" name="role">
<CfSelect v-model="model.role" :options="roles" />
</CfFormField>
<CfFormField label="备注" name="bio">
<CfTextarea v-model="model.bio" :rows="2" />
</CfFormField>
<CfFormField name="agree">
<CfCheckbox v-model="model.agree">同意保留此账号</CfCheckbox>
</CfFormField>
</CfForm>
</div>
</template> <script setup>
import { reactive, ref } from 'vue';
import { CfForm, CfFormField, CfInput, CfTextarea, CfSelect, CfButton, CfCheckbox } from '@chufix-design/vue';
const locked = ref<boolean>(true);
const model = reactive({
name: '陈奇',
role: 'admin',
bio: '管理员',
agree: true,
});
const roles = [
{ label: '管理员', value: 'admin' },
{ label: '普通用户', value: 'user' },
];
</script>
<template>
<div class="demo-stack">
<CfButton size="sm" variant="tertiary" @click="locked = !locked">
{{ locked ? '解锁表单' : '锁定表单' }}
</CfButton>
<CfForm :model="model" :disabled="locked" layout="vertical">
<CfFormField label="姓名" name="name">
<CfInput v-model="model.name" />
</CfFormField>
<CfFormField label="角色" name="role">
<CfSelect v-model="model.role" :options="roles" />
</CfFormField>
<CfFormField label="备注" name="bio">
<CfTextarea v-model="model.bio" :rows="2" />
</CfFormField>
<CfFormField name="agree">
<CfCheckbox v-model="model.agree">同意保留此账号</CfCheckbox>
</CfFormField>
</CfForm>
</div>
</template> import { useState } from 'react';
import { CfButton, CfCheckbox, CfForm, CfFormField, CfInput, CfSelect, CfTextarea } from '@chufix-design/react';
export default function Demo() {
const [locked, setLocked] = useState<boolean>(true);
const model = {
name: '陈奇',
role: 'admin',
bio: '管理员',
agree: true,
};
const roles = [
{ label: '管理员', value: 'admin' },
{ label: '普通用户', value: 'user' },
];
return (
<>
<div className="demo-stack">
<CfButton size="sm" variant="tertiary" onClick={() => setLocked(!locked)}>
{locked ? '解锁表单' : '锁定表单'}
</CfButton>
<CfForm model={model} disabled={locked} layout="vertical">
<CfFormField label="姓名" name="name">
<CfInput value={model.name} />
</CfFormField>
<CfFormField label="角色" name="role">
<CfSelect value={model.role} options={roles} />
</CfFormField>
<CfFormField label="备注" name="bio">
<CfTextarea value={model.bio} rows={2} />
</CfFormField>
<CfFormField name="agree">
<CfCheckbox value={model.agree}>同意保留此账号</CfCheckbox>
</CfFormField>
</CfForm>
</div>
</>
);
} import { useState } from 'react';
import { CfButton, CfCheckbox, CfForm, CfFormField, CfInput, CfSelect, CfTextarea } from '@chufix-design/react';
export default function Demo() {
const [locked, setLocked] = useState<boolean>(true);
const model = {
name: '陈奇',
role: 'admin',
bio: '管理员',
agree: true,
};
const roles = [
{ label: '管理员', value: 'admin' },
{ label: '普通用户', value: 'user' },
];
return (
<>
<div className="demo-stack">
<CfButton size="sm" variant="tertiary" onClick={() => setLocked(!locked)}>
{locked ? '解锁表单' : '锁定表单'}
</CfButton>
<CfForm model={model} disabled={locked} layout="vertical">
<CfFormField label="姓名" name="name">
<CfInput value={model.name} />
</CfFormField>
<CfFormField label="角色" name="role">
<CfSelect value={model.role} options={roles} />
</CfFormField>
<CfFormField label="备注" name="bio">
<CfTextarea value={model.bio} rows={2} />
</CfFormField>
<CfFormField name="agree">
<CfCheckbox value={model.agree}>同意保留此账号</CfCheckbox>
</CfFormField>
</CfForm>
</div>
</>
);
} 尺寸
Form 的 size 会作为子控件的默认 size,子控件如果显式指定 size 会覆盖父级。
适合表单整体调节信息密度。
size = sm
size = md(默认)
size = lg
<script setup lang="ts">
import { reactive } from 'vue';
import { CfForm, CfFormField, CfInput, CfSelect } from '@chufix-design/vue';
const a = reactive({ name: '', role: 'admin' });
const b = reactive({ name: '', role: 'admin' });
const c = reactive({ name: '', role: 'admin' });
const roles = [
{ label: '管理员', value: 'admin' },
{ label: '普通用户', value: 'user' },
];
</script>
<template>
<div class="demo-stack">
<div>
<p class="demo-hint">size = sm</p>
<CfForm :model="a" size="sm" layout="vertical">
<CfFormField label="姓名" name="name"><CfInput v-model="a.name" /></CfFormField>
<CfFormField label="角色" name="role"><CfSelect v-model="a.role" :options="roles" /></CfFormField>
</CfForm>
</div>
<div>
<p class="demo-hint">size = md(默认)</p>
<CfForm :model="b" size="md" layout="vertical">
<CfFormField label="姓名" name="name"><CfInput v-model="b.name" /></CfFormField>
<CfFormField label="角色" name="role"><CfSelect v-model="b.role" :options="roles" /></CfFormField>
</CfForm>
</div>
<div>
<p class="demo-hint">size = lg</p>
<CfForm :model="c" size="lg" layout="vertical">
<CfFormField label="姓名" name="name"><CfInput v-model="c.name" /></CfFormField>
<CfFormField label="角色" name="role"><CfSelect v-model="c.role" :options="roles" /></CfFormField>
</CfForm>
</div>
</div>
</template>
<style scoped>
.demo-hint { margin: 0 0 8px; color: var(--fg-3); font-size: var(--t-12); }
</style> <script setup>
import { reactive } from 'vue';
import { CfForm, CfFormField, CfInput, CfSelect } from '@chufix-design/vue';
const a = reactive({ name: '', role: 'admin' });
const b = reactive({ name: '', role: 'admin' });
const c = reactive({ name: '', role: 'admin' });
const roles = [
{ label: '管理员', value: 'admin' },
{ label: '普通用户', value: 'user' },
];
</script>
<template>
<div class="demo-stack">
<div>
<p class="demo-hint">size = sm</p>
<CfForm :model="a" size="sm" layout="vertical">
<CfFormField label="姓名" name="name"><CfInput v-model="a.name" /></CfFormField>
<CfFormField label="角色" name="role"><CfSelect v-model="a.role" :options="roles" /></CfFormField>
</CfForm>
</div>
<div>
<p class="demo-hint">size = md(默认)</p>
<CfForm :model="b" size="md" layout="vertical">
<CfFormField label="姓名" name="name"><CfInput v-model="b.name" /></CfFormField>
<CfFormField label="角色" name="role"><CfSelect v-model="b.role" :options="roles" /></CfFormField>
</CfForm>
</div>
<div>
<p class="demo-hint">size = lg</p>
<CfForm :model="c" size="lg" layout="vertical">
<CfFormField label="姓名" name="name"><CfInput v-model="c.name" /></CfFormField>
<CfFormField label="角色" name="role"><CfSelect v-model="c.role" :options="roles" /></CfFormField>
</CfForm>
</div>
</div>
</template>
<style scoped>
.demo-hint { margin: 0 0 8px; color: var(--fg-3); font-size: var(--t-12); }
</style> import { CfForm, CfFormField, CfInput, CfSelect } from '@chufix-design/react';
export default function Demo() {
const a = { name: '', role: 'admin' };
const b = { name: '', role: 'admin' };
const c = { name: '', role: 'admin' };
const roles = [
{ label: '管理员', value: 'admin' },
{ label: '普通用户', value: 'user' },
];
return (
<>
<div className="demo-stack">
<div>
<p className="demo-hint">size = sm</p>
<CfForm model={a} size="sm" layout="vertical">
<CfFormField label="姓名" name="name"><CfInput value={a.name} /></CfFormField>
<CfFormField label="角色" name="role"><CfSelect value={a.role} options={roles} /></CfFormField>
</CfForm>
</div>
<div>
<p className="demo-hint">size = md(默认)</p>
<CfForm model={b} size="md" layout="vertical">
<CfFormField label="姓名" name="name"><CfInput value={b.name} /></CfFormField>
<CfFormField label="角色" name="role"><CfSelect value={b.role} options={roles} /></CfFormField>
</CfForm>
</div>
<div>
<p className="demo-hint">size = lg</p>
<CfForm model={c} size="lg" layout="vertical">
<CfFormField label="姓名" name="name"><CfInput value={c.name} /></CfFormField>
<CfFormField label="角色" name="role"><CfSelect value={c.role} options={roles} /></CfFormField>
</CfForm>
</div>
</div>
</>
);
} import { CfForm, CfFormField, CfInput, CfSelect } from '@chufix-design/react';
export default function Demo() {
const a = { name: '', role: 'admin' };
const b = { name: '', role: 'admin' };
const c = { name: '', role: 'admin' };
const roles = [
{ label: '管理员', value: 'admin' },
{ label: '普通用户', value: 'user' },
];
return (
<>
<div className="demo-stack">
<div>
<p className="demo-hint">size = sm</p>
<CfForm model={a} size="sm" layout="vertical">
<CfFormField label="姓名" name="name"><CfInput value={a.name} /></CfFormField>
<CfFormField label="角色" name="role"><CfSelect value={a.role} options={roles} /></CfFormField>
</CfForm>
</div>
<div>
<p className="demo-hint">size = md(默认)</p>
<CfForm model={b} size="md" layout="vertical">
<CfFormField label="姓名" name="name"><CfInput value={b.name} /></CfFormField>
<CfFormField label="角色" name="role"><CfSelect value={b.role} options={roles} /></CfFormField>
</CfForm>
</div>
<div>
<p className="demo-hint">size = lg</p>
<CfForm model={c} size="lg" layout="vertical">
<CfFormField label="姓名" name="name"><CfInput value={c.name} /></CfFormField>
<CfFormField label="角色" name="role"><CfSelect value={c.role} options={roles} /></CfFormField>
</CfForm>
</div>
</div>
</>
);
} FormField 作用域插槽
FormField 默认插槽暴露 { id, describedBy, invalid } 三个变量,用于把第三方或自定义控件正确接入 a11y 体系:
id—— 控件 id,与 label 的for关联,自动生成describedBy—— 提示 / 错误文案的 id,写到控件的aria-describedbyinvalid—— 当前字段是否有错误,写到aria-invalid
适合需要嵌入原生 input、富文本编辑器、第三方控件库等场景。
<script setup lang="ts">
import { reactive } from 'vue';
import { CfForm, CfFormField, CfButton } from '@chufix-design/vue';
const model = reactive({ url: '' });
const rules = {
url: [{ required: true, type: 'url' as const, message: '请输入合法 URL' }],
};
function onSubmit(e: { valid: boolean; values: Record<string, unknown> }): void {
if (e.valid) alert(`提交:${(e.values as { url: string }).url}`);
}
</script>
<template>
<CfForm :model="model" :rules="rules" layout="vertical" @submit="onSubmit">
<CfFormField label="资源地址" name="url" hint="必须是 http(s):// 开头">
<template #default="{ id, describedBy, invalid }">
<input
:id="id"
v-model="model.url"
:aria-describedby="describedBy"
:aria-invalid="invalid"
placeholder="https://example.com"
class="raw-input"
/>
</template>
</CfFormField>
<CfButton variant="primary" type="submit">提交</CfButton>
</CfForm>
</template>
<style scoped>
.raw-input {
width: 100%;
height: 32px;
padding: 0 10px;
border: 1px solid var(--line-2);
border-radius: var(--r-4);
background: var(--bg-inset);
color: var(--fg-1);
font: inherit;
}
.raw-input[aria-invalid='true'] {
border-color: var(--status-error);
}
.raw-input:focus { outline: none; box-shadow: var(--focus-ring); }
</style> <script setup>
import { reactive } from 'vue';
import { CfForm, CfFormField, CfButton } from '@chufix-design/vue';
const model = reactive({ url: '' });
const rules = {
url: [{ required: true, type: 'url', message: '请输入合法 URL' }],
};
function onSubmit(e: { valid: boolean; values: Record<string, unknown> }): void {
if (e.valid) alert(`提交:${(e.values as { url: string }).url}`);
}
</script>
<template>
<CfForm :model="model" :rules="rules" layout="vertical" @submit="onSubmit">
<CfFormField label="资源地址" name="url" hint="必须是 http(s):// 开头">
<template #default="{ id, describedBy, invalid }">
<input
:id="id"
v-model="model.url"
:aria-describedby="describedBy"
:aria-invalid="invalid"
placeholder="https://example.com"
class="raw-input"
/>
</template>
</CfFormField>
<CfButton variant="primary" type="submit">提交</CfButton>
</CfForm>
</template>
<style scoped>
.raw-input {
width: 100%;
height: 32px;
padding: 0 10px;
border: 1px solid var(--line-2);
border-radius: var(--r-4);
background: var(--bg-inset);
color: var(--fg-1);
font: inherit;
}
.raw-input[aria-invalid='true'] {
border-color: var(--status-error);
}
.raw-input:focus { outline: none; box-shadow: var(--focus-ring); }
</style> import { CfForm, CfFormField } from '@chufix-design/react';
export default function Demo() {
const model = { url: '' };
const rules = {
url: [{ required: true, type: 'url' as const, message: '请输入合法 URL' }],
};
function onSubmit(e: { valid: boolean; values: Record<string, unknown> }): void {
if (e.valid) alert(`提交:${(e.values as { url: string }).url}`);
}
return (
<>
<CfForm model={model} rules={rules} layout="vertical" onSubmit={onSubmit}>
<CfFormField label="资源地址" name="url" hint="必须是 http(s):// 开头">
<input id={id} value={model.url} ariaDescribedby={describedBy} ariaInvalid={invalid} placeholder="https://example.com" className="raw-input" />
</>
);
} import { CfForm, CfFormField } from '@chufix-design/react';
export default function Demo() {
const model = { url: '' };
const rules = {
url: [{ required: true, type: 'url', message: '请输入合法 URL' }],
};
function onSubmit(e: { valid: boolean; values: Record<string, unknown> }): void {
if (e.valid) alert(`提交:${(e.values as { url: string }).url}`);
}
return (
<>
<CfForm model={model} rules={rules} layout="vertical" onSubmit={onSubmit}>
<CfFormField label="资源地址" name="url" hint="必须是 http(s):// 开头">
<input id={id} value={model.url} ariaDescribedby={describedBy} ariaInvalid={invalid} placeholder="https://example.com" className="raw-input" />
</>
);
} 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 布局(单字段调整) |
API · FormField Slots
| Slot | Scoped props | 说明 |
|---|---|---|
default | { id: string; describedBy: string; invalid: boolean } | 控件位置;scoped 参数用于把 aria-describedby / aria-invalid / id 绑到自定义控件。React 端 children 是函数式签名 ({ id, describedBy, invalid }) => ReactNode |
FieldRule 字段
| 字段 | 含义 |
|---|---|
required | 不允许 undefined / null / ” / 空数组 |
min / max | 字符串/数组的长度范围;number 类型时直接比较数值 |
pattern | 正则匹配(仅对字符串生效) |
type | 内置类型校验:'string' | 'number' | 'email' | 'url' | 'array' |
validator(value, model) | 自定义;返回错误字符串或 void。可异步 |
message | 覆盖默认错误文案 |
反馈与讨论
Form 表单 的讨论