|
@@ -1,619 +0,0 @@
|
|
|
-# 小程序表单开发规范 (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管理复杂表单状态
|
|
|