Преглед на файлове

Merge remote-tracking branch 'upstream/shadcn-ui' into file-shadcn

yourname преди 4 месеца
родител
ревизия
dcb628ad59
променени са 85 файла, в които са добавени 9827 реда и са изтрити 2950 реда
  1. 6 0
      .roo/commands/check-api-实体与页面字段对应检查.md
  2. 5 0
      .roo/commands/check-curd-页面实体路由对应检查.md
  3. 382 0
      .roo/commands/check-filter-检查并修正筛选表单.md
  4. 6 0
      .roo/commands/check-实体与schema对应检查.md
  5. 112 0
      .roo/commands/check-监控数据统计修改前检查.md
  6. 264 0
      .roo/commands/check-管理后台菜单修改前检查.md
  7. 123 0
      .roo/commands/command-创建新Roo指令.md
  8. 152 0
      .roo/commands/download-从URL下载文件到MinIO.md
  9. 161 0
      .roo/commands/file-实体文件关联开发指南.md
  10. 32 0
      .roo/commands/generic-crud-从SQL创建CRUD.md
  11. 12 0
      .roo/commands/generic-crud-创建公共只读路由.md
  12. 60 0
      .roo/commands/generic-crud-创建实体schema.md
  13. 504 0
      .roo/commands/generic-crud-扩展路由开发指南.md
  14. 11 0
      .roo/commands/generic-crud-注册公共只读路由.md
  15. 9 0
      .roo/commands/generic-crud-路由注册指南.md
  16. 15 0
      .roo/commands/generic-crud-通用CRUD开发指南.md
  17. 203 0
      .roo/commands/mini-auth-小程序认证钩子使用.md
  18. 16 0
      .roo/commands/mini-check-页面实体路由检查.md
  19. 623 0
      .roo/commands/mini-form-小程序表单开发指南.md
  20. 9 0
      .roo/commands/mini-navbar-顶部导航条使用.md
  21. 233 0
      .roo/commands/mini-platform-小程序环境检测.md
  22. 370 0
      .roo/commands/mini-rpc-小程序RPC开发规范.md
  23. 6 0
      .roo/commands/mini-shadui-可用组件检查.md
  24. 1193 0
      .roo/commands/mini-shadui-页面开发指南.md
  25. 7 0
      .roo/commands/mini-tabbar-布局组件使用.md
  26. 6 0
      .roo/commands/mini-tabbar-页面加入标签栏.md
  27. 490 0
      .roo/commands/mini-ui-小程序UI开发指南.md
  28. 282 0
      .roo/commands/mini-ui-轮播图组件使用.md
  29. 56 0
      .roo/commands/rename_files.sh
  30. 15 0
      .roo/commands/rpc-type-提取响应请求类型.md
  31. 16 0
      .roo/commands/schema-添加中文错误提示.md
  32. 215 0
      .roo/commands/shadcn-使用设备类型选择器.md
  33. 5 0
      .roo/commands/shadcn-创建页面和组件.md
  34. 19 0
      .roo/commands/shadcn-实体选择器创建.md
  35. 219 0
      .roo/commands/shadcn-管理页表单分离.md
  36. 722 0
      .roo/commands/shadcn-管理页表单开发.md
  37. 879 0
      .roo/commands/shadcn-管理页面开发指南.md
  38. 7 0
      .roo/commands/show.sh
  39. 213 0
      .roo/commands/user-实体用户关联开发.md
  40. 28 13
      .roo/rules/01-general.md
  41. 1 1
      .roo/rules/07-openapi.md
  42. 26 21
      .roo/rules/10-entity.md
  43. 3 4
      .roo/rules/11-custom-crud.md
  44. 3 3
      .roo/rules/11-entity-creation.md
  45. 59 2
      .roo/rules/12-generic-crud.md
  46. 1 1
      server.js
  47. 0 43
      src/client/admin-shadcn/components/ErrorPage.tsx
  48. 0 25
      src/client/admin-shadcn/components/NotFoundPage.tsx
  49. 0 38
      src/client/admin-shadcn/components/ProtectedRoute.tsx
  50. 0 140
      src/client/admin-shadcn/hooks/AuthProvider.tsx
  51. 0 53
      src/client/admin-shadcn/index.tsx
  52. 0 237
      src/client/admin-shadcn/pages/Dashboard.tsx
  53. 0 178
      src/client/admin-shadcn/pages/Login.tsx
  54. 0 0
      src/client/admin/components/AvatarSelector.tsx
  55. 0 0
      src/client/admin/components/DataTablePagination.tsx
  56. 14 14
      src/client/admin/components/ErrorPage.tsx
  57. 2 3
      src/client/admin/components/NotFoundPage.tsx
  58. 9 8
      src/client/admin/components/ProtectedRoute.tsx
  59. 18 25
      src/client/admin/index.tsx
  60. 185 157
      src/client/admin/layouts/MainLayout.tsx
  61. 43 23
      src/client/admin/menu.tsx
  62. 226 64
      src/client/admin/pages/Dashboard.tsx
  63. 133 84
      src/client/admin/pages/Login.tsx
  64. 546 269
      src/client/admin/pages/Users.tsx
  65. 0 65
      src/client/home-shadcn/components/ErrorPage.tsx
  66. 0 49
      src/client/home-shadcn/components/NotFoundPage.tsx
  67. 0 44
      src/client/home-shadcn/components/ProtectedRoute.tsx
  68. 0 140
      src/client/home-shadcn/hooks/AuthProvider.tsx
  69. 0 36
      src/client/home-shadcn/index.tsx
  70. 0 14
      src/client/home-shadcn/layouts/MainLayout.tsx
  71. 0 133
      src/client/home-shadcn/pages/LoginPage.tsx
  72. 0 234
      src/client/home-shadcn/pages/MemberPage.tsx
  73. 0 178
      src/client/home-shadcn/pages/RegisterPage.tsx
  74. 0 49
      src/client/home-shadcn/routes.tsx
  75. 52 36
      src/client/home/components/ErrorPage.tsx
  76. 35 21
      src/client/home/components/NotFoundPage.tsx
  77. 16 9
      src/client/home/components/ProtectedRoute.tsx
  78. 8 0
      src/client/home/index.tsx
  79. 44 2
      src/client/home/pages/HomePage.tsx
  80. 110 110
      src/client/home/pages/LoginPage.tsx
  81. 208 127
      src/client/home/pages/MemberPage.tsx
  82. 141 153
      src/client/home/pages/RegisterPage.tsx
  83. 2 2
      src/client/index.tsx
  84. 204 125
      src/server/utils/generic-crud.routes.ts
  85. 52 17
      src/server/utils/generic-crud.service.ts

+ 6 - 0
.roo/commands/check-api-实体与页面字段对应检查.md

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

+ 5 - 0
.roo/commands/check-curd-页面实体路由对应检查.md

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

+ 382 - 0
.roo/commands/check-filter-检查并修正筛选表单.md

@@ -0,0 +1,382 @@
+---
+description: "检查并修正筛选表单指令"
+---
+
+# 筛选表单检查与修复指南
+
+## 问题描述
+
+在使用 React Hook Form 和状态管理进行筛选表单开发时,常见的同步问题包括:
+
+1. **表单状态与查询参数不同步**:重置操作只清除查询参数,但表单字段值保持不变
+2. **表单验证问题**:字段验证规则不完整或错误
+3. **UI状态不一致**:筛选条件显示与实际查询条件不符
+
+## 检查步骤
+
+### 1. 检查表单重置功能
+```bash
+# 检查 handleReset 函数是否同时重置表单和查询参数
+grep -n "handleReset" src/**/*.tsx
+```
+
+### 2. 检查表单字段同步
+```bash
+# 检查表单字段是否与查询参数正确映射
+grep -n "setSearchParams\|form.reset" src/**/*.tsx
+```
+
+### 3. 验证表单字段定义
+```bash
+# 检查所有表单字段是否正确定义
+grep -n "FormField\|control" src/**/*.tsx | head -10
+```
+
+## 常见问题及修复方案
+
+### 问题1: 重置功能不完整
+
+**症状**:点击重置按钮后,查询参数被清除但表单字段值保持不变
+
+**修复方案**:
+```typescript
+// 错误示例 - 只重置查询参数
+const handleReset = () => {
+  setSearchParams({
+    status: undefined,
+    keyword: undefined,
+    // ...其他参数
+  });
+};
+
+// 正确示例 - 同时重置表单和查询参数
+const handleReset = () => {
+  // 重置表单字段值
+  form.reset({
+    status: undefined,
+    keyword: undefined,
+    dateRange: undefined,
+    // ...所有表单字段
+  });
+  
+  // 重置搜索参数
+  setSearchParams({
+    status: undefined,
+    keyword: undefined,
+    startDate: undefined,
+    endDate: undefined,
+    // ...对应查询参数
+  });
+};
+```
+
+### 问题2: Select组件缺少"全部"选项
+
+**症状**:筛选表单中的Select组件缺少"全部"选项,用户无法取消筛选
+
+**修复方案**:
+```typescript
+// 错误示例 - 缺少全部选项
+<SelectContent>
+  {Object.values(StatusEnum).map(status => (
+    <SelectItem key={status} value={status}>
+      {StatusNameMap[status]}
+    </SelectItem>
+  ))}
+</SelectContent>
+
+// 正确示例 - 添加全部选项(使用特殊值"all"并转换为undefined)
+<Select onValueChange={(value) => field.onChange(value === "all" ? undefined : value)} value={field.value || "all"}>
+  <FormControl>
+    <SelectTrigger>
+      <SelectValue placeholder="全部状态" />
+    </SelectTrigger>
+  </FormControl>
+  <SelectContent>
+    <SelectItem value="all">全部状态</SelectItem>
+    {Object.values(StatusEnum).map(status => (
+      <SelectItem key={status} value={status}>
+        {StatusNameMap[status]}
+      </SelectItem>
+    ))}
+  </SelectContent>
+</Select>
+```
+
+**注意事项**:
+- Radix UI Select组件不允许使用空字符串作为SelectItem的value
+- 使用特殊值"all"来表示全部选项,在onChange时转换为undefined
+- 在value prop中使用 `field.value || "all"` 来确保选中状态正确显示
+
+### 问题2: 表单字段映射错误
+
+**症状**:表单字段名称与查询参数名称不一致
+
+**修复方案**:
+```typescript
+// 确保表单字段名与查询参数名正确映射
+const handleSearch = (values: any) => {
+  setSearchParams({
+    status: values.status,           // 表单字段名 -> 查询参数名
+    keyword: values.keyword,         // 保持一致
+    startDate: values.dateRange?.[0]?.toISOString(),  // 复杂映射
+    endDate: values.dateRange?.[1]?.toISOString(),
+  });
+};
+```
+
+### 问题3: 初始状态不一致
+
+**症状**:组件挂载时表单初始值与查询参数不匹配
+
+**修复方案**:
+```typescript
+// 使用 useEffect 同步初始状态
+useEffect(() => {
+  const initialValues = {
+    status: searchParams.status || undefined,
+    keyword: searchParams.keyword || undefined,
+    dateRange: searchParams.startDate && searchParams.endDate 
+      ? [new Date(searchParams.startDate), new Date(searchParams.endDate)]
+      : undefined,
+  };
+  form.reset(initialValues);
+}, []);
+```
+
+## 通用CRUD filters参数使用指南
+
+### 问题4: 未使用通用CRUD的filters参数
+
+**症状**:页面使用独立的查询参数而不是通用CRUD的标准filters参数
+
+**修复方案**:
+```typescript
+// 错误示例 - 使用独立参数
+const res = await client.$get({
+  query: {
+    page: 1,
+    pageSize: 10,
+    status: searchParams.status,
+    problemType: searchParams.problemType,
+    keyword: searchParams.keyword,
+    startDate: searchParams.startDate,
+    endDate: searchParams.endDate,
+  }
+});
+
+// 正确示例 - 使用通用CRUD filters参数
+const filters: any = {};
+if (searchParams.status) filters.status = searchParams.status;
+if (searchParams.problemType) filters.problemType = searchParams.problemType;
+if (searchParams.startDate && searchParams.endDate) {
+  filters.createdAt = {
+    between: [searchParams.startDate, searchParams.endDate]
+  };
+}
+
+const res = await client.$get({
+  query: {
+    page: 1,
+    pageSize: 10,
+    keyword: searchParams.keyword,  // 独立的关键词参数
+    filters: Object.keys(filters).length > 0 ? JSON.stringify(filters) : undefined,
+  }
+});
+```
+
+### 问题5: 关联字段搜索与筛选不支持
+
+**症状**:无法通过关联实体的字段进行搜索和筛选(如设备名称、用户名称等)
+
+**修复方案**:
+
+1. **配置searchFields和relations**:
+```typescript
+const routes = createCrudRoutes({
+  entity: YourEntity,
+  searchFields: ['title', 'description', 'device.zichanInfo.assetName'], // 支持关联字段搜索(格式:relation.field 或 relation.nestedRelation.field)
+  relations: ['device.zichanInfo'], // 确保关联数据被加载
+  // ...其他配置
+});
+```
+
+2. **使用关联字段筛选**:
+```typescript
+// 在filters参数中使用关联字段进行精确筛选
+const filters = {
+  'device.zichanInfo.assetName': '服务器001',           // 精确匹配
+  'device.zichanInfo.status': { gte: 1 },              // 范围查询
+  'device.zichanInfo.department.name': '%IT%'          // 模糊匹配
+};
+
+const res = await client.$get({
+  query: {
+    page: 1,
+    pageSize: 10,
+    filters: JSON.stringify(filters)
+  }
+});
+```
+
+**别名生成规则**:
+- 单级关联:`device.zichanInfo` → 别名:`device_zichanInfo`
+- 嵌套关联:`device.zichanInfo.department` → 别名:`device_zichanInfo_department`
+- 字段引用:`device_zichanInfo.assetName`, `device_zichanInfo_department.name`
+
+**支持的筛选操作**:
+- 精确匹配:`{ "relation.field": "value" }`
+- 模糊匹配:`{ "relation.field": "%value%" }`
+- 范围查询:`{ "relation.field": { "gte": min, "lte": max } }`
+- IN查询:`{ "relation.field": [value1, value2, ...] }`
+
+## 完整代码示例
+
+### 修复后的筛选表单组件(使用通用CRUD filters)
+
+```typescript
+import React from 'react';
+import { useForm } from 'react-hook-form';
+import { Form, FormField, FormItem, FormLabel, FormControl } from '@/components/ui/form';
+import { Input } from '@/components/ui/input';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Button } from '@/components/ui/button';
+import { workOrderClient } from '@/client/api';
+
+export function FilterForm() {
+  const [searchParams, setSearchParams] = useState({});
+  const form = useForm();
+
+  // 搜索处理 - 使用通用CRUD filters参数
+  const handleSearch = (values: any) => {
+    // 构建filters对象
+    const filters: any = {};
+    if (values.status) filters.status = values.status;
+    if (values.problemType) filters.problemType = values.problemType;
+    if (values.dateRange?.[0] && values.dateRange?.[1]) {
+      filters.createdAt = {
+        between: [values.dateRange[0], values.dateRange[1]]
+      };
+    }
+
+    setSearchParams({
+      keyword: values.keyword,
+      filters: Object.keys(filters).length > 0 ? JSON.stringify(filters) : undefined,
+    });
+  };
+
+  // 重置处理 - 修复后的版本
+  const handleReset = () => {
+    // 重置表单字段值
+    form.reset({
+      status: undefined,
+      keyword: undefined,
+      problemType: undefined,
+      dateRange: undefined,
+    });
+    
+    // 重置搜索参数
+    setSearchParams({
+      keyword: undefined,
+      filters: undefined,
+    });
+  };
+
+  return (
+    <Form {...form}>
+      <form onSubmit={form.handleSubmit(handleSearch)}>
+        <FormField
+          control={form.control}
+          name="status"
+          render={({ field }) => (
+            <FormItem>
+              <FormLabel>状态</FormLabel>
+              <Select onValueChange={(value) => field.onChange(value === "all" ? undefined : value)} value={field.value || "all"}>
+                <FormControl>
+                  <SelectTrigger>
+                    <SelectValue placeholder="选择状态" />
+                  </SelectTrigger>
+                </FormControl>
+                <SelectContent>
+                  <SelectItem value="all">全部状态</SelectItem>
+                  <SelectItem value="active">活跃</SelectItem>
+                  <SelectItem value="inactive">非活跃</SelectItem>
+                </SelectContent>
+              </Select>
+            </FormItem>
+          )}
+        />
+        
+        <FormField
+          control={form.control}
+          name="keyword"
+          render={({ field }) => (
+            <FormItem>
+              <FormLabel>关键字</FormLabel>
+              <FormControl>
+                <Input placeholder="输入关键字" {...field} />
+              </FormControl>
+            </FormItem>
+          )}
+        />
+        
+        <div className="flex gap-2">
+          <Button type="submit">查询</Button>
+          <Button type="button" variant="outline" onClick={handleReset}>
+            重置
+          </Button>
+        </div>
+      </form>
+    </Form>
+  );
+}
+```
+
+## 最佳实践
+
+1. **保持同步**:始终确保表单状态与查询参数同步
+2. **完整重置**:重置操作应同时清除表单字段和查询参数
+3. **类型安全**:为表单值和查询参数定义明确的 TypeScript 类型
+4. **测试验证**:编写测试用例验证重置功能正常工作
+5. **用户体验**:重置后应自动触发数据重新加载
+
+## 自动化检查脚本
+
+```bash
+#!/bin/bash
+# 检查筛选表单常见问题
+
+echo "=== 筛选表单检查 ==="
+
+# 检查 handleReset 函数
+echo "1. 检查 handleReset 函数:"
+grep -A 10 -B 2 "handleReset" src/**/*.tsx | grep -E "setSearchParams|form.reset"
+
+# 检查表单字段映射
+echo "2. 检查表单字段映射:"
+grep -A 5 -B 5 "handleSearch" src/**/*.tsx
+
+# 检查初始状态同步
+echo "3. 检查初始状态同步:"
+grep -A 10 -B 2 "useEffect.*searchParams" src/**/*.tsx
+
+# 检查通用CRUD filters参数使用
+echo "4. 检查通用CRUD filters参数使用:"
+grep -A 5 -B 5 "filters.*JSON.stringify" src/**/*.tsx
+
+# 检查关联字段搜索配置
+echo "5. 检查关联字段搜索配置:"
+grep -A 3 -B 3 "searchFields.*\." src/server/api/**/index.ts
+
+echo "=== 检查完成 ==="
+```
+
+## 最佳实践更新
+
+6. **标准接口**:优先使用通用CRUD的filters参数而不是独立参数
+7. **关联搜索**:合理配置searchFields和relations支持关联字段搜索
+8. **性能优化**:避免在filters中使用过多关联查询,必要时使用自定义服务方法
+
+使用此指令可以快速识别和修复筛选表单中的常见同步问题,包括通用CRUD filters参数的使用和关联字段搜索功能。
+
+使用此指令可以快速识别和修复筛选表单中的常见同步问题。

+ 6 - 0
.roo/commands/check-实体与schema对应检查.md

@@ -0,0 +1,6 @@
+---
+description: "检查schema与实体是否对应指令"
+---
+
+检查schema与实体是否对应
+没对应的修改 schema

+ 112 - 0
.roo/commands/check-监控数据统计修改前检查.md

@@ -0,0 +1,112 @@
+---
+description: "监控数据统计修改前检查指令"
+---
+
+监控数据统计修改前先按需求检查所需文件,为修改提供上下文
+
+## 概述
+本文档提供了监控数据统计功能相关的所有文件检查清单,包括页面、API、实体、路由等。
+
+## 1. 前端页面文件
+
+### 1.1 监控统计页面
+- **文件**: `src/client/admin/pages/MonitorStats.tsx`
+- **状态**: ✅ 已存在
+- **功能**: 显示监控数据统计表格,支持时间范围查询
+- **关键组件**: 
+  - 时间范围选择表单
+  - 统计数据表格
+  - 手工巡检功能
+
+### 1.2 相关组件
+- **监控数据表格组件**: `src/client/admin/components/MonitorStats/MonitorStatsTable.tsx`
+- **监控数据Hook**: `src/client/admin/hooks/useMonitorData.ts`
+
+## 2. 后端API文件
+
+### 2.1 监控统计API路由
+- **主路由文件**: `src/server/api/monitor-stats/index.ts`
+- **GET路由**: `src/server/api/monitor-stats/get.ts`
+- **API注册**: `src/server/api.ts` (第184行)
+
+### 2.2 相关API路由
+- **监控数据API**: `src/server/api/monitor-data/`
+- **监控地图API**: `src/server/api/monitor-map/`
+- **监控任务API**: `src/server/api/monitor-task/`
+
+## 3. 实体定义文件
+
+### 3.1 设备监控数据实体
+- **实体文件**: `src/server/modules/devices/device-monitor-data.entity.ts`
+- **Schema文件**: `src/server/modules/devices/device-monitor-data.schema.ts`
+- **关键字段**:
+  - `deviceId`: 设备ID
+  - `metricType`: 监控指标类型
+  - `metricValue`: 监控值
+  - `collectTime`: 采集时间
+  - `status`: 状态(0正常/1异常)
+
+### 3.2 相关实体
+- **设备实例**: `src/server/modules/devices/device-instances.entity.ts`
+- **设备类型**: `src/server/modules/devices/device-types.entity.ts`
+
+## 4. 服务层文件
+
+### 4.1 设备监控数据服务
+- **服务文件**: `src/server/modules/devices/device-monitor-data.service.ts`
+- **关键方法**:
+  - `queryMonitorStats()`: 查询监控统计数据
+  - `getMonitorDataList()`: 获取监控数据列表
+  - `addMonitorData()`: 添加监控数据
+
+### 4.2 相关服务
+- **监控服务**: `src/server/modules/monitor/monitor.service.ts`
+- **告警服务**: `src/server/modules/monitor/alert.service.ts`
+
+## 5. 客户端API定义
+
+### 5.1 监控统计客户端
+- **定义位置**: `src/client/api.ts` (第134-136行)
+- **客户端名称**: `monitorStatsClient`
+- **类型定义**: `MonitorStatsRoutes`
+
+### 5.2 相关客户端
+- **监控数据客户端**: `monitorDataClient`
+- **监控地图客户端**: `amonitorMapClient`
+
+## 6. 类型定义文件
+
+### 6.1 监控类型定义
+- **文件**: `src/share/monitorTypes.ts`
+- **包含类型**:
+  - `MetricType`: 监控指标类型枚举
+  - 各种监控相关的接口定义
+
+### 6.2 响应类型
+- **监控统计响应**: `MonitorStatsItem` 接口
+- **监控数据响应**: `MonitorDataResponse` 类型
+
+## 7. 数据库查询分析
+
+### 7.1 统计查询逻辑
+- **位置**: `device-monitor-data.service.ts` 第203-268行
+- **查询特点**:
+  - 按5分钟时间分组
+  - 关联设备实例和设备类型
+  - 统计异常设备数量
+  - 使用原生SQL查询构建
+
+### 7.2 时间处理
+- **开始时间**: `time_group` 字段 (UTC时间)
+- **结束时间**: `time_group + 4分钟` (UTC时间)
+- **时间格式**: `YYYY-MM-DD HH:mm:ss`
+
+## 10. 相关功能链接
+
+### 10.1 巡检功能
+- **巡检结果API**: `src/server/api/inspection-results/`
+- **巡检任务API**: `src/server/api/inspections/`
+
+### 10.2 告警功能
+- **告警规则API**: `src/server/api/alert-rules/`
+- **告警记录API**: `src/server/api/alerts/`

+ 264 - 0
.roo/commands/check-管理后台菜单修改前检查.md

@@ -0,0 +1,264 @@
+---
+description: "管理后台菜单修改前检查指令"
+---
+
+管理后台菜单修改前先按需求检查所需文件,为修改提供上下文
+
+## 概述
+本文档提供了管理后台菜单功能相关的所有文件检查清单,包括菜单配置、页面组件、API路由、实体定义等。
+
+## 1. 菜单配置文件
+
+### 1.1 主菜单配置文件
+- **文件**: `src/client/admin/menu.tsx`
+- **状态**: ✅ 已存在
+- **功能**: 定义管理后台所有菜单项结构、图标、路径和权限
+- **关键配置**:
+  - 菜单项数组 `menuItems` (第85-328行)
+  - 用户菜单项 `userMenuItems` (第331-349行)
+  - 菜单搜索Hook `useMenuSearch`
+  - 菜单状态管理Hook `useMenu`
+
+### 1.2 菜单项结构
+- **顶级菜单**: 9个主要模块
+- **子菜单**: 多个模块包含二级菜单
+- **权限控制**: 部分菜单项包含权限配置
+
+## 2. 前端页面文件
+
+### 2.1 路由配置文件
+- **文件**: `src/client/admin/routes.tsx`
+- **状态**: ✅ 已存在
+- **功能**: 定义所有页面路由与组件的映射关系
+- **关键配置**:
+  - 路由路径与页面组件对应关系
+  - 错误页面处理
+  - 路由保护配置
+
+### 2.2 页面组件文件
+根据菜单配置,需要检查以下页面组件文件:
+
+#### 监控管理模块
+- `src/client/admin/pages/DeviceMonitor.tsx` - 设备实时监控
+- `src/client/admin/pages/TemperatureHumidity.tsx` - 温湿度监控  
+- `src/client/admin/pages/SmokeWater.tsx` - 烟感及水浸监控
+- `src/client/admin/pages/DeviceMap.tsx` - 设备地图监控
+- `src/client/admin/pages/AlertRecords.tsx` - 告警记录
+- `src/client/admin/pages/AlertHandleLogs.tsx` - 告警处理记录
+- `src/client/admin/pages/AlertNotifyConfig.tsx` - 告警通知配置
+- `src/client/admin/pages/DeviceAlertRule.tsx` - 设备告警规则
+- `src/client/admin/pages/MonitorTask.tsx` - 监控任务
+
+#### 资产管理模块
+- `src/client/admin/pages/ZichanCategory.tsx` - 资产分类
+- `src/client/admin/pages/ZichanArea.tsx` - 资产区域
+- `src/client/admin/pages/Zichan.tsx` - 资产信息
+- `src/client/admin/pages/ZichanTransfer.tsx` - 资产流转
+
+#### 设备管理模块
+- `src/client/admin/pages/DeviceTypes.tsx` - 设备类型
+- `src/client/admin/pages/DeviceInstances.tsx` - 设备实例
+- `src/client/admin/pages/ModbusRtuDevices.tsx` - Modbus RTU设备
+- `src/client/admin/pages/GreenhouseProtocol.tsx` - 温室协议设置
+- `src/client/admin/pages/RackInfo.tsx` - 机柜管理
+- `src/client/admin/pages/RackServerType.tsx` - 机柜服务器类型
+- `src/client/admin/pages/RackServer.tsx` - 机柜服务器
+
+#### 其他模块
+- `src/client/admin/pages/WorkOrders.tsx` - 工单管理
+- `src/client/admin/pages/MonitorStats.tsx` - 监控数据统计
+- `src/client/admin/pages/InspectionResults.tsx` - 巡检结果查询
+- `src/client/admin/pages/Users.tsx` - 用户管理
+- `src/client/admin/pages/ThemeSettings.tsx` - 主题设置
+- `src/client/admin/pages/TimeCalibration.tsx` - 时间校准
+- `src/client/admin/pages/Settings.tsx` - 系统配置
+- `src/client/admin/pages/KnowInfo.tsx` - 知识库
+- `src/client/admin/pages/Files.tsx` - 文件库
+- `src/client/admin/pages/Messages.tsx` - 消息中心
+- `src/client/admin/pages/SmsModule.tsx` - 短信模块
+
+## 3. 后端API文件
+
+### 3.1 API路由注册文件
+- **文件**: `src/server/api.ts`
+- **状态**: ✅ 已存在
+- **功能**: 注册所有API路由到Hono应用
+- **关键配置**:
+  - 路由导入和注册 (第4-36行)
+  - 路由类型定义 (第196-229行)
+
+### 3.2 各模块API路由
+根据菜单功能,需要检查以下API路由:
+
+#### 监控相关API
+- `src/server/api/monitor-data/` - 监控数据API
+- `src/server/api/monitor-map/` - 监控地图API
+- `src/server/api/monitor-task/` - 监控任务API
+- `src/server/api/monitor-stats/` - 监控统计API
+- `src/server/api/alerts/` - 告警API
+- `src/server/api/alert-handle-logs/` - 告警处理记录API
+- `src/server/api/alert-notify-configs/` - 告警通知配置API
+- `src/server/api/alert-rules/` - 告警规则API
+
+#### 资产相关API
+- `src/server/api/zichan/` - 资产信息API
+- `src/server/api/zichan-category/` - 资产分类API
+- `src/server/api/zichan-area/` - 资产区域API
+- `src/server/api/zichan-transfer/` - 资产流转API
+
+#### 设备相关API
+- `src/server/api/device-types/` - 设备类型API
+- `src/server/api/device-instances/` - 设备实例API
+- `src/server/api/modbus-rtu/` - Modbus RTU设备API
+- `src/server/api/rack-info/` - 机柜信息API
+- `src/server/api/rack-server-type/` - 机柜服务器类型API
+- `src/server/api/rack-server/` - 机柜服务器API
+
+#### 其他API
+- `src/server/api/workorders/` - 工单API
+- `src/server/api/inspections/` - 巡检API
+- `src/server/api/inspection-results/` - 巡检结果API
+- `src/server/api/users/` - 用户API
+- `src/server/api/theme-settings/` - 主题设置API
+- `src/server/api/time/` - 时间API
+- `src/server/api/know-info/` - 知识库API
+- `src/server/api/files/` - 文件API
+- `src/server/api/sms-test/` - 短信测试API
+
+## 4. 客户端API定义
+
+### 4.1 客户端API文件
+- **文件**: `src/client/api.ts`
+- **状态**: ✅ 已存在
+- **功能**: 定义所有前端API客户端实例
+- **关键配置**:
+  - 各模块客户端定义 (第38-157行)
+  - 类型导入 (第3-31行)
+
+### 4.2 各模块客户端
+需要检查以下客户端定义:
+- `authClient` - 认证客户端
+- `userClient` - 用户客户端
+- `zichanClient` - 资产客户端
+- `deviceTypesClient` - 设备类型客户端
+- `deviceInstancesClient` - 设备实例客户端
+- `monitorDataClient` - 监控数据客户端
+- `alertsClient` - 告警客户端
+- `workOrderClient` - 工单客户端
+- 等其他相关客户端
+
+## 5. 实体定义文件
+
+### 5.1 主要实体类
+根据菜单功能,需要检查以下实体文件:
+
+#### 监控相关实体
+- `src/server/modules/monitor/` 目录下的实体文件
+- 设备监控数据实体
+- 告警规则实体
+- 监控任务实体
+
+#### 资产相关实体
+- `src/server/modules/zichan/` 目录下的实体文件
+- 资产信息实体
+- 资产分类实体
+- 资产区域实体
+
+#### 设备相关实体
+- `src/server/modules/devices/` 目录下的实体文件
+- 设备类型实体
+- 设备实例实体
+- 机柜相关实体
+
+#### 其他实体
+- `src/server/modules/users/` - 用户实体
+- `src/server/modules/files/` - 文件实体
+- `src/server/modules/workorders/` - 工单实体
+
+## 6. 服务层文件
+
+### 6.1 主要服务类
+需要检查以下服务文件:
+- `src/server/modules/monitor/monitor.service.ts` - 监控服务
+- `src/server/modules/zichan/zichan.service.ts` - 资产服务
+- `src/server/modules/devices/device.service.ts` - 设备服务
+- `src/server/modules/users/user.service.ts` - 用户服务
+- 等其他相关服务
+
+## 7. 权限配置
+
+### 7.1 菜单权限
+- **位置**: `src/client/admin/menu.tsx` 中的 `permission` 字段
+- **当前配置**:
+  - 用户管理: `user:manage`
+  - 系统设置: `system:settings`
+  - 内容管理: `content:manage`
+  - 消息中心: `message:view`
+
+### 7.2 权限验证
+- **中间件**: `src/server/middleware/auth.middleware.ts`
+- **权限检查**: 确保路由有正确的权限验证
+
+## 8. 修改检查清单
+
+### 8.1 添加新菜单项
+1. [ ] 在 `menu.tsx` 中添加菜单项配置
+2. [ ] 创建对应的页面组件文件
+3. [ ] 在 `routes.tsx` 中注册路由
+4. [ ] 创建或更新对应的API路由
+5. [ ] 在 `api.ts` 中注册API路由
+6. [ ] 在客户端API中添加对应客户端
+7. [ ] 创建或更新对应的实体和服务
+8. [ ] 配置权限控制(如需要)
+
+### 8.2 修改现有菜单项
+1. [ ] 更新 `menu.tsx` 中的菜单配置
+2. [ ] 检查对应的页面组件是否需要修改
+3. [ ] 检查API路由是否需要更新
+4. [ ] 验证权限配置是否正确
+
+### 8.3 删除菜单项
+1. [ ] 从 `menu.tsx` 中移除菜单项
+2. [ ] 考虑是否删除对应的页面组件
+3. [ ] 考虑是否删除对应的API路由
+4. [ ] 清理不再使用的客户端API
+
+## 9. 常见问题
+
+### 9.1 菜单项不显示
+- 检查权限配置是否正确
+- 验证用户是否有访问权限
+- 检查路由路径是否正确
+
+### 9.2 页面404错误
+- 检查 `routes.tsx` 中的路由配置
+- 验证页面组件文件是否存在
+- 检查导入路径是否正确
+
+### 9.3 API调用失败
+- 检查客户端API定义是否正确
+- 验证API路由是否已注册
+- 检查权限中间件配置
+
+## 10. 最佳实践
+
+1. **保持一致性**: 菜单项、路由路径、API路径保持命名一致
+2. **权限控制**: 为敏感功能添加适当的权限控制
+3. **错误处理**: 确保页面和API都有适当的错误处理
+4. **类型安全**: 使用TypeScript确保类型安全
+5. **文档更新**: 修改菜单时更新相关文档
+
+## 11. 相关文件链接
+
+### 11.1 核心配置文件
+- 菜单配置: `src/client/admin/menu.tsx`
+- 路由配置: `src/client/admin/routes.tsx`
+- API注册: `src/server/api.ts`
+- 客户端API: `src/client/api.ts`
+
+### 11.2 工具函数
+- 认证中间件: `src/server/middleware/auth.middleware.ts`
+- 错误处理: `src/server/utils/errorHandler.ts`
+- Axios配置: `src/client/utils/axios-fetch.ts`
+
+通过以上检查清单,可以确保在修改管理后台菜单时全面考虑所有相关文件,避免遗漏和错误。

+ 123 - 0
.roo/commands/command-创建新Roo指令.md

@@ -0,0 +1,123 @@
+---
+description: 快速创建新的Roo Code指令,包括标准CRUD指令、自定义指令和常用工作流模板
+argument-hint: <指令类型> [实体名称] [模块名称]
+---
+
+# 创建指令
+
+根据指定的类型和参数快速创建新的Roo Code指令文件。
+
+## 使用方式
+
+- `/command-create crud User users` - 创建用户管理标准CRUD指令
+- `/command-create custom complex-report` - 创建复杂报表自定义指令
+- `/command-create mini-form Login` - 创建小程序登录表单指令
+
+## 指令类型
+
+### 1. 标准CRUD指令 (`crud`)
+创建包含完整增删改查功能的指令模板
+
+**参数**:
+- 实体名称 (PascalCase)
+- 模块名称 (snake_case)
+
+**示例**: `/command-create crud Product products`
+
+### 2. 自定义指令 (`custom`)
+创建灵活的自定义业务逻辑指令
+
+**参数**:
+- 指令名称 (kebab-case)
+
+**示例**: `/command-create custom data-analysis`
+
+### 3. 小程序表单指令 (`mini-form`)
+创建小程序表单页面指令模板
+
+**参数**:
+- 表单名称 (PascalCase)
+
+**示例**: `/command-create mini-form UserProfile`
+
+## 创建步骤
+
+### 步骤1: 确定指令类型
+根据业务需求选择合适的指令类型:
+- 简单数据管理 → 标准CRUD
+- 复杂业务逻辑 → 自定义指令
+- 小程序页面 → 小程序表单
+
+### 步骤2: 命名规范
+- 指令文件名使用 kebab-case
+- 实体名使用 PascalCase
+- 模块名使用 snake_case
+
+### 步骤3: 选择存放位置
+- 项目特定指令: `.roo/commands/`
+- 全局通用指令: `~/.roo/commands/`
+
+## 标准CRUD指令模板
+
+创建标准CRUD指令时,将自动生成:
+1. 实体定义文件
+2. Service类
+3. API路由
+4. 前端页面
+5. 客户端API调用
+
+## 自定义指令模板
+
+创建自定义指令时,提供:
+1. 基础指令框架
+2. 业务逻辑占位
+3. 错误处理模板
+4. 进度跟踪示例
+
+## 小程序表单指令模板
+
+创建小程序表单指令时,包含:
+1. 表单验证模式
+2. 页面组件结构
+3. 样式规范
+4. 提交处理逻辑
+
+## 最佳实践
+
+1. **指令命名**: 使用动词+名词结构,如 `generate-report`, `validate-data`
+2. **参数设计**: 明确指令所需的参数类型和格式
+3. **错误处理**: 包含完整的错误处理和用户反馈
+4. **文档说明**: 在指令顶部添加清晰的描述和使用示例
+5. **版本管理**: 将指令文件纳入版本控制系统
+
+## 示例指令文件结构
+
+```
+.roo/commands/
+├── crud-user-management.md      # 用户管理CRUD
+├── custom-monthly-report.md     # 月度报表生成
+├── mini-form-user-login.md      # 小程序登录表单
+└── api-endpoint-generator.md    # API端点生成器
+```
+
+## 快速创建检查清单
+
+- [ ] 确定指令类型和用途
+- [ ] 选择合适的命名规范
+- [ ] 定义清晰的参数结构
+- [ ] 添加详细的描述文档
+- [ ] 包含错误处理逻辑
+- [ ] 提供使用示例
+- [ ] 测试指令功能
+- [ ] 更新指令索引
+
+## 常见问题
+
+**Q: 如何创建跨项目的通用指令?**
+A: 将指令文件放在 `~/.roo/commands/` 目录下,所有项目都可访问
+
+**Q: 指令参数如何验证?**
+A: 在指令内容中使用正则表达式或条件判断来验证参数格式
+
+**Q: 如何共享团队指令?**
+A: 将指令文件提交到项目仓库的 `.roo/commands/` 目录,团队成员可同步使用

+ 152 - 0
.roo/commands/download-从URL下载文件到MinIO.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. **错误日志**: 所有错误都会记录详细日志便于调试

+ 161 - 0
.roo/commands/file-实体文件关联开发指南.md

@@ -0,0 +1,161 @@
+---
+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` | `fileId` |
+| 关联实体 | `{前缀}File` | `file` |
+| 描述注释 | `{描述}文件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' })
+fileId!: number | null;
+
+@ManyToOne(() => File, { nullable: true })
+@JoinColumn({ name: 'avatar_file_id', referencedColumnName: 'id' })
+file!: File | null;
+```
+
+**Schema定义** ([`src/server/modules/users/user.schema.ts`](src/server/modules/users/user.schema.ts:33-45)):
+```typescript
+fileId: z.number().int().positive().nullable().openapi({
+  example: 1,
+  description: '头像文件ID'
+}),
+file: 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 关联,参考广告实体的实现。
+
+### 场景3: 通用CRUD路由配置
+对于使用通用CRUD路由的实体,需要配置 `relations` 以自动关联查询文件信息:
+
+```typescript
+// 示例:广告实体的通用CRUD配置
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { Advertisement } from '@/server/modules/advertisements/advertisement.entity';
+import { AdvertisementSchema, CreateAdvertisementDto, UpdateAdvertisementDto } from '@/server/modules/advertisements/advertisement.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const advertisementRoutes = createCrudRoutes({
+  entity: Advertisement,
+  createSchema: CreateAdvertisementDto,
+  updateSchema: UpdateAdvertisementDto,
+  getSchema: AdvertisementSchema,
+  listSchema: AdvertisementSchema,
+  searchFields: ['title', 'code'],
+  relations: ['imageFile'], // 关键配置:自动关联查询图片文件
+  middleware: [authMiddleware]
+});
+```
+
+## 注意事项
+
+1. **空值处理**: 字段必须支持 `null`,允许无文件关联
+2. **级联操作**: 默认不级联,删除文件不会影响关联实体
+3. **文件验证**: 前端需先上传文件获取文件ID,再进行实体关联
+4. **类型安全**: 确保所有字段类型定义一致(number | null)
+5. **关联查询**: 配置 `relations` 后,通用CRUD会自动处理关联查询
+
+## 扩展说明
+
+此标准基于用户实体的头像文件关联实现,适用于项目中所有需要文件关联的实体。后续实体按此标准实现即可保持统一性。

+ 32 - 0
.roo/commands/generic-crud-从SQL创建CRUD.md

@@ -0,0 +1,32 @@
+---
+description: "从旧数据sql创建通用curd开发指令"
+---
+先分析旧数据表结构,找出原有的关联关系,在后续创建时加上关联。如商品与商品分类的关联,订单商品与订单的关联等
+
+然后逐个按通用curd开发规范开发
+
+创建实体类 your-entity.entity.ts
+创建实体Zod Schema定义 your-entity.schema.ts
+注册实体到数据源
+创建服务类继承GenericCrudService
+创建通用CRUD路由
+注册路由到API
+创建客户端API调用方法
+创建管理后台页面,按照 .roo/commands/shadcn-manage-page.md 指令规范
+注册路由和菜单
+
+注意:
+1. 旧数据表文件中的时间字段定义,创建时统一改为使用 TypeORM 标准的 @CreateDateColumn 和 @UpdateDateColumn 格式
+2. 为每个验证字段添加 中文错误提示
+
+    示例:
+    ```typescript
+    // 用户 schema
+    export const UserSchema = z.object({
+    username: z.string().min(3, '用户名至少3个字符').max(255, '最多255个字符'),
+    password: z.string().min(6, '密码至少6位').max(255, '最多255位'),
+    phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号'),
+    email: z.email('请输入正确的邮箱格式'),
+    name: z.string().max(255, '姓名最多255个字符').optional()
+    });
+3. 旧数据表文件中的创建人,修改人字段定义,创建时统一改为使用  createdBy, updateBy

+ 12 - 0
.roo/commands/generic-crud-创建公共只读路由.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 前缀

+ 60 - 0
.roo/commands/generic-crud-创建实体schema.md

@@ -0,0 +1,60 @@
+---
+description: "创建已有实体的schema指令"
+---
+
+按通用curd开发规范开发
+
+检查已有实体类文件 your-entity.entity.ts
+检查其关联的实体的 schema文件, 如 file.schema.ts、 user.schema.ts等
+创建实体Zod Schema定义文件 your-entity.schema.ts
+
+注意:
+1. 将在当前实体中定义的枚举移到 your-entity.schema.ts 中定义
+2. 为每个验证字段添加 中文错误提示
+
+    示例:
+    ```typescript
+    // 用户 schema
+    export const UserSchema = z.object({
+        username: z.string().min(3, '用户名至少3个字符').max(255, '最多255个字符'),
+        password: z.string().min(6, '密码至少6位').max(255, '最多255位'),
+        phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号'),
+        email: z.email('请输入正确的邮箱格式'),
+        name: z.string().max(255, '姓名最多255个字符').optional()
+    });
+3. 有关联的实体,其 zod schema 要import 用上
+    示例:
+    ```typescript
+    import { ZichanCategorySchema } from './zichan-category.schema';
+    export const ZichanInfoSchema = z.object({
+        id: z.number().int('必须是整数').positive('必须是正整数').openapi({
+            description: '资产信息ID',
+            example: 1
+        }),
+        category: ZichanCategorySchema.nullable().openapi('设备分类信息'),
+    });
+    ```
+4. 实体中import用到的枚举,your-entity.schema.ts 中也要import用上
+    示例:
+    ```typescript
+    // 教室状态枚举
+    export enum ClassroomStatus {
+    CLOSED = 0,  // 关闭
+    OPEN = 1     // 开放
+    }
+    // 在当前zod v4中 z.enum 代替了 z.nativeEnum;  
+    z.object({
+        // z.enum(ClassroomStatus) 等价于 z.nativeEnum(ClassroomStatus), 不要 z.enum([ClassroomStatus.CLOSED, ClassroomStatus.OPEN]),应该是 z.enum(ClassroomStatus)
+        z.enum(ClassroomStatus).nullable().openapi({
+            description: '状态 (0关闭 1开放)',
+            example: ClassroomStatus.OPEN
+        }),
+    })
+    ```
+
+5. 在当前 zod v4中, z.coerce.date(), z.coerce.number() 等,都要添加类型泛型指定
+    示例:
+    ```typescript
+    z.coerce.date<Date>()
+    z.coerce.number<number>()
+    ```

+ 504 - 0
.roo/commands/generic-crud-扩展路由开发指南.md

@@ -0,0 +1,504 @@
+---
+description: "通用curd扩展路由开发指令"
+---
+
+# 通用CRUD扩展路由开发指令
+
+本指令基于通用CRUD规范,指导如何为已存在的通用CRUD路由添加自定义扩展路由,采用模块化方式保持代码清晰。
+
+## 适用场景
+
+当通用CRUD提供的标准路由(GET /, POST /, GET /{id}, PUT /{id}, DELETE /{id})无法满足业务需求时,需要添加自定义业务路由。
+
+## 开发流程
+
+### 1. **定位现有通用CRUD路由文件**
+找到对应的通用CRUD路由文件,通常位于:
+- `src/server/api/[实体名]/index.ts`
+
+### 2. **创建扩展路由文件**
+为每个扩展功能创建单独的路由文件:
+
+```
+src/server/api/your-entity/
+├── index.ts            # 聚合路由(已存在)
+├── batch/              # 新增 - 批量操作
+│   └── delete.ts       # 批量删除
+├── [id]/               # 新增 - 单条记录扩展操作
+│   ├── status.ts       # 状态更新
+│   ├── toggle.ts       # 状态切换
+│   └── audit.ts        # 审核操作
+├── export.ts           # 新增 - 数据导出
+├── import.ts           # 新增 - 数据导入
+├── stats.ts            # 新增 - 统计信息
+└── upload.ts           # 新增 - 文件上传
+```
+
+### 3. **创建独立扩展路由文件**
+
+#### 3.1 批量删除路由 - `batch/delete.ts`
+```typescript
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { YourEntityService } from '@/server/modules/your-module/your-entity.service';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const routeDef = createRoute({
+  method: 'delete',
+  path: '/',
+  middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': {
+          schema: z.object({
+            ids: z.array(z.number().int().positive()).openapi({
+              description: '要删除的ID列表',
+              example: [1, 2, 3]
+            })
+          })
+        }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '批量删除成功',
+      content: {
+        'application/json': {
+          schema: z.object({
+            deletedCount: z.number().openapi({ example: 3, description: '删除的记录数' })
+          })
+        }
+      }
+    },
+    400: { description: '请求参数错误', content: { 'application/json': { schema: ErrorSchema } } },
+    500: { description: '服务器错误', content: { 'application/json': { schema: ErrorSchema } } }
+  }
+});
+
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const { ids } = await c.req.json();
+    const service = new YourEntityService(AppDataSource);
+    
+    let deletedCount = 0;
+    for (const id of ids) {
+      const result = await service.delete(id);
+      if (result) deletedCount++;
+    }
+    
+    return c.json({ deletedCount }, 200);
+  } catch (error) {
+    return c.json({ code: 500, message: '批量删除失败' }, 500);
+  }
+});
+
+export default app;
+```
+
+#### 3.2 状态更新路由 - `[id]/status.ts`
+```typescript
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { YourEntitySchema } from '@/server/modules/your-module/your-entity.schema';
+import { parseWithAwait } from '@/server/utils/parseWithAwait';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { YourEntityService } from '@/server/modules/your-module/your-entity.service';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const routeDef = createRoute({
+  method: 'patch',
+  path: '/',
+  middleware: [authMiddleware],
+  request: {
+    params: z.object({
+      id: z.string().openapi({
+        param: { name: 'id', in: 'path' },
+        example: '1',
+        description: '记录ID'
+      })
+    }),
+    body: {
+      content: {
+        'application/json': {
+          schema: z.object({
+            status: z.number().openapi({ example: 1, description: '新状态值' })
+          })
+        }
+      }
+    }
+  },
+  responses: {
+    200: { description: '状态更新成功', content: { 'application/json': { schema: YourEntitySchema } } },
+    404: { description: '记录不存在', content: { 'application/json': { schema: ErrorSchema } } },
+    500: { description: '服务器错误', content: { 'application/json': { schema: ErrorSchema } } }
+  }
+});
+
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const { id } = c.req.valid('param');
+    const { status } = await c.req.json();
+    const service = new YourEntityService(AppDataSource);
+    
+    const result = await service.update(Number(id), { status });
+    if (!result) {
+      return c.json({ code: 404, message: '记录不存在' }, 404);
+    }
+    
+    // 使用 parseWithAwait 处理响应数据
+    const validatedResult = await parseWithAwait(YourEntitySchema, result);
+    return c.json(validatedResult, 200);
+  } catch (error) {
+    return c.json({ code: 500, message: '状态更新失败' }, 500);
+  }
+});
+
+export default app;
+```
+
+#### 3.3 数据导出路由 - `export.ts`
+```typescript
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { YourEntityService } from '@/server/modules/your-module/your-entity.service';
+import { YourEntitySchema } from '@/server/modules/your-module/your-entity.schema';
+import { parseWithAwait } from '@/server/utils/parseWithAwait';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const routeDef = createRoute({
+  method: 'get',
+  path: '/',
+  middleware: [authMiddleware],
+  request: {
+    query: z.object({
+      format: z.enum(['csv', 'xlsx']).default('csv').openapi({
+        description: '导出格式',
+        example: 'csv'
+      }),
+      keyword: z.string().optional().openapi({
+        description: '搜索关键词',
+        example: '测试'
+      }),
+      filters: z.string().optional().openapi({
+        description: '筛选条件(JSON字符串)',
+        example: '{"status":1}'
+      })
+    })
+  },
+  responses: {
+    200: {
+      description: '导出文件',
+      content: {
+        'text/csv': { schema: z.string() },
+        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': { schema: z.any() }
+      }
+    }
+  }
+});
+
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const { format, keyword, filters } = c.req.valid('query');
+    const service = new YourEntityService(AppDataSource);
+    
+    let filterObj = {};
+    if (filters) {
+      try {
+        filterObj = JSON.parse(filters);
+      } catch (e) {
+        return c.json({ code: 400, message: '筛选条件格式错误' }, 400);
+      }
+    }
+    
+    const [data] = await service.getList(1, 1000, keyword, undefined, filterObj);
+    
+    // 使用 parseWithAwait 处理数据格式
+    const validatedData = await parseWithAwait(z.array(YourEntitySchema), data);
+    
+    if (format === 'csv') {
+      const csv = convertToCSV(validatedData);
+      return new Response(csv, {
+        headers: {
+          'Content-Type': 'text/csv',
+          'Content-Disposition': 'attachment; filename="export.csv"'
+        }
+      });
+    }
+    
+    return c.json({ code: 400, message: '不支持的导出格式' }, 400);
+  } catch (error) {
+    return c.json({ code: 500, message: '导出失败' }, 500);
+  }
+});
+
+export default app;
+
+function convertToCSV(data: any[]): string {
+  if (!data || data.length === 0) return '';
+  
+  const headers = Object.keys(data[0]);
+  const csvHeaders = headers.join(',');
+  const csvRows = data.map(row =>
+    headers.map(header => {
+      const value = row[header];
+      return typeof value === 'string' && value.includes(',') ? `"${value}"` : value;
+    }).join(',')
+  );
+  
+  return [csvHeaders, ...csvRows].join('\n');
+}
+```
+
+### 4. **聚合所有路由**
+在 `index.ts` 中聚合基础CRUD路由和所有扩展路由:
+
+```typescript
+// src/server/api/your-entity/index.ts
+import { OpenAPIHono } from '@hono/zod-openapi';
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { YourEntity } from '@/server/modules/your-module/your-entity.entity';
+import { YourEntitySchema, CreateYourEntityDto, UpdateYourEntityDto } from '@/server/modules/your-module/your-entity.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+// 导入基础路由和各扩展路由
+import batchDeleteRoute from './batch/delete';
+import statusUpdateRoute from './[id]/status';
+import exportRoute from './export';
+
+// 1. 创建基础CRUD路由
+const yourEntityRoutes = createCrudRoutes({
+  entity: YourEntity,
+  createSchema: CreateYourEntityDto,
+  updateSchema: UpdateYourEntityDto,
+  getSchema: YourEntitySchema,
+  listSchema: YourEntitySchema,
+  searchFields: ['name', 'description'],
+  middleware: [authMiddleware]
+});
+
+// 2. 聚合所有路由(保持链式)
+const app = new OpenAPIHono()
+  .route('/batch', batchDeleteRoute)        // 批量操作路由
+  .route('/:id/status', statusUpdateRoute)  // 状态更新路由
+  .route('/export', exportRoute)          // 导出路由
+  .route('/', yourEntityRoutes);               // 基础CRUD路由必需放最后,不然自定义路由会覆盖掉基础CRUD路由
+
+// 3. 导出聚合后的路由
+export default app;
+```
+
+
+## 常见扩展场景
+
+### 1. **批量操作**
+- 批量删除:`DELETE /your-entity/batch`
+- 批量更新状态:`PATCH /your-entity/batch/status`
+- 批量导入:`POST /your-entity/import`
+
+### 2. **数据统计**
+- 获取统计信息:`GET /your-entity/stats`
+- 获取图表数据:`GET /your-entity/chart-data`
+
+### 3. **文件相关**
+- 上传文件:`POST /your-entity/upload`
+- 下载文件:`GET /your-entity/download/{id}`
+- 导出数据:`GET /your-entity/export`
+
+### 4. **状态管理**
+- 状态切换:`PATCH /your-entity/{id}/toggle-status`
+- 审核操作:`POST /your-entity/{id}/audit`
+
+### 5. **关联操作**
+- 获取关联数据:`GET /your-entity/{id}/related-data`
+- 更新关联关系:`PUT /your-entity/{id}/relations`
+
+## 命名规范
+
+- **路径命名**:使用RESTful风格,动词用HTTP方法表示
+- **批量操作**:使用复数名词,如 `/batch`, `/import`, `/export`
+- **状态变更**:使用 PATCH 方法,路径中体现操作,如 `/status`, `/toggle`
+- **自定义方法**:避免在路径中使用动词,用名词+参数表示
+
+## 扩展路由 Schema 文件规范
+
+### Schema 文件位置
+所有扩展路由的 Zod Schema 定义必须遵循以下文件位置规范:
+
+```
+src/server/modules/[模块名]/
+├── [实体名].entity.ts      # 实体定义
+├── [实体名].schema.ts      # 实体Schema定义(已存在)
+└── schemas/                # 扩展路由专用Schema目录(新增)
+    ├── batch/              # 批量操作Schema
+    │   └── delete.schema.ts
+    ├── [id]/               # 单条记录操作Schema
+    │   ├── status.schema.ts
+    │   ├── toggle.schema.ts
+    │   └── audit.schema.ts
+    ├── export.schema.ts    # 导出操作Schema
+    ├── import.schema.ts    # 导入操作Schema
+    └── stats.schema.ts     # 统计操作Schema
+```
+
+### Schema 文件命名规范
+- **文件名**:`[操作名].schema.ts`
+- **导出**:必须包含完整的请求/响应Schema定义
+- **引用**:在扩展路由文件中直接引用对应的Schema文件
+
+### Schema 文件示例
+
+#### 批量删除Schema - `schemas/batch/delete.schema.ts`
+```typescript
+import { z } from '@hono/zod-openapi';
+
+// 请求Schema
+export const BatchDeleteRequestSchema = z.object({
+  ids: z.array(z.number().int().positive()).openapi({
+    description: '要删除的ID列表',
+    example: [1, 2, 3]
+  })
+});
+
+// 响应Schema
+export const BatchDeleteResponseSchema = z.object({
+  deletedCount: z.number().openapi({
+    example: 3,
+    description: '删除的记录数'
+  })
+});
+
+// 类型定义
+export type BatchDeleteRequest = z.infer<typeof BatchDeleteRequestSchema>;
+export type BatchDeleteResponse = z.infer<typeof BatchDeleteResponseSchema>;
+```
+
+#### 状态更新Schema - `schemas/[id]/status.schema.ts`
+```typescript
+import { z } from '@hono/zod-openapi';
+import { YourEntitySchema } from '../../your-entity.schema';
+
+// 路径参数Schema
+export const StatusUpdateParamsSchema = z.object({
+  id: z.string().openapi({
+    param: { name: 'id', in: 'path' },
+    example: '1',
+    description: '记录ID'
+  })
+});
+
+// 请求体Schema
+export const StatusUpdateBodySchema = z.object({
+  status: z.number().openapi({
+    example: 1,
+    description: '新状态值'
+  })
+});
+
+// 响应Schema(复用实体Schema)
+export const StatusUpdateResponseSchema = YourEntitySchema;
+
+// 类型定义
+export type StatusUpdateParams = z.infer<typeof StatusUpdateParamsSchema>;
+export type StatusUpdateBody = z.infer<typeof StatusUpdateBodySchema>;
+export type StatusUpdateResponse = z.infer<typeof StatusUpdateResponseSchema>;
+```
+
+### 在扩展路由中的使用方式
+
+#### 引用Schema文件
+```typescript
+// src/server/api/your-entity/batch/delete.ts
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { BatchDeleteRequestSchema, BatchDeleteResponseSchema } from '@/server/modules/your-module/schemas/batch/delete.schema';
+// ...其他导入
+```
+
+### 最佳实践
+1. **Schema复用**:尽量复用实体已有的Schema定义
+2. **类型安全**:所有Schema必须包含完整的OpenAPI元数据
+3. **模块化**:每个扩展路由对应独立的Schema文件
+4. **命名一致**:Schema文件名与路由功能保持一致
+5. **导出规范**:同时导出Schema和对应的TypeScript类型
+
+## 注意事项
+
+1. **模块化设计**:每个扩展功能单独一个文件,保持代码清晰
+2. **路径一致性**:扩展路由的路径要与聚合时的路径匹配
+3. **类型安全**:为所有自定义路由定义完整的OpenAPI schema
+4. **错误处理**:统一使用标准错误响应格式
+5. **权限控制**:为敏感操作添加适当的中间件
+6. **性能考虑**:批量操作要考虑事务处理和性能优化
+7. **Schema管理**:所有Schema必须放在指定的schemas目录下
+8. **版本兼容**:Schema变更要保持向后兼容性
+9. **数据验证**:所有查询类路由必须使用 `parseWithAwait` 处理响应数据,确保类型安全
+
+## parseWithAwait 使用规范
+
+### 概述
+`parseWithAwait` 是通用CRUD模块提供的数据验证工具,用于确保返回数据的类型安全,支持异步验证和转换。
+
+### 使用场景
+所有涉及数据查询和返回的扩展路由都应使用 `parseWithAwait` 处理响应数据。
+
+### 基本用法
+```typescript
+import { parseWithAwait } from '@/server/utils/parseWithAwait';
+
+// 验证单个实体
+const validatedEntity = await parseWithAwait(YourEntitySchema, entityData);
+
+// 验证实体数组
+const validatedEntities = await parseWithAwait(z.array(YourEntitySchema), entitiesData);
+```
+
+### 集成示例
+```typescript
+// 在扩展路由中使用
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const data = await yourService.getList();
+    
+    // 使用 parseWithAwait 确保数据格式正确
+    const validatedData = await parseWithAwait(z.array(YourEntitySchema), data);
+    
+    return c.json({
+      data: validatedData,
+      pagination: { total, current: page, pageSize }
+    }, 200);
+  } catch (error) {
+    return c.json({ code: 500, message: '获取数据失败' }, 500);
+  }
+});
+```
+
+### 优势
+- **类型安全**:确保返回数据完全符合Zod schema定义
+- **异步支持**:支持异步验证和转换操作
+- **错误处理**:提供详细的验证错误信息
+- **性能优化**:避免运行时类型错误
+- **向后兼容**:与现有代码完全兼容
+
+### 最佳实践
+1. **所有查询路由**:GET请求返回数据前必须使用 `parseWithAwait`
+2. **列表查询**:使用 `z.array(EntitySchema)` 格式验证数组
+3. **单条查询**:直接使用实体Schema验证单个对象
+4. **错误处理**:捕获并适当处理验证错误
+
+## 验证步骤
+
+1. 创建独立的扩展路由文件
+2. 实现各路由的业务逻辑
+3. 在 index.ts 中聚合所有路由
+4. 测试所有API端点
+5. 验证RPC客户端能正确识别所有路由
+6. 更新前端API客户端(如需要)

+ 11 - 0
.roo/commands/generic-crud-注册公共只读路由.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/commands/generic-crud-路由注册指南.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/commands/generic-crud-通用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-page.md 指令规范
+注册路由和菜单

+ 203 - 0
.roo/commands/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. 更新用户信息后会自动同步到本地存储

+ 16 - 0
.roo/commands/mini-check-页面实体路由检查.md

@@ -0,0 +1,16 @@
+---
+description: "检查页面相关的实体,schema, CRUD路由指令"
+---
+
+检查页面相关的实体,schema, CRUD路由, mini/src/api.ts中的rpc client 定义,以收集进行页面开发所需的上下文
+如果 mini/src/api.ts中的rpc client 定义缺少,就根据 src/server/api.ts中的api路由类型定义来 先添加
+
+简化的FileSchema定义: 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: '文件信息'
+}),

+ 623 - 0
.roo/commands/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/commands/mini-navbar-顶部导航条使用.md

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

+ 233 - 0
.roo/commands/mini-platform-小程序环境检测.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/commands/mini-rpc-小程序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)
+  }
+}

+ 6 - 0
.roo/commands/mini-shadui-可用组件检查.md

@@ -0,0 +1,6 @@
+---
+description: "检查小程序当前可用的shadui组件指令"
+---
+
+检查页面相关的小程序当前可用的shadui组件 定义,以收集进行页面开发所需的上下文
+在 mini/src/components/ui/ 下

+ 1193 - 0
.roo/commands/mini-shadui-页面开发指南.md

@@ -0,0 +1,1193 @@
+---
+description: "小程序shadui页面的开发指令"
+---
+
+# 小程序页面开发指令
+
+## 概述
+本指令规范了基于Taro + React + Shadui + Tailwind CSS的小程序页面开发流程,包含tabbar页和非tabbar页的创建标准和最佳实践,涵盖了认证、RPC调用、React Query v5使用等核心功能。
+
+## 小程序Shadui路径
+mini/src/components/ui
+
+## 当前可用的Shadui组件
+基于项目实际文件,当前小程序可用的shadui组件如下:
+
+### 基础组件
+- **Button** - 按钮组件 (`button.tsx`)
+- **Card** - 卡片组件 (`card.tsx`)
+- **Input** - 输入框组件 (`input.tsx`)
+- **Label** - 标签组件 (`label.tsx`)
+- **Form** - 表单组件 (`form.tsx`)
+
+### 交互组件
+- **AvatarUpload** - 头像上传组件 (`avatar-upload.tsx`)
+- **Carousel** - 轮播图组件 (`carousel.tsx`)
+- **Image** - 图片组件 (`image.tsx`)
+
+### 导航组件
+- **Navbar** - 顶部导航栏组件 (`navbar.tsx`)
+- **TabBar** - 底部标签栏组件 (`tab-bar.tsx`)
+
+### 布局组件
+- **TabBarLayout**: 用于tabbar页面,包含底部导航
+
+- 根据需求可扩展更多业务组件
+
+## 组件使用示例
+
+### Button 组件
+```typescript
+import { Button } from '@/components/ui/button'
+
+// 基础用法
+<Button onClick={handleClick}>主要按钮</Button>
+
+// 不同尺寸
+<Button size="sm">小按钮</Button>
+<Button size="md">中按钮</Button>
+<Button size="lg">大按钮</Button>
+
+// 不同样式
+<Button variant="primary">主要按钮</Button>
+<Button variant="secondary">次要按钮</Button>
+<Button variant="outline">边框按钮</Button>
+<Button variant="ghost">幽灵按钮</Button>
+```
+
+### Input 组件
+```typescript
+import { Input } from '@/components/ui/input'
+
+// 基础用法
+<Input placeholder="请输入内容" />
+
+// 受控组件
+<Input value={value} onChange={handleChange} />
+
+// 不同类型
+<Input type="text" placeholder="文本输入" />
+<Input type="number" placeholder="数字输入" />
+<Input type="password" placeholder="密码输入" />
+```
+
+### Form 组件
+```typescript
+import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'
+import { useForm } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { z } from 'zod'
+
+const formSchema = z.object({
+  username: z.string().min(2, '用户名至少2个字符'),
+  email: z.string().email('请输入有效的邮箱地址')
+})
+
+const form = useForm({
+  resolver: zodResolver(formSchema),
+  defaultValues: { username: '', email: '' }
+})
+
+<Form {...form}>
+  <FormField
+    name="username"
+    render={({ field }) => (
+      <FormItem>
+        <FormLabel>用户名</FormLabel>
+        <FormControl>
+          <Input placeholder="请输入用户名" {...field} />
+        </FormControl>
+        <FormMessage />
+      </FormItem>
+    )}
+  />
+</Form>
+```
+
+### Card 组件
+```typescript
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+
+<Card>
+  <CardHeader>
+    <CardTitle>卡片标题</CardTitle>
+  </CardHeader>
+  <CardContent>
+    <Text>卡片内容</Text>
+  </CardContent>
+</Card>
+```
+
+### Navbar 组件
+```typescript
+import { Navbar } from '@/components/ui/navbar'
+
+// 基础用法
+<Navbar title="页面标题" />
+
+// 带返回按钮
+<Navbar 
+  title="页面标题" 
+  leftIcon="i-heroicons-chevron-left-20-solid"
+  onClickLeft={() => Taro.navigateBack()}
+/>
+
+// 带右侧操作
+<Navbar 
+  title="页面标题"
+  rightIcon="i-heroicons-share-20-solid"
+  onClickRight={handleShare}
+/>
+```
+
+### Carousel 组件
+```typescript
+// 实际页面使用示例
+export function HomeCarousel() {
+  const bannerItems: CarouselItem[] = [
+    {
+      src: 'https://via.placeholder.com/750x400/3B82F6/FFFFFF?text=Banner+1',
+      title: '新品上市',
+      description: '最新款式,限时优惠',
+      link: '/pages/goods/new-arrival'
+    },
+    {
+      src: 'https://via.placeholder.com/750x400/EF4444/FFFFFF?text=Banner+2',
+      title: '限时秒杀',
+      description: '每日特价,不容错过',
+      link: '/pages/goods/flash-sale'
+    },
+    {
+      src: 'https://via.placeholder.com/750x400/10B981/FFFFFF?text=Banner+3',
+      title: '会员专享',
+      description: '会员专享折扣和福利',
+      link: '/pages/member/benefits'
+    }
+  ]
+
+  const handleBannerClick = (item: CarouselItem, index: number) => {
+    if (item.link) {
+      // 使用Taro跳转
+      Taro.navigateTo({
+        url: item.link
+      })
+    }
+  }
+
+  return (
+    <View className="w-full">
+      <Carousel
+        items={bannerItems}
+        height={400}
+        autoplay={true}
+        interval={4000}
+        circular={true}
+        rounded="none"
+        onItemClick={handleBannerClick}
+      />
+    </View>
+  )
+}
+```
+
+## 页面类型分类
+
+### 1. TabBar页面(底部导航页)
+特点:
+- 使用 `TabBarLayout` 布局组件
+- 路径配置在 `mini/src/app.config.ts` 中的 `tabBar.list`
+- 包含底部导航栏,用户可直接切换
+- 通常包含 `Navbar` 顶部导航组件
+- 示例页面:首页、发现、个人中心
+
+### 2. 非TabBar页面(独立页面)
+特点:
+- 不使用 `TabBarLayout`,直接渲染内容
+- 使用 `Navbar` 组件作为顶部导航
+- 需要手动处理返回导航
+- 示例页面:登录、注册、详情页
+
+## 开发流程
+
+### 1. 创建页面目录
+```bash
+# TabBar页面
+mkdir -p mini/src/pages/[页面名称]
+
+# 非TabBar页面
+mkdir -p mini/src/pages/[页面名称]
+```
+
+### 2. 创建页面文件
+
+#### TabBar页面模板
+```typescript
+// mini/src/pages/[页面名称]/index.tsx
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import { TabBarLayout } from '@/layouts/tab-bar-layout'
+import { Navbar } from '@/components/ui/navbar'
+import { Button } from '@/components/ui/button'
+import { Card } from '@/components/ui/card'
+import './index.css'
+
+const [页面名称]Page: React.FC = () => {
+  return (
+    <TabBarLayout activeKey="[对应tabBar.key]">
+      <Navbar
+        title="页面标题"
+        rightIcon="i-heroicons-[图标名称]-20-solid"
+        onClickRight={() => console.log('点击右上角')}
+        leftIcon=""
+      />
+      <View className="px-4 py-4">
+        <Card>
+          <CardHeader>
+            <CardTitle>欢迎使用</CardTitle>
+          </CardHeader>
+          <CardContent>
+            <Text>这是一个使用shadui组件的TabBar页面</Text>
+            <Button className="mt-4">开始使用</Button>
+          </CardContent>
+        </Card>
+      </View>
+    </TabBarLayout>
+  )
+}
+
+export default [页面名称]Page
+```
+
+#### 非TabBar页面模板
+```typescript
+// mini/src/pages/[页面名称]/index.tsx
+import { View } from '@tarojs/components'
+import { useEffect } from 'react'
+import Taro from '@tarojs/taro'
+import { Navbar } from '@/components/ui/navbar'
+import { Card } from '@/components/ui/card'
+import { Button } from '@/components/ui/button'
+import './index.css'
+
+export default function [页面名称]() {
+  useEffect(() => {
+    Taro.setNavigationBarTitle({
+      title: '页面标题'
+    })
+  }, [])
+
+  return (
+    <View className="min-h-screen bg-gray-50">
+      <Navbar
+        title="页面标题"
+        backgroundColor="bg-transparent"
+        textColor="text-gray-900"
+        border={false}
+      />
+      <View className="px-6 py-4">
+        <Card>
+          <CardContent>
+            <Text>这是一个使用shadui组件的非TabBar页面</Text>
+            <Button className="mt-4">返回</Button>
+          </CardContent>
+        </Card>
+      </View>
+    </View>
+  )
+}
+```
+
+### 3. 页面配置文件
+```typescript
+// mini/src/pages/[页面名称]/index.config.ts
+export default definePageConfig({
+  navigationBarTitleText: '页面标题',
+  enablePullDownRefresh: true,
+  backgroundTextStyle: 'dark',
+  navigationBarBackgroundColor: '#ffffff',
+  navigationBarTextStyle: 'black'
+})
+```
+
+### 4. 样式文件
+统一使用tailwindcss类,index.css为空即可
+```css
+/* mini/src/pages/[页面名称]/index.css */
+
+```
+
+## 高级功能模板
+
+### 1. 带认证的页面模板
+```typescript
+// mini/src/pages/[需要认证的页面]/index.tsx
+import { View, Text } from '@tarojs/components'
+import { useEffect } from 'react'
+import { useAuth } from '@/utils/auth'
+import Taro from '@tarojs/taro'
+import { Navbar } from '@/components/ui/navbar'
+import { Card } from '@/components/ui/card'
+
+export default function ProtectedPage() {
+  const { user, isLoading, isLoggedIn } = useAuth()
+
+  useEffect(() => {
+    if (!isLoading && !isLoggedIn) {
+      Taro.navigateTo({ url: '/pages/login/index' })
+    }
+  }, [isLoading, isLoggedIn])
+
+  if (isLoading) {
+    return (
+      <View className="flex-1 flex items-center justify-center">
+        <View className="i-heroicons-arrow-path-20-solid animate-spin w-8 h-8 text-blue-500" />
+      </View>
+    )
+  }
+
+  if (!user) return null
+
+  return (
+    <View className="min-h-screen bg-gray-50">
+      <Navbar title="受保护页面" leftIcon="" />
+      <View className="px-4 py-4">
+        <Text>欢迎, {user.username}</Text>
+      </View>
+    </View>
+  )
+}
+```
+
+### 2. 带API调用的页面模板
+```typescript
+// mini/src/pages/[数据展示页面]/index.tsx
+import { View, ScrollView } from '@tarojs/components'
+import { useQuery } from '@tanstack/react-query'
+import { userClient } from '@/api'
+import { InferResponseType } from 'hono'
+import Taro from '@tarojs/taro'
+
+type UserListResponse = InferResponseType<typeof userClient.$get, 200>
+
+export default function UserListPage() {
+  const { data, isLoading, error } = useQuery<UserListResponse>({
+    queryKey: ['users'],
+    queryFn: async () => {
+      const response = await userClient.$get({})
+      if (response.status !== 200) {
+        throw new Error('获取用户列表失败')
+      }
+      return response.json()
+    },
+    staleTime: 5 * 60 * 1000, // 5分钟
+  })
+
+  if (isLoading) {
+    return (
+      <View className="flex-1 flex items-center justify-center">
+        <View className="i-heroicons-arrow-path-20-solid animate-spin w-8 h-8 text-blue-500" />
+      </View>
+    )
+  }
+
+  if (error) {
+    return (
+      <View className="flex-1 flex items-center justify-center">
+        <Text className="text-red-500">{error.message}</Text>
+      </View>
+    )
+  }
+
+  return (
+    <ScrollView className="h-screen">
+      <Navbar title="用户列表" leftIcon="" />
+      <View className="px-4 py-4">
+        {data?.data.map(user => (
+          <View key={user.id} className="bg-white rounded-lg p-4 mb-3">
+            <Text>{user.username}</Text>
+          </View>
+        ))}
+      </View>
+    </ScrollView>
+  )
+}
+```
+
+### 3. 带表单提交的页面模板
+```typescript
+// mini/src/pages/[表单页面]/index.tsx
+import { View } from '@tarojs/components'
+import { useState } from 'react'
+import { useForm } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { z } from 'zod'
+import { useMutation } from '@tanstack/react-query'
+import { userClient } from '@/api'
+import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'
+import { Input } from '@/components/ui/input'
+import { Button } from '@/components/ui/button'
+import Taro from '@tarojs/taro'
+
+const formSchema = z.object({
+  username: z.string().min(3, '用户名至少3个字符'),
+  email: z.string().email('请输入有效的邮箱地址'),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号')
+})
+
+type FormData = z.infer<typeof formSchema>
+
+export default function CreateUserPage() {
+  const [loading, setLoading] = useState(false)
+  
+  const form = useForm<FormData>({
+    resolver: zodResolver(formSchema),
+    defaultValues: {
+      username: '',
+      email: '',
+      phone: ''
+    }
+  })
+
+  const mutation = useMutation({
+    mutationFn: async (data: FormData) => {
+      const response = await userClient.$post({ json: data })
+      if (response.status !== 201) {
+        throw new Error('创建用户失败')
+      }
+      return response.json()
+    },
+    onSuccess: () => {
+      Taro.showToast({
+        title: '创建成功',
+        icon: 'success'
+      })
+      Taro.navigateBack()
+    },
+    onError: (error) => {
+      Taro.showToast({
+        title: error.message || '创建失败',
+        icon: 'none'
+      })
+    }
+  })
+
+  const onSubmit = async (data: FormData) => {
+    setLoading(true)
+    try {
+      await mutation.mutateAsync(data)
+    } finally {
+      setLoading(false)
+    }
+  }
+
+  return (
+    <View className="min-h-screen bg-gray-50">
+      <Navbar title="创建用户" leftIcon="" />
+      <View className="px-4 py-4">
+        <Form {...form}>
+          <View className="space-y-4">
+            <FormField
+              control={form.control}
+              name="username"
+              render={({ field }) => (
+                <FormItem>
+                  <FormLabel>用户名</FormLabel>
+                  <FormControl>
+                    <Input placeholder="请输入用户名" {...field} />
+                  </FormControl>
+                  <FormMessage />
+                </FormItem>
+              )}
+            />
+            <Button
+              className="w-full"
+              onClick={form.handleSubmit(onSubmit)}
+              disabled={loading}
+            >
+              {loading ? '创建中...' : '创建用户'}
+            </Button>
+          </View>
+        </Form>
+      </View>
+    </View>
+  )
+}
+```
+
+## 认证功能使用
+
+### 1. useAuth Hook 使用规范
+```typescript
+import { useAuth } from '@/utils/auth'
+
+// 在页面或组件中使用
+const { 
+  user,           // 当前用户信息
+  login,          // 登录函数
+  logout,         // 登出函数
+  register,       // 注册函数
+  updateUser,     // 更新用户信息
+  isLoading,      // 加载状态
+  isLoggedIn      // 是否已登录
+} = useAuth()
+
+// 使用示例
+const handleLogin = async (formData) => {
+  try {
+    await login(formData)
+    Taro.switchTab({ url: '/pages/index/index' })
+  } catch (error) {
+    console.error('登录失败:', error)
+  }
+}
+```
+
+### 2. 页面权限控制
+```typescript
+// 在需要认证的页面顶部
+const { user, isLoading, isLoggedIn } = useAuth()
+
+useEffect(() => {
+  if (!isLoading && !isLoggedIn) {
+    Taro.navigateTo({ url: '/pages/login/index' })
+  }
+}, [isLoading, isLoggedIn])
+
+// 或者使用路由守卫模式
+```
+
+## RPC客户端调用规范
+
+### 1. 客户端导入
+```typescript
+// 从api.ts导入对应的客户端
+import { authClient, userClient, fileClient } from '@/api'
+```
+
+### 2. 类型提取
+```typescript
+import { InferResponseType, InferRequestType } from 'hono'
+
+// 响应类型提取
+type UserResponse = InferResponseType<typeof userClient.$get, 200>
+type UserDetailResponse = InferResponseType<typeof userClient[':id']['$get'], 200>
+
+// 请求类型提取
+type CreateUserRequest = InferRequestType<typeof userClient.$post>['json']
+type UpdateUserRequest = InferRequestType<typeof userClient[':id']['$put']>['json']
+```
+
+### 3. 调用示例
+```typescript
+// GET请求 - 列表
+const response = await userClient.$get({
+  query: {
+    page: 1,
+    pageSize: 10,
+    keyword: 'search'
+  }
+})
+
+// GET请求 - 单条
+const response = await userClient[':id'].$get({
+  param: { id: userId }
+})
+
+// POST请求
+const response = await userClient.$post({
+  json: {
+    username: 'newuser',
+    email: 'user@example.com'
+  }
+})
+
+// PUT请求
+const response = await userClient[':id'].$put({
+  param: { id: userId },
+  json: { username: 'updated' }
+})
+
+// DELETE请求
+const response = await userClient[':id'].$delete({
+  param: { id: userId }
+})
+```
+
+## React Query v5使用规范
+
+### 1. 查询配置
+```typescript
+const { data, isLoading, error, refetch } = useQuery({
+  queryKey: ['users', page, keyword], // 唯一的查询键
+  queryFn: async () => {
+    const response = await userClient.$get({
+      query: { page, pageSize: 10, keyword }
+    })
+    if (response.status !== 200) {
+      throw new Error('获取数据失败')
+    }
+    return response.json()
+  },
+  staleTime: 5 * 60 * 1000, // 5分钟
+  cacheTime: 10 * 60 * 1000, // 10分钟
+  retry: 3, // 重试3次
+  enabled: !!keyword, // 条件查询
+})
+```
+
+### 2. 变更操作
+```typescript
+const queryClient = useQueryClient()
+
+const mutation = useMutation({
+  mutationFn: async (data: CreateUserRequest) => {
+    const response = await userClient.$post({ json: data })
+    if (response.status !== 201) {
+      throw new Error('创建失败')
+    }
+    return response.json()
+  },
+  onSuccess: () => {
+    // 成功后刷新相关查询
+    queryClient.invalidateQueries({ queryKey: ['users'] })
+    Taro.showToast({ title: '创建成功', icon: 'success' })
+  },
+  onError: (error) => {
+    Taro.showToast({ 
+      title: error.message || '操作失败', 
+      icon: 'none' 
+    })
+  }
+})
+```
+
+### 3. 删除操作
+```typescript
+const queryClient = useQueryClient()
+
+const mutation = useMutation({
+  mutationFn: async (id: number) => {
+    const response = await deliveryAddressClient[':id'].$delete({
+      param: { id }
+    })
+    if (response.status !== 204) {
+      throw new Error('删除地址失败')
+    }
+    return response.json()
+  },
+  onSuccess: () => {
+    queryClient.invalidateQueries({ queryKey: ['delivery-addresses'] })
+    Taro.showToast({
+      title: '删除成功',
+      icon: 'success'
+    })
+  },
+  onError: (error) => {
+    Taro.showToast({
+      title: error.message || '删除失败',
+      icon: 'none'
+    })
+  }
+})
+```
+
+### 4. 分页查询
+#### 标准分页(useQuery)
+```typescript
+const useUserList = (page: number, pageSize: number = 10) => {
+  return useQuery({
+    queryKey: ['users', page, pageSize],
+    queryFn: async () => {
+      const response = await userClient.$get({
+        query: { page, pageSize }
+      })
+      return response.json()
+    },
+    keepPreviousData: true, // 保持上一页数据
+  })
+}
+```
+
+#### 移动端无限滚动分页(useInfiniteQuery)
+```typescript
+import { useInfiniteQuery } from '@tanstack/react-query'
+
+const useInfiniteUserList = (keyword?: string) => {
+  return useInfiniteQuery({
+    queryKey: ['users-infinite', keyword],
+    queryFn: async ({ pageParam = 1 }) => {
+      const response = await userClient.$get({
+        query: {
+          page: pageParam,
+          pageSize: 10,
+          keyword
+        }
+      })
+      if (response.status !== 200) {
+        throw new Error('获取用户列表失败')
+      }
+      return response.json()
+    },
+    getNextPageParam: (lastPage, allPages) => {
+      const totalPages = Math.ceil(lastPage.pagination.total / lastPage.pagination.pageSize)
+      const nextPage = allPages.length + 1
+      return nextPage <= totalPages ? nextPage : undefined
+    },
+    staleTime: 5 * 60 * 1000,
+  })
+}
+
+// 使用示例
+const {
+  data,
+  isLoading,
+  isFetchingNextPage,
+  fetchNextPage,
+  hasNextPage,
+  refetch
+} = useInfiniteUserList(searchKeyword)
+
+// 合并所有分页数据
+const allUsers = data?.pages.flatMap(page => page.data) || []
+
+// 触底加载更多处理
+const handleScrollToLower = () => {
+  if (hasNextPage && !isFetchingNextPage) {
+    fetchNextPage()
+  }
+}
+```
+
+#### 移动端分页页面模板
+```typescript
+// mini/src/pages/[无限滚动列表]/index.tsx
+import { View, ScrollView } from '@tarojs/components'
+import { useInfiniteQuery } from '@tanstack/react-query'
+import { goodsClient } from '@/api'
+import { InferResponseType } from 'hono'
+import Taro from '@tarojs/taro'
+
+type GoodsResponse = InferResponseType<typeof goodsClient.$get, 200>
+
+export default function InfiniteGoodsList() {
+  const [searchKeyword, setSearchKeyword] = useState('')
+
+  const {
+    data,
+    isLoading,
+    isFetchingNextPage,
+    fetchNextPage,
+    hasNextPage,
+    refetch
+  } = useInfiniteQuery({
+    queryKey: ['goods-infinite', searchKeyword],
+    queryFn: async ({ pageParam = 1 }) => {
+      const response = await goodsClient.$get({
+        query: {
+          page: pageParam,
+          pageSize: 10,
+          keyword: searchKeyword
+        }
+      })
+      if (response.status !== 200) {
+        throw new Error('获取商品失败')
+      }
+      return response.json()
+    },
+    getNextPageParam: (lastPage) => {
+      const { pagination } = lastPage
+      const totalPages = Math.ceil(pagination.total / pagination.pageSize)
+      return pagination.current < totalPages ? pagination.current + 1 : undefined
+    },
+    staleTime: 5 * 60 * 1000,
+    initialPageParam: 1,
+  })
+
+  // 合并所有分页数据
+  const allGoods = data?.pages.flatMap(page => page.data) || []
+
+  // 触底加载更多
+  const handleScrollToLower = () => {
+    if (hasNextPage && !isFetchingNextPage) {
+      fetchNextPage()
+    }
+  }
+
+  // 下拉刷新
+  const onPullDownRefresh = () => {
+    refetch().finally(() => {
+      Taro.stopPullDownRefresh()
+    })
+  }
+
+  return (
+    <ScrollView
+      className="h-screen"
+      scrollY
+      onScrollToLower={handleScrollToLower}
+      refresherEnabled
+      refresherTriggered={false}
+      onRefresherRefresh={onPullDownRefresh}
+    >
+      <View className="px-4 py-4">
+        {isLoading ? (
+          <View className="flex justify-center py-10">
+            <View className="i-heroicons-arrow-path-20-solid animate-spin w-8 h-8 text-blue-500" />
+          </View>
+        ) : (
+          <>
+            {allGoods.map((item) => (
+              <View key={item.id} className="bg-white rounded-lg p-4 mb-3">
+                <Text>{item.name}</Text>
+              </View>
+            ))}
+            
+            {isFetchingNextPage && (
+              <View className="flex justify-center py-4">
+                <View className="i-heroicons-arrow-path-20-solid animate-spin w-6 h-6 text-blue-500" />
+                <Text className="ml-2 text-sm text-gray-500">加载更多...</Text>
+              </View>
+            )}
+            
+            {!hasNextPage && allGoods.length > 0 && (
+              <View className="text-center py-4 text-sm text-gray-400">
+                没有更多了
+              </View>
+            )}
+          </>
+        )}
+      </View>
+    </ScrollView>
+  )
+}
+```
+
+## 表单处理规范
+
+### 1. 表单Schema定义
+```typescript
+// 在schemas目录下定义
+import { z } from 'zod'
+
+export const userSchema = z.object({
+  username: z.string()
+    .min(3, '用户名至少3个字符')
+    .max(20, '用户名最多20个字符')
+    .regex(/^\S+$/, '用户名不能包含空格'),
+  email: z.string().email('请输入有效的邮箱地址'),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号')
+})
+
+export type UserFormData = z.infer<typeof userSchema>
+```
+
+### 2. 表单使用
+```typescript
+import { useForm } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { userSchema, type UserFormData } from '@/schemas/user.schema'
+
+const form = useForm<UserFormData>({
+  resolver: zodResolver(userSchema),
+  defaultValues: {
+    username: '',
+    email: '',
+    phone: ''
+  }
+})
+
+// 表单提交
+const onSubmit = async (data: UserFormData) => {
+  try {
+    await mutation.mutateAsync(data)
+  } catch (error) {
+    console.error('表单提交失败:', error)
+  }
+}
+```
+
+## 错误处理规范
+
+### 1. 统一的错误处理
+```typescript
+const handleApiError = (error: any) => {
+  const message = error.response?.data?.message || error.message || '操作失败'
+  
+  if (error.response?.status === 401) {
+    Taro.showModal({
+      title: '未登录',
+      content: '请先登录',
+      success: () => {
+        Taro.navigateTo({ url: '/pages/login/index' })
+      }
+    })
+  } else if (error.response?.status === 403) {
+    Taro.showToast({ title: '权限不足', icon: 'none' })
+  } else if (error.response?.status === 404) {
+    Taro.showToast({ title: '资源不存在', icon: 'none' })
+  } else if (error.response?.status >= 500) {
+    Taro.showToast({ title: '服务器错误,请稍后重试', icon: 'none' })
+  } else {
+    Taro.showToast({ title: message, icon: 'none' })
+  }
+}
+```
+
+### 2. 页面级错误处理
+```typescript
+const { data, isLoading, error } = useQuery({
+  // ...查询配置
+})
+
+if (error) {
+  return (
+    <View className="flex-1 flex items-center justify-center">
+      <View className="text-center">
+        <View className="i-heroicons-exclamation-triangle-20-solid w-12 h-12 text-red-500 mx-auto mb-4" />
+        <Text className="text-gray-600 mb-4">{error.message}</Text>
+        <Button onClick={() => refetch()}>重新加载</Button>
+      </View>
+    </View>
+  )
+}
+```
+
+## 页面模板示例
+
+### 1. TabBar页面标准结构
+```typescript
+// 示例:首页
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import { TabBarLayout } from '@/layouts/tab-bar-layout'
+import { Navbar } from '@/components/ui/navbar'
+import { Button } from '@/components/ui/button'
+import './index.css'
+
+const HomePage: React.FC = () => {
+  return (
+    <TabBarLayout activeKey="home">
+      <Navbar
+        title="首页"
+        rightIcon="i-heroicons-bell-20-solid"
+        onClickRight={() => console.log('点击通知')}
+        leftIcon=""
+      />
+      <View className="px-4 py-4">
+        <Text className="text-2xl font-bold text-gray-900">欢迎使用</Text>
+        <View className="mt-4">
+          <Text className="text-gray-600">这是一个简洁优雅的小程序首页</Text>
+        </View>
+      </View>
+    </TabBarLayout>
+  )
+}
+
+export default HomePage
+```
+
+### 2. 非TabBar页面标准结构
+```typescript
+// 示例:登录页
+import { View } from '@tarojs/components'
+import { useEffect } from 'react'
+import Taro from '@tarojs/taro'
+import { Navbar } from '@/components/ui/navbar'
+import { Button } from '@/components/ui/button'
+import './index.css'
+
+export default function Login() {
+  useEffect(() => {
+    Taro.setNavigationBarTitle({
+      title: '用户登录'
+    })
+  }, [])
+
+  return (
+    <View className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-indigo-50">
+      <Navbar
+        title="用户登录"
+        backgroundColor="bg-transparent"
+        textColor="text-gray-900"
+        border={false}
+      />
+      <View className="flex-1 px-6 py-12">
+        {/* Logo区域 */}
+        <View className="flex flex-col items-center mb-10">
+          <View className="w-20 h-20 mb-4 rounded-full bg-white shadow-lg flex items-center justify-center">
+            <View className="i-heroicons-user-circle-20-solid w-12 h-12 text-blue-500" />
+          </View>
+          <Text className="text-2xl font-bold text-gray-900 mb-1">欢迎回来</Text>
+        </View>
+
+        {/* 表单区域 */}
+        <View className="bg-white rounded-2xl shadow-sm p-6">
+          <View className="space-y-5">
+            {/* 表单内容 */}
+          </View>
+        </View>
+      </View>
+    </View>
+  )
+}
+```
+
+
+## 路由配置
+
+### 1. TabBar页面配置
+```typescript
+// mini/src/app.config.ts
+export default defineAppConfig({
+  pages: [
+    'pages/index/index',
+    'pages/explore/index',
+    'pages/profile/index',
+    // 其他页面
+  ],
+  tabBar: {
+    color: '#666666',
+    selectedColor: '#1976D2',
+    backgroundColor: '#ffffff',
+    borderStyle: 'black',
+    list: [
+      {
+        pagePath: 'pages/index/index',
+        text: '首页',
+        iconPath: 'assets/icons/home.png',
+        selectedIconPath: 'assets/icons/home-active.png'
+      },
+      {
+        pagePath: 'pages/explore/index',
+        text: '发现',
+        iconPath: 'assets/icons/explore.png',
+        selectedIconPath: 'assets/icons/explore-active.png'
+      },
+      {
+        pagePath: 'pages/profile/index',
+        text: '我的',
+        iconPath: 'assets/icons/profile.png',
+        selectedIconPath: 'assets/icons/profile-active.png'
+      }
+    ]
+  }
+})
+```
+
+### 2. 非TabBar页面路由
+非TabBar页面会自动添加到pages数组中,无需额外配置tabBar。
+
+## 最佳实践
+
+### 1. 命名规范
+- 页面目录:使用小写+中划线命名,如 `user-profile`
+- 组件名称:使用PascalCase,如 `UserProfilePage`
+- 文件名:使用小写+中划线命名,如 `user-profile.tsx`
+
+### 2. 样式规范
+- 使用Tailwind CSS原子类
+- 避免使用px,使用rpx单位
+- 页面背景色统一使用 `bg-gray-50` 或 `bg-white`
+
+### 3. 状态管理
+- 使用React hooks进行状态管理
+- 复杂状态使用Context API
+- 用户信息使用 `useAuth` hook
+
+### 4. 错误处理
+- 使用Taro.showToast显示错误信息
+- 网络请求使用try-catch包裹
+- 提供友好的用户反馈
+
+### 5. 性能优化
+- 使用懒加载组件
+- 避免不必要的重新渲染
+- 合理使用useMemo和useCallback
+
+## 常用工具函数
+
+### 1. 页面跳转
+```typescript
+// Tab页面跳转
+Taro.switchTab({ url: '/pages/index/index' })
+
+// 普通页面跳转
+Taro.navigateTo({ url: '/pages/login/index' })
+
+// 返回上一页
+Taro.navigateBack()
+
+// 重定向(清除当前页面历史)
+Taro.redirectTo({ url: '/pages/login/index' })
+
+// 重新启动应用
+Taro.reLaunch({ url: '/pages/index/index' })
+```
+
+### 2. 用户交互
+```typescript
+// 显示提示
+Taro.showToast({
+  title: '操作成功',
+  icon: 'success',
+  duration: 2000
+})
+
+// 显示加载
+Taro.showLoading({
+  title: '加载中...',
+  mask: true
+})
+Taro.hideLoading()
+
+// 显示确认对话框
+Taro.showModal({
+  title: '确认操作',
+  content: '确定要执行此操作吗?',
+  success: (res) => {
+    if (res.confirm) {
+      // 用户点击确认
+    }
+  }
+})
+
+// 显示操作菜单
+Taro.showActionSheet({
+  itemList: ['选项1', '选项2', '选项3'],
+  success: (res) => {
+    console.log('用户选择了', res.tapIndex)
+  }
+})
+```
+
+### 3. 本地存储
+```typescript
+// 存储数据
+Taro.setStorageSync('key', 'value')
+Taro.setStorageSync('user', JSON.stringify(user))
+
+// 获取数据
+const value = Taro.getStorageSync('key')
+const user = JSON.parse(Taro.getStorageSync('user') || '{}')
+
+// 移除数据
+Taro.removeStorageSync('key')
+
+// 清空所有数据
+Taro.clearStorageSync()
+```
+
+### 4. 设备信息
+```typescript
+// 获取系统信息
+const systemInfo = Taro.getSystemInfoSync()
+const { screenWidth, screenHeight, windowWidth, windowHeight, statusBarHeight } = systemInfo
+
+// 获取用户位置
+Taro.getLocation({
+  type: 'wgs84',
+  success: (res) => {
+    console.log('纬度:', res.latitude)
+    console.log('经度:', res.longitude)
+  }
+})

+ 7 - 0
.roo/commands/mini-tabbar-布局组件使用.md

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

+ 6 - 0
.roo/commands/mini-tabbar-页面加入标签栏.md

@@ -0,0 +1,6 @@
+---
+description: "将页面作为 小程序 tabbar指令"
+---
+
+检查 mini/src/app.config.ts, mini/src/layouts/tab-bar-layout.tsx
+将页面作为 小程序 tabbar

+ 490 - 0
.roo/commands/mini-ui-小程序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版本

+ 282 - 0
.roo/commands/mini-ui-轮播图组件使用.md

@@ -0,0 +1,282 @@
+---
+description: "小程序轮播图组件使用指令"
+---
+
+```tsx
+import { View } from '@tarojs/components'
+import { Carousel, CarouselItem } from './carousel'
+import { useState } from 'react'
+import Taro from '@tarojs/taro'
+
+// 示例1:基础用法
+export function BasicCarouselExample() {
+  const items: CarouselItem[] = [
+    {
+      src: 'https://picsum.photos/400/200?random=1',
+      title: '第一张轮播图',
+      description: '这是第一张轮播图的描述文字'
+    },
+    {
+      src: 'https://picsum.photos/400/200?random=2',
+      title: '第二张轮播图',
+      description: '这是第二张轮播图的描述文字'
+    },
+    {
+      src: 'https://picsum.photos/400/200?random=3',
+      title: '第三张轮播图',
+      description: '这是第三张轮播图的描述文字'
+    }
+  ]
+
+  return (
+    <View className="p-4">
+      <View className="text-lg font-semibold mb-4">基础轮播图</View>
+      <Carousel items={items} />
+    </View>
+  )
+}
+
+// 示例2:自定义配置
+export function CustomCarouselExample() {
+  const items: CarouselItem[] = [
+    {
+      src: 'https://picsum.photos/400/300?random=4',
+      title: '精选商品',
+      description: '限时优惠,立即抢购'
+    },
+    {
+      src: 'https://picsum.photos/400/300?random=5',
+      title: '新品上市',
+      description: '最新款式,引领潮流'
+    },
+    {
+      src: 'https://picsum.photos/400/300?random=6',
+      title: '特价促销',
+      description: '全场5折起,不容错过'
+    }
+  ]
+
+  return (
+    <View className="p-4">
+      <View className="text-lg font-semibold mb-4">自定义配置轮播图</View>
+      <Carousel
+        items={items}
+        height={300}
+        interval={4000}
+        indicatorPosition="bottom"
+        rounded="lg"
+        onItemClick={(item, index) => {
+          console.log('点击了第', index + 1, '张轮播图:', item.title)
+        }}
+        onChange={(current) => {
+          console.log('切换到第', current + 1, '张')
+        }}
+      />
+    </View>
+  )
+}
+
+// 示例3:无指示器轮播
+export function NoIndicatorsCarouselExample() {
+  const items: CarouselItem[] = [
+    {
+      src: 'https://picsum.photos/400/150?random=7',
+      link: '/pages/product/1'
+    },
+    {
+      src: 'https://picsum.photos/400/150?random=8',
+      link: '/pages/product/2'
+    },
+    {
+      src: 'https://picsum.photos/400/150?random=9',
+      link: '/pages/product/3'
+    }
+  ]
+
+  return (
+    <View className="p-4">
+      <View className="text-lg font-semibold mb-4">无指示器轮播图</View>
+      <Carousel
+        items={items}
+        showIndicators={false}
+        height={150}
+        rounded="md"
+      />
+    </View>
+  )
+}
+
+// 示例4:圆形指示器轮播
+export function CircularIndicatorsCarouselExample() {
+  const items: CarouselItem[] = [
+    {
+      src: 'https://picsum.photos/400/250?random=10',
+      title: '活动预告'
+    },
+    {
+      src: 'https://picsum.photos/400/250?random=11',
+      title: '会员专享'
+    },
+    {
+      src: 'https://picsum.photos/400/250?random=12',
+      title: '积分兑换'
+    },
+    {
+      src: 'https://picsum.photos/400/250?random=13',
+      title: '限时秒杀'
+    }
+  ]
+
+  return (
+    <View className="p-4">
+      <View className="text-lg font-semibold mb-4">圆形指示器轮播图</View>
+      <Carousel
+        items={items}
+        autoplay={true}
+        interval={2000}
+        indicatorPosition="bottom"
+        rounded="xl"
+        height={250}
+      />
+    </View>
+  )
+}
+
+// 示例5:顶部指示器轮播
+export function TopIndicatorsCarouselExample() {
+  const items: CarouselItem[] = [
+    {
+      src: 'https://picsum.photos/400/180?random=14',
+      title: '顶部指示器',
+      description: '指示器位于顶部'
+    },
+    {
+      src: 'https://picsum.photos/400/180?random=15',
+      title: '美观设计',
+      description: '简洁优雅的界面'
+    },
+    {
+      src: 'https://picsum.photos/400/180?random=16',
+      title: '用户体验',
+      description: '流畅的交互体验'
+    }
+  ]
+
+  return (
+    <View className="p-4">
+      <View className="text-lg font-semibold mb-4">顶部指示器轮播图</View>
+      <Carousel
+        items={items}
+        indicatorPosition="top"
+        height={180}
+        rounded="lg"
+      />
+    </View>
+  )
+}
+
+// 综合示例:轮播图页面
+export function CarouselDemoPage() {
+  const [currentExample, setCurrentExample] = useState(0)
+
+  const examples = [
+    { title: '基础用法', component: BasicCarouselExample },
+    { title: '自定义配置', component: CustomCarouselExample },
+    { title: '无指示器', component: NoIndicatorsCarouselExample },
+    { title: '圆形指示器', component: CircularIndicatorsCarouselExample },
+    { title: '顶部指示器', component: TopIndicatorsCarouselExample }
+  ]
+
+  const CurrentComponent = examples[currentExample].component
+
+  return (
+    <View className="min-h-screen bg-gray-50">
+      <View className="p-4 bg-white">
+        <View className="text-xl font-bold text-center mb-4">
+          轮播图组件示例
+        </View>
+        
+        {/* 切换示例 */}
+        <View className="flex justify-center gap-2 mb-4">
+          {examples.map((example, index) => (
+            <View
+              key={index}
+              className={`px-3 py-1 text-sm rounded-full cursor-pointer ${
+                currentExample === index
+                  ? 'bg-blue-500 text-white'
+                  : 'bg-gray-200 text-gray-700'
+              }`}
+              onClick={() => setCurrentExample(index)}
+            >
+              {example.title}
+            </View>
+          ))}
+        </View>
+      </View>
+
+      <View className="p-4">
+        <CurrentComponent />
+      </View>
+
+      {/* 使用说明 */}
+      <View className="p-4 bg-white mt-4">
+        <View className="text-lg font-semibold mb-2">使用说明</View>
+        <View className="text-sm text-gray-600 space-y-1">
+          <View>• 支持自动播放和手动切换</View>
+          <View>• 可自定义指示器位置和样式</View>
+          <View>• 支持点击事件和切换回调</View>
+          <View>• 内置图片懒加载和错误处理</View>
+          <View>• 响应式设计,适配不同屏幕尺寸</View>
+        </View>
+      </View>
+    </View>
+  )
+}
+
+// 实际页面使用示例
+export function HomeCarousel() {
+  const bannerItems: CarouselItem[] = [
+    {
+      src: 'https://via.placeholder.com/750x400/3B82F6/FFFFFF?text=Banner+1',
+      title: '新品上市',
+      description: '最新款式,限时优惠',
+      link: '/pages/goods/new-arrival'
+    },
+    {
+      src: 'https://via.placeholder.com/750x400/EF4444/FFFFFF?text=Banner+2',
+      title: '限时秒杀',
+      description: '每日特价,不容错过',
+      link: '/pages/goods/flash-sale'
+    },
+    {
+      src: 'https://via.placeholder.com/750x400/10B981/FFFFFF?text=Banner+3',
+      title: '会员专享',
+      description: '会员专享折扣和福利',
+      link: '/pages/member/benefits'
+    }
+  ]
+
+  const handleBannerClick = (item: CarouselItem, index: number) => {
+    if (item.link) {
+      // 使用Taro跳转
+      Taro.navigateTo({
+        url: item.link
+      })
+    }
+  }
+
+  return (
+    <View className="w-full">
+      <Carousel
+        items={bannerItems}
+        height={400}
+        autoplay={true}
+        interval={4000}
+        circular={true}
+        rounded="none"
+        onItemClick={handleBannerClick}
+      />
+    </View>
+  )
+}
+```

+ 56 - 0
.roo/commands/rename_files.sh

@@ -0,0 +1,56 @@
+#!/bin/bash
+
+# 重命名命令文件为中文名称的脚本
+# 保留原始英文前缀,添加简洁中文描述
+
+declare -A rename_map=(
+    # 英文前缀保持两节单词,中文名动词在前,10-20字
+    ["check-api-entity-schema.md"]="check-api-实体与页面字段对应检查.md"
+    ["check-curd-entity-schema.md"]="check-curd-页面实体路由对应检查.md"
+    ["check-entity-schema.md"]="check-实体与schema对应检查.md"
+    ["command-create.md"]="command-创建新Roo指令.md"
+    ["download-file-from-url.md"]="download-从URL下载文件到MinIO.md"
+    ["file-relation.md"]="file-实体文件关联开发指南.md"
+    ["generic-crud-extend.md"]="generic-crud-扩展路由开发指南.md"
+    ["generic-crud-from-sql.md"]="generic-crud-从SQL创建CRUD.md"
+    ["generic-crud-public-create.md"]="generic-crud-创建公共只读路由.md"
+    ["generic-crud-public-reg.md"]="generic-crud-注册公共只读路由.md"
+    ["generic-crud-reg.md"]="generic-crud-路由注册指南.md"
+    ["generic-crud-schema-create.md"]="generic-crud-创建实体schema.md"
+    ["generic-crud.md"]="generic-crud-通用CRUD开发指南.md"
+    ["mini-auth.md"]="mini-auth-小程序认证钩子使用.md"
+    ["mini-check-curd-entity-schema.md"]="mini-check-页面实体路由检查.md"
+    ["mini-form.md"]="mini-form-小程序表单开发指南.md"
+    ["mini-navbar.md"]="mini-navbar-顶部导航条使用.md"
+    ["mini-platform-check.md"]="mini-platform-小程序环境检测.md"
+    ["mini-rpc.md"]="mini-rpc-小程序RPC开发规范.md"
+    ["mini-shadui-check.md"]="mini-shadui-可用组件检查.md"
+    ["mini-shadui-page.md"]="mini-shadui-页面开发指南.md"
+    ["mini-tabbar-join.md"]="mini-tabbar-页面加入标签栏.md"
+    ["mini-tabbar-layout.md"]="mini-tabbar-布局组件使用.md"
+    ["mini-ui-carousel.md"]="mini-ui-轮播图组件使用.md"
+    ["mini-ui.md"]="mini-ui-小程序UI开发指南.md"
+    ["rpc-type.md"]="rpc-type-提取响应请求类型.md"
+    ["schema-error-msg-cn.md"]="schema-添加中文错误提示.md"
+    ["shadcn-entity-selector.md"]="shadcn-实体选择器创建.md"
+    ["shadcn-manage-form-split.md"]="shadcn-管理页表单分离.md"
+    ["shadcn-manage-form.md"]="shadcn-管理页表单开发.md"
+    ["shadcn-manage-page.md"]="shadcn-管理页面开发指南.md"
+    ["shadcn.md"]="shadcn-创建页面和组件.md"
+    ["user-relation.md"]="user-实体用户关联开发.md"
+)
+
+echo "开始重命名文件..."
+
+for old_name in "${!rename_map[@]}"; do
+    new_name="${rename_map[$old_name]}"
+    
+    if [[ -f "$old_name" ]]; then
+        mv -v "$old_name" "$new_name"
+        echo "✓ 已重命名: $old_name -> $new_name"
+    else
+        echo "⚠️  文件不存在: $old_name"
+    fi
+done
+
+echo "重命名完成!"

+ 15 - 0
.roo/commands/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']
+```

+ 16 - 0
.roo/commands/schema-添加中文错误提示.md

@@ -0,0 +1,16 @@
+---
+description: "给schema加上中文错误提示指令"
+---
+
+为每个验证字段添加 中文错误提示
+
+示例:
+```typescript
+// 用户 schema
+export const UserSchema = z.object({
+  username: z.string().min(3, '用户名至少3个字符').max(255, '最多255个字符'),
+  password: z.string().min(6, '密码至少6位').max(255, '最多255位'),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号'),
+  email: z.email('请输入正确的邮箱格式'),
+  name: z.string().max(255, '姓名最多255个字符').optional()
+});

+ 215 - 0
.roo/commands/shadcn-使用设备类型选择器.md

@@ -0,0 +1,215 @@
+---
+description: "使用设备类型选择器指令"
+---
+
+## 描述
+本指令指导如何在管理后台页面中使用设备类型选择器组件(DeviceTypeSelector),包括基本用法、高级配置和最佳实践。
+
+## 快速开始
+
+### 基本用法
+```typescript
+import { DeviceTypeSelector } from '@/client/admin/components/DeviceTypeSelector';
+
+// 在组件中使用
+<DeviceTypeSelector
+  value={selectedTypeId}
+  onChange={(id) => setSelectedTypeId(id)}
+  placeholder="选择设备类型"
+/>
+```
+
+### 带图片的高级用法
+```typescript
+<DeviceTypeSelector
+  value={selectedTypeId}
+  onChange={(id) => setSelectedTypeId(id)}
+  includeImage={true}
+  onlyEnabled={true}
+  placeholder="选择启用的设备类型(带图片)"
+/>
+```
+
+## 参数说明
+
+| 参数 | 类型 | 默认值 | 描述 |
+|------|------|--------|------|
+| `value` | `number` | `undefined` | 当前选中的设备类型ID |
+| `onChange` | `(value: number) => void` | `undefined` | 选择变化时的回调函数 |
+| `placeholder` | `string` | `"选择设备类型"` | 选择框占位符文本 |
+| `disabled` | `boolean` | `false` | 是否禁用选择器 |
+| `onlyEnabled` | `boolean` | `true` | 是否只显示启用的设备类型 |
+| `includeImage` | `boolean` | `false` | 是否在选项中显示设备类型图片 |
+
+## 完整示例
+
+### 在表单中使用
+```typescript
+import { useForm } from 'react-hook-form';
+import { Form, FormField, FormItem, FormLabel, FormControl } from '@/client/components/ui/form';
+import { DeviceTypeSelector } from '@/client/admin/components/DeviceTypeSelector';
+
+const form = useForm({
+  defaultValues: {
+    deviceTypeId: undefined as number | undefined
+  }
+});
+
+<Form {...form}>
+  <FormField
+    control={form.control}
+    name="deviceTypeId"
+    render={({ field }) => (
+      <FormItem>
+        <FormLabel>设备类型</FormLabel>
+        <FormControl>
+          <DeviceTypeSelector
+            value={field.value}
+            onChange={field.onChange}
+            placeholder="请选择设备类型"
+            includeImage={true}
+          />
+        </FormControl>
+      </FormItem>
+    )}
+  />
+</Form>
+```
+
+### 在表格筛选中使用
+```typescript
+import { useState } from 'react';
+import { DeviceTypeSelector } from '@/client/admin/components/DeviceTypeSelector';
+
+const [filters, setFilters] = useState({
+  typeId: undefined as number | undefined,
+  keyword: ''
+});
+
+const handleTypeChange = (typeId: number) => {
+  setFilters(prev => ({ ...prev, typeId }));
+  // 触发数据重新加载
+  refetch();
+};
+
+<DeviceTypeSelector
+  value={filters.typeId}
+  onChange={handleTypeChange}
+  placeholder="按设备类型筛选"
+  onlyEnabled={true}
+/>
+```
+
+## 状态管理集成
+
+### 与React Query集成
+```typescript
+import { useQuery } from '@tanstack/react-query';
+import { deviceTypesClient } from '@/client/api';
+
+// 如果需要获取选中的设备类型详情
+const { data: selectedDeviceType } = useQuery({
+  queryKey: ['deviceType', selectedTypeId],
+  queryFn: async () => {
+    if (!selectedTypeId) return null;
+    const res = await deviceTypesClient[':id'].$get({
+      param: { id: selectedTypeId.toString() }
+    });
+    if (res.status === 200) return await res.json();
+    return null;
+  },
+  enabled: !!selectedTypeId
+});
+```
+
+### 与Zod验证集成
+```typescript
+import { z } from 'zod';
+
+const formSchema = z.object({
+  deviceTypeId: z.number({
+    required_error: "请选择设备类型",
+    invalid_type_error: "设备类型ID必须是数字"
+  }).int().positive()
+});
+
+// 在表单提交时验证
+const onSubmit = (data: z.infer<typeof formSchema>) => {
+  // 处理表单提交
+};
+```
+
+## 最佳实践
+
+### 1. 错误处理
+```typescript
+<DeviceTypeSelector
+  value={selectedTypeId}
+  onChange={(id) => {
+    try {
+      setSelectedTypeId(id);
+    } catch (error) {
+      console.error('选择设备类型失败:', error);
+      toast.error('选择失败,请重试');
+    }
+  }}
+/>
+```
+
+### 2. 性能优化
+```typescript
+// 使用防抖处理频繁的选择变化
+import { debounce } from 'lodash-es';
+
+const debouncedTypeChange = debounce((id: number) => {
+  setSelectedTypeId(id);
+}, 300);
+
+<DeviceTypeSelector
+  onChange={debouncedTypeChange}
+/>
+```
+
+### 3. 无障碍访问
+```typescript
+<DeviceTypeSelector
+  placeholder="选择设备类型(按回车键打开选项)"
+  // 添加ARIA标签
+  aria-label="设备类型选择器"
+/>
+```
+
+## 常见问题
+
+### Q: 选择器不显示图片怎么办?
+A: 确保API路由配置了正确的relations参数:
+```typescript
+// 在 src/server/api/device-types/index.ts 中
+relations: ['imageFile']  // 必须包含这个配置
+```
+
+### Q: 如何自定义选项的显示格式?
+A: 可以修改选择器组件中的渲染逻辑,或者创建自定义的选择器变体。
+
+### Q: 选择器加载失败如何处理?
+A: 组件内置了错误处理,但可以在onChange回调中添加额外的错误处理逻辑。
+
+## 相关组件
+
+- [`UserSelector`](src/client/admin/components/UserSelector.tsx) - 用户选择器
+- [`DeviceSelector`](src/client/admin/components/DeviceSelector.tsx) - 设备选择器  
+- [`FileSelector`](src/client/admin/components/FileSelector.tsx) - 文件选择器
+
+## 版本历史
+
+- v1.0.0 (2024-01-15): 初始版本,支持基本设备类型选择
+- v1.1.0 (2024-01-20): 添加图片显示支持
+- v1.2.0 (2024-01-25): 增加启用状态过滤选项
+
+## 技术支持
+
+如果在使用过程中遇到问题,请检查:
+1. API服务是否正常运行
+2. 设备类型数据是否存在
+3. 网络连接是否正常
+4. 用户权限是否足够

+ 5 - 0
.roo/commands/shadcn-创建页面和组件.md

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

+ 19 - 0
.roo/commands/shadcn-实体选择器创建.md

@@ -0,0 +1,19 @@
+---
+description: "基于实体名称的选择器组件创建指令"
+---
+
+# 基于实体名称的选择器组件抽取指令
+
+## 指令描述
+根据实际实体名称创建专门的选择器组件,每个实体对应一个独立的选择器,保持命名一致性和可预测性。
+
+## 使用方式
+执行此指令将根据项目中实际的实体名称自动创建对应的选择器组件,格式为 `[EntityName]Selector`。
+
+## 实体选择器命名规范
+- 广告类型 → `AdvertisementTypeSelector`
+- 用户 → `UserSelector`
+- 角色 → `RoleSelector`
+- 文件 → `FileSelector`
+- 产品 → `ProductSelector`
+- 通用格式: `[实体名]Selector`

+ 219 - 0
.roo/commands/shadcn-管理页表单分离.md

@@ -0,0 +1,219 @@
+---
+description: "Shadcn-ui 管理页表单创建/编辑表单分离指令"
+---
+
+将 创建/编辑表单分离
+不要 <Form {...(isCreateForm ? createForm : updateForm)}></Form>
+要     
+{isCreateForm ? (
+  // 创建表单(独立渲染)
+  <Form {...createForm}>
+  </Form>
+) : (
+  // 编辑表单(独立渲染)
+  <Form {...updateForm}>
+  </Form>
+)}
+
+
+### 创建/编辑表单分离模式(推荐)
+
+基于 `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>
+```
+

+ 722 - 0
.roo/commands/shadcn-管理页表单开发.md

@@ -0,0 +1,722 @@
+---
+description: "Shadcn-ui 管理页表单开发指令"
+---
+
+## 概述
+基于 `src/client/admin-shadcn/pages/Users.tsx` 中用户管理表单的实现,提取可复用的表单开发模式和最佳实践,适用于基于 Shadcn-ui 的管理后台表单开发。
+
+## 核心特性
+
+### 1. 类型安全表单
+- **后端Schema复用**:直接使用后端定义的 Zod Schema
+- **RPC类型提取**:从 Hono 客户端自动推断类型
+- **一致的类型定义**:前后端类型完全同步
+
+### 2. 表单状态管理(推荐:创建/编辑表单分离模式)
+- **分离表单实例**:为创建和编辑分别使用独立的表单实例, Form组件也分开
+- **类型安全**:创建使用CreateSchema,编辑使用UpdateSchema,避免类型冲突
+- **字段差异处理**:创建时的必填字段在编辑时变为可选,敏感字段特殊处理
+- **状态隔离**:两种模式的状态完全独立,避免交叉污染
+
+### 3. 统一的UI组件模式
+- **Shadcn-ui组件集成**:使用标准的 Shadcn-ui 表单组件
+- **响应式布局**:适配不同屏幕尺寸
+- **无障碍支持**:完整的 ARIA 属性支持
+
+## 开发模板
+
+### 基础结构模板(创建/编辑分离模式)
+```typescript
+// 1. 类型定义(使用后端真实类型)
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
+import { Input } from '@/client/components/ui/input';
+import { Button } from '@/client/components/ui/button';
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
+
+// 2. 分离的表单配置
+const [isCreateForm, setIsCreateForm] = useState(true);
+const [editingEntity, setEditingEntity] = useState<any>(null); // 用于存储编辑时的实体数据
+
+// 3. 独立的表单实例
+const createForm = useForm<CreateRequest>({
+  resolver: zodResolver(CreateEntityDto), // 使用创建专用的Schema
+  defaultValues: {
+    // 创建时必填字段的默认值
+  },
+});
+
+const updateForm = useForm<UpdateRequest>({
+  resolver: zodResolver(UpdateEntityDto), // 使用更新专用的Schema
+  defaultValues: {
+    // 更新时可选字段的默认值(会被实际数据覆盖)
+  },
+});
+
+// 4. 表单切换逻辑(核心模式)
+const handleCreateEntity = () => {
+  setEditingEntity(null);
+  setIsCreateForm(true);
+  createForm.reset({
+    // 创建时的初始值(必填字段必须有值)
+  });
+  setIsModalOpen(true);
+};
+
+const handleEditEntity = (entity: EntityResponse) => {
+  setEditingEntity(entity);
+  setIsCreateForm(false);
+  updateForm.reset({
+    ...entity,
+    // 特殊处理:敏感字段在编辑时设为可选
+    password: undefined, // 密码在更新时可选,不修改则留空
+    // 其他需要特殊处理的字段
+  });
+  setIsModalOpen(true);
+};
+```
+
+### 表单字段模板
+
+#### 文本输入框
+```typescript
+<FormField
+  control={form.control}
+  name="username"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel className="flex items-center">
+        用户名
+        <span className="text-red-500 ml-1">*</span>
+      </FormLabel>
+      <FormControl>
+        <Input placeholder="请输入用户名" {...field} />
+      </FormControl>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+#### 邮箱输入框
+```typescript
+<FormField
+  control={form.control}
+  name="email"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel>邮箱</FormLabel>
+      <FormControl>
+        <Input type="email" placeholder="请输入邮箱" {...field} />
+      </FormControl>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+#### 密码输入框
+```typescript
+<FormField
+  control={form.control}
+  name="password"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel className="flex items-center">
+        密码
+        <span className="text-red-500 ml-1">*</span>
+      </FormLabel>
+      <FormControl>
+        <Input type="password" placeholder="请输入密码" {...field} />
+      </FormControl>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+#### 开关控件(布尔值)
+```typescript
+<FormField
+  control={form.control}
+  name="isDisabled"
+  render={({ field }) => (
+    <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
+      <div className="space-y-0.5">
+        <FormLabel className="text-base">状态</FormLabel>
+        <FormDescription>
+          禁用后用户将无法登录系统
+        </FormDescription>
+      </div>
+      <FormControl>
+        <Switch
+          checked={field.value === 1}
+          onCheckedChange={(checked) => field.onChange(checked ? 1 : 0)}
+        />
+      </FormControl>
+    </FormItem>
+  )}
+/>
+```
+
+#### 可选字段处理
+```typescript
+// 创建时:必须提供值
+nickname: z.string().optional()
+
+// 更新时:完全可选
+nickname: z.string().optional()
+```
+
+## 高级表单组件模板
+
+### 头像选择器集成
+```typescript
+import AvatarSelector from '@/client/admin-shadcn/components/AvatarSelector';
+
+<FormField
+  control={form.control}
+  name="avatarFileId"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel>头像</FormLabel>
+      <FormControl>
+        <AvatarSelector
+          value={field.value || undefined}
+          onChange={(value) => field.onChange(value)}
+          maxSize={2}
+          uploadPath="/avatars"
+          uploadButtonText="上传头像"
+          previewSize="medium"
+          placeholder="选择头像"
+        />
+      </FormControl>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+### 下拉选择框
+```typescript
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/client/components/ui/select';
+
+<FormField
+  control={form.control}
+  name="status"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel>状态</FormLabel>
+      <Select onValueChange={field.onChange} defaultValue={String(field.value)}>
+        <FormControl>
+          <SelectTrigger>
+            <SelectValue placeholder="请选择状态" />
+          </SelectTrigger>
+        </FormControl>
+        <SelectContent>
+          <SelectItem value="0">启用</SelectItem>
+          <SelectItem value="1">禁用</SelectItem>
+        </SelectContent>
+      </Select>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+### 数字输入框
+```typescript
+<FormField
+  control={form.control}
+  name="age"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel>年龄</FormLabel>
+      <FormControl>
+        <Input 
+          type="number" 
+          placeholder="请输入年龄"
+          {...field}
+          onChange={(e) => field.onChange(Number(e.target.value))}
+        />
+      </FormControl>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+### 日期选择器
+```typescript
+import { Popover, PopoverContent, PopoverTrigger } from '@/client/components/ui/popover';
+import { Calendar } from '@/client/components/ui/calendar';
+import { CalendarIcon } from 'lucide-react';
+import { cn } from '@/client/lib/utils';
+
+<FormField
+  control={form.control}
+  name="birthDate"
+  render={({ field }) => (
+    <FormItem className="flex flex-col">
+      <FormLabel>出生日期</FormLabel>
+      <Popover>
+        <PopoverTrigger asChild>
+          <FormControl>
+            <Button
+              variant={"outline"}
+              className={cn(
+                "w-[240px] pl-3 text-left font-normal",
+                !field.value && "text-muted-foreground"
+              )}
+            >
+              {field.value ? (
+                format(field.value, "yyyy-MM-dd")
+              ) : (
+                <span>选择日期</span>
+              )}
+              <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
+            </Button>
+          </FormControl>
+        </PopoverTrigger>
+        <PopoverContent className="w-auto p-0" align="start">
+          <Calendar
+            mode="single"
+            selected={field.value}
+            onSelect={field.onChange}
+            disabled={(date) =>
+              date > new Date() || date < new Date("1900-01-01")
+            }
+            initialFocus
+          />
+        </PopoverContent>
+      </Popover>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+### 文本域
+```typescript
+import { Textarea } from '@/client/components/ui/textarea';
+
+<FormField
+  control={form.control}
+  name="description"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel>描述</FormLabel>
+      <FormControl>
+        <Textarea
+          placeholder="请输入描述信息"
+          className="resize-none"
+          {...field}
+        />
+      </FormControl>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+### 复选框组
+```typescript
+import { Checkbox } from '@/client/components/ui/checkbox';
+
+<FormField
+  control={form.control}
+  name="permissions"
+  render={() => (
+    <FormItem>
+      <div className="mb-4">
+        <FormLabel className="text-base">权限设置</FormLabel>
+        <FormDescription>
+          选择该用户拥有的权限
+        </FormDescription>
+      </div>
+      <div className="space-y-2">
+        {permissions.map((permission) => (
+          <FormField
+            key={permission.id}
+            control={form.control}
+            name="permissions"
+            render={({ field }) => {
+              return (
+                <FormItem
+                  key={permission.id}
+                  className="flex flex-row items-start space-x-3 space-y-0"
+                >
+                  <FormControl>
+                    <Checkbox
+                      checked={field.value?.includes(permission.id)}
+                      onCheckedChange={(checked) => {
+                        return checked
+                          ? field.onChange([...field.value, permission.id])
+                          : field.onChange(
+                              field.value?.filter(
+                                (value) => value !== permission.id
+                              )
+                            )
+                      }}
+                    />
+                  </FormControl>
+                  <div className="space-y-1 leading-none">
+                    <FormLabel>
+                      {permission.name}
+                    </FormLabel>
+                    <FormDescription>
+                      {permission.description}
+                    </FormDescription>
+                  </div>
+                </FormItem>
+              )
+            }}
+          />
+        ))}
+      </div>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+## 表单状态管理模板
+
+### 创建/编辑表单分离模式(推荐)
+
+基于 `src/client/admin-shadcn/pages/Users.tsx` 的最佳实践,创建/编辑表单分离模式通过以下方式解决创建/编辑时数据对象类型差异问题:
+
+#### 核心优势
+1. **类型安全**:创建使用 `CreateEntityDto`,编辑使用 `UpdateEntityDto`,避免类型冲突
+2. **字段差异处理**:创建时的必填字段在编辑时自动变为可选
+3. **敏感字段处理**:密码等敏感字段在编辑时可设为可选
+4. **状态隔离**:两种模式完全独立,避免状态污染
+
+#### 完整实现模板
+```typescript
+// 1. 类型定义(使用真实后端类型)
+type CreateRequest = InferRequestType<typeof apiClient.$post>['json'];
+type UpdateRequest = InferRequestType<typeof apiClient[':id']['$put']>['json'];
+type EntityResponse = InferResponseType<typeof apiClient.$get, 200>['data'][0];
+
+// 2. 状态管理
+const [isModalOpen, setIsModalOpen] = useState(false);
+const [editingEntity, setEditingEntity] = useState<EntityResponse | null>(null);
+const [isCreateForm, setIsCreateForm] = useState(true);
+
+// 3. 分离的表单实例
+const createForm = useForm<CreateRequest>({
+  resolver: zodResolver(CreateEntityDto),
+  defaultValues: {
+    // 创建时必须提供的默认值
+  },
+});
+
+const updateForm = useForm<UpdateRequest>({
+  resolver: zodResolver(UpdateEntityDto),
+  defaultValues: {
+    // 更新时的默认值(会被实际数据覆盖)
+  },
+});
+
+// 4. 表单操作函数
+const handleCreate = () => {
+  setEditingEntity(null);
+  setIsCreateForm(true);
+  createForm.reset({
+    // 创建时的初始值
+    status: 1, // 示例:默认启用
+  });
+  setIsModalOpen(true);
+};
+
+const handleEdit = (entity: EntityResponse) => {
+  setEditingEntity(entity);
+  setIsCreateForm(false);
+  updateForm.reset({
+    ...entity,
+    // 关键:处理创建/编辑字段差异
+    password: undefined, // 密码在更新时可选
+    confirmPassword: undefined,
+    // 其他需要特殊处理的字段
+  });
+  setIsModalOpen(true);
+};
+
+// 5. 提交处理
+const handleCreateSubmit = async (data: CreateRequest) => {
+  try {
+    const res = await apiClient.$post({ json: data });
+    if (res.status !== 201) throw new Error('创建失败');
+    toast.success('创建成功');
+    setIsModalOpen(false);
+    refetch();
+  } catch (error) {
+    toast.error('创建失败,请重试');
+  }
+};
+
+const handleUpdateSubmit = async (data: UpdateRequest) => {
+  if (!editingEntity) return;
+  
+  try {
+    const res = await apiClient[':id']['$put']({
+      param: { id: editingEntity.id },
+      json: data
+    });
+    if (res.status !== 200) throw new Error('更新失败');
+    toast.success('更新成功');
+    setIsModalOpen(false);
+    refetch();
+  } catch (error) {
+    toast.error('更新失败,请重试');
+  }
+};
+```
+
+#### 对话框渲染模板
+```tsx
+<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
+  <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
+    <DialogHeader>
+      <DialogTitle>{isCreateForm ? '创建实体' : '编辑实体'}</DialogTitle>
+      <DialogDescription>
+        {isCreateForm ? '创建一个新的实体' : '编辑现有实体信息'}
+      </DialogDescription>
+    </DialogHeader>
+    
+    {isCreateForm ? (
+      // 创建表单(独立渲染)
+      <Form {...createForm}>
+        <form onSubmit={createForm.handleSubmit(handleCreateSubmit)} className="space-y-4">
+          {/* 创建专用字段 - 必填 */}
+          <FormField
+            control={createForm.control}
+            name="username"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel className="flex items-center">
+                  用户名
+                  <span className="text-red-500 ml-1">*</span>
+                </FormLabel>
+                <FormControl>
+                  <Input placeholder="请输入用户名" {...field} />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+          
+          {/* 创建时必填的密码 */}
+          <FormField
+            control={createForm.control}
+            name="password"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel className="flex items-center">
+                  密码
+                  <span className="text-red-500 ml-1">*</span>
+                </FormLabel>
+                <FormControl>
+                  <Input type="password" placeholder="请输入密码" {...field} />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+          
+          <DialogFooter>
+            <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+              取消
+            </Button>
+            <Button type="submit">创建</Button>
+          </DialogFooter>
+        </form>
+      </Form>
+    ) : (
+      // 编辑表单(独立渲染)
+      <Form {...updateForm}>
+        <form onSubmit={updateForm.handleSubmit(handleUpdateSubmit)} className="space-y-4">
+          {/* 编辑专用字段 - 可选 */}
+          <FormField
+            control={updateForm.control}
+            name="username"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel className="flex items-center">
+                  用户名
+                  <span className="text-red-500 ml-1">*</span>
+                </FormLabel>
+                <FormControl>
+                  <Input placeholder="请输入用户名" {...field} />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+          
+          {/* 编辑时可选的密码 */}
+          <FormField
+            control={updateForm.control}
+            name="password"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel>新密码</FormLabel>
+                <FormControl>
+                  <Input type="password" placeholder="留空则不修改密码" {...field} />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+          
+          <DialogFooter>
+            <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+              取消
+            </Button>
+            <Button type="submit">更新</Button>
+          </DialogFooter>
+        </form>
+      </Form>
+    )}
+  </DialogContent>
+</Dialog>
+```
+
+## 最佳实践
+
+### 1. 表单验证
+- 使用 Zod Schema 进行类型验证
+- 必填字段标记红色星号
+- 提供清晰的错误提示
+
+### 2. 用户体验
+- 表单提交时显示加载状态
+- 操作成功后显示 toast 通知
+- 支持键盘导航和提交
+
+### 3. 数据管理
+- 创建后自动刷新数据列表
+- 编辑时回填现有数据
+- 支持表单重置功能
+
+### 4. 响应式设计
+- 对话框最大宽度 `sm:max-w-[500px]`
+- 表单间距统一使用 `space-y-4`
+- 移动端友好的布局
+
+### 5. 性能优化
+- 使用 React.memo 优化表单组件
+- 合理使用 useCallback 和 useMemo
+- 避免不必要的重新渲染
+
+### 6. 错误处理
+- 统一的错误处理机制
+- 友好的错误提示信息
+- 网络错误重试机制
+
+## 使用示例
+
+### 完整实现参考
+```typescript
+// 创建记录
+const handleCreateSubmit = async (data: CreateFormData) => {
+  try {
+    const res = await apiClient.$post({ json: data });
+    if (res.status !== 201) throw new Error('创建失败');
+    toast.success('创建成功');
+    setIsModalOpen(false);
+    refetch();
+  } catch (error) {
+    toast.error('创建失败,请重试');
+  }
+};
+
+// 更新记录
+const handleUpdateSubmit = async (data: UpdateFormData) => {
+  try {
+    const res = await apiClient[':id']['$put']({
+      param: { id: editingData.id },
+      json: data
+    });
+    if (res.status !== 200) throw new Error('更新失败');
+    toast.success('更新成功');
+    setIsModalOpen(false);
+    refetch();
+  } catch (error) {
+    toast.error('更新失败,请重试');
+  }
+};
+```
+
+## 组件导入清单
+```typescript
+// 表单相关
+import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
+import { Input } from '@/client/components/ui/input';
+import { Button } from '@/client/components/ui/button';
+import { Switch } from '@/client/components/ui/switch';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/client/components/ui/select';
+import { Textarea } from '@/client/components/ui/textarea';
+import { Checkbox } from '@/client/components/ui/checkbox';
+
+// 对话框相关
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
+
+// 高级组件
+import { Popover, PopoverContent, PopoverTrigger } from '@/client/components/ui/popover';
+import { Calendar } from '@/client/components/ui/calendar';
+import AvatarSelector from '@/client/admin-shadcn/components/AvatarSelector';
+
+// 表单工具
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { toast } from 'sonner';
+import { format } from 'date-fns';
+import { CalendarIcon } from 'lucide-react';
+import { cn } from '@/client/lib/utils';
+```
+
+## 常见问题解决方案
+
+### 1. 表单默认值问题
+```typescript
+// 正确处理 null/undefined 值
+defaultValues: {
+  name: null, // 允许 null
+  description: undefined, // 允许 undefined
+}
+```
+
+### 2. 数字类型转换
+```typescript
+// 在 onChange 中转换类型
+onChange={(e) => field.onChange(Number(e.target.value))}
+```
+
+### 3. 日期类型处理
+```typescript
+// 日期选择器返回值处理
+onSelect={(date) => field.onChange(date ? new Date(date) : null)}
+```
+
+### 4. 数组类型处理
+```typescript
+// 复选框组处理数组
+onCheckedChange={(checked) => {
+  const newValue = checked 
+    ? [...field.value, item.id] 
+    : field.value.filter(id => id !== item.id);
+  field.onChange(newValue);
+}}
+```
+
+### 5. 表单重置注意事项
+```typescript
+// 更新表单时正确重置
+updateForm.reset({
+  ...data,
+  password: undefined, // 密码字段特殊处理
+  confirmPassword: undefined,
+});

+ 879 - 0
.roo/commands/shadcn-管理页面开发指南.md

@@ -0,0 +1,879 @@
+---
+description: "Shadcn-ui 管理页开发指令"
+---
+
+## 概述
+基于 `src/client/admin/pages/Users.tsx` 中用户管理页的实现,提取可复用的开发模式和最佳实践,适用于基于 Shadcn-ui 的管理后台页面开发。
+
+## 页面结构规范
+
+### 1. 文件位置
+- **管理后台页面**: `src/client/admin/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>
+
+{data?.data.length === 0 && !isLoading && (
+  <div className="text-center py-8">
+    <p className="text-muted-foreground">暂无数据</p>
+  </div>
+)}
+
+<DataTablePagination
+  currentPage={searchParams.page}
+  pageSize={searchParams.limit}
+  totalCount={data?.pagination.total || 0}
+  onPageChange={(page, limit) => setSearchParams(prev => ({ ...prev, page, limit }))}
+/>
+```
+
+## 表单开发模式
+
+### 1. 表单组件结构
+```typescript
+// 创建表单
+const createForm = useForm<CreateRequest>({
+  resolver: zodResolver(createFormSchema),
+  defaultValues: {
+    // 默认值设置
+  },
+});
+
+// 更新表单
+const updateForm = useForm<UpdateRequest>({
+  resolver: zodResolver(updateFormSchema),
+  defaultValues: {
+    // 更新时默认值
+  },
+});
+```
+
+### 2. 模态框表单(创建/编辑分离模式)
+
+将 创建/编辑表单分离
+不要 <Form {...(isCreateForm ? createForm : updateForm)}></Form>
+要     
+{isCreateForm ? (
+  // 创建表单(独立渲染)
+  <Form {...createForm}>
+  </Form>
+) : (
+  // 编辑表单(独立渲染)
+  <Form {...updateForm}>
+  </Form>
+)}
+
+```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. ImageSelector组件使用
+```tsx
+import { ImageSelector } from '@/client/admin/components/ImageSelector';
+
+<FormField
+  control={form.control}
+  name="avatarFileId"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel>头像</FormLabel>
+      <FormControl>
+        <ImageSelector
+          value={field.value || undefined}
+          onChange={(value) => field.onChange(value)}
+          maxSize={2} // MB
+          uploadPath="/avatars"
+          uploadButtonText="上传头像"
+          previewSize="medium"
+          placeholder="选择头像"
+        />
+      </FormControl>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+## 文件上传集成
+
+### 1. FileSelector组件使用
+```tsx
+import { FileSelector } from '@/client/admin/components/FileSelector';
+
+<FormField
+  control={form.control}
+  name="avatarFileId"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel>头像</FormLabel>
+      <FormControl>
+        <FileSelector
+          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. 骨架屏模式
+
+#### 1.1 导入依赖
+```typescript
+import { Skeleton } from '@/client/components/ui/skeleton';
+```
+
+#### 1.2 完整骨架屏实现
+```tsx
+if (isLoading) {
+  return (
+    <div className="space-y-4">
+      {/* 标题区域骨架 */}
+      <div className="flex justify-between items-center">
+        <Skeleton className="h-8 w-48" />
+        <Skeleton className="h-10 w-32" />
+      </div>
+      
+      {/* 搜索区域骨架 */}
+      <Card>
+        <CardHeader>
+          <Skeleton className="h-6 w-1/4" />
+        </CardHeader>
+        <CardContent>
+          <Skeleton className="h-10 w-full max-w-sm" />
+        </CardContent>
+      </Card>
+      
+      {/* 表格骨架 */}
+      <Card>
+        <CardHeader>
+          <Skeleton className="h-6 w-1/3" />
+        </CardHeader>
+        <CardContent>
+          <Table>
+            <TableHeader>
+              <TableRow>
+                {[...Array(5)].map((_, i) => (
+                  <TableHead key={i}>
+                    <Skeleton className="h-4 w-full" />
+                  </TableHead>
+                ))}
+              </TableRow>
+            </TableHeader>
+            <TableBody>
+              {[...Array(5)].map((_, i) => (
+                <TableRow key={i}>
+                  {[...Array(5)].map((_, j) => (
+                    <TableCell key={j}>
+                      <Skeleton className="h-4 w-full" />
+                    </TableCell>
+                  ))}
+                </TableRow>
+              ))}
+            </TableBody>
+          </Table>
+        </CardContent>
+      </Card>
+    </div>
+  );
+}
+```
+
+#### 1.3 简化骨架屏(推荐)
+```tsx
+if (isLoading) {
+  return (
+    <div className="space-y-4">
+      <div className="flex justify-between items-center">
+        <Skeleton className="h-8 w-48" />
+        <Skeleton className="h-10 w-32" />
+      </div>
+      
+      <Card>
+        <CardContent className="pt-6">
+          <div className="space-y-3">
+            {[...Array(5)].map((_, i) => (
+              <div key={i} className="flex gap-4">
+                <Skeleton className="h-10 flex-1" />
+                <Skeleton className="h-10 flex-1" />
+                <Skeleton className="h-10 flex-1" />
+                <Skeleton className="h-10 w-20" />
+              </div>
+            ))}
+          </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>
+
+// 删除成功状态码为204
+const confirmDelete = async () => {
+  if (!entityToDelete) return;
+  
+  try {
+    const res = await entityClient[':id']['$delete']({
+      param: { id: entityToDelete.toString() }
+    });
+    
+    if (res.status === 204) {
+      toast.success('删除成功');
+      setDeleteDialogOpen(false);
+      refetch(); // 刷新数据
+    } else {
+      throw new Error('删除失败');
+    }
+  } catch (error) {
+    toast.error('删除失败,请重试');
+  }
+};
+```
+
+## 样式规范
+
+### 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`
+- **图片字段**: 使用 `ImageSelector`
+
+### 3. 业务逻辑复用
+- 保持相同的CRUD操作模式
+- 复用分页、搜索、排序逻辑
+- 统一的状态管理模式
+- 一致的表单验证和错误处理
+
+## 高级组件集成
+
+### 1. DataTablePagination 分页组件
+
+#### 1.1 使用方式
+```tsx
+import { DataTablePagination } from '@/client/admin/components/DataTablePagination';
+
+<DataTablePagination
+  currentPage={searchParams.page}
+  pageSize={searchParams.limit}
+  totalCount={data?.pagination.total || 0}
+  onPageChange={(page, limit) => setSearchParams(prev => ({ ...prev, page, limit }))}
+/>
+```
+
+#### 1.2 参数说明
+| 参数 | 类型 | 描述 |
+|------|------|------|
+| currentPage | number | 当前页码 |
+| pageSize | number | 每页显示条数 |
+| totalCount | number | 总记录数 |
+| onPageChange | function | 页码变化回调函数 |
+
+#### 1.3 集成到状态管理
+```typescript
+const [searchParams, setSearchParams] = useState({
+  page: 1,
+  limit: 10,
+  search: ''
+});
+
+// 在数据查询中
+const { data } = useQuery({
+  queryKey: ['entities', searchParams],
+  queryFn: async () => {
+    const res = await client.$get({
+      query: {
+        page: searchParams.page,
+        pageSize: searchParams.limit,
+        keyword: searchParams.search
+      }
+    });
+    return await res.json();
+  }
+});
+```
+
+### 2. 关联实体Selector组件
+
+#### 2.1 AdvertisementTypeSelector 示例
+```tsx
+import AdvertisementTypeSelector from '@/client/admin/components/AdvertisementTypeSelector';
+
+<FormField
+  control={form.control}
+  name="typeId"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel>广告类型</FormLabel>
+      <FormControl>
+        <AdvertisementTypeSelector
+          value={field.value}
+          onChange={field.onChange}
+          placeholder="请选择广告类型"
+        />
+      </FormControl>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+#### 2.2 自定义Selector开发模式
+```typescript
+// 通用Selector接口设计
+interface EntitySelectorProps {
+  value?: number;
+  onChange?: (value: number) => void;
+  placeholder?: string;
+  disabled?: boolean;
+}
+
+// 实现模式
+const EntitySelector: React.FC<EntitySelectorProps> = ({
+  value,
+  onChange,
+  placeholder = "请选择",
+  disabled
+}) => {
+  const { data } = useQuery({
+    queryKey: ['entities'],
+    queryFn: async () => {
+      const res = await entityClient.$get();
+      return await res.json();
+    }
+  });
+
+  return (
+    <Select value={value?.toString()} onValueChange={(v) => onChange?.(parseInt(v))}>
+      <SelectTrigger disabled={disabled}>
+        <SelectValue placeholder={placeholder} />
+      </SelectTrigger>
+      <SelectContent>
+        {data?.data.map((item) => (
+          <SelectItem key={item.id} value={item.id.toString()}>
+            {item.name}
+          </SelectItem>
+        ))}
+      </SelectContent>
+    </Select>
+  );
+};
+```
+
+#### 2.3 图片选择器集成
+```tsx
+import ImageSelector from '@/client/admin/components/ImageSelector';
+
+<FormField
+  control={form.control}
+  name="imageFileId"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel>广告图片</FormLabel>
+      <FormControl>
+        <ImageSelector
+          value={field.value || undefined}
+          onChange={field.onChange}
+          maxSize={2} // MB
+          uploadPath="/advertisements"
+          uploadButtonText="上传广告图片"
+          previewSize="medium"
+          placeholder="选择广告图片"
+          title="选择广告图片"
+          description="上传新图片或从已有图片中选择"
+        />
+      </FormControl>
+      <FormDescription>推荐尺寸:1200x400px,支持jpg、png格式</FormDescription>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+### 3. 复杂字段展示模式
+
+#### 3.1 关联实体字段展示
+```tsx
+<TableCell>
+  {advertisement.advertisementType?.name || '-'}
+</TableCell>
+```
+
+#### 3.2 状态字段展示
+```tsx
+<TableCell>
+  <Badge variant={advertisement.status === 1 ? 'default' : 'secondary'}>
+    {advertisement.status === 1 ? '启用' : '禁用'}
+  </Badge>
+</TableCell>
+```
+
+#### 3.3 图片字段展示
+```tsx
+<TableCell>
+  {advertisement.imageFile?.fullUrl ? (
+    <img
+      src={advertisement.imageFile.fullUrl}
+      alt={advertisement.title || '图片'}
+      className="w-16 h-10 object-cover rounded"
+      onError={(e) => {
+        e.currentTarget.src = '/placeholder.png';
+      }}
+    />
+  ) : (
+    <span className="text-muted-foreground text-xs">无图片</span>
+  )}
+</TableCell>
+```
+
+### 4. 表单字段类型映射
+
+#### 4.1 标准字段映射
+| 字段类型 | 组件 | 示例 |
+|----------|------|------|
+| 文本输入 | Input | `<Input placeholder="请输入标题" {...field} />` |
+| 长文本 | Textarea | `<Textarea placeholder="请输入描述" {...field} />` |
+| 选择器 | Select | `<Select value={field.value} onValueChange={field.onChange}>` |
+| 数字输入 | Input | `<Input type="number" {...field} />` |
+| 日期选择 | DatePicker | `<DatePicker selected={field.value} onChange={field.onChange} />` |
+| 开关 | Switch | `<Switch checked={field.value} onCheckedChange={field.onChange} />` |
+| 文件上传 | ImageSelector | `<ImageSelector value={field.value} onChange={field.onChange} />` |
+
+#### 4.2 关联实体选择
+```tsx
+// 直接使用Selector组件
+<FormField
+  control={form.control}
+  name="typeId"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel>广告类型</FormLabel>
+      <FormControl>
+        <AdvertisementTypeSelector {...field} />
+      </FormControl>
+    </FormItem>
+  )}
+/>
+```
+
+### 4.3 日期格式化规范
+
+#### 4.3.1 导入依赖
+```typescript
+import { format } from 'date-fns';
+```
+
+#### 4.3.2 日期显示格式
+```tsx
+// 标准日期时间格式:yyyy-MM-dd HH:mm
+<TableCell>
+  {user.createdAt ? format(new Date(user.createdAt), 'yyyy-MM-dd HH:mm') : '-'}
+</TableCell>
+
+// 仅日期格式:yyyy-MM-dd
+<TableCell>
+  {user.birthday ? format(new Date(user.birthday), 'yyyy-MM-dd') : '-'}
+</TableCell>
+
+// 完整时间格式:yyyy-MM-dd HH:mm:ss
+<TableCell>
+  {user.updatedAt ? format(new Date(user.updatedAt), 'yyyy-MM-dd HH:mm:ss') : '-'}
+</TableCell>
+```
+
+#### 4.3.3 日期输入格式
+```tsx
+// 在表单中使用日期选择器
+<FormField
+  control={form.control}
+  name="startDate"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel>开始日期</FormLabel>
+      <FormControl>
+        <Input
+          type="date"
+          {...field}
+          value={field.value ? format(new Date(field.value), 'yyyy-MM-dd') : ''}
+          onChange={(e) => field.onChange(e.target.value)}
+        />
+      </FormControl>
+    </FormItem>
+  )}
+/>
+```
+
+#### 4.3.4 相对时间显示(可选)
+```typescript
+import { formatDistanceToNow } from 'date-fns';
+import { zhCN } from 'date-fns/locale';
+
+// 相对时间显示
+<TableCell>
+  {user.createdAt ? formatDistanceToNow(new Date(user.createdAt), { addSuffix: true, locale: zhCN }) : '-'}
+</TableCell>
+```
+
+### 4.4 消息通知规范
+
+#### 4.4.1 导入依赖
+```typescript
+import { toast } from 'sonner';
+```
+
+#### 4.4.2 使用规范
+```typescript
+// 成功通知
+toast.success('操作成功');
+toast.success('用户创建成功');
+
+// 错误通知
+toast.error('操作失败');
+toast.error('创建用户失败,请重试');
+
+// 警告通知
+toast.warning('请确认操作');
+toast.warning('该操作将删除所有相关数据');
+
+// 信息通知
+toast.info('操作提示');
+toast.info('正在处理中,请稍候...');
+```
+
+#### 4.4.3 与API响应集成
+```typescript
+try {
+  const res = await entityClient.$post({ json: data });
+  if (res.status === 201) {
+    toast.success('创建成功');
+    setIsModalOpen(false);
+    refetch();
+  } else {
+    const error = await res.json();
+    toast.error(error.message || '操作失败');
+  }
+} catch (error) {
+  console.error('操作失败:', error);
+  toast.error('网络错误,请重试');
+}
+```

+ 7 - 0
.roo/commands/show.sh

@@ -0,0 +1,7 @@
+for file in *; do
+    if [ -f "$file" ]; then
+      echo "===== $file ====="
+      head -n 3 "$file"
+      echo
+    fi
+  done

+ 213 - 0
.roo/commands/user-实体用户关联开发.md

@@ -0,0 +1,213 @@
+---
+description: "实体现有字段与用户实体关联开发指令"
+---
+
+
+## 适用场景
+
+当实体中已经存在用户相关的字段(如 `handlerId`, `createdBy`, `userId`, `operatorId` 等),需要将这些字段与 `User` 实体建立关联关系时使用。
+
+## 开发步骤
+
+### 1. 修改实体定义
+
+找到实体文件 `src/server/modules/[模块名]/[实体名].entity.ts`,将现有用户字段改为关联关系:
+
+#### 1.1 替换字段类型和装饰器
+
+```typescript
+// 修改前:原始字段定义
+@Column({ name: 'handler_id', type: 'int', comment: '处理人ID' })
+handlerId!: number;
+
+// 修改后:单向关联关系定义
+@ManyToOne(() => User)
+@JoinColumn({ name: 'handler_id' })
+handler!: User;
+
+// 如果允许为空(单向关联)
+@ManyToOne(() => User, { nullable: true })
+@JoinColumn({ name: 'handler_id' })
+handler?: User | null;
+```
+
+#### 1.2 添加User实体导入
+
+```typescript
+// 在实体顶部添加
+import { User } from '@/server/modules/users/user.entity';
+```
+
+#### 1.3 在User实体中添加反向关联(可选)
+
+如果需要双向关联,在User实体中添加对应的关系定义:
+
+```typescript
+// 在User实体中添加对应的关系定义(双向关联)
+@OneToMany(() => AlertHandleLog, log => log.handler)
+alertHandleLogs?: AlertHandleLog[];
+```
+
+> **注意**:单向关联不需要此步骤,保持现有配置即可
+
+### 2. 更新Zod Schema
+
+在对应的 schema 文件中同步修改:
+
+#### 2.1 修改实体Schema
+
+```typescript
+// 在文件顶部添加导入
+import { UserSchema } from '@/server/modules/users/user.schema';
+
+// 修改前
+handlerId: z.number().int().positive().openapi({
+  description: '处理人ID',
+  example: 1
+})
+
+// 修改后 - 使用导入的UserSchema
+handler: UserSchema.omit({ password: true }).nullable().optional().openapi({
+  description: '处理人信息'
+})
+```
+
+#### 2.2 修改Create/Update DTO
+
+```typescript
+// 修改前:Create DTO
+export const CreateAlertHandleLogDto = z.object({
+  handlerId: z.number().int().positive().openapi({
+    description: '处理人ID',
+    example: 1
+  }),
+  // ... 其他字段
+});
+
+// 修改后:Create DTO
+export const CreateAlertHandleLogDto = z.object({
+  handlerId: z.number().int().positive().openapi({
+    description: '处理人ID',
+    example: 1
+  }),
+  // ... 其他字段
+});
+
+// Update DTO保持不变,但handlerId改为optional
+export const UpdateAlertHandleLogDto = z.object({
+  handlerId: z.number().int().positive().optional().openapi({
+    description: '处理人ID',
+    example: 1
+  }),
+  // ... 其他字段
+});
+```
+
+### 4. 更新通用CRUD配置
+
+如果使用通用CRUD路由,需要配置relations:
+
+```typescript
+const routes = createCrudRoutes({
+  entity: AlertHandleLog,
+  createSchema: CreateAlertHandleLogDto,
+  updateSchema: UpdateAlertHandleLogDto,
+  getSchema: AlertHandleLogSchema,
+  listSchema: AlertHandleLogSchema,
+  relations: ['handler'], // 添加关联配置
+  middleware: [authMiddleware]
+});
+```
+
+## 命名规范
+
+| 类型 | 命名格式 | 示例 | 说明 |
+|------|----------|------|------|
+| 数据库字段 | `{前缀}_id` | `handler_id` | 保持原有字段名不变 |
+| 实体字段 | `{前缀}` | `handler` | 关联实体对象 |
+| 反向关联 | `{实体名}s` | `alertHandleLogs` | User实体中的集合名 |
+| 外键字段 | `{前缀}Id` | `handlerId` | DTO中的外键字段 |
+
+## 完整示例
+
+### 实体类 ([`src/server/modules/alerts/alert-handle-log.entity.ts`](src/server/modules/alerts/alert-handle-log.entity.ts))
+
+```typescript
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
+import { User } from '@/server/modules/users/user.entity';
+
+@Entity('alert_handle_logs')
+export class AlertHandleLog {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'handler_id', type: 'int', comment: '处理人ID' })
+  handlerId!: number;
+
+  // 单向关联定义(无需反向引用)
+  @ManyToOne(() => User)
+  @JoinColumn({ name: 'handler_id' })
+  handler!: User;
+
+  // ... 其他字段
+}
+```
+
+### User实体反向关联(可选,仅双向关联需要)
+
+如果需要双向关联,在User实体中添加:
+
+```typescript
+@OneToMany(() => AlertHandleLog, log => log.handler)
+alertHandleLogs?: AlertHandleLog[];
+```
+
+> **单向关联说明**:当前配置为单向关联,无需在User实体中添加反向关系定义
+
+### Schema定义 ([`src/server/modules/alerts/alert-handle-log.schema.ts`](src/server/modules/alerts/alert-handle-log.schema.ts))
+
+```typescript
+// 引入用户Schema
+import { UserSchema } from '@/server/modules/users/user.schema';
+
+// 响应Schema - 使用UserSchema.omit去掉敏感字段
+handler: UserSchema.omit({ password: true }).nullable().optional().openapi({
+  description: '处理人信息'
+}),
+
+// 请求DTO中的外键字段
+handlerId: z.number().int().positive().openapi({
+  description: '处理人ID',
+  example: 1
+})
+```
+
+## 注意事项
+
+1. **数据库兼容性**:现有数据库字段保持不变,只是从整数字段升级为外键约束
+2. **API兼容性**:DTO中的外键字段名保持不变(如handlerId),确保API接口兼容
+3. **查询性能**:配置relations后,关联查询会自动优化,避免N+1问题
+4. **级联操作**:默认不级联删除,删除用户不会影响关联记录
+5. **空值处理**:如果handlerId为null,handler关联对象将为null
+
+## 常见问题
+
+### Q: 如何支持多个用户关联字段?
+A: 重复上述步骤,为每个用户字段建立独立的关联关系,例如:
+```typescript
+@ManyToOne(() => User, user => user.createdAlerts)
+@JoinColumn({ name: 'created_by' })
+creator!: User;
+
+@ManyToOne(() => User, user => user.updatedAlerts)
+@JoinColumn({ name: 'updated_by' })
+updater?: User | null;
+```
+
+### Q: 如何处理复合用户关联?
+A: 使用相同的方法,只是字段名不同:
+```typescript
+@ManyToOne(() => User, user => user.assignedTasks)
+@JoinColumn({ name: 'assignee_id' })
+assignee!: User;
+```

+ 28 - 13
.roo/rules/01-general.md

@@ -12,24 +12,22 @@ src/
 │   └── middleware/ # 中间件
 ```
 
+#### 小程序 (Mini)
+```
+mini/
+├── config/       # 配置文件
+├── src/          # 源代码
+│   ├── pages/    # 页面组件
+│   ├── utils/    # 工具函数
+│   └── assets/   # 静态资源
+└── types/        # 类型定义
+```
+
 
 ## 技术栈
 
 ### 前端
 
-#### 管理后台 (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** (严格模式) - 类型系统
@@ -59,12 +57,27 @@ src/
 + @hookform/resolvers 5.2.1
 + zod 4.0.14
 
+#### 小程序 (Mini)
+- **Taro 4** - 跨端框架
+- **React 18** - 前端框架
+- **TypeScript** (严格模式) - 类型系统
+- **Tailwind CSS 4** - 样式框架
+- **@egoist/tailwindcss-icons** - 图标样式
+- **clsx** - class样式
+- **React Query (TanStack) 5** - 数据获取和缓存
++ @hookform/resolvers 5.2.1
+- **React Hook Form 7** - 表单处理
++ zod 4.0.14
+
 ### 后端
 - **Hono 4** - Web框架
 - **TypeORM** - ORM框架 (MySQL)
 - **MySQL 2** - 数据库驱动
+- **Redis (ioredis)** - 缓存和会话管理
 - **bcrypt** - 密码加密
 - **jsonwebtoken** - JWT认证
+- **OpenAI SDK** - AI集成
+- **MinIO** - 对象存储
 - **compression** - 压缩中间件
 
 ### 工具链
@@ -76,6 +89,8 @@ src/
 - **dayjs** - 日期处理
 - **axios** - HTTP客户端
 
+### 数据可视化
+
 ### 开发工具
 - **TypeScript** - 类型检查
 - **Tailwind CSS** - 原子化CSS

+ 1 - 1
.roo/rules/07-openapi.md

@@ -15,7 +15,7 @@
 
 4. **URL参数类型转换**:
    - ❌ 直接使用z.number()验证URL查询参数
-   - ✅ 必须使用z.coerce.number()自动转换字符串参数
+   - ✅ 必须使用z.coerce.number<number>()自动转换字符串参数, zod v4必须要加 <number>
 
 5. **OpenAPI元数据**:
    - ❌ 路径参数缺少OpenAPI描述

+ 26 - 21
.roo/rules/10-entity.md

@@ -108,10 +108,11 @@ updatedAt!: Date;
 ```typescript
 import { z } from '@hono/zod-openapi';
 export const EntitySchema = z.object({
-  id: z.number().int().positive().openapi({ description: 'ID说明' }),
+  id: z.number().int('必须是整数').positive('必须是正整数').openapi({ description: 'ID说明' }),
   // 字符串字段
   fieldName: z.string()
-    .max(255)
+    .min(1, '不能为空')
+    .max(255, '最多255个字符')
     .nullable()
     .openapi({
       description: '字段说明',
@@ -119,10 +120,14 @@ export const EntitySchema = z.object({
     }),
   // 数字字段
   numberField: z.number()
+    .int('必须是整数')
+    .positive('必须是正数')
+    .min(1, '最小值为1')
+    .max(9999, '最大值为9999')
     .default(默认值)
     .openapi({...}),
   // 日期字段
-  dateField: z.date().openapi({...})
+  dateField: z.coerce.date('日期格式不正确').openapi({...})
 });
 ```
 
@@ -134,19 +139,19 @@ export const EntitySchema = z.object({
 
 ```typescript
 // 整数类型
-z.coerce.number().int().positive().openapi({
+z.coerce.number().int('必须是整数').positive('必须是正整数').openapi({
   description: '正整数ID',
   example: 1
 });
 
 // 小数类型
-z.coerce.number().multipleOf(0.01).openapi({
+z.coerce.number().multipleOf(0.01, '最多保留两位小数').openapi({
   description: '金额,保留两位小数',
   example: 19.99
 });
 
 // 状态类型(0/1)
-z.coerce.number().int().min(0).max(1).openapi({
+z.coerce.number().int('必须是整数').min(0, '最小值为0').max(1, '最大值为1').openapi({
   description: '状态(0-禁用,1-启用)',
   example: 1
 });
@@ -158,18 +163,18 @@ z.coerce.number().int().min(0).max(1).openapi({
 
 ```typescript
 // 日期时间类型
-z.coerce.date().openapi({
+z.coerce.date('日期格式不正确').openapi({
   description: '创建时间',
   example: '2023-10-01T12:00:00Z'
 });
 
 // 日期范围查询
 const DateRangeSchema = z.object({
-  startDate: z.coerce.date().openapi({
+  startDate: z.coerce.date('开始日期格式不正确').openapi({
     description: '开始日期',
     example: '2023-10-01T00:00:00Z'
   }),
-  endDate: z.coerce.date().openapi({
+  endDate: z.coerce.date('结束日期格式不正确').openapi({
     description: '结束日期',
     example: '2023-10-31T23:59:59Z'
   })
@@ -182,7 +187,7 @@ const DateRangeSchema = z.object({
 
 ```typescript
 // 布尔类型
-z.coerce.boolean().openapi({
+z.coerce.boolean('必须是布尔值').openapi({
   description: '是否启用',
   example: true
 });
@@ -200,29 +205,29 @@ z.coerce.boolean().openapi({
 
 ```typescript
 export const CreateEntityDto = z.object({
-  name: z.string().max(255).openapi({
+  name: z.string().min(1, '名称不能为空').max(255, '名称最多255个字符').openapi({
     description: '名称',
     example: '示例名称'
   }),
-  quantity: z.coerce.number().int().min(1).openapi({
+  quantity: z.coerce.number().int('数量必须是整数').min(1, '数量最小为1').max(9999, '数量最大为9999').openapi({
     description: '数量',
     example: 10
   }),
-  price: z.coerce.number().multipleOf(0.01).openapi({
+  price: z.coerce.number().multipleOf(0.01, '价格最多保留两位小数').min(0.01, '价格必须大于0').max(999999.99, '价格不能超过999999.99').openapi({
     description: '价格',
     example: 99.99
   }),
-  isActive: z.coerce.boolean().default(true).openapi({
+  isActive: z.coerce.boolean('是否激活必须是布尔值').default(true).openapi({
     description: '是否激活',
     example: true
   }),
-  expireDate: z.coerce.date().openapi({
+  expireDate: z.coerce.date('过期日期格式不正确').min(new Date(), '过期日期不能早于当前时间').openapi({
     description: '过期日期',
     example: '2024-12-31T23:59:59Z'
   }),
   // 选填字段示例
   // nullable字段必须显式添加optional()
-  description: z.string().max(500).nullable().optional().openapi({
+  description: z.string().max(500, '描述最多500个字符').nullable().optional().openapi({
     description: '商品描述(选填)',
     example: '这是一个可选的商品描述信息'
   })
@@ -236,23 +241,23 @@ export const CreateEntityDto = z.object({
 
 ```typescript
 export const UpdateEntityDto = z.object({
-  name: z.string().max(255).optional().openapi({
+  name: z.string().min(1, '名称不能为空').max(255, '名称最多255个字符').optional().openapi({
     description: '名称',
     example: '更新后的名称'
   }),
-  quantity: z.coerce.number().int().min(1).optional().openapi({
+  quantity: z.coerce.number().int('数量必须是整数').min(1, '数量最小为1').max(9999, '数量最大为9999').optional().openapi({
     description: '数量',
     example: 20
   }),
-  price: z.coerce.number().multipleOf(0.01).optional().openapi({
+  price: z.coerce.number().multipleOf(0.01, '价格最多保留两位小数').min(0.01, '价格必须大于0').max(999999.99, '价格不能超过999999.99').optional().openapi({
     description: '价格',
     example: 89.99
   }),
-  isActive: z.coerce.boolean().optional().openapi({
+  isActive: z.coerce.boolean('是否激活必须是布尔值').optional().openapi({
     description: '是否激活',
     example: false
   }),
-  expireDate: z.coerce.date().optional().openapi({
+  expireDate: z.coerce.date('过期日期格式不正确').optional().openapi({
     description: '过期日期',
     example: '2025-12-31T23:59:59Z'
   })

+ 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. 参考现有模块实现保持风格一致

+ 59 - 2
.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 生成的路由
 
@@ -100,12 +101,40 @@ export default yourEntityRoutes;
 
 | 方法 | 路径 | 描述 |
 |------|------|------|
-| GET | `/` | 获取实体列表(支持分页、搜索、排序、关联查询) |
+| GET | `/` | 获取实体列表(支持分页、搜索、排序、关联查询、关联字段筛选) |
 | POST | `/` | 创建新实体 |
 | GET | `/{id}` | 获取单个实体详情(支持关联查询) |
 | 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入口文件中注册:
@@ -265,7 +294,35 @@ PUT /api/v1/policy-news/1
 
 ## 4. 高级用法
 
-### 4.1 自定义中间件
+### 4.1 关联字段搜索与筛选
+
+通用CRUD模块支持通过关联关系进行字段搜索和筛选,使用点号表示法指定关联字段路径(格式:`relation.field` 或 `relation.nestedRelation.field`):
+
+```typescript
+// 筛选关联字段示例
+const filters = {
+  'contract.client.name': 'John',           // 精确匹配:client.name = 'John'
+  'contract.client.age': { gte: 18 },       // 范围查询:client.age >= 18
+  'contract.client.department.name': '%IT%' // 模糊匹配:department.name LIKE '%IT%'
+};
+
+// API调用示例
+GET /api/v1/entities?filters={"contract.client.name":"John","contract.client.age":{"gte":18}}
+```
+
+**别名生成规则:**
+- 单级关联:`contract.client` → 别名:`contract_client`
+- 嵌套关联:`contract.client.department` → 别名:`contract_client_department`
+- 字段引用:`contract_client.name`, `contract_client_department.name`
+
+**支持的搜索与筛选操作:**
+- 关键词搜索:支持在 `searchFields` 中配置关联字段进行关键词搜索
+- 精确匹配:`{ "relation.field": "value" }`
+- 模糊匹配:`{ "relation.field": "%value%" }`
+- 范围查询:`{ "relation.field": { "gte": min, "lte": max } }`
+- IN查询:`{ "relation.field": [value1, value2, ...] }`
+
+### 4.2 自定义中间件
 
 可以为CRUD路由添加自定义中间件,如认证和权限控制:
 

+ 1 - 1
server.js

@@ -295,7 +295,7 @@ app.use(async (c) => {
       vite.ssrFixStacktrace(e);
     }
     console.error('请求处理错误:', e.stack);
-    return c.text('服务器内部错误', 500);
+    return c.text('服务器内部错误' + e.stack, 500);
   }
 });
 

+ 0 - 43
src/client/admin-shadcn/components/ErrorPage.tsx

@@ -1,43 +0,0 @@
-import React from 'react';
-import { useRouteError, useNavigate } from 'react-router';
-import { Alert, AlertDescription, AlertTitle } from '@/client/components/ui/alert';
-import { Button } from '@/client/components/ui/button';
-import { AlertCircle } from 'lucide-react';
-
-export const ErrorPage = () => {
-  const navigate = useNavigate();
-  const error = useRouteError() as any;
-  const errorMessage = error?.statusText || error?.message || '未知错误';
-  
-  return (
-    <div className="flex flex-col items-center justify-center flex-grow p-4">
-      <div className="max-w-3xl w-full">
-        <h1 className="text-2xl font-bold mb-4">发生错误</h1>
-        <Alert variant="destructive" className="mb-4">
-          <AlertCircle className="h-4 w-4" />
-          <AlertTitle>{error?.message || '未知错误'}</AlertTitle>
-          <AlertDescription>
-            {error?.stack ? (
-              <pre className="text-xs overflow-auto p-2 bg-muted rounded mt-2">
-                {error.stack}
-              </pre>
-            ) : null}
-          </AlertDescription>
-        </Alert>
-        <div className="flex gap-4">
-          <Button 
-            onClick={() => navigate(0)}
-          >
-            重新加载
-          </Button>
-          <Button 
-            variant="outline"
-            onClick={() => navigate('/admin')}
-          >
-            返回首页
-          </Button>
-        </div>
-      </div>
-    </div>
-  );
-};

+ 0 - 25
src/client/admin-shadcn/components/NotFoundPage.tsx

@@ -1,25 +0,0 @@
-import React from 'react';
-import { useNavigate } from 'react-router';
-import { Button } from '@/client/components/ui/button';
-
-export const NotFoundPage = () => {
-  const navigate = useNavigate();
-  
-  return (
-    <div className="flex flex-col items-center justify-center flex-grow p-4">
-      <div className="max-w-3xl w-full">
-        <h1 className="text-2xl font-bold mb-4">404 - 页面未找到</h1>
-        <p className="mb-6 text-muted-foreground">
-          您访问的页面不存在或已被移除
-        </p>
-        <div className="flex gap-4">
-          <Button 
-            onClick={() => navigate('/admin')}
-          >
-            返回首页
-          </Button>
-        </div>
-      </div>
-    </div>
-  );
-};

+ 0 - 38
src/client/admin-shadcn/components/ProtectedRoute.tsx

@@ -1,38 +0,0 @@
-import React, { useEffect } from 'react';
-import { useNavigate } from 'react-router';
-import { useAuth } from '../hooks/AuthProvider';
-import { Skeleton } from '@/client/components/ui/skeleton';
-
-export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
-  const { isAuthenticated, isLoading } = useAuth();
-  const navigate = useNavigate();
-  
-  useEffect(() => {
-    // 只有在加载完成且未认证时才重定向
-    if (!isLoading && !isAuthenticated) {
-      navigate('/admin/login', { replace: true });
-    }
-  }, [isAuthenticated, isLoading, navigate]);
-  
-  // 显示加载状态,直到认证检查完成
-  if (isLoading) {
-    return (
-      <div className="flex justify-center items-center h-screen">
-        <div className="space-y-2">
-          <Skeleton className="h-12 w-12 rounded-full" />
-          <div className="space-y-2">
-            <Skeleton className="h-4 w-[250px]" />
-            <Skeleton className="h-4 w-[200px]" />
-          </div>
-        </div>
-      </div>
-    );
-  }
-  
-  // 如果未认证且不再加载中,不显示任何内容(等待重定向)
-  if (!isAuthenticated) {
-    return null;
-  }
-  
-  return children;
-};

+ 0 - 140
src/client/admin-shadcn/hooks/AuthProvider.tsx

@@ -1,140 +0,0 @@
-import React, { useState, useEffect, createContext, useContext } from 'react';
-
-import {
-  useQuery,
-  useQueryClient,
-} from '@tanstack/react-query';
-import axios from 'axios';
-import 'dayjs/locale/zh-cn';
-import type {
-  AuthContextType
-} from '@/share/types';
-import { authClient } from '@/client/api';
-import type { InferResponseType, InferRequestType } from 'hono/client';
-
-type User = InferResponseType<typeof authClient.me.$get, 200>;
-
-
-// 创建认证上下文
-const AuthContext = createContext<AuthContextType<User> | null>(null);
-
-// 认证提供器组件
-export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
-  const [user, setUser] = useState<User | null>(null);
-  const [token, setToken] = useState<string | null>(localStorage.getItem('token'));
-  const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
-  const queryClient = useQueryClient();
-
-  // 声明handleLogout函数
-  const handleLogout = async () => {
-    try {
-      // 如果已登录,调用登出API
-      if (token) {
-        await authClient.logout.$post();
-      }
-    } catch (error) {
-      console.error('登出请求失败:', error);
-    } finally {
-      // 清除本地状态
-      setToken(null);
-      setUser(null);
-      setIsAuthenticated(false);
-      localStorage.removeItem('token');
-      // 清除Authorization头
-      delete axios.defaults.headers.common['Authorization'];
-      console.log('登出时已删除全局Authorization头');
-      // 清除所有查询缓存
-      queryClient.clear();
-    }
-  };
-
-  // 使用useQuery检查登录状态
-  const { isLoading } = useQuery({
-    queryKey: ['auth', 'status', token],
-    queryFn: async () => {
-      if (!token) {
-        setIsAuthenticated(false);
-        setUser(null);
-        return null;
-      }
-
-      try {
-        // 设置全局默认请求头
-        axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
-        // 使用API验证当前用户
-        const res = await authClient.me.$get();
-        if (res.status !== 200) {
-          const result = await res.json();
-          throw new Error(result.message)
-        }
-        const currentUser = await res.json();
-        setUser(currentUser);
-        setIsAuthenticated(true);
-        return { isValid: true, user: currentUser };
-      } catch (error) {
-        return { isValid: false };
-      }
-    },
-    enabled: !!token,
-    refetchOnWindowFocus: false,
-    retry: false
-  });
-
-  const handleLogin = async (username: string, password: string, latitude?: number, longitude?: number): Promise<void> => {
-    try {
-      // 使用AuthAPI登录
-      const response = await authClient.login.$post({
-        json: {
-          username,
-          password
-        }
-      })
-      if (response.status !== 200) {
-        const result = await response.json()
-        throw new Error(result.message);
-      }
-
-      const result = await response.json()
-
-      // 保存token和用户信息
-      const { token: newToken, user: newUser } = result;
-
-      // 设置全局默认请求头
-      axios.defaults.headers.common['Authorization'] = `Bearer ${newToken}`;
-
-      // 保存状态
-      setToken(newToken);
-      setUser(newUser);
-      setIsAuthenticated(true);
-      localStorage.setItem('token', newToken);
-
-    } catch (error) {
-      console.error('登录失败:', error);
-      throw error;
-    }
-  };
-
-  return (
-    <AuthContext.Provider
-      value={{
-        user,
-        token,
-        login: handleLogin,
-        logout: handleLogout,
-        isAuthenticated,
-        isLoading
-      }}
-    >
-      {children}
-    </AuthContext.Provider>
-  );
-};
-
-// 使用上下文的钩子
-export const useAuth = () => {
-  const context = useContext(AuthContext);
-  if (!context) {
-    throw new Error('useAuth必须在AuthProvider内部使用');
-  }
-  return context;
-};

+ 0 - 53
src/client/admin-shadcn/index.tsx

@@ -1,53 +0,0 @@
-import { createRoot } from 'react-dom/client'
-import { RouterProvider } from 'react-router';
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { Toaster } from '@/client/components/ui/sonner';
-import dayjs from 'dayjs';
-import weekday from 'dayjs/plugin/weekday';
-import localeData from 'dayjs/plugin/localeData';
-import 'dayjs/locale/zh-cn';
-
-import { AuthProvider } from './hooks/AuthProvider';
-import { router } from './routes';
-
-// 配置 dayjs 插件
-dayjs.extend(weekday);
-dayjs.extend(localeData);
-
-// 设置 dayjs 语言
-dayjs.locale('zh-cn');
-
-// 创建QueryClient实例
-const queryClient = new QueryClient({
-  defaultOptions: {
-    queries: {
-      retry: 1,
-      refetchOnWindowFocus: false,
-    },
-  },
-});
-
-// 应用入口组件
-const App = () => {
-  return (
-    <QueryClientProvider client={queryClient}>
-      <AuthProvider>
-        <RouterProvider router={router} />
-        <Toaster 
-          position="top-right"
-          expand={false}
-          richColors
-          closeButton
-        />
-      </AuthProvider>
-    </QueryClientProvider>
-  )
-};
-
-const rootElement = document.getElementById('root')
-if (rootElement) {
-  const root = createRoot(rootElement)
-  root.render(
-    <App />
-  )
-}

+ 0 - 237
src/client/admin-shadcn/pages/Dashboard.tsx

@@ -1,237 +0,0 @@
-import React from 'react';
-import { Users, Bell, Eye, TrendingUp, TrendingDown, Activity } from 'lucide-react';
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
-import { Badge } from '@/client/components/ui/badge';
-import { Progress } from '@/client/components/ui/progress';
-
-// 仪表盘页面
-export const DashboardPage = () => {
-  const stats = [
-    {
-      title: '活跃用户',
-      value: '112,893',
-      icon: Users,
-      color: 'text-blue-500',
-      bgColor: 'bg-blue-50',
-      trend: 12.5,
-      trendDirection: 'up',
-      description: '较昨日增长 12.5%',
-    },
-    {
-      title: '系统消息',
-      value: '93',
-      icon: Bell,
-      color: 'text-yellow-500',
-      bgColor: 'bg-yellow-50',
-      trend: 0,
-      trendDirection: 'neutral',
-      description: '其中 5 条未读',
-    },
-    {
-      title: '在线用户',
-      value: '1,128',
-      icon: Eye,
-      color: 'text-purple-500',
-      bgColor: 'bg-purple-50',
-      trend: 32.1,
-      trendDirection: 'up',
-      description: '当前在线率 32.1%',
-    },
-  ];
-
-  const recentActivities = [
-    {
-      id: 1,
-      user: '张三',
-      action: '登录系统',
-      time: '2分钟前',
-      status: 'success',
-    },
-    {
-      id: 2,
-      user: '李四',
-      action: '创建了新用户',
-      time: '5分钟前',
-      status: 'info',
-    },
-    {
-      id: 3,
-      user: '王五',
-      action: '删除了用户',
-      time: '10分钟前',
-      status: 'warning',
-    },
-    {
-      id: 4,
-      user: '赵六',
-      action: '修改了配置',
-      time: '15分钟前',
-      status: 'info',
-    },
-  ];
-
-  const systemMetrics = [
-    {
-      name: 'CPU使用率',
-      value: 65,
-      color: 'bg-green-500',
-    },
-    {
-      name: '内存使用率',
-      value: 78,
-      color: 'bg-blue-500',
-    },
-    {
-      name: '磁盘使用率',
-      value: 45,
-      color: 'bg-purple-500',
-    },
-    {
-      name: '网络使用率',
-      value: 32,
-      color: 'bg-orange-500',
-    },
-  ];
-
-  return (
-    <div className="space-y-6">
-      <div>
-        <h1 className="text-3xl font-bold tracking-tight">仪表盘</h1>
-        <p className="text-muted-foreground">
-          欢迎回来!这里是系统概览和关键指标。
-        </p>
-      </div>
-
-      {/* 统计卡片 */}
-      <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
-        {stats.map((stat, index) => {
-          const Icon = stat.icon;
-          return (
-            <Card key={index} className="transition-all duration-300 hover:shadow-lg">
-              <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
-                <CardTitle className="text-sm font-medium">
-                  {stat.title}
-                </CardTitle>
-                <div className={`p-2 rounded-full ${stat.bgColor}`}>
-                  <Icon className={`h-4 w-4 ${stat.color}`} />
-                </div>
-              </CardHeader>
-              <CardContent>
-                <div className="text-2xl font-bold">{stat.value}</div>
-                <div className="flex items-center space-x-2">
-                  {stat.trendDirection === 'up' && (
-                    <TrendingUp className="h-4 w-4 text-green-500" />
-                  )}
-                  {stat.trendDirection === 'down' && (
-                    <TrendingDown className="h-4 w-4 text-red-500" />
-                  )}
-                  <p className="text-xs text-muted-foreground">{stat.description}</p>
-                </div>
-              </CardContent>
-            </Card>
-          );
-        })}
-      </div>
-
-      <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
-        {/* 系统性能 */}
-        <Card className="lg:col-span-4">
-          <CardHeader>
-            <CardTitle>系统性能</CardTitle>
-            <CardDescription>
-              当前系统各项资源的使用情况
-            </CardDescription>
-          </CardHeader>
-          <CardContent>
-            <div className="space-y-4">
-              {systemMetrics.map((metric, index) => (
-                <div key={index} className="space-y-2">
-                  <div className="flex justify-between text-sm">
-                    <span className="font-medium">{metric.name}</span>
-                    <span className="text-muted-foreground">{metric.value}%</span>
-                  </div>
-                  <Progress value={metric.value} className="h-2" />
-                </div>
-              ))}
-            </div>
-          </CardContent>
-        </Card>
-
-        {/* 最近活动 */}
-        <Card className="lg:col-span-3">
-          <CardHeader>
-            <CardTitle>最近活动</CardTitle>
-            <CardDescription>
-              系统最新操作记录
-            </CardDescription>
-          </CardHeader>
-          <CardContent>
-            <div className="space-y-4">
-              {recentActivities.map((activity) => (
-                <div key={activity.id} className="flex items-center space-x-4">
-                  <div className="flex-shrink-0">
-                    <div className={`p-2 rounded-full ${
-                      activity.status === 'success' ? 'bg-green-100' :
-                      activity.status === 'warning' ? 'bg-yellow-100' : 'bg-blue-100'
-                    }`}>
-                      <Activity className={`h-4 w-4 ${
-                        activity.status === 'success' ? 'text-green-600' :
-                        activity.status === 'warning' ? 'text-yellow-600' : 'text-blue-600'
-                      }`} />
-                    </div>
-                  </div>
-                  <div className="flex-1 space-y-1">
-                    <p className="text-sm font-medium">
-                      {activity.user} {activity.action}
-                    </p>
-                    <p className="text-sm text-muted-foreground">
-                      {activity.time}
-                    </p>
-                  </div>
-                </div>
-              ))}
-            </div>
-          </CardContent>
-        </Card>
-      </div>
-
-      {/* 快捷操作 */}
-      <Card>
-        <CardHeader>
-          <CardTitle>快捷操作</CardTitle>
-          <CardDescription>
-            常用的管理功能
-          </CardDescription>
-        </CardHeader>
-        <CardContent>
-          <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
-            <Card className="hover:shadow-md transition-all cursor-pointer">
-              <CardHeader className="pb-3">
-                <CardTitle className="text-base">用户管理</CardTitle>
-                <CardDescription>查看和管理所有用户</CardDescription>
-              </CardHeader>
-            </Card>
-            <Card className="hover:shadow-md transition-all cursor-pointer">
-              <CardHeader className="pb-3">
-                <CardTitle className="text-base">系统设置</CardTitle>
-                <CardDescription>配置系统参数</CardDescription>
-              </CardHeader>
-            </Card>
-            <Card className="hover:shadow-md transition-all cursor-pointer">
-              <CardHeader className="pb-3">
-                <CardTitle className="text-base">数据备份</CardTitle>
-                <CardDescription>执行数据备份操作</CardDescription>
-              </CardHeader>
-            </Card>
-            <Card className="hover:shadow-md transition-all cursor-pointer">
-              <CardHeader className="pb-3">
-                <CardTitle className="text-base">日志查看</CardTitle>
-                <CardDescription>查看系统日志</CardDescription>
-              </CardHeader>
-            </Card>
-          </div>
-        </CardContent>
-      </Card>
-    </div>
-  );
-};

+ 0 - 178
src/client/admin-shadcn/pages/Login.tsx

@@ -1,178 +0,0 @@
-import { useState } from 'react';
-import { useNavigate } from 'react-router';
-import { useAuth } from '../hooks/AuthProvider';
-import { useForm } from 'react-hook-form';
-import { zodResolver } from '@hookform/resolvers/zod';
-import { z } from 'zod';
-import { toast } from 'sonner';
-import { Eye, EyeOff, User, Lock } from 'lucide-react';
-import { Button } from '@/client/components/ui/button';
-import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/client/components/ui/card';
-import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
-import { Input } from '@/client/components/ui/input';
-
-// 表单验证Schema
-const loginSchema = z.object({
-  username: z.string().min(1, '请输入用户名'),
-  password: z.string().min(1, '请输入密码'),
-});
-
-type LoginFormData = z.infer<typeof loginSchema>;
-
-// 登录页面
-export const LoginPage = () => {
-  const { login } = useAuth();
-  const [isLoading, setIsLoading] = useState(false);
-  const [showPassword, setShowPassword] = useState(false);
-  const navigate = useNavigate();
-  
-  const form = useForm<LoginFormData>({
-    resolver: zodResolver(loginSchema),
-    defaultValues: {
-      username: '',
-      password: '',
-    },
-  });
-
-  const handleSubmit = async (data: LoginFormData) => {
-    try {
-      setIsLoading(true);
-      
-      // 获取地理位置
-      let latitude: number | undefined;
-      let longitude: number | undefined;
-      
-      try {
-        if (navigator.geolocation) {
-          const position = await new Promise<GeolocationPosition>((resolve, reject) => {
-            navigator.geolocation.getCurrentPosition(resolve, reject);
-          });
-          latitude = position.coords.latitude;
-          longitude = position.coords.longitude;
-        }
-      } catch (geoError) {
-        console.warn('获取地理位置失败:', geoError);
-      }
-      
-      await login(data.username, data.password, latitude, longitude);
-      // 登录成功后跳转到管理后台首页
-      navigate('/admin/dashboard');
-      toast.success('登录成功!欢迎回来');
-    } catch (error: any) {
-      toast.error(error instanceof Error ? error.message : '登录失败');
-    } finally {
-      setIsLoading(false);
-    }
-  };
-  
-  return (
-    <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 py-12 px-4 sm:px-6 lg:px-8">
-      <div className="max-w-md w-full space-y-8">
-        <div className="text-center">
-          <div className="mx-auto w-16 h-16 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-full flex items-center justify-center shadow-lg">
-            <User className="h-8 w-8 text-white" />
-          </div>
-          <h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
-            管理后台登录
-          </h2>
-          <p className="mt-2 text-center text-sm text-gray-600">
-            请输入您的账号和密码继续操作
-          </p>
-        </div>
-        
-        <Card className="shadow-xl border-0">
-          <CardHeader className="text-center">
-            <CardTitle className="text-2xl">欢迎登录</CardTitle>
-            <CardDescription>
-              使用您的账户信息登录系统
-            </CardDescription>
-          </CardHeader>
-          <CardContent>
-            <Form {...form}>
-              <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
-                <FormField
-                  control={form.control}
-                  name="username"
-                  render={({ field }) => (
-                    <FormItem>
-                      <FormLabel>用户名</FormLabel>
-                      <FormControl>
-                        <div className="relative">
-                          <User className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
-                          <Input
-                            placeholder="请输入用户名"
-                            className="pl-10"
-                            {...field}
-                          />
-                        </div>
-                      </FormControl>
-                      <FormMessage />
-                    </FormItem>
-                  )}
-                />
-                
-                <FormField
-                  control={form.control}
-                  name="password"
-                  render={({ field }) => (
-                    <FormItem>
-                      <FormLabel>密码</FormLabel>
-                      <FormControl>
-                        <div className="relative">
-                          <Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
-                          <Input
-                            type={showPassword ? 'text' : 'password'}
-                            placeholder="请输入密码"
-                            className="pl-10 pr-10"
-                            {...field}
-                          />
-                          <button
-                            type="button"
-                            className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
-                            onClick={() => setShowPassword(!showPassword)}
-                          >
-                            {showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
-                          </button>
-                        </div>
-                      </FormControl>
-                      <FormMessage />
-                    </FormItem>
-                  )}
-                />
-                
-                <Button
-                  type="submit"
-                  className="w-full"
-                  disabled={isLoading}
-                >
-                  {isLoading ? (
-                    <>
-                      <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
-                      登录中...
-                    </>
-                  ) : (
-                    '登录'
-                  )}
-                </Button>
-              </form>
-            </Form>
-          </CardContent>
-          <CardFooter className="flex flex-col items-center space-y-2">
-            <div className="text-sm text-gray-500">
-              测试账号: <span className="font-medium text-gray-700">admin</span> / <span className="font-medium text-gray-700">admin123</span>
-            </div>
-            <div className="text-xs text-gray-400">
-              © {new Date().getFullYear()} 管理系统. 保留所有权利.
-            </div>
-          </CardFooter>
-        </Card>
-        
-        <div className="text-center">
-          <p className="text-sm text-gray-500">
-            遇到问题?<a href="#" className="font-medium text-indigo-600 hover:text-indigo-500">联系管理员</a>
-          </p>
-        </div>
-      </div>
-    </div>
-  );
-};

+ 0 - 0
src/client/admin-shadcn/components/AvatarSelector.tsx → src/client/admin/components/AvatarSelector.tsx


+ 0 - 0
src/client/admin-shadcn/components/DataTablePagination.tsx → src/client/admin/components/DataTablePagination.tsx


+ 14 - 14
src/client/admin/components/ErrorPage.tsx

@@ -1,6 +1,8 @@
 import React from 'react';
 import { useRouteError, useNavigate } from 'react-router';
-import { Alert, Button } from 'antd';
+import { Alert, AlertDescription, AlertTitle } from '@/client/components/ui/alert';
+import { Button } from '@/client/components/ui/button';
+import { AlertCircle } from 'lucide-react';
 
 export const ErrorPage = () => {
   const navigate = useNavigate();
@@ -8,30 +10,28 @@ export const ErrorPage = () => {
   const errorMessage = error?.statusText || error?.message || '未知错误';
   
   return (
-    <div className="flex flex-col items-center justify-center flex-grow p-4"
-    >
+    <div className="flex flex-col items-center justify-center flex-grow p-4">
       <div className="max-w-3xl w-full">
         <h1 className="text-2xl font-bold mb-4">发生错误</h1>
-        <Alert 
-          type="error"
-          message={error?.message || '未知错误'}
-          description={
-            error?.stack ? (
-              <pre className="text-xs overflow-auto p-2 bg-gray-100 dark:bg-gray-800 rounded">
+        <Alert variant="destructive" className="mb-4">
+          <AlertCircle className="h-4 w-4" />
+          <AlertTitle>{error?.message || '未知错误'}</AlertTitle>
+          <AlertDescription>
+            {error?.stack ? (
+              <pre className="text-xs overflow-auto p-2 bg-muted rounded mt-2">
                 {error.stack}
               </pre>
-            ) : null
-          }
-          className="mb-4"
-        />
+            ) : null}
+          </AlertDescription>
+        </Alert>
         <div className="flex gap-4">
           <Button 
-            type="primary" 
             onClick={() => navigate(0)}
           >
             重新加载
           </Button>
           <Button 
+            variant="outline"
             onClick={() => navigate('/admin')}
           >
             返回首页

+ 2 - 3
src/client/admin/components/NotFoundPage.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 import { useNavigate } from 'react-router';
-import { Button } from 'antd';
+import { Button } from '@/client/components/ui/button';
 
 export const NotFoundPage = () => {
   const navigate = useNavigate();
@@ -9,12 +9,11 @@ export const NotFoundPage = () => {
     <div className="flex flex-col items-center justify-center flex-grow p-4">
       <div className="max-w-3xl w-full">
         <h1 className="text-2xl font-bold mb-4">404 - 页面未找到</h1>
-        <p className="mb-6 text-gray-600 dark:text-gray-300">
+        <p className="mb-6 text-muted-foreground">
           您访问的页面不存在或已被移除
         </p>
         <div className="flex gap-4">
           <Button 
-            type="primary" 
             onClick={() => navigate('/admin')}
           >
             返回首页

+ 9 - 8
src/client/admin/components/ProtectedRoute.tsx

@@ -1,12 +1,7 @@
 import React, { useEffect } from 'react';
-import { 
-  useNavigate,
-} from 'react-router';
+import { useNavigate } from 'react-router';
 import { useAuth } from '../hooks/AuthProvider';
-
-
-
-
+import { Skeleton } from '@/client/components/ui/skeleton';
 
 export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
   const { isAuthenticated, isLoading } = useAuth();
@@ -23,7 +18,13 @@ export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
   if (isLoading) {
     return (
       <div className="flex justify-center items-center h-screen">
-        <div className="loader ease-linear rounded-full border-4 border-t-4 border-gray-200 h-12 w-12"></div>
+        <div className="space-y-2">
+          <Skeleton className="h-12 w-12 rounded-full" />
+          <div className="space-y-2">
+            <Skeleton className="h-4 w-[250px]" />
+            <Skeleton className="h-4 w-[200px]" />
+          </div>
+        </div>
       </div>
     );
   }

+ 18 - 25
src/client/admin/index.tsx

@@ -1,12 +1,11 @@
 import { createRoot } from 'react-dom/client'
 import { RouterProvider } from 'react-router';
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { App as AntdApp , ConfigProvider} from 'antd'
+import { Toaster } from '@/client/components/ui/sonner';
 import dayjs from 'dayjs';
 import weekday from 'dayjs/plugin/weekday';
 import localeData from 'dayjs/plugin/localeData';
 import 'dayjs/locale/zh-cn';
-import zhCN from 'antd/locale/zh_CN';
 
 import { AuthProvider } from './hooks/AuthProvider';
 import { router } from './routes';
@@ -19,34 +18,28 @@ dayjs.extend(localeData);
 dayjs.locale('zh-cn');
 
 // 创建QueryClient实例
-const queryClient = new QueryClient();
+const queryClient = new QueryClient({
+  defaultOptions: {
+    queries: {
+      retry: 1,
+      refetchOnWindowFocus: false,
+    },
+  },
+});
 
 // 应用入口组件
 const App = () => {
   return (
     <QueryClientProvider client={queryClient}>
-      <ConfigProvider locale={zhCN} theme={{
-        token: {
-          colorPrimary: '#1890ff',
-          borderRadius: 4,
-          colorBgContainer: '#f5f5f5',
-        },
-        components: {
-          Button: {
-            borderRadius: 4,
-          },
-          Card: {
-            borderRadius: 6,
-            boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
-          }
-        }
-      }}>
-        <AntdApp>
-          <AuthProvider>
-            <RouterProvider router={router} />
-          </AuthProvider>
-        </AntdApp>
-      </ConfigProvider>
+      <AuthProvider>
+        <RouterProvider router={router} />
+        <Toaster 
+          position="top-right"
+          expand={false}
+          richColors
+          closeButton
+        />
+      </AuthProvider>
     </QueryClientProvider>
   )
 };

+ 185 - 157
src/client/admin/layouts/MainLayout.tsx

@@ -1,24 +1,25 @@
-import React, { useState, useEffect, useMemo } from 'react';
+import { useState, useEffect, useMemo } from 'react';
 import {
   Outlet,
   useLocation,
 } from 'react-router';
 import {
-  Layout, Button, Space, Badge, Avatar, Dropdown, Typography, Input, Menu,
-} from 'antd';
-import {
-  MenuFoldOutlined,
-  MenuUnfoldOutlined,
-  BellOutlined,
-  VerticalAlignTopOutlined,
-  UserOutlined
-} from '@ant-design/icons';
+  Bell,
+  Menu,
+  User,
+  ChevronDown
+} from 'lucide-react';
 import { useAuth } from '../hooks/AuthProvider';
-import { useMenu, useMenuSearch, type MenuItem } from '../menu';
+import { useMenu, type MenuItem } from '../menu';
 import { getGlobalConfig } from '@/client/utils/utils';
-
-const { Header, Sider, Content } = Layout;
-
+import { Button } from '@/client/components/ui/button';
+import { Input } from '@/client/components/ui/input';
+import { Avatar, AvatarFallback, AvatarImage } from '@/client/components/ui/avatar';
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/client/components/ui/dropdown-menu';
+import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/client/components/ui/sheet';
+import { ScrollArea } from '@/client/components/ui/scroll-area';
+import { cn } from '@/client/lib/utils';
+import { Badge } from '@/client/components/ui/badge';
 /**
  * 主布局组件
  * 包含侧边栏、顶部导航和内容区域
@@ -27,46 +28,17 @@ export const MainLayout = () => {
   const { user } = useAuth();
   const [showBackTop, setShowBackTop] = useState(false);
   const location = useLocation();
+  const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
   
   // 使用菜单hook
   const {
     menuItems,
     userMenuItems,
-    openKeys,
     collapsed,
     setCollapsed,
-    handleMenuClick: handleRawMenuClick,
-    onOpenChange
+    handleMenuClick
   } = useMenu();
   
-  // 处理菜单点击
-  const handleMenuClick = (key: string) => {
-    const item = findMenuItem(menuItems, key);
-    if (item && 'label' in item) {
-      handleRawMenuClick(item);
-    }
-  };
-  
-  // 查找菜单项
-  const findMenuItem = (items: MenuItem[], key: string): MenuItem | null => {
-    for (const item of items) {
-      if (!item) continue;
-      if (item.key === key) return item;
-      if (item.children) {
-        const found = findMenuItem(item.children, key);
-        if (found) return found;
-      }
-    }
-    return null;
-  };
-  
-  // 使用菜单搜索hook
-  const {
-    searchText,
-    setSearchText,
-    filteredMenuItems
-  } = useMenuSearch(menuItems);
-  
   // 获取当前选中的菜单项
   const selectedKey = useMemo(() => {
     const findSelectedKey = (items: MenuItem[]): string | null => {
@@ -102,127 +74,183 @@ export const MainLayout = () => {
     });
   };
 
-  
   // 应用名称 - 从CONFIG中获取或使用默认值
   const appName = getGlobalConfig('APP_NAME') || '应用Starter';
   
-  return (
-    <Layout style={{ minHeight: '100vh' }}>
-      <Sider
-        trigger={null}
-        collapsible
-        collapsed={collapsed}
-        width={240}
-        className="custom-sider"
-        theme='light'
-        style={{
-          overflow: 'auto',
-          height: '100vh',
-          position: 'fixed',
-          left: 0,
-          top: 0,
-          bottom: 0,
-          zIndex: 100,
-          transition: 'all 0.2s ease',
-          boxShadow: '2px 0 8px 0 rgba(29, 35, 41, 0.05)',
-          background: 'linear-gradient(180deg, #001529 0%, #003a6c 100%)',
-        }}
-      >
-        <div className="p-4">
-          <Typography.Title level={2} className="text-xl font-bold truncate">
-            <span className="text-white">{collapsed ? '应用' : appName}</span>
-          </Typography.Title>
-          
-          {/* 菜单搜索框 */}
-          {!collapsed && (
-            <div className="mb-4">
-              <Input.Search
-                placeholder="搜索菜单..."
-                allowClear
-                value={searchText}
-                onChange={(e) => setSearchText(e.target.value)}
-              />
-            </div>
-          )}
-        </div>
-        
-        {/* 菜单列表 */}
-        <Menu
-          theme='dark'
-          mode="inline"
-          items={filteredMenuItems}
-          openKeys={openKeys}
-          selectedKeys={[selectedKey]}
-          onOpenChange={onOpenChange}
-          onClick={({ key }) => handleMenuClick(key)}
-          inlineCollapsed={collapsed}
-          style={{
-            backgroundColor: 'transparent',
-            borderRight: 'none'
-          }}
-        />
-      </Sider>
+
+  // 侧边栏内容
+  const SidebarContent = () => (
+    <div className="flex h-full flex-col">
+      <div className="p-4 border-b">
+        <h2 className="text-lg font-semibold truncate">
+          {collapsed ? '应用' : appName}
+        </h2>
+        {!collapsed && (
+          <div className="mt-4">
+            <Input
+              placeholder="搜索菜单..."
+              className="h-8"
+            />
+          </div>
+        )}
+      </div>
       
-      <Layout style={{ marginLeft: collapsed ? 80 : 240, transition: 'margin-left 0.2s' }}>
-        <div className="sticky top-0 z-50 bg-white shadow-sm transition-all duration-200 h-16 flex items-center justify-between pl-2"
-          style={{
-            boxShadow: '0 1px 8px rgba(0,21,41,0.12)',
-            borderBottom: '1px solid #f0f0f0'
-          }}
-        >
-          <Button
-            type="text"
-            icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
-            onClick={() => setCollapsed(!collapsed)}
-            className="w-16 h-16"
-          />
-          
-          <Space size="middle" className="mr-4">
-            <Badge count={5} offset={[0, 5]}>
-              <Button 
-                type="text" 
-                icon={<BellOutlined />}
-              />
-            </Badge>
-            
-            <Dropdown menu={{ items: userMenuItems }}>
-              <Space className="cursor-pointer">
-                <Avatar 
-                  src={user?.avatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?q=80&w=40&auto=format&fit=crop'}
-                  icon={!user?.avatar && !navigator.onLine && <UserOutlined />}
-                />
-                <span>
-                  {user?.nickname || user?.username}
-                </span>
-              </Space>
-            </Dropdown>
-          </Space>
-        </div>
-        
-        <Content className="m-6" style={{ overflow: 'initial', transition: 'all 0.2s ease' }}>
-          <div className="site-layout-content p-6 rounded-lg bg-white shadow-sm transition-all duration-300 hover:shadow-md">
+      <ScrollArea className="flex-1">
+        <nav className="p-2">
+          {menuItems.map((item) => (
+            <div key={item.key}>
+              <Button
+                variant={selectedKey === item.key ? "default" : "ghost"}
+                className={cn(
+                  "w-full justify-start mb-1",
+                  selectedKey === item.key && "bg-primary text-primary-foreground"
+                )}
+                onClick={() => {
+                  handleMenuClick(item);
+                  setIsMobileMenuOpen(false);
+                }}
+              >
+                {item.icon}
+                {!collapsed && <span className="ml-2">{item.label}</span>}
+              </Button>
+              
+              {item.children && !collapsed && (
+                <div className="ml-4">
+                  {item.children.map((child) => (
+                    <Button
+                      key={child.key}
+                      variant={selectedKey === child.key ? "default" : "ghost"}
+                      className={cn(
+                        "w-full justify-start mb-1 text-sm",
+                        selectedKey === child.key && "bg-primary text-primary-foreground"
+                      )}
+                      onClick={() => {
+                        handleMenuClick(child);
+                        setIsMobileMenuOpen(false);
+                      }}
+                    >
+                      {child.icon && <span className="ml-2">{child.icon}</span>}
+                      <span className={child.icon ? "ml-2" : "ml-6"}>{child.label}</span>
+                    </Button>
+                  ))}
+                </div>
+              )}
+            </div>
+          ))}
+        </nav>
+      </ScrollArea>
+    </div>
+  );
+
+  return (
+    <div className="flex h-screen bg-background">
+      {/* Desktop Sidebar */}
+      <aside className={cn(
+        "hidden md:block border-r bg-background transition-all duration-200",
+        collapsed ? "w-16" : "w-64"
+      )}>
+        <SidebarContent />
+      </aside>
+
+      {/* Mobile Sidebar */}
+      <Sheet open={isMobileMenuOpen} onOpenChange={setIsMobileMenuOpen}>
+        <SheetContent side="left" className="w-64 p-0">
+          <SheetHeader className="p-4">
+            <SheetTitle>{appName}</SheetTitle>
+          </SheetHeader>
+          <SidebarContent />
+        </SheetContent>
+      </Sheet>
+
+      <div className="flex-1 flex flex-col overflow-hidden">
+        {/* Header */}
+        <header className="flex h-16 items-center justify-between border-b bg-background px-4">
+          <div className="flex items-center gap-2">
+            <Button
+              variant="ghost"
+              size="icon"
+              className="md:hidden"
+              onClick={() => setIsMobileMenuOpen(true)}
+            >
+              <Menu className="h-4 w-4" />
+            </Button>
+            <Button
+              variant="ghost"
+              size="icon"
+              className="hidden md:block"
+              onClick={() => setCollapsed(!collapsed)}
+            >
+              <Menu className="h-4 w-4" />
+            </Button>
+          </div>
+
+          <div className="flex items-center gap-4">
+            <Button variant="ghost" size="icon" className="relative">
+              <Bell className="h-4 w-4" />
+              <Badge className="absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center text-xs">
+                5
+              </Badge>
+            </Button>
+
+            <DropdownMenu>
+              <DropdownMenuTrigger asChild>
+                <Button variant="ghost" className="relative h-8 w-8 rounded-full">
+                  <Avatar className="h-8 w-8">
+                    <AvatarImage
+                      src={user?.avatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?q=80&w=40&auto=format&fit=crop'}
+                      alt={user?.username || 'User'}
+                    />
+                    <AvatarFallback>
+                      <User className="h-4 w-4" />
+                    </AvatarFallback>
+                  </Avatar>
+                </Button>
+              </DropdownMenuTrigger>
+              <DropdownMenuContent className="w-56" align="end" forceMount>
+                <DropdownMenuLabel className="font-normal">
+                  <div className="flex flex-col space-y-1">
+                    <p className="text-sm font-medium leading-none">
+                      {user?.nickname || user?.username}
+                    </p>
+                    <p className="text-xs leading-none text-muted-foreground">
+                      {user?.email}
+                    </p>
+                  </div>
+                </DropdownMenuLabel>
+                <DropdownMenuSeparator />
+                {userMenuItems.map((item) => (
+                  item.type === 'separator' ? (
+                    <DropdownMenuSeparator key={item.key} />
+                  ) : (
+                    <DropdownMenuItem key={item.key} onClick={item.onClick}>
+                      {item.icon && item.icon}
+                      <span>{item.label}</span>
+                    </DropdownMenuItem>
+                  )
+                ))}
+              </DropdownMenuContent>
+            </DropdownMenu>
+          </div>
+        </header>
+
+        {/* Main Content */}
+        <main className="flex-1 overflow-auto p-4">
+          <div className="max-w-7xl mx-auto">
             <Outlet />
           </div>
           
-          {/* 回到顶部按钮 */}
+          {/* Back to top button */}
           {showBackTop && (
             <Button
-              type="primary"
-              shape="circle"
-              icon={<VerticalAlignTopOutlined />}
-              size="large"
+              size="icon"
+              className="fixed bottom-4 right-4 rounded-full shadow-lg"
               onClick={scrollToTop}
-              style={{
-                position: 'fixed',
-                right: 30,
-                bottom: 30,
-                zIndex: 1000,
-                boxShadow: '0 3px 6px rgba(0,0,0,0.16)',
-              }}
-            />
+            >
+              <ChevronDown className="h-4 w-4 rotate-180" />
+            </Button>
           )}
-        </Content>
-      </Layout>
-    </Layout>
+        </main>
+      </div>
+    </div>
   );
-};
+};

+ 43 - 23
src/client/admin/menu.tsx

@@ -1,14 +1,15 @@
 import React from 'react';
 import { useNavigate } from 'react-router';
 import { useAuth } from './hooks/AuthProvider';
-import type { MenuProps } from 'antd';
 import {
-  UserOutlined,
-  DashboardOutlined,
-  TeamOutlined,
-  InfoCircleOutlined,
-  FileTextOutlined,
-} from '@ant-design/icons';
+  Users,
+  Settings,
+  User,
+  LogOut,
+  BarChart3,
+  LayoutDashboard,
+  File
+} from 'lucide-react';
 
 export interface MenuItem {
   key: string;
@@ -17,6 +18,7 @@ export interface MenuItem {
   children?: MenuItem[];
   path?: string;
   permission?: string;
+  onClick?: () => void;
 }
 
 /**
@@ -69,44 +71,67 @@ export const useMenu = () => {
   const navigate = useNavigate();
   const { logout: handleLogout } = useAuth();
   const [collapsed, setCollapsed] = React.useState(false);
-  const [openKeys, setOpenKeys] = React.useState<string[]>([]);
 
   // 基础菜单项配置
   const menuItems: MenuItem[] = [
     {
       key: 'dashboard',
       label: '控制台',
-      icon: <DashboardOutlined />,
+      icon: <LayoutDashboard className="h-4 w-4" />,
       path: '/admin/dashboard'
     },
     {
       key: 'users',
       label: '用户管理',
-      icon: <TeamOutlined />,
+      icon: <Users className="h-4 w-4" />,
       path: '/admin/users',
       permission: 'user:manage'
     },
     {
       key: 'files',
       label: '文件管理',
-      icon: <FileTextOutlined />,
+      icon: <File className="h-4 w-4" />,
       path: '/admin/files',
       permission: 'file:manage'
     },
+    {
+      key: 'analytics',
+      label: '数据分析',
+      icon: <BarChart3 className="h-4 w-4" />,
+      path: '/admin/analytics',
+      permission: 'analytics:view'
+    },
+    {
+      key: 'settings',
+      label: '系统设置',
+      icon: <Settings className="h-4 w-4" />,
+      path: '/admin/settings',
+      permission: 'settings:manage'
+    },
   ];
 
   // 用户菜单项
-  const userMenuItems: MenuProps['items'] = [
+  const userMenuItems = [
     {
       key: 'profile',
       label: '个人资料',
-      icon: <UserOutlined />
+      icon: <User className="mr-2 h-4 w-4" />,
+      onClick: () => navigate('/admin/profile')
+    },
+    {
+      key: 'settings',
+      label: '账户设置',
+      icon: <Settings className="mr-2 h-4 w-4" />,
+      onClick: () => navigate('/admin/account-settings')
+    },
+    {
+      type: 'separator',
+      key: 'divider',
     },
     {
       key: 'logout',
       label: '退出登录',
-      icon: <InfoCircleOutlined />,
-      danger: true,
+      icon: <LogOut className="mr-2 h-4 w-4" />,
       onClick: () => handleLogout()
     }
   ];
@@ -116,21 +141,16 @@ export const useMenu = () => {
     if (item.path) {
       navigate(item.path);
     }
-  };
-
-  // 处理菜单展开变化
-  const onOpenChange = (keys: string[]) => {
-    const latestOpenKey = keys.find(key => openKeys.indexOf(key) === -1);
-    setOpenKeys(latestOpenKey ? [latestOpenKey] : []);
+    if (item.onClick) {
+      item.onClick();
+    }
   };
 
   return {
     menuItems,
     userMenuItems,
-    openKeys,
     collapsed,
     setCollapsed,
     handleMenuClick,
-    onOpenChange
   };
 };

+ 226 - 64
src/client/admin/pages/Dashboard.tsx

@@ -1,75 +1,237 @@
 import React from 'react';
-import {
-  Card, Row, Col, Typography, Statistic, Space
-} from 'antd';
-import {
-  UserOutlined, BellOutlined, EyeOutlined
-} from '@ant-design/icons';
-
-const { Title } = Typography;
+import { Users, Bell, Eye, TrendingUp, TrendingDown, Activity } from 'lucide-react';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
+import { Badge } from '@/client/components/ui/badge';
+import { Progress } from '@/client/components/ui/progress';
 
 // 仪表盘页面
 export const DashboardPage = () => {
+  const stats = [
+    {
+      title: '活跃用户',
+      value: '112,893',
+      icon: Users,
+      color: 'text-blue-500',
+      bgColor: 'bg-blue-50',
+      trend: 12.5,
+      trendDirection: 'up',
+      description: '较昨日增长 12.5%',
+    },
+    {
+      title: '系统消息',
+      value: '93',
+      icon: Bell,
+      color: 'text-yellow-500',
+      bgColor: 'bg-yellow-50',
+      trend: 0,
+      trendDirection: 'neutral',
+      description: '其中 5 条未读',
+    },
+    {
+      title: '在线用户',
+      value: '1,128',
+      icon: Eye,
+      color: 'text-purple-500',
+      bgColor: 'bg-purple-50',
+      trend: 32.1,
+      trendDirection: 'up',
+      description: '当前在线率 32.1%',
+    },
+  ];
+
+  const recentActivities = [
+    {
+      id: 1,
+      user: '张三',
+      action: '登录系统',
+      time: '2分钟前',
+      status: 'success',
+    },
+    {
+      id: 2,
+      user: '李四',
+      action: '创建了新用户',
+      time: '5分钟前',
+      status: 'info',
+    },
+    {
+      id: 3,
+      user: '王五',
+      action: '删除了用户',
+      time: '10分钟前',
+      status: 'warning',
+    },
+    {
+      id: 4,
+      user: '赵六',
+      action: '修改了配置',
+      time: '15分钟前',
+      status: 'info',
+    },
+  ];
+
+  const systemMetrics = [
+    {
+      name: 'CPU使用率',
+      value: 65,
+      color: 'bg-green-500',
+    },
+    {
+      name: '内存使用率',
+      value: 78,
+      color: 'bg-blue-500',
+    },
+    {
+      name: '磁盘使用率',
+      value: 45,
+      color: 'bg-purple-500',
+    },
+    {
+      name: '网络使用率',
+      value: 32,
+      color: 'bg-orange-500',
+    },
+  ];
+
   return (
-    <div>
-      <div className="mb-6 flex justify-between items-center">
-        <Title level={2}>仪表盘</Title>
+    <div className="space-y-6">
+      <div>
+        <h1 className="text-3xl font-bold tracking-tight">仪表盘</h1>
+        <p className="text-muted-foreground">
+          欢迎回来!这里是系统概览和关键指标。
+        </p>
       </div>
-      <Row gutter={[16, 16]}>
-        <Col xs={24} sm={12} lg={8}>
-          <Card className="shadow-sm transition-all duration-300 hover:shadow-md">
-            <div className="flex items-center justify-between mb-2">
-              <Typography.Title level={5}>活跃用户</Typography.Title>
-              <UserOutlined style={{ fontSize: 24, color: '#1890ff' }} />
-            </div>
-            <Statistic
-              value={112893}
-              loading={false}
-              valueStyle={{ fontSize: 28 }}
-              prefix={<span style={{ color: '#52c41a' }}>↑</span>}
-              suffix="人"
-            />
-            <div style={{ marginTop: 8, fontSize: 12, color: '#8c8c8c' }}>
-              较昨日增长 12.5%
-            </div>
-          </Card>
-        </Col>
-        <Col xs={24} sm={12} lg={8}>
-          <Card className="shadow-sm transition-all duration-300 hover:shadow-md">
-            <div className="flex items-center justify-between mb-2">
-              <Typography.Title level={5}>系统消息</Typography.Title>
-              <BellOutlined style={{ fontSize: 24, color: '#faad14' }} />
-            </div>
-            <Statistic
-              value={93}
-              loading={false}
-              valueStyle={{ fontSize: 28 }}
-              prefix={<span style={{ color: '#faad14' }}>●</span>}
-              suffix="条"
-            />
-            <div style={{ marginTop: 8, fontSize: 12, color: '#8c8c8c' }}>
-              其中 5 条未读
-            </div>
-          </Card>
-        </Col>
-        <Col xs={24} sm={12} lg={8}>
-          <Card className="shadow-sm transition-all duration-300 hover:shadow-md">
-            <div className="flex items-center justify-between mb-2">
-              <Typography.Title level={5}>在线用户</Typography.Title>
-              <EyeOutlined style={{ fontSize: 24, color: '#722ed1' }} />
+
+      {/* 统计卡片 */}
+      <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
+        {stats.map((stat, index) => {
+          const Icon = stat.icon;
+          return (
+            <Card key={index} className="transition-all duration-300 hover:shadow-lg">
+              <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+                <CardTitle className="text-sm font-medium">
+                  {stat.title}
+                </CardTitle>
+                <div className={`p-2 rounded-full ${stat.bgColor}`}>
+                  <Icon className={`h-4 w-4 ${stat.color}`} />
+                </div>
+              </CardHeader>
+              <CardContent>
+                <div className="text-2xl font-bold">{stat.value}</div>
+                <div className="flex items-center space-x-2">
+                  {stat.trendDirection === 'up' && (
+                    <TrendingUp className="h-4 w-4 text-green-500" />
+                  )}
+                  {stat.trendDirection === 'down' && (
+                    <TrendingDown className="h-4 w-4 text-red-500" />
+                  )}
+                  <p className="text-xs text-muted-foreground">{stat.description}</p>
+                </div>
+              </CardContent>
+            </Card>
+          );
+        })}
+      </div>
+
+      <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
+        {/* 系统性能 */}
+        <Card className="lg:col-span-4">
+          <CardHeader>
+            <CardTitle>系统性能</CardTitle>
+            <CardDescription>
+              当前系统各项资源的使用情况
+            </CardDescription>
+          </CardHeader>
+          <CardContent>
+            <div className="space-y-4">
+              {systemMetrics.map((metric, index) => (
+                <div key={index} className="space-y-2">
+                  <div className="flex justify-between text-sm">
+                    <span className="font-medium">{metric.name}</span>
+                    <span className="text-muted-foreground">{metric.value}%</span>
+                  </div>
+                  <Progress value={metric.value} className="h-2" />
+                </div>
+              ))}
             </div>
-            <Statistic
-              value={1128}
-              loading={false}
-              valueStyle={{ fontSize: 28 }}
-              suffix="人"
-            />
-            <div style={{ marginTop: 8, fontSize: 12, color: '#8c8c8c' }}>
-              当前在线率 32.1%
+          </CardContent>
+        </Card>
+
+        {/* 最近活动 */}
+        <Card className="lg:col-span-3">
+          <CardHeader>
+            <CardTitle>最近活动</CardTitle>
+            <CardDescription>
+              系统最新操作记录
+            </CardDescription>
+          </CardHeader>
+          <CardContent>
+            <div className="space-y-4">
+              {recentActivities.map((activity) => (
+                <div key={activity.id} className="flex items-center space-x-4">
+                  <div className="flex-shrink-0">
+                    <div className={`p-2 rounded-full ${
+                      activity.status === 'success' ? 'bg-green-100' :
+                      activity.status === 'warning' ? 'bg-yellow-100' : 'bg-blue-100'
+                    }`}>
+                      <Activity className={`h-4 w-4 ${
+                        activity.status === 'success' ? 'text-green-600' :
+                        activity.status === 'warning' ? 'text-yellow-600' : 'text-blue-600'
+                      }`} />
+                    </div>
+                  </div>
+                  <div className="flex-1 space-y-1">
+                    <p className="text-sm font-medium">
+                      {activity.user} {activity.action}
+                    </p>
+                    <p className="text-sm text-muted-foreground">
+                      {activity.time}
+                    </p>
+                  </div>
+                </div>
+              ))}
             </div>
-          </Card>
-        </Col>
-      </Row>
+          </CardContent>
+        </Card>
+      </div>
+
+      {/* 快捷操作 */}
+      <Card>
+        <CardHeader>
+          <CardTitle>快捷操作</CardTitle>
+          <CardDescription>
+            常用的管理功能
+          </CardDescription>
+        </CardHeader>
+        <CardContent>
+          <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
+            <Card className="hover:shadow-md transition-all cursor-pointer">
+              <CardHeader className="pb-3">
+                <CardTitle className="text-base">用户管理</CardTitle>
+                <CardDescription>查看和管理所有用户</CardDescription>
+              </CardHeader>
+            </Card>
+            <Card className="hover:shadow-md transition-all cursor-pointer">
+              <CardHeader className="pb-3">
+                <CardTitle className="text-base">系统设置</CardTitle>
+                <CardDescription>配置系统参数</CardDescription>
+              </CardHeader>
+            </Card>
+            <Card className="hover:shadow-md transition-all cursor-pointer">
+              <CardHeader className="pb-3">
+                <CardTitle className="text-base">数据备份</CardTitle>
+                <CardDescription>执行数据备份操作</CardDescription>
+              </CardHeader>
+            </Card>
+            <Card className="hover:shadow-md transition-all cursor-pointer">
+              <CardHeader className="pb-3">
+                <CardTitle className="text-base">日志查看</CardTitle>
+                <CardDescription>查看系统日志</CardDescription>
+              </CardHeader>
+            </Card>
+          </div>
+        </CardContent>
+      </Card>
     </div>
   );
 };

+ 133 - 84
src/client/admin/pages/Login.tsx

@@ -1,34 +1,42 @@
-import React, { useState } from 'react';
-import {
-  Form,
-  Input,
-  Button,
-  Card,
-  App,
-} from 'antd';
-import {
-  UserOutlined,
-  LockOutlined,
-  EyeOutlined,
-  EyeInvisibleOutlined
-} from '@ant-design/icons';
+import { useState } from 'react';
 import { useNavigate } from 'react-router';
-import {
-  useAuth,
-} from '../hooks/AuthProvider';
+import { useAuth } from '../hooks/AuthProvider';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { z } from 'zod';
+import { toast } from 'sonner';
+import { Eye, EyeOff, User, Lock } from 'lucide-react';
+import { Button } from '@/client/components/ui/button';
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/client/components/ui/card';
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
+import { Input } from '@/client/components/ui/input';
 
+// 表单验证Schema
+const loginSchema = z.object({
+  username: z.string().min(1, '请输入用户名'),
+  password: z.string().min(1, '请输入密码'),
+});
+
+type LoginFormData = z.infer<typeof loginSchema>;
 
 // 登录页面
 export const LoginPage = () => {
-  const { message } = App.useApp();
   const { login } = useAuth();
-  const [form] = Form.useForm();
-  const [loading, setLoading] = useState(false);
+  const [isLoading, setIsLoading] = useState(false);
+  const [showPassword, setShowPassword] = useState(false);
   const navigate = useNavigate();
   
-  const handleSubmit = async (values: { username: string; password: string }) => {
+  const form = useForm<LoginFormData>({
+    resolver: zodResolver(loginSchema),
+    defaultValues: {
+      username: '',
+      password: '',
+    },
+  });
+
+  const handleSubmit = async (data: LoginFormData) => {
     try {
-      setLoading(true);
+      setIsLoading(true);
       
       // 获取地理位置
       let latitude: number | undefined;
@@ -46,84 +54,125 @@ export const LoginPage = () => {
         console.warn('获取地理位置失败:', geoError);
       }
       
-      await login(values.username, values.password, latitude, longitude);
+      await login(data.username, data.password, latitude, longitude);
       // 登录成功后跳转到管理后台首页
       navigate('/admin/dashboard');
+      toast.success('登录成功!欢迎回来');
     } catch (error: any) {
-      message.error(error instanceof Error ? error.message : '登录失败');
+      toast.error(error instanceof Error ? error.message : '登录失败');
     } finally {
-      setLoading(false);
+      setIsLoading(false);
     }
   };
   
   return (
-    <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 py-12 px-4 sm:px-6 lg:px-8">
+    <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 py-12 px-4 sm:px-6 lg:px-8">
       <div className="max-w-md w-full space-y-8">
         <div className="text-center">
-          <div className="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
-            <UserOutlined style={{ fontSize: 32, color: '#1890ff' }} />
+          <div className="mx-auto w-16 h-16 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-full flex items-center justify-center shadow-lg">
+            <User className="h-8 w-8 text-white" />
           </div>
-          <h2 className="mt-2 text-center text-3xl font-extrabold text-gray-900">
+          <h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
             管理后台登录
           </h2>
-          <p className="mt-2 text-gray-500">请输入您的账号和密码</p>
+          <p className="mt-2 text-center text-sm text-gray-600">
+            请输入您的账号和密码继续操作
+          </p>
         </div>
         
-        <Card className="shadow-lg border-none transition-all duration-300 hover:shadow-xl">
-          <Form
-            form={form}
-            name="login"
-            onFinish={handleSubmit}
-            autoComplete="off"
-            layout="vertical"
-          >
-            <Form.Item
-              name="username"
-              rules={[{ required: true, message: '请输入用户名' }]}
-              label="用户名"
-            >
-              <Input
-                prefix={<UserOutlined className="text-primary" />}
-                placeholder="请输入用户名"
-                size="large"
-                className="transition-all duration-200 focus:border-primary focus:ring-1 focus:ring-primary"
-              />
-            </Form.Item>
-            
-            <Form.Item
-              name="password"
-              rules={[{ required: true, message: '请输入密码' }]}
-              label="密码"
-            >
-              <Input.Password
-                prefix={<LockOutlined className="text-primary" />}
-                placeholder="请输入密码"
-                size="large"
-                iconRender={(visible) => (visible ? <EyeOutlined /> : <EyeInvisibleOutlined />)}
-                className="transition-all duration-200 focus:border-primary focus:ring-1 focus:ring-primary"
-              />
-            </Form.Item>
-            
-            <Form.Item>
-              <Button
-                type="primary"
-                htmlType="submit"
-                size="large"
-                block
-                loading={loading}
-                className="h-12 text-lg transition-all duration-200 hover:shadow-lg"
-              >
-                登录
-              </Button>
-            </Form.Item>
-          </Form>
-          
-          <div className="mt-6 text-center text-gray-500 text-sm">
-            <p>测试账号: <span className="font-medium">admin</span> / <span className="font-medium">admin123</span></p>
-            <p className="mt-1">© {new Date().getFullYear()} 管理系统. 保留所有权利.</p>
-          </div>
+        <Card className="shadow-xl border-0">
+          <CardHeader className="text-center">
+            <CardTitle className="text-2xl">欢迎登录</CardTitle>
+            <CardDescription>
+              使用您的账户信息登录系统
+            </CardDescription>
+          </CardHeader>
+          <CardContent>
+            <Form {...form}>
+              <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
+                <FormField
+                  control={form.control}
+                  name="username"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>用户名</FormLabel>
+                      <FormControl>
+                        <div className="relative">
+                          <User className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
+                          <Input
+                            placeholder="请输入用户名"
+                            className="pl-10"
+                            {...field}
+                          />
+                        </div>
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+                
+                <FormField
+                  control={form.control}
+                  name="password"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>密码</FormLabel>
+                      <FormControl>
+                        <div className="relative">
+                          <Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
+                          <Input
+                            type={showPassword ? 'text' : 'password'}
+                            placeholder="请输入密码"
+                            className="pl-10 pr-10"
+                            {...field}
+                          />
+                          <button
+                            type="button"
+                            className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
+                            onClick={() => setShowPassword(!showPassword)}
+                          >
+                            {showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
+                          </button>
+                        </div>
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+                
+                <Button
+                  type="submit"
+                  className="w-full"
+                  disabled={isLoading}
+                >
+                  {isLoading ? (
+                    <>
+                      <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
+                      登录中...
+                    </>
+                  ) : (
+                    '登录'
+                  )}
+                </Button>
+              </form>
+            </Form>
+          </CardContent>
+          <CardFooter className="flex flex-col items-center space-y-2">
+            <div className="text-sm text-gray-500">
+              测试账号: <span className="font-medium text-gray-700">admin</span> / <span className="font-medium text-gray-700">admin123</span>
+            </div>
+            <div className="text-xs text-gray-400">
+              © {new Date().getFullYear()} 管理系统. 保留所有权利.
+            </div>
+          </CardFooter>
         </Card>
+        
+        <div className="text-center">
+          <p className="text-sm text-gray-500">
+            遇到问题?<a href="#" className="font-medium text-indigo-600 hover:text-indigo-500">联系管理员</a>
+          </p>
+        </div>
       </div>
     </div>
   );
-};
+};

+ 546 - 269
src/client/admin/pages/Users.tsx

@@ -1,34 +1,75 @@
 import React, { useState } from 'react';
-import {
-  Button, Table, Space, Form, Input, Select, Modal, Card, Typography, Tag, Popconfirm,
-  App
-} from 'antd';
 import { useQuery } from '@tanstack/react-query';
-import dayjs from 'dayjs';
-import { roleClient, userClient } from '@/client/api';
-import type { InferResponseType, InferRequestType } from 'hono/client';
-
-type UserListResponse = InferResponseType<typeof userClient.$get, 200>;
-type RoleListResponse = InferResponseType<typeof roleClient.$get, 200>;
-type CreateRoleRequest = InferRequestType<typeof roleClient.$post>['json'];
-type UserDetailResponse = InferResponseType<typeof userClient[':id']['$get'], 200>;
+import { format } from 'date-fns';
+import { Plus, Search, Edit, Trash2 } from 'lucide-react';
+import { userClient } from '@/client/api';
+import type { InferRequestType, InferResponseType } from 'hono/client';
+import { Button } from '@/client/components/ui/button';
+import { Input } from '@/client/components/ui/input';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/client/components/ui/table';
+import { Badge } from '@/client/components/ui/badge';
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
+import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
+import { DataTablePagination } from '@/client/admin/components/DataTablePagination';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { toast } from 'sonner';
+import { Skeleton } from '@/client/components/ui/skeleton';
+import { Switch } from '@/client/components/ui/switch';
+import { DisabledStatus } from '@/share/types';
+import { CreateUserDto, UpdateUserDto } from '@/server/modules/users/user.schema';
+
+// 使用RPC方式提取类型
 type CreateUserRequest = InferRequestType<typeof userClient.$post>['json'];
 type UpdateUserRequest = InferRequestType<typeof userClient[':id']['$put']>['json'];
+type UserResponse = InferResponseType<typeof userClient.$get, 200>['data'][0];
+
+// 直接使用后端定义的 schema
+const createUserFormSchema = CreateUserDto;
+const updateUserFormSchema = UpdateUserDto;
 
-const { Title } = Typography;
+type CreateUserFormData = CreateUserRequest;
+type UpdateUserFormData = UpdateUserRequest;
 
-// 用户管理页面
 export const UsersPage = () => {
-  const { message } = App.useApp();
   const [searchParams, setSearchParams] = useState({
     page: 1,
     limit: 10,
     search: ''
   });
-  const [modalVisible, setModalVisible] = useState(false);
-  const [modalTitle, setModalTitle] = useState('');
+  const [isModalOpen, setIsModalOpen] = useState(false);
   const [editingUser, setEditingUser] = useState<any>(null);
-  const [form] = Form.useForm();
+  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+  const [userToDelete, setUserToDelete] = useState<number | null>(null);
+
+  const [isCreateForm, setIsCreateForm] = useState(true);
+  
+  const createForm = useForm<CreateUserFormData>({
+    resolver: zodResolver(createUserFormSchema),
+    defaultValues: {
+      username: '',
+      nickname: undefined,
+      email: null,
+      phone: null,
+      name: null,
+      password: '',
+      isDisabled: DisabledStatus.ENABLED,
+    },
+  });
+
+  const updateForm = useForm<UpdateUserFormData>({
+    resolver: zodResolver(updateUserFormSchema),
+    defaultValues: {
+      username: undefined,
+      nickname: undefined,
+      email: null,
+      phone: null,
+      name: null,
+      password: undefined,
+      isDisabled: undefined,
+    },
+  });
 
   const { data: usersData, isLoading, refetch } = useQuery({
     queryKey: ['users', searchParams],
@@ -48,295 +89,531 @@ export const UsersPage = () => {
   });
 
   const users = usersData?.data || [];
-  const pagination = {
-    current: searchParams.page,
-    pageSize: searchParams.limit,
-    total: usersData?.pagination?.total || 0
-  };
+  const totalCount = usersData?.pagination?.total || 0;
 
   // 处理搜索
-  const handleSearch = (values: any) => {
-    setSearchParams(prev => ({
-      ...prev,
-      search: values.search || '',
-      page: 1
-    }));
+  const handleSearch = (e: React.FormEvent) => {
+    e.preventDefault();
+    setSearchParams(prev => ({ ...prev, page: 1 }));
   };
 
-  // 处理分页变化
-  const handleTableChange = (newPagination: any) => {
-    setSearchParams(prev => ({
-      ...prev,
-      page: newPagination.current,
-      limit: newPagination.pageSize
-    }));
+  // 处理分页
+  const handlePageChange = (page: number, limit: number) => {
+    setSearchParams(prev => ({ ...prev, page, limit }));
   };
 
-  // 打开创建用户模态框
-  const showCreateModal = () => {
-    setModalTitle('创建用户');
+  // 打开创建用户对话框
+  const handleCreateUser = () => {
     setEditingUser(null);
-    form.resetFields();
-    setModalVisible(true);
+    setIsCreateForm(true);
+    createForm.reset({
+      username: '',
+      nickname: null,
+      email: null,
+      phone: null,
+      name: null,
+      password: '',
+      isDisabled: DisabledStatus.ENABLED,
+    });
+    setIsModalOpen(true);
   };
 
-  // 打开编辑用户模态框
-  const showEditModal = (user: any) => {
-    setModalTitle('编辑用户');
+  // 打开编辑用户对话框
+  const handleEditUser = (user: UserResponse) => {
     setEditingUser(user);
-    form.setFieldsValue(user);
-    setModalVisible(true);
+    setIsCreateForm(false);
+    updateForm.reset({
+      username: user.username,
+      nickname: user.nickname,
+      email: user.email,
+      phone: user.phone,
+      name: user.name,
+      isDisabled: user.isDisabled,
+    });
+    setIsModalOpen(true);
   };
 
-  // 处理模态框确认
-  const handleModalOk = async () => {
+  // 处理创建表单提交
+  const handleCreateSubmit = async (data: CreateUserFormData) => {
     try {
-      const values = await form.validateFields();
-      
-      if (editingUser) {
-        // 编辑用户
-        const res = await userClient[':id']['$put']({
-          param: { id: editingUser.id },
-          json: values
-        });
-        if (res.status !== 200) {
-          throw new Error('更新用户失败');
-        }
-        message.success('用户更新成功');
-      } else {
-        // 创建用户
-        const res = await userClient.$post({
-          json: values
-        });
-        if (res.status !== 201) {
-          throw new Error('创建用户失败');
-        }
-        message.success('用户创建成功');
+      const res = await userClient.$post({
+        json: data
+      });
+      if (res.status !== 201) {
+        throw new Error('创建用户失败');
       }
-      
-      setModalVisible(false);
-      form.resetFields();
-      refetch(); // 刷新用户列表
+      toast.success('用户创建成功');
+      setIsModalOpen(false);
+      refetch();
     } catch (error) {
-      console.error('表单提交失败:', error);
-      message.error('操作失败,请重试');
+      console.error('创建用户失败:', error);
+      toast.error('创建失败,请重试');
+    }
+  };
+
+  // 处理更新表单提交
+  const handleUpdateSubmit = async (data: UpdateUserFormData) => {
+    if (!editingUser) return;
+    
+    try {
+      const res = await userClient[':id']['$put']({
+        param: { id: editingUser.id },
+        json: data
+      });
+      if (res.status !== 200) {
+        throw new Error('更新用户失败');
+      }
+      toast.success('用户更新成功');
+      setIsModalOpen(false);
+      refetch();
+    } catch (error) {
+      console.error('更新用户失败:', error);
+      toast.error('更新失败,请重试');
     }
   };
 
   // 处理删除用户
-  const handleDelete = async (id: number) => {
+  const handleDeleteUser = (id: number) => {
+    setUserToDelete(id);
+    setDeleteDialogOpen(true);
+  };
+
+  const confirmDelete = async () => {
+    if (!userToDelete) return;
+    
     try {
       const res = await userClient[':id']['$delete']({
-        param: { id }
+        param: { id: userToDelete }
       });
       if (res.status !== 204) {
         throw new Error('删除用户失败');
       }
-      message.success('用户删除成功');
-      refetch(); // 刷新用户列表
+      toast.success('用户删除成功');
+      refetch();
     } catch (error) {
       console.error('删除用户失败:', error);
-      message.error('删除失败,请重试');
+      toast.error('删除失败,请重试');
+    } finally {
+      setDeleteDialogOpen(false);
+      setUserToDelete(null);
     }
   };
-  
-  const columns = [
-    {
-      title: '用户名',
-      dataIndex: 'username',
-      key: 'username',
-    },
-    {
-      title: '昵称',
-      dataIndex: 'nickname',
-      key: 'nickname',
-    },
-    {
-      title: '邮箱',
-      dataIndex: 'email',
-      key: 'email',
-    },
-    {
-      title: '真实姓名',
-      dataIndex: 'name',
-      key: 'name',
-    },
-    {
-      title: '角色',
-      dataIndex: 'role',
-      key: 'role',
-      render: (role: string) => (
-        <Tag color={role === 'admin' ? 'red' : 'blue'}>
-          {role === 'admin' ? '管理员' : '普通用户'}
-        </Tag>
-      ),
-    },
-    {
-      title: '创建时间',
-      dataIndex: 'created_at',
-      key: 'created_at',
-      render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
-    },
-    {
-      title: '操作',
-      key: 'action',
-      render: (_: any, record: any) => (
-        <Space size="middle">
-          <Button type="link" onClick={() => showEditModal(record)}>
-            编辑
+
+  // 渲染加载骨架
+  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>
-          <Popconfirm
-            title="确定要删除此用户吗?"
-            onConfirm={() => handleDelete(record.id)}
-            okText="确定"
-            cancelText="取消"
-          >
-            <Button type="link" danger>
-              删除
-            </Button>
-          </Popconfirm>
-        </Space>
-      ),
-    },
-  ];
-  
+        </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>
+    );
+  }
+
   return (
-    <div>
-      <div className="mb-6 flex justify-between items-center">
-        <Title level={2}>用户管理</Title>
+    <div className="space-y-4">
+      <div className="flex justify-between items-center">
+        <h1 className="text-2xl font-bold">用户管理</h1>
+        <Button onClick={handleCreateUser}>
+          <Plus className="mr-2 h-4 w-4" />
+          创建用户
+        </Button>
       </div>
-      <Card className="shadow-md transition-all duration-300 hover:shadow-lg">
-        <Form layout="inline" onFinish={handleSearch} style={{ marginBottom: 16, padding: '16px 0' }}>
-          <Form.Item name="search" label="搜索">
-            <Input placeholder="用户名/昵称/邮箱" allowClear />
-          </Form.Item>
-          <Form.Item>
-            <Space>
-              <Button type="primary" htmlType="submit">
+
+      <Card>
+        <CardHeader>
+          <CardTitle>用户列表</CardTitle>
+          <CardDescription>
+            管理系统中的所有用户,共 {totalCount} 位用户
+          </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>
-              <Button type="primary" onClick={showCreateModal}>
-                创建用户
-              </Button>
-            </Space>
-          </Form.Item>
-        </Form>
-
-        <Table
-          columns={columns}
-          dataSource={users}
-          loading={isLoading}
-          rowKey="id"
-          pagination={{
-            ...pagination,
-            showSizeChanger: true,
-            showQuickJumper: true,
-            showTotal: (total) => `共 ${total} 条记录`
-          }}
-          onChange={handleTableChange}
-          bordered
-          scroll={{ x: 'max-content' }}
-          rowClassName={(record, index) => index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}
-        />
+            </form>
+          </div>
+
+          <div className="rounded-md border">
+            <Table>
+              <TableHeader>
+                <TableRow>
+                  <TableHead>用户名</TableHead>
+                  <TableHead>昵称</TableHead>
+                  <TableHead>邮箱</TableHead>
+                  <TableHead>真实姓名</TableHead>
+                  <TableHead>角色</TableHead>
+                  <TableHead>状态</TableHead>
+                  <TableHead>创建时间</TableHead>
+                  <TableHead className="text-right">操作</TableHead>
+                </TableRow>
+              </TableHeader>
+              <TableBody>
+                {users.map((user) => (
+                  <TableRow key={user.id}>
+                    <TableCell className="font-medium">{user.username}</TableCell>
+                    <TableCell>{user.nickname || '-'}</TableCell>
+                    <TableCell>{user.email || '-'}</TableCell>
+                    <TableCell>{user.name || '-'}</TableCell>
+                    <TableCell>
+                      <Badge
+                        variant={user.roles?.some((role: any) => role.name === 'admin') ? 'destructive' : 'default'}
+                        className="capitalize"
+                      >
+                        {user.roles?.some((role: any) => role.name === 'admin') ? '管理员' : '普通用户'}
+                      </Badge>
+                    </TableCell>
+                    <TableCell>
+                      <Badge
+                        variant={user.isDisabled === 1 ? 'secondary' : 'default'}
+                      >
+                        {user.isDisabled === 1 ? '禁用' : '启用'}
+                      </Badge>
+                    </TableCell>
+                    <TableCell>
+                      {format(new Date(user.createdAt), 'yyyy-MM-dd HH:mm')}
+                    </TableCell>
+                    <TableCell className="text-right">
+                      <div className="flex justify-end gap-2">
+                        <Button
+                          variant="ghost"
+                          size="icon"
+                          onClick={() => handleEditUser(user)}
+                        >
+                          <Edit className="h-4 w-4" />
+                        </Button>
+                        <Button
+                          variant="ghost"
+                          size="icon"
+                          onClick={() => handleDeleteUser(user.id)}
+                        >
+                          <Trash2 className="h-4 w-4" />
+                        </Button>
+                      </div>
+                    </TableCell>
+                  </TableRow>
+                ))}
+              </TableBody>
+            </Table>
+          </div>
+
+          <DataTablePagination
+            currentPage={searchParams.page}
+            totalCount={totalCount}
+            pageSize={searchParams.limit}
+            onPageChange={handlePageChange}
+          />
+        </CardContent>
       </Card>
 
-      {/* 创建/编辑用户模态框 */}
-      <Modal
-        title={modalTitle}
-        open={modalVisible}
-        onOk={handleModalOk}
-        onCancel={() => {
-          setModalVisible(false);
-          form.resetFields();
-        }}
-        width={600}
-        centered
-        destroyOnClose
-        maskClosable={false}
-      >
-        <Form
-          form={form}
-          layout="vertical"
-          labelCol={{ span: 5 }}
-          wrapperCol={{ span: 19 }}
-        >
-          <Form.Item
-            name="username"
-            label="用户名"
-            required
-            rules={[
-              { required: true, message: '请输入用户名' },
-              { min: 3, message: '用户名至少3个字符' }
-            ]}
-          >
-            <Input placeholder="请输入用户名" />
-          </Form.Item>
-
-          <Form.Item
-            name="nickname"
-            label="昵称"
-            rules={[{ required: false, message: '请输入昵称' }]}
-          >
-            <Input placeholder="请输入昵称" />
-          </Form.Item>
-
-          <Form.Item
-            name="email"
-            label="邮箱"
-            rules={[
-              { required: false, message: '请输入邮箱' },
-              { type: 'email', message: '请输入有效的邮箱地址' }
-            ]}
-          >
-            <Input placeholder="请输入邮箱" />
-          </Form.Item>
-
-          <Form.Item
-            name="phone"
-            label="手机号"
-            rules={[
-              { required: false, message: '请输入手机号' },
-              { pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号' }
-            ]}
-          >
-            <Input placeholder="请输入手机号" />
-          </Form.Item>
-
-          <Form.Item
-            name="name"
-            label="真实姓名"
-            rules={[{ required: false, message: '请输入真实姓名' }]}
-          >
-            <Input placeholder="请输入真实姓名" />
-          </Form.Item>
-
-          {!editingUser && (
-            <Form.Item
-              name="password"
-              label="密码"
-              required
-              rules={[
-                { required: true, message: '请输入密码' },
-                { min: 6, message: '密码至少6个字符' }
-              ]}
-            >
-              <Input.Password placeholder="请输入密码" />
-            </Form.Item>
+      {/* 创建/编辑用户对话框 */}
+      <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
+        <DialogContent className="sm:max-w-[500px]">
+          <DialogHeader>
+            <DialogTitle>
+              {editingUser ? '编辑用户' : '创建用户'}
+            </DialogTitle>
+            <DialogDescription>
+              {editingUser ? '编辑现有用户信息' : '创建一个新的用户账户'}
+            </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="nickname"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>昵称</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入昵称" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="email"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>邮箱</FormLabel>
+                      <FormControl>
+                        <Input type="email" placeholder="请输入邮箱" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="phone"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>手机号</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入手机号" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="name"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>真实姓名</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>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.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>
+                  )}
+                />
+
+                <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="nickname"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>昵称</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入昵称" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="email"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>邮箱</FormLabel>
+                      <FormControl>
+                        <Input type="email" placeholder="请输入邮箱" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="phone"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>手机号</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入手机号" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="name"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>真实姓名</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>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.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>
+                  )}
+                />
+
+                <DialogFooter>
+                  <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                    取消
+                  </Button>
+                  <Button type="submit">
+                    更新用户
+                  </Button>
+                </DialogFooter>
+              </form>
+            </Form>
           )}
+        </DialogContent>
+      </Dialog>
 
-          <Form.Item
-            name="isDisabled"
-            label="状态"
-            required
-            rules={[{ required: true, message: '请选择状态' }]}
-          >
-            <Select placeholder="请选择状态">
-              <Select.Option value={0}>启用</Select.Option>
-              <Select.Option value={1}>禁用</Select.Option>
-            </Select>
-          </Form.Item>
-        </Form>
-      </Modal>
+      {/* 删除确认对话框 */}
+      <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>
     </div>
   );
 };

+ 0 - 65
src/client/home-shadcn/components/ErrorPage.tsx

@@ -1,65 +0,0 @@
-import React from 'react';
-import { useRouteError, useNavigate } from 'react-router';
-import { AlertCircle, RotateCcw, Home } from 'lucide-react';
-import { Button } from '@/client/components/ui/button';
-import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/client/components/ui/card';
-import { Alert, AlertDescription, AlertTitle } from '@/client/components/ui/alert';
-
-export const ErrorPage = () => {
-  const navigate = useNavigate();
-  const error = useRouteError() as any;
-  const errorMessage = error?.statusText || error?.message || '未知错误';
-  
-  return (
-    <div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800 p-4">
-      <Card className="w-full max-w-md border-0 shadow-xl">
-        <CardHeader className="bg-destructive/10">
-          <CardTitle className="flex items-center space-x-2 text-destructive">
-            <AlertCircle className="h-6 w-6" />
-            <span>发生错误</span>
-          </CardTitle>
-          <CardDescription>
-            抱歉,页面加载时遇到了问题
-          </CardDescription>
-        </CardHeader>
-        
-        <CardContent className="space-y-4">
-          <Alert variant="destructive">
-            <AlertCircle className="h-4 w-4" />
-            <AlertTitle>错误详情</AlertTitle>
-            <AlertDescription>
-              {error?.message || '未知错误'}
-            </AlertDescription>
-          </Alert>
-          
-          {error?.stack && (
-            <div className="space-y-2">
-              <h4 className="text-sm font-medium text-muted-foreground">技术信息</h4>
-              <pre className="text-xs text-muted-foreground bg-muted/50 p-3 rounded-lg overflow-x-auto max-h-40">
-                {error.stack}
-              </pre>
-            </div>
-          )}
-        </CardContent>
-        
-        <CardFooter className="flex gap-2">
-          <Button
-            onClick={() => navigate(0)}
-            className="flex-1"
-            variant="outline"
-          >
-            <RotateCcw className="h-4 w-4 mr-2" />
-            重新加载
-          </Button>
-          <Button
-            onClick={() => navigate('/')}
-            className="flex-1"
-          >
-            <Home className="h-4 w-4 mr-2" />
-            返回首页
-          </Button>
-        </CardFooter>
-      </Card>
-    </div>
-  );
-};

+ 0 - 49
src/client/home-shadcn/components/NotFoundPage.tsx

@@ -1,49 +0,0 @@
-import React from 'react';
-import { useNavigate } from 'react-router';
-import { ArrowLeft, Home } from 'lucide-react';
-import { Button } from '@/client/components/ui/button';
-import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/client/components/ui/card';
-
-export const NotFoundPage = () => {
-  const navigate = useNavigate();
-  
-  return (
-    <div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800 p-4">
-      <Card className="w-full max-w-md border-0 shadow-xl">
-        <CardHeader className="text-center">
-          <div className="mx-auto flex h-20 w-20 items-center justify-center rounded-full bg-primary/10 mb-4">
-            <span className="text-4xl font-bold text-primary">404</span>
-          </div>
-          <CardTitle className="text-2xl">页面未找到</CardTitle>
-          <CardDescription>
-            抱歉,您访问的页面不存在或已被移除
-          </CardDescription>
-        </CardHeader>
-        
-        <CardContent className="text-center">
-          <p className="text-muted-foreground">
-            请检查URL是否正确,或尝试以下操作:
-          </p>
-        </CardContent>
-        
-        <CardFooter className="flex gap-2">
-          <Button
-            onClick={() => navigate(-1)}
-            variant="outline"
-            className="flex-1"
-          >
-            <ArrowLeft className="h-4 w-4 mr-2" />
-            返回上一页
-          </Button>
-          <Button
-            onClick={() => navigate('/')}
-            className="flex-1"
-          >
-            <Home className="h-4 w-4 mr-2" />
-            返回首页
-          </Button>
-        </CardFooter>
-      </Card>
-    </div>
-  );
-};

+ 0 - 44
src/client/home-shadcn/components/ProtectedRoute.tsx

@@ -1,44 +0,0 @@
-import React, { useEffect } from 'react';
-import { useNavigate } from 'react-router';
-import { useAuth } from '../hooks/AuthProvider';
-import { Skeleton } from '@/client/components/ui/skeleton';
-import { Card, CardContent } from '@/client/components/ui/card';
-
-export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
-  const { isAuthenticated, isLoading } = useAuth();
-  const navigate = useNavigate();
-  
-  useEffect(() => {
-    // 只有在加载完成且未认证时才重定向
-    if (!isLoading && !isAuthenticated) {
-      navigate('/login', { replace: true });
-    }
-  }, [isAuthenticated, isLoading, navigate]);
-  
-  // 显示加载状态,直到认证检查完成
-  if (isLoading) {
-    return (
-      <div className="flex justify-center items-center min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
-        <Card className="border-0 shadow-lg">
-          <CardContent className="flex flex-col items-center space-y-4 py-12 px-8">
-            <div className="relative">
-              <Skeleton className="h-12 w-12 rounded-full" />
-              <div className="absolute inset-0 rounded-full bg-primary/20 animate-pulse" />
-            </div>
-            <div className="space-y-2 text-center">
-              <Skeleton className="h-4 w-32" />
-              <Skeleton className="h-3 w-24" />
-            </div>
-          </CardContent>
-        </Card>
-      </div>
-    );
-  }
-  
-  // 如果未认证且不再加载中,不显示任何内容(等待重定向)
-  if (!isAuthenticated) {
-    return null;
-  }
-  
-  return children;
-};

+ 0 - 140
src/client/home-shadcn/hooks/AuthProvider.tsx

@@ -1,140 +0,0 @@
-import React, { useState, useEffect, createContext, useContext } from 'react';
-
-import {
-  useQuery,
-  useQueryClient,
-} from '@tanstack/react-query';
-import axios from 'axios';
-import 'dayjs/locale/zh-cn';
-import type {
-  AuthContextType
-} from '@/share/types';
-import { authClient } from '@/client/api';
-import type { InferResponseType, InferRequestType } from 'hono/client';
-
-export type User = InferResponseType<typeof authClient.me.$get, 200>;
-
-
-// 创建认证上下文
-const AuthContext = createContext<AuthContextType<User> | null>(null);
-
-// 认证提供器组件
-export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
-  const [user, setUser] = useState<User | null>(null);
-  const [token, setToken] = useState<string | null>(localStorage.getItem('token'));
-  const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
-  const queryClient = useQueryClient();
-
-  // 声明handleLogout函数
-  const handleLogout = async () => {
-    try {
-      // 如果已登录,调用登出API
-      if (token) {
-        await authClient.logout.$post();
-      }
-    } catch (error) {
-      console.error('登出请求失败:', error);
-    } finally {
-      // 清除本地状态
-      setToken(null);
-      setUser(null);
-      setIsAuthenticated(false);
-      localStorage.removeItem('token');
-      // 清除Authorization头
-      delete axios.defaults.headers.common['Authorization'];
-      console.log('登出时已删除全局Authorization头');
-      // 清除所有查询缓存
-      queryClient.clear();
-    }
-  };
-
-  // 使用useQuery检查登录状态
-  const { isFetching: isLoading } = useQuery({
-    queryKey: ['auth', 'status', token],
-    queryFn: async () => {
-      if (!token) {
-        setIsAuthenticated(false);
-        setUser(null);
-        return null;
-      }
-
-      try {
-        // 设置全局默认请求头
-        axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
-        // 使用API验证当前用户
-        const res = await authClient.me.$get();
-        if (res.status !== 200) {
-          const result = await res.json();
-          throw new Error(result.message)
-        }
-        const currentUser = await res.json();
-        setUser(currentUser);
-        setIsAuthenticated(true);
-        return { isValid: true, user: currentUser };
-      } catch (error) {
-        return { isValid: false };
-      }
-    },
-    enabled: !!token,
-    refetchOnWindowFocus: false,
-    retry: false
-  });
-
-  const handleLogin = async (username: string, password: string, latitude?: number, longitude?: number): Promise<void> => {
-    try {
-      // 使用AuthAPI登录
-      const response = await authClient.login.$post({
-        json: {
-          username,
-          password
-        }
-      })
-      if (response.status !== 200) {
-        const result = await response.json()
-        throw new Error(result.message);
-      }
-
-      const result = await response.json()
-
-      // 保存token和用户信息
-      const { token: newToken, user: newUser } = result;
-
-      // 设置全局默认请求头
-      axios.defaults.headers.common['Authorization'] = `Bearer ${newToken}`;
-
-      // 保存状态
-      setToken(newToken);
-      setUser(newUser);
-      setIsAuthenticated(true);
-      localStorage.setItem('token', newToken);
-
-    } catch (error) {
-      console.error('登录失败:', error);
-      throw error;
-    }
-  };
-
-  return (
-    <AuthContext.Provider
-      value={{
-        user,
-        token,
-        login: handleLogin,
-        logout: handleLogout,
-        isAuthenticated,
-        isLoading
-      }}
-    >
-      {children}
-    </AuthContext.Provider>
-  );
-};
-
-// 使用上下文的钩子
-export const useAuth = () => {
-  const context = useContext(AuthContext);
-  if (!context) {
-    throw new Error('useAuth必须在AuthProvider内部使用');
-  }
-  return context;
-};

+ 0 - 36
src/client/home-shadcn/index.tsx

@@ -1,36 +0,0 @@
-import { createRoot } from 'react-dom/client'
-import { getGlobalConfig } from '../utils/utils'
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
-import { AuthProvider } from './hooks/AuthProvider'
-import { RouterProvider } from 'react-router-dom'
-import { router } from './routes'
-import { Toaster } from '@/client/components/ui/sonner'
-
-// 创建QueryClient实例
-const queryClient = new QueryClient();
-
-// 应用入口组件
-const App = () => {
-  return (
-    <QueryClientProvider client={queryClient}>
-      <AuthProvider>
-        <RouterProvider router={router} />
-        <Toaster 
-          position="top-right"
-          expand={false}
-          richColors
-          closeButton
-          duration={3000}
-        />
-      </AuthProvider>
-    </QueryClientProvider>
-  )
-};
-
-const rootElement = document.getElementById('root')
-if (rootElement) {
-  const root = createRoot(rootElement)
-  root.render(
-    <App />
-  )
-}

+ 0 - 14
src/client/home-shadcn/layouts/MainLayout.tsx

@@ -1,14 +0,0 @@
-import React, { useState, useEffect, useMemo } from 'react';
-import {
-  Outlet
-} from 'react-router';
-
-/**
- * 主布局组件
- * 包含侧边栏、顶部导航和内容区域
- */
-export const MainLayout = () => {
-  return (
-    <Outlet />
-  );
-};

+ 0 - 133
src/client/home-shadcn/pages/LoginPage.tsx

@@ -1,133 +0,0 @@
-import React from 'react';
-import { useForm } from 'react-hook-form';
-import { zodResolver } from '@hookform/resolvers/zod';
-import { z } from 'zod';
-import { Link, useNavigate } from 'react-router-dom';
-import { useAuth } from '@/client/home-shadcn/hooks/AuthProvider';
-import { toast } from 'sonner';
-
-import { Button } from '@/client/components/ui/button';
-import { Input } from '@/client/components/ui/input';
-import { Label } from '@/client/components/ui/label';
-import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/client/components/ui/card';
-import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
-import { Eye, EyeOff, User, Lock } from 'lucide-react';
-
-const loginSchema = z.object({
-  username: z.string().min(3, '用户名至少3个字符'),
-  password: z.string().min(6, '密码至少6个字符'),
-});
-
-type LoginFormData = z.infer<typeof loginSchema>;
-
-const LoginPage: React.FC = () => {
-  const { login } = useAuth();
-  const navigate = useNavigate();
-  
-  const form = useForm<LoginFormData>({
-    resolver: zodResolver(loginSchema),
-    defaultValues: {
-      username: '',
-      password: '',
-    },
-  });
-
-  const onSubmit = async (data: LoginFormData) => {
-    try {
-      await login(data.username, data.password);
-      toast.success('登录成功!');
-      navigate('/');
-    } catch (error) {
-      toast.error(error instanceof Error ? error.message : '登录失败,请检查用户名和密码');
-    }
-  };
-
-  return (
-    <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 px-4 py-12">
-      <Card className="w-full max-w-md border-0 shadow-xl">
-        <CardHeader className="space-y-1">
-          <CardTitle className="text-2xl font-bold text-center">欢迎回来</CardTitle>
-          <CardDescription className="text-center">
-            登录您的账号以继续
-          </CardDescription>
-        </CardHeader>
-        
-        <CardContent>
-          <Form {...form}>
-            <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
-              <FormField
-                control={form.control}
-                name="username"
-                render={({ field }) => (
-                  <FormItem>
-                    <FormLabel>用户名</FormLabel>
-                    <FormControl>
-                      <div className="relative">
-                        <User className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
-                        <Input
-                          placeholder="请输入用户名"
-                          className="pl-10"
-                          {...field}
-                        />
-                      </div>
-                    </FormControl>
-                    <FormMessage />
-                  </FormItem>
-                )}
-              />
-
-              <FormField
-                control={form.control}
-                name="password"
-                render={({ field }) => (
-                  <FormItem>
-                    <FormLabel>密码</FormLabel>
-                    <FormControl>
-                      <div className="relative">
-                        <Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
-                        <Input
-                          type="password"
-                          placeholder="请输入密码"
-                          className="pl-10"
-                          {...field}
-                        />
-                      </div>
-                    </FormControl>
-                    <FormMessage />
-                  </FormItem>
-                )}
-              />
-
-              <Button 
-                type="submit" 
-                className="w-full"
-                disabled={form.formState.isSubmitting}
-              >
-                {form.formState.isSubmitting ? '登录中...' : '登录'}
-              </Button>
-            </form>
-          </Form>
-        </CardContent>
-
-        <CardFooter className="flex flex-col space-y-4">
-          <div className="text-sm text-center">
-            <span className="text-muted-foreground">还没有账号?</span>
-            <Button
-              variant="link"
-              className="px-1"
-              asChild
-            >
-              <Link to="/register">立即注册</Link>
-            </Button>
-          </div>
-          
-          <div className="text-xs text-center text-muted-foreground">
-            <p>测试账号:admin / admin123</p>
-          </div>
-        </CardFooter>
-      </Card>
-    </div>
-  );
-};
-
-export default LoginPage;

+ 0 - 234
src/client/home-shadcn/pages/MemberPage.tsx

@@ -1,234 +0,0 @@
-import React from 'react';
-import { useNavigate } from 'react-router-dom';
-import { useAuth } from '@/client/home-shadcn/hooks/AuthProvider';
-import { Button } from '@/client/components/ui/button';
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
-import { Avatar, AvatarFallback, AvatarImage } from '@/client/components/ui/avatar';
-import { Badge } from '@/client/components/ui/badge';
-import { Separator } from '@/client/components/ui/separator';
-import { 
-  User, 
-  MapPin, 
-  Globe, 
-  Calendar, 
-  LogOut, 
-  Settings,
-  UserCog,
-  ShieldCheck,
-  Clock
-} from 'lucide-react';
-import { format } from 'date-fns';
-import { zhCN } from 'date-fns/locale';
-
-const MemberPage: React.FC = () => {
-  const navigate = useNavigate();
-  const { user, logout } = useAuth();
-
-  if (!user) {
-    return (
-      <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 px-4">
-        <Card className="max-w-md">
-          <CardHeader>
-            <CardTitle>用户不存在</CardTitle>
-            <CardDescription>请先登录后再访问此页面</CardDescription>
-          </CardHeader>
-          <CardContent>
-            <Button onClick={() => navigate('/')} className="w-full">
-              返回首页
-            </Button>
-          </CardContent>
-        </Card>
-      </div>
-    );
-  }
-
-  return (
-    <div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
-      <div className="container mx-auto py-8">
-        <div className="mx-auto max-w-4xl space-y-8">
-          {/* 用户资料卡片 */}
-          <Card className="border-0 shadow-lg">
-            <CardContent className="pt-6">
-              <div className="flex flex-col items-center space-y-4">
-                <Avatar className="h-24 w-24">
-                  <AvatarImage 
-                    src={user.avatar || `https://avatar.vercel.sh/${user.username}`} 
-                    alt={user.nickname || user.username}
-                  />
-                  <AvatarFallback className="text-2xl bg-primary text-primary-foreground">
-                    {user.username?.charAt(0).toUpperCase()}
-                  </AvatarFallback>
-                </Avatar>
-                
-                <div className="text-center space-y-2">
-                  <h1 className="text-3xl font-bold">{user.nickname || user.username}</h1>
-                  <p className="text-muted-foreground">@{user.username}</p>
-                </div>
-
-                <div className="flex items-center space-x-4">
-                  <div className="text-center">
-                    <p className="text-2xl font-bold">0</p>
-                    <p className="text-sm text-muted-foreground">内容</p>
-                  </div>
-                  <Separator orientation="vertical" className="h-8" />
-                  <div className="text-center">
-                    <p className="text-2xl font-bold">0</p>
-                    <p className="text-sm text-muted-foreground">关注</p>
-                  </div>
-                  <Separator orientation="vertical" className="h-8" />
-                  <div className="text-center">
-                    <p className="text-2xl font-bold">0</p>
-                    <p className="text-sm text-muted-foreground">粉丝</p>
-                  </div>
-                </div>
-
-                <div className="flex items-center space-x-2">
-                  <Button 
-                    onClick={() => navigate('/profile/edit')}
-                    className="flex items-center space-x-2"
-                  >
-                    <UserCog className="h-4 w-4" />
-                    <span>编辑资料</span>
-                  </Button>
-                  
-                  <Button 
-                    variant="outline"
-                    onClick={async () => {
-                      await logout();
-                      navigate('/');
-                    }}
-                    className="flex items-center space-x-2"
-                  >
-                    <LogOut className="h-4 w-4" />
-                    <span>退出登录</span>
-                  </Button>
-                </div>
-              </div>
-            </CardContent>
-          </Card>
-
-          {/* 个人资料详情 */}
-          <Card className="border-0 shadow-lg">
-            <CardHeader>
-              <CardTitle className="flex items-center space-x-2">
-                <User className="h-5 w-5" />
-                <span>个人资料</span>
-              </CardTitle>
-            </CardHeader>
-            <CardContent className="space-y-6">
-              <div className="grid gap-4">
-                <div className="space-y-1">
-                  <div className="flex items-center space-x-2 text-sm text-muted-foreground">
-                    <User className="h-4 w-4" />
-                    <span>用户名</span>
-                  </div>
-                  <p className="font-medium">{user.username}</p>
-                </div>
-
-                <div className="space-y-1">
-                  <div className="flex items-center space-x-2 text-sm text-muted-foreground">
-                    <ShieldCheck className="h-4 w-4" />
-                    <span>邮箱</span>
-                  </div>
-                  <p className="font-medium">{user.email || '未设置'}</p>
-                </div>
-
-                {(user as any).location && (
-                  <div className="space-y-1">
-                    <div className="flex items-center space-x-2 text-sm text-muted-foreground">
-                      <MapPin className="h-4 w-4" />
-                      <span>位置</span>
-                    </div>
-                    <p className="font-medium">{(user as any).location}</p>
-                  </div>
-                )}
-
-                {(user as any).website && (
-                  <div className="space-y-1">
-                    <div className="flex items-center space-x-2 text-sm text-muted-foreground">
-                      <Globe className="h-4 w-4" />
-                      <span>网站</span>
-                    </div>
-                    <a 
-                      href={(user as any).website}
-                      target="_blank"
-                      rel="noopener noreferrer"
-                      className="font-medium text-primary hover:underline"
-                    >
-                      {(user as any).website}
-                    </a>
-                  </div>
-                )}
-
-                {(user as any).bio && (
-                  <div className="space-y-1">
-                    <div className="flex items-center space-x-2 text-sm text-muted-foreground">
-                      <User className="h-4 w-4" />
-                      <span>个人简介</span>
-                    </div>
-                    <p className="font-medium">{(user as any).bio}</p>
-                  </div>
-                )}
-              </div>
-
-              <Separator />
-
-              <div className="grid gap-4 md:grid-cols-2">
-                <div className="space-y-1">
-                  <div className="flex items-center space-x-2 text-sm text-muted-foreground">
-                    <Calendar className="h-4 w-4" />
-                    <span>注册时间</span>
-                  </div>
-                  <p className="font-medium">
-                    {user.createdAt ? format(new Date(user.createdAt), 'yyyy年MM月dd日', { locale: zhCN }) : '未知'}
-                  </p>
-                </div>
-
-                <div className="space-y-1">
-                  <div className="flex items-center space-x-2 text-sm text-muted-foreground">
-                    <Clock className="h-4 w-4" />
-                    <span>最后登录</span>
-                  </div>
-                  <p className="font-medium">
-                    {user.updatedAt ? format(new Date(user.updatedAt), 'yyyy年MM月dd日 HH:mm', { locale: zhCN }) : '从未登录'}
-                  </p>
-                </div>
-              </div>
-            </CardContent>
-          </Card>
-
-          {/* 设置区域 */}
-          <Card className="border-0 shadow-lg">
-            <CardHeader>
-              <CardTitle className="flex items-center space-x-2">
-                <Settings className="h-5 w-5" />
-                <span>账号设置</span>
-              </CardTitle>
-            </CardHeader>
-            <CardContent className="space-y-4">
-              <Button 
-                variant="outline" 
-                className="w-full justify-start"
-                onClick={() => navigate('/profile/security')}
-              >
-                <ShieldCheck className="h-4 w-4 mr-2" />
-                <span>安全设置</span>
-              </Button>
-              
-              <Button 
-                variant="outline" 
-                className="w-full justify-start"
-                onClick={() => navigate('/profile/preferences')}
-              >
-                <Settings className="h-4 w-4 mr-2" />
-                <span>偏好设置</span>
-              </Button>
-            </CardContent>
-          </Card>
-        </div>
-      </div>
-    </div>
-  );
-};
-
-export default MemberPage;

+ 0 - 178
src/client/home-shadcn/pages/RegisterPage.tsx

@@ -1,178 +0,0 @@
-import React from 'react';
-import { useForm } from 'react-hook-form';
-import { zodResolver } from '@hookform/resolvers/zod';
-import { z } from 'zod';
-import { Link, useNavigate } from 'react-router-dom';
-import { useAuth } from '@/client/home-shadcn/hooks/AuthProvider';
-import { toast } from 'sonner';
-import { authClient } from '@/client/api';
-
-import { Button } from '@/client/components/ui/button';
-import { Input } from '@/client/components/ui/input';
-import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/client/components/ui/card';
-import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
-import { Eye, EyeOff, User, Lock } from 'lucide-react';
-
-const registerSchema = z.object({
-  username: z.string()
-    .min(3, '用户名至少3个字符')
-    .max(20, '用户名不能超过20个字符')
-    .regex(/^[a-zA-Z0-9_-]+$/, '用户名只能包含字母、数字、下划线和连字符'),
-  password: z.string()
-    .min(6, '密码至少6个字符')
-    .max(30, '密码不能超过30个字符'),
-  confirmPassword: z.string(),
-}).refine((data) => data.password === data.confirmPassword, {
-  message: '两次密码输入不一致',
-  path: ['confirmPassword'],
-});
-
-type RegisterFormData = z.infer<typeof registerSchema>;
-
-const RegisterPage: React.FC = () => {
-  const { login } = useAuth();
-  const navigate = useNavigate();
-  
-  const form = useForm<RegisterFormData>({
-    resolver: zodResolver(registerSchema),
-    defaultValues: {
-      username: '',
-      password: '',
-      confirmPassword: '',
-    },
-  });
-
-  const onSubmit = async (data: RegisterFormData) => {
-    try {
-      const response = await authClient.register.$post({
-        json: {
-          username: data.username,
-          password: data.password,
-        }
-      });
-      
-      if (response.status !== 201) {
-        const result = await response.json();
-        throw new Error(result.message || '注册失败');
-      }
-      
-      // 注册成功后自动登录
-      await login(data.username, data.password);
-      toast.success('注册成功!正在为您登录...');
-      navigate('/');
-    } catch (error) {
-      toast.error(error instanceof Error ? error.message : '注册失败,请稍后重试');
-    }
-  };
-
-  return (
-    <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 px-4 py-12">
-      <Card className="w-full max-w-md border-0 shadow-xl">
-        <CardHeader className="space-y-1">
-          <CardTitle className="text-2xl font-bold text-center">创建账号</CardTitle>
-          <CardDescription className="text-center">
-            填写以下信息创建新账号
-          </CardDescription>
-        </CardHeader>
-        
-        <CardContent>
-          <Form {...form}>
-            <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
-              <FormField
-                control={form.control}
-                name="username"
-                render={({ field }) => (
-                  <FormItem>
-                    <FormLabel>用户名</FormLabel>
-                    <FormControl>
-                      <div className="relative">
-                        <User className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
-                        <Input
-                          placeholder="请输入用户名"
-                          className="pl-10"
-                          {...field}
-                        />
-                      </div>
-                    </FormControl>
-                    <FormMessage />
-                  </FormItem>
-                )}
-              />
-
-              <FormField
-                control={form.control}
-                name="password"
-                render={({ field }) => (
-                  <FormItem>
-                    <FormLabel>密码</FormLabel>
-                    <FormControl>
-                      <div className="relative">
-                        <Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
-                        <Input
-                          type="password"
-                          placeholder="请输入密码"
-                          className="pl-10"
-                          {...field}
-                        />
-                      </div>
-                    </FormControl>
-                    <FormMessage />
-                  </FormItem>
-                )}
-              />
-
-              <FormField
-                control={form.control}
-                name="confirmPassword"
-                render={({ field }) => (
-                  <FormItem>
-                    <FormLabel>确认密码</FormLabel>
-                    <FormControl>
-                      <div className="relative">
-                        <Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
-                        <Input
-                          type="password"
-                          placeholder="请再次输入密码"
-                          className="pl-10"
-                          {...field}
-                        />
-                      </div>
-                    </FormControl>
-                    <FormMessage />
-                  </FormItem>
-                )}
-              />
-
-              <Button 
-                type="submit" 
-                className="w-full"
-                disabled={form.formState.isSubmitting}
-              >
-                {form.formState.isSubmitting ? '注册中...' : '注册账号'}
-              </Button>
-            </form>
-          </Form>
-        </CardContent>
-
-        <CardFooter className="flex flex-col space-y-4">
-          <div className="text-sm text-center">
-            <span className="text-muted-foreground">已有账号?</span>
-            <Button
-              variant="link"
-              className="px-1"
-              asChild
-            >
-              <Link to="/login">立即登录</Link>
-            </Button>
-          </div>
-          
-          <div className="text-xs text-center text-muted-foreground">
-            <p>注册即表示您同意我们的服务条款</p>
-          </div>
-        </CardFooter>
-      </Card>
-    </div>
-  );
-};
-
-export default RegisterPage;

+ 0 - 49
src/client/home-shadcn/routes.tsx

@@ -1,49 +0,0 @@
-import React from 'react';
-import { createBrowserRouter, Navigate } from 'react-router';
-import { ProtectedRoute } from './components/ProtectedRoute';
-import { ErrorPage } from './components/ErrorPage';
-import { NotFoundPage } from './components/NotFoundPage';
-import HomePage from './pages/HomePage';
-import { MainLayout } from './layouts/MainLayout';
-import LoginPage from './pages/LoginPage';
-import RegisterPage from './pages/RegisterPage';
-import MemberPage from './pages/MemberPage';
-
-export const router = createBrowserRouter([
-  {
-    path: '/',
-    element: <HomePage />
-  },
-  {
-    path: '/login',
-    element: <LoginPage />
-  },
-  {
-    path: '/register',
-    element: <RegisterPage />
-  },
-  {
-    path: '/member',
-    element: (
-      <ProtectedRoute>
-        <MainLayout />
-      </ProtectedRoute>
-    ),
-    children: [
-      {
-        path: '',
-        element: <MemberPage />
-      },
-      {
-        path: '*',
-        element: <NotFoundPage />,
-        errorElement: <ErrorPage />
-      },
-    ],
-  },
-  {
-    path: '*',
-    element: <NotFoundPage />,
-    errorElement: <ErrorPage />
-  },
-]);

+ 52 - 36
src/client/home/components/ErrorPage.tsx

@@ -1,5 +1,9 @@
 import React from 'react';
 import { useRouteError, useNavigate } from 'react-router';
+import { AlertCircle, RotateCcw, Home } from 'lucide-react';
+import { Button } from '@/client/components/ui/button';
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/client/components/ui/card';
+import { Alert, AlertDescription, AlertTitle } from '@/client/components/ui/alert';
 
 export const ErrorPage = () => {
   const navigate = useNavigate();
@@ -7,43 +11,55 @@ export const ErrorPage = () => {
   const errorMessage = error?.statusText || error?.message || '未知错误';
   
   return (
-    <div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 p-4">
-      <div className="w-full max-w-md bg-white dark:bg-gray-800 rounded-xl shadow-lg overflow-hidden transition-all duration-300 hover:shadow-xl">
-        <div className="bg-red-50 dark:bg-red-900/30 px-6 py-4 border-b border-red-100 dark:border-red-800">
-          <h1 className="text-2xl font-bold text-red-600 dark:text-red-400">发生错误</h1>
-        </div>
-        <div className="p-6">
-          <div className="flex items-start mb-4">
-            <div className="flex-shrink-0 bg-red-100 dark:bg-red-900/50 p-3 rounded-full">
-              <svg className="w-8 h-8 text-red-500 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
-                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
-              </svg>
+    <div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800 p-4">
+      <Card className="w-full max-w-md border-0 shadow-xl">
+        <CardHeader className="bg-destructive/10">
+          <CardTitle className="flex items-center space-x-2 text-destructive">
+            <AlertCircle className="h-6 w-6" />
+            <span>发生错误</span>
+          </CardTitle>
+          <CardDescription>
+            抱歉,页面加载时遇到了问题
+          </CardDescription>
+        </CardHeader>
+        
+        <CardContent className="space-y-4">
+          <Alert variant="destructive">
+            <AlertCircle className="h-4 w-4" />
+            <AlertTitle>错误详情</AlertTitle>
+            <AlertDescription>
+              {error?.message || '未知错误'}
+            </AlertDescription>
+          </Alert>
+          
+          {error?.stack && (
+            <div className="space-y-2">
+              <h4 className="text-sm font-medium text-muted-foreground">技术信息</h4>
+              <pre className="text-xs text-muted-foreground bg-muted/50 p-3 rounded-lg overflow-x-auto max-h-40">
+                {error.stack}
+              </pre>
             </div>
-            <div className="ml-4">
-              <h3 className="text-lg font-medium text-gray-900 dark:text-white">{error?.message || '未知错误'}</h3>
-              {error?.stack && (
-                <pre className="mt-2 text-xs text-gray-600 dark:text-gray-300 bg-gray-50 dark:bg-gray-700 p-3 rounded overflow-x-auto max-h-40">
-                  {error.stack}
-                </pre>
-              )}
-            </div>
-          </div>
-          <div className="flex gap-4">
-            <button
-              onClick={() => navigate(0)}
-              className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors duration-200"
-            >
-              重新加载
-            </button>
-            <button
-              onClick={() => navigate('/')}
-              className="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 shadow-sm text-sm font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors duration-200"
-            >
-              返回首页
-            </button>
-          </div>
-        </div>
-      </div>
+          )}
+        </CardContent>
+        
+        <CardFooter className="flex gap-2">
+          <Button
+            onClick={() => navigate(0)}
+            className="flex-1"
+            variant="outline"
+          >
+            <RotateCcw className="h-4 w-4 mr-2" />
+            重新加载
+          </Button>
+          <Button
+            onClick={() => navigate('/')}
+            className="flex-1"
+          >
+            <Home className="h-4 w-4 mr-2" />
+            返回首页
+          </Button>
+        </CardFooter>
+      </Card>
     </div>
   );
 };

+ 35 - 21
src/client/home/components/NotFoundPage.tsx

@@ -1,35 +1,49 @@
 import React from 'react';
 import { useNavigate } from 'react-router';
+import { ArrowLeft, Home } from 'lucide-react';
+import { Button } from '@/client/components/ui/button';
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/client/components/ui/card';
 
 export const NotFoundPage = () => {
   const navigate = useNavigate();
   
   return (
-    <div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 p-4">
-      <div className="w-full max-w-md bg-white dark:bg-gray-800 rounded-xl shadow-lg overflow-hidden transition-all duration-300 hover:shadow-xl">
-        <div className="bg-blue-50 dark:bg-blue-900/30 px-6 py-4 border-b border-blue-100 dark:border-blue-800">
-          <h1 className="text-2xl font-bold text-blue-600 dark:text-blue-400">404 - 页面未找到</h1>
-        </div>
-        <div className="p-6">
-          <div className="flex items-start mb-6">
-            <div className="flex-shrink-0 bg-blue-100 dark:bg-blue-900/50 p-3 rounded-full">
-              <svg className="w-10 h-10 text-blue-500 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
-                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
-              </svg>
-            </div>
-            <div className="ml-4">
-              <p className="text-lg text-gray-700 dark:text-gray-300">您访问的页面不存在或已被移除</p>
-              <p className="mt-2 text-gray-500 dark:text-gray-400">请检查URL是否正确或返回首页</p>
-            </div>
+    <div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800 p-4">
+      <Card className="w-full max-w-md border-0 shadow-xl">
+        <CardHeader className="text-center">
+          <div className="mx-auto flex h-20 w-20 items-center justify-center rounded-full bg-primary/10 mb-4">
+            <span className="text-4xl font-bold text-primary">404</span>
           </div>
-          <button
+          <CardTitle className="text-2xl">页面未找到</CardTitle>
+          <CardDescription>
+            抱歉,您访问的页面不存在或已被移除
+          </CardDescription>
+        </CardHeader>
+        
+        <CardContent className="text-center">
+          <p className="text-muted-foreground">
+            请检查URL是否正确,或尝试以下操作:
+          </p>
+        </CardContent>
+        
+        <CardFooter className="flex gap-2">
+          <Button
+            onClick={() => navigate(-1)}
+            variant="outline"
+            className="flex-1"
+          >
+            <ArrowLeft className="h-4 w-4 mr-2" />
+            返回上一页
+          </Button>
+          <Button
             onClick={() => navigate('/')}
-            className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200 w-full"
+            className="flex-1"
           >
+            <Home className="h-4 w-4 mr-2" />
             返回首页
-          </button>
-        </div>
-      </div>
+          </Button>
+        </CardFooter>
+      </Card>
     </div>
   );
 };

+ 16 - 9
src/client/home/components/ProtectedRoute.tsx

@@ -1,12 +1,8 @@
 import React, { useEffect } from 'react';
-import { 
-  useNavigate,
-} from 'react-router';
+import { useNavigate } from 'react-router';
 import { useAuth } from '../hooks/AuthProvider';
-
-
-
-
+import { Skeleton } from '@/client/components/ui/skeleton';
+import { Card, CardContent } from '@/client/components/ui/card';
 
 export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
   const { isAuthenticated, isLoading } = useAuth();
@@ -22,8 +18,19 @@ export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
   // 显示加载状态,直到认证检查完成
   if (isLoading) {
     return (
-      <div className="flex justify-center items-center h-screen">
-        <div className="loader ease-linear rounded-full border-4 border-t-4 border-gray-200 h-12 w-12"></div>
+      <div className="flex justify-center items-center min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
+        <Card className="border-0 shadow-lg">
+          <CardContent className="flex flex-col items-center space-y-4 py-12 px-8">
+            <div className="relative">
+              <Skeleton className="h-12 w-12 rounded-full" />
+              <div className="absolute inset-0 rounded-full bg-primary/20 animate-pulse" />
+            </div>
+            <div className="space-y-2 text-center">
+              <Skeleton className="h-4 w-32" />
+              <Skeleton className="h-3 w-24" />
+            </div>
+          </CardContent>
+        </Card>
       </div>
     );
   }

+ 8 - 0
src/client/home/index.tsx

@@ -4,6 +4,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
 import { AuthProvider } from './hooks/AuthProvider'
 import { RouterProvider } from 'react-router-dom'
 import { router } from './routes'
+import { Toaster } from '@/client/components/ui/sonner'
 
 // 创建QueryClient实例
 const queryClient = new QueryClient();
@@ -14,6 +15,13 @@ const App = () => {
     <QueryClientProvider client={queryClient}>
       <AuthProvider>
         <RouterProvider router={router} />
+        <Toaster 
+          position="top-right"
+          expand={false}
+          richColors
+          closeButton
+          duration={3000}
+        />
       </AuthProvider>
     </QueryClientProvider>
   )

+ 44 - 2
src/client/home/pages/HomePage.tsx

@@ -1,6 +1,18 @@
 import React from 'react';
 import { useAuth } from '@/client/home/hooks/AuthProvider';
 import { useNavigate } from 'react-router-dom';
+import { Button } from '@/client/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
+import { Avatar, AvatarFallback, AvatarImage } from '@/client/components/ui/avatar';
+import { Badge } from '@/client/components/ui/badge';
+import { 
+  ShieldCheck, 
+  UserCircle, 
+  Smartphone, 
+  LogIn, 
+  UserPlus,
+  ExternalLink
+} from 'lucide-react';
 
 const HomePage: React.FC = () => {
   const { user } = useAuth();
@@ -53,7 +65,7 @@ const HomePage: React.FC = () => {
           )}
         </div>
       </header>
-      
+
       {/* 主内容区 */}
       <main className="flex-grow container mx-auto px-4 pt-24 pb-12">
         {/* 英雄区域 */}
@@ -194,9 +206,39 @@ const HomePage: React.FC = () => {
               </ul>
             </div>
           </div>
+
+          {/* 快速链接 */}
+          <Card className="border-0 shadow-lg">
+            <CardHeader>
+              <CardTitle className="flex items-center space-x-2">
+                <ExternalLink className="h-5 w-5" />
+                <span>快速访问</span>
+              </CardTitle>
+            </CardHeader>
+            <CardContent>
+              <div className="flex flex-wrap gap-4">
+                <Button
+                  variant="outline"
+                  onClick={() => window.open('/admin', '_blank')}
+                  className="flex items-center space-x-2"
+                >
+                  <span>管理后台</span>
+                  <ExternalLink className="h-4 w-4" />
+                </Button>
+                <Button
+                  variant="outline"
+                  onClick={() => window.open('/ui', '_blank')}
+                  className="flex items-center space-x-2"
+                >
+                  <span>API 文档</span>
+                  <ExternalLink className="h-4 w-4" />
+                </Button>
+              </div>
+            </CardContent>
+          </Card>
         </div>
       </main>
-      
+
       {/* 页脚 */}
       <footer className="bg-white border-t border-gray-100 py-8">
         <div className="container mx-auto px-4">

+ 110 - 110
src/client/home/pages/LoginPage.tsx

@@ -1,131 +1,131 @@
-import React, { useState } from 'react';
+import React from 'react';
 import { useForm } from 'react-hook-form';
-import { EyeIcon, EyeSlashIcon, UserIcon, LockClosedIcon } from '@heroicons/react/24/outline';
-import { useNavigate } from 'react-router-dom';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { z } from 'zod';
+import { Link, useNavigate } from 'react-router-dom';
 import { useAuth } from '@/client/home/hooks/AuthProvider';
+import { toast } from 'sonner';
+
+import { Button } from '@/client/components/ui/button';
+import { Input } from '@/client/components/ui/input';
+import { Label } from '@/client/components/ui/label';
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/client/components/ui/card';
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
+import { Eye, EyeOff, User, Lock } from 'lucide-react';
+
+const loginSchema = z.object({
+  username: z.string().min(3, '用户名至少3个字符'),
+  password: z.string().min(6, '密码至少6个字符'),
+});
+
+type LoginFormData = z.infer<typeof loginSchema>;
 
 const LoginPage: React.FC = () => {
-  const { register, handleSubmit, formState: { errors } } = useForm();
-  const [showPassword, setShowPassword] = useState(false);
-  const [loading, setLoading] = useState(false);
   const { login } = useAuth();
   const navigate = useNavigate();
+  
+  const form = useForm<LoginFormData>({
+    resolver: zodResolver(loginSchema),
+    defaultValues: {
+      username: '',
+      password: '',
+    },
+  });
 
-  const onSubmit = async (data: any) => {
+  const onSubmit = async (data: LoginFormData) => {
     try {
-      setLoading(true);
       await login(data.username, data.password);
+      toast.success('登录成功!');
       navigate('/');
     } catch (error) {
-      console.error('Login error:', error);
-      alert((error as Error).message || '登录失败,请检查用户名和密码');
-    } finally {
-      setLoading(false);
+      toast.error(error instanceof Error ? error.message : '登录失败,请检查用户名和密码');
     }
   };
 
   return (
-    <div className="flex justify-center items-center min-h-screen bg-gray-100">
-      <div className="w-full max-w-md bg-white rounded-lg shadow-md overflow-hidden">
-        <div className="p-6 sm:p-8">
-          <div className="text-center mb-8">
-            <h2 className="text-2xl font-bold text-gray-900">网站登录</h2>
-            <p className="mt-2 text-sm text-gray-600">登录您的账号以继续</p>
-          </div>
-          
-          <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
-            <div>
-              <label htmlFor="username" className="block text-sm font-medium text-gray-700 mb-1">
-                用户名
-              </label>
-              <div className="relative">
-                <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
-                  <UserIcon className="h-5 w-5 text-gray-400" />
-                </div>
-                <input
-                  id="username"
-                  type="text"
-                  className={`w-full pl-10 pr-3 py-2 border ${errors.username ? 'border-red-300' : 'border-gray-300'} rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent`}
-                  placeholder="请输入用户名"
-                  {...register('username', { 
-                    required: '用户名不能为空',
-                    minLength: { value: 3, message: '用户名至少3个字符' }
-                  })}
-                />
-              </div>
-              {errors.username && (
-                <p className="mt-1 text-sm text-red-600">{errors.username.message?.toString()}</p>
-              )}
-            </div>
-            
-            <div>
-              <label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
-                密码
-              </label>
-              <div className="relative">
-                <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
-                  <LockClosedIcon className="h-5 w-5 text-gray-400" />
-                </div>
-                <input
-                  id="password"
-                  type={showPassword ? 'text' : 'password'}
-                  className={`w-full pl-10 pr-10 py-2 border ${errors.password ? 'border-red-300' : 'border-gray-300'} rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent`}
-                  placeholder="请输入密码"
-                  {...register('password', { 
-                    required: '密码不能为空',
-                    minLength: { value: 6, message: '密码至少6个字符' }
-                  })}
-                />
-                <button 
-                  type="button"
-                  className="absolute inset-y-0 right-0 pr-3 flex items-center"
-                  onClick={() => setShowPassword(!showPassword)}
-                >
-                  {showPassword ? (
-                    <EyeSlashIcon className="h-5 w-5 text-gray-400" />
-                  ) : (
-                    <EyeIcon className="h-5 w-5 text-gray-400" />
-                  )}
-                </button>
-              </div>
-              {errors.password && (
-                <p className="mt-1 text-sm text-red-600">{errors.password.message?.toString()}</p>
-              )}
-            </div>
-            
-            <div>
-              <button
-                type="submit"
-                disabled={loading}
-                className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
+    <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 px-4 py-12">
+      <Card className="w-full max-w-md border-0 shadow-xl">
+        <CardHeader className="space-y-1">
+          <CardTitle className="text-2xl font-bold text-center">欢迎回来</CardTitle>
+          <CardDescription className="text-center">
+            登录您的账号以继续
+          </CardDescription>
+        </CardHeader>
+        
+        <CardContent>
+          <Form {...form}>
+            <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
+              <FormField
+                control={form.control}
+                name="username"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>用户名</FormLabel>
+                    <FormControl>
+                      <div className="relative">
+                        <User className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
+                        <Input
+                          placeholder="请输入用户名"
+                          className="pl-10"
+                          {...field}
+                        />
+                      </div>
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <FormField
+                control={form.control}
+                name="password"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>密码</FormLabel>
+                    <FormControl>
+                      <div className="relative">
+                        <Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
+                        <Input
+                          type="password"
+                          placeholder="请输入密码"
+                          className="pl-10"
+                          {...field}
+                        />
+                      </div>
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <Button 
+                type="submit" 
+                className="w-full"
+                disabled={form.formState.isSubmitting}
               >
-                {loading ? '登录中...' : '登录'}
-              </button>
-            </div>
-          </form>
+                {form.formState.isSubmitting ? '登录中...' : '登录'}
+              </Button>
+            </form>
+          </Form>
+        </CardContent>
+
+        <CardFooter className="flex flex-col space-y-4">
+          <div className="text-sm text-center">
+            <span className="text-muted-foreground">还没有账号?</span>
+            <Button
+              variant="link"
+              className="px-1"
+              asChild
+            >
+              <Link to="/register">立即注册</Link>
+            </Button>
+          </div>
           
-          <div className="mt-6">
-            <div className="relative">
-              <div className="absolute inset-0 flex items-center">
-                <div className="w-full border-t border-gray-300"></div>
-              </div>
-              <div className="relative flex justify-center text-sm">
-                <span className="px-2 bg-white text-gray-500">还没有账号?</span>
-              </div>
-            </div>
-            
-            <div className="mt-4">
-              <button
-                type="button"
-                onClick={() => navigate('/register')}
-                className="w-full flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
-              >
-                注册账号
-              </button>
-            </div>
+          <div className="text-xs text-center text-muted-foreground">
+            <p>测试账号:admin / admin123</p>
           </div>
-        </div>
-      </div>
+        </CardFooter>
+      </Card>
     </div>
   );
 };

+ 208 - 127
src/client/home/pages/MemberPage.tsx

@@ -1,11 +1,24 @@
-import debug from 'debug';
 import React from 'react';
-import { UserIcon, PencilIcon } from '@heroicons/react/24/outline';
-import { useParams, useNavigate } from 'react-router-dom';
-import { useQuery } from '@tanstack/react-query';
-import type { InferResponseType } from 'hono/client';
-import { userClient } from '@/client/api';
-import { useAuth, User } from '@/client/home/hooks/AuthProvider';
+import { useNavigate } from 'react-router-dom';
+import { useAuth } from '@/client/home/hooks/AuthProvider';
+import { Button } from '@/client/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
+import { Avatar, AvatarFallback, AvatarImage } from '@/client/components/ui/avatar';
+import { Badge } from '@/client/components/ui/badge';
+import { Separator } from '@/client/components/ui/separator';
+import { 
+  User, 
+  MapPin, 
+  Globe, 
+  Calendar, 
+  LogOut, 
+  Settings,
+  UserCog,
+  ShieldCheck,
+  Clock
+} from 'lucide-react';
+import { format } from 'date-fns';
+import { zhCN } from 'date-fns/locale';
 
 const MemberPage: React.FC = () => {
   const navigate = useNavigate();
@@ -13,137 +26,205 @@ const MemberPage: React.FC = () => {
 
   if (!user) {
     return (
-      <div className="text-center py-12">
-        <h2 className="text-2xl font-bold text-gray-900 mb-4">用户不存在</h2>
-        <button
-          onClick={() => navigate('/')}
-          className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
-        >
-          返回首页
-        </button>
+      <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 px-4">
+        <Card className="max-w-md">
+          <CardHeader>
+            <CardTitle>用户不存在</CardTitle>
+            <CardDescription>请先登录后再访问此页面</CardDescription>
+          </CardHeader>
+          <CardContent>
+            <Button onClick={() => navigate('/')} className="w-full">
+              返回首页
+            </Button>
+          </CardContent>
+        </Card>
       </div>
     );
   }
 
   return (
-    <div className="min-h-screen bg-gray-50">
-      <div className="container mx-auto px-4 py-8 max-w-4xl">
-        {/* 用户资料卡片 */}
-        <div className="bg-white rounded-lg shadow-sm p-6 mb-8">
-          <div className="flex flex-col items-center">
-            <div className="w-24 h-24 rounded-full bg-gray-200 flex items-center justify-center mb-4">
-              {user.avatar ? (
-                <img src={user.avatar} alt={user.nickname || user.username} className="h-full w-full object-cover rounded-full" />
-              ) : (
-                <UserIcon className="h-12 w-12 text-gray-500" />
-              )}
-            </div>
-            
-            <h1 className="text-2xl font-bold text-gray-900 mb-1">{user.nickname || user.username}</h1>
-            
-            <div className="flex space-x-8 my-4">
-              <div className="text-center">
-                <p className="text-2xl font-semibold text-gray-900">0</p>
-                <p className="text-sm text-gray-500">内容</p>
+    <div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
+      <div className="container mx-auto py-8">
+        <div className="mx-auto max-w-4xl space-y-8">
+          {/* 用户资料卡片 */}
+          <Card className="border-0 shadow-lg">
+            <CardContent className="pt-6">
+              <div className="flex flex-col items-center space-y-4">
+                <Avatar className="h-24 w-24">
+                  <AvatarImage 
+                    src={user.avatar || `https://avatar.vercel.sh/${user.username}`} 
+                    alt={user.nickname || user.username}
+                  />
+                  <AvatarFallback className="text-2xl bg-primary text-primary-foreground">
+                    {user.username?.charAt(0).toUpperCase()}
+                  </AvatarFallback>
+                </Avatar>
+                
+                <div className="text-center space-y-2">
+                  <h1 className="text-3xl font-bold">{user.nickname || user.username}</h1>
+                  <p className="text-muted-foreground">@{user.username}</p>
+                </div>
+
+                <div className="flex items-center space-x-4">
+                  <div className="text-center">
+                    <p className="text-2xl font-bold">0</p>
+                    <p className="text-sm text-muted-foreground">内容</p>
+                  </div>
+                  <Separator orientation="vertical" className="h-8" />
+                  <div className="text-center">
+                    <p className="text-2xl font-bold">0</p>
+                    <p className="text-sm text-muted-foreground">关注</p>
+                  </div>
+                  <Separator orientation="vertical" className="h-8" />
+                  <div className="text-center">
+                    <p className="text-2xl font-bold">0</p>
+                    <p className="text-sm text-muted-foreground">粉丝</p>
+                  </div>
+                </div>
+
+                <div className="flex items-center space-x-2">
+                  <Button 
+                    onClick={() => navigate('/profile/edit')}
+                    className="flex items-center space-x-2"
+                  >
+                    <UserCog className="h-4 w-4" />
+                    <span>编辑资料</span>
+                  </Button>
+                  
+                  <Button 
+                    variant="outline"
+                    onClick={async () => {
+                      await logout();
+                      navigate('/');
+                    }}
+                    className="flex items-center space-x-2"
+                  >
+                    <LogOut className="h-4 w-4" />
+                    <span>退出登录</span>
+                  </Button>
+                </div>
               </div>
-              <div className="text-center">
-                <p className="text-2xl font-semibold text-gray-900">0</p>
-                <p className="text-sm text-gray-500">关注</p>
+            </CardContent>
+          </Card>
+
+          {/* 个人资料详情 */}
+          <Card className="border-0 shadow-lg">
+            <CardHeader>
+              <CardTitle className="flex items-center space-x-2">
+                <User className="h-5 w-5" />
+                <span>个人资料</span>
+              </CardTitle>
+            </CardHeader>
+            <CardContent className="space-y-6">
+              <div className="grid gap-4">
+                <div className="space-y-1">
+                  <div className="flex items-center space-x-2 text-sm text-muted-foreground">
+                    <User className="h-4 w-4" />
+                    <span>用户名</span>
+                  </div>
+                  <p className="font-medium">{user.username}</p>
+                </div>
+
+                <div className="space-y-1">
+                  <div className="flex items-center space-x-2 text-sm text-muted-foreground">
+                    <ShieldCheck className="h-4 w-4" />
+                    <span>邮箱</span>
+                  </div>
+                  <p className="font-medium">{user.email || '未设置'}</p>
+                </div>
+
+                {(user as any).location && (
+                  <div className="space-y-1">
+                    <div className="flex items-center space-x-2 text-sm text-muted-foreground">
+                      <MapPin className="h-4 w-4" />
+                      <span>位置</span>
+                    </div>
+                    <p className="font-medium">{(user as any).location}</p>
+                  </div>
+                )}
+
+                {(user as any).website && (
+                  <div className="space-y-1">
+                    <div className="flex items-center space-x-2 text-sm text-muted-foreground">
+                      <Globe className="h-4 w-4" />
+                      <span>网站</span>
+                    </div>
+                    <a 
+                      href={(user as any).website}
+                      target="_blank"
+                      rel="noopener noreferrer"
+                      className="font-medium text-primary hover:underline"
+                    >
+                      {(user as any).website}
+                    </a>
+                  </div>
+                )}
+
+                {(user as any).bio && (
+                  <div className="space-y-1">
+                    <div className="flex items-center space-x-2 text-sm text-muted-foreground">
+                      <User className="h-4 w-4" />
+                      <span>个人简介</span>
+                    </div>
+                    <p className="font-medium">{(user as any).bio}</p>
+                  </div>
+                )}
               </div>
-              <div className="text-center">
-                <p className="text-2xl font-semibold text-gray-900">0</p>
-                <p className="text-sm text-gray-500">粉丝</p>
+
+              <Separator />
+
+              <div className="grid gap-4 md:grid-cols-2">
+                <div className="space-y-1">
+                  <div className="flex items-center space-x-2 text-sm text-muted-foreground">
+                    <Calendar className="h-4 w-4" />
+                    <span>注册时间</span>
+                  </div>
+                  <p className="font-medium">
+                    {user.createdAt ? format(new Date(user.createdAt), 'yyyy年MM月dd日', { locale: zhCN }) : '未知'}
+                  </p>
+                </div>
+
+                <div className="space-y-1">
+                  <div className="flex items-center space-x-2 text-sm text-muted-foreground">
+                    <Clock className="h-4 w-4" />
+                    <span>最后登录</span>
+                  </div>
+                  <p className="font-medium">
+                    {user.updatedAt ? format(new Date(user.updatedAt), 'yyyy年MM月dd日 HH:mm', { locale: zhCN }) : '从未登录'}
+                  </p>
+                </div>
               </div>
-            </div>
-            
-            <div className="flex">
-              <button
-                onClick={() => navigate('/profile/edit')}
-                className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 flex items-center"
+            </CardContent>
+          </Card>
+
+          {/* 设置区域 */}
+          <Card className="border-0 shadow-lg">
+            <CardHeader>
+              <CardTitle className="flex items-center space-x-2">
+                <Settings className="h-5 w-5" />
+                <span>账号设置</span>
+              </CardTitle>
+            </CardHeader>
+            <CardContent className="space-y-4">
+              <Button 
+                variant="outline" 
+                className="w-full justify-start"
+                onClick={() => navigate('/profile/security')}
               >
-                <PencilIcon className="w-4 h-4 mr-2" />
-                编辑资料
-              </button>
+                <ShieldCheck className="h-4 w-4 mr-2" />
+                <span>安全设置</span>
+              </Button>
               
-              <button
-                onClick={async () => {
-                  await logout();
-                  navigate('/');
-                }}
-                className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ml-4"
+              <Button 
+                variant="outline" 
+                className="w-full justify-start"
+                onClick={() => navigate('/profile/preferences')}
               >
-                退出登录
-              </button>
-
-            </div>
-            
-            {(user as any).bio && (
-              <p className="mt-4 text-center text-gray-600 max-w-lg">
-                {(user as any).bio}
-              </p>
-            )}
-            
-            <div className="flex items-center mt-4 space-x-4">
-              {(user as any).location && (
-                <div className="flex items-center text-gray-600">
-                  <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
-                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
-                  </svg>
-                  <span className="text-sm">{(user as any).location}</span>
-                </div>
-              )}
-              {(user as any).website && (
-                <a
-                  href={(user as any).website}
-                  target="_blank"
-                  rel="noopener noreferrer"
-                  className="flex items-center text-blue-600 hover:text-blue-800"
-                >
-                  <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6v6m0 0v6m0-6h-6" />
-                  </svg>
-                  <span className="text-sm truncate max-w-[150px]">{(user as any).website}</span>
-                </a>
-              )}
-            </div>
-          </div>
-        </div>
-        
-        {/* 用户内容区域 */}
-        <div className="bg-white rounded-lg shadow-sm p-6">
-          <h2 className="text-xl font-semibold mb-6">个人资料</h2>
-          
-          <div className="space-y-4">
-            <div className="border-b border-gray-100 pb-4">
-              <h3 className="text-sm font-medium text-gray-500 mb-1">用户名</h3>
-              <p className="text-gray-900">{user.username}</p>
-            </div>
-            
-            <div className="border-b border-gray-100 pb-4">
-              <h3 className="text-sm font-medium text-gray-500 mb-1">电子邮箱</h3>
-              <p className="text-gray-900">{user.email || '未设置'}</p>
-            </div>
-            
-            <div className="border-b border-gray-100 pb-4">
-              <h3 className="text-sm font-medium text-gray-500 mb-1">注册时间</h3>
-              <p className="text-gray-900">{user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '未知'}</p>
-            </div>
-            
-            <div className="border-b border-gray-100 pb-4">
-              <h3 className="text-sm font-medium text-gray-500 mb-1">最后登录</h3>
-              <p className="text-gray-900">{user.updatedAt ? new Date(user.updatedAt).toLocaleString() : '从未登录'}</p>
-            </div>
-          </div>
-          
-          <div className="mt-8">
-            <button
-              onClick={() => navigate('/profile/security')}
-              className="w-full py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
-            >
-              安全设置
-            </button>
-          </div>
+                <Settings className="h-4 w-4 mr-2" />
+                <span>偏好设置</span>
+              </Button>
+            </CardContent>
+          </Card>
         </div>
       </div>
     </div>

+ 141 - 153
src/client/home/pages/RegisterPage.tsx

@@ -1,24 +1,49 @@
-import React, { useState } from 'react';
+import React from 'react';
 import { useForm } from 'react-hook-form';
-import { EyeIcon, EyeSlashIcon, UserIcon, LockClosedIcon } from '@heroicons/react/24/outline';
-import { useNavigate } from 'react-router-dom';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { z } from 'zod';
+import { Link, useNavigate } from 'react-router-dom';
 import { useAuth } from '@/client/home/hooks/AuthProvider';
+import { toast } from 'sonner';
 import { authClient } from '@/client/api';
 
+import { Button } from '@/client/components/ui/button';
+import { Input } from '@/client/components/ui/input';
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/client/components/ui/card';
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
+import { Eye, EyeOff, User, Lock } from 'lucide-react';
+
+const registerSchema = z.object({
+  username: z.string()
+    .min(3, '用户名至少3个字符')
+    .max(20, '用户名不能超过20个字符')
+    .regex(/^[a-zA-Z0-9_-]+$/, '用户名只能包含字母、数字、下划线和连字符'),
+  password: z.string()
+    .min(6, '密码至少6个字符')
+    .max(30, '密码不能超过30个字符'),
+  confirmPassword: z.string(),
+}).refine((data) => data.password === data.confirmPassword, {
+  message: '两次密码输入不一致',
+  path: ['confirmPassword'],
+});
+
+type RegisterFormData = z.infer<typeof registerSchema>;
+
 const RegisterPage: React.FC = () => {
-  const { register, handleSubmit, watch, formState: { errors } } = useForm();
-  const [showPassword, setShowPassword] = useState(false);
-  const [showConfirmPassword, setShowConfirmPassword] = useState(false);
-  const [loading, setLoading] = useState(false);
   const { login } = useAuth();
   const navigate = useNavigate();
-  const password = watch('password', '');
+  
+  const form = useForm<RegisterFormData>({
+    resolver: zodResolver(registerSchema),
+    defaultValues: {
+      username: '',
+      password: '',
+      confirmPassword: '',
+    },
+  });
 
-  const onSubmit = async (data: any) => {
+  const onSubmit = async (data: RegisterFormData) => {
     try {
-      setLoading(true);
-      
-      // 调用注册API
       const response = await authClient.register.$post({
         json: {
           username: data.username,
@@ -33,156 +58,119 @@ const RegisterPage: React.FC = () => {
       
       // 注册成功后自动登录
       await login(data.username, data.password);
-      
-      // 跳转到首页
+      toast.success('注册成功!正在为您登录...');
       navigate('/');
     } catch (error) {
-      console.error('Registration error:', error);
-      alert((error as Error).message || '注册失败,请稍后重试');
-    } finally {
-      setLoading(false);
+      toast.error(error instanceof Error ? error.message : '注册失败,请稍后重试');
     }
   };
 
   return (
-    <div className="flex justify-center items-center min-h-screen bg-gray-100">
-      <div className="w-full max-w-md bg-white rounded-lg shadow-md overflow-hidden">
-        <div className="p-6 sm:p-8">
-          <div className="text-center mb-8">
-            <h2 className="text-2xl font-bold text-gray-900">账号注册</h2>
-            <p className="mt-2 text-sm text-gray-600">创建新账号以开始使用</p>
-          </div>
-          
-          <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
-            <div>
-              <label htmlFor="username" className="block text-sm font-medium text-gray-700 mb-1">
-                用户名
-              </label>
-              <div className="relative">
-                <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
-                  <UserIcon className="h-5 w-5 text-gray-400" />
-                </div>
-                <input
-                  id="username"
-                  type="text"
-                  className={`w-full pl-10 pr-3 py-2 border ${errors.username ? 'border-red-300' : 'border-gray-300'} rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent`}
-                  placeholder="请输入用户名"
-                  {...register('username', { 
-                    required: '用户名不能为空',
-                    minLength: { value: 3, message: '用户名至少3个字符' },
-                    maxLength: { value: 20, message: '用户名不能超过20个字符' }
-                  })}
-                />
-              </div>
-              {errors.username && (
-                <p className="mt-1 text-sm text-red-600">{errors.username.message?.toString()}</p>
-              )}
-            </div>
-            
-            <div>
-              <label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
-                密码
-              </label>
-              <div className="relative">
-                <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
-                  <LockClosedIcon className="h-5 w-5 text-gray-400" />
-                </div>
-                <input
-                  id="password"
-                  type={showPassword ? 'text' : 'password'}
-                  className={`w-full pl-10 pr-10 py-2 border ${errors.password ? 'border-red-300' : 'border-gray-300'} rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent`}
-                  placeholder="请输入密码"
-                  {...register('password', { 
-                    required: '密码不能为空',
-                    minLength: { value: 6, message: '密码至少6个字符' },
-                    maxLength: { value: 30, message: '密码不能超过30个字符' }
-                  })}
-                />
-                <button 
-                  type="button"
-                  className="absolute inset-y-0 right-0 pr-3 flex items-center"
-                  onClick={() => setShowPassword(!showPassword)}
-                >
-                  {showPassword ? (
-                    <EyeSlashIcon className="h-5 w-5 text-gray-400" />
-                  ) : (
-                    <EyeIcon className="h-5 w-5 text-gray-400" />
-                  )}
-                </button>
-              </div>
-              {errors.password && (
-                <p className="mt-1 text-sm text-red-600">{errors.password.message?.toString()}</p>
-              )}
-            </div>
-            
-            <div>
-              <label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 mb-1">
-                确认密码
-              </label>
-              <div className="relative">
-                <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
-                  <LockClosedIcon className="h-5 w-5 text-gray-400" />
-                </div>
-                <input
-                  id="confirmPassword"
-                  type={showConfirmPassword ? 'text' : 'password'}
-                  className={`w-full pl-10 pr-10 py-2 border ${errors.confirmPassword ? 'border-red-300' : 'border-gray-300'} rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent`}
-                  placeholder="请再次输入密码"
-                  {...register('confirmPassword', { 
-                    required: '请确认密码',
-                    validate: value => value === password || '两次密码输入不一致'
-                  })}
-                />
-                <button 
-                  type="button"
-                  className="absolute inset-y-0 right-0 pr-3 flex items-center"
-                  onClick={() => setShowConfirmPassword(!showConfirmPassword)}
-                >
-                  {showConfirmPassword ? (
-                    <EyeSlashIcon className="h-5 w-5 text-gray-400" />
-                  ) : (
-                    <EyeIcon className="h-5 w-5 text-gray-400" />
-                  )}
-                </button>
-              </div>
-              {errors.confirmPassword && (
-                <p className="mt-1 text-sm text-red-600">{errors.confirmPassword.message?.toString()}</p>
-              )}
-            </div>
-            
-            <div>
-              <button
-                type="submit"
-                disabled={loading}
-                className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
+    <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 px-4 py-12">
+      <Card className="w-full max-w-md border-0 shadow-xl">
+        <CardHeader className="space-y-1">
+          <CardTitle className="text-2xl font-bold text-center">创建账号</CardTitle>
+          <CardDescription className="text-center">
+            填写以下信息创建新账号
+          </CardDescription>
+        </CardHeader>
+        
+        <CardContent>
+          <Form {...form}>
+            <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
+              <FormField
+                control={form.control}
+                name="username"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>用户名</FormLabel>
+                    <FormControl>
+                      <div className="relative">
+                        <User className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
+                        <Input
+                          placeholder="请输入用户名"
+                          className="pl-10"
+                          {...field}
+                        />
+                      </div>
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <FormField
+                control={form.control}
+                name="password"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>密码</FormLabel>
+                    <FormControl>
+                      <div className="relative">
+                        <Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
+                        <Input
+                          type="password"
+                          placeholder="请输入密码"
+                          className="pl-10"
+                          {...field}
+                        />
+                      </div>
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <FormField
+                control={form.control}
+                name="confirmPassword"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>确认密码</FormLabel>
+                    <FormControl>
+                      <div className="relative">
+                        <Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
+                        <Input
+                          type="password"
+                          placeholder="请再次输入密码"
+                          className="pl-10"
+                          {...field}
+                        />
+                      </div>
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <Button 
+                type="submit" 
+                className="w-full"
+                disabled={form.formState.isSubmitting}
               >
-                {loading ? '注册中...' : '注册'}
-              </button>
-            </div>
-          </form>
+                {form.formState.isSubmitting ? '注册中...' : '注册账号'}
+              </Button>
+            </form>
+          </Form>
+        </CardContent>
+
+        <CardFooter className="flex flex-col space-y-4">
+          <div className="text-sm text-center">
+            <span className="text-muted-foreground">已有账号?</span>
+            <Button
+              variant="link"
+              className="px-1"
+              asChild
+            >
+              <Link to="/login">立即登录</Link>
+            </Button>
+          </div>
           
-          <div className="mt-6">
-            <div className="relative">
-              <div className="absolute inset-0 flex items-center">
-                <div className="w-full border-t border-gray-300"></div>
-              </div>
-              <div className="relative flex justify-center text-sm">
-                <span className="px-2 bg-white text-gray-500">已有账号?</span>
-              </div>
-            </div>
-            
-            <div className="mt-4">
-              <button
-                type="button"
-                onClick={() => navigate('/login')}
-                className="w-full flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
-              >
-                返回登录
-              </button>
-            </div>
+          <div className="text-xs text-center text-muted-foreground">
+            <p>注册即表示您同意我们的服务条款</p>
           </div>
-        </div>
-      </div>
+        </CardFooter>
+      </Card>
     </div>
   );
 };

+ 2 - 2
src/client/index.tsx

@@ -1,6 +1,6 @@
 // 如果当前是在 /big 下
 if (window.location.pathname.startsWith('/admin')) {
-  import('./admin-shadcn/index')
+  import('./admin/index')
 } else {
-  import('./home-shadcn/index')
+  import('./home/index')
 }

+ 204 - 125
src/server/utils/generic-crud.routes.ts

@@ -6,6 +6,7 @@ import { AuthContext } from '../types/context';
 import { ObjectLiteral } from 'typeorm';
 import { AppDataSource } from '../data-source';
 import { parseWithAwait } from './parseWithAwait';
+import { parseWithAwait } from './parseWithAwait';
 
 export function createCrudRoutes<
   T extends ObjectLiteral,
@@ -14,7 +15,7 @@ export function createCrudRoutes<
   GetSchema extends z.ZodSchema = z.ZodSchema,
   ListSchema extends z.ZodSchema = z.ZodSchema
 >(options: CrudOptions<T, CreateSchema, UpdateSchema, GetSchema, ListSchema>) {
-  const { entity, createSchema, updateSchema, getSchema, listSchema, searchFields, relations, middleware = [], userTracking, relationFields } = options;
+  const { entity, createSchema, updateSchema, getSchema, listSchema, searchFields, relations, middleware = [], userTracking, relationFields, readOnly = false } = options;
   
   // 创建CRUD服务实例
   // 抽象类不能直接实例化,需要创建具体实现类
@@ -35,11 +36,11 @@ export function createCrudRoutes<
     middleware,
     request: {
       query: z.object({
-        page: z.coerce.number().int().positive().default(1).openapi({
+        page: z.coerce.number<number>().int().positive().default(1).openapi({
           example: 1,
           description: '页码,从1开始'
         }),
-        pageSize: z.coerce.number().int().positive().default(10).openapi({
+        pageSize: z.coerce.number<number>().int().positive().default(10).openapi({
           example: 10,
           description: '每页数量'
         }),
@@ -218,135 +219,213 @@ export function createCrudRoutes<
   });
   
   // 注册路由处理函数
-  const routes = app
-    .openapi(listRoute, async (c) => {
-      try {
-        const query = c.req.valid('query') as any;
-        const { page, pageSize, keyword, sortBy, sortOrder, filters } = query;
-        
-        // 构建排序对象
-        const order: any = {};
-        if (sortBy) {
-          order[sortBy] = sortOrder || 'DESC';
-        } else {
-          order['id'] = 'DESC';
-        }
-        
-        // 解析筛选条件
-        let parsedFilters: any = undefined;
-        if (filters) {
-          try {
-            parsedFilters = JSON.parse(filters);
-          } catch (e) {
-            return c.json({ code: 400, message: '筛选条件格式错误' }, 400);
+  
+  // 只读模式下只注册 GET 路由
+  if (!readOnly) {
+    // 完整 CRUD 路由
+    const routes = app
+      .openapi(listRoute, async (c) => {
+        try {
+          const query = c.req.valid('query') as any;
+          const { page, pageSize, keyword, sortBy, sortOrder, filters } = query;
+          
+          // 构建排序对象
+          const order: any = {};
+          if (sortBy) {
+            order[sortBy] = sortOrder || 'DESC';
+          } else {
+            order['id'] = 'DESC';
           }
+          
+          // 解析筛选条件
+          let parsedFilters: any = undefined;
+          if (filters) {
+            try {
+              parsedFilters = JSON.parse(filters);
+            } catch (e) {
+              return c.json({ code: 400, message: '筛选条件格式错误' }, 400);
+            }
+          }
+          
+          const [data, total] = await crudService.getList(
+            page,
+            pageSize,
+            keyword,
+            searchFields,
+            undefined,
+            relations || [],
+            order,
+            parsedFilters
+          );
+          
+          return c.json({
+            // data: z.array(listSchema).parse(data),
+            data: await parseWithAwait(z.array(listSchema), data),
+            pagination: { total, current: page, pageSize }
+          }, 200);
+        } catch (error) {
+          if (error instanceof z.ZodError) {
+            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+          }
+          return c.json({
+            code: 500,
+            message: error instanceof Error ? error.message : '获取列表失败'
+          }, 500);
         }
-        
-        const [data, total] = await crudService.getList(
-          page,
-          pageSize,
-          keyword,
-          searchFields,
-          undefined,
-          relations || [],
-          order,
-          parsedFilters
-        );
-        
-        return c.json({
-          // data: z.array(listSchema).parse(data),
-          data: await parseWithAwait(z.array(listSchema), data),
-          pagination: { total, current: page, pageSize }
-        }, 200);
-      } catch (error) {
-        if (error instanceof z.ZodError) {
-          return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
-        }
-        return c.json({
-          code: 500,
-          message: error instanceof Error ? error.message : '获取列表失败'
-        }, 500);
-      }
-    })
-    .openapi(createRouteDef, async (c: any) => {
-      try {
-        const data = c.req.valid('json');
-        const user = c.get('user');
-        const result = await crudService.create(data, user?.id);
-        return c.json(result, 201);
-      } catch (error) {
-        if (error instanceof z.ZodError) {
-          return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
-        }
-        return c.json({
-          code: 500,
-          message: error instanceof Error ? error.message : '创建资源失败'
-        }, 500);
-      }
-    })
-    .openapi(getRouteDef, async (c: any) => {
-      try {
-        const { id } = c.req.valid('param');
-        const result = await crudService.getById(id, relations || []);
-        
-        if (!result) {
-          return c.json({ code: 404, message: '资源不存在' }, 404);
+      })
+      .openapi(createRouteDef, async (c: any) => {
+        try {
+          const data = c.req.valid('json');
+          const user = c.get('user');
+          const result = await crudService.create(data, user?.id);
+          return c.json(result, 201);
+        } catch (error) {
+          if (error instanceof z.ZodError) {
+            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+          }
+          return c.json({
+            code: 500,
+            message: error instanceof Error ? error.message : '创建资源失败'
+          }, 500);
         }
-        
-        // return c.json(await getSchema.parseAsync(result), 200);
-        return c.json(await parseWithAwait(getSchema, result), 200);
-      } catch (error) {
-        if (error instanceof z.ZodError) {
-          return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+      })
+      .openapi(getRouteDef, async (c: any) => {
+        try {
+          const { id } = c.req.valid('param');
+          const result = await crudService.getById(id, relations || []);
+          
+          if (!result) {
+            return c.json({ code: 404, message: '资源不存在' }, 404);
+          }
+          
+          // return c.json(await getSchema.parseAsync(result), 200);
+          return c.json(await parseWithAwait(getSchema, result), 200);
+        } catch (error) {
+          if (error instanceof z.ZodError) {
+            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+          }
+          return c.json({
+            code: 500,
+            message: error instanceof Error ? error.message : '获取资源失败'
+          }, 500);
         }
-        return c.json({
-          code: 500,
-          message: error instanceof Error ? error.message : '获取资源失败'
-        }, 500);
-      }
-    })
-    .openapi(updateRouteDef, async (c: any) => {
-      try {
-        const { id } = c.req.valid('param');
-        const data = c.req.valid('json');
-        const user = c.get('user');
-        const result = await crudService.update(id, data, user?.id);
-        
-        if (!result) {
-          return c.json({ code: 404, message: '资源不存在' }, 404);
+      })
+      .openapi(updateRouteDef, async (c: any) => {
+        try {
+          const { id } = c.req.valid('param');
+          const data = c.req.valid('json');
+          const user = c.get('user');
+          const result = await crudService.update(id, data, user?.id);
+          
+          if (!result) {
+            return c.json({ code: 404, message: '资源不存在' }, 404);
+          }
+          
+          return c.json(result, 200);
+        } catch (error) {
+          if (error instanceof z.ZodError) {
+            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+          }
+          return c.json({
+            code: 500,
+            message: error instanceof Error ? error.message : '更新资源失败'
+          }, 500);
         }
-        
-        return c.json(result, 200);
-      } catch (error) {
-        if (error instanceof z.ZodError) {
-          return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+      })
+      .openapi(deleteRouteDef, async (c: any) => {
+        try {
+          const { id } = c.req.valid('param');
+          const success = await crudService.delete(id);
+          
+          if (!success) {
+            return c.json({ code: 404, message: '资源不存在' }, 404);
+          }
+          
+          return c.body(null, 204);
+        } catch (error) {
+          if (error instanceof z.ZodError) {
+            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+          }
+          return c.json({
+            code: 500,
+            message: error instanceof Error ? error.message : '删除资源失败'
+          }, 500);
         }
-        return c.json({
-          code: 500,
-          message: error instanceof Error ? error.message : '更新资源失败'
-        }, 500);
-      }
-    })
-    .openapi(deleteRouteDef, async (c: any) => {
-      try {
-        const { id } = c.req.valid('param');
-        const success = await crudService.delete(id);
-        
-        if (!success) {
-          return c.json({ code: 404, message: '资源不存在' }, 404);
+      });
+
+    return routes;
+  } else {
+    // 只读模式,只注册 GET 路由
+    const routes = app
+      .openapi(listRoute, async (c) => {
+        try {
+          const query = c.req.valid('query') as any;
+          const { page, pageSize, keyword, sortBy, sortOrder, filters } = query;
+          
+          // 构建排序对象
+          const order: any = {};
+          if (sortBy) {
+            order[sortBy] = sortOrder || 'DESC';
+          } else {
+            order['id'] = 'DESC';
+          }
+          
+          // 解析筛选条件
+          let parsedFilters: any = undefined;
+          if (filters) {
+            try {
+              parsedFilters = JSON.parse(filters);
+            } catch (e) {
+              return c.json({ code: 400, message: '筛选条件格式错误' }, 400);
+            }
+          }
+          
+          const [data, total] = await crudService.getList(
+            page,
+            pageSize,
+            keyword,
+            searchFields,
+            undefined,
+            relations || [],
+            order,
+            parsedFilters
+          );
+          
+          return c.json({
+            data: await parseWithAwait(z.array(listSchema), data),
+            pagination: { total, current: page, pageSize }
+          }, 200);
+        } catch (error) {
+          if (error instanceof z.ZodError) {
+            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+          }
+          return c.json({
+            code: 500,
+            message: error instanceof Error ? error.message : '获取列表失败'
+          }, 500);
         }
-        
-        return c.body(null, 204);
-      } catch (error) {
-        if (error instanceof z.ZodError) {
-          return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+      })
+      .openapi(getRouteDef, async (c: any) => {
+        try {
+          const { id } = c.req.valid('param');
+          const result = await crudService.getById(id, relations || []);
+          
+          if (!result) {
+            return c.json({ code: 404, message: '资源不存在' }, 404);
+          }
+          
+          return c.json(await parseWithAwait(getSchema, result), 200);
+        } catch (error) {
+          if (error instanceof z.ZodError) {
+            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+          }
+          return c.json({
+            code: 500,
+            message: error instanceof Error ? error.message : '获取资源失败'
+          }, 500);
         }
-        return c.json({
-          code: 500,
-          message: error instanceof Error ? error.message : '删除资源失败'
-        }, 500);
-      }
-    });
+      });
+    return routes;
+  }
   
-  return routes;
 }

+ 52 - 17
src/server/utils/generic-crud.service.ts

@@ -39,24 +39,43 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
     const query = this.repository.createQueryBuilder('entity');
 
     // 添加关联关系(支持嵌套关联,如 ['contract.client'])
+    // 使用一致的别名生成策略,确保搜索时能正确引用关联字段
     if (relations.length > 0) {
-      relations.forEach((relation, relationIndex) => {
+      relations.forEach((relation) => {
         const parts = relation.split('.');
         let currentAlias = 'entity';
         
         parts.forEach((part, index) => {
-          const newAlias = index === 0 ? part : `${currentAlias}_${relationIndex}`;
+          // 生成一致的别名:对于嵌套关联,使用下划线连接路径
+          const newAlias = index === 0 ? part : parts.slice(0, index + 1).join('_');
           query.leftJoinAndSelect(`${currentAlias}.${part}`, newAlias);
           currentAlias = newAlias;
         });
       });
     }
 
-    // 关键词搜索
+    // 关键词搜索 - 支持关联字段搜索(格式:relation.field 或 relation.nestedRelation.field)
     if (keyword && searchFields && searchFields.length > 0) {
-      query.andWhere(searchFields.map(field => `entity.${field} LIKE :keyword`).join(' OR '), {
-        keyword: `%${keyword}%`
+      const searchConditions: string[] = [];
+      const searchParams: Record<string, string> = { keyword: `%${keyword}%` };
+
+      searchFields.forEach((field) => {
+        // 检查是否为关联字段(包含点号)
+        if (field.includes('.')) {
+          const parts = field.split('.');
+          const alias = parts.slice(0, -1).join('_'); // 使用下划线连接关系路径作为别名
+          const fieldName = parts[parts.length - 1];
+          
+          searchConditions.push(`${alias}.${fieldName} LIKE :keyword`);
+        } else {
+          // 普通字段搜索
+          searchConditions.push(`entity.${field} LIKE :keyword`);
+        }
       });
+
+      if (searchConditions.length > 0) {
+        query.andWhere(`(${searchConditions.join(' OR ')})`, searchParams);
+      }
     }
 
     // 条件查询
@@ -74,38 +93,48 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
         if (value !== undefined && value !== null && value !== '') {
           const fieldName = key.startsWith('_') ? key.substring(1) : key;
           
+          // 检查是否为关联字段(包含点号)
+          let tableAlias = 'entity';
+          let actualFieldName = fieldName;
+          
+          if (fieldName.includes('.')) {
+            const parts = fieldName.split('.');
+            tableAlias = parts.slice(0, -1).join('_'); // 使用下划线连接关系路径作为别名
+            actualFieldName = parts[parts.length - 1];
+          }
+          
           // 支持不同类型的筛选
           if (Array.isArray(value)) {
             // 数组类型:IN查询
             if (value.length > 0) {
-              query.andWhere(`entity.${fieldName} IN (:...${key})`, { [key]: value });
+              query.andWhere(`${tableAlias}.${actualFieldName} IN (:...${key})`, { [key]: value });
             }
           } else if (typeof value === 'string' && value.includes('%')) {
             // 模糊匹配
-            query.andWhere(`entity.${fieldName} LIKE :${key}`, { [key]: value });
+            query.andWhere(`${tableAlias}.${actualFieldName} LIKE :${key}`, { [key]: value });
           } else if (typeof value === 'object' && value !== null) {
             // 范围查询
             if ('gte' in value) {
-              query.andWhere(`entity.${fieldName} >= :${key}_gte`, { [`${key}_gte`]: value.gte });
+              query.andWhere(`${tableAlias}.${actualFieldName} >= :${key}_gte`, { [`${key}_gte`]: value.gte });
             }
             if ('gt' in value) {
-              query.andWhere(`entity.${fieldName} > :${key}_gt`, { [`${key}_gt`]: value.gt });
+              query.andWhere(`${tableAlias}.${actualFieldName} > :${key}_gt`, { [`${key}_gt`]: value.gt });
             }
             if ('lte' in value) {
-              query.andWhere(`entity.${fieldName} <= :${key}_lte`, { [`${key}_lte`]: value.lte });
+              query.andWhere(`${tableAlias}.${actualFieldName} <= :${key}_lte`, { [`${key}_lte`]: value.lte });
             }
             if ('lt' in value) {
-              query.andWhere(`entity.${fieldName} < :${key}_lt`, { [`${key}_lt`]: value.lt });
+              query.andWhere(`${tableAlias}.${actualFieldName} < :${key}_lt`, { [`${key}_lt`]: value.lt });
             }
             if ('between' in value && Array.isArray(value.between) && value.between.length === 2) {
-              query.andWhere(`entity.${fieldName} BETWEEN :${key}_start AND :${key}_end`, {
+              query.andWhere(`${tableAlias}.${actualFieldName} BETWEEN :${key}_start AND :${key}_end`, {
                 [`${key}_start`]: value.between[0],
                 [`${key}_end`]: value.between[1]
               });
             }
           } else {
             // 精确匹配
-            query.andWhere(`entity.${fieldName} = :${key}`, { [key]: value });
+            query.andWhere(`${tableAlias}.${actualFieldName} = :${key}`, { [key]: value });
           }
         }
       });
@@ -116,7 +145,11 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
       query.orderBy(`entity.${key}`, direction);
     });
 
-    return query.skip(skip).take(pageSize).getManyAndCount();
+    const finalQuery = query.skip(skip).take(pageSize);
+
+    // console.log(finalQuery.getSql())
+
+    return finalQuery.getManyAndCount();
   }
 
   /**
@@ -143,12 +176,13 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
       userIdField = 'userId'
     } = this.userTrackingOptions;
 
-    // 设置创建人和更新人
-    if (isCreate && createdByField && data.hasOwnProperty(createdByField)) {
+    // 设置创建人
+    if (isCreate && createdByField) {
       data[createdByField] = userId;
     }
 
-    if (updatedByField && data.hasOwnProperty(updatedByField)) {
+    // 设置更新人
+    if (updatedByField) {
       data[updatedByField] = userId;
     }
 
@@ -289,4 +323,5 @@ export type CrudOptions<
   middleware?: any[];
   userTracking?: UserTrackingOptions;
   relationFields?: RelationFieldOptions;
+  readOnly?: boolean;
 };