Forráskód Böngészése

📝 docs(mini-ui): update mini program UI documentation

- update description in mini-ui command documentation
- add comprehensive mini program form development specification
- include form component system implementation with React Hook Form
- add validation integration examples using Zod
- provide best practices for form validation, error handling and performance optimization
yourname 4 hónapja
szülő
commit
f833a28134
2 módosított fájl, 621 hozzáadás és 2 törlés
  1. 2 2
      .roo/commands/mini-ui.md
  2. 619 0
      .roo/rules/17-mini-program-forms.md

+ 2 - 2
.roo/commands/mini-ui.md

@@ -1,5 +1,5 @@
 ---
-description: "按小程序ui规范"
+description: "小程序ui开发指令"
 ---
 
-按小程序ui规范
+按小程序ui规范,小程序表单开发规范

+ 619 - 0
.roo/rules/17-mini-program-forms.md

@@ -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管理复杂表单状态