---
description: "Shadcn-ui 管理页表单开发指令"
---

## 概述
基于 `src/client/admin-shadcn/pages/Users.tsx` 中用户管理表单的实现，提取可复用的表单开发模式和最佳实践，适用于基于 Shadcn-ui 的管理后台表单开发。

## 核心特性

### 1. 类型安全表单
- **后端Schema复用**：直接使用后端定义的 Zod Schema
- **RPC类型提取**：从 Hono 客户端自动推断类型
- **一致的类型定义**：前后端类型完全同步

### 2. 表单状态管理（推荐：创建/编辑表单分离模式）
- **分离表单实例**：为创建和编辑分别使用独立的表单实例, Form组件也分开
- **类型安全**：创建使用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
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>
  )}
/>
```

## 表单状态管理模板

### 创建/编辑表单分离模式（推荐）

基于 `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 [editingEntity, setEditingEntity] = useState<EntityResponse | null>(null);
const [isCreateForm, setIsCreateForm] = useState(true);

// 3. 分离的表单实例
const createForm = useForm<CreateRequest>({
  resolver: zodResolver(CreateEntityDto),
  defaultValues: {
    // 创建时必须提供的默认值
  },
});

const updateForm = useForm<UpdateRequest>({
  resolver: zodResolver(UpdateEntityDto),
  defaultValues: {
    // 更新时的默认值（会被实际数据覆盖）
  },
});

// 4. 表单操作函数
const handleCreate = () => {
  setEditingEntity(null);
  setIsCreateForm(true);
  createForm.reset({
    // 创建时的初始值
    status: 1, // 示例：默认启用
  });
  setIsModalOpen(true);
};

const handleEdit = (entity: EntityResponse) => {
  setEditingEntity(entity);
  setIsCreateForm(false);
  updateForm.reset({
    ...entity,
    // 关键：处理创建/编辑字段差异
    password: undefined, // 密码在更新时可选
    confirmPassword: undefined,
    // 其他需要特殊处理的字段
  });
  setIsModalOpen(true);
};

// 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();
  } catch (error) {
    toast.error('创建失败，请重试');
  }
};

const handleUpdateSubmit = async (data: UpdateRequest) => {
  if (!editingEntity) return;
  
  try {
    const res = await apiClient[':id']['$put']({
      param: { id: editingEntity.id },
      json: data
    });
    if (res.status !== 200) throw new Error('更新失败');
    toast.success('更新成功');
    setIsModalOpen(false);
    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. 表单验证
- 使用 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,
});