2026-09-20 15:53:28 +08:00
|
|
|
---
|
2026-09-21 17:27:45 +08:00
|
|
|
title: 组合资料表单
|
|
|
|
|
description: 用 Field、Input、Select 和 Button 组合可访问表单,同时把状态留在应用层。
|
2026-09-20 15:53:28 +08:00
|
|
|
order: 30
|
|
|
|
|
toc:
|
2026-09-21 17:27:45 +08:00
|
|
|
- id: complete-example
|
|
|
|
|
title: 完整示例
|
2026-09-20 15:53:28 +08:00
|
|
|
- id: validation
|
2026-09-21 17:27:45 +08:00
|
|
|
title: 验证状态
|
|
|
|
|
- id: controlled-select
|
|
|
|
|
title: 受控 Select
|
2026-09-20 15:53:28 +08:00
|
|
|
- id: styling
|
2026-09-21 17:27:45 +08:00
|
|
|
title: 样式覆盖
|
2026-09-20 15:53:28 +08:00
|
|
|
---
|
|
|
|
|
|
2026-09-21 17:27:45 +08:00
|
|
|
## 完整示例 {#complete-example}
|
2026-09-20 15:53:28 +08:00
|
|
|
|
|
|
|
|
```tsx
|
2026-09-21 17:27:45 +08:00
|
|
|
import * as React from "react"
|
2026-09-20 15:53:28 +08:00
|
|
|
import { Button } from "@workspace/ui/components/button"
|
2026-09-21 17:27:45 +08:00
|
|
|
import {
|
|
|
|
|
Field,
|
|
|
|
|
FieldDescription,
|
|
|
|
|
FieldError,
|
|
|
|
|
FieldGroup,
|
|
|
|
|
FieldLabel,
|
|
|
|
|
} from "@workspace/ui/components/field"
|
2026-09-20 15:53:28 +08:00
|
|
|
import { Input } from "@workspace/ui/components/input"
|
2026-09-21 17:27:45 +08:00
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from "@workspace/ui/components/select"
|
2026-09-20 15:53:28 +08:00
|
|
|
|
2026-09-21 17:27:45 +08:00
|
|
|
export function ProfileForm() {
|
|
|
|
|
const [role, setRole] = React.useState("editor")
|
|
|
|
|
const [error, setError] = React.useState("")
|
|
|
|
|
|
|
|
|
|
function submit(event: React.FormEvent<HTMLFormElement>) {
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
const data = new FormData(event.currentTarget)
|
|
|
|
|
setError(data.get("displayName") ? "" : "请输入显示名称")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<form onSubmit={submit} className="max-w-md space-y-6">
|
|
|
|
|
<FieldGroup>
|
|
|
|
|
<Field data-invalid={Boolean(error)}>
|
|
|
|
|
<FieldLabel htmlFor="display-name">显示名称</FieldLabel>
|
|
|
|
|
<Input
|
|
|
|
|
id="display-name"
|
|
|
|
|
name="displayName"
|
|
|
|
|
aria-invalid={Boolean(error)}
|
|
|
|
|
aria-describedby="display-name-error"
|
|
|
|
|
/>
|
|
|
|
|
<FieldDescription>其他成员会看到这个名称。</FieldDescription>
|
|
|
|
|
<FieldError id="display-name-error">{error}</FieldError>
|
|
|
|
|
</Field>
|
|
|
|
|
|
|
|
|
|
<Field>
|
|
|
|
|
<FieldLabel htmlFor="role">角色</FieldLabel>
|
|
|
|
|
<Select
|
|
|
|
|
value={role}
|
|
|
|
|
onValueChange={(value) => setRole(value ?? "editor")}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger id="role">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="viewer">只读</SelectItem>
|
|
|
|
|
<SelectItem value="editor">编辑者</SelectItem>
|
|
|
|
|
<SelectItem value="admin">管理员</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</Field>
|
|
|
|
|
</FieldGroup>
|
|
|
|
|
|
|
|
|
|
<Button type="submit">保存资料</Button>
|
|
|
|
|
</form>
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-09-20 15:53:28 +08:00
|
|
|
```
|
|
|
|
|
|
2026-09-21 17:27:45 +08:00
|
|
|
## 验证状态 {#validation}
|
|
|
|
|
|
|
|
|
|
UI 包不选择验证库。应用负责产生错误,再把同一状态连接到 `Field data-invalid`、输入控件的 `aria-invalid` 和 `FieldError`。多个 Schema 错误可通过 `FieldError errors={errors}` 传入,它会去重后渲染列表。
|
|
|
|
|
|
|
|
|
|
## 受控 Select {#controlled-select}
|
2026-09-20 15:53:28 +08:00
|
|
|
|
2026-09-21 17:27:45 +08:00
|
|
|
本地 Select 基于 Base UI,使用 `value` 与 `onValueChange`。`SelectTrigger` 的箭头由组件自己渲染,只需放入 `SelectValue`;不要再添加第二个 Chevron。弹层默认与 trigger 对齐并使用其宽度,可通过 `SelectContent` 的 `side`、`align`、`sideOffset` 与 `alignItemWithTrigger` 调整。
|
2026-09-20 15:53:28 +08:00
|
|
|
|
2026-09-21 17:27:45 +08:00
|
|
|
## 样式覆盖 {#styling}
|
2026-09-20 15:53:28 +08:00
|
|
|
|
2026-09-21 17:27:45 +08:00
|
|
|
先使用组件已有的 `variant`、`size` 和结构部件,再用 `className` 处理页面布局。组件内部使用 `cn` 合并 class,因此消费端的 Tailwind class 可以覆盖默认 utility。跨多个页面重复的覆盖应沉淀为新 variant 或复合组件,而不是复制长 class 字符串。
|