<script setup lang="ts">
import { reactive, ref } from 'vue'
import * as v from 'valibot'
import { Button, Form, FormField, FormLayout, Input, Text } from '@hina-ui/vue'
const schema = v.object({
lastName: v.pipe(v.string('请输入姓氏'), v.nonEmpty('请输入姓氏')),
firstName: v.pipe(v.string('请输入名字'), v.nonEmpty('请输入名字')),
email: v.pipe(v.string('请输入邮箱'), v.nonEmpty('请输入邮箱'), v.email('邮箱格式不正确')),
phone: v.string(),
})
const values = reactive({ lastName: '', firstName: '', email: '', phone: '' })
const saved = ref('')
async function save(data: unknown) {
await new Promise(resolve => setTimeout(resolve, 600))
saved.value = JSON.stringify(data)
}
</script>
<template>
<Form
v-slot="{ submitting }"
:values="values"
:rules="schema"
class="w-full max-w-lg"
@submit="save"
>
<FormLayout legend="联系人" description="姓名与至少一种联系方式" :columns="2">
<FormField name="lastName" label="姓" required>
<Input v-model="values.lastName" />
</FormField>
<FormField name="firstName" label="名" required>
<Input v-model="values.firstName" />
</FormField>
<FormField name="email" label="邮箱" required class="sm:col-span-2">
<Input v-model="values.email" type="email" />
</FormField>
<FormField name="phone" label="电话" class="sm:col-span-2">
<Input v-model="values.phone" type="tel" />
</FormField>
</FormLayout>
<Button type="submit" :loading="submitting" class="self-start">保存</Button>
<Text v-if="saved" tone="muted" size="sm">已保存:{{ saved }}</Text>
</Form>
</template>
vue
用法
import { FormLayout } from '@hina-ui/vue'
ts
表单布局把一组相关的字段放进一个 fieldset,legend 是这组字段的标题,description 是说明;columns 决定字段排成几列,窄屏时自动收成一列。它通常放在 Form 里,把长表单分成几个部分;单独使用也可以。
<script setup lang="ts">
import { ref } from 'vue'
import { FormField, FormLayout, Input } from '@hina-ui/vue'
const name = ref('')
const email = ref('')
</script>
<template>
<FormLayout legend="账号" description="登录时使用" class="w-80">
<FormField label="用户名">
<Input v-model="name" />
</FormField>
<FormField label="邮箱">
<Input v-model="email" type="email" />
</FormField>
</FormLayout>
</template>
vue
示例
标签列对齐
在布局上统一设置 orientation、label-width 和 description-placement,内部 FormField 会继承这些设置,也可以逐项覆盖。嵌套 FormLayout 继承外层配置,其覆盖值只作用于自己的后代。columns 仍表示一行放几个字段,不表示标签与控件的列数。
响应式模式下,每个字段的可用宽度小于 32rem 时改为纵向排列。没有标签和标签侧说明的字段占满整行,适合包装自带文案的 Switch 或 Checkbox。
<script setup lang="ts">
import { ref } from 'vue'
import { FormField, FormLayout, Input, Switch } from '@hina-ui/vue'
const name = ref('Hina')
const email = ref('hello@example.com')
const sync = ref(true)
</script>
<template>
<FormLayout
legend="个人资料设置"
orientation="responsive"
label-width="9rem"
description-placement="label"
class="w-full"
>
<FormField label="显示名称" description="显示在你的公开个人资料中。">
<Input v-model="name" />
</FormField>
<FormField
label="邮箱地址"
description="使用可以接收通知的邮箱地址。"
description-placement="control"
>
<Input v-model="email" type="email" />
</FormField>
<FormField>
<Switch
v-model="sync"
control-placement="end"
block
description="在其他设备上继续上次的阅读进度。"
>
同步阅读进度
</Switch>
</FormField>
</FormLayout>
</template>
vue
多列
columns 取 1 到 4;需要占满一行的字段给它加上 sm:col-span-2 这类跨列的类。
<script setup lang="ts">
import { reactive } from 'vue'
import { FormField, FormLayout, Input } from '@hina-ui/vue'
const values = reactive({ province: '', city: '', district: '', street: '' })
</script>
<template>
<FormLayout legend="收货地址" :columns="3" class="w-full max-w-lg">
<FormField label="省">
<Input v-model="values.province" />
</FormField>
<FormField label="市">
<Input v-model="values.city" />
</FormField>
<FormField label="区">
<Input v-model="values.district" />
</FormField>
<FormField label="详细地址" class="sm:col-span-3">
<Input v-model="values.street" />
</FormField>
</FormLayout>
</template>
vue
分组
一个表单里放多个布局,每组各有标题与说明,表单的间距把各组隔开。
<script setup lang="ts">
import { reactive, ref } from 'vue'
import * as v from 'valibot'
import { Button, Form, FormField, FormLayout, Input, Switch, Text, Textarea } from '@hina-ui/vue'
const schema = v.object({
name: v.pipe(v.string('请输入昵称'), v.nonEmpty('请输入昵称')),
bio: v.pipe(v.string(), v.maxLength(80, '简介不超过 80 个字')),
notifyEmail: v.boolean(),
notifyPush: v.boolean(),
})
const values = reactive({ name: '', bio: '', notifyEmail: true, notifyPush: false })
const saved = ref('')
async function save(data: unknown) {
await new Promise(resolve => setTimeout(resolve, 600))
saved.value = JSON.stringify(data)
}
</script>
<template>
<Form v-slot="{ submitting }" :values="values" :rules="schema" class="w-80" @submit="save">
<FormLayout legend="个人资料" description="公开显示在个人页">
<FormField name="name" label="昵称" required>
<Input v-model="values.name" />
</FormField>
<FormField name="bio" label="简介">
<Textarea v-model="values.bio" />
</FormField>
</FormLayout>
<FormLayout legend="通知" description="有新的回复或者关注时提醒你">
<FormField name="notifyEmail">
<Switch v-model="values.notifyEmail">邮件</Switch>
</FormField>
<FormField name="notifyPush">
<Switch v-model="values.notifyPush">推送</Switch>
</FormField>
</FormLayout>
<Button type="submit" :loading="submitting" class="self-start">保存</Button>
<Text v-if="saved" tone="muted" size="sm">已保存:{{ saved }}</Text>
</Form>
</template>
vue
禁用一组
disabled 禁用这一组里的所有字段;表单整体禁用时,每一组也随之禁用。
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { FormField, FormLayout, Input, Stack, Switch } from '@hina-ui/vue'
const values = reactive({ name: '星见书音', email: 'shion@example.com' })
const locked = ref(true)
</script>
<template>
<Stack gap="md" align="stretch" class="w-80">
<Switch v-model="locked">锁定账号信息</Switch>
<FormLayout legend="账号" :disabled="locked">
<FormField label="用户名">
<Input v-model="values.name" />
</FormField>
<FormField label="邮箱">
<Input v-model="values.email" type="email" />
</FormField>
</FormLayout>
</Stack>
</template>
vue
行为
- 栅格从
sm断点起按columns分列,更窄的屏幕上是一列;四列在sm到lg之间先收成两列。 - 组内字段照常通过
name从表单取得错误,布局不改变字段与表单的关系。 disabled同时写到fieldset上并传给组内的字段。
无障碍
- 根元素是
fieldset,标题是legend,辅助技术会把组名读在每个字段之前。 - 说明文字通过
aria-describedby关联到fieldset。
API
Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
legend | string | — | 这组字段的标题 |
description | string | — | 说明文字 |
columns | 1 | 2 | 3 | 4 | 1 | 栅格列数 |
orientation | 'vertical' | 'horizontal' | 'responsive' | 'vertical' | 后代字段的默认布局 |
descriptionPlacement | 'label' | 'control' | 'control' | 后代字段的默认说明位置 |
labelWidth | string | number | '10rem' | 统一标签列宽:CSS 长度或像素数 |
disabled | boolean | false | 是否禁用整组字段 |
class | string | — | 追加至根元素的类名 |
插槽
| 插槽 | 说明 |
|---|---|
| default | 字段 |
legend | 标题内容 |
description | 说明内容 |