|
@@ -0,0 +1,619 @@
|
|
|
|
|
+# 小程序表单开发规范 (Tailwind CSS版)
|
|
|
|
|
+
|
|
|
|
|
+## 概述
|
|
|
|
|
+
|
|
|
|
|
+本规范定义了基于Taro框架的小程序表单开发标准,采用react-hook-form进行状态管理,zod进行表单验证,Tailwind CSS v4进行样式设计。
|
|
|
|
|
+
|
|
|
|
|
+## 技术栈
|
|
|
|
|
+
|
|
|
|
|
+- **Taro 4** - 跨端小程序框架
|
|
|
|
|
+- **React 18** - 前端框架
|
|
|
|
|
+- **React Hook Form 7** - 表单状态管理
|
|
|
|
|
+- **Zod 4** - 模式验证
|
|
|
|
|
+- **@hookform/resolvers** - 验证器集成
|
|
|
|
|
+- **Tailwind CSS v4** - 原子化CSS框架
|
|
|
|
|
+
|
|
|
|
|
+## 目录结构
|
|
|
|
|
+
|
|
|
|
|
+### 推荐结构(大型/复用表单)
|
|
|
|
|
+```
|
|
|
|
|
+mini/
|
|
|
|
|
+├── src/
|
|
|
|
|
+│ ├── components/
|
|
|
|
|
+│ │ └── ui/
|
|
|
|
|
+│ │ ├── form.tsx # 表单核心组件
|
|
|
|
|
+│ │ ├── input.tsx # 输入框组件
|
|
|
|
|
+│ │ ├── label.tsx # 标签组件
|
|
|
|
|
+│ │ └── button.tsx # 按钮组件
|
|
|
|
|
+│ ├── utils/
|
|
|
|
|
+│ │ ├── cn.ts # 类名合并工具
|
|
|
|
|
+│ │ └── validators.ts # 验证规则(可选)
|
|
|
|
|
+│ └── schemas/
|
|
|
|
|
+│ └── user.schema.ts # 表单验证模式(可选)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 简化结构(小型/单次使用表单)
|
|
|
|
|
+```
|
|
|
|
|
+mini/
|
|
|
|
|
+├── src/
|
|
|
|
|
+│ ├── components/
|
|
|
|
|
+│ │ └── ui/
|
|
|
|
|
+│ │ ├── form.tsx # 表单核心组件
|
|
|
|
|
+│ │ ├── input.tsx # 输入框组件
|
|
|
|
|
+│ │ └── button.tsx # 按钮组件
|
|
|
|
|
+└── src/pages/
|
|
|
|
|
+ └── your-page/
|
|
|
|
|
+ └── index.tsx # 验证规则直接定义在页面中
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 核心组件
|
|
|
|
|
+
|
|
|
|
|
+### 1. 表单组件系统
|
|
|
|
|
+
|
|
|
|
|
+#### 1.1 Form 组件
|
|
|
|
|
+```typescript
|
|
|
|
|
+// mini/src/components/ui/form.tsx
|
|
|
|
|
+import { createContext, useContext, forwardRef } from 'react'
|
|
|
|
|
+import { useFormContext } from 'react-hook-form'
|
|
|
|
|
+import { cn } from '@/utils/cn'
|
|
|
|
|
+
|
|
|
|
|
+const Form = forwardRef<
|
|
|
|
|
+ React.ElementRef<typeof TaroForm>,
|
|
|
|
|
+ React.ComponentPropsWithoutRef<typeof TaroForm>
|
|
|
|
|
+>(({ className, ...props }, ref) => {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <TaroForm
|
|
|
|
|
+ ref={ref}
|
|
|
|
|
+ className={cn('space-y-6', className)}
|
|
|
|
|
+ {...props}
|
|
|
|
|
+ />
|
|
|
|
|
+ )
|
|
|
|
|
+})
|
|
|
|
|
+Form.displayName = 'Form'
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 1.2 FormField 组件
|
|
|
|
|
+```typescript
|
|
|
|
|
+const FormField = forwardRef<
|
|
|
|
|
+ React.ElementRef<typeof Controller>,
|
|
|
|
|
+ React.ComponentPropsWithoutRef<typeof Controller>
|
|
|
|
|
+>(({ render, ...props }, ref) => {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Controller
|
|
|
|
|
+ ref={ref}
|
|
|
|
|
+ render={({ field, fieldState, formState }) => (
|
|
|
|
|
+ <FormItemContext.Provider value={{ name: props.name, fieldState, formState }}>
|
|
|
|
|
+ {render({ field, fieldState, formState })}
|
|
|
|
|
+ </FormItemContext.Provider>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {...props}
|
|
|
|
|
+ />
|
|
|
|
|
+ )
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 1.3 FormItem 组件布局
|
|
|
|
|
+```typescript
|
|
|
|
|
+const FormItem = forwardRef<
|
|
|
|
|
+ React.ElementRef<typeof View>,
|
|
|
|
|
+ React.ComponentPropsWithoutRef<typeof View>
|
|
|
|
|
+>(({ className, ...props }, ref) => {
|
|
|
|
|
+ const id = useId()
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <FormItemContext.Provider value={{ id }}>
|
|
|
|
|
+ <View
|
|
|
|
|
+ ref={ref}
|
|
|
|
|
+ className={cn('space-y-2', className)}
|
|
|
|
|
+ {...props}
|
|
|
|
|
+ />
|
|
|
|
|
+ </FormItemContext.Provider>
|
|
|
|
|
+ )
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 1.4 FormLabel 组件
|
|
|
|
|
+```typescript
|
|
|
|
|
+const FormLabel = forwardRef<
|
|
|
|
|
+ React.ElementRef<typeof Text>,
|
|
|
|
|
+ React.ComponentPropsWithoutRef<typeof Text>
|
|
|
|
|
+>(({ className, ...props }, ref) => {
|
|
|
|
|
+ const { error, formItemId } = useFormField()
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Label
|
|
|
|
|
+ ref={ref}
|
|
|
|
|
+ className={cn(
|
|
|
|
|
+ error && 'text-destructive',
|
|
|
|
|
+ className
|
|
|
|
|
+ )}
|
|
|
|
|
+ htmlFor={formItemId}
|
|
|
|
|
+ {...props}
|
|
|
|
|
+ />
|
|
|
|
|
+ )
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 1.5 FormControl 组件
|
|
|
|
|
+```typescript
|
|
|
|
|
+const FormControl = forwardRef<
|
|
|
|
|
+ React.ElementRef<typeof View>,
|
|
|
|
|
+ React.ComponentPropsWithoutRef<typeof View>
|
|
|
|
|
+>(({ ...props }, ref) => {
|
|
|
|
|
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <View
|
|
|
|
|
+ ref={ref}
|
|
|
|
|
+ id={formItemId}
|
|
|
|
|
+ aria-describedby={
|
|
|
|
|
+ !error
|
|
|
|
|
+ ? `${formDescriptionId}`
|
|
|
|
|
+ : `${formDescriptionId} ${formMessageId}`
|
|
|
|
|
+ }
|
|
|
|
|
+ aria-invalid={!!error}
|
|
|
|
|
+ {...props}
|
|
|
|
|
+ />
|
|
|
|
|
+ )
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 1.6 FormMessage 组件
|
|
|
|
|
+```typescript
|
|
|
|
|
+const FormMessage = forwardRef<
|
|
|
|
|
+ React.ElementRef<typeof Text>,
|
|
|
|
|
+ React.ComponentPropsWithoutRef<typeof Text>
|
|
|
|
|
+>(({ className, children, ...props }, ref) => {
|
|
|
|
|
+ const { error, formMessageId } = useFormField()
|
|
|
|
|
+ const body = error ? String(error?.message) : children
|
|
|
|
|
+
|
|
|
|
|
+ if (!body) {
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Text
|
|
|
|
|
+ ref={ref}
|
|
|
|
|
+ id={formMessageId}
|
|
|
|
|
+ className={cn('text-sm font-medium text-destructive', className)}
|
|
|
|
|
+ {...props}
|
|
|
|
|
+ >
|
|
|
|
|
+ {body}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ )
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 2. 表单验证集成
|
|
|
|
|
+
|
|
|
|
|
+#### 2.1 验证模式定义
|
|
|
|
|
+
|
|
|
|
|
+##### 方式1:大型/复用表单(推荐结构)
|
|
|
|
|
+```typescript
|
|
|
|
|
+// mini/src/schemas/user.schema.ts
|
|
|
|
|
+import { z } from 'zod'
|
|
|
|
|
+
|
|
|
|
|
+export const userSchema = z.object({
|
|
|
|
|
+ username: z.string()
|
|
|
|
|
+ .min(2, '用户名至少2个字符')
|
|
|
|
|
+ .max(20, '用户名最多20个字符')
|
|
|
|
|
+ .regex(/^[a-zA-Z0-9_]+$/, '用户名只能包含字母、数字和下划线'),
|
|
|
|
|
+
|
|
|
|
|
+ phone: z.string()
|
|
|
|
|
+ .regex(/^1[3-9]\d{9}$/, '请输入正确的手机号码'),
|
|
|
|
|
+
|
|
|
|
|
+ email: z.string()
|
|
|
|
|
+ .email('请输入正确的邮箱地址'),
|
|
|
|
|
+
|
|
|
|
|
+ password: z.string()
|
|
|
|
|
+ .min(6, '密码至少6个字符')
|
|
|
|
|
+ .regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{6,}$/, '密码必须包含大小写字母和数字'),
|
|
|
|
|
+
|
|
|
|
|
+ confirmPassword: z.string()
|
|
|
|
|
+}).refine((data) => data.password === data.confirmPassword, {
|
|
|
|
|
+ message: '两次输入的密码不一致',
|
|
|
|
|
+ path: ['confirmPassword']
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+export type UserFormData = z.infer<typeof userSchema>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+##### 方式2:小型/单次使用表单(简化结构)
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 直接在页面文件中定义
|
|
|
|
|
+import { z } from 'zod'
|
|
|
|
|
+import { zodResolver } from '@hookform/resolvers/zod'
|
|
|
|
|
+
|
|
|
|
|
+const loginSchema = z.object({
|
|
|
|
|
+ phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号码'),
|
|
|
|
|
+ password: z.string().min(1, '请输入密码')
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+type LoginFormData = z.infer<typeof loginSchema>
|
|
|
|
|
+
|
|
|
|
|
+// 使用方式
|
|
|
|
|
+const form = useForm<LoginFormData>({
|
|
|
|
|
+ resolver: zodResolver(loginSchema),
|
|
|
|
|
+ defaultValues: {
|
|
|
|
|
+ phone: '',
|
|
|
|
|
+ password: ''
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 2.2 验证器配置(可选)
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 对于复用验证规则,可以创建 utils/validators.ts
|
|
|
|
|
+// 小型表单可直接在页面中定义,无需单独文件
|
|
|
|
|
+
|
|
|
|
|
+// 常用验证规则(可选)
|
|
|
|
|
+export const phoneSchema = z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号码')
|
|
|
|
|
+export const emailSchema = z.string().email('请输入正确的邮箱地址')
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 3. 表单组件使用示例
|
|
|
|
|
+
|
|
|
|
|
+#### 3.1 完整表单示例
|
|
|
|
|
+```typescript
|
|
|
|
|
+// mini/src/pages/register/index.tsx
|
|
|
|
|
+import { useForm } from 'react-hook-form'
|
|
|
|
|
+import { zodResolver } from '@hookform/resolvers/zod'
|
|
|
|
|
+import { userSchema, UserFormData } from '@/schemas/user.schema'
|
|
|
|
|
+import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'
|
|
|
|
|
+import { Input } from '@/components/ui/input'
|
|
|
|
|
+import { Button } from '@/components/ui/button'
|
|
|
|
|
+
|
|
|
|
|
+export default function RegisterPage() {
|
|
|
|
|
+ const form = useForm<UserFormData>({
|
|
|
|
|
+ resolver: zodResolver(userSchema),
|
|
|
|
|
+ defaultValues: {
|
|
|
|
|
+ username: '',
|
|
|
|
|
+ phone: '',
|
|
|
|
|
+ email: '',
|
|
|
|
|
+ password: '',
|
|
|
|
|
+ confirmPassword: ''
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const onSubmit = async (data: UserFormData) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 提交表单逻辑
|
|
|
|
|
+ console.log('表单数据:', data)
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('提交失败:', error)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <View className="min-h-screen bg-gray-50 p-4">
|
|
|
|
|
+ <View className="max-w-md mx-auto">
|
|
|
|
|
+ <Text className="text-2xl font-bold text-center mb-6">用户注册</Text>
|
|
|
|
|
+
|
|
|
|
|
+ <Form {...form}>
|
|
|
|
|
+ <TaroForm onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
|
|
|
+ <FormField
|
|
|
|
|
+ control={form.control}
|
|
|
|
|
+ name="username"
|
|
|
|
|
+ render={({ field }) => (
|
|
|
|
|
+ <FormItem>
|
|
|
|
|
+ <FormLabel>用户名</FormLabel>
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ placeholder="请输入用户名"
|
|
|
|
|
+ {...field}
|
|
|
|
|
+ className="h-10"
|
|
|
|
|
+ />
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ <FormMessage />
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <FormField
|
|
|
|
|
+ control={form.control}
|
|
|
|
|
+ name="phone"
|
|
|
|
|
+ render={({ field }) => (
|
|
|
|
|
+ <FormItem>
|
|
|
|
|
+ <FormLabel>手机号</FormLabel>
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ type="tel"
|
|
|
|
|
+ placeholder="请输入手机号"
|
|
|
|
|
+ {...field}
|
|
|
|
|
+ className="h-10"
|
|
|
|
|
+ />
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ <FormMessage />
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <FormField
|
|
|
|
|
+ control={form.control}
|
|
|
|
|
+ name="email"
|
|
|
|
|
+ render={({ field }) => (
|
|
|
|
|
+ <FormItem>
|
|
|
|
|
+ <FormLabel>邮箱</FormLabel>
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ type="email"
|
|
|
|
|
+ placeholder="请输入邮箱"
|
|
|
|
|
+ {...field}
|
|
|
|
|
+ className="h-10"
|
|
|
|
|
+ />
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ <FormMessage />
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <FormField
|
|
|
|
|
+ control={form.control}
|
|
|
|
|
+ name="password"
|
|
|
|
|
+ render={({ field }) => (
|
|
|
|
|
+ <FormItem>
|
|
|
|
|
+ <FormLabel>密码</FormLabel>
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ type="password"
|
|
|
|
|
+ placeholder="请输入密码"
|
|
|
|
|
+ {...field}
|
|
|
|
|
+ className="h-10"
|
|
|
|
|
+ />
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ <FormMessage />
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <FormField
|
|
|
|
|
+ control={form.control}
|
|
|
|
|
+ name="confirmPassword"
|
|
|
|
|
+ render={({ field }) => (
|
|
|
|
|
+ <FormItem>
|
|
|
|
|
+ <FormLabel>确认密码</FormLabel>
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ type="password"
|
|
|
|
|
+ placeholder="请再次输入密码"
|
|
|
|
|
+ {...field}
|
|
|
|
|
+ className="h-10"
|
|
|
|
|
+ />
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ <FormMessage />
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="submit"
|
|
|
|
|
+ className="w-full h-10 bg-blue-500 text-white"
|
|
|
|
|
+ loading={form.formState.isSubmitting}
|
|
|
|
|
+ >
|
|
|
|
|
+ 注册
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </TaroForm>
|
|
|
|
|
+ </Form>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 3.2 登录表单示例
|
|
|
|
|
+```typescript
|
|
|
|
|
+// mini/src/pages/login/index.tsx
|
|
|
|
|
+const loginSchema = z.object({
|
|
|
|
|
+ phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号码'),
|
|
|
|
|
+ password: z.string().min(1, '请输入密码')
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+export default function LoginPage() {
|
|
|
|
|
+ const form = useForm({
|
|
|
|
|
+ resolver: zodResolver(loginSchema),
|
|
|
|
|
+ defaultValues: {
|
|
|
|
|
+ phone: '',
|
|
|
|
|
+ password: ''
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <View className="min-h-screen bg-gray-50 p-4">
|
|
|
|
|
+ <View className="max-w-md mx-auto">
|
|
|
|
|
+ <Text className="text-2xl font-bold text-center mb-6">用户登录</Text>
|
|
|
|
|
+
|
|
|
|
|
+ <Form {...form}>
|
|
|
|
|
+ <TaroForm onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
|
|
|
+ <FormField
|
|
|
|
|
+ control={form.control}
|
|
|
|
|
+ name="phone"
|
|
|
|
|
+ render={({ field }) => (
|
|
|
|
|
+ <FormItem>
|
|
|
|
|
+ <FormLabel>手机号</FormLabel>
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ type="tel"
|
|
|
|
|
+ placeholder="请输入手机号"
|
|
|
|
|
+ {...field}
|
|
|
|
|
+ className="h-10"
|
|
|
|
|
+ />
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ <FormMessage />
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <FormField
|
|
|
|
|
+ control={form.control}
|
|
|
|
|
+ name="password"
|
|
|
|
|
+ render={({ field }) => (
|
|
|
|
|
+ <FormItem>
|
|
|
|
|
+ <FormLabel>密码</FormLabel>
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ type="password"
|
|
|
|
|
+ placeholder="请输入密码"
|
|
|
|
|
+ {...field}
|
|
|
|
|
+ className="h-10"
|
|
|
|
|
+ />
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ <FormMessage />
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <Button type="submit" className="w-full h-10 bg-blue-500 text-white">
|
|
|
|
|
+ 登录
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </TaroForm>
|
|
|
|
|
+ </Form>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 4. 表单验证最佳实践
|
|
|
|
|
+
|
|
|
|
|
+#### 4.1 实时验证
|
|
|
|
|
+```typescript
|
|
|
|
|
+const form = useForm({
|
|
|
|
|
+ resolver: zodResolver(userSchema),
|
|
|
|
|
+ mode: 'onChange', // 实时验证
|
|
|
|
|
+ reValidateMode: 'onChange',
|
|
|
|
|
+ defaultValues: {
|
|
|
|
|
+ username: '',
|
|
|
|
|
+ phone: ''
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 4.2 异步验证
|
|
|
|
|
+```typescript
|
|
|
|
|
+const asyncUsernameSchema = z.object({
|
|
|
|
|
+ username: z.string()
|
|
|
|
|
+ .min(2, '用户名至少2个字符')
|
|
|
|
|
+ .max(20, '用户名最多20个字符')
|
|
|
|
|
+ .refine(async (username) => {
|
|
|
|
|
+ const response = await checkUsernameAvailability(username)
|
|
|
|
|
+ return response.available
|
|
|
|
|
+ }, '用户名已被占用')
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 4.3 错误处理
|
|
|
|
|
+```typescript
|
|
|
|
|
+const onSubmit = async (data: UserFormData) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await registerUser(data)
|
|
|
|
|
+ Taro.showToast({ title: '注册成功' })
|
|
|
|
|
+ Taro.navigateBack()
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ if (error.response?.data?.errors) {
|
|
|
|
|
+ Object.keys(error.response.data.errors).forEach(field => {
|
|
|
|
|
+ form.setError(field as any, {
|
|
|
|
|
+ message: error.response.data.errors[field][0]
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 5. 样式规范
|
|
|
|
|
+
|
|
|
|
|
+#### 5.1 表单布局
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 标准间距
|
|
|
|
|
+const formSpacing = {
|
|
|
|
|
+ item: 'space-y-2', // 表单项内部间距
|
|
|
|
|
+ section: 'space-y-6', // 表单区域间距
|
|
|
|
|
+ group: 'space-y-4' // 表单组间距
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 输入框样式
|
|
|
|
|
+const inputStyles = {
|
|
|
|
|
+ base: 'h-10 px-3 bg-white border border-gray-300 rounded-md',
|
|
|
|
|
+ focus: 'focus:border-blue-500 focus:ring-1 focus:ring-blue-500',
|
|
|
|
|
+ error: 'border-red-500 focus:border-red-500 focus:ring-red-500'
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 5.2 响应式设计
|
|
|
|
|
+```typescript
|
|
|
|
|
+<View className="max-w-md mx-auto p-4 sm:p-6 md:p-8">
|
|
|
|
|
+ <Form className="space-y-4 sm:space-y-6">
|
|
|
|
|
+ <FormItem className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <Input className="w-full" />
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ </Form>
|
|
|
|
|
+</View>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 6. 性能优化
|
|
|
|
|
+
|
|
|
|
|
+#### 6.1 表单防抖
|
|
|
|
|
+```typescript
|
|
|
|
|
+import { debounce } from 'lodash'
|
|
|
|
|
+
|
|
|
|
|
+const debouncedValidate = debounce((value) => {
|
|
|
|
|
+ form.trigger('username')
|
|
|
|
|
+}, 300)
|
|
|
|
|
+
|
|
|
|
|
+const handleUsernameChange = (value: string) => {
|
|
|
|
|
+ form.setValue('username', value)
|
|
|
|
|
+ debouncedValidate(value)
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 6.2 条件渲染
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 避免不必要的重渲染
|
|
|
|
|
+const FormFieldMemo = React.memo(({ name, control, render }) => (
|
|
|
|
|
+ <FormField
|
|
|
|
|
+ name={name}
|
|
|
|
|
+ control={control}
|
|
|
|
|
+ render={render}
|
|
|
|
|
+ />
|
|
|
|
|
+))
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 7. 无障碍支持
|
|
|
|
|
+
|
|
|
|
|
+#### 7.1 语义化标签
|
|
|
|
|
+```typescript
|
|
|
|
|
+<FormItem>
|
|
|
|
|
+ <FormLabel>
|
|
|
|
|
+ <Text className="sr-only">用户名</Text>
|
|
|
|
|
+ </FormLabel>
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ aria-label="用户名"
|
|
|
|
|
+ aria-required="true"
|
|
|
|
|
+ aria-invalid={!!form.formState.errors.username}
|
|
|
|
|
+ />
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+</FormItem>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 7.2 键盘导航
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 支持Tab键导航
|
|
|
|
|
+const handleKeyPress = (e) => {
|
|
|
|
|
+ if (e.key === 'Enter') {
|
|
|
|
|
+ form.handleSubmit(onSubmit)()
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 注意事项
|
|
|
|
|
+
|
|
|
|
|
+1. **类型安全**:始终使用TypeScript定义表单数据类型
|
|
|
|
|
+2. **验证时机**:根据业务需求选择合适的验证时机(onChange/onBlur/onSubmit)
|
|
|
|
|
+3. **错误处理**:提供清晰的用户友好的错误消息
|
|
|
|
|
+4. **性能考虑**:避免在大型表单中使用实时验证
|
|
|
|
|
+5. **无障碍**:确保表单对所有用户可访问
|
|
|
|
|
+6. **移动端适配**:测试在小屏幕设备上的显示效果
|
|
|
|
|
+7. **状态管理**:合理使用React Hook Form的API管理复杂表单状态
|