# 管理后台开发规范 ## 版本信息 | 版本 | 日期 | 描述 | 作者 | |------|------|------|------| | 1.1 | 2025-10-16 | 补充日期时间处理规范,修复表单时间显示问题 | Winston | | 1.0 | 2025-10-16 | 初始版本,基于现有管理后台实现 | Winston | ## 概述 本文档定义了出行服务项目管理后台的开发标准和最佳实践,确保所有管理后台功能开发的一致性和可维护性。 ## 技术栈要求 ### 核心框架 - **React**: 19.1.0+,使用函数组件和Hooks - **TypeScript**: 严格模式,类型安全优先 - **React Router**: v7,声明式路由管理 ### 状态管理 - **@tanstack/react-query**: 服务端状态管理 - **React Context**: 本地状态和全局状态 - **React Hook Form**: 表单状态管理 ### UI组件库 - **shadcn/ui**: 基于Radix UI的组件库 - **Tailwind CSS**: 4.1.11+,原子化样式 - **Lucide React**: 图标库 ### 开发工具 - **Vite**: 7.0.0+,构建工具 - **Zod**: 表单验证和类型定义 - **Sonner**: Toast通知 ## 页面开发规范 ### 页面结构标准 每个管理后台页面应遵循以下结构: ```typescript import React, { useState, useMemo } from 'react'; import { useQuery, useMutation } from '@tanstack/react-query'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { toast } from 'sonner'; // 1. 导入必要的组件和工具 import { Button } from '@/client/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/client/components/ui/card'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/client/components/ui/table'; // 2. 定义类型(使用后端Schema生成的类型) type EntityResponse = InferResponseType['data'][0]; // 3. 页面组件定义 export const EntityPage = () => { // 4. 状态管理 const [searchParams, setSearchParams] = useState({ page: 1, limit: 10, keyword: '' }); // 5. 数据获取 const { data, isLoading, refetch } = useQuery({ queryKey: ['entities', searchParams], queryFn: async () => { const res = await entityClient.$get({ query: searchParams }); if (res.status !== 200) throw new Error('获取数据失败'); return await res.json(); } }); // 6. 渲染逻辑 return (
{/* 页面标题和操作按钮 */}

实体管理

