瀏覽代碼

📝 docs(roo): 添加项目开发指令文档

- 新增通用CRUD开发规范文档模板
- 添加小程序端开发指令集合
- 补充管理后台表单开发规范
- 完善文件关联和用户认证相关文档
yourname 4 月之前
父節點
當前提交
130c4c3baf

+ 6 - 0
roo/check-api-entity-schema.md

@@ -0,0 +1,6 @@
+---
+description: "检查api相关的实体,schema 与 页面字段是否对应指令"
+---
+
+检查api相关的实体,schema 定义,看与 页面字段是否对应
+没对应的就修改页面字段以与实体,schema对应

+ 5 - 0
roo/check-curd-entity-schema.md

@@ -0,0 +1,5 @@
+---
+description: "检查页面相关的实体,schema, CRUD路由指令"
+---
+
+检查页面相关的实体,schema, CRUD路由 定义,以收集进行页面开发所需的上下文

+ 152 - 0
roo/download-file-from-url.md

@@ -0,0 +1,152 @@
+---
+description: "从任意URL下载文件并保存到MinIO,同时创建数据库记录"
+---
+
+# FileService.downloadAndSaveFromUrl 使用指令
+
+## 功能概述
+`downloadAndSaveFromUrl` 是 FileService 中新增的统一方法,用于从任意URL下载文件并保存到MinIO,同时创建数据库记录。
+
+## 基本使用
+
+### 导入服务
+```typescript
+import { FileService } from '@/server/modules/files/file.service';
+import { DataSource } from 'typeorm';
+
+// 在服务中注入
+const fileService = new FileService(dataSource);
+```
+
+### 基础调用
+```typescript
+const result = await fileService.downloadAndSaveFromUrl(
+  'https://example.com/image.jpg',
+  {
+    uploadUserId: 123, // 必需:上传用户ID
+  }
+);
+// 返回: { file: File实体, url: 文件访问URL }
+```
+
+## 参数说明
+
+### 必填参数
+| 参数 | 类型 | 说明 |
+|------|------|------|
+| url | string | 要下载的文件URL |
+| fileData.uploadUserId | number | 上传用户的ID |
+
+### 可选参数
+| 参数 | 类型 | 默认值 | 说明 |
+|------|------|--------|------|
+| fileData.mimeType | string | 自动推断 | 文件MIME类型 |
+| fileData.customFileName | string | 自动获取 | 自定义文件名 |
+| fileData.customPath | string | 自动生成 | 自定义存储路径 |
+| options.timeout | number | 30000 | 下载超时时间(ms) |
+| options.retries | number | 0 | 重试次数 |
+
+## 使用场景示例
+
+### 1. 下载用户头像
+```typescript
+const avatarFile = await fileService.downloadAndSaveFromUrl(
+  'https://thirdwx.qlogo.cn/mmopen/vi_32/xxx/132',
+  {
+    uploadUserId: userId,
+    customPath: 'avatars/',
+    mimeType: 'image/jpeg'
+  }
+);
+```
+
+### 2. 下载文档附件
+```typescript
+const docFile = await fileService.downloadAndSaveFromUrl(
+  'https://example.com/report.pdf',
+  {
+    uploadUserId: userId,
+    customFileName: 'monthly-report.pdf',
+    customPath: 'documents/reports/'
+  }
+);
+```
+
+### 3. 批量下载图片
+```typescript
+const imageUrls = ['url1.jpg', 'url2.png', 'url3.gif'];
+const results = await Promise.all(
+  imageUrls.map(url => 
+    fileService.downloadAndSaveFromUrl(url, { uploadUserId: userId })
+  )
+);
+```
+
+## 错误处理
+
+### 异常类型
+- `从URL下载文件失败`: 网络或服务器错误
+- `文件保存失败`: MinIO存储或数据库错误
+
+### 使用示例
+```typescript
+try {
+  const result = await fileService.downloadAndSaveFromUrl(url, { uploadUserId });
+  return result.file.id;
+} catch (error) {
+  console.error('下载失败:', error.message);
+  return null; // 或抛出异常
+}
+```
+
+## 高级配置
+
+### 自定义文件名和路径
+```typescript
+await fileService.downloadAndSaveFromUrl(
+  'https://cdn.example.com/avatar.png',
+  {
+    uploadUserId: 1001,
+    customFileName: 'user-1001-avatar.png',
+    customPath: 'users/1001/profile/'
+  }
+);
+```
+
+### 设置超时时间
+```typescript
+await fileService.downloadAndSaveFromUrl(
+  'https://large-file.example.com/video.mp4',
+  {
+    uploadUserId: userId
+  },
+  {
+    timeout: 60000, // 60秒超时
+    retries: 2      // 重试2次
+  }
+);
+```
+
+## 返回值结构
+```typescript
+{
+  file: {
+    id: number,
+    name: string,
+    path: string,
+    size: number,
+    mimeType: string,
+    url: string,
+    // ...其他File实体字段
+  },
+  url: string // MinIO访问URL
+}
+```
+
+## 注意事项
+
+1. **网络要求**: 确保服务器能够访问目标URL
+2. **文件大小**: 大文件下载可能超时,可调整timeout参数
+3. **文件名冲突**: 系统自动添加UUID避免冲突
+4. **MIME类型**: 优先使用提供的mimeType,否则自动推断
+5. **错误日志**: 所有错误都会记录详细日志便于调试

+ 138 - 0
roo/file-relation.md

@@ -0,0 +1,138 @@
+---
+description: "实体与文件关联开发指令 - 指导如何在实体中添加文件关联字段"
+---
+
+# 实体与文件关联开发指令
+
+## 概述
+本指令指导如何在实体中添加与文件管理系统的关联关系,基于用户实体(UserEntity)的最佳实践。
+
+## 文件关联标准实现
+
+### 1. 实体定义规范
+
+在实体类中添加文件关联字段:
+
+```typescript
+// 1. 导入文件实体
+import { File } from '@/server/modules/files/file.entity';
+
+// 2. 在实体类中添加字段
+@Column({ 
+  name: '{field_prefix}_file_id', 
+  type: 'int', 
+  unsigned: true, 
+  nullable: true, 
+  comment: '{描述}文件ID' 
+})
+{fieldPrefix}FileId!: number | null;
+
+@ManyToOne(() => File, { nullable: true })
+@JoinColumn({ 
+  name: '{field_prefix}_file_id', 
+  referencedColumnName: 'id' 
+})
+{fieldPrefix}File!: File | null;
+```
+
+### 2. Schema定义规范
+
+在实体的schema文件中添加文件关联字段:
+
+```typescript
+// 基础字段定义
+{fieldPrefix}FileId: z.number()
+  .int()
+  .positive()
+  .nullable()
+  .openapi({
+    example: 1,
+    description: '{描述}文件ID'
+  }),
+
+// 关联文件对象(用于响应)
+{fieldPrefix}File: z.object({
+  id: z.number().int().positive().openapi({ description: '文件ID' }),
+  name: z.string().max(255).openapi({ description: '文件名', example: 'example.jpg' }),
+  fullUrl: z.string().openapi({ description: '文件完整URL', example: 'https://example.com/file.jpg' }),
+  type: z.string().nullable().openapi({ description: '文件类型', example: 'image/jpeg' }),
+  size: z.number().nullable().openapi({ description: '文件大小(字节)', example: 102400 })
+}).nullable().optional().openapi({
+  description: '{描述}文件信息'
+}),
+```
+
+### 3. 命名规范
+
+| 类型 | 命名格式 | 示例 |
+|------|----------|------|
+| 数据库字段 | `{前缀}_file_id` | `avatar_file_id` |
+| 实体字段 | `{前缀}FileId` | `avatarFileId` |
+| 关联实体 | `{前缀}File` | `avatarFile` |
+| 描述注释 | `{描述}文件ID` | `头像文件ID` |
+
+### 4. 完整示例 - 基于用户实体
+
+**实体类** ([`src/server/modules/users/user.entity.ts`](src/server/modules/users/user.entity.ts:29-34)):
+```typescript
+@Column({ name: 'avatar_file_id', type: 'int', unsigned: true, nullable: true, comment: '头像文件ID' })
+avatarFileId!: number | null;
+
+@ManyToOne(() => File, { nullable: true })
+@JoinColumn({ name: 'avatar_file_id', referencedColumnName: 'id' })
+avatarFile!: File | null;
+```
+
+**Schema定义** ([`src/server/modules/users/user.schema.ts`](src/server/modules/users/user.schema.ts:33-45)):
+```typescript
+avatarFileId: z.number().int().positive().nullable().openapi({
+  example: 1,
+  description: '头像文件ID'
+}),
+avatarFile: z.object({
+  id: z.number().int().positive().openapi({ description: '文件ID' }),
+  name: z.string().max(255).openapi({ description: '文件名', example: 'avatar.jpg' }),
+  fullUrl: z.string().openapi({ description: '文件完整URL', example: 'https://example.com/avatar.jpg' }),
+  type: z.string().nullable().openapi({ description: '文件类型', example: 'image/jpeg' }),
+  size: z.number().nullable().openapi({ description: '文件大小(字节)', example: 102400 })
+}).nullable().optional().openapi({
+  description: '头像文件信息'
+}),
+```
+
+## 使用步骤
+
+### 步骤1: 添加实体字段
+1. 在实体类中添加 `{prefix}FileId` 和 `{prefix}File` 字段
+2. 使用 `@ManyToOne` 关联 `File` 实体
+3. 配置 `@JoinColumn` 指定外键字段名
+
+### 步骤2: 添加Schema定义
+1. 在 `Create{Entity}Dto` 中添加 `{prefix}FileId` 字段
+2. 在 `Update{Entity}Dto` 中添加可选的 `{prefix}FileId` 字段
+3. 在实体Schema中添加 `{prefix}File` 对象用于响应
+
+### 步骤3: 数据库迁移
+确保数据库表包含 `{prefix}_file_id` 字段,类型为 INT UNSIGNED NULL
+
+## 使用场景
+
+### 场景1: 单文件关联
+适用于实体只需要关联单个文件的情况,如:
+- 用户头像
+- 商品封面图
+- 文档附件
+
+### 场景2: 多文件关联
+如需关联多个文件,请使用 ManyToMany 关联,参考广告实体的实现。
+
+## 注意事项
+
+1. **空值处理**: 字段必须支持 `null`,允许无文件关联
+2. **级联操作**: 默认不级联,删除文件不会影响关联实体
+3. **文件验证**: 前端需先上传文件获取文件ID,再进行实体关联
+4. **类型安全**: 确保所有字段类型定义一致(number | null)
+
+## 扩展说明
+
+此标准基于用户实体的头像文件关联实现,适用于项目中所有需要文件关联的实体。后续实体按此标准实现即可保持统一性。

