# 管理后台开发规范

## 版本信息
| 版本 | 日期 | 描述 | 作者 |
|------|------|------|------|
| 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<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
// 后端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
// 标准时间输入字段
<FormField
  control={form.control}
  name="startDate"
  render={({ field }) => (
    <FormItem>
      <FormLabel>开始时间 *</FormLabel>
      <FormControl>
        <Input
          type="datetime-local"
          {...field}
        />
      </FormControl>
      <FormDescription>
        活动的开始时间
      </FormDescription>
      <FormMessage />
    </FormItem>
  )}
/>
```

#### 时间显示规范
```typescript
// 表格中的时间显示
<TableCell>
  {new Date(activity.startDate).toLocaleString('zh-CN')}
</TableCell>

// 详情页中的时间显示
<div className="text-sm text-muted-foreground">
  创建时间: {format(new Date(entity.createdAt), 'yyyy-MM-dd HH:mm:ss')}
</div>
```

## 搜索和筛选规范

### 搜索功能
```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();
});
```

## RPC Client 使用规范

### 客户端创建和配置

#### 客户端导入
```typescript
// 从统一的API模块导入客户端
import { userClient, roleClient, fileClient } from '@/client/api';
import type { InferRequestType, InferResponseType } from 'hono/client';
```

#### 类型提取规范
```typescript
// 使用InferResponseType提取响应类型
type UserResponse = InferResponseType<typeof userClient.$get, 200>['data'][0];
type UserListResponse = InferResponseType<typeof userClient.$get, 200>;

// 使用InferRequestType提取请求类型
type CreateUserRequest = InferRequestType<typeof userClient.$post>['json'];
type UpdateUserRequest = InferRequestType<typeof userClient[':id']['$put']>['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<any>) => {
  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<string, unknown>);
```

### 性能优化

#### 查询缓存配置
```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 🏗️