Przeglądaj źródła

📝 docs(admin): 更新管理后台开发规范和路由配置

- 更新通用CRUD指令文档,明确按照管理后台界面开发规范创建页面
- 重写管理后台界面开发规范,新增类型安全表单、表单状态管理等核心特性
- 添加表单开发模板,包括基础结构、各类表单字段和对话框集成示例
- 新增领券日志菜单和路由配置,使用Ticket图标标识
- 完善表单最佳实践,补充组件导入清单和完整实现参考

📝 docs(menu): 添加领券日志菜单

- 新增coupon-logs菜单项,使用Ticket图标
- 设置路径为/admin/coupon-logs,权限为coupon:view

📝 docs(routes): 添加领券日志页面路由

- 新增coupon-logs路由配置,指向CouponLogsPage组件
- 配置错误处理页面为ErrorPage
yourname 6 miesięcy temu
rodzic
commit
0a52b79c2e

+ 1 - 1
.roo/commands/generic-crud.md

@@ -11,5 +11,5 @@ description: "通用curd开发指令"
 创建通用CRUD路由
 注册路由到API
 创建客户端API调用方法
-创建管理后台领券日志页面
+创建管理后台页面,按照 管理后台界面开发规范
 注册路由和菜单

+ 239 - 167
.roo/rules/13-ui-style.md

@@ -1,169 +1,241 @@
 # 管理后台界面开发规范
 