+ 12 - 0
roo/generic-crud-public-create.md

@@ -0,0 +1,12 @@
+---
+description: "通用curd公共只读路由创建指令"
+---
+
+按通用curd开发规范进行公共只读路由创建
+
+步骤
+在src/server/api/public下创建公共只读路由
+注册路由到API   src/server/api.ts
+创建客户端API调用方法   src/client/api.ts mini/src/api.ts
+
+注意: 需要 public 前缀

+ 11 - 0
roo/generic-crud-public-reg.md

@@ -0,0 +1,11 @@
+---
+description: "通用curd公共只读路由注册指令"
+---
+
+按通用curd开发规范进行公共只读路由注册
+
+
+注册路由到API   src/server/api.ts
+创建客户端API调用方法   src/client/api.ts  mini/src/api.ts
+
+注意: 需要 public 前缀

+ 9 - 0
roo/generic-crud-reg.md

@@ -0,0 +1,9 @@
+---
+description: "通用curd路由注册指令"
+---
+
+按通用curd开发规范进行路由注册
+
+
+注册路由到API   src/server/api.ts
+创建客户端API调用方法   src/client/api.ts  mini/src/api.ts

+ 15 - 0
roo/generic-crud.md

@@ -0,0 +1,15 @@
+---
+description: "通用curd开发指令"
+---
+
+按通用curd开发规范开发
+
+创建实体类 your-entity.entity.ts
+创建实体Zod Schema定义 your-entity.schema.ts
+注册实体到数据源
+创建服务类继承GenericCrudService
+创建通用CRUD路由
+注册路由到API
+创建客户端API调用方法
+创建管理后台页面,按照 .roo/commands/shadcn-manage-form.md 指令规范
+注册路由和菜单

+ 203 - 0
roo/mini-auth.md

@@ -0,0 +1,203 @@
+---
+description: "小程序 useAuth hook 使用指南"
+---
+
+# useAuth Hook 使用指南
+
+## 基本导入
+
+```typescript
+import { useAuth } from '@/utils/auth'
+```
+
+## 使用方式
+
+在组件中使用:
+
+```typescript
+const { user, login, logout, register, updateUser, isLoading, isLoggedIn } = useAuth()
+```
+
+## API 说明
+
+| 属性 | 类型 | 说明 |
+|------|------|------|
+| `user` | `User \| null` | 当前登录用户信息 |
+| `login` | `(data: LoginRequest) => Promise<User>` | 登录函数 |
+| `logout` | `() => Promise<void>` | 退出登录函数 |
+| `register` | `(data: RegisterRequest) => Promise<User>` | 注册函数 |
+| `updateUser` | `(userData: Partial<User>) => Promise<User>` | 更新用户信息 |
+| `isLoading` | `boolean` | 是否正在加载 |
+| `isLoggedIn` | `boolean` | 是否已登录 |
+
+## 使用示例
+
+### 获取用户信息
+
+```typescript
+const ProfilePage = () => {
+  const { user, isLoading } = useAuth()
+
+  if (isLoading) {
+    return <View>加载中...</View>
+  }
+
+  if (!user) {
+    return <View>请先登录</View>
+  }
+
+  return (
+    <View>
+      <Text>用户名: {user.username}</Text>
+      <Text>邮箱: {user.email}</Text>
+    </View>
+  )
+}
+```
+
+### 处理登录
+
+```typescript
+const LoginPage = () => {
+  const { login } = useAuth()
+  
+  const handleLogin = async (formData) => {
+    try {
+      const user = await login({
+        username: formData.username,
+        password: formData.password
+      })
+      // 登录成功后的处理
+    } catch (error) {
+      // 处理登录错误
+    }
+  }
+}
+```
+
+### 处理注册
+
+```typescript
+const RegisterPage = () => {
+  const { register } = useAuth()
+  
+  const handleRegister = async (formData) => {
+    try {
+      const user = await register({
+        username: formData.username,
+        password: formData.password,
+        email: formData.email
+      })
+      // 注册成功后的处理
+    } catch (error) {
+      // 处理注册错误
+    }
+  }
+}
+```
+
+### 处理退出登录
+
+```typescript
+const ProfilePage = () => {
+  const { logout } = useAuth()
+  
+  const handleLogout = async () => {
+    try {
+      await logout()
+      // 退出成功后会自动跳转到登录页
+    } catch (error) {
+      // 处理退出错误
+    }
+  }
+}
+```
+
+### 更新用户信息
+
+```typescript
+const ProfilePage = () => {
+  const { user, updateUser } = useAuth()
+  
+  const handleUpdateAvatar = async (avatarFileId) => {
+    if (user) {
+      const updatedUser = {
+        ...user,
+        avatarFileId: avatarFileId
+      }
+      await updateUser(updatedUser)
+    }
+  }
+}
+```
+
+### 完整示例
+
+```typescript
+const ProfilePage = () => {
+  const { user, logout, isLoading, updateUser } = useAuth()
+  
+  const handleLogout = async () => {
+    try {
+      await Taro.showModal({
+        title: '退出登录',
+        content: '确定要退出登录吗?',
+        success: async (res) => {
+          if (res.confirm) {
+            await logout()
+            // 退出成功后会自动跳转到登录页
+          }
+        }
+      })
+    } catch (error) {
+      // 处理错误
+    }
+  }
+
+  const handleAvatarUpload = async (result) => {
+    try {
+      if (user) {
+        const updatedUser = {
+          ...user,
+          avatarFileId: result.fileId
+        }
+        await updateUser(updatedUser)
+        Taro.showToast({ title: '头像更新成功', icon: 'success' })
+      }
+    } catch (error) {
+      Taro.showToast({ title: '更新失败', icon: 'none' })
+    }
+  }
+
+  if (isLoading) {
+    return <View className="flex justify-center items-center h-screen">加载中...</View>
+  }
+
+  if (!user) {
+    return (
+      <View className="flex justify-center items-center h-screen">
+        <Text className="mb-4">请先登录</Text>
+        <Button onClick={() => Taro.navigateTo({ url: '/pages/login/index' })}>
+          去登录
+        </Button>
+      </View>
+    )
+  }
+
+  return (
+    <View>
+      <Image src={user.avatarFile?.fullUrl} className="w-20 h-20 rounded-full" />
+      <Text>{user.username}</Text>
+      <Button onClick={handleLogout}>退出登录</Button>
+    </View>
+  )
+}
+```
+
+## 注意事项
+
+1. 使用 `useAuth` 的组件必须包裹在 `AuthProvider` 内
+2. 所有认证相关的 API 调用都会自动处理 token 和错误提示
+3. 用户信息会自动缓存到本地存储,避免重复请求
+4. 退出登录会自动清除本地存储的 token 和用户信息
+5. 更新用户信息后会自动同步到本地存储

