FormLayout 表单布局

把字段分组排成栅格,带有标题与说明。

联系人

姓名与至少一种联系方式

<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
说明内容