Dynamic Form
Schema-driven dynamic form with validation, multiple field types, and readonly mode
"use client";
import {
DynamicForm,
type FormSchema,
} from "@/components/composed/dynamic-form/dynamic-form";
const schema: FormSchema = {
title: "快速开始",
// description: "展示各种字段类型的基本用法",
fields: [
{
name: "name",
label: "姓名",
type: "input",
required: true,
placeholder: "请输入您的姓名",
},
{
name: "category",
label: "分类",
type: "select",
required: true,
options: [
{ value: "tech", label: "技术" },
{ value: "design", label: "设计" },
{ value: "product", label: "产品" },
],
},
{
name: "priority",
label: "优先级",
type: "radio",
options: [
{ value: "low", label: "低" },
{ value: "medium", label: "中" },
{ value: "high", label: "高" },
],
// orientation: "horizontal",
defaultValue: "medium",
},
{
name: "active",
label: "启用",
type: "switch",
defaultValue: true,
},
],
};
export function DynamicFormDemo() {
return (
<DynamicForm
schema={schema}
onFinish={(values) => {
console.log("提交:", values);
}}
/>
);
}
Dynamic Form 是一个基于 Schema 配置的动态表单组件,支持多种表单控件、表单验证和只读模式,特别适用于 AI 对话场景中的用户交互表单。
概述
- Schema 驱动:通过 JSON 配置动态生成表单
- 类型安全:完整的 TypeScript 类型定义
- 灵活验证:基于 Zod 的强大验证能力
- 丰富控件:支持 8 种表单控件类型
- 只读模式:支持表单数据的只读展示
- 实例方法:类似 Ant Design 的 API 设计
快速开始
import { DynamicForm, type FormSchema } from "@/registry/wuhan/composed/dynamic-form";
const schema: FormSchema = {
title: "用户信息",
fields: [
{
name: "username",
label: "用户名",
type: "input",
required: true,
},
{
name: "email",
label: "邮箱",
type: "input",
},
],
};
export function Example() {
return (
<DynamicForm
schema={schema}
onFinish={(values) => console.log(values)}
/>
);
}特性
- 8 种表单控件:input、textarea、select、radio、checkbox、switch、slider、number
- 自定义组件:支持使用任何具备 value 和 onChange 属性的自定义组件
- Zod 验证集成:强大的表单验证能力
- 只读模式:展示 label 而非 value,适合数据预览
- 实例方法:getFieldsError、getFieldsValue、validateFields、setFields、resetFields、submit
- 表单值监听:onValuesChange 回调实时监听表单变化
- 自定义渲染:支持字段级别的自定义渲染函数(高级用法)
- 响应式布局:支持 vertical、horizontal、responsive 三种布局方向
- AI 场景友好:提供 generateJsonSchema 辅助 AI 理解表单结构
安装
代码演示
基本
基础用法,展示多种字段类型。
请填写您的基本信息
"use client";
import {
DynamicForm,
type FormSchema,
} from "@/components/composed/dynamic-form/dynamic-form";
const schema: FormSchema = {
title: "用户信息",
description: "请填写您的基本信息",
fields: [
{
name: "username",
label: "用户名",
type: "input",
required: true,
placeholder: "请输入用户名",
},
{
name: "email",
label: "邮箱",
type: "input",
placeholder: "example@email.com",
},
{
name: "bio",
label: "个人简介",
type: "textarea",
placeholder: "介绍一下自己...",
},
{
name: "role",
label: "角色",
type: "select",
required: true,
options: [
{ value: "user", label: "普通用户" },
{ value: "admin", label: "管理员" },
{ value: "guest", label: "访客" },
],
},
{
name: "notifications",
label: "接收通知",
type: "switch",
defaultValue: true,
},
],
};
export function DynamicFormDefault() {
return (
<DynamicForm
schema={schema}
onFinish={(values) => {
console.log("提交的数据:", values);
}}
/>
);
}
表单验证
带 Zod 验证的表单,展示错误提示。
带验证的用户注册表单
"use client";
import {
DynamicForm,
type FormSchema,
} from "@/components/composed/dynamic-form/dynamic-form";
import { z } from "zod";
const schema: FormSchema = {
title: "注册表单",
description: "带验证的用户注册表单",
fields: [
{
name: "username",
label: "用户名",
type: "input",
required: true,
placeholder: "至少3个字符",
},
{
name: "email",
label: "邮箱",
type: "input",
required: true,
placeholder: "有效的邮箱地址",
},
{
name: "password",
label: "密码",
type: "input",
required: true,
placeholder: "至少6个字符",
},
{
name: "age",
label: "年龄",
type: "number",
required: true,
min: 18,
max: 100,
placeholder: "必须年满18岁",
},
{
name: "website",
label: "个人网站",
type: "input",
placeholder: "https://example.com",
},
{
name: "agree",
label: "同意用户协议",
type: "checkbox",
required: true,
},
],
};
const validateSchema = z.object({
username: z.string().min(3, "用户名至少需要3个字符"),
email: z.string().email("请输入有效的邮箱地址"),
password: z.string().min(6, "密码至少需要6个字符"),
age: z.number().min(18, "必须年满18岁").max(100, "年龄不能超过100岁"),
website: z.string().url("请输入有效的URL").optional().or(z.literal("")),
agree: z.boolean().refine((val) => val === true, {
message: "必须同意用户协议",
}),
});
export function DynamicFormValidation() {
return (
<DynamicForm
schema={schema}
validateSchema={validateSchema}
onFinish={(values) => {
console.log("验证通过,提交的数据:", values);
}}
onFinishFailed={(errorInfo) => {
console.log("验证失败:", errorInfo);
}}
/>
);
}
设置只读状态
只读模式展示已填写的表单数据。
已完成的用户资料信息
"use client";
import {
DynamicForm,
type FormSchema,
} from "@/components/composed/dynamic-form/dynamic-form";
const schema: FormSchema = {
title: "用户资料",
description: "已完成的用户资料信息",
fields: [
{
name: "username",
label: "用户名",
type: "input",
},
{
name: "email",
label: "邮箱",
type: "input",
},
{
name: "role",
label: "角色",
type: "select",
options: [
{ value: "admin", label: "管理员" },
{ value: "user", label: "普通用户" },
],
},
{
name: "plan",
label: "订阅计划",
type: "radio",
options: [
{ value: "free", label: "免费版" },
{ value: "pro", label: "专业版" },
{ value: "enterprise", label: "企业版" },
],
orientation: "horizontal",
},
{
name: "notifications",
label: "接收通知",
type: "switch",
},
{
name: "bio",
label: "个人简介",
type: "textarea",
},
],
};
const initialValues = {
username: "张三",
email: "zhangsan@example.com",
role: "admin",
plan: "pro",
notifications: true,
bio: "热爱编程的开发者,专注于前端技术栈。",
};
export function DynamicFormReadonly() {
return (
<DynamicForm
schema={schema}
initialValues={initialValues}
readonly={true}
showActions={false}
/>
);
}
Pending 状态
Pending 状态,显示状态标签,支持表单编辑和提交。
请填写以下信息
"use client";
import { DynamicForm } from "@/components/composed/dynamic-form/dynamic-form";
import type { FormSchema } from "@/components/composed/dynamic-form/dynamic-form";
const schema: FormSchema = {
title: "用户信息表单",
description: "请填写以下信息",
fields: [
{
name: "username",
label: "用户名",
type: "input",
placeholder: "请输入用户名",
required: true,
},
{
name: "email",
label: "邮箱",
type: "input",
placeholder: "请输入邮箱地址",
required: true,
},
{
name: "bio",
label: "个人简介",
type: "textarea",
placeholder: "请输入个人简介",
},
],
};
export function DynamicFormPending() {
return (
<DynamicForm
schema={schema}
status="pending"
onFinish={(values) => console.log("表单提交:", values)}
/>
);
}
Confirmed 状态
Confirmed 状态,显示状态标签,自动设置为只读,隐藏 Footer 按钮。
表单已确认,处于只读状态
"use client";
import { DynamicForm } from "@/components/composed/dynamic-form/dynamic-form";
import type { FormSchema } from "@/components/composed/dynamic-form/dynamic-form";
const schema: FormSchema = {
title: "用户信息表单",
description: "表单已确认,处于只读状态",
fields: [
{
name: "username",
label: "用户名",
type: "input",
placeholder: "请输入用户名",
required: true,
},
{
name: "email",
label: "邮箱",
type: "input",
placeholder: "请输入邮箱地址",
required: true,
},
{
name: "bio",
label: "个人简介",
type: "textarea",
placeholder: "请输入个人简介",
},
],
};
export function DynamicFormConfirmed() {
return (
<DynamicForm
schema={schema}
status="confirmed"
initialValues={{
username: "张三",
email: "zhangsan@example.com",
bio: "这是一段个人简介",
}}
onFinish={(values) => console.log("表单提交:", values)}
/>
);
}
实例方法
使用实例方法控制表单。
"use client";
import { useRef, useState } from "react";
import {
DynamicForm,
type DynamicFormRef,
type FormSchema,
} from "@/components/composed/dynamic-form/dynamic-form";
import { Button } from "@/components/ui/button";
const schema: FormSchema = {
title: "设置",
fields: [
{
name: "name",
label: "名称",
type: "input",
required: true,
placeholder: "请输入名称",
},
{
name: "email",
label: "邮箱",
type: "input",
required: true,
placeholder: "请输入邮箱",
},
{
name: "theme",
label: "主题",
type: "select",
options: [
{ value: "light", label: "浅色" },
{ value: "dark", label: "深色" },
{ value: "auto", label: "自动" },
],
defaultValue: "auto",
},
],
};
export function DynamicFormRefMethods() {
const formRef = useRef<DynamicFormRef>(null);
const [output, setOutput] = useState<string>("");
const handleGetValues = () => {
const values = formRef.current?.getFieldsValue();
setOutput(`当前值: ${JSON.stringify(values, null, 2)}`);
};
const handleGetErrors = () => {
const errors = formRef.current?.getFieldsError();
setOutput(`错误信息: ${JSON.stringify(errors, null, 2)}`);
};
const handleValidate = async () => {
try {
const values = await formRef.current?.validateFields();
setOutput(`验证通过: ${JSON.stringify(values, null, 2)}`);
} catch {
setOutput(`验证失败`);
}
};
const handleReset = () => {
formRef.current?.resetFields();
setOutput("表单已重置");
};
const handleSetValues = () => {
formRef.current?.setFields([
{ name: "name", value: "预设名称" },
{ name: "email", value: "preset@example.com" },
{ name: "theme", value: "dark" },
]);
setOutput("已设置预设值");
};
return (
<div className="space-y-4">
<DynamicForm ref={formRef} schema={schema} showActions={false} />
<div className="flex flex-wrap gap-2">
<Button variant="outline" size="sm" onClick={handleGetValues}>
获取值
</Button>
<Button variant="outline" size="sm" onClick={handleGetErrors}>
获取错误
</Button>
<Button variant="outline" size="sm" onClick={handleValidate}>
验证
</Button>
<Button variant="outline" size="sm" onClick={handleSetValues}>
设置预设值
</Button>
<Button variant="outline" size="sm" onClick={handleReset}>
重置
</Button>
</div>
{output && (
<pre className="rounded-md bg-muted p-4 text-sm">
<code>{output}</code>
</pre>
)}
</div>
);
}
AI 场景
AI 对话场景中的动态表单生成。
💡 AI 场景:用户说「帮我规划一次旅行」,AI 返回以下表单让用户填写详细信息
AI 为您生成的旅行计划表单
"use client";
import { useState } from "react";
import {
DynamicForm,
type FormSchema,
} from "@/components/composed/dynamic-form/dynamic-form";
import { generateJsonSchema } from "@/components/composed/dynamic-form/dynamic-form-utils";
import { Button } from "@/components/ui/button";
// 模拟 AI 返回的表单配置
const aiGeneratedSchema: FormSchema = {
title: "旅行计划",
description: "AI 为您生成的旅行计划表单",
fields: [
{
name: "destination",
label: "目的地",
type: "input",
required: true,
placeholder: "您想去哪里?",
},
{
name: "duration",
label: "旅行时长(天)",
type: "number",
required: true,
min: 1,
max: 30,
defaultValue: 7,
},
{
name: "budget",
label: "预算范围",
type: "slider",
range: {
min: 1000,
max: 50000,
step: 1000,
},
defaultValue: 10000,
},
{
name: "travelStyle",
label: "旅行风格",
type: "radio",
required: true,
options: [
{ value: "relaxed", label: "休闲放松" },
{ value: "adventure", label: "冒险探索" },
{ value: "cultural", label: "文化体验" },
{ value: "food", label: "美食之旅" },
],
// orientation: "horizontal",
},
{
name: "interests",
label: "感兴趣的活动",
type: "checkbox",
options: [
{ value: "hiking", label: "徒步" },
{ value: "diving", label: "潜水" },
{ value: "museum", label: "博物馆" },
{ value: "shopping", label: "购物" },
],
},
{
name: "needGuide",
label: "需要导游",
type: "switch",
defaultValue: false,
},
{
name: "specialRequests",
label: "特殊要求",
type: "textarea",
placeholder: "请告诉我们您的特殊需求...",
},
],
};
export function DynamicFormAIScenario() {
const [showSchema, setShowSchema] = useState(false);
const [formData, setFormData] = useState<any>(null);
// 生成给 AI 的 JSON Schema
const jsonSchema = generateJsonSchema(aiGeneratedSchema.fields);
return (
<div className="space-y-4">
<div className="rounded-md border p-4">
<p className="mb-2 text-sm text-muted-foreground">
💡 AI 场景:用户说「帮我规划一次旅行」,AI
返回以下表单让用户填写详细信息
</p>
<Button
variant="outline"
size="sm"
onClick={() => setShowSchema(!showSchema)}
>
{showSchema ? "隐藏" : "查看"} AI JSON Schema
</Button>
{showSchema && (
<pre className="mt-4 overflow-auto rounded-md bg-muted p-4 text-xs">
<code>{JSON.stringify(jsonSchema, null, 2)}</code>
</pre>
)}
</div>
<DynamicForm
schema={aiGeneratedSchema}
onFinish={(values) => {
setFormData(values);
console.log("用户提交的旅行计划:", values);
}}
onValuesChange={(values) => {
console.log("表单值变化:", values);
}}
/>
{formData && (
<div className="rounded-md border bg-muted/50 p-4">
<h3 className="mb-2 font-semibold">提交的数据:</h3>
<pre className="overflow-auto text-xs">
<code>{JSON.stringify(formData, null, 2)}</code>
</pre>
</div>
)}
</div>
);
}
设置额外元素
表单头部带额外信息,如状态标签。
填写订单详细信息
"use client";
import {
DynamicForm,
type FormSchema,
} from "@/components/composed/dynamic-form/dynamic-form";
import { StatusTag } from "@/components/composed/status-tag/status-tag";
const schema: FormSchema = {
title: "订单信息",
description: "填写订单详细信息",
fields: [
{
name: "orderNumber",
label: "订单编号",
type: "input",
placeholder: "自动生成",
disabled: true,
defaultValue: "ORD-2026-001",
},
{
name: "customerName",
label: "客户姓名",
type: "input",
required: true,
placeholder: "请输入客户姓名",
},
{
name: "product",
label: "产品",
type: "select",
required: true,
options: [
{ value: "laptop", label: "笔记本电脑" },
{ value: "phone", label: "智能手机" },
{ value: "tablet", label: "平板电脑" },
],
},
{
name: "quantity",
label: "数量",
type: "number",
defaultValue: 1,
min: 1,
max: 100,
},
{
name: "urgent",
label: "加急处理",
type: "switch",
defaultValue: false,
},
{
name: "notes",
label: "备注",
type: "textarea",
placeholder: "请输入备注信息",
},
],
};
export function DynamicFormWithExtra() {
return (
<DynamicForm
schema={schema}
extra={<StatusTag status="pending" />}
onFinish={(values) => {
console.log("提交:", values);
}}
/>
);
}
自定义组件
通过 component 属性使用自定义表单组件。自定义组件必须具备 value 和 onChange 属性,这是受控组件的基本要求。
展示多个自定义组件的使用
"use client";
import * as React from "react";
import {
DynamicForm,
type FormSchema,
type CustomFieldComponentProps,
} from "@/components/composed/dynamic-form/dynamic-form";
import { cn } from "@/lib/utils";
/**
* 复杂自定义组件示例:地址选择器
* 包含省份和城市两个输入框
* 展示如何在一个自定义组件中管理多个输入字段
*/
interface AddressValue {
province?: string;
city?: string;
}
function AddressSelector({
value,
onChange,
disabled,
error,
}: CustomFieldComponentProps) {
const addressValue = (value as AddressValue) || {};
// 省份和城市数据
const provinces = [
{ name: "广东省", cities: ["广州市", "深圳市", "珠海市", "东莞市"] },
{ name: "浙江省", cities: ["杭州市", "宁波市", "温州市", "绍兴市"] },
{ name: "江苏省", cities: ["南京市", "苏州市", "无锡市", "常州市"] },
{ name: "上海市", cities: ["黄浦区", "徐汇区", "长宁区", "静安区"] },
{ name: "北京市", cities: ["东城区", "西城区", "朝阳区", "海淀区"] },
];
const [selectedProvince, setSelectedProvince] = React.useState<string>(
addressValue.province || "",
);
const [selectedCity, setSelectedCity] = React.useState<string>(
addressValue.city || "",
);
// 当省份或城市改变时,更新表单值
React.useEffect(() => {
onChange({
province: selectedProvince,
city: selectedCity,
});
}, [selectedProvince, selectedCity, onChange]);
// 当省份改变时,重置城市
const handleProvinceChange = (province: string) => {
setSelectedProvince(province);
setSelectedCity("");
};
const currentProvince = provinces.find((p) => p.name === selectedProvince);
const cities = currentProvince ? currentProvince.cities : [];
return (
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
{/* 省份选择 */}
<div className="space-y-1.5">
<label className="text-sm font-medium">省份</label>
<select
value={selectedProvince}
onChange={(e) => handleProvinceChange(e.target.value)}
disabled={disabled}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
"disabled:cursor-not-allowed disabled:opacity-50",
error && "border-destructive",
)}
>
<option value="">请选择省份</option>
{provinces.map((province) => (
<option key={province.name} value={province.name}>
{province.name}
</option>
))}
</select>
</div>
{/* 城市选择 */}
<div className="space-y-1.5">
<label className="text-sm font-medium">城市</label>
<select
value={selectedCity}
onChange={(e) => setSelectedCity(e.target.value)}
disabled={disabled || !selectedProvince}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
"disabled:cursor-not-allowed disabled:opacity-50",
error && "border-destructive",
)}
>
<option value="">请选择城市</option>
{cities.map((city) => (
<option key={city} value={city}>
{city}
</option>
))}
</select>
</div>
</div>
{/* 显示当前选择 */}
{selectedProvince && selectedCity && (
<div className="text-sm text-muted-foreground">
已选择:{selectedProvince} / {selectedCity}
</div>
)}
</div>
);
}
/**
* 自定义价格范围输入组件
* 包含最小值和最大值两个数字输入框
*/
interface PriceRangeValue {
min?: number;
max?: number;
}
function PriceRangeInput({
value,
onChange,
disabled,
error,
field: _field,
}: CustomFieldComponentProps) {
const rangeValue = (value as PriceRangeValue) || {};
const [minValue, setMinValue] = React.useState<number | "">(
rangeValue.min ?? "",
);
const [maxValue, setMaxValue] = React.useState<number | "">(
rangeValue.max ?? "",
);
// 当 min 或 max 改变时,更新表单值
React.useEffect(() => {
onChange({
min: minValue === "" ? undefined : minValue,
max: maxValue === "" ? undefined : maxValue,
});
}, [minValue, maxValue, onChange]);
return (
<div className="space-y-2">
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<label className="text-sm font-medium">最小值</label>
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground">
¥
</span>
<input
type="number"
value={minValue}
onChange={(e) =>
setMinValue(e.target.value === "" ? "" : Number(e.target.value))
}
disabled={disabled}
placeholder="0"
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent pl-7 pr-3 py-1 text-sm shadow-sm transition-colors",
"placeholder:text-muted-foreground",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
"disabled:cursor-not-allowed disabled:opacity-50",
error && "border-destructive",
)}
/>
</div>
</div>
<div className="space-y-1.5">
<label className="text-sm font-medium">最大值</label>
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground">
¥
</span>
<input
type="number"
value={maxValue}
onChange={(e) =>
setMaxValue(e.target.value === "" ? "" : Number(e.target.value))
}
disabled={disabled}
placeholder="不限"
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent pl-7 pr-3 py-1 text-sm shadow-sm transition-colors",
"placeholder:text-muted-foreground",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
"disabled:cursor-not-allowed disabled:opacity-50",
error && "border-destructive",
)}
/>
</div>
</div>
</div>
{/* 验证提示 */}
{minValue !== "" && maxValue !== "" && minValue > maxValue && (
<p className="text-sm text-destructive">最小值不能大于最大值</p>
)}
{/* 显示当前范围 */}
{(minValue !== "" || maxValue !== "") && (
<div className="text-sm text-muted-foreground">
价格范围:
{minValue !== "" ? `¥${minValue}` : "不限"} -{" "}
{maxValue !== "" ? `¥${maxValue}` : "不限"}
</div>
)}
</div>
);
}
/**
* 自定义颜色选择器组件
* 必须实现 value 和 onChange 属性
*/
function ColorPicker({
value,
onChange,
disabled,
error,
}: CustomFieldComponentProps) {
const colors = [
{ name: "红色", value: "#ef4444" },
{ name: "橙色", value: "#f97316" },
{ name: "黄色", value: "#eab308" },
{ name: "绿色", value: "#22c55e" },
{ name: "蓝色", value: "#3b82f6" },
{ name: "紫色", value: "#a855f7" },
{ name: "粉色", value: "#ec4899" },
{ name: "灰色", value: "#6b7280" },
];
const currentValue = value as string | undefined;
return (
<div className="space-y-2">
<div className="grid grid-cols-4 gap-2">
{colors.map((color) => (
<button
key={color.value}
type="button"
disabled={disabled}
onClick={() => onChange(color.value)}
className={cn(
"h-12 rounded-md border-2 transition-all hover:scale-105",
"focus:outline-none focus:ring-2 focus:ring-offset-2",
currentValue === color.value
? "border-primary ring-2 ring-primary ring-offset-2"
: "border-transparent",
disabled && "opacity-50 cursor-not-allowed",
error && "border-destructive",
)}
style={{ backgroundColor: color.value }}
title={color.name}
/>
))}
</div>
{currentValue && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<div
className="h-4 w-4 rounded border"
style={{ backgroundColor: currentValue }}
/>
<span>{currentValue}</span>
</div>
)}
</div>
);
}
const schema: FormSchema = {
title: "商品信息表单",
description: "展示多个自定义组件的使用",
fields: [
{
name: "productName",
label: "商品名称",
type: "input",
required: true,
placeholder: "请输入商品名称",
},
{
name: "address",
label: "发货地址",
type: "input", // type 可以是任意值,因为使用了 component
required: true,
component: AddressSelector,
description: "选择省份和城市(两个输入框组成的自定义组件)",
defaultValue: {},
},
{
name: "priceRange",
label: "价格范围",
type: "input",
component: PriceRangeInput,
description: "设置商品的价格区间(包含最小值和最大值)",
defaultValue: {},
},
{
name: "color",
label: "主题颜色",
type: "input",
component: ColorPicker,
description: "选择商品的主题颜色",
},
{
name: "category",
label: "商品分类",
type: "select",
required: true,
options: [
{ value: "electronics", label: "电子产品" },
{ value: "clothing", label: "服装" },
{ value: "food", label: "食品" },
{ value: "books", label: "图书" },
],
},
{
name: "description",
label: "商品描述",
type: "textarea",
placeholder: "请输入商品描述...",
},
],
};
export function DynamicFormCustomComponent() {
return (
<DynamicForm
schema={schema}
onFinish={(values) => {
console.log("提交的数据:", values);
alert(JSON.stringify(values, null, 2));
}}
/>
);
}
自定义组件示例
以上示例展示了三个不同复杂度的自定义组件:
- 地址选择器(AddressSelector):包含省份和城市两个下拉框,展示如何在一个组件中管理多个输入字段
- 价格范围输入(PriceRangeInput):包含最小值和最大值两个数字输入框,展示如何处理复杂的数据结构
- 颜色选择器(ColorPicker):简单的颜色选择组件,展示基础的自定义组件实现
自定义组件要求
当使用 component 属性时,您的自定义组件需要遵循以下规范:
必需属性:
value: 字段的当前值,组件应根据此值显示状态onChange: 值变更处理函数,当用户交互改变值时调用此函数
可选属性:
onBlur: 字段失焦处理函数,用于触发验证error: 字段错误信息(FieldError类型),可用于显示错误状态disabled: 是否禁用组件field: 完整的字段配置对象,可访问其他配置信息(如placeholder、description等)
复杂组件示例:地址选择器
下面是一个完整的自定义组件示例,展示如何创建包含多个输入框的组件:
import type { CustomFieldComponentProps } from "@/registry/wuhan/composed/dynamic-form";
interface AddressValue {
province?: string;
city?: string;
}
function AddressSelector({ value, onChange, disabled, error }: CustomFieldComponentProps) {
const addressValue = (value as AddressValue) || {};
const [selectedProvince, setSelectedProvince] = useState(addressValue.province || "");
const [selectedCity, setSelectedCity] = useState(addressValue.city || "");
// 当省份或城市改变时,更新表单值
useEffect(() => {
onChange({
province: selectedProvince,
city: selectedCity,
});
}, [selectedProvince, selectedCity, onChange]);
return (
<div className="grid grid-cols-2 gap-3">
<select
value={selectedProvince}
onChange={(e) => setSelectedProvince(e.target.value)}
disabled={disabled}
>
<option value="">请选择省份</option>
{/* ... 省份选项 */}
</select>
<select
value={selectedCity}
onChange={(e) => setSelectedCity(e.target.value)}
disabled={disabled || !selectedProvince}
>
<option value="">请选择城市</option>
{/* ... 城市选项 */}
</select>
</div>
);
}
// 在 schema 中使用
const schema = {
fields: [
{
name: "address",
label: "地址",
type: "input",
component: AddressSelector,
defaultValue: {},
},
],
};这种设计遵循 React 受控组件的标准模式,确保表单状态的一致性和可预测性。您可以创建任何符合此接口的自定义组件,如颜色选择器、标签输入、富文本编辑器等。
API
DynamicForm
主表单组件,封装了表单状态管理和验证逻辑。
Props
| Prop | Type | Default | Description |
|---|---|---|---|
schema | FormSchema | - | 表单配置 Schema(必填) |
className | string | - | 自定义样式类名 |
style | React.CSSProperties | - | 内联样式 |
initialValues | Record<string, any> | {} | 表单初始值 |
onValuesChange | (values) => void | - | 表单值变化回调 |
onFinish | (values) => void | - | 表单提交成功回调 |
onFinishFailed | (errorInfo) => void | - | 表单提交失败回调 |
validateSchema | z.ZodType<any> | - | Zod 验证 Schema |
readonly | boolean | false | 是否只读模式 |
status | "pending" | "confirmed" | - | 状态,只支持 pending 和 confirmed,confirmed 状态下自动设置为只读并隐藏 Footer |
showActions | boolean | true | 是否显示操作按钮 |
submitText | string | "提交" | 提交按钮文本 |
resetText | string | "重置" | 重置按钮文本 |
showTitle | boolean | true | 是否显示表单标题 |
extra | React.ReactNode | - | 表单头部额外信息(如状态标签) |
Ref Methods
通过 ref 可以访问以下实例方法:
| Method | Type | Description |
|---|---|---|
getFieldsError | (nameList?: string[]) => FieldErrorInfo[] | 获取字段错误信息 |
getFieldsValue | (nameList?: string[]) => Record<string, any> | 获取字段值 |
validateFields | (nameList?: string[]) => Promise<Record<string, any>> | 触发表单验证 |
setFields | (fields: Array<{name, value?, errors?}>) => void | 设置字段状态 |
resetFields | (nameList?: string[]) => void | 重置字段到初始值 |
submit | () => Promise<void> | 提交表单 |
getForm | () => UseFormReturn<any> | 获取 react-hook-form 实例 |
Example
import { useRef } from "react";
import { DynamicForm, type DynamicFormRef } from "@/registry/wuhan/composed/dynamic-form";
function FormWithMethods() {
const formRef = useRef<DynamicFormRef>(null);
const handleGetValues = () => {
const values = formRef.current?.getFieldsValue();
console.log(values);
};
const handleValidate = async () => {
try {
const values = await formRef.current?.validateFields();
console.log("验证通过:", values);
} catch (error) {
console.error("验证失败");
}
};
return (
<div>
<DynamicForm ref={formRef} schema={schema} showActions={false} />
<button onClick={handleGetValues}>获取值</button>
<button onClick={handleValidate}>验证</button>
</div>
);
}FormSchema
表单配置 Schema 的结构定义。
| Property | Type | Description |
|---|---|---|
title | string | 表单标题 |
description | string | 表单描述 |
fields | FieldSchema[] | 字段配置列表(必填) |
FieldSchema
单个字段的配置结构。
| Property | Type | Description |
|---|---|---|
name | string | 字段名(必填,唯一) |
label | string | 字段标签(必填) |
type | FieldType | 字段类型(必填) |
description | string | 字段描述 |
placeholder | string | 占位符 |
defaultValue | any | 默认值 |
required | boolean | 是否必填 |
disabled | boolean | 是否禁用 |
options | FieldOption[] | 选项列表(select、radio 必需) |
orientation | 'vertical' | 'horizontal' | 'responsive' | 布局方向 |
min | number | 最小值(number 类型) |
max | number | 最大值(number 类型) |
step | number | 步进值(number 类型) |
range | {min, max, step?} | 范围配置(slider 类型) |
component | React.ComponentType<CustomFieldComponentProps> | 自定义组件(需要具备 value 和 onChange 属性) |
render | (props) => ReactNode | 自定义渲染函数(优先级高于 component) |
FieldType
支持的字段类型。
| Type | Description | Required Props |
|---|---|---|
input | 单行文本输入 | - |
textarea | 多行文本输入 | - |
select | 下拉选择 | options |
radio | 单选按钮组 | options |
checkbox | 复选框 | - |
switch | 开关 | - |
slider | 滑块 | range |
number | 数字输入 | - |
FieldOption
选择类字段的选项配置。
| Property | Type | Description |
|---|---|---|
value | string | number | boolean | 选项值 |
label | string | 选项显示文本 |
disabled | boolean | 是否禁用该选项 |
CustomFieldComponentProps
自定义组件必须实现的属性接口。
| Property | Type | Required | Description |
|---|---|---|---|
value | unknown | ✅ | 字段的当前值 |
onChange | (value: unknown) => void | ✅ | 值变更处理函数 |
onBlur | () => void | ❌ | 字段失焦处理函数 |
error | FieldError | ❌ | 字段错误信息 |
disabled | boolean | ❌ | 是否禁用 |
field | FieldSchema | ❌ | 完整的字段配置对象 |
简单示例(单个输入):
import type { CustomFieldComponentProps } from "@/registry/wuhan/composed/dynamic-form";
function CustomRating({ value, onChange, disabled }: CustomFieldComponentProps) {
const rating = (value as number) || 0;
return (
<div className="flex gap-1">
{[1, 2, 3, 4, 5].map((star) => (
<button
key={star}
type="button"
disabled={disabled}
onClick={() => onChange(star)}
className={star <= rating ? "text-yellow-500" : "text-gray-300"}
>
★
</button>
))}
</div>
);
}复杂示例(多个输入):
import { useState, useEffect } from "react";
import type { CustomFieldComponentProps } from "@/registry/wuhan/composed/dynamic-form";
interface DateRangeValue {
start?: string;
end?: string;
}
function DateRangePicker({ value, onChange, disabled }: CustomFieldComponentProps) {
const rangeValue = (value as DateRangeValue) || {};
const [start, setStart] = useState(rangeValue.start || "");
const [end, setEnd] = useState(rangeValue.end || "");
// 当任一日期改变时,更新表单值
useEffect(() => {
onChange({ start, end });
}, [start, end, onChange]);
return (
<div className="flex gap-2">
<input
type="date"
value={start}
onChange={(e) => setStart(e.target.value)}
disabled={disabled}
/>
<span>至</span>
<input
type="date"
value={end}
onChange={(e) => setEnd(e.target.value)}
disabled={disabled}
/>
</div>
);
}
className={star <= rating ? "text-yellow-500" : "text-gray-300"}
>
★
</button>
))}
</div>
);
}工具函数
generateJsonSchema
根据字段配置生成 AI 兼容的 JSON Schema。
import { generateJsonSchema } from "@/registry/wuhan/composed/dynamic-form-utils";
const jsonSchema = generateJsonSchema(schema.fields);
// 将 jsonSchema 提供给 AI,让 AI 按照这个规范输出extractDefaultValues
从字段配置中提取默认值。
import { extractDefaultValues } from "@/registry/wuhan/composed/dynamic-form-utils";
const defaultValues = extractDefaultValues(schema.fields);getDisplayLabel
获取只读模式下的显示标签。
import { getDisplayLabel } from "@/registry/wuhan/composed/dynamic-form-utils";
const displayText = getDisplayLabel(value, field);
// 对于 select: 返回对应的 label
// 对于 checkbox/switch: 返回 "是" 或 "否"使用场景
- AI 对话:根据 AI 返回的配置动态生成表单
- 动态配置:后台配置化的表单生成
- 数据录入:快速构建各类数据录入表单
- 表单预览:只读模式展示已填写的表单数据
- 表单验证:需要复杂验证规则的表单场景
最佳实践
- 表单验证:使用 Zod Schema 定义验证规则,确保类型安全
- 初始值:合理设置
defaultValue和initialValues,避免非受控组件警告 - 只读模式:用于数据展示时启用
readonly,提升用户体验 - 实例方法:复杂交互场景下使用
ref访问实例方法 - 自定义组件:
- 确保自定义组件实现
value和onChange属性(受控组件模式) - 使用 TypeScript 时,导入
CustomFieldComponentProps类型确保类型安全 - 优先使用
component而非render,除非需要完全自定义布局 - 在自定义组件中适当处理
error和disabled状态
- 确保自定义组件实现
- 自定义渲染:对于需要完全控制布局的字段,使用
render函数(优先级高于component)
注意事项
- 确保字段的
name在同一表单中唯一 - 选择类字段(select、radio)必须提供
options属性 - 使用
validateSchema时,字段名需要与 Schema 中定义的字段名一致 - 只读模式下,表单不会进行验证,因此可以隐藏操作按钮
- 对于 select 类型在只读模式下,确保
options中包含当前值对应的选项 - 自定义组件必须遵循受控组件模式(controlled component pattern),即通过
value接收值,通过onChange通知变更
原语组件
Dynamic Form 基于以下原语组件构建:
DynamicFormLayoutPrimitive- 表单布局容器DynamicFormHeaderPrimitive- 表单头部DynamicFormTitlePrimitive- 表单标题DynamicFormBodyLayout- 表单主体布局DynamicFormFooterPrimitive- 表单底部操作栏
这些原语可以在 registry/wuhan/blocks/dynamic-form/dynamic-form-01.tsx 中找到。