+ 623 - 0
roo/mini-form.md

@@ -0,0 +1,623 @@
+---
+description: "小程序表单开发指令"
+---
+
+# 小程序表单开发规范 (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管理复杂表单状态

+ 9 - 0
roo/mini-navbar.md

@@ -0,0 +1,9 @@
+---
+description: "顶部导航条Navbar使用指令"
+---
+
+使用 import { Navbar } from '@/components/ui/navbar'
+
+使用前先查看 mini/src/components/ui/navbar.tsx 了解具体用法
+
+注意:一级页面不需要返回按钮

+ 233 - 0
roo/mini-platform-check.md

@@ -0,0 +1,233 @@
+---
+description: "小程序平台检测工具 - 用于识别当前运行平台环境"
+---
+
+# 小程序平台检测工具使用文档
+
+## 功能概述
+
+`mini/src/utils/platform.ts` 提供了一套完整的平台检测工具,用于在 Taro 跨端小程序中识别当前运行环境,支持微信小程序、H5 网页端等多种平台的条件判断。
+
+## 导入方式
+
+```typescript
+import { getPlatform, isWeapp, isH5 } from '@/utils/platform'
+```
+
+## 核心功能
+
+### 1. 获取当前平台
+```typescript
+getPlatform(): TaroGeneral.ENV_TYPE
+```
+返回当前运行环境的平台类型枚举值。
+
+**返回值说明:**
+- `WEAPP`: 微信小程序
+- `WEB`: H5 网页端
+- `RN`: React Native
+- `SWAN`: 百度智能小程序
+- `ALIPAY`: 支付宝小程序
+- `TT`: 字节跳动小程序
+- `QQ`: QQ 小程序
+
+**使用示例:**
+```typescript
+import { getPlatform } from '@/utils/platform'
+import Taro from '@tarojs/taro'
+
+const currentPlatform = getPlatform()
+console.log('当前平台:', currentPlatform)
+
+// 根据平台执行不同逻辑
+switch (currentPlatform) {
+  case Taro.ENV_TYPE.WEAPP:
+    // 微信小程序专属逻辑
+    break
+  case Taro.ENV_TYPE.WEB:
+    // H5 网页端专属逻辑
+    break
+  default:
+    // 其他平台通用逻辑
+}
+```
+
+### 2. 是否为微信小程序
+```typescript
+isWeapp(): boolean
+```
+判断当前是否在微信小程序环境中运行。
+
+**使用示例:**
+```typescript
+import { isWeapp } from '@/utils/platform'
+
+if (isWeapp()) {
+  // 微信小程序专属功能
+  wx.login({
+    success: (res) => {
+      console.log('微信登录成功:', res.code)
+    }
+  })
+  
+  // 使用小程序 API
+  wx.getUserProfile({
+    desc: '用于完善用户资料',
+    success: (res) => {
+      console.log('用户信息:', res.userInfo)
+    }
+  })
+} else {
+  // 非小程序环境的替代方案
+  console.log('当前不是微信小程序环境')
+}
+```
+
+### 3. 是否为 H5 网页端
+```typescript
+isH5(): boolean
+```
+判断当前是否在 H5 网页端环境中运行。
+
+**使用示例:**
+```typescript
+import { isH5 } from '@/utils/platform'
+
+if (isH5()) {
+  // H5 网页端专属功能
+  // 使用浏览器 API
+  localStorage.setItem('token', 'your-token')
+  
+  // 使用 DOM API
+  window.addEventListener('resize', handleResize)
+} else {
+  // 小程序环境的替代方案
+  Taro.setStorageSync('token', 'your-token')
+}
+```
+
+## 实际应用场景
+
+### 场景1:条件渲染组件
+```typescript
+import { isWeapp, isH5 } from '@/utils/platform'
+
+const PlatformSpecificButton = () => {
+  if (isWeapp()) {
+    return (
+      <Button onClick={() => wx.navigateToMiniProgram({ appId: 'targetAppId' })}>
+        打开其他小程序
+      </Button>
+    )
+  }
+  
+  if (isH5()) {
+    return (
+      <Button onClick={() => window.open('https://example.com', '_blank')}>
+        打开外部链接
+      </Button>
+    )
+  }
+  
+  return null
+}
+```
+
+### 场景2:平台差异化 API 调用
+```typescript
+import { isWeapp, isH5 } from '@/utils/platform'
+
+const uploadImage = async (file: File) => {
+  if (isWeapp()) {
+    // 小程序上传
+    return new Promise((resolve, reject) => {
+      wx.uploadFile({
+        url: '/api/upload',
+        filePath: file.path,
+        name: 'file',
+        success: resolve,
+        fail: reject
+      })
+    })
+  }
+  
+  if (isH5()) {
+    // H5 上传
+    const formData = new FormData()
+    formData.append('file', file)
+    
+    const response = await fetch('/api/upload', {
+      method: 'POST',
+      body: formData
+    })
+    return response.json()
+  }
+}
+```
+
+### 场景3:平台特定样式处理
+```typescript
+import { isWeapp, isH5 } from '@/utils/platform'
+
+const getPlatformStyles = () => {
+  const baseStyles = 'p-4 rounded-lg'
+  
+  if (isWeapp()) {
+    return `${baseStyles} bg-green-100 text-green-800`
+  }
+  
+  if (isH5()) {
+    return `${baseStyles} bg-blue-100 text-blue-800 shadow-lg`
+  }
+  
+  return baseStyles
+}
+```
+
+## 与 Taro API 的集成
+
+平台检测工具与 Taro 的 API 完美集成,可以结合使用:
+
+```typescript
+import { isWeapp, isH5 } from '@/utils/platform'
+import Taro from '@tarojs/taro'
+
+// 平台特定的导航
+const navigateToPage = (url: string) => {
+  if (isWeapp()) {
+    Taro.navigateTo({ url })
+  } else if (isH5()) {
+    window.location.href = url
+  }
+}
+
+// 平台特定的存储
+const setStorage = (key: string, value: any) => {
+  if (isWeapp()) {
+    Taro.setStorageSync(key, value)
+  } else if (isH5()) {
+    localStorage.setItem(key, JSON.stringify(value))
+  }
+}
+```
+
+## 注意事项
+
+1. **必须在 Taro 环境中使用**:这些工具函数依赖于 Taro 的运行时环境
+2. **服务端渲染**:在 SSR 环境中使用时需要添加环境判断
+3. **测试环境**:在单元测试时可能需要 mock Taro 环境
+4. **性能优化**:工具函数都是轻量级的,不会带来性能开销
+
+## 扩展建议
+
+可以根据项目需要扩展更多平台检测函数:
+
+```typescript
+// 在 platform.ts 中添加更多检测函数
+export const isAlipay = (): boolean => {
+  return getPlatform() === Taro.ENV_TYPE.ALIPAY
+}
+
+export const isBaidu = (): boolean => {
+  return getPlatform() === Taro.ENV_TYPE.SWAN
+}

+ 370 - 0
roo/mini-rpc.md

@@ -0,0 +1,370 @@
+---
+description: "小程序RPC客户端开发规范 - 基于Taro + Hono RPC的完整实现指南"
+---
+
+# 小程序RPC开发规范
+
+## 概述
+
+本文档定义了小程序端使用Taro框架结合Hono RPC客户端的标准开发规范。基于现有的`mini/src/api.ts`、`mini/src/utils/rpc-client.ts`和`mini/src/pages/login/wechat-login.tsx`中的最佳实践。
+
+## 核心架构
+
+### 1. RPC客户端配置
+
+#### 1.1 客户端初始化 (`mini/src/utils/rpc-client.ts`)
+
+```typescript
+// 环境配置
+const API_BASE_URL = process.env.TARO_APP_API_BASE_URL || 'http://localhost:3000'
+
+// 自定义fetch适配Taro.request
+const taroFetch: any = async (input, init) => {
+  const url = typeof input === 'string' ? input : input.url
+  const method = init.method || 'GET'
+  
+  const requestHeaders: Record<string, string> = init.headers;
+
+  // 自动设置content-type
+  const keyOfContentType = Object.keys(requestHeaders).find(item => item.toLowerCase() === 'content-type')
+  if (!keyOfContentType) {
+    requestHeaders['content-type'] = 'application/json'
+  }
+
+  // 构建Taro请求选项
+  const options: Taro.request.Option = {
+    url,
+    method: method as any,
+    data: init.body,
+    header: requestHeaders
+  }
+
+  // 自动添加token认证
+  const token = Taro.getStorageSync('mini_token')
+  if (token) {
+    options.header = {
+      ...options.header,
+      'Authorization': `Bearer ${token}`
+    }
+  }
+
+  try {
+    const response = await Taro.request(options)
+    
+    // 处理响应数据
+    const body = response.statusCode === 204
+      ? null
+      : responseHeaders['content-type']!.includes('application/json')
+        ? JSON.stringify(response.data)
+        : response.data;
+
+    return new ResponsePolyfill(
+      body,
+      {
+        status: response.statusCode,
+        statusText: response.errMsg || 'OK',
+        headers: responseHeaders
+      }
+    )
+  } catch (error) {
+    console.error('API Error:', error)
+    Taro.showToast({
+      title: error.message || '网络错误',
+      icon: 'none'
+    })
+    throw error
+  }
+}
+
+// 创建Hono RPC客户端工厂函数
+export const rpcClient = <T extends Hono>() => {
+  return hc<T>(`${API_BASE_URL}`, {
+    fetch: taroFetch
+  })
+}
+```
+
+#### 1.2 客户端API定义 (`mini/src/api.ts`)
+
+```typescript
+import type { AuthRoutes, UserRoutes, RoleRoutes, FileRoutes } from '@/server/api'
+import { rpcClient } from './utils/rpc-client'
+
+// 创建各个模块的RPC客户端
+export const authClient = rpcClient<AuthRoutes>().api.v1.auth
+export const userClient = rpcClient<UserRoutes>().api.v1.users
+export const roleClient = rpcClient<RoleRoutes>().api.v1.roles
+export const fileClient = rpcClient<FileRoutes>().api.v1.files
+```
+
+## 使用规范
+
+### 2.1 调用方式
+
+#### 标准GET请求
+```typescript
+const response = await userClient.$get({
+  query: {
+    page: 1,
+    pageSize: 10
+  }
+})
+```
+
+#### POST请求(带请求体)
+```typescript
+const response = await authClient['mini-login'].$post({
+  json: {
+    code: loginRes.code,
+    userInfo: userProfile.userInfo
+  }
+})
+```
+
+#### 带路径参数的请求
+```typescript
+const response = await userClient[':id'].$get({
+  param: {
+    id: userId
+  }
+})
+```
+
+### 2.2 响应处理规范
+
+#### 成功响应处理
+```typescript
+if (response.status === 200) {
+  const { token, user, isNewUser } = await response.json()
+  
+  // 保存token到本地存储
+  Taro.setStorageSync('mini_token', token)
+  Taro.setStorageSync('userInfo', user)
+  
+  // 显示成功提示
+  Taro.showToast({
+    title: isNewUser ? '注册成功' : '登录成功',
+    icon: 'success',
+    duration: 1500
+  })
+}
+```
+
+#### 错误响应处理
+```typescript
+try {
+  const response = await authClient['mini-login'].$post({
+    json: { code, userInfo }
+  })
+  
+  if (response.status !== 200) {
+    const errorData = await response.json()
+    throw new Error(errorData.message || '操作失败')
+  }
+} catch (error: any) {
+  const errorMessage = error.message || '网络错误'
+  
+  // 分类处理错误
+  if (errorMessage.includes('用户拒绝授权')) {
+    Taro.showModal({
+      title: '提示',
+      content: '需要授权才能使用小程序的全部功能',
+      showCancel: false
+    })
+  } else {
+    Taro.showToast({
+      title: errorMessage,
+      icon: 'none',
+      duration: 3000
+    })
+  }
+}
+```
+
+## 微信小程序特殊场景
+
+### 3.1 微信登录流程
+
+基于`mini/src/pages/login/wechat-login.tsx`的最佳实践:
+
+```typescript
+const handleWechatLogin = async () => {
+  try {
+    Taro.showLoading({
+      title: '登录中...',
+      mask: true
+    })
+
+    // 1. 获取用户信息授权
+    const userProfile = await Taro.getUserProfile({
+      desc: '用于完善用户资料'
+    })
+
+    // 2. 获取登录code
+    const loginRes = await Taro.login()
+    
+    if (!loginRes.code) {
+      throw new Error('获取登录凭证失败')
+    }
+
+    // 3. 调用RPC接口
+    const response = await authClient['mini-login'].$post({
+      json: {
+        code: loginRes.code,
+        userInfo: userProfile.userInfo
+      }
+    })
+
+    Taro.hideLoading()
+
+    if (response.status === 200) {
+      const { token, user, isNewUser } = await response.json()
+      
+      // 4. 保存登录态
+      Taro.setStorageSync('userInfo', user)
+      Taro.setStorageSync('mini_token', token)
+      
+      // 5. 跳转页面
+      Taro.switchTab({ url: '/pages/index/index' })
+    }
+  } catch (error) {
+    Taro.hideLoading()
+    // 错误处理...
+  }
+}
+```
+
+### 3.2 平台检测
+
+```typescript
+import { isWeapp } from '@/utils/platform'
+
+// 检查是否为微信小程序环境
+const wechatEnv = isWeapp()
+if (!wechatEnv) {
+  Taro.showModal({
+    title: '提示',
+    content: '微信登录功能仅支持在微信小程序中使用',
+    showCancel: false
+  })
+}
+```
+
+## 开发规范
+
+### 4.1 文件结构
+
+```
+mini/
+├── src/
+│   ├── api.ts              # RPC客户端定义
+│   ├── utils/
+│   │   └── rpc-client.ts   # RPC客户端工厂
+│   └── pages/
+│       └── [功能页面]/
+│           └── index.tsx   # 页面逻辑
+```
+
+### 4.2 命名规范
+
+- **客户端命名**:`[模块名]Client`(如`authClient`、`userClient`)
+- **方法命名**:遵循RESTful规范(如`$get`、`$post`、`$put`、`$delete`)
+- **路径命名**:使用小写字母和连字符(如`mini-login`)
+
+### 4.3 类型安全
+
+```typescript
+// 使用InferResponseType提取响应类型
+import type { InferResponseType } from 'hono/client'
+type LoginResponse = InferResponseType<typeof authClient['mini-login']['$post'], 200>
+
+// 使用InferRequestType提取请求类型
+import type { InferRequestType } from 'hono/client'
+type LoginRequest = InferRequestType<typeof authClient['mini-login']['$post']>['json']
+```
+
+### 4.4 环境配置
+
+在`mini/.env`中配置API地址:
+
+```bash
+TARO_APP_API_BASE_URL=https://your-api-domain.com
+```
+
+## 最佳实践
+
+### 5.1 请求封装
+
+```typescript
+// 创建通用请求hook
+const useApiRequest = () => {
+  const [loading, setLoading] = useState(false)
+  
+  const request = async <T>(
+    apiCall: () => Promise<Response>,
+    successCallback?: (data: T) => void,
+    errorCallback?: (error: Error) => void
+  ) => {
+    setLoading(true)
+    try {
+      const response = await apiCall()
+      const data = await response.json()
+      
+      if (response.status === 200) {
+        successCallback?.(data)
+      } else {
+        throw new Error(data.message || '请求失败')
+      }
+    } catch (error) {
+      errorCallback?.(error)
+    } finally {
+      setLoading(false)
+    }
+  }
+  
+  return { loading, request }
+}
+```
+
+### 5.2 错误处理
+
+```typescript
+const handleApiError = (error: any) => {
+  const message = error.message || '网络错误'
+  
+  // 网络错误
+  if (message.includes('Network') || message.includes('网络')) {
+    Taro.showModal({
+      title: '网络错误',
+      content: '请检查网络连接后重试',
+      showCancel: false
+    })
+    return
+  }
+  
+  // 业务错误
+  Taro.showToast({
+    title: message,
+    icon: 'none',
+    duration: 3000
+  })
+}
+```
+
+### 5.3 加载状态管理
+
+```typescript
+const [loading, setLoading] = useState(false)
+
+const handleRequest = async () => {
+  setLoading(true)
+  Taro.showLoading({ title: '加载中...' })
+  
+  try {
+    const response = await apiClient.method.$post({ json: data })
+    // 处理响应...
+  } finally {
+    Taro.hideLoading()
+    setLoading(false)
+  }
+}