{/* 内容区域 */} 实体列表 {/* 表格或列表内容 */}
); }; ``` ### 数据表格规范 #### 表格结构要求 ```typescript // 标准表格结构 ID 名称 状态 操作 {data?.data?.map((item) => ( {item.id} {item.name} {item.status === 'active' ? '启用' : '禁用'}
))}
``` #### 分页组件使用 ```typescript // 使用标准分页组件 setSearchParams(prev => ({ ...prev, page, limit }))} /> ``` ### 表单处理规范 #### 表单验证 ```typescript // 使用Zod Schema进行表单验证 const formSchema = z.object({ name: z.string().min(1, '名称不能为空'), email: z.string().email('请输入有效的邮箱地址'), phone: z.string().optional(), }); const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { name: '', email: '', phone: '', }, }); ``` #### 表单提交 ```typescript // 标准表单提交处理 const handleSubmit = async (data: FormData) => { try { const res = await entityClient.$post({ json: data }); if (res.status !== 201) throw new Error('创建失败'); toast.success('创建成功'); refetch(); // 重新获取数据 } catch { toast.error('操作失败,请重试'); } }; ``` ### 日期时间处理规范 #### 时间格式标准 ```typescript // 后端Schema定义 - 使用z.coerce.date()确保时间字段保持为Date对象 const activitySchema = z.object({ startDate: z.coerce.date(), endDate: z.coerce.date(), createdAt: z.coerce.date(), updatedAt: z.coerce.date() }); // 前端表单定义 - 使用datetime-local输入框 const formSchema = z.object({ startDate: z.string().datetime('开始日期格式不正确'), endDate: z.string().datetime('结束日期格式不正确') }); ``` #### 时间格式转换函数 ```typescript import { format } from 'date-fns'; // 将ISO日期时间格式化为 datetime-local 输入框需要的格式 const formatDateTimeForInput = (dateString: string): string => { const date = new Date(dateString); // 使用 date-fns 转换为 YYYY-MM-DDTHH:mm 格式 return format(date, "yyyy-MM-dd'T'HH:mm"); }; // 将表单时间转换为ISO格式 const formatDateTimeForAPI = (dateTimeString: string): string => { const date = new Date(dateTimeString); return date.toISOString(); }; ``` #### 表单中的时间字段使用 ```typescript // 表单默认值设置 - 正确处理时间格式 const form = useForm({ resolver: zodResolver(formSchema), defaultValues: initialData ? { name: initialData.name || '', startDate: initialData.startDate ? formatDateTimeForInput(initialData.startDate) : '', endDate: initialData.endDate ? formatDateTimeForInput(initialData.endDate) : '', } : { name: '', startDate: '', endDate: '', } }); // 表单提交前的时间格式转换 const handleSubmit = async (data: FormData) => { const submitData = { ...data, startDate: formatDateTimeForAPI(data.startDate), endDate: formatDateTimeForAPI(data.endDate) }; await entityClient.$post({ json: submitData }); }; ``` #### 时间输入组件 ```typescript // 标准时间输入字段 ( 开始时间 * 活动的开始时间 )} /> ``` #### 时间显示规范 ```typescript // 表格中的时间显示 {new Date(activity.startDate).toLocaleString('zh-CN')} // 详情页中的时间显示
创建时间: {format(new Date(entity.createdAt), 'yyyy-MM-dd HH:mm:ss')}
``` ## 搜索和筛选规范 ### 搜索功能 ```typescript // 防抖搜索实现 const debounce = (func: Function, delay: number) => { let timeoutId: NodeJS.Timeout; return (...args: any[]) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => func(...args), delay); }; }; const debouncedSearch = useCallback( debounce((keyword: string) => { setSearchParams(prev => ({ ...prev, keyword, page: 1 })); }, 300), [] ); ``` ### 高级筛选 ```typescript // 筛选状态管理 const [filters, setFilters] = useState({ status: undefined as string | undefined, category: [] as number[], dateRange: undefined as { start?: string; end?: string } | undefined }); // 筛选条件显示 const hasActiveFilters = useMemo(() => { return filters.status !== undefined || filters.category.length > 0 || filters.dateRange !== undefined; }, [filters]); ``` ## 权限控制规范 ### 基于角色的访问控制 ```typescript // 权限检查Hook const usePermission = () => { const { user } = useAuth(); const hasPermission = (permission: string) => { return user?.roles?.some(role => role.permissions.includes(permission) ) ?? false; }; return { hasPermission }; }; // 页面级别权限控制 const ProtectedPage = () => { const { hasPermission } = usePermission(); if (!hasPermission('page:entity:view')) { return ; } return ; }; ``` ### 操作级别权限控制 ```typescript // 条件渲染操作按钮 {hasPermission('entity:create') && ( )} {hasPermission('entity:delete') && ( )} ``` ## 用户体验规范 ### 加载状态处理 ```typescript // 表格骨架屏 const renderTableSkeleton = () => (
{Array.from({ length: 5 }).map((_, index) => (
))}
); // 使用 {isLoading ? renderTableSkeleton() : renderTableContent()} ``` ### 错误处理 ```typescript // 统一错误处理 const handleOperation = async (operation: () => Promise) => { try { await operation(); toast.success('操作成功'); } catch (error) { console.error('操作失败:', error); toast.error('操作失败,请重试'); } }; ``` ### 确认对话框 ```typescript // 删除确认对话框 const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [itemToDelete, setItemToDelete] = useState(null); const handleDelete = (id: number) => { setItemToDelete(id); setDeleteDialogOpen(true); }; const confirmDelete = async () => { if (!itemToDelete) return; await handleOperation(async () => { const res = await entityClient[':id'].$delete({ param: { id: itemToDelete } }); if (res.status !== 204) throw new Error('删除失败'); }); setDeleteDialogOpen(false); setItemToDelete(null); }; ``` ## 组件使用规范 ### shadcn/ui组件使用 #### 按钮组件 ```typescript // 标准按钮使用 ``` #### 表单组件 ```typescript // 表单字段标准结构 ( 字段标签 )} /> ``` #### 对话框组件 ```typescript // 标准对话框使用 对话框标题 对话框描述 {/* 对话框内容 */} ``` ### 自定义组件开发 #### 组件命名规范 - 使用PascalCase命名 - 文件名与组件名一致 - 导出命名组件 #### 组件Props定义 ```typescript interface CustomComponentProps { value?: string; onChange?: (value: string) => void; placeholder?: string; disabled?: boolean; className?: string; } export const CustomComponent: React.FC = ({ value, onChange, placeholder, disabled = false, className }) => { // 组件实现 }; ``` ## 响应式设计规范 ### 移动端适配 ```typescript // 使用Tailwind响应式类
{/* 内容 */}
// 移动端菜单 {/* 移动端菜单内容 */} ``` ### 布局规范 - 桌面端:侧边栏 + 主内容区 - 移动端:汉堡菜单 + 全屏内容 - 断点:sm(640px), md(768px), lg(1024px), xl(1280px) ## 性能优化规范 ### 代码分割 ```typescript // 使用React.lazy进行代码分割 const LazyComponent = React.lazy(() => import('./LazyComponent')); // 在路由中使用 加载中...}> ``` ### 数据缓存 ```typescript // React Query缓存配置 const { data } = useQuery({ queryKey: ['entities', searchParams], queryFn: fetchEntities, staleTime: 5 * 60 * 1000, // 5分钟 cacheTime: 10 * 60 * 1000, // 10分钟 }); ``` ## 测试规范 ### 组件测试 ```typescript // 使用Testing Library进行组件测试 import { render, screen, fireEvent } from '@testing-library/react'; import { EntityPage } from './EntityPage'; describe('EntityPage', () => { it('should render entity list', () => { render(); expect(screen.getByText('实体管理')).toBeInTheDocument(); }); }); ``` ### E2E测试 ```typescript // 使用Playwright进行E2E测试 test('should create new entity', async ({ page }) => { await page.goto('/admin/entities'); await page.click('button:has-text("创建实体")'); await page.fill('input[name="name"]', '测试实体'); await page.click('button:has-text("创建")'); await expect(page.locator('text=创建成功')).toBeVisible(); }); ``` ## RPC Client 使用规范 ### 客户端创建和配置 #### 客户端导入 ```typescript // 从统一的API模块导入客户端 import { userClient, roleClient, fileClient } from '@/client/api'; import type { InferRequestType, InferResponseType } from 'hono/client'; ``` #### 类型提取规范 ```typescript // 使用InferResponseType提取响应类型 type UserResponse = InferResponseType['data'][0]; type UserListResponse = InferResponseType; // 使用InferRequestType提取请求类型 type CreateUserRequest = InferRequestType['json']; type UpdateUserRequest = InferRequestType['json']; ``` ### API 调用规范 #### GET 请求 ```typescript // 基础查询 const { data, isLoading, refetch } = useQuery({ queryKey: ['users', searchParams], queryFn: async () => { const res = await userClient.$get({ query: { page: searchParams.page, pageSize: searchParams.limit, keyword: searchParams.keyword, filters: hasActiveFilters ? JSON.stringify(filters) : undefined } }); if (res.status !== 200) throw new Error('获取数据失败'); return await res.json(); } }); // 带路径参数的查询 const { data } = useQuery({ queryKey: ['user', userId], queryFn: async () => { const res = await userClient[':id'].$get({ param: { id: userId } }); if (res.status !== 200) throw new Error('获取用户详情失败'); return await res.json(); } }); ``` #### POST 请求 ```typescript // 创建资源 const handleCreate = async (data: CreateUserRequest) => { try { const res = await userClient.$post({ json: data }); if (res.status !== 201) throw new Error('创建失败'); toast.success('创建成功'); refetch(); // 重新获取数据 } catch { toast.error('创建失败,请重试'); } }; ``` #### PUT 请求 ```typescript // 更新资源 const handleUpdate = async (id: number, data: UpdateUserRequest) => { try { const res = await userClient[':id']['$put']({ param: { id }, json: data }); if (res.status !== 200) throw new Error('更新失败'); toast.success('更新成功'); refetch(); } catch { toast.error('更新失败,请重试'); } }; ``` #### DELETE 请求 ```typescript // 删除资源 const handleDelete = async (id: number) => { try { const res = await userClient[':id']['$delete']({ param: { id } }); if (res.status !== 204) throw new Error('删除失败'); toast.success('删除成功'); refetch(); } catch { toast.error('删除失败,请重试'); } }; ``` ### 错误处理规范 #### 统一错误处理模式 ```typescript // 统一操作处理函数 const handleOperation = async (operation: () => Promise) => { try { await operation(); toast.success('操作成功'); } catch (error) { console.error('操作失败:', error); toast.error('操作失败,请重试'); } }; // 使用示例 const handleCreateUser = async (data: CreateUserRequest) => { await handleOperation(async () => { const res = await userClient.$post({ json: data }); if (res.status !== 201) throw new Error('创建失败'); }); }; ``` #### 状态码检查 ```typescript // 标准状态码检查模式 const checkStatus = (res: Response, expectedStatus: number) => { if (res.status !== expectedStatus) { throw new Error(`操作失败,状态码: ${res.status}`); } }; // 使用示例 const res = await userClient.$post({ json: data }); checkStatus(res, 201); // 期望201 Created ``` ### 查询参数规范 #### 分页参数 ```typescript const searchParams = { page: 1, // 当前页码 pageSize: 10, // 每页数量 keyword: '' // 搜索关键词 }; ``` #### 筛选参数 ```typescript const filters = { status: undefined as string | undefined, category: [] as number[], dateRange: undefined as { start?: string; end?: string } | undefined }; // 将筛选条件转换为查询参数 const filterParams = Object.keys(filters).reduce((acc, key) => { const value = filters[key as keyof typeof filters]; if (value !== undefined && value !== null && (!Array.isArray(value) || value.length > 0)) { acc[key] = value; } return acc; }, {} as Record); ``` ### 性能优化 #### 查询缓存配置 ```typescript const { data } = useQuery({ queryKey: ['users', searchParams, filters], queryFn: fetchUsers, staleTime: 5 * 60 * 1000, // 5分钟 cacheTime: 10 * 60 * 1000, // 10分钟 refetchOnWindowFocus: false // 避免窗口聚焦时重复请求 }); ``` #### 防抖搜索 ```typescript // 防抖函数 const debounce = (func: Function, delay: number) => { let timeoutId: NodeJS.Timeout; return (...args: any[]) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => func(...args), delay); }; }; // 防抖搜索 const debouncedSearch = useCallback( debounce((keyword: string) => { setSearchParams(prev => ({ ...prev, keyword, page: 1 })); }, 300), [] ); ``` ## 代码质量规范 ### 代码风格 - 使用ESLint + Prettier进行代码格式化 - 遵循TypeScript严格模式 - 使用函数组件和Hooks - 避免使用any类型 ### 文件组织 ```text src/client/admin/ ├── components/ # 管理后台专用组件 ├── hooks/ # 管理后台Hooks ├── layouts/ # 布局组件 ├── pages/ # 页面组件 ├── utils/ # 工具函数 └── types/ # 类型定义 ``` ## 部署和构建规范 ### 环境变量 ```bash # 前端环境变量 VITE_API_BASE_URL=http://localhost:8080/api VITE_APP_NAME=管理后台 ``` ### 构建配置 ```typescript // vite.config.ts export default defineConfig({ build: { rollupOptions: { output: { manualChunks: { vendor: ['react', 'react-dom'], ui: ['@/client/components/ui'] } } } } }); ``` --- **文档状态**: 正式版 **下次评审**: 2025-11-16 **维护者**: Winston 🏗️