| 版本 | 日期 | 描述 | 作者 |
|---|---|---|---|
| 1.0 | 2025-10-16 | 初始版本,基于现有管理后台实现 | Winston |
本文档定义了出行服务项目管理后台的开发标准和最佳实践,确保所有管理后台功能开发的一致性和可维护性。
每个管理后台页面应遵循以下结构:
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<typeof entityClient.$get, 200>['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 (
<div className="space-y-4">
{/* 页面标题和操作按钮 */}
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold">实体管理</h1>
<Button>创建实体</Button>
</div>
{/* 内容区域 */}
<Card>
<CardHeader>
<CardTitle>实体列表</CardTitle>
</CardHeader>
<CardContent>
{/* 表格或列表内容 */}
</CardContent>
</Card>
</div>
);
};
// 标准表格结构
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>名称</TableHead>
<TableHead>状态</TableHead>
<TableHead className="text-right">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data?.data?.map((item) => (
<TableRow key={item.id}>
<TableCell className="font-medium">{item.id}</TableCell>
<TableCell>{item.name}</TableCell>
<TableCell>
<Badge variant={item.status === 'active' ? 'default' : 'secondary'}>
{item.status === 'active' ? '启用' : '禁用'}
</Badge>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button variant="ghost" size="icon">
<Edit className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" className="text-red-600">
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
// 使用标准分页组件
<DataTablePagination
currentPage={searchParams.page}
totalCount={data?.pagination?.total || 0}
pageSize={searchParams.limit}
onPageChange={(page, limit) => setSearchParams(prev => ({ ...prev, page, limit }))}
/>
// 使用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: '',
},
});
// 标准表单提交处理
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('操作失败,请重试');
}
};
// 防抖搜索实现
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),
[]
);
// 筛选状态管理
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]);
// 权限检查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 <ErrorPage statusCode={403} />;
}
return <EntityPage />;
};
// 条件渲染操作按钮
{hasPermission('entity:create') && (
<Button onClick={handleCreate}>
创建实体
</Button>
)}
{hasPermission('entity:delete') && (
<Button variant="ghost" size="icon" onClick={handleDelete}>
<Trash2 className="h-4 w-4" />
</Button>
)}
// 表格骨架屏
const renderTableSkeleton = () => (
<div className="space-y-2">
{Array.from({ length: 5 }).map((_, index) => (
<div key={index} className="flex space-x-4">
<Skeleton className="h-4 flex-1" />
<Skeleton className="h-4 flex-1" />
<Skeleton className="h-4 flex-1" />
</div>
))}
</div>
);
// 使用
{isLoading ? renderTableSkeleton() : renderTableContent()}
// 统一错误处理
const handleOperation = async (operation: () => Promise<any>) => {
try {
await operation();
toast.success('操作成功');
} catch (error) {
console.error('操作失败:', error);
toast.error('操作失败,请重试');
}
};
// 删除确认对话框
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [itemToDelete, setItemToDelete] = useState<number | null>(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);
};
// 标准按钮使用
<Button variant="default" size="default">主要按钮</Button>
<Button variant="outline" size="sm">次要按钮</Button>
<Button variant="ghost" size="icon">图标按钮</Button>
<Button variant="destructive">危险操作</Button>
// 表单字段标准结构
<FormField
control={form.control}
name="fieldName"
render={({ field }) => (
<FormItem>
<FormLabel>字段标签</FormLabel>
<FormControl>
<Input placeholder="请输入内容" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
// 标准对话框使用
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>对话框标题</DialogTitle>
<DialogDescription>对话框描述</DialogDescription>
</DialogHeader>
{/* 对话框内容 */}
<DialogFooter>
<Button variant="outline" onClick={() => setIsOpen(false)}>取消</Button>
<Button onClick={handleConfirm}>确认</Button>
</DialogFooter>
</DialogContent>
</Dialog>
interface CustomComponentProps {
value?: string;
onChange?: (value: string) => void;
placeholder?: string;
disabled?: boolean;
className?: string;
}
export const CustomComponent: React.FC<CustomComponentProps> = ({
value,
onChange,
placeholder,
disabled = false,
className
}) => {
// 组件实现
};
// 使用Tailwind响应式类
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* 内容 */}
</div>
// 移动端菜单
<Sheet open={isMobileMenuOpen} onOpenChange={setIsMobileMenuOpen}>
<SheetContent side="left" className="w-64 p-0">
{/* 移动端菜单内容 */}
</SheetContent>
</Sheet>
// 使用React.lazy进行代码分割
const LazyComponent = React.lazy(() => import('./LazyComponent'));
// 在路由中使用
<React.Suspense fallback={<div>加载中...</div>}>
<LazyComponent />
</React.Suspense>
// React Query缓存配置
const { data } = useQuery({
queryKey: ['entities', searchParams],
queryFn: fetchEntities,
staleTime: 5 * 60 * 1000, // 5分钟
cacheTime: 10 * 60 * 1000, // 10分钟
});
// 使用Testing Library进行组件测试
import { render, screen, fireEvent } from '@testing-library/react';
import { EntityPage } from './EntityPage';
describe('EntityPage', () => {
it('should render entity list', () => {
render(<EntityPage />);
expect(screen.getByText('实体管理')).toBeInTheDocument();
});
});
// 使用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();
});
src/client/admin/
├── components/ # 管理后台专用组件
├── hooks/ # 管理后台Hooks
├── layouts/ # 布局组件
├── pages/ # 页面组件
├── utils/ # 工具函数
└── types/ # 类型定义
# 前端环境变量
VITE_API_BASE_URL=http://localhost:8080/api
VITE_APP_NAME=管理后台
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
ui: ['@/client/components/ui']
}
}
}
}
});
文档状态: 正式版 下次评审: 2025-11-16 维护者: Winston 🏗️