+ 7 - 0
roo/mini-tabbar-layout.md

@@ -0,0 +1,7 @@
+---
+description: "tabbar布局组件使用指令"
+---
+
+一级页面需要 使用 import { TabBarLayout } from '@/layouts/tab-bar-layout'
+
+使用前先查看 mini/src/layouts/tab-bar-layout.tsx 了解具体用法

+ 490 - 0
roo/mini-ui.md

@@ -0,0 +1,490 @@
+---
+description: "小程序ui开发指令"
+---
+
+按小程序ui规范,小程序表单开发规范(.roo/commands/mini-form.md)
+
+# 小程序UI开发规范 (Tailwind CSS v4)
+
+## 概述
+
+本规范定义了基于Taro框架的小程序UI开发标准,采用Tailwind CSS v4原子化样式和Heroicons图标库,遵循shadcn/ui组件设计模式。
+
+## 技术栈
+
+- **Taro 4** - 跨端小程序框架
+- **React 18** - 前端框架
+- **Tailwind CSS v4** - 原子化CSS框架
+- **@egoist/tailwindcss-icons** - 图标库集成
+- **@weapp-tailwindcss/merge** - Tailwind类名合并工具(小程序版tailwind-merge)
+- **clsx** - 条件样式类名管理
+
+## 目录结构
+
+```
+mini/
+├── src/
+│   ├── components/
+│   │   └── ui/           # UI组件库
+│   │       ├── button.tsx
+│   │       ├── input.tsx
+│   │       ├── card.tsx
+│   │       └── ...
+│   ├── pages/
+│   ├── utils/
+│   └── app.css           # Tailwind样式入口
+├── tailwind.config.js    # Tailwind配置
+└── postcss.config.js     # PostCSS配置
+```
+
+## 样式规范
+
+### 1. Tailwind CSS v4 使用规范
+
+#### 1.1 基础类名使用
+```typescript
+// ✅ 正确使用原子类
+<View className="flex items-center justify-between p-4 bg-white rounded-lg shadow-sm">
+  <Text className="text-lg font-semibold text-gray-900">标题</Text>
+</View>
+
+// ❌ 避免使用内联样式
+<View style={{ display: 'flex', alignItems: 'center', padding: 16 }}>
+  <Text style={{ fontSize: 18, fontWeight: '600' }}>标题</Text>
+</View>
+```
+
+#### 1.2 类名合并规范
+```typescript
+// ✅ 使用twMerge处理动态类名冲突
+import { twMerge } from '@weapp-tailwindcss/merge'
+
+// 处理静态和动态类名的冲突
+<View className={twMerge('px-4 py-2', isActive ? 'bg-blue-500' : 'bg-gray-200')}>
+  <Text>按钮</Text>
+</View>
+
+// 处理多个条件类名的合并
+<View className={twMerge(
+  'flex items-center',
+  isActive && 'bg-blue-500 text-white',
+  isDisabled && 'opacity-50 cursor-not-allowed',
+  customClassName
+)}>
+  <Text>复杂组件</Text>
+</View>
+
+// ❌ 避免手动拼接类名导致冲突
+<View className={`px-4 py-2 ${isActive ? 'bg-blue-500' : 'bg-gray-200'} ${customClassName}`}>
+  <Text>按钮</Text>
+</View>
+```
+
+#### 1.2 响应式设计
+```typescript
+// 使用Tailwind的响应式前缀
+<View className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+  <View className="w-full sm:w-1/2 md:w-1/3" />
+</View>
+```
+
+#### 1.3 状态样式
+```typescript
+// 悬停和焦点状态
+<Button className="bg-blue-500 hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
+  点击按钮
+</Button>
+
+// 禁用状态
+<Button className="disabled:opacity-50 disabled:cursor-not-allowed">
+  禁用按钮
+</Button>
+```
+
+### 2. 图标使用规范
+
+#### 2.1 图标
+使用`@egoist/tailwindcss-icons`提供的图标类名:
+"mdi", "lucide", "heroicons", "heroicons-outline", "heroicons-solid"
+
+```typescript
+// 基础用法
+<View className="i-heroicons-user-20-solid text-gray-600" />
+<Button className="flex items-center gap-2">
+  <View className="i-heroicons-plus-20-solid" />
+  <Text>添加</Text>
+</Button>
+
+// 图标大小和颜色
+<View className="i-heroicons-home-16-solid w-6 h-6 text-blue-500" />
+<View className="i-heroicons-cog-8-tooth-20-solid w-8 h-8 text-gray-400" />
+
+// 图标变体
+// solid - 实心图标
+// outline - 轮廓图标
+// mini - 迷你图标 (20x20)
+// micro - 微型图标 (16x16)
+<View className="i-heroicons-heart-20-solid text-red-500" />
+<View className="i-heroicons-heart-20-outline text-red-500" />
+```
+
+#### 2.2 图标命名规则
+```
+i-heroicons-[图标名]-[大小]-[变体]
+```
+- 大小: 16 | 20 | 24
+- 变体: solid | outline
+
+### 3. UI组件规范
+
+#### 3.1 组件文件结构
+每个UI组件应包含:
+```typescript
+// mini/src/components/ui/button.tsx
+import { Button as TaroButton, ButtonProps } from '@tarojs/components'
+import { cn } from '@/utils/cn'
+import { cva, type VariantProps } from 'class-variance-authority'
+
+const buttonVariants = cva(
+  'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background',
+  {
+    variants: {
+      variant: {
+        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
+        destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
+        outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
+        secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
+        ghost: 'hover:bg-accent hover:text-accent-foreground',
+        link: 'underline-offset-4 hover:underline text-primary',
+      },
+      size: {
+        default: 'h-10 py-2 px-4',
+        sm: 'h-9 px-3 rounded-md',
+        lg: 'h-11 px-8 rounded-md',
+        icon: 'h-10 w-10',
+      },
+    },
+    defaultVariants: {
+      variant: 'default',
+      size: 'default',
+    },
+  }
+)
+
+interface ButtonProps extends ButtonProps, VariantProps<typeof buttonVariants> {}
+
+export function Button({ className, variant, size, ...props }: ButtonProps) {
+  return (
+    <TaroButton
+      className={cn(buttonVariants({ variant, size, className }))}
+      {...props}
+    />
+  )
+}
+```
+
+#### 3.2 常用组件示例
+
+**按钮组件 (Button)**
+```typescript
+// 使用示例
+<Button variant="primary" size="lg" onClick={handleClick}>
+  <View className="i-heroicons-plus-20-solid mr-2" />
+  创建用户
+</Button>
+
+<Button variant="outline" size="sm" disabled={loading}>
+  {loading && <View className="i-heroicons-arrow-path-20-solid animate-spin mr-2" />}
+  加载中...
+</Button>
+```
+
+**卡片组件 (Card)**
+```typescript
+<Card className="p-6 bg-white rounded-lg shadow-sm">
+  <CardHeader>
+    <View className="flex items-center justify-between">
+      <Text className="text-lg font-semibold">用户信息</Text>
+      <View className="i-heroicons-user-circle-20-solid text-gray-400" />
+    </View>
+  </CardHeader>
+  <CardContent>
+    <Text className="text-gray-600">用户详情内容</Text>
+  </CardContent>
+</Card>
+```
+
+**输入框组件 (Input)**
+```typescript
+<View className="space-y-2">
+  <Label htmlFor="username">用户名</Label>
+  <View className="relative">
+    <View className="absolute left-3 top-1/2 -translate-y-1/2">
+      <View className="i-heroicons-user-20-solid text-gray-400 w-5 h-5" />
+    </View>
+    <Input
+      id="username"
+      className="pl-10"
+      placeholder="请输入用户名"
+      value={username}
+      onInput={handleInput}
+    />
+  </View>
+</View>
+```
+
+### 4. 页面布局规范
+
+#### 4.1 页面容器
+```typescript
+// 主容器
+<View className="min-h-screen bg-gray-50">
+  <View className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
+    {/* 页面内容 */}
+  </View>
+</View>
+
+// 卡片布局
+<View className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
+  {/* 卡片内容 */}
+</View>
+```
+
+#### 4.2 响应式断点
+- `sm`: 640px
+- `md`: 768px  
+- `lg`: 1024px
+- `xl`: 1280px
+- `2xl`: 1536px
+
+### 5. 主题配置
+
+#### 5.1 颜色系统
+```css
+/* 在 app.css 中定义 */
+:root {
+  --primary: 59 130 246;
+  --primary-foreground: 255 255 255;
+  --secondary: 107 114 128;
+  --secondary-foreground: 255 255 255;
+  --accent: 243 244 246;
+  --accent-foreground: 17 24 39;
+  --destructive: 239 68 68;
+  --destructive-foreground: 255 255 255;
+  --muted: 249 250 251;
+  --muted-foreground: 107 114 128;
+  --border: 229 231 235;
+  --input: 255 255 255;
+  --ring: 59 130 246;
+  --background: 255 255 255;
+  --foreground: 17 24 39;
+}
+```
+
+#### 5.2 Tailwind配置
+```javascript
+// tailwind.config.js
+module.exports = {
+  content: [
+    './src/**/*.{js,ts,jsx,tsx}',
+  ],
+  theme: {
+    extend: {
+      colors: {
+        primary: 'rgb(var(--primary))',
+        'primary-foreground': 'rgb(var(--primary-foreground))',
+        secondary: 'rgb(var(--secondary))',
+        'secondary-foreground': 'rgb(var(--secondary-foreground))',
+        accent: 'rgb(var(--accent))',
+        'accent-foreground': 'rgb(var(--accent-foreground))',
+        destructive: 'rgb(var(--destructive))',
+        'destructive-foreground': 'rgb(var(--destructive-foreground))',
+        muted: 'rgb(var(--muted))',
+        'muted-foreground': 'rgb(var(--muted-foreground))',
+        border: 'rgb(var(--border))',
+        input: 'rgb(var(--input))',
+        ring: 'rgb(var(--ring))',
+        background: 'rgb(var(--background))',
+        foreground: 'rgb(var(--foreground))',
+      },
+    },
+  },
+  plugins: [
+    require('@egoist/tailwindcss-icons')({
+      // 配置Heroicons
+      collections: {
+        heroicons: {
+          solid: true,
+          outline: true,
+          mini: true,
+        },
+      },
+    }),
+  ],
+}
+```
+
+### 6. 工具函数
+
+#### 6.1 类名合并工具
+```typescript
+// mini/src/utils/cn.ts
+import { clsx, type ClassValue } from 'clsx'
+import { twMerge } from '@weapp-tailwindcss/merge'
+
+export function cn(...inputs: ClassValue[]) {
+  return twMerge(clsx(inputs))
+}
+```
+
+#### 6.2 小程序专用类名处理
+```typescript
+// 小程序环境下的类名合并
+import { twMerge } from '@weapp-tailwindcss/merge'
+
+// 标准用法(自动处理小程序转义)
+const classes = twMerge('px-2 py-1 bg-red hover:bg-dark-red', 'p-3 bg-[#B91C1C]')
+// → 'hovercbg-dark-red p-3 bg-_hB91C1C_'
+
+// 手动指定版本(如果需要)
+import { twMerge as twMergeV4 } from '@weapp-tailwindcss/merge/v4'
+import { twMerge as twMergeV3 } from '@weapp-tailwindcss/merge/v3'
+
+// 使用cva进行组件变体管理
+import { cva } from 'class-variance-authority'
+
+const buttonVariants = cva(
+  'inline-flex items-center justify-center rounded-md text-sm font-medium',
+  {
+    variants: {
+      variant: {
+        default: 'bg-blue-500 text-white hover:bg-blue-600',
+        destructive: 'bg-red-500 text-white hover:bg-red-600',
+      },
+      size: {
+        sm: 'h-8 px-3 text-xs',
+        lg: 'h-12 px-6 text-base',
+      },
+    },
+  }
+)
+```
+
+### 7. 最佳实践
+
+#### 7.1 状态管理
+```typescript
+// 使用React Hook进行状态管理
+const [loading, setLoading] = useState(false)
+const [data, setData] = useState<User[]>([])
+
+// 加载状态显示
+{loading ? (
+  <View className="flex justify-center py-8">
+    <View className="i-heroicons-arrow-path-20-solid animate-spin w-8 h-8 text-blue-500" />
+  </View>
+) : (
+  <View className="grid grid-cols-1 gap-4">
+    {data.map(item => <Card key={item.id} {...item} />)}
+  </View>
+)}
+```
+
+#### 7.2 错误处理
+```typescript
+// 错误状态展示
+<View className="text-center py-8">
+  <View className="i-heroicons-exclamation-triangle-20-solid w-12 h-12 text-red-500 mx-auto mb-4" />
+  <Text className="text-gray-600">加载失败,请稍后重试</Text>
+  <Button variant="outline" size="sm" onClick={retry} className="mt-4">
+    重新加载
+  </Button>
+</View>
+```
+
+### 8. 性能优化
+
+#### 8.1 样式优化
+- 使用Tailwind的JIT模式,只生成用到的类名
+- 避免内联样式,全部使用类名
+- 合理使用`@apply`提取重复样式
+
+#### 8.2 图标优化
+- 使用CSS图标而非图片图标
+- 图标按需加载,不使用的图标不会被打包
+- 合理使用图标大小,避免过大图标
+
+### 9. 调试工具
+
+#### 9.1 开发调试
+```typescript
+// 添加调试样式类
+<View className="border border-red-500 debug">
+  <Text>调试内容</Text>
+</View>
+
+// 使用Tailwind的调试工具
+// 在开发环境中添加
+// <View className="outline outline-1 outline-red-500" />
+```
+
+### 10. tailwind-merge使用规范
+
+#### 10.1 基本用法
+```typescript
+// 单类名合并
+const result = twMerge('px-2 py-1 bg-red hover:bg-dark-red', 'p-3 bg-[#B91C1C]')
+// → 'hovercbg-dark-red p-3 bg-_hB91C1C_'
+
+// 处理冲突类名
+twMerge('px-4', 'px-2') // → 'px-2'
+twMerge('text-red-500', 'text-blue-500') // → 'text-blue-500'
+```
+
+#### 10.2 条件类名处理
+```typescript
+// 使用cn工具函数处理条件类名
+import { cn } from '@/utils/cn'
+
+const Button = ({ variant, size, disabled, className }) => {
+  return (
+    <Button
+      className={cn(
+        'inline-flex items-center justify-center rounded-md',
+        variant === 'primary' && 'bg-blue-500 text-white',
+        variant === 'secondary' && 'bg-gray-200 text-gray-800',
+        size === 'sm' && 'px-3 py-1 text-sm',
+        size === 'lg' && 'px-6 py-3 text-lg',
+        disabled && 'opacity-50 cursor-not-allowed',
+        className // 允许外部覆盖
+      )}
+    >
+      按钮
+    </Button>
+  )
+}
+```
+
+#### 10.3 小程序特殊处理
+```typescript
+// 跨端使用
+import { create } from '@weapp-tailwindcss/merge'
+
+const { twMerge } = create({
+  // 在当前环境为小程序时启用转义
+  disableEscape: true
+})
+
+// 版本选择
+import { twMerge as twMergeV4 } from '@weapp-tailwindcss/merge/v4' // Tailwind v4
+import { twMerge as twMergeV3 } from '@weapp-tailwindcss/merge/v3' // Tailwind v3
+```
+
+## 注意事项
+
+1. **兼容性**:确保所有类名在小程序环境中有效
+2. **性能**:避免过度嵌套和复杂选择器
+3. **可维护性**:保持组件结构清晰,样式统一
+4. **可读性**:合理使用空格和换行,提高代码可读性
+5. **tailwind-merge**:始终使用twMerge或cn工具函数处理动态类名,避免类名冲突
+6. **版本兼容**:根据Tailwind CSS版本选择正确的tailwind-merge版本

