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

Form 表单

表单封装层 —— 三种布局、规则校验、异步 validator、命令式 validate / reset / submit、自动滚动到第一个错误。

基础用法

<CfForm> 提供布局上下文,<CfFormField> 包裹每一个表单项,统一渲染 label、必填星号、提示 / 错误文案。最简形式只是个布局封装。

背景 视口

用于登录

src/App.vue
<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 —— 所有字段挤在一行,常用于搜索条 / 工具栏
背景 视口
src/App.vue
<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 等库的项目。

背景 视口

用于登录与接收通知

src/App.vue
<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) },
  ],
};
背景 视口

用于登录与接收通知

src/App.vue
<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 — 典型场景是”用户名是否被占用”这种需要查后端的校验。

背景 视口

`admin` / `root` / `chufix` 已被占用,可触发异步验证

src/App.vue
<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" 关闭。

背景 视口

最多 200 字

src/App.vue
<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 的真实表单模板。

背景 视口

用于登录与接收通知

src/App.vue
<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。

背景 视口
src/App.vue
<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

src/App.vue
<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-describedby
  • invalid —— 当前字段是否有错误,写到 aria-invalid

适合需要嵌入原生 input、富文本编辑器、第三方控件库等场景。

背景 视口

必须是 http(s):// 开头

src/App.vue
<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'默认尺寸
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 布局(单字段调整)

API · FormField Slots

SlotScoped 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 表单 的讨论

0
0 / 600
正在加载评论...