-## 1. 布局规范
-
-### 1.1 整体布局结构
-- 采用三栏布局:侧边导航栏 + 顶部操作栏 + 主内容区
-- 侧边栏固定宽度240px,支持折叠功能
-- 顶部导航栏高度固定为64px
-- 主内容区边距统一为24px
-
-### 1.2 响应式设计
-- 桌面端:完整三栏布局
-- 平板端:可折叠侧边栏
-- 移动端:侧边栏转为抽屉式导航
-
-### 1.3 容器样式
-- 卡片容器使用白色背景(#ffffff)
-- 卡片阴影使用 `shadow-sm transition-all duration-300 hover:shadow-md`
-- 卡片边框使用 `border: none`
-- 卡片圆角统一为 `border-radius: 6px`
-
-## 2. 色彩规范
-
-### 2.1 主色调
-- 主色:蓝色(#1890ff),用于主要按钮、选中状态和关键交互元素
-- 辅助色:绿色(#52c41a)用于成功状态,红色(#ff4d4f)用于错误状态,黄色(#faad14)用于警告状态
-
-### 2.2 中性色
-- 背景色:浅灰(#f5f5f5)用于页面背景,白色(#ffffff)用于卡片背景
-- 文本色:深灰(#1f2937)用于主要文本,中灰(#6b7280)用于次要文本,浅灰(#9ca3af)用于提示文本
-
-## 3. 组件样式规范
-
-### 3.1 按钮样式
-- 主要按钮:使用主色调背景,白色文字
-- 按钮高度统一为40px,大型按钮使用48px
-- 按钮圆角统一为4px
-- 按钮文本使用14px字体
-- 按钮添加悬停效果:`hover:shadow-lg transition-all duration-200`
-
-### 3.2 表单元素
-- 输入框高度统一为40px
-- 输入框前缀图标颜色使用主色调
-- 表单标签宽度统一为80px
-- 表单布局使用垂直布局,标签在上,输入框在下
-- 输入框聚焦状态:`focus:border-primary focus:ring-1 focus:ring-primary`
-
-### 3.5 日期表单组件
-- 日期选择器使用 `DatePicker` 组件,时间选择使用 `TimePicker` 组件
-- 日期选择器大小与输入框保持一致:`size="middle"`
-- 日期格式统一为 `YYYY-MM-DD`,时间格式为 `HH:mm:ss`
-- 日期范围选择使用 `RangePicker` 组件,格式为 `[YYYY-MM-DD, YYYY-MM-DD]`
-- 日期选择器添加清除按钮:`allowClear`
-- 日期选择器添加占位提示:`placeholder="请选择日期"`
-- 日期选择器禁用未来日期:`disabledDate={(current) => current && current > dayjs().endOf('day')}`(根据业务需求调整)
-- 日期对象规范:始终使用dayjs对象而非原生Date对象,避免出现"isValid is not a function"错误
-  ```typescript
-  // 错误示例 - 使用原生Date对象
-  form.setFieldsValue({
-    noteDate: new Date(record.noteDate) // 导致验证失败
-  });
-  
-  // 正确示例 - 使用dayjs对象
-  form.setFieldsValue({
-    noteDate: dayjs(record.noteDate) // 正常支持验证方法
-  });
-  ```
-- 日期时间转换规范:
-  ```typescript
-  // 日期对象转字符串(提交给后端)
-  const formatDate = (date: Dayjs | null) => {
-    return date ? date.format('YYYY-MM-DD') : '';
-  };
-  
-  // 字符串转日期对象(从后端接收)
-  const parseDate = (str: string) => {
-    return str ? dayjs(str) : null;
-  };
-  
-  // 日期时间对象转字符串
-  const formatDateTime = (date: Dayjs | null) => {
-    return date ? date.format('YYYY-MM-DD HH:mm:ss') : '';
-  };
-  
-  // 日期范围转换
-  const formatDateRange = (range: [Dayjs | null, Dayjs | null]) => {
-    return range && range[0] && range[1]
-      ? [range[0].format('YYYY-MM-DD'), range[1].format('YYYY-MM-DD')]
-      : [];
-  };
-  ```
-
-### 3.3 表格样式
-- 表格添加边框:`bordered`
-- 表头背景色使用浅灰(#f9fafb)
-- 表格行添加交替背景色:`rowClassName={(record, index) => index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}`
-- 支持横向滚动:`scroll={{ x: 'max-content' }}`
-
-### 3.4 卡片组件
-- 卡片标题区使用 `flex items-center justify-between` 布局
-- 统计数字使用28px字体大小
-- 添加卡片图标时使用24px大小,颜色与统计项主题匹配
-- 卡片底部添加辅助信息,使用12px浅灰色字体
-
-## 4. 页面规范
-
-### 4.1 页面标题
-- 页面标题使用 `Title level={2}` 组件
-- 标题区添加 `mb-6 flex justify-between items-center` 样式
-- 标题右侧可放置操作按钮组
-
-### 4.2 登录页面
-- 使用渐变背景:`bg-gradient-to-br from-blue-50 to-indigo-100`
-- 登录卡片居中显示,添加阴影效果:`shadow-lg`
-- 登录表单添加图标前缀增强可读性
-- 底部添加版权信息和测试账号提示
-
-### 4.3 数据展示页面
-- 数据卡片使用响应式布局,在不同屏幕尺寸下自动调整列数
-- 关键数据使用 `Statistic` 组件展示
-- 添加数据趋势指示和环比增长信息
-- 数据加载状态使用 `loading` 属性
-
-## 5. 交互规范
-
-### 5.1 悬停效果
-- 可交互元素添加悬停效果
-- 卡片悬停效果:`hover:shadow-md transition-all duration-300`
-- 按钮悬停效果:`hover:shadow-lg transition-all duration-200`
-
-### 5.2 模态框
-- 模态框使用 `destroyOnClose` 属性确保每次打开都是新实例
-- 模态框居中显示:`centered`
-- 禁止点击遮罩关闭:`maskClosable={false}`
-- 表单模态框使用垂直布局
-
-### 5.3 反馈机制
-- 操作成功/失败使用 `message` 组件提供反馈
-- 加载状态使用 `loading` 属性显示加载指示器
-- 删除等危险操作使用 `Popconfirm` 组件二次确认
-
-## 5.4 消息提示规范
-- 统一使用App.useApp()获取message实例
-  ```typescript
-  import { App } from 'antd';
-  const { message } = App.useApp();
-  ```
-- 消息提示使用明确的类型区分:
-  ```typescript
-  message.success('操作成功');
-  message.error('操作失败');
-  message.warning('警告信息');
-  message.info('提示信息');
-  ```
-- 消息显示时长统一使用默认值,重要操作可适当延长:`message.success('操作成功', 3);`
-
-## 6. 图标规范
-
-### 6.1 图标选择
-- 用户相关:UserOutlined
-- 密码相关:LockOutlined
-- 搜索相关:SearchOutlined
-- 消息相关:BellOutlined
-- 眼睛相关:EyeOutlined/EyeInvisibleOutlined
-
-### 6.2 图标样式
-- 功能图标大小统一为24px
-- 前缀图标颜色与主题匹配
-- 操作图标使用 `Button type="link"` 样式
+## 概述
+基于 `src/client/admin-shadcn/pages/Users.tsx` 中用户管理表单的实现,提取可复用的表单开发模式和最佳实践,适用于基于 Shadcn-ui 的管理后台表单开发。
+
+## 核心特性
+
+### 1. 类型安全表单
+- **后端Schema复用**:直接使用后端定义的 Zod Schema
+- **RPC类型提取**:从 Hono 客户端自动推断类型
+- **一致的类型定义**:前后端类型完全同步
+
+### 2. 表单状态管理
+- **创建/编辑模式切换**:单一表单处理两种状态
+- **智能默认值**:根据模式自动设置表单初始值
+- **表单重置**:模式切换时自动重置表单状态
+
+### 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 [editingData, setEditingData] = useState<any>(null);
+
+const createForm = useForm<CreateType>({
+  resolver: zodResolver(createSchema),
+  defaultValues: {/* 创建时的默认值 */},
+});
+
+const updateForm = useForm<UpdateType>({
+  resolver: zodResolver(updateSchema),
+  defaultValues: {/* 更新时的默认值 */},
+});
+```
+
+### 表单字段模板
+
+#### 文本输入框
+```typescript
+<FormField
+  control={form.control}
+  name="username"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel className="flex items-center">
+        用户名
+        <span className="text-red-500 ml-1">*</span>
+      </FormLabel>
+      <FormControl>
+        <Input placeholder="请输入用户名" {...field} />
+      </FormControl>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+#### 邮箱输入框
+```typescript
+<FormField
+  control={form.control}
+  name="email"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel>邮箱</FormLabel>
+      <FormControl>
+        <Input type="email" placeholder="请输入邮箱" {...field} />
+      </FormControl>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+#### 密码输入框
+```typescript
+<FormField
+  control={form.control}
+  name="password"
+  render={({ field }) => (
+    <FormItem>
+      <FormLabel className="flex items-center">
+        密码
+        <span className="text-red-500 ml-1">*</span>
+      </FormLabel>
+      <FormControl>
+        <Input type="password" placeholder="请输入密码" {...field} />
+      </FormControl>
+      <FormMessage />
+    </FormItem>
+  )}
+/>
+```
+
+#### 开关控件(布尔值)
+```typescript
+<FormField
+  control={form.control}
+  name="isDisabled"
+  render={({ field }) => (
+    <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
+      <div className="space-y-0.5">
+        <FormLabel className="text-base">状态</FormLabel>
+        <FormDescription>
+          禁用后用户将无法登录系统
+        </FormDescription>
+      </div>
+      <FormControl>
+        <Switch
+          checked={field.value === 1}
+          onCheckedChange={(checked) => field.onChange(checked ? 1 : 0)}
+        />
+      </FormControl>
+    </FormItem>
+  )}
+/>
+```
+
+#### 可选字段处理
+```typescript
+// 创建时:必须提供值
+nickname: z.string().optional()
+
+// 更新时:完全可选
+nickname: z.string().optional()
+```
+
+### 对话框集成模板
+```typescript
+<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
+  <DialogContent className="sm:max-w-[500px]">
+    <DialogHeader>
+      <DialogTitle>{isCreateForm ? '创建' : '编辑'}用户</DialogTitle>
+      <DialogDescription>
+        {isCreateForm ? '创建新记录' : '编辑现有记录'}
+      </DialogDescription>
+    </DialogHeader>
+    
+    <Form {...(isCreateForm ? createForm : updateForm)}>
+      <form onSubmit={form.handleSubmit(isCreateForm ? handleCreate : handleUpdate)} className="space-y-4">
+        {/* 表单字段 */}
+        
+        <DialogFooter>
+          <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+            取消
+          </Button>
+          <Button type="submit">
+            {isCreateForm ? '创建' : '更新'}
+          </Button>
+        </DialogFooter>
+      </form>
+    </Form>
+  </DialogContent>
+</Dialog>
+```
+
+## 最佳实践
+
+### 1. 表单验证
+- 使用 Zod Schema 进行类型验证
+- 必填字段标记红色星号
+- 提供清晰的错误提示
+
+### 2. 用户体验
+- 表单提交时显示加载状态
+- 操作成功后显示 toast 通知
+- 支持键盘导航和提交
+
+### 3. 数据管理
+- 创建后自动刷新数据列表
+- 编辑时回填现有数据
+- 支持表单重置功能
+
+### 4. 响应式设计
+- 对话框最大宽度 `sm:max-w-[500px]`
+- 表单间距统一使用 `space-y-4`
+- 移动端友好的布局
+
+## 使用示例
+
+### 完整实现参考
+```typescript
+// 创建用户
+const handleCreateSubmit = async (data: CreateUserFormData) => {
+  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: UpdateUserFormData) => {
+  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 { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
+
+// 表单工具
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { toast } from 'sonner';

+ 9 - 1
src/client/admin-shadcn/menu.tsx

@@ -8,7 +8,8 @@ import {
   LogOut,
   BarChart3,
   LayoutDashboard,
-  File
+  File,
+  Ticket
 } from 'lucide-react';
 
 export interface MenuItem {
@@ -101,6 +102,13 @@ export const useMenu = () => {
       path: '/admin/analytics',
       permission: 'analytics:view'
     },
+    {
+      key: 'coupon-logs',
+      label: '领券日志',
+      icon: <Ticket className="h-4 w-4" />,
+      path: '/admin/coupon-logs',
+      permission: 'coupon:view'
+    },
     {
       key: 'settings',
       label: '系统设置',

+ 6 - 0
src/client/admin-shadcn/routes.tsx

@@ -8,6 +8,7 @@ import { DashboardPage } from './pages/Dashboard';
 import { UsersPage } from './pages/Users';
 import { LoginPage } from './pages/Login';
 import { FilesPage } from './pages/Files';
+import { default as CouponLogsPage } from './pages/CouponLogs';
 
 export const router = createBrowserRouter([
   {
@@ -45,6 +46,11 @@ export const router = createBrowserRouter([
         element: <FilesPage />,
         errorElement: <ErrorPage />
       },
+      {
+        path: 'coupon-logs',
+        element: <CouponLogsPage />,
+        errorElement: <ErrorPage />
+      },
       {
         path: '*',
         element: <NotFoundPage />,