+ 15 - 0
roo/rpc-type.md

@@ -0,0 +1,15 @@
+---
+description: "提取rpc响应,请求类型"
+---
+
+rpc类型安全
+示例:
+```typescript
+// 使用InferResponseType提取响应类型
+import type { InferResponseType } from 'hono/client'
+type LoginResponse = InferResponseType<typeof authClient['mini-login']['$post'], 200>
+
+// 使用InferRequestType提取请求类型
+import type { InferRequestType } from 'hono/client'
+type LoginRequest = InferRequestType<typeof authClient['mini-login']['$post']>['json']
+```

+ 158 - 0
roo/schema-error-msg-cn.md

@@ -0,0 +1,158 @@
+---
+description: "给schema加上中文错误提示"
+---
+
+为每个验证字段添加 message 属性的中文错误提示
+
+
+示例:
+```typescript
+// 基础用户 schema(包含所有字段)
+export const UserSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '用户ID' }),
+  username: z.string().min(3, '用户名至少3个字符').max(255, '用户名最多255个字符').openapi({
+    example: 'admin',
+    description: '用户名,3-255个字符'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').openapi({
+    example: 'password123',
+    description: '密码,最少6位'
+  }),
+  phone: z.string().max(255, '手机号最多255个字符').nullable().openapi({
+    example: '13800138000',
+    description: '手机号'
+  }),
+  email: z.email('请输入正确的邮箱格式').max(255, '邮箱最多255个字符').nullable().openapi({
+    example: 'user@example.com',
+    description: '邮箱'
+  }),
+  nickname: z.string().max(255, '昵称最多255个字符').nullable().openapi({
+    example: '昵称',
+    description: '用户昵称'
+  }),
+  name: z.string().max(255, '姓名最多255个字符').nullable().openapi({
+    example: '张三',
+    description: '真实姓名'
+  }),
+  avatarFileId: z.number().int().positive().nullable().openapi({
+    example: 1,
+    description: '头像文件ID'
+  }),
+  avatarFile: z.object({
+    id: z.number().int().positive().openapi({ description: '文件ID' }),
+    name: z.string().max(255).openapi({ description: '文件名', example: 'avatar.jpg' }),
+    fullUrl: z.string().openapi({ description: '文件完整URL', example: 'https://example.com/avatar.jpg' }),
+    type: z.string().nullable().openapi({ description: '文件类型', example: 'image/jpeg' }),
+    size: z.number().nullable().openapi({ description: '文件大小(字节)', example: 102400 })
+  }).nullable().optional().openapi({
+    description: '头像文件信息'
+  }),
+  openid: z.string().max(255).nullable().optional().openapi({
+    example: 'oABCDEFGH123456789',
+    description: '微信小程序openid'
+  }),
+  unionid: z.string().max(255).nullable().optional().openapi({
+    example: 'unionid123456789',
+    description: '微信unionid'
+  }),
+  registrationSource: z.string().max(20).default('web').openapi({
+    example: 'miniapp',
+    description: '注册来源: web, miniapp'
+  }),
+  isDisabled: z.number().int().min(0).max(1).default(DisabledStatus.ENABLED).openapi({
+    example: DisabledStatus.ENABLED,
+    description: '是否禁用(0:启用,1:禁用)'
+  }),
+  isDeleted: z.number().int().min(0).max(1).default(DeleteStatus.NOT_DELETED).openapi({
+    example: DeleteStatus.NOT_DELETED,
+    description: '是否删除(0:未删除,1:已删除)'
+  }),
+  roles: z.array(RoleSchema).optional().openapi({
+    example: [
+      {
+        id: 1,
+        name: 'admin',
+        description: '管理员',
+        permissions: ['user:create'],
+        createdAt: new Date(),
+        updatedAt: new Date()
+      }
+    ],
+    description: '用户角色列表'
+  }),
+  createdAt: z.coerce.date().openapi({ description: '创建时间' }),
+  updatedAt: z.coerce.date().openapi({ description: '更新时间' })
+});
+
+// 创建用户请求 schema
+export const CreateUserDto = z.object({
+  username: z.string().min(3, '用户名至少3个字符').max(255, '用户名最多255个字符').openapi({
+    example: 'admin',
+    description: '用户名,3-255个字符'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').openapi({
+    example: 'password123',
+    description: '密码,最少6位'
+  }),
+  phone: z.string().max(255, '手机号最多255个字符').nullable().optional().openapi({
+    example: '13800138000',
+    description: '手机号'
+  }),
+  email: z.email('请输入正确的邮箱格式').max(255, '邮箱最多255个字符').nullable().optional().openapi({
+    example: 'user@example.com',
+    description: '邮箱'
+  }),
+  nickname: z.string().max(255, '昵称最多255个字符').nullable().optional().openapi({
+    example: '昵称',
+    description: '用户昵称'
+  }),
+  name: z.string().max(255, '姓名最多255个字符').nullable().optional().openapi({
+    example: '张三',
+    description: '真实姓名'
+  }),
+  avatarFileId: z.number().int().positive().nullable().optional().openapi({
+    example: 1,
+    description: '头像文件ID'
+  }),
+  isDisabled: z.number().int().min(0, '状态值只能是0或1').max(1, '状态值只能是0或1').default(DisabledStatus.ENABLED).optional().openapi({
+    example: DisabledStatus.ENABLED,
+    description: '是否禁用(0:启用,1:禁用)'
+  })
+});
+
+// 更新用户请求 schema
+export const UpdateUserDto = z.object({
+  username: z.string().min(3, '用户名至少3个字符').max(255, '用户名最多255个字符').optional().openapi({
+    example: 'admin',
+    description: '用户名,3-255个字符'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').optional().openapi({
+    example: 'password123',
+    description: '密码,最少6位'
+  }),
+  phone: z.string().max(255, '手机号最多255个字符').nullable().optional().openapi({
+    example: '13800138000',
+    description: '手机号'
+  }),
+  email: z.email('请输入正确的邮箱格式').max(255, '邮箱最多255个字符').nullable().optional().openapi({
+    example: 'user@example.com',
+    description: '邮箱'
+  }),
+  nickname: z.string().max(255, '昵称最多255个字符').nullable().optional().openapi({
+    example: '昵称',
+    description: '用户昵称'
+  }),
+  name: z.string().max(255, '姓名最多255个字符').nullable().optional().openapi({
+    example: '张三',
+    description: '真实姓名'
+  }),
+  avatarFileId: z.number().int().positive().nullable().optional().openapi({
+    example: 1,
+    description: '头像文件ID'
+  }),
+  isDisabled: z.number().int().min(0, '状态值只能是0或1').max(1, '状态值只能是0或1').optional().openapi({
+    example: DisabledStatus.ENABLED,
+    description: '是否禁用(0:启用,1:禁用)'
+  })
+});
+```

