Browse Source

📝 docs(shadcn-manage-form-split): 更新表单分离文档内容

- 简化文档结构,突出核心表单分离模式
- 移除大量示例代码和详细说明,保留关键指导原则
- 明确指出应使用条件渲染方式分离创建和编辑表单
- 删除原有的核心特性、开发模板、最佳实践等详细章节
yourname 3 months ago
parent
commit
2c594b6bbb
1 changed files with 12 additions and 543 deletions
  1. 12 543
      .roo/commands/shadcn-manage-form-split.md

+ 12 - 543
.roo/commands/shadcn-manage-form-split.md

@@ -2,414 +2,19 @@
 description: "Shadcn-ui 管理页表单创建/编辑表单分离指令"
 ---
 
-将 创建/编辑表单分离, Form组件也分开
+将 创建/编辑表单分离
+不要 <Form {...(isCreateForm ? createForm : updateForm)}></Form>
+要     
+{isCreateForm ? (
+  // 创建表单(独立渲染)
+  <Form {...createForm}>
+  </Form>
+) : (
+  // 编辑表单(独立渲染)
+  <Form {...updateForm}>
+  </Form>
+)}
 
-## 核心特性
-
-### 1. 类型安全表单
-- **后端Schema复用**:直接使用后端定义的 Zod Schema
-- **RPC类型提取**:从 Hono 客户端自动推断类型
-- **一致的类型定义**:前后端类型完全同步
-
-### 2. 表单状态管理(推荐:创建/编辑表单分离模式)
-- **分离表单实例**:为创建和编辑分别使用独立的表单实例
-- **类型安全**:创建使用CreateSchema,编辑使用UpdateSchema,避免类型冲突
-- **字段差异处理**:创建时的必填字段在编辑时变为可选,敏感字段特殊处理
-- **状态隔离**:两种模式的状态完全独立,避免交叉污染
-
-### 3. 统一的UI组件模式
-- **Shadcn-ui组件集成**:使用标准的 Shadcn-ui 表单组件
-- **响应式布局**:适配不同屏幕尺寸
-- **无障碍支持**:完整的 ARIA 属性支持
-
-## 开发模板
-
-### 基础结构模板(创建/编辑分离模式)
-```typescript
-// 1. 类型定义(使用后端真实类型)
-import { useForm } from 'react-hook-form';
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
-import { Input } from '@/client/components/ui/input';
-import { Button } from '@/client/components/ui/button';
-import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
-
-// 2. 分离的表单配置
-const [isCreateForm, setIsCreateForm] = useState(true);
-const [editingEntity, setEditingEntity] = useState<any>(null); // 用于存储编辑时的实体数据
-
-// 3. 独立的表单实例
-const createForm = useForm<CreateRequest>({
-  resolver: zodResolver(CreateEntityDto), // 使用创建专用的Schema
-  defaultValues: {
-    // 创建时必填字段的默认值
-  },
-});
-
-const updateForm = useForm<UpdateRequest>({
-  resolver: zodResolver(UpdateEntityDto), // 使用更新专用的Schema
-  defaultValues: {
-    // 更新时可选字段的默认值(会被实际数据覆盖)
-  },
-});
-
-// 4. 表单切换逻辑(核心模式)
-const handleCreateEntity = () => {
-  setEditingEntity(null);
-  setIsCreateForm(true);
-  createForm.reset({
-    // 创建时的初始值(必填字段必须有值)
-  });
-  setIsModalOpen(true);
-};
-
-const handleEditEntity = (entity: EntityResponse) => {
-  setEditingEntity(entity);
-  setIsCreateForm(false);
-  updateForm.reset({
-    ...entity,
-    // 特殊处理:敏感字段在编辑时设为可选
-    password: undefined, // 密码在更新时可选,不修改则留空
-    // 其他需要特殊处理的字段
-  });
-  setIsModalOpen(true);
-};
-```
-
-### 表单字段模板
-
-#### 文本输入框
-```typescript
-<FormField
-  control={form.control}
-  name="username"
-  render={({ field }) => (
-    <FormItem>
-      <FormLabel className="flex items-center">
-        用户名
-        <span className="text-red-500 ml-1">*</span>
-      </FormLabel>
-      <FormControl>
-        <Input placeholder="请输入用户名" {...field} />
-      </FormControl>
-      <FormMessage />
-    </FormItem>
-  )}
-/>
-```
-
-#### 邮箱输入框
-```typescript
-<FormField
-  control={form.control}
-  name="email"
-  render={({ field }) => (
-    <FormItem>
-      <FormLabel>邮箱</FormLabel>
-      <FormControl>
-        <Input type="email" placeholder="请输入邮箱" {...field} />
-      </FormControl>
-      <FormMessage />
-    </FormItem>
-  )}
-/>
-```
-
-#### 密码输入框
-```typescript
-<FormField
-  control={form.control}
-  name="password"
-  render={({ field }) => (
-    <FormItem>
-      <FormLabel className="flex items-center">
-        密码
-        <span className="text-red-500 ml-1">*</span>
-      </FormLabel>
-      <FormControl>
-        <Input type="password" placeholder="请输入密码" {...field} />
-      </FormControl>
-      <FormMessage />
-    </FormItem>
-  )}
-/>
-```
-
-#### 开关控件(布尔值)
-```typescript
-<FormField
-  control={form.control}
-  name="isDisabled"
-  render={({ field }) => (
-    <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
-      <div className="space-y-0.5">
-        <FormLabel className="text-base">状态</FormLabel>
-        <FormDescription>
-          禁用后用户将无法登录系统
-        </FormDescription>
-      </div>
-      <FormControl>
-        <Switch
-          checked={field.value === 1}
-          onCheckedChange={(checked) => field.onChange(checked ? 1 : 0)}
-        />
-      </FormControl>
-    </FormItem>
-  )}
-/>
-```
-
-#### 可选字段处理
-```typescript
-// 创建时:必须提供值
-nickname: z.string().optional()
-
-// 更新时:完全可选
-nickname: z.string().optional()
-```
-
-### 对话框集成模板
-```typescript
-<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
-  <DialogContent className="sm:max-w-[500px]">
-    <DialogHeader>
-      <DialogTitle>{isCreateForm ? '创建' : '编辑'}用户</DialogTitle>
-      <DialogDescription>
-        {isCreateForm ? '创建新记录' : '编辑现有记录'}
-      </DialogDescription>
-    </DialogHeader>
-    
-    <Form {...(isCreateForm ? createForm : updateForm)}>
-      <form onSubmit={form.handleSubmit(isCreateForm ? handleCreate : handleUpdate)} className="space-y-4">
-        {/* 表单字段 */}
-        
-        <DialogFooter>
-          <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
-            取消
-          </Button>
-          <Button type="submit">
-            {isCreateForm ? '创建' : '更新'}
-          </Button>
-        </DialogFooter>
-      </form>
-    </Form>
-  </DialogContent>
-</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>
-  )}
-/>
-```
-
-## 表单状态管理模板
 
 ### 创建/编辑表单分离模式(推荐)
 
