|
|
@@ -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';
|