+ 750 - 0
roo/shadcn-manage-form-split.md

@@ -0,0 +1,750 @@
+---
+description: "Shadcn-ui 管理页表单创建/编辑表单分离指令"
+---
+
+将 创建/编辑表单分离, 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>
+  )}
+/>
+```
+
+## 表单状态管理模板
+
+### 创建/编辑表单分离模式(推荐)
+
+基于 `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,
+});

+ 751 - 0
roo/shadcn-manage-form.md

@@ -0,0 +1,751 @@
+---
+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
+<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>
+  )}
+/>
+```
+
+## 表单状态管理模板
+
+### 创建/编辑表单分离模式(推荐)
+
+基于 `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,
+});

+ 425 - 0
roo/shadcn-manage-page.md

@@ -0,0 +1,425 @@
+---
+description: "Shadcn-ui 管理页开发指令"
+---
+
+## 概述
+基于 `src/client/admin-shadcn/pages/Users.tsx` 中用户管理页的实现,提取可复用的开发模式和最佳实践,适用于基于 Shadcn-ui 的管理后台页面开发。
+
+## 页面结构规范
+
+### 1. 文件位置
+- **管理后台页面**: `src/client/admin-shadcn/pages/[EntityName].tsx`
+
+### 2. 页面组件结构
+```typescript
+// 1. 类型导入和定义
+type CreateRequest = InferRequestType<typeof client.$post>['json'];
+type UpdateRequest = InferRequestType<typeof client[':id']['$put']>['json'];
+type EntityResponse = InferResponseType<typeof client.$get, 200>['data'][0];
+
+// 2. 表单Schema直接使用后端定义
+const createFormSchema = CreateEntityDto;
+const updateFormSchema = UpdateEntityDto;
+
+// 3. 主页面组件
+export const EntityPage = () => {
+  // 状态管理
+  const [searchParams, setSearchParams] = useState({ page: 1, limit: 10, search: '' });
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [editingEntity, setEditingEntity] = useState<any>(null);
+  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+  const [entityToDelete, setEntityToDelete] = useState<number | null>(null);
+  
+  // 表单实例
+  const createForm = useForm<CreateRequest>({...});
+  const updateForm = useForm<UpdateRequest>({...});
+  
+  // 数据查询
+  const { data, isLoading, refetch } = useQuery({...});
+  
+  // 业务逻辑函数
+  const handleSearch = () => {...};
+  const handleCreateEntity = () => {...};
+  const handleEditEntity = () => {...};
+  const handleDeleteEntity = () => {...};
+  
+  // 渲染
+  return (...);
+};
+```
+
+## 核心开发模式
+
+### 1. 类型驱动的开发
+- **RPC类型提取**: 使用 `InferRequestType` 和 `InferResponseType` 从后端API自动提取类型
+- **Schema复用**: 直接使用后端定义的Zod Schema作为表单验证
+- **类型安全**: 所有API调用都有完整的TypeScript类型支持
+
+### 2. 状态管理模式
+```typescript
+// 分页和搜索参数
+const [searchParams, setSearchParams] = useState({
+  page: 1,
+  limit: 10,
+  search: '',
+  // 其他筛选条件...
+});
+
+// 模态框状态
+const [isModalOpen, setIsModalOpen] = useState(false);
+const [editingEntity, setEditingEntity] = useState<any>(null);
+const [isCreateForm, setIsCreateForm] = useState(true);
+```
+
+### 3. 数据获取模式
+```typescript
+const { data, isLoading, refetch } = useQuery({
+  queryKey: ['entities', searchParams],
+  queryFn: async () => {
+    const res = await entityClient.$get({
+      query: {
+        page: searchParams.page,
+        pageSize: searchParams.limit,
+        keyword: searchParams.search,
+        // 其他查询参数...
+      }
+    });
+    if (res.status !== 200) throw new Error('获取列表失败');
+    return await res.json();
+  }
+});
+```
+
+## 页面布局规范
+
+### 1. 页面标题区域
+```tsx
+<div className="flex justify-between items-center">
+  <h1 className="text-2xl font-bold">页面标题</h1>
+  <Button onClick={handleCreateEntity}>
+    <Plus className="mr-2 h-4 w-4" />
+    创建实体
+  </Button>
+</div>
+```
+
+### 2. 搜索区域
+```tsx
+<Card>
+  <CardHeader>
+    <CardTitle>列表标题</CardTitle>
+    <CardDescription>列表描述信息</CardDescription>
+  </CardHeader>
+  <CardContent>
+    <div className="mb-4">
+      <form onSubmit={handleSearch} className="flex gap-2">
+        <div className="relative flex-1 max-w-sm">
+          <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
+          <Input
+            placeholder="搜索提示..."
+            value={searchParams.search}
+            onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
+            className="pl-8"
+          />
+        </div>
+        <Button type="submit" variant="outline">
+          搜索
+        </Button>
+      </form>
+    </div>
+  </CardContent>
+</Card>
+```
+
+### 3. 数据表格
+```tsx
+<div className="rounded-md border">
+  <Table>
+    <TableHeader>
+      <TableRow>
+        <TableHead>列标题1</TableHead>
+        <TableHead>列标题2</TableHead>
+        <TableHead className="text-right">操作</TableHead>
+      </TableRow>
+    </TableHeader>
+    <TableBody>
+      {data.map((item) => (
+        <TableRow key={item.id}>
+          <TableCell>{item.field1}</TableCell>
+          <TableCell>{item.field2}</TableCell>
+          <TableCell className="text-right">
+            <div className="flex justify-end gap-2">
+              <Button variant="ghost" size="icon" onClick={() => handleEdit(item)}>
+                <Edit className="h-4 w-4" />
+              </Button>
+              <Button variant="ghost" size="icon" onClick={() => handleDelete(item.id)}>
+                <Trash2 className="h-4 w-4" />
+              </Button>
+            </div>
+          </TableCell>
+        </TableRow>
+      ))}
+    </TableBody>
+  </Table>
+</div>
+```
+
+## 表单开发模式
+
+### 1. 表单组件结构
+```typescript
+// 创建表单
+const createForm = useForm<CreateRequest>({
+  resolver: zodResolver(createFormSchema),
+  defaultValues: {
+    // 默认值设置
+  },
+});
+
+// 更新表单
+const updateForm = useForm<UpdateRequest>({
+  resolver: zodResolver(updateFormSchema),
+  defaultValues: {
+    // 更新时默认值
+  },
+});
+```
+
+### 2. 模态框表单(创建/编辑分离模式)
+```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">
+          {/* 创建专用字段 */}
+          <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>
+```
+
+### 3. 表单字段模式
+```tsx
+<FormField
+  control={form.control}
+  name="fieldName"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel className="flex items-center">
+        字段标签
+        {isRequired && <span className="text-red-500 ml-1">*</span>}
+      </FormLabel>
+      <FormControl>
+        <Input placeholder="请输入..." {...field} />
+      </FormControl>
+      <FormDescription>字段描述信息</FormDescription>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+## 图片上传集成
+
+### 1. AvatarSelector组件使用
+```tsx
+<FormField
+  control={form.control}
+  name="avatarFileId"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel>头像</FormLabel>
+      <FormControl>
+        <AvatarSelector
+          value={field.value || undefined}
+          onChange={(value) => field.onChange(value)}
+          maxSize={2} // MB
+          uploadPath="/avatars"
+          uploadButtonText="上传头像"
+          previewSize="medium"
+          placeholder="选择头像"
+        />
+      </FormControl>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+## 状态管理最佳实践
+
+### 1. 状态提升策略
+- **表单状态**: 使用React Hook Form管理
+- **UI状态**: 使用useState管理模态框、加载状态等
+- **服务器状态**: 使用React Query管理数据获取和缓存
+
+### 2. 数据刷新策略
+```typescript
+// 操作成功后刷新数据
+const handleCreateSubmit = async (data: CreateRequest) => {
+  try {
+    const res = await entityClient.$post({ json: data });
+    if (res.status !== 201) throw new Error('创建失败');
+    toast.success('创建成功');
+    setIsModalOpen(false);
+    refetch(); // 刷新数据
+  } catch (error) {
+    toast.error('操作失败,请重试');
+  }
+};
+```
+
+## 加载状态处理
+
+### 1. 骨架屏模式
+```tsx
+if (isLoading) {
+  return (
+    <div className="space-y-4">
+      <div className="flex justify-between items-center">
+        <h1 className="text-2xl font-bold">页面标题</h1>
+        <Button disabled>
+          <Plus className="mr-2 h-4 w-4" />
+          创建实体
+        </Button>
+      </div>
+      
+      <Card>
+        <CardHeader>
+          <Skeleton className="h-6 w-1/4" />
+        </CardHeader>
+        <CardContent>
+          <div className="space-y-2">
+            <Skeleton className="h-4 w-full" />
+            <Skeleton className="h-4 w-full" />
+            <Skeleton className="h-4 w-full" />
+          </div>
+        </CardContent>
+      </Card>
+    </div>
+  );
+}
+```
+
+### 2. 空数据状态
+```tsx
+{users.length === 0 && !isLoading && (
+  <div className="text-center py-8">
+    <p className="text-muted-foreground">暂无数据</p>
+  </div>
+)}
+```
+
+## 错误处理模式
+
+### 1. API错误处理
+```typescript
+try {
+  const res = await entityClient.$post({ json: data });
+  if (res.status !== 201) throw new Error('操作失败');
+  toast.success('操作成功');
+} catch (error) {
+  console.error('操作失败:', error);
+  toast.error('操作失败,请重试');
+}
+```
+
+### 2. 删除确认模式
+```tsx
+const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+const [entityToDelete, setEntityToDelete] = useState<number | null>(null);
+
+// 删除确认对话框
+<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
+  <DialogContent>
+    <DialogHeader>
+      <DialogTitle>确认删除</DialogTitle>
+      <DialogDescription>
+        确定要删除这个实体吗?此操作无法撤销。
+      </DialogDescription>
+    </DialogHeader>
+    <DialogFooter>
+      <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
+        取消
+      </Button>
+      <Button variant="destructive" onClick={confirmDelete}>
+        删除
+      </Button>
+    </DialogFooter>
+  </DialogContent>
+</Dialog>
+```
+
+## 样式规范
+
+### 1. 间距系统
+- 页面标题区域: `space-y-4`
+- 卡片内容: `space-y-4`
+- 表单字段: `space-y-4`
+- 按钮组: `gap-2`
+
+### 2. 响应式设计
+- 模态框最大宽度: `sm:max-w-[500px]`
+- 模态框最大高度: `max-h-[90vh]`
+- 搜索输入框: `max-w-sm`
+
+### 3. 视觉层次
+- 标题: `text-2xl font-bold`
+- 卡片标题: `text-lg font-semibold`
+- 描述文字: `text-sm text-muted-foreground`
+
+## 开发流程
+
+### 1. 创建新管理页面
+1. 复制 `Users.tsx` 作为模板
+2. 替换以下部分:
+   - API客户端导入
+   - 类型定义
+   - 表单Schema引用
+   - 页面标题和描述
+   - 表格列定义
+   - 表单字段定义
+3. 根据业务需求调整字段和逻辑
+
+### 2. 字段映射规范
+- **文本字段**: 使用 `Input`
+- **长文本**: 使用 `Textarea`
+- **选择字段**: 使用 `Select`
+- **开关字段**: 使用 `Switch`
+- **日期字段**: 使用 `DatePicker`
+- **图片字段**: 使用 `AvatarSelector`
+
+### 3. 业务逻辑复用
+- 保持相同的CRUD操作模式
+- 复用分页、搜索、排序逻辑
+- 统一的状态管理模式
+- 一致的表单验证和错误处理

+ 5 - 0
roo/shadcn.md

@@ -0,0 +1,5 @@
+---
+description: "使用shadcn创建页面及组件"
+---
+
+shadcn配置在 components.json