Jelajahi Sumber

✨ feat(admin): 新增订单管理功能

- 在管理后台添加订单管理菜单和路由
- 实现订单列表页面,包含搜索、筛选、分页功能
- 添加订单统计面板,显示各状态订单数量
- 实现订单详情查看对话框
- 更新API客户端以支持订单相关接口
- 添加完整的集成测试用例

📝 docs(architecture): 更新枚举定义规范

- 在编码标准文档中添加枚举定义规范
- 要求所有业务枚举必须在共享类型文件中定义
- 更新CRUD规范文档,添加共享类型定义示例

♻️ refactor(orders): 重构订单模块枚举定义

- 将订单状态和支付状态枚举移至共享类型文件
- 更新订单实体、服务和schema的导入路径
- 确保前端可以安全导入共享枚举类型
yourname 3 bulan lalu
induk
melakukan
82d4a709ad

+ 1 - 0
docs/architecture/coding-standards.md

@@ -28,6 +28,7 @@
 - **CRUD开发**: 遵循 [通用CRUD规范](./generic-crud-standards.md) 进行开发
 - **CRUD开发**: 遵循 [通用CRUD规范](./generic-crud-standards.md) 进行开发
 - **实体设计**: 使用TypeORM装饰器定义字段,为所有字段添加 `comment` 配置,包含时间戳字段
 - **实体设计**: 使用TypeORM装饰器定义字段,为所有字段添加 `comment` 配置,包含时间戳字段
 - **Schema设计**: 创建、更新、响应使用不同的Zod schema
 - **Schema设计**: 创建、更新、响应使用不同的Zod schema
+- **枚举定义**: 所有业务枚举必须在共享类型文件中定义(如 `packages/server/src/share/*.types.ts`),避免在实体文件中定义,确保前端可安全导入
 - **路由生成**: 使用 `createCrudRoutes` 自动生成API路由
 - **路由生成**: 使用 `createCrudRoutes` 自动生成API路由
 - **用户跟踪**: 实现用户跟踪功能记录操作人信息
 - **用户跟踪**: 实现用户跟踪功能记录操作人信息
 
 

+ 24 - 1
docs/architecture/generic-crud-standards.md

@@ -127,15 +127,35 @@ export class User {
 }
 }
 ```
 ```
 
 
-### 2. 创建 Zod Schema
+### 2. 创建共享类型和 Zod Schema
+
+```typescript
+// packages/server/src/share/user.types.ts
+export enum UserStatus {
+  ACTIVE = 'active',
+  INACTIVE = 'inactive',
+  SUSPENDED = 'suspended'
+}
+
+export enum UserRole {
+  ADMIN = 'admin',
+  USER = 'user',
+  GUEST = 'guest'
+}
+
+// 其他共享类型定义...
+```
 
 
 ```typescript
 ```typescript
 // user.schema.ts
 // user.schema.ts
 import { z } from '@hono/zod-openapi';
 import { z } from '@hono/zod-openapi';
+import { UserStatus, UserRole } from '../../share/user.types';
 
 
 export const UserCreateSchema = z.object({
 export const UserCreateSchema = z.object({
   username: z.string().min(3).max(50),
   username: z.string().min(3).max(50),
   email: z.string().email().optional().nullable(),
   email: z.string().email().optional().nullable(),
+  status: z.nativeEnum(UserStatus).default(UserStatus.ACTIVE),
+  role: z.nativeEnum(UserRole).default(UserRole.USER)
 });
 });
 
 
 export const UserUpdateSchema = UserCreateSchema.partial();
 export const UserUpdateSchema = UserCreateSchema.partial();
@@ -144,6 +164,8 @@ export const UserGetSchema = z.object({
   id: z.number(),
   id: z.number(),
   username: z.string(),
   username: z.string(),
   email: z.string().email().nullable(),
   email: z.string().email().nullable(),
+  status: z.nativeEnum(UserStatus),
+  role: z.nativeEnum(UserRole),
   createdAt: z.string().datetime(),
   createdAt: z.string().datetime(),
   updatedAt: z.string().datetime(),
   updatedAt: z.string().datetime(),
 });
 });
@@ -251,6 +273,7 @@ searchFields: ['contract.client.name']
 - 创建和更新使用不同的 schema
 - 创建和更新使用不同的 schema
 - 响应 schema 应包含完整字段
 - 响应 schema 应包含完整字段
 - 使用 `.optional()` 和 `.nullable()` 明确字段可选性
 - 使用 `.optional()` 和 `.nullable()` 明确字段可选性
+- **枚举定义**: 所有业务枚举(如状态、类型等)必须在共享类型文件中定义(如 `packages/server/src/share/*.types.ts`),避免在实体文件中定义,以确保前端可以安全导入
 
 
 ### 3. 性能优化
 ### 3. 性能优化
 - 合理设置分页大小(默认10条)
 - 合理设置分页大小(默认10条)

+ 1 - 15
packages/server/src/modules/orders/order.entity.ts

@@ -1,21 +1,7 @@
 import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
 import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
 import { UserEntity } from '../users/user.entity';
 import { UserEntity } from '../users/user.entity';
 import { RouteEntity } from '../routes/route.entity';
 import { RouteEntity } from '../routes/route.entity';