@@ -612,139 +217,3 @@ const handleUpdateSubmit = async (data: UpdateRequest) => {
 </Dialog>
 ```
 
-## 最佳实践
-
-### 1. 表单验证
-- 使用 Zod Schema 进行类型验证
-- 必填字段标记红色星号
-- 提供清晰的错误提示
-
-### 2. 用户体验
-- 表单提交时显示加载状态
-- 操作成功后显示 toast 通知
-- 支持键盘导航和提交
-
-### 3. 数据管理
-- 创建后自动刷新数据列表
-- 编辑时回填现有数据
-- 支持表单重置功能
-
-### 4. 响应式设计
-- 对话框最大宽度 `sm:max-w-[500px]`
-- 表单间距统一使用 `space-y-4`
-- 移动端友好的布局
-
-### 5. 性能优化
-- 使用 React.memo 优化表单组件
-- 合理使用 useCallback 和 useMemo
-- 避免不必要的重新渲染
-
-### 6. 错误处理
-- 统一的错误处理机制
-- 友好的错误提示信息
-- 网络错误重试机制
-
-## 使用示例
-
-### 完整实现参考
-```typescript
-// 创建记录
-const handleCreateSubmit = async (data: CreateFormData) => {
-  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: UpdateFormData) => {
-  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('更新失败,请重试');
-  }
-};
-```
-
-## 组件导入清单
-```typescript
-// 表单相关
-import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
-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 { 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,
-});