Procházet zdrojové kódy

📝 docs(shadcn-form): add advanced form component templates and best practices

- add advanced form component templates including avatar selector, select, number input, date picker, textarea and checkbox group
- add form state management templates for create/update operations
- add common problem solutions for form development
- expand best practices with performance optimization and error handling sections
- update example code with more complete component imports and usage scenarios
yourname před 5 měsíci
rodič
revize
2e9c740b6b
1 změnil soubory, kde provedl 353 přidání a 5 odebrání
  1. 353 5
      .roo/commands/shadcn-manage-form.md

+ 353 - 5
.roo/commands/shadcn-manage-form.md

@@ -170,6 +170,289 @@ nickname: z.string().optional()
 </Dialog>
 ```
 
+## 高级表单组件模板
+
+### 头像选择器集成
+```typescript
+import AvatarSelector from '@/client/admin-shadcn/components/AvatarSelector';
+
+<FormField
+  control={form.control}
+  name="avatarFileId"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel>头像</FormLabel>
+      <FormControl>
+        <AvatarSelector
+          value={field.value || undefined}
+          onChange={(value) => field.onChange(value)}
+          maxSize={2}
+          uploadPath="/avatars"
+          uploadButtonText="上传头像"
+          previewSize="medium"
+          placeholder="选择头像"
+        />
+      </FormControl>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+### 下拉选择框
+```typescript
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/client/components/ui/select';
+
+<FormField
+  control={form.control}
+  name="status"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel>状态</FormLabel>
+      <Select onValueChange={field.onChange} defaultValue={String(field.value)}>
+        <FormControl>
+          <SelectTrigger>
+            <SelectValue placeholder="请选择状态" />
+          </SelectTrigger>
+        </FormControl>
+        <SelectContent>
+          <SelectItem value="0">启用</SelectItem>
+          <SelectItem value="1">禁用</SelectItem>
+        </SelectContent>
+      </Select>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+### 数字输入框
+```typescript
+<FormField
+  control={form.control}
+  name="age"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel>年龄</FormLabel>
+      <FormControl>
+        <Input 
+          type="number" 
+          placeholder="请输入年龄"
+          {...field}
+          onChange={(e) => field.onChange(Number(e.target.value))}
+        />
+      </FormControl>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+### 日期选择器
+```typescript
+import { Popover, PopoverContent, PopoverTrigger } from '@/client/components/ui/popover';
+import { Calendar } from '@/client/components/ui/calendar';
+import { CalendarIcon } from 'lucide-react';
+import { cn } from '@/client/lib/utils';
+
+<FormField
+  control={form.control}
+  name="birthDate"
+  render={({ field }) => (
+    <FormItem className="flex flex-col">
+      <FormLabel>出生日期</FormLabel>
+      <Popover>
+        <PopoverTrigger asChild>
+          <FormControl>
+            <Button
+              variant={"outline"}
+              className={cn(
+                "w-[240px] pl-3 text-left font-normal",
+                !field.value && "text-muted-foreground"
+              )}
+            >
+              {field.value ? (
+                format(field.value, "yyyy-MM-dd")
+              ) : (
+                <span>选择日期</span>
+              )}
+              <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
+            </Button>
+          </FormControl>
+        </PopoverTrigger>
+        <PopoverContent className="w-auto p-0" align="start">
+          <Calendar
+            mode="single"
+            selected={field.value}
+            onSelect={field.onChange}
+            disabled={(date) =>
+              date > new Date() || date < new Date("1900-01-01")
+            }
+            initialFocus
+          />
+        </PopoverContent>
+      </Popover>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+### 文本域
+```typescript
+import { Textarea } from '@/client/components/ui/textarea';
+
+<FormField
+  control={form.control}
+  name="description"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel>描述</FormLabel>
+      <FormControl>
+        <Textarea
+          placeholder="请输入描述信息"
+          className="resize-none"
+          {...field}
+        />
+      </FormControl>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+### 复选框组
+```typescript
+import { Checkbox } from '@/client/components/ui/checkbox';
+
+<FormField
+  control={form.control}
+  name="permissions"
+  render={() => (
+    <FormItem>
+      <div className="mb-4">
+        <FormLabel className="text-base">权限设置</FormLabel>
+        <FormDescription>
+          选择该用户拥有的权限
+        </FormDescription>
+      </div>
+      <div className="space-y-2">
+        {permissions.map((permission) => (
+          <FormField
+            key={permission.id}
+            control={form.control}
+            name="permissions"
+            render={({ field }) => {
+              return (
+                <FormItem
+                  key={permission.id}
+                  className="flex flex-row items-start space-x-3 space-y-0"
+                >
+                  <FormControl>
+                    <Checkbox
+                      checked={field.value?.includes(permission.id)}
+                      onCheckedChange={(checked) => {
+                        return checked
+                          ? field.onChange([...field.value, permission.id])
+                          : field.onChange(
+                              field.value?.filter(
+                                (value) => value !== permission.id
+                              )
+                            )
+                      }}
+                    />
+                  </FormControl>
+                  <div className="space-y-1 leading-none">
+                    <FormLabel>
+                      {permission.name}
+                    </FormLabel>
+                    <FormDescription>
+                      {permission.description}
+                    </FormDescription>
+                  </div>
+                </FormItem>
+              )
+            }}
+          />
+        ))}
+      </div>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+## 表单状态管理模板
+
+### 完整的表单状态管理
+```typescript
+const [isModalOpen, setIsModalOpen] = useState(false);
+const [editingData, setEditingData] = useState<any>(null);
+const [isCreateForm, setIsCreateForm] = useState(true);
+
+// 表单实例
+const createForm = useForm<CreateType>({
+  resolver: zodResolver(createSchema),
+  defaultValues: {/* 默认值 */}
+});
+
+const updateForm = useForm<UpdateType>({
+  resolver: zodResolver(updateSchema),
+  defaultValues: {/* 默认值 */}
+});
+
+// 打开创建表单
+const handleCreate = () => {
+  setEditingData(null);
+  setIsCreateForm(true);
+  createForm.reset({/* 创建时的默认值 */});
+  setIsModalOpen(true);
+};
+
+// 打开编辑表单
+const handleEdit = (data: any) => {
+  setEditingData(data);
+  setIsCreateForm(false);
+  updateForm.reset({
+    // 使用现有数据填充表单
+    ...data,
+    // 特殊处理可选字段
+    password: undefined, // 密码在更新时可选
+  });
+  setIsModalOpen(true);
+};
+
+// 提交处理
+const handleCreateSubmit = async (data: CreateType) => {
+  try {
+    const res = await apiClient.$post({ json: data });
+    if (res.status !== 201) throw new Error('创建失败');
+    toast.success('创建成功');
+    setIsModalOpen(false);
+    refetch(); // 刷新数据
+  } catch (error) {
+    toast.error('创建失败,请重试');
+  }
+};
+
+const handleUpdateSubmit = async (data: UpdateType) => {
+  if (!editingData) return;
+  
+  try {
+    const res = await apiClient[':id']['$put']({
+      param: { id: editingData.id },
+      json: data
+    });
+    if (res.status !== 200) throw new Error('更新失败');
+    toast.success('更新成功');
+    setIsModalOpen(false);
+    refetch(); // 刷新数据
+  } catch (error) {
+    toast.error('更新失败,请重试');
+  }
+};
+```
+
 ## 最佳实践
 
 ### 1. 表单验证
@@ -192,12 +475,22 @@ nickname: z.string().optional()
 - 表单间距统一使用 `space-y-4`
 - 移动端友好的布局
 
+### 5. 性能优化
+- 使用 React.memo 优化表单组件
+- 合理使用 useCallback 和 useMemo
+- 避免不必要的重新渲染
+
+### 6. 错误处理
+- 统一的错误处理机制
+- 友好的错误提示信息
+- 网络错误重试机制
+
 ## 使用示例
 
 ### 完整实现参考
 ```typescript
