admin-dashboard-standards.md 12 KB

管理后台开发规范

版本信息

版本 日期 描述 作者
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通知

页面开发规范

页面结构标准

每个管理后台页面应遵循以下结构:

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);
};

组件使用规范

shadcn/ui组件使用

按钮组件

// 标准按钮使用
<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>

自定义组件开发

组件命名规范

  • 使用PascalCase命名
  • 文件名与组件名一致
  • 导出命名组件

组件Props定义

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>

布局规范

  • 桌面端:侧边栏 + 主内容区
  • 移动端:汉堡菜单 + 全屏内容
  • 断点:sm(640px), md(768px), lg(1024px), xl(1280px)

性能优化规范

代码分割

// 使用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();
  });
});

E2E测试

// 使用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类型

文件组织

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 🏗️