浏览代码

🔧 chore(project): 重构文档目录结构并更新开发规范

- 将roo目录下的命令文档迁移至.roo/commands/统一管理
- 删除过时的roo目录及重复文档
- 更新实体创建规范:实体类与schema文件分离
- 添加只读模式支持到通用CRUD路由
- 完善shadcn-ui表单开发文档,推荐创建/编辑表单分离模式
yourname 3 月之前
父节点
当前提交
8ca7f39954

+ 0 - 0
roo/check-api-entity-schema.md → .roo/commands/check-api-entity-schema.md


+ 0 - 0
roo/check-curd-entity-schema.md → .roo/commands/check-curd-entity-schema.md


+ 0 - 0
roo/file-relation.md → .roo/commands/file-relation.md


+ 0 - 0
roo/generic-crud-public-create.md → .roo/commands/generic-crud-public-create.md


+ 0 - 0
roo/generic-crud-public-reg.md → .roo/commands/generic-crud-public-reg.md


+ 0 - 0
roo/generic-crud-reg.md → .roo/commands/generic-crud-reg.md


+ 0 - 0
roo/generic-crud.md → .roo/commands/generic-crud.md


+ 0 - 0
roo/mini-auth.md → .roo/commands/mini-auth.md


+ 0 - 0
roo/mini-form.md → .roo/commands/mini-form.md


+ 0 - 0
roo/mini-navbar.md → .roo/commands/mini-navbar.md


+ 0 - 0
roo/mini-rpc.md → .roo/commands/mini-rpc.md


+ 0 - 0
roo/mini-tabbar-layout.md → .roo/commands/mini-tabbar-layout.md


+ 486 - 1
.roo/commands/mini-ui.md

@@ -2,4 +2,489 @@
 description: "小程序ui开发指令"
 ---
 
-按小程序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版本

+ 0 - 0
roo/rpc-type.md → .roo/commands/rpc-type.md


+ 0 - 0
roo/schema-error-msg-cn.md → .roo/commands/schema-error-msg-cn.md


+ 0 - 0
roo/shadcn-manage-form-split.md → .roo/commands/shadcn-manage-form-split.md


+ 527 - 19
.roo/commands/shadcn-manage-form.md

@@ -12,10 +12,11 @@ description: "Shadcn-ui 管理页表单开发指令"
 - **RPC类型提取**:从 Hono 客户端自动推断类型
 - **一致的类型定义**:前后端类型完全同步
 
-### 2. 表单状态管理
-- **创建/编辑模式切换**:单一表单处理两种状态
-- **智能默认值**:根据模式自动设置表单初始值
-- **表单重置**:模式切换时自动重置表单状态
+### 2. 表单状态管理(推荐:创建/编辑表单分离模式)
+- **分离表单实例**:为创建和编辑分别使用独立的表单实例, Form组件也分开
+- **类型安全**:创建使用CreateSchema,编辑使用UpdateSchema,避免类型冲突
+- **字段差异处理**:创建时的必填字段在编辑时变为可选,敏感字段特殊处理
+- **状态隔离**:两种模式的状态完全独立,避免交叉污染
 
 ### 3. 统一的UI组件模式
 - **Shadcn-ui组件集成**:使用标准的 Shadcn-ui 表单组件
@@ -24,9 +25,9 @@ description: "Shadcn-ui 管理页表单开发指令"
 
 ## 开发模板
 