-// 创建用户
-const handleCreateSubmit = async (data: CreateUserFormData) => {
+// 创建记录
+const handleCreateSubmit = async (data: CreateFormData) => {
   try {
     const res = await apiClient.$post({ json: data });
     if (res.status !== 201) throw new Error('创建失败');
@@ -209,8 +502,8 @@ const handleCreateSubmit = async (data: CreateUserFormData) => {
   }
 };
 
-// 更新用户
-const handleUpdateSubmit = async (data: UpdateUserFormData) => {
+// 更新记录
+const handleUpdateSubmit = async (data: UpdateFormData) => {
   try {
     const res = await apiClient[':id']['$put']({
       param: { id: editingData.id },
@@ -233,11 +526,66 @@ import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, For
 import { Input } from '@/client/components/ui/input';
 import { Button } from '@/client/components/ui/button';
 import { Switch } from '@/client/components/ui/switch';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/client/components/ui/select';
+import { Textarea } from '@/client/components/ui/textarea';
+import { Checkbox } from '@/client/components/ui/checkbox';
 
 // 对话框相关
 import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
 
+// 高级组件
+import { Popover, PopoverContent, PopoverTrigger } from '@/client/components/ui/popover';
+import { Calendar } from '@/client/components/ui/calendar';
+import AvatarSelector from '@/client/admin-shadcn/components/AvatarSelector';
+
 // 表单工具
 import { useForm } from 'react-hook-form';
 import { zodResolver } from '@hookform/resolvers/zod';
-import { toast } from 'sonner';
+import { toast } from 'sonner';
+import { format } from 'date-fns';
+import { CalendarIcon } from 'lucide-react';
+import { cn } from '@/client/lib/utils';
+```
+
+## 常见问题解决方案
+
+### 1. 表单默认值问题
+```typescript
+// 正确处理 null/undefined 值
+defaultValues: {
+  name: null, // 允许 null
+  description: undefined, // 允许 undefined
+}
+```
+
+### 2. 数字类型转换
+```typescript
+// 在 onChange 中转换类型
+onChange={(e) => field.onChange(Number(e.target.value))}
+```
+
+### 3. 日期类型处理
+```typescript
+// 日期选择器返回值处理
+onSelect={(date) => field.onChange(date ? new Date(date) : null)}
+```
+
+### 4. 数组类型处理
+```typescript
+// 复选框组处理数组
+onCheckedChange={(checked) => {
+  const newValue = checked 
+    ? [...field.value, item.id] 
+    : field.value.filter(id => id !== item.id);
+  field.onChange(newValue);
+}}
+```
+
+### 5. 表单重置注意事项
+```typescript
+// 更新表单时正确重置
+updateForm.reset({
+  ...data,
+  password: undefined, // 密码字段特殊处理
+  confirmPassword: undefined,
+});