Преглед изворни кода

📝 docs(shadcn): 优化表单开发指南,推荐创建/编辑表单分离模式

- 重构表单状态管理章节,主推创建/编辑表单分离模式
- 增加分离模式优势说明:类型安全、字段差异处理、敏感字段特殊处理、状态隔离
- 提供完整的分离模式实现模板,包括类型定义、表单实例、切换逻辑
- 更新模态框表单模板,实现创建/编辑表单独立渲染
- 完善表单字段处理示例,明确必填字段标记和敏感字段处理方式
- 修复原模板中类型定义不明确和状态管理混乱的问题
yourname пре 7 месеци
родитељ
комит
496d4efcdc
2 измењених фајлова са 230 додато и 56 уклоњено
  1. 200 40
      .roo/commands/shadcn-manage-form.md
  2. 30 16
      .roo/commands/shadcn-manage-page.md

+ 200 - 40
.roo/commands/shadcn-manage-form.md

@@ -12,10 +12,11 @@ description: "Shadcn-ui 管理页表单开发指令"
 - **RPC类型提取**:从 Hono 客户端自动推断类型
 - **一致的类型定义**:前后端类型完全同步
 
-### 2. 表单状态管理
-- **创建/编辑模式切换**:单一表单处理两种状态
-- **智能默认值**:根据模式自动设置表单初始值
-- **表单重置**:模式切换时自动重置表单状态
+### 2. 表单状态管理(推荐:创建/编辑表单分离模式)
+- **分离表单实例**:为创建和编辑分别使用独立的表单实例
+- **类型安全**:创建使用CreateSchema,编辑使用UpdateSchema,避免类型冲突
+- **字段差异处理**:创建时的必填字段在编辑时变为可选,敏感字段特殊处理
+- **状态隔离**:两种模式的状态完全独立,避免交叉污染
 
 ### 3. 统一的UI组件模式
 - **Shadcn-ui组件集成**:使用标准的 Shadcn-ui 表单组件
@@ -24,9 +25,9 @@ description: "Shadcn-ui 管理页表单开发指令"
 
 ## 开发模板
 
-### 基础结构模板
+### 基础结构模板(创建/编辑分离模式)
 ```typescript
-// 1. 类型定义
+// 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';
@@ -34,19 +35,46 @@ 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. 表单配置
+// 2. 分离的表单配置
 const [isCreateForm, setIsCreateForm] = useState(true);
-const [editingData, setEditingData] = useState<any>(null);
-
-const createForm = useForm<CreateType>({
-  resolver: zodResolver(createSchema),
-  defaultValues: {/* 创建时的默认值 */},
+const [editingEntity, setEditingEntity] = useState<any>(null); // 用于存储编辑时的实体数据
+
+// 3. 独立的表单实例
+const createForm = useForm<CreateRequest>({
+  resolver: zodResolver(CreateEntityDto), // 使用创建专用的Schema
+  defaultValues: {
+    // 创建时必填字段的默认值
+  },
 });
 
-const updateForm = useForm<UpdateType>({
-  resolver: zodResolver(updateSchema),
-  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);
+};
 ```
 
 ### 表单字段模板
@@ -384,75 +412,207 @@ import { Checkbox } from '@/client/components/ui/checkbox';
 
 ## 表单状态管理模板
 
-### 完整的表单状态管理
+### 创建/编辑表单分离模式(推荐)
+
+基于 `src/client/admin-shadcn/pages/Users.tsx` 的最佳实践,创建/编辑表单分离模式通过以下方式解决创建/编辑时数据对象类型差异问题:
+
+#### 核心优势
+1. **类型安全**:创建使用 `CreateEntityDto`,编辑使用 `UpdateEntityDto`,避免类型冲突
+2. **字段差异处理**:创建时的必填字段在编辑时自动变为可选
+3. **敏感字段处理**:密码等敏感字段在编辑时可设为可选
+4. **状态隔离**:两种模式完全独立,避免状态污染
+
+#### 完整实现模板
 ```typescript
+// 1. 类型定义(使用真实后端类型)
+type CreateRequest = InferRequestType<typeof apiClient.$post>['json'];
+type UpdateRequest = InferRequestType<typeof apiClient[':id']['$put']>['json'];
+type EntityResponse = InferResponseType<typeof apiClient.$get, 200>['data'][0];
+
+// 2. 状态管理
 const [isModalOpen, setIsModalOpen] = useState(false);
-const [editingData, setEditingData] = useState<any>(null);
+const [editingEntity, setEditingEntity] = useState<EntityResponse | null>(null);
 const [isCreateForm, setIsCreateForm] = useState(true);
 
-// 表单实例
-const createForm = useForm<CreateType>({
-  resolver: zodResolver(createSchema),
-  defaultValues: {/* 默认值 */}
+// 3. 分离的表单实例
+const createForm = useForm<CreateRequest>({
+  resolver: zodResolver(CreateEntityDto),
+  defaultValues: {
+    // 创建时必须提供的默认值
+  },
 });
 
-const updateForm = useForm<UpdateType>({
-  resolver: zodResolver(updateSchema),
-  defaultValues: {/* 默认值 */}
+const updateForm = useForm<UpdateRequest>({
+  resolver: zodResolver(UpdateEntityDto),
+  defaultValues: {
+    // 更新时的默认值(会被实际数据覆盖)
+  },
 });
 
-// 打开创建表单
+// 4. 表单操作函数
 const handleCreate = () => {
-  setEditingData(null);
+  setEditingEntity(null);
   setIsCreateForm(true);
-  createForm.reset({/* 创建时的默认值 */});
+  createForm.reset({
+    // 创建时的初始值
+    status: 1, // 示例:默认启用
+  });
   setIsModalOpen(true);
 };
 
-// 打开编辑表单
-const handleEdit = (data: any) => {
-  setEditingData(data);
+const handleEdit = (entity: EntityResponse) => {
+  setEditingEntity(entity);
   setIsCreateForm(false);
   updateForm.reset({
-    // 使用现有数据填充表单
-    ...data,
-    // 特殊处理可选字段
+    ...entity,
+    // 关键:处理创建/编辑字段差异
     password: undefined, // 密码在更新时可选
+    confirmPassword: undefined,
+    // 其他需要特殊处理的字段
   });
   setIsModalOpen(true);
 };
 
-// 提交处理
-const handleCreateSubmit = async (data: CreateType) => {
+// 5. 提交处理
+const handleCreateSubmit = async (data: CreateRequest) => {
   try {
     const res = await apiClient.$post({ json: data });
     if (res.status !== 201) throw new Error('创建失败');
     toast.success('创建成功');
     setIsModalOpen(false);
-    refetch(); // 刷新数据
+    refetch();
   } catch (error) {
     toast.error('创建失败,请重试');
   }
 };
 
-const handleUpdateSubmit = async (data: UpdateType) => {
-  if (!editingData) return;
+const handleUpdateSubmit = async (data: UpdateRequest) => {
+  if (!editingEntity) return;
   
   try {
     const res = await apiClient[':id']['$put']({
-      param: { id: editingData.id },
+      param: { id: editingEntity.id },
       json: data
     });
     if (res.status !== 200) throw new Error('更新失败');
     toast.success('更新成功');
     setIsModalOpen(false);
-    refetch(); // 刷新数据
+    refetch();
   } catch (error) {
     toast.error('更新失败,请重试');
   }
 };
 ```
 