-
-export enum OrderStatus {
-  PENDING_PAYMENT = '待支付',
-  WAITING_DEPARTURE = '待出发',
-  IN_PROGRESS = '行程中',
-  COMPLETED = '已完成',
-  CANCELLED = '已取消'
-}
-
-export enum PaymentStatus {
-  PENDING = '待支付',
-  PAID = '已支付',
-  FAILED = '支付失败',
-  REFUNDED = '已退款'
-}
+import { OrderStatus, PaymentStatus } from '../../share/order.types';
 
 
 @Entity('orders')
 @Entity('orders')
 export class Order {
 export class Order {

+ 1 - 1
packages/server/src/modules/orders/order.schema.ts

@@ -1,5 +1,5 @@
 import { z } from 'zod';
 import { z } from 'zod';
-import { OrderStatus, PaymentStatus } from './order.entity';
+import { OrderStatus, PaymentStatus } from '../../share/order.types';
 
 
 // 创建订单Schema
 // 创建订单Schema
 export const OrderCreateSchema = z.object({
 export const OrderCreateSchema = z.object({

+ 2 - 1
packages/server/src/modules/orders/order.service.ts

@@ -1,5 +1,6 @@
 import { AppDataSource } from '../../data-source';
 import { AppDataSource } from '../../data-source';
-import { Order, OrderStatus } from './order.entity';
+import { Order } from './order.entity';
+import { OrderStatus } from '../../share/order.types';
 import { OrderStats } from './order.schema';
 import { OrderStats } from './order.schema';
 
 
 export class OrderService {
 export class OrderService {

+ 15 - 2
packages/server/src/share/order.types.ts

@@ -1,7 +1,20 @@
-import { OrderStatus, PaymentStatus } from '../modules/orders/order.entity';
+export enum OrderStatus {
+  PENDING_PAYMENT = '待支付',
+  WAITING_DEPARTURE = '待出发',
+  IN_PROGRESS = '行程中',
+  COMPLETED = '已完成',
+  CANCELLED = '已取消'
+}
+
+export enum PaymentStatus {
+  PENDING = '待支付',
+  PAID = '已支付',
+  FAILED = '支付失败',
+  REFUNDED = '已退款'
+}
+
 import { OrderResponse, OrderStats } from '../modules/orders/order.schema';
 import { OrderResponse, OrderStats } from '../modules/orders/order.schema';
 
 
-export { OrderStatus, PaymentStatus };
 export type { OrderResponse, OrderStats };
 export type { OrderResponse, OrderStats };
 
 
 // 订单列表响应类型
 // 订单列表响应类型

+ 9 - 1
web/src/client/admin/menu.tsx

@@ -13,7 +13,8 @@ import {
   MapPin,
   MapPin,
   Globe,
   Globe,
   Map,
   Map,
-  Users2
+  Users2,
+  ShoppingCart
 } from 'lucide-react';
 } from 'lucide-react';
 
 
 export interface MenuItem {
 export interface MenuItem {
@@ -155,6 +156,13 @@ export const useMenu = () => {
       path: '/admin/files',
       path: '/admin/files',
       permission: 'file:manage'
       permission: 'file:manage'
     },
     },
+    {
+      key: 'orders',
+      label: '订单管理',
+      icon: <ShoppingCart className="h-4 w-4" />,
+      path: '/admin/orders',
+      permission: 'order:view'
+    },
     {
     {
       key: 'analytics',
       key: 'analytics',
       label: '数据分析',
       label: '数据分析',

+ 514 - 0
web/src/client/admin/pages/Orders.tsx

@@ -0,0 +1,514 @@
+import React, { useState, useMemo, useCallback } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { format } from 'date-fns';
+import { Search, Filter, X, Eye } from 'lucide-react';
+import { orderClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
+import { Button } from '@/client/components/ui/button';
+import { Input } from '@/client/components/ui/input';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/client/components/ui/table';
+import { Badge } from '@/client/components/ui/badge';
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
+import { DataTablePagination } from '@/client/admin/components/DataTablePagination';
+import { Skeleton } from '@/client/components/ui/skeleton';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/client/components/ui/select';
+import { OrderStatus, PaymentStatus } from '@d8d/server/share/order.types';
+
+// 使用RPC方式提取类型
+type OrderResponse = InferResponseType<typeof orderClient.$get, 200>['data'][0];
+
+export const OrdersPage = () => {
+  const [searchParams, setSearchParams] = useState({
+    page: 1,
+    pageSize: 10,
+    search: ''
+  });
+  const [filters, setFilters] = useState({
+    status: undefined as OrderStatus | undefined,
+    paymentStatus: undefined as PaymentStatus | undefined
+  });
+  const [showFilters, setShowFilters] = useState(false);
+  const [detailDialogOpen, setDetailDialogOpen] = useState(false);
+  const [selectedOrder, setSelectedOrder] = useState<OrderResponse | null>(null);
+
+  // 获取订单列表
+  const { data: ordersData, isLoading } = useQuery({
+    queryKey: ['orders', searchParams, filters],
+    queryFn: async () => {
+      const res = await orderClient.$get({
+        query: {
+          page: searchParams.page,
+          pageSize: searchParams.pageSize,
+          keyword: searchParams.search,
+          filters: JSON.stringify({
+            status: filters.status,
+            paymentStatus: filters.paymentStatus
+          })
+        }
+      });
+      if (res.status !== 200) {
+        throw new Error('获取订单列表失败');
+      }
+      return await res.json();
+    }
+  });
+
+  // 获取订单统计
+  const { data: statsData } = useQuery({
+    queryKey: ['order-stats'],
+    queryFn: async () => {
+      const res = await orderClient.stats.$get();
+      if (res.status !== 200) {
+        throw new Error('获取订单统计失败');
+      }
+      return await res.json();
+    }
+  });
+
+  const orders = ordersData?.data || [];
+  const totalCount = ordersData?.pagination?.total || 0;
+  const stats = statsData || {
+    total: 0,
+    pendingPayment: 0,
+    waitingDeparture: 0,
+    inProgress: 0,
+    completed: 0,
+    cancelled: 0
+  };
+
+  // 防抖搜索函数
+  const debounce = (func: Function, delay: number) => {
+    let timeoutId: NodeJS.Timeout;
+    return (...args: any[]) => {
+      clearTimeout(timeoutId);
+      timeoutId = setTimeout(() => func(...args), delay);
+    };
+  };
+
+  // 使用useCallback包装防抖搜索
+  const debouncedSearch = useCallback(
+    debounce((search: string) => {
+      setSearchParams(prev => ({ ...prev, search, page: 1 }));
+    }, 300),
+    []
+  );
+
+  // 处理搜索输入变化
+  const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const search = e.target.value;
+    setSearchParams(prev => ({ ...prev, search }));
+    debouncedSearch(search);
+  };
+
+  // 处理搜索表单提交
+  const handleSearch = (e: React.FormEvent) => {
+    e.preventDefault();
+    setSearchParams(prev => ({ ...prev, page: 1 }));
+  };
+
+  // 处理分页
+  const handlePageChange = (page: number, pageSize: number) => {
+    setSearchParams(prev => ({ ...prev, page, pageSize }));
+  };
+
+  // 处理过滤条件变化
+  const handleFilterChange = (newFilters: Partial<typeof filters>) => {
+    setFilters(prev => ({ ...prev, ...newFilters }));
+    setSearchParams(prev => ({ ...prev, page: 1 }));
+  };
+
+  // 重置所有过滤条件
+  const resetFilters = () => {
+    setFilters({
+      status: undefined,
+      paymentStatus: undefined
+    });
+    setSearchParams(prev => ({ ...prev, page: 1 }));
+  };
+
+  // 检查是否有活跃的过滤条件
+  const hasActiveFilters = useMemo(() => {
+    return filters.status !== undefined || filters.paymentStatus !== undefined;
+  }, [filters]);
+
+  // 打开订单详情对话框
+  const handleViewOrder = (order: OrderResponse) => {
+    setSelectedOrder(order);
+    setDetailDialogOpen(true);
+  };
+
+  // 获取订单状态对应的颜色
+  const getStatusColor = (status: OrderStatus) => {
+    switch (status) {
+      case OrderStatus.PENDING_PAYMENT:
+        return 'secondary';
+      case OrderStatus.WAITING_DEPARTURE:
+        return 'default';
+      case OrderStatus.IN_PROGRESS:
+        return 'default';
+      case OrderStatus.COMPLETED:
+        return 'default';
+      case OrderStatus.CANCELLED:
+        return 'destructive';
+      default:
+        return 'default';
+    }
+  };
+
+  // 获取支付状态对应的颜色
+  const getPaymentStatusColor = (status: PaymentStatus) => {
+    switch (status) {
+      case PaymentStatus.PENDING:
+        return 'secondary';
+      case PaymentStatus.PAID:
+        return 'default';
+      case PaymentStatus.FAILED:
+        return 'destructive';
+      case PaymentStatus.REFUNDED:
+        return 'secondary';
+      default:
+        return 'default';
+    }
+  };
+
+  // 渲染表格部分的骨架屏
+  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" />
+          <Skeleton className="h-4 flex-1" />
+          <Skeleton className="h-4 flex-1" />
+          <Skeleton className="h-4 flex-1" />
+          <Skeleton className="h-4 flex-1" />
+          <Skeleton className="h-4 w-16" />
+        </div>
+      ))}
+    </div>
+  );
+
+  return (
+    <div className="space-y-4">
+      <div className="flex justify-between items-center">
+        <h1 className="text-2xl font-bold">订单管理</h1>
+      </div>
+
+      {/* 订单统计面板 */}
+      <div className="grid grid-cols-2 md:grid-cols-6 gap-4">
+        <Card data-testid="total-orders-card">
+          <CardHeader className="p-4">
+            <CardTitle className="text-sm font-medium">总订单数</CardTitle>
+            <CardDescription className="text-2xl font-bold" data-testid="total-orders-count">{stats.total}</CardDescription>
+          </CardHeader>
+        </Card>
+        <Card data-testid="pending-payment-card">
+          <CardHeader className="p-4">
+            <CardTitle className="text-sm font-medium">待支付</CardTitle>
+            <CardDescription className="text-2xl font-bold" data-testid="pending-payment-count">{stats.pendingPayment}</CardDescription>
+          </CardHeader>
+        </Card>
+        <Card data-testid="waiting-departure-card">
+          <CardHeader className="p-4">
+            <CardTitle className="text-sm font-medium">待出发</CardTitle>
+            <CardDescription className="text-2xl font-bold" data-testid="waiting-departure-count">{stats.waitingDeparture}</CardDescription>
+          </CardHeader>
+        </Card>
+        <Card data-testid="in-progress-card">
+          <CardHeader className="p-4">
+            <CardTitle className="text-sm font-medium">行程中</CardTitle>
+            <CardDescription className="text-2xl font-bold" data-testid="in-progress-count">{stats.inProgress}</CardDescription>
+          </CardHeader>
+        </Card>
+        <Card data-testid="completed-card">
+          <CardHeader className="p-4">
+            <CardTitle className="text-sm font-medium">已完成</CardTitle>
+            <CardDescription className="text-2xl font-bold" data-testid="completed-count">{stats.completed}</CardDescription>
+          </CardHeader>
+        </Card>
+        <Card data-testid="cancelled-card">
+          <CardHeader className="p-4">
+            <CardTitle className="text-sm font-medium">已取消</CardTitle>
+            <CardDescription className="text-2xl font-bold" data-testid="cancelled-count">{stats.cancelled}</CardDescription>
+          </CardHeader>
+        </Card>
+      </div>
+
+      <Card>
+        <CardHeader>
+          <CardTitle>订单列表</CardTitle>
+          <CardDescription>
+            管理系统中的所有订单,共 {totalCount} 个订单
+          </CardDescription>
+        </CardHeader>
+        <CardContent>
+          <div className="mb-4 space-y-4">
+            <form onSubmit={handleSearch} className="flex gap-2">
+              <div className="relative flex-1 max-w-sm">
+                <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
+                <Input
+                  placeholder="搜索订单号、用户信息..."
+                  value={searchParams.search}
+                  onChange={handleSearchChange}
+                  className="pl-8"
+                />
+              </div>
+              <Button type="submit" variant="outline">
+                搜索
+              </Button>
+              <Button
+                type="button"
+                variant="outline"
+                onClick={() => setShowFilters(!showFilters)}
+                className="flex items-center gap-2"
+              >
+                <Filter className="h-4 w-4" />
+                高级筛选
+                {hasActiveFilters && (
+                  <Badge variant="secondary" className="ml-1">
+                    {Object.values(filters).filter(v => v !== undefined).length}
+                  </Badge>
+                )}
+              </Button>
+              {hasActiveFilters && (
+                <Button
+                  type="button"
+                  variant="ghost"
+                  onClick={resetFilters}
+                  className="flex items-center gap-2"
+                >
+                  <X className="h-4 w-4" />
+                  重置
+                </Button>
+              )}
+            </form>
+
+            {showFilters && (
+              <div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 border rounded-lg bg-muted/50">
+                {/* 订单状态筛选 */}
+                <div className="space-y-2">
+                  <label className="text-sm font-medium">订单状态</label>
+                  <Select
+                    value={filters.status || 'all'}
+                    onValueChange={(value) =>
+                      handleFilterChange({
+                        status: value === 'all' ? undefined : value as OrderStatus
+                      })
+                    }
+                  >
+                    <SelectTrigger>
+                      <SelectValue placeholder="选择订单状态" />
+                    </SelectTrigger>
+                    <SelectContent>
+                      <SelectItem value="all">全部状态</SelectItem>
+                      <SelectItem value={OrderStatus.PENDING_PAYMENT}>待支付</SelectItem>
+                      <SelectItem value={OrderStatus.WAITING_DEPARTURE}>待出发</SelectItem>
+                      <SelectItem value={OrderStatus.IN_PROGRESS}>行程中</SelectItem>
+                      <SelectItem value={OrderStatus.COMPLETED}>已完成</SelectItem>
+                      <SelectItem value={OrderStatus.CANCELLED}>已取消</SelectItem>
+                    </SelectContent>
+                  </Select>
+                </div>
+
+                {/* 支付状态筛选 */}
+                <div className="space-y-2">
+                  <label className="text-sm font-medium">支付状态</label>
+                  <Select
+                    value={filters.paymentStatus || 'all'}
+                    onValueChange={(value) =>
+                      handleFilterChange({
+                        paymentStatus: value === 'all' ? undefined : value as PaymentStatus
+                      })
+                    }
+                  >
+                    <SelectTrigger>
+                      <SelectValue placeholder="选择支付状态" />
+                    </SelectTrigger>
+                    <SelectContent>
+                      <SelectItem value="all">全部状态</SelectItem>
+                      <SelectItem value={PaymentStatus.PENDING}>待支付</SelectItem>
+                      <SelectItem value={PaymentStatus.PAID}>已支付</SelectItem>
+                      <SelectItem value={PaymentStatus.FAILED}>支付失败</SelectItem>
+                      <SelectItem value={PaymentStatus.REFUNDED}>已退款</SelectItem>
+                    </SelectContent>
+                  </Select>
+                </div>
+              </div>
+            )}
+
+            {/* 过滤条件标签 */}
+            {hasActiveFilters && (
+              <div className="flex flex-wrap gap-2">
+                {filters.status && (
+                  <Badge variant="secondary" className="flex items-center gap-1">
+                    订单状态: {filters.status}
+                    <X
+                      className="h-3 w-3 cursor-pointer"
+                      onClick={() => handleFilterChange({ status: undefined })}
+                    />
+                  </Badge>
+                )}
+                {filters.paymentStatus && (
+                  <Badge variant="secondary" className="flex items-center gap-1">
+                    支付状态: {filters.paymentStatus}
+                    <X
+                      className="h-3 w-3 cursor-pointer"
+                      onClick={() => handleFilterChange({ paymentStatus: undefined })}
+                    />
+                  </Badge>
+                )}
+              </div>
+            )}
+          </div>
+
+          <div className="rounded-md border">
+            <Table>
+              <TableHeader>
+                <TableRow>
+                  <TableHead>订单号</TableHead>
+                  <TableHead>用户</TableHead>
+                  <TableHead>路线</TableHead>
+                  <TableHead>乘客数量</TableHead>
+                  <TableHead>订单金额</TableHead>
+                  <TableHead>订单状态</TableHead>
+                  <TableHead>支付状态</TableHead>
+                  <TableHead>创建时间</TableHead>
+                  <TableHead className="text-right">操作</TableHead>
+                </TableRow>
+              </TableHeader>
+              <TableBody>
+                {isLoading ? (
+                  // 显示表格骨架屏
+                  <TableRow>
+                    <TableCell colSpan={9} className="p-4">
+                      {renderTableSkeleton()}
+                    </TableCell>
+                  </TableRow>
+                ) : (
+                  // 显示实际订单数据
+                  orders.map((order) => (
+                    <TableRow key={order.id}>
+                      <TableCell className="font-medium">#{order.id}</TableCell>
+                      <TableCell>
+                        {order.user?.username || '未知用户'}
+                        {order.user?.phone && ` (${order.user.phone})`}
+                      </TableCell>
+                      <TableCell>{order.route?.name || '未知路线'}</TableCell>
+                      <TableCell>{order.passengerCount}</TableCell>
+                      <TableCell>¥{order.totalAmount}</TableCell>
+                      <TableCell>
+                        <Badge variant={getStatusColor(order.status)}>
+                          {order.status}
+                        </Badge>
+                      </TableCell>
+                      <TableCell>
+                        <Badge variant={getPaymentStatusColor(order.paymentStatus)}>
+                          {order.paymentStatus}
+                        </Badge>
+                      </TableCell>
+                      <TableCell>
+                        {format(new Date(order.createdAt), 'yyyy-MM-dd HH:mm')}
+                      </TableCell>
+                      <TableCell className="text-right">
+                        <div className="flex justify-end gap-2">
+                          <Button
+                            variant="ghost"
+                            size="icon"
+                            onClick={() => handleViewOrder(order)}
+                          >
+                            <Eye className="h-4 w-4" />
+                          </Button>
+                        </div>
+                      </TableCell>
+                    </TableRow>
+                  ))
+                )}
+              </TableBody>
+            </Table>
+          </div>
+
+          <DataTablePagination
+            currentPage={searchParams.page}
+            totalCount={totalCount}
+            pageSize={searchParams.pageSize}
+            onPageChange={handlePageChange}
+          />
+        </CardContent>
+      </Card>
+
+      {/* 订单详情对话框 */}
+      <Dialog open={detailDialogOpen} onOpenChange={setDetailDialogOpen}>
+        <DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
+          <DialogHeader>
+            <DialogTitle>订单详情</DialogTitle>
+            <DialogDescription>
+              查看订单的详细信息
+            </DialogDescription>
+          </DialogHeader>
+
+          {selectedOrder && (
+            <div className="space-y-4">
+              <div className="grid grid-cols-2 gap-4">
+                <div>
+                  <h4 className="font-medium">订单信息</h4>
+                  <div className="text-sm text-muted-foreground space-y-1 mt-2">
+                    <div>订单号: #{selectedOrder.id}</div>
+                    <div>订单状态: {selectedOrder.status}</div>
+                    <div>支付状态: {selectedOrder.paymentStatus}</div>
+                    <div>创建时间: {format(new Date(selectedOrder.createdAt), 'yyyy-MM-dd HH:mm:ss')}</div>
+                  </div>
+                </div>
+                <div>
+                  <h4 className="font-medium">用户信息</h4>
+                  <div className="text-sm text-muted-foreground space-y-1 mt-2">
+                    <div>用户名: {selectedOrder.user?.username || '未知'}</div>
+                    <div>手机号: {selectedOrder.user?.phone || '未知'}</div>
+                  </div>
+                </div>
+              </div>
+
+              <div>
+                <h4 className="font-medium">路线信息</h4>
+                <div className="text-sm text-muted-foreground space-y-1 mt-2">
+                  <div>路线名称: {selectedOrder.route?.name || '未知'}</div>
+                  <div>路线描述: {selectedOrder.route?.description || '无描述'}</div>
+                </div>
+              </div>
+
+              <div>
+                <h4 className="font-medium">订单详情</h4>
+                <div className="text-sm text-muted-foreground space-y-1 mt-2">
+                  <div>乘客数量: {selectedOrder.passengerCount}</div>
+                  <div>订单金额: ¥{selectedOrder.totalAmount}</div>
+                </div>
+              </div>
+
+              {selectedOrder.passengerSnapshots && selectedOrder.passengerSnapshots.length > 0 && (
+                <div>
+                  <h4 className="font-medium">乘客信息</h4>
+                  <div className="text-sm text-muted-foreground space-y-1 mt-2">
+                    {selectedOrder.passengerSnapshots.map((passenger: any, index: number) => (
+                      <div key={index} className="border-b pb-2">
+                        <div>乘客 {index + 1}: {passenger.name || '未知'}</div>
+                        <div>身份证: {passenger.idCard || '未知'}</div>
+                        <div>手机号: {passenger.phone || '未知'}</div>
+                      </div>
+                    ))}
+                  </div>
+                </div>
+              )}
+            </div>
+          )}
+
+          <DialogFooter>
+            <Button onClick={() => setDetailDialogOpen(false)}>
+              关闭
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+    </div>
+  );
+};

+ 6 - 0
web/src/client/admin/routes.tsx

@@ -12,6 +12,7 @@ import { RoutesPage } from './pages/Routes';
 import { AreasPage } from './pages/Areas';
 import { AreasPage } from './pages/Areas';
 import { LocationsPage } from './pages/Locations';
 import { LocationsPage } from './pages/Locations';
 import { PassengersPage } from './pages/Passengers';
 import { PassengersPage } from './pages/Passengers';
+import { OrdersPage } from './pages/Orders';
 
 
 export const router = createBrowserRouter([
 export const router = createBrowserRouter([
   {
   {
@@ -74,6 +75,11 @@ export const router = createBrowserRouter([
         element: <PassengersPage />,
         element: <PassengersPage />,
         errorElement: <ErrorPage />
         errorElement: <ErrorPage />
       },
       },
+      {
+        path: 'orders',
+        element: <OrdersPage />,
+        errorElement: <ErrorPage />
+      },
       {
       {
         path: '*',
         path: '*',
         element: <NotFoundPage />,
         element: <NotFoundPage />,

+ 5 - 1
web/src/client/api.ts

@@ -4,7 +4,7 @@ import type {
   AuthRoutes, UserRoutes, RoleRoutes,
   AuthRoutes, UserRoutes, RoleRoutes,
   FileRoutes, AdminActivitiesRoutes, AdminRoutesRoutes,
   FileRoutes, AdminActivitiesRoutes, AdminRoutesRoutes,
   AdminAreasRoutes, AdminLocationsRoutes, RoutesRoutes,
   AdminAreasRoutes, AdminLocationsRoutes, RoutesRoutes,
-  AdminPassengersRoutes
+  AdminPassengersRoutes, AdminOrdersRoutes
 } from '@d8d/server/api';
 } from '@d8d/server/api';
 
 
 // 创建 axios 适配器
 // 创建 axios 适配器
@@ -101,3 +101,7 @@ export const publicRouteClient = hc<RoutesRoutes>('/', {
 export const passengerClient = hc<AdminPassengersRoutes>('/', {
 export const passengerClient = hc<AdminPassengersRoutes>('/', {
   fetch: axiosFetch,
   fetch: axiosFetch,
 }).api.v1.admin.passengers;
 }).api.v1.admin.passengers;
+
+export const orderClient = hc<AdminOrdersRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.admin.orders;

+ 462 - 0
web/tests/integration/client/admin/orders.test.tsx

@@ -0,0 +1,462 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import '@testing-library/jest-dom';
+import { OrdersPage } from '@/client/admin/pages/Orders';
+import { TestWrapper } from '~/utils/client/test-render';
+
+// Import mocked modules
+import { orderClient } from '@/client/api';
+import { toast } from 'sonner';
+import { OrderStatus, PaymentStatus } from '@d8d/server/share/order.types';
+
+// Mock API 客户端
+vi.mock('@/client/api', () => ({
+  orderClient: {
+    $get: vi.fn().mockResolvedValue({
+      status: 200,
+      ok: true,
+      json: async () => ({
+        data: [
+          {
+            id: 1,
+            userId: 1,
+            routeId: 1,
+            passengerCount: 2,
+            totalAmount: 100.5,
+            status: OrderStatus.PENDING_PAYMENT,
+            paymentStatus: PaymentStatus.PENDING,
+            passengerSnapshots: [
+              { name: '张三', idCard: '123456789012345678', phone: '13800138000' },
+              { name: '李四', idCard: '123456789012345679', phone: '13800138001' }
+            ],
+            routeSnapshot: { name: '测试路线', description: '测试路线描述' },
+            createdBy: 1,
+            updatedBy: null,
+            createdAt: '2024-01-01T00:00:00.000Z',
+            updatedAt: '2024-01-01T00:00:00.000Z',
+            user: {
+              id: 1,
+              username: 'testuser',
+              phone: '13800138000'
+            },
+            route: {
+              id: 1,
+              name: '测试路线',
+              description: '测试路线描述'
+            }
+          }
+        ],
+        total: 1,
+        page: 1,
+        pageSize: 10
+      })
+    }),
+    stats: {
+      $get: vi.fn().mockResolvedValue({
+        status: 200,
+        ok: true,
+        json: async () => ({
+          total: 10,
+          pendingPayment: 2,
+          waitingDeparture: 3,
+          inProgress: 1,
+          completed: 3,
+          cancelled: 1
+        })
+      })
+    }
+  }
+}));
+
+// Mock toast
+vi.mock('sonner', () => ({
+  toast: {
+    success: vi.fn(),
+    error: vi.fn(),
+  }
+}));
+
+describe('OrdersPage 集成测试', () => {
+  const user = userEvent.setup();
+
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('应该正确渲染订单管理页面标题', async () => {
+    render(
+      <TestWrapper>
+        <OrdersPage />
+      </TestWrapper>
+    );
+
+    expect(screen.getByText('订单管理')).toBeInTheDocument();
+  });
+
+  it('应该显示订单统计面板', async () => {
+    render(
+      <TestWrapper>
+        <OrdersPage />
+      </TestWrapper>
+    );
+
+    // 等待数据加载
+    await waitFor(() => {
+      expect(screen.getByText('总订单数')).toBeInTheDocument();
+      expect(screen.getByTestId('total-orders-count')).toHaveTextContent('10'); // 总订单数
+      expect(screen.getByText('待支付')).toBeInTheDocument();
+      expect(screen.getByTestId('pending-payment-count')).toHaveTextContent('2'); // 待支付数量
+      expect(screen.getByText('待出发')).toBeInTheDocument();
+      expect(screen.getByTestId('waiting-departure-count')).toHaveTextContent('3'); // 待出发数量
+      expect(screen.getByText('行程中')).toBeInTheDocument();
+      expect(screen.getByTestId('in-progress-count')).toHaveTextContent('1'); // 行程中数量
+      expect(screen.getByText('已完成')).toBeInTheDocument();
+      expect(screen.getByTestId('completed-count')).toHaveTextContent('3'); // 已完成数量
+      expect(screen.getByText('已取消')).toBeInTheDocument();
+      expect(screen.getByTestId('cancelled-count')).toHaveTextContent('1'); // 已取消数量
+    });
+  });
+
+  it('应该显示订单列表和搜索功能', async () => {
+    render(
+      <TestWrapper>
+        <OrdersPage />
+      </TestWrapper>
+    );
+
+    // 等待数据加载
+    await waitFor(() => {
+      expect(screen.getByPlaceholderText('搜索订单号、用户信息...')).toBeInTheDocument();
+    });
+
+    expect(screen.getByText('搜索')).toBeInTheDocument();
+    expect(screen.getByText('高级筛选')).toBeInTheDocument();
+  });
+
+  it('应该处理搜索功能', async () => {
+    render(
+      <TestWrapper>
+        <OrdersPage />
+      </TestWrapper>
+    );
+
+    const searchInput = screen.getByPlaceholderText('搜索订单号、用户信息...');
+    const searchButton = screen.getByText('搜索');
+
+    // 输入搜索关键词
+    await user.type(searchInput, 'testuser');
+    await user.click(searchButton);
+
+    // 验证搜索参数被设置
+    expect(searchInput).toHaveValue('testuser');
+  });
+
+  it('应该显示高级筛选功能', async () => {
+    render(
+      <TestWrapper>
+        <OrdersPage />
+      </TestWrapper>
+    );
+
+    const filterButton = screen.getByRole('button', { name: '高级筛选' });
+    await user.click(filterButton);
+
+    // 验证筛选表单显示
+    expect(screen.getByText('订单状态')).toBeInTheDocument();
+    expect(screen.getByText('支付状态')).toBeInTheDocument();
+  });
+
+  it('应该处理订单状态筛选', async () => {
+    render(
+      <TestWrapper>
+        <OrdersPage />
+      </TestWrapper>
+    );
+
+    const filterButton = screen.getByRole('button', { name: '高级筛选' });
+    await user.click(filterButton);
+
+    // 验证筛选表单显示和状态筛选标签
+    expect(screen.getByText('订单状态')).toBeInTheDocument();
+
+    // 验证状态筛选器存在(通过查找Select组件)
+    const selectElements = document.querySelectorAll('[role="combobox"]');
+    expect(selectElements.length).toBeGreaterThan(0);
+  });
+
+  it('应该显示分页组件', async () => {
+    render(
+      <TestWrapper>
+        <OrdersPage />
+      </TestWrapper>
+    );
+
+    // 验证分页控件存在
+    await waitFor(() => {
+      expect(screen.getByText(/共 \d+ 个订单/)).toBeInTheDocument();
+    });
+  });
+
+  it('应该处理表格数据加载状态', async () => {
+    render(
+      <TestWrapper>
+        <OrdersPage />
+      </TestWrapper>
+    );
+
+    // 验证骨架屏或加载状态
+    const skeletonElements = document.querySelectorAll('[data-slot="skeleton"]');
+    expect(skeletonElements.length).toBeGreaterThan(0);
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByText('testuser')).toBeInTheDocument();
+    });
+  });
+
+  it('应该显示正确的表格列标题', async () => {
+    render(
+      <TestWrapper>
+        <OrdersPage />
+      </TestWrapper>
+    );
+
+    // 等待数据加载
+    await waitFor(() => {
+      expect(screen.getByText('订单号')).toBeInTheDocument();
+      expect(screen.getByText('用户')).toBeInTheDocument();
+      expect(screen.getByText('路线')).toBeInTheDocument();
+      expect(screen.getByText('乘客数量')).toBeInTheDocument();
+      expect(screen.getByText('订单金额')).toBeInTheDocument();
+      expect(screen.getByText('订单状态')).toBeInTheDocument();
+      expect(screen.getByText('支付状态')).toBeInTheDocument();
+      expect(screen.getByText('创建时间')).toBeInTheDocument();
+      expect(screen.getByText('操作')).toBeInTheDocument();
+    });
+  });
+
+  it('应该包含查看详情操作按钮', async () => {
+    const { container } = render(
+      <TestWrapper>
+        <OrdersPage />
+      </TestWrapper>
+    );
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByText('testuser')).toBeInTheDocument();
+
+      // 查找操作按钮(通过按钮元素)
+      const actionButtons = container.querySelectorAll('button');
+      const hasViewButtons = Array.from(actionButtons).some(button =>
+        button.innerHTML.includes('eye')
+      );
+
+      expect(hasViewButtons).toBe(true);
+    });
+  });
+
+  it('应该打开订单详情对话框', async () => {
+    render(
+      <TestWrapper>
+        <OrdersPage />
+      </TestWrapper>
+    );
+
+    // 等待数据加载
+    await waitFor(() => {
+      expect(screen.getByText('testuser')).toBeInTheDocument();
+    });
+
+    // 查找查看详情按钮
+    const viewButtons = screen.getAllByRole('button').filter(btn =>
+      btn.innerHTML.includes('eye')
+    );
+
+    if (viewButtons.length > 0) {
+      await user.click(viewButtons[0]);
+
+      // 验证详情对话框打开
+      await waitFor(() => {
+        expect(screen.getByRole('heading', { name: '订单详情' })).toBeInTheDocument();
+      });
+    }
+  });
+
+  it('应该在详情对话框中显示订单信息', async () => {
+    render(
+      <TestWrapper>
+        <OrdersPage />
+      </TestWrapper>
+    );
+
+    // 等待数据加载
+    await waitFor(() => {
+      expect(screen.getByText('testuser')).toBeInTheDocument();
+    });
+
+    // 查找查看详情按钮
+    const viewButtons = screen.getAllByRole('button').filter(btn =>
+      btn.innerHTML.includes('eye')
+    );
+
+    if (viewButtons.length > 0) {
+      await user.click(viewButtons[0]);
+
+      // 验证详情对话框内容
+      await waitFor(() => {
+        expect(screen.getByText('订单信息')).toBeInTheDocument();
+        expect(screen.getByText('用户信息')).toBeInTheDocument();
+        expect(screen.getByText('路线信息')).toBeInTheDocument();
+        expect(screen.getByText('订单详情')).toBeInTheDocument();
+        expect(screen.getByText('乘客信息')).toBeInTheDocument();
+
+        // 验证具体数据
+        expect(screen.getByText('#1')).toBeInTheDocument(); // 订单号
+        expect(screen.getByText('待支付')).toBeInTheDocument(); // 订单状态
+        expect(screen.getByText('testuser')).toBeInTheDocument(); // 用户名
+        expect(screen.getByText('测试路线')).toBeInTheDocument(); // 路线名称
+        expect(screen.getByText('2')).toBeInTheDocument(); // 乘客数量
+        expect(screen.getByText('¥100.5')).toBeInTheDocument(); // 订单金额
+      });
+    }
+  });
+
+  it('应该处理API错误场景', async () => {
+    // 模拟API错误
+    (orderClient.$get as any).mockResolvedValueOnce({
+      status: 500,
+      ok: false,
+      json: async () => ({ error: 'Internal server error' })
+    });
+
+    render(
+      <TestWrapper>
+        <OrdersPage />
+      </TestWrapper>
+    );
+
+    // 验证页面仍然渲染基本结构
+    expect(screen.getByText('订单管理')).toBeInTheDocument();
+
+    // 验证错误处理(组件应该优雅处理错误)
+    await waitFor(() => {
+      expect(screen.queryByText('testuser')).not.toBeInTheDocument();
+    });
+  });
+
+  it('应该处理响应式布局', async () => {
+    const { container } = render(
+      <TestWrapper>
+        <OrdersPage />
+      </TestWrapper>
+    );
+
+    // 等待数据加载
+    await waitFor(() => {
+      expect(screen.getByText('testuser')).toBeInTheDocument();
+    });
+
+    // 展开筛选表单以显示响应式网格
+    const filterButton = screen.getByRole('button', { name: '高级筛选' });
+    await user.click(filterButton);
+
+    // 验证响应式网格类名
+    const gridElements = container.querySelectorAll('.grid');
+    expect(gridElements.length).toBeGreaterThan(0);
+
+    // 验证响应式类名存在
+    const hasResponsiveClasses = container.innerHTML.includes('md:grid-cols-2');
+    expect(hasResponsiveClasses).toBe(true);
+  });
+
+  it('应该显示订单总数信息', async () => {
+    render(
+      <TestWrapper>
+        <OrdersPage />
+      </TestWrapper>
+    );
+
+    // 验证订单总数显示
+    await waitFor(() => {
+      expect(screen.getByText(/共 \d+ 个订单/)).toBeInTheDocument();
+    });
+  });
+
+  it('应该处理统计API错误场景', async () => {
+    // 模拟统计API错误
+    (orderClient.stats.$get as any).mockResolvedValueOnce({
+      status: 500,
+      ok: false,
+      json: async () => ({ error: 'Internal server error' })
+    });
+
+    render(
+      <TestWrapper>
+        <OrdersPage />
+      </TestWrapper>
+    );
+
+    // 验证页面仍然渲染基本结构
+    expect(screen.getByText('订单管理')).toBeInTheDocument();
+
+    // 验证统计面板显示默认值
+    await waitFor(() => {
+      expect(screen.getByText('总订单数')).toBeInTheDocument();
+      expect(screen.getByText('0')).toBeInTheDocument(); // 默认值
+    });
+  });
+
+  it('应该处理防抖搜索功能', async () => {
+    render(
+      <TestWrapper>
+        <OrdersPage />
+      </TestWrapper>
+    );
+
+    const searchInput = screen.getByPlaceholderText('搜索订单号、用户信息...');
+
+    // 快速输入多个字符
+    await user.type(searchInput, 'test');
+    await user.type(searchInput, 'user');
+
+    // 验证输入值正确
+    expect(searchInput).toHaveValue('testuser');
+
+    // 等待防抖延迟
+    await waitFor(() => {
+      expect(orderClient.$get).toHaveBeenCalled();
+    }, { timeout: 500 });
+  });
+
+  it('应该处理筛选条件重置', async () => {
+    render(
+      <TestWrapper>
+        <OrdersPage />
+      </TestWrapper>
+    );
+
+    // 打开筛选
+    const filterButton = screen.getByRole('button', { name: '高级筛选' });
+    await user.click(filterButton);
+
+    // 设置筛选条件
+    const statusSelect = document.querySelectorAll('[role="combobox"]')[0];
+    await user.click(statusSelect);
+
+    // 选择订单状态
+    const statusOption = screen.getByText('待支付');
+    await user.click(statusOption);
+
+    // 验证重置按钮显示
+    const resetButton = screen.getByRole('button', { name: '重置' });
+    expect(resetButton).toBeInTheDocument();
+
+    // 点击重置
+    await user.click(resetButton);
+
+    // 验证筛选条件被重置
+    expect(screen.queryByText('待支付')).not.toBeInTheDocument();
+  });
+});