-### 基础结构模板
+### 基础结构模板(创建/编辑分离模式)
 ```typescript
-// 1. 类型定义
+// 1. 类型定义(使用后端真实类型)
 import { useForm } from 'react-hook-form';
 import { zodResolver } from '@hookform/resolvers/zod';
 import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
@@ -34,19 +35,46 @@ import { Input } from '@/client/components/ui/input';
 import { Button } from '@/client/components/ui/button';
 import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
 
-// 2. 表单配置
+// 2. 分离的表单配置
 const [isCreateForm, setIsCreateForm] = useState(true);
-const [editingData, setEditingData] = useState<any>(null);
+const [editingEntity, setEditingEntity] = useState<any>(null); // 用于存储编辑时的实体数据
 
-const createForm = useForm<CreateType>({
-  resolver: zodResolver(createSchema),
-  defaultValues: {/* 创建时的默认值 */},
+// 3. 独立的表单实例
+const createForm = useForm<CreateRequest>({
+  resolver: zodResolver(CreateEntityDto), // 使用创建专用的Schema
+  defaultValues: {
+    // 创建时必填字段的默认值
+  },
 });
 
-const updateForm = useForm<UpdateType>({
-  resolver: zodResolver(updateSchema),
-  defaultValues: {/* 更新时的默认值 */},
+const updateForm = useForm<UpdateRequest>({
+  resolver: zodResolver(UpdateEntityDto), // 使用更新专用的Schema
+  defaultValues: {
+    // 更新时可选字段的默认值(会被实际数据覆盖)
+  },
 });
+
+// 4. 表单切换逻辑(核心模式)
+const handleCreateEntity = () => {
+  setEditingEntity(null);
+  setIsCreateForm(true);
+  createForm.reset({
+    // 创建时的初始值(必填字段必须有值)
+  });
+  setIsModalOpen(true);
+};
+
+const handleEditEntity = (entity: EntityResponse) => {
+  setEditingEntity(entity);
+  setIsCreateForm(false);
+  updateForm.reset({
+    ...entity,
+    // 特殊处理:敏感字段在编辑时设为可选
+    password: undefined, // 密码在更新时可选,不修改则留空
+    // 其他需要特殊处理的字段
+  });
+  setIsModalOpen(true);
+};
 ```
 
 ### 表单字段模板
@@ -170,6 +198,421 @@ nickname: z.string().optional()
 </Dialog>
 ```
 
+## 高级表单组件模板
+
+### 头像选择器集成
+```typescript
+import AvatarSelector from '@/client/admin-shadcn/components/AvatarSelector';
+
+<FormField
+  control={form.control}
+  name="avatarFileId"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel>头像</FormLabel>
+      <FormControl>
+        <AvatarSelector
+          value={field.value || undefined}
+          onChange={(value) => field.onChange(value)}
+          maxSize={2}
+          uploadPath="/avatars"
+          uploadButtonText="上传头像"
+          previewSize="medium"
+          placeholder="选择头像"
+        />
+      </FormControl>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+### 下拉选择框
+```typescript
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/client/components/ui/select';
+
+<FormField
+  control={form.control}
+  name="status"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel>状态</FormLabel>
+      <Select onValueChange={field.onChange} defaultValue={String(field.value)}>
+        <FormControl>
+          <SelectTrigger>
+            <SelectValue placeholder="请选择状态" />
+          </SelectTrigger>
+        </FormControl>
+        <SelectContent>
+          <SelectItem value="0">启用</SelectItem>
+          <SelectItem value="1">禁用</SelectItem>
+        </SelectContent>
+      </Select>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+### 数字输入框
+```typescript
+<FormField
+  control={form.control}
+  name="age"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel>年龄</FormLabel>
+      <FormControl>
+        <Input 
+          type="number" 
+          placeholder="请输入年龄"
+          {...field}
+          onChange={(e) => field.onChange(Number(e.target.value))}
+        />
+      </FormControl>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+### 日期选择器
+```typescript
+import { Popover, PopoverContent, PopoverTrigger } from '@/client/components/ui/popover';
+import { Calendar } from '@/client/components/ui/calendar';
+import { CalendarIcon } from 'lucide-react';
+import { cn } from '@/client/lib/utils';
+
+<FormField
+  control={form.control}
+  name="birthDate"
+  render={({ field }) => (
+    <FormItem className="flex flex-col">
+      <FormLabel>出生日期</FormLabel>
+      <Popover>
+        <PopoverTrigger asChild>
+          <FormControl>
+            <Button
+              variant={"outline"}
+              className={cn(
+                "w-[240px] pl-3 text-left font-normal",
+                !field.value && "text-muted-foreground"
+              )}
+            >
+              {field.value ? (
+                format(field.value, "yyyy-MM-dd")
+              ) : (
+                <span>选择日期</span>
+              )}
+              <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
+            </Button>
+          </FormControl>
+        </PopoverTrigger>
+        <PopoverContent className="w-auto p-0" align="start">
+          <Calendar
+            mode="single"
+            selected={field.value}
+            onSelect={field.onChange}
+            disabled={(date) =>
+              date > new Date() || date < new Date("1900-01-01")
+            }
+            initialFocus
+          />
+        </PopoverContent>
+      </Popover>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+### 文本域
+```typescript
+import { Textarea } from '@/client/components/ui/textarea';
+
+<FormField
+  control={form.control}
+  name="description"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel>描述</FormLabel>
+      <FormControl>
+        <Textarea
+          placeholder="请输入描述信息"
+          className="resize-none"
+          {...field}
+        />
+      </FormControl>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+### 复选框组
+```typescript
+import { Checkbox } from '@/client/components/ui/checkbox';
+
+<FormField
+  control={form.control}
+  name="permissions"
+  render={() => (
+    <FormItem>
+      <div className="mb-4">
+        <FormLabel className="text-base">权限设置</FormLabel>
+        <FormDescription>
+          选择该用户拥有的权限
+        </FormDescription>
+      </div>
+      <div className="space-y-2">
+        {permissions.map((permission) => (
+          <FormField
+            key={permission.id}
+            control={form.control}
+            name="permissions"
+            render={({ field }) => {
+              return (
+                <FormItem
+                  key={permission.id}
+                  className="flex flex-row items-start space-x-3 space-y-0"
+                >
+                  <FormControl>
+                    <Checkbox
+                      checked={field.value?.includes(permission.id)}
+                      onCheckedChange={(checked) => {
+                        return checked
+                          ? field.onChange([...field.value, permission.id])
+                          : field.onChange(
+                              field.value?.filter(
+                                (value) => value !== permission.id
+                              )
+                            )
+                      }}
+                    />
+                  </FormControl>
+                  <div className="space-y-1 leading-none">
+                    <FormLabel>
+                      {permission.name}
+                    </FormLabel>
+                    <FormDescription>
+                      {permission.description}
+                    </FormDescription>
+                  </div>
+                </FormItem>
+              )
+            }}
+          />
+        ))}
+      </div>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+## 表单状态管理模板
+
+### 创建/编辑表单分离模式(推荐)
+
+基于 `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. 表单验证
@@ -192,12 +635,22 @@ nickname: z.string().optional()
 - 表单间距统一使用 `space-y-4`
 - 移动端友好的布局
 