+#### 对话框渲染模板
+```tsx
+<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
+  <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
+    <DialogHeader>
+      <DialogTitle>{isCreateForm ? '创建实体' : '编辑实体'}</DialogTitle>
+      <DialogDescription>
+        {isCreateForm ? '创建一个新的实体' : '编辑现有实体信息'}
+      </DialogDescription>
+    </DialogHeader>
+    
+    {isCreateForm ? (
+      // 创建表单(独立渲染)
+      <Form {...createForm}>
+        <form onSubmit={createForm.handleSubmit(handleCreateSubmit)} className="space-y-4">
+          {/* 创建专用字段 - 必填 */}
+          <FormField
+            control={createForm.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>
+            )}
+          />
+          
+          {/* 创建时必填的密码 */}
+          <FormField
+            control={createForm.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>
+            )}
+          />
+          
+          <DialogFooter>
+            <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+              取消
+            </Button>
+            <Button type="submit">创建</Button>
+          </DialogFooter>
+        </form>
+      </Form>
+    ) : (
+      // 编辑表单(独立渲染)
+      <Form {...updateForm}>
+        <form onSubmit={updateForm.handleSubmit(handleUpdateSubmit)} className="space-y-4">
+          {/* 编辑专用字段 - 可选 */}
+          <FormField
+            control={updateForm.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>
+            )}
+          />
+          
+          {/* 编辑时可选的密码 */}
+          <FormField
+            control={updateForm.control}
+            name="password"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel>新密码</FormLabel>
+                <FormControl>
+                  <Input type="password" placeholder="留空则不修改密码" {...field} />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+          
+          <DialogFooter>
+            <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+              取消
+            </Button>
+            <Button type="submit">更新</Button>
+          </DialogFooter>
+        </form>
+      </Form>
+    )}
+  </DialogContent>
+</Dialog>
+```
+
 ## 最佳实践
 
 ### 1. 表单验证

+ 30 - 16
.roo/commands/shadcn-manage-page.md

@@ -185,30 +185,44 @@ const updateForm = useForm<UpdateRequest>({
 });
 ```
 
-### 2. 模态框表单
+### 2. 模态框表单(创建/编辑分离模式)
 ```tsx
 <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
   <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
     <DialogHeader>
-      <DialogTitle>{editingEntity ? '编辑实体' : '创建实体'}</DialogTitle>
+      <DialogTitle>{isCreateForm ? '创建实体' : '编辑实体'}</DialogTitle>
       <DialogDescription>
-        {editingEntity ? '编辑现有实体信息' : '创建一个新的实体'}
+        {isCreateForm ? '创建一个新的实体' : '编辑现有实体信息'}
       </DialogDescription>
     </DialogHeader>
     
-    <Form {...(isCreateForm ? createForm : updateForm)}>
-      <form onSubmit={...} className="space-y-4">
-        {/* 表单字段 */}
-        <DialogFooter>
-          <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
-            取消
-          </Button>
-          <Button type="submit">
-            提交
-          </Button>
-        </DialogFooter>
-      </form>
-    </Form>
+    {isCreateForm ? (
+      // 创建表单
+      <Form {...createForm}>
+        <form onSubmit={createForm.handleSubmit(handleCreateSubmit)} className="space-y-4">
+          {/* 创建专用字段 */}
+          <DialogFooter>
+            <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+              取消
+            </Button>
+            <Button type="submit">创建</Button>
+          </DialogFooter>
+        </form>
+      </Form>
+    ) : (
+      // 编辑表单
+      <Form {...updateForm}>
+        <form onSubmit={updateForm.handleSubmit(handleUpdateSubmit)} className="space-y-4">
+          {/* 编辑专用字段 */}
+          <DialogFooter>
+            <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+              取消
+            </Button>
+            <Button type="submit">更新</Button>
+          </DialogFooter>
+        </form>
+      </Form>
+    )}
   </DialogContent>
 </Dialog>
 ```