|
@@ -0,0 +1,528 @@
|
|
|
|
|
+# 管理后台开发规范
|
|
|
|
|
+
|
|
|
|
|
+## 版本信息
|
|
|
|
|
+| 版本 | 日期 | 描述 | 作者 |
|
|
|
|
|
+|------|------|------|------|
|
|
|
|
|
+| 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<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>
|
|
|
|
|
+ );
|
|
|
|
|
+};
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 数据表格规范
|
|
|
|
|
+
|
|
|
|
|
+#### 表格结构要求
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 标准表格结构
|
|
|
|
|
+<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>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 分页组件使用
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 使用标准分页组件
|
|
|
|
|
+<DataTablePagination
|
|
|
|
|
+ currentPage={searchParams.page}
|
|
|
|
|
+ totalCount={data?.pagination?.total || 0}
|
|
|
|
|
+ pageSize={searchParams.limit}
|
|
|
|
|
+ onPageChange={(page, limit) => 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
|
|
|
|
|
+// 防抖搜索实现
|
|
|
|
|
+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 <ErrorPage statusCode={403} />;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return <EntityPage />;
|
|
|
|
|
+};
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 操作级别权限控制
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 条件渲染操作按钮
|
|
|
|
|
+{hasPermission('entity:create') && (
|
|
|
|
|
+ <Button onClick={handleCreate}>
|
|
|
|
|
+ 创建实体
|
|
|
|
|
+ </Button>
|
|
|
|
|
+)}
|
|
|
|
|
+
|
|
|
|
|
+{hasPermission('entity:delete') && (
|
|
|
|
|
+ <Button variant="ghost" size="icon" onClick={handleDelete}>
|
|
|
|
|
+ <Trash2 className="h-4 w-4" />
|
|
|
|
|
+ </Button>
|
|
|
|
|
+)}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 用户体验规范
|
|
|
|
|
+
|
|
|
|
|
+### 加载状态处理
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 表格骨架屏
|
|
|
|
|
+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()}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 错误处理
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 统一错误处理
|
|
|
|
|
+const handleOperation = async (operation: () => Promise<any>) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await operation();
|
|
|
|
|
+ toast.success('操作成功');
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('操作失败:', error);
|
|
|
|
|
+ toast.error('操作失败,请重试');
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 确认对话框
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 删除确认对话框
|
|
|
|
|
+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);
|
|
|
|
|
+};
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 组件使用规范
|
|
|
|
|
+
|
|
|
|
|
+### shadcn/ui组件使用
|
|
|
|
|
+
|
|
|
|
|
+#### 按钮组件
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 标准按钮使用
|
|
|
|
|
+<Button variant="default" size="default">主要按钮</Button>
|
|
|
|
|
+<Button variant="outline" size="sm">次要按钮</Button>
|
|
|
|
|
+<Button variant="ghost" size="icon">图标按钮</Button>
|
|
|
|
|
+<Button variant="destructive">危险操作</Button>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 表单组件
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 表单字段标准结构
|
|
|
|
|
+<FormField
|
|
|
|
|
+ control={form.control}
|
|
|
|
|
+ name="fieldName"
|
|
|
|
|
+ render={({ field }) => (
|
|
|
|
|
+ <FormItem>
|
|
|
|
|
+ <FormLabel>字段标签</FormLabel>
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <Input placeholder="请输入内容" {...field} />
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ <FormMessage />
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+/>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 对话框组件
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 标准对话框使用
|
|
|
|
|
+<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>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 自定义组件开发
|
|
|
|
|
+
|
|
|
|
|
+#### 组件命名规范
|
|
|
|
|
+- 使用PascalCase命名
|
|
|
|
|
+- 文件名与组件名一致
|
|
|
|
|
+- 导出命名组件
|
|
|
|
|
+
|
|
|
|
|
+#### 组件Props定义
|
|
|
|
|
+```typescript
|
|
|
|
|
+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
|
|
|
|
|
+}) => {
|
|
|
|
|
+ // 组件实现
|
|
|
|
|
+};
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 响应式设计规范
|
|
|
|
|
+
|
|
|
|
|
+### 移动端适配
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 使用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>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 布局规范
|
|
|
|
|
+- 桌面端:侧边栏 + 主内容区
|
|
|
|
|
+- 移动端:汉堡菜单 + 全屏内容
|
|
|
|
|
+- 断点:sm(640px), md(768px), lg(1024px), xl(1280px)
|
|
|
|
|
+
|
|
|
|
|
+## 性能优化规范
|
|
|
|
|
+
|
|
|
|
|
+### 代码分割
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 使用React.lazy进行代码分割
|
|
|
|
|
+const LazyComponent = React.lazy(() => import('./LazyComponent'));
|
|
|
|
|
+
|
|
|
|
|
+// 在路由中使用
|
|
|
|
|
+<React.Suspense fallback={<div>加载中...</div>}>
|
|
|
|
|
+ <LazyComponent />
|
|
|
|
|
+</React.Suspense>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 数据缓存
|
|
|
|
|
+```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(<EntityPage />);
|
|
|
|
|
+ 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();
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 代码质量规范
|
|
|
|
|
+
|
|
|
|
|
+### 代码风格
|
|
|
|
|
+- 使用ESLint + Prettier进行代码格式化
|
|
|
|
|
+- 遵循TypeScript严格模式
|
|
|
|
|
+- 使用函数组件和Hooks
|
|
|
|
|
+- 避免使用any类型
|
|
|
|
|
+
|
|
|
|
|
+### 文件组织
|
|
|
|
|
+```text
|
|
|
|
|
+src/client/admin/
|
|
|
|
|
+├── components/ # 管理后台专用组件
|
|
|
|
|
+├── hooks/ # 管理后台Hooks
|
|
|
|
|
+├── layouts/ # 布局组件
|
|
|
|
|
+├── pages/ # 页面组件
|
|
|
|
|
+├── utils/ # 工具函数
|
|
|
|
|
+└── types/ # 类型定义
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 部署和构建规范
|
|
|
|
|
+
|
|
|
|
|
+### 环境变量
|
|
|
|
|
+```bash
|
|
|
|
|
+# 前端环境变量
|
|
|
|
|
+VITE_API_BASE_URL=http://localhost:3000/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 🏗️
|