+### 5. 性能优化
+- 使用 React.memo 优化表单组件
+- 合理使用 useCallback 和 useMemo
+- 避免不必要的重新渲染
+
+### 6. 错误处理
+- 统一的错误处理机制
+- 友好的错误提示信息
+- 网络错误重试机制
+
 ## 使用示例
 
 ### 完整实现参考
 ```typescript
-// 创建用户
-const handleCreateSubmit = async (data: CreateUserFormData) => {
+// 创建记录
+const handleCreateSubmit = async (data: CreateFormData) => {
   try {
     const res = await apiClient.$post({ json: data });
     if (res.status !== 201) throw new Error('创建失败');
@@ -209,8 +662,8 @@ const handleCreateSubmit = async (data: CreateUserFormData) => {
   }
 };
 
-// 更新用户
-const handleUpdateSubmit = async (data: UpdateUserFormData) => {
+// 更新记录
+const handleUpdateSubmit = async (data: UpdateFormData) => {
   try {
     const res = await apiClient[':id']['$put']({
       param: { id: editingData.id },
@@ -233,11 +686,66 @@ import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, For
 import { Input } from '@/client/components/ui/input';
 import { Button } from '@/client/components/ui/button';
 import { Switch } from '@/client/components/ui/switch';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/client/components/ui/select';
+import { Textarea } from '@/client/components/ui/textarea';
+import { Checkbox } from '@/client/components/ui/checkbox';
 
 // 对话框相关
 import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
 
+// 高级组件
+import { Popover, PopoverContent, PopoverTrigger } from '@/client/components/ui/popover';
+import { Calendar } from '@/client/components/ui/calendar';
+import AvatarSelector from '@/client/admin-shadcn/components/AvatarSelector';
+
 // 表单工具
 import { useForm } from 'react-hook-form';
 import { zodResolver } from '@hookform/resolvers/zod';
-import { toast } from 'sonner';
+import { toast } from 'sonner';
+import { format } from 'date-fns';
+import { CalendarIcon } from 'lucide-react';
+import { cn } from '@/client/lib/utils';
+```
+
+## 常见问题解决方案
+
+### 1. 表单默认值问题
+```typescript
+// 正确处理 null/undefined 值
+defaultValues: {
+  name: null, // 允许 null
+  description: undefined, // 允许 undefined
+}
+```
+
+### 2. 数字类型转换
+```typescript
+// 在 onChange 中转换类型
+onChange={(e) => field.onChange(Number(e.target.value))}
+```
+
+### 3. 日期类型处理
+```typescript
+// 日期选择器返回值处理
+onSelect={(date) => field.onChange(date ? new Date(date) : null)}
+```
+
+### 4. 数组类型处理
+```typescript
+// 复选框组处理数组
+onCheckedChange={(checked) => {
+  const newValue = checked 
+    ? [...field.value, item.id] 
+    : field.value.filter(id => id !== item.id);
+  field.onChange(newValue);
+}}
+```
+
+### 5. 表单重置注意事项
+```typescript
+// 更新表单时正确重置
+updateForm.reset({
+  ...data,
+  password: undefined, // 密码字段特殊处理
+  confirmPassword: undefined,
+});

+ 0 - 0
roo/shadcn-manage-page.md → .roo/commands/shadcn-manage-page.md


+ 0 - 15
.roo/rules/01-general.md

@@ -28,19 +28,6 @@ mini/
 
 ### 前端
 
-#### 管理后台 (Admin) antd
-- **React 19** - 前端框架
-- **TypeScript** (严格模式) - 类型系统
-- **Vite 7** - 构建工具
-- **React Router 7** - 路由管理
-- **Ant Design 5** - UI组件库
-- **Tailwind CSS 4** - 样式框架
-- **React Query (TanStack) 5** - 数据获取和缓存
-- **React Hook Form** - 表单处理
-- **Lucide React** - 图标库
-- **Heroicons** - 图标库
-- **React Toastify** - 消息通知
-
 #### 管理后台 (Admin) shadcn-ui
 - **React 19** - 前端框架
 - **TypeScript** (严格模式) - 类型系统
@@ -103,8 +90,6 @@ mini/
 - **axios** - HTTP客户端
 
 ### 数据可视化
-- **Ant Design Plots** - 图表库
-- **Three.js** - 3D可视化
 
 ### 开发工具
 - **TypeScript** - 类型检查

+ 3 - 4
.roo/rules/11-custom-crud.md

@@ -78,7 +78,6 @@
      import { DataSource, Repository } from 'typeorm';
      import { YourEntity } from './your-entity.entity';
      import { CreateYourEntityDto, UpdateYourEntityDto } from './your-entity.entity';
-     import { AppError } from '@/server/utils/errorHandler';
      
      export class YourEntityService {
        private repository: Repository<YourEntity>;
@@ -113,7 +112,7 @@
        async findById(id: number): Promise<YourEntity> {
          const entity = await this.repository.findOneBy({ id });
          if (!entity) {
-           throw new AppError('实体不存在', 404);
+           throw new Error('实体不存在');
          }
          return entity;
        }
@@ -125,7 +124,7 @@
          // 业务规则验证示例
          const existing = await this.repository.findOneBy({ name: data.name });
          if (existing) {
-           throw new AppError('名称已存在', 400);
+           throw new Error('名称已存在');
          }
          
          const entity = this.repository.create(data);
@@ -155,7 +154,7 @@
          
          // 权限检查示例
          if (entity.createdBy !== userId) {
-           throw new AppError('没有删除权限', 403);
+           throw new Error('没有删除权限');
          }
          
          await this.repository.remove(entity);

+ 3 - 3
.roo/rules/11-entity-creation.md

@@ -20,7 +20,7 @@
 
 ### 开发步骤概要
 
-1. **创建实体**:在`src/server/modules/[模块名]/[实体名].entity.ts`定义实体类和Zod Schema
+1. **创建实体**:在`src/server/modules/[模块名]/[实体名].entity.ts`定义实体类和在`src/server/modules/[模块名]/[实体名].schema.ts`定义实体Zod Schema
 2. **注册实体**:在`src/server/data-source.ts`中注册新实体
 3. **创建Service**:继承`GenericCrudService`实现基础CRUD操作
 4. **创建API路由**:使用`createCrudRoutes`快速生成CRUD路由
@@ -38,7 +38,7 @@
 
 ### 开发步骤概要
 
-1. **创建实体**:在`src/server/modules/[模块名]/[实体名].entity.ts`定义实体类和Zod Schema
+1. **创建实体**:在`src/server/modules/[模块名]/[实体名].entity.ts`定义实体类和在`src/server/modules/[模块名]/[实体名].schema.ts`定义实体Zod Schema
 2. **注册实体**:在`src/server/data-source.ts`中注册新实体
 3. **创建自定义Service**:实现包含复杂业务逻辑的数据访问方法
 4. **创建自定义API路由**:手动实现CRUD路由及处理逻辑
@@ -52,7 +52,7 @@
 详细流程请参见[自定义复杂CRUD开发流程规范](./11-custom-crud.md)
 ## 注意事项
 
-1. 实体Schema必须在实体文件中定义,路由中直接引用,不要重复定义
+1. 实体Schema必须在实体.schema.ts文件中定义,路由中直接引用,不要重复定义
 2. 前端表格/表单字段必须与实体定义保持一致
 3. 确保所有API调用都有正确的类型推断
 4. 参考现有模块实现保持风格一致

+ 29 - 0
.roo/rules/12-generic-crud.md

@@ -93,6 +93,7 @@ export default yourEntityRoutes;
 | `relations` | `string[]` | 关联查询配置,指定需要关联查询的关系 | 否 |
 | `middleware` | `any[]` | 应用于所有CRUD路由的中间件数组 | 否 |
 | `relationFields` | `RelationFieldOptions` | 多对多关联字段配置,支持通过ID数组操作关联关系 | 否 |
+| `readOnly` | `boolean` | 只读模式,只生成GET路由,默认false | 否 |
 
 ### 2.3 生成的路由
 
@@ -106,6 +107,34 @@ export default yourEntityRoutes;
 | PUT | `/{id}` | 更新实体 |
 | DELETE | `/{id}` | 删除实体 |
 
+#### 2.3.1 只读模式 (readOnly)
+
+通过设置 `readOnly: true` 可以创建只读路由,仅包含读取操作:
+
+| 方法 | 路径 | 描述 |
+|------|------|------|
+| GET | `/` | 获取实体列表(支持分页、搜索、排序、关联查询) |
+| GET | `/{id}` | 获取单个实体详情(支持关联查询) |
+
+**使用示例:**
+```typescript
+const readOnlyRoutes = createCrudRoutes({
+  entity: Advertisement,
+  createSchema: CreateAdvertisementDto,
+  updateSchema: UpdateAdvertisementDto,
+  getSchema: AdvertisementSchema,
+  listSchema: AdvertisementSchema,
+  readOnly: true, // 启用只读模式
+  searchFields: ['title', 'description'],
+  relations: ['imageFile'],
+});
+```
+
+**适用场景:**
+- 公开API接口(如游客访问的广告列表)
+- 只读数据展示
+- 需要限制修改操作的接口
+
 ### 2.4 路由注册
 
 生成的路由需要在API入口文件中注册:

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

@@ -1,152 +0,0 @@
----
-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. **错误日志**: 所有错误都会记录详细日志便于调试

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

@@ -1,233 +0,0 @@
----
-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
-}

+ 0 - 490
roo/mini-ui.md

@@ -1,490 +0,0 @@
----
-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版本

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

@@ -1,751 +0,0 @@
----
-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,
-});

+ 0 - 5
roo/shadcn.md

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