Răsfoiți Sursa

✨ feat(order): 新增订单和支付功能模块

- 新增订单确认页面,支持包车和拼车两种模式
- 新增支付成功页面,包含司机分配和积分奖励功能
- 在API客户端中添加订单和支付模块支持
- 扩展Card组件支持点击事件
- 更新路线列表页面,添加预订功能跳转
- 新增订单和路线相关类型定义
- 在服务器端添加获取单个路线详情的API接口
- 更新Claude配置,添加类型检查命令支持
- 在迁移指南中添加shadcn/ui组件使用规范

📝 docs(architecture): 更新组件使用规范文档

- 添加shadcn/ui组件引入规范说明
- 提供Button和Dialog组件的正确使用示例
- 明确禁止使用Taro原生组件和自定义模态框
- 规范组件导入路径和使用方式
yourname 3 luni în urmă
părinte
comite
960e471071

+ 2 - 1
.claude/settings.local.json

@@ -36,7 +36,8 @@
       "Bash(mkdir:*)",
       "Bash(mkdir:*)",
       "Bash(xargs sed:*)",
       "Bash(xargs sed:*)",
       "Bash(git diff:*)",
       "Bash(git diff:*)",
-      "Bash(pnpm build)"
+      "Bash(pnpm build)",
+      "Bash(pnpm run typecheck:*)"
     ],
     ],
     "deny": [],
     "deny": [],
     "ask": []
     "ask": []

+ 64 - 0
docs/architecture/mini-demo-migration-guide.md

@@ -50,6 +50,70 @@
 | `<scroll-view>` | `<ScrollView>` | 滚动视图 |
 | `<scroll-view>` | `<ScrollView>` | 滚动视图 |
 | `<swiper>` | `<Swiper>` | 轮播组件 |
 | `<swiper>` | `<Swiper>` | 轮播组件 |
 
 
+### shadcn/ui 组件引入规范
+
+#### Button 组件
+```typescript
+// 正确引入方式
+import { Button } from '@/components/ui/button'
+
+// 使用示例
+<Button variant="default" size="default">
+  按钮文本
+</Button>
+
+// 可用变体
+<Button variant="default">默认</Button>
+<Button variant="destructive">危险</Button>
+<Button variant="outline">轮廓</Button>
+<Button variant="secondary">次要</Button>
+<Button variant="ghost">幽灵</Button>
+<Button variant="link">链接</Button>
+```
+
+#### Dialog 组件
+```typescript
+// 正确引入方式
+import {
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
+  DialogFooter
+} from '@/components/ui/dialog'
+
+// 使用示例
+<Dialog open={isOpen} onOpenChange={setIsOpen}>
+  <DialogContent>
+    <DialogHeader>
+      <DialogTitle>对话框标题</DialogTitle>
+    </DialogHeader>
+    <Text>对话框内容</Text>
+    <DialogFooter>
+      <Button variant="outline" onClick={() => setIsOpen(false)}>
+        取消
+      </Button>
+      <Button onClick={handleConfirm}>
+        确认
+      </Button>
+    </DialogFooter>
+  </DialogContent>
+</Dialog>
+```
+
+#### 禁止使用的导入方式
+```typescript
+// ❌ 错误 - 使用Taro原生Button
+import { Button } from '@tarojs/components'
+
+// ❌ 错误 - 使用自定义模态框
+import Modal from '@/components/modal'
+
+// ✅ 正确 - 使用shadcn/ui组件
+import { Button } from '@/components/ui/button'
+import { Dialog } from '@/components/ui/dialog'
+```
+
 ### 自定义组件映射
 ### 自定义组件映射
 
 
 | 原组件 | 新组件 | 位置 |
 | 原组件 | 新组件 | 位置 |

+ 6 - 2
mini/src/api.ts

@@ -1,4 +1,4 @@
-import type { AuthRoutes, UserRoutes, RoleRoutes, FileRoutes, AreasUserRoutes, LocationsUserRoutes, RoutesRoutes, PassengersRoutes } from '@d8d/server'
+import type { AuthRoutes, UserRoutes, RoleRoutes, FileRoutes, AreasUserRoutes, LocationsUserRoutes, RoutesRoutes, PassengersRoutes, OrdersRoutes, PaymentRoutes } from '@d8d/server'
 import { rpcClient } from './utils/rpc-client'
 import { rpcClient } from './utils/rpc-client'
 
 
 // 创建各个模块的RPC客户端
 // 创建各个模块的RPC客户端
@@ -17,4 +17,8 @@ export const locationClient = rpcClient<LocationsUserRoutes>().api.v1.locations
 // @ts-ignore
 // @ts-ignore
 export const routeClient = rpcClient<RoutesRoutes>().api.v1.routes
 export const routeClient = rpcClient<RoutesRoutes>().api.v1.routes
 // @ts-ignore
 // @ts-ignore
-export const passengerClient = rpcClient<PassengersRoutes>().api.v1.passengers
+export const passengerClient = rpcClient<PassengersRoutes>().api.v1.passengers
+// @ts-ignore
+export const orderClient = rpcClient<OrdersRoutes>().api.v1.orders
+// @ts-ignore
+export const paymentClient = rpcClient<PaymentRoutes>().api.v1.payment

+ 3 - 1
mini/src/app.config.ts

@@ -9,7 +9,9 @@ export default defineAppConfig({
     'pages/select-activity/ActivitySelectPage',
     'pages/select-activity/ActivitySelectPage',
     'pages/schedule-list/ScheduleListPage',
     'pages/schedule-list/ScheduleListPage',
     'pages/passengers/passengers',
     'pages/passengers/passengers',
-    'pages/passengers/add-passenger'
+    'pages/passengers/add-passenger',
+    'pages/order/index',
+    'pages/pay-success/index'
   ],
   ],
   window: {
   window: {
     backgroundTextStyle: 'light',
     backgroundTextStyle: 'light',

+ 3 - 2
mini/src/components/ui/card.tsx

@@ -4,11 +4,12 @@ import { cn } from '@/utils/cn'
 interface CardProps {
 interface CardProps {
   className?: string
   className?: string
   children: React.ReactNode
   children: React.ReactNode
+  onClick?: () => void
 }
 }
 
 
-export function Card({ className, children }: CardProps) {
+export function Card({ className, children, onClick }: CardProps) {
   return (
   return (
-    <View className={cn("bg-white rounded-xl shadow-sm", className)}>
+    <View className={cn("bg-white rounded-xl shadow-sm", className)} onClick={onClick}>
       {children}
       {children}
     </View>
     </View>
   )
   )

+ 5 - 0
mini/src/pages/order/index.config.ts

@@ -0,0 +1,5 @@
+export default definePageConfig({
+  navigationBarTitleText: '订单确认',
+  enableShareAppMessage: true,
+  enableShareTimeline: true
+})

+ 552 - 0
mini/src/pages/order/index.tsx

@@ -0,0 +1,552 @@
+import { View, Text, ScrollView } from '@tarojs/components'
+import Taro, { useRouter, navigateTo } from '@tarojs/taro'
+import { useState, useEffect } from 'react'
+import { useQuery, useMutation } from '@tanstack/react-query'
+import { orderClient, paymentClient, routeClient, passengerClient } from '@/api'
+import { Navbar, NavbarPresets } from '@/components/ui/navbar'
+import { Button } from '@/components/ui/button'
+import { Card, CardContent } from '@/components/ui/card'
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
+import type { InferResponseType , InferRequestType} from 'hono/client'
+import { isWeapp } from '@/utils/platform'
+
+// 使用RPC方式提取类型
+type Passenger = InferResponseType<typeof passengerClient.$get, 200>['data'][0]
+type OrderCreateRequest = InferRequestType<typeof orderClient.$post>['json']
+
+// 模拟数据 - 待替换为真实API
+export default function OrderPage() {
+  const router = useRouter()
+  const { routeId, activityName, type } = router.params
+
+  const [passengers, setPassengers] = useState<Passenger[]>([])
+  const [phoneNumber, setPhoneNumber] = useState('')
+  const [hasPhoneNumber, setHasPhoneNumber] = useState(false)
+  const [totalPrice, setTotalPrice] = useState(0)
+  const [originalPrice, setOriginalPrice] = useState(0)
+  const [isCharter] = useState(type === 'business-charter')
+  const [showPassengerSelector, setShowPassengerSelector] = useState(false)
+
+  // 使用react-query获取路线数据
+  const { data: schedule, isLoading: isLoadingRoute } = useQuery({
+    queryKey: ['route', routeId],
+    queryFn: async () => {
+      const response = await routeClient[':id'].$get({
+        param: {
+          id: Number(routeId)
+        }
+      })
+
+      if (!response.ok) {
+        throw new Error(`获取路线数据失败: ${response.status}`)
+      }
+
+      const route = await response.json()
+      return route.data
+    },
+    enabled: !!routeId
+  })
+
+  // 使用react-query获取已保存的乘客
+  const { data: savedPassengers } = useQuery({
+    queryKey: ['passengers'],
+    queryFn: async () => {
+      const response = await passengerClient.$get({
+        query: {
+          page: 1,
+          pageSize: 100
+        }
+      })
+
+      if (!response.ok) {
+        throw new Error(`获取乘客数据失败: ${response.status}`)
+      }
+
+      const result = await response.json()
+      return result.data || []
+    }
+  })
+
+  // 使用react-query创建订单
+  const createOrderMutation = useMutation({
+    mutationFn: async (orderData: OrderCreateRequest) => {
+      const response = await orderClient.$post({
+        json: orderData
+      })
+
+      if (!response.ok) {
+        const errorData = await response.json()
+        throw new Error(errorData.message || '订单创建失败')
+      }
+
+      return await response.json()
+    }
+  })
+
+  // 使用react-query创建支付
+  const createPaymentMutation = useMutation({
+    mutationFn: async (paymentData: { orderId: number; totalAmount: number; description: string }) => {
+      const response = await paymentClient.$post({
+        json: paymentData
+      })
+
+      if (!response.ok) {
+        const errorData = await response.json()
+        throw new Error(errorData.message || '支付创建失败')
+      }
+
+      return await response.json()
+    }
+  })
+
+  // 计算总价格
+  const calculateTotalPrice = () => {
+    if (!schedule) return
+
+    let original = 0
+    if (isCharter) {
+      // 包车按车计费
+      original = schedule.price
+    } else {
+      // 拼车按人计费
+      original = passengers.length * schedule.price
+    }
+
+    setOriginalPrice(original)
+    setTotalPrice(original)
+  }
+
+  useEffect(() => {
+    calculateTotalPrice()
+  }, [schedule, passengers, isCharter])
+
+  // 获取手机号
+  const handleGetPhoneNumber = (e: any) => {
+    if (e.detail.errMsg === 'getPhoneNumber:ok') {
+      // TODO: 实际项目中需要发送code到后端获取手机号
+      setPhoneNumber('138****8888')
+      setHasPhoneNumber(true)
+    } else {
+      console.error('获取手机号失败:', e.detail.errMsg)
+    }
+  }
+
+  // 添加乘客
+  const handleAddPassenger = () => {
+    if (isWeapp() && !hasPhoneNumber) {
+      // TODO: 显示提示
+      return
+    }
+
+    if (savedPassengers && savedPassengers.length > 0) {
+      setShowPassengerSelector(true)
+    } else {
+      navigateTo({
+        url: '/pages/passengers/add-passenger'
+      })
+    }
+  }
+
+  // 选择已保存的乘客
+  const handleSelectSavedPassenger = (passenger: Passenger) => {
+    // 检查是否已经添加过这个乘车人
+    const existingPassenger = passengers.find(p => p.idNumber === passenger.idNumber)
+    if (existingPassenger) {
+      // TODO: 显示提示
+      return
+    }
+
+    setPassengers([...passengers, passenger])
+    setShowPassengerSelector(false)
+  }
+
+  // 删除乘客
+  const handleDeletePassenger = (index: number) => {
+    const newPassengers = [...passengers]
+    newPassengers.splice(index, 1)
+    setPassengers(newPassengers)
+  }
+
+  // 添加新乘客
+  const handleAddNewPassenger = () => {
+    setShowPassengerSelector(false)
+    navigateTo({
+      url: '/pages/passengers/add-passenger'
+    })
+  }
+
+  // 管理乘客
+  const handleManagePassengers = () => {
+    setShowPassengerSelector(false)
+    navigateTo({
+      url: '/pages/passengers/passengers'
+    })
+  }
+
+  // 创建订单并支付
+  const handlePay = async () => {
+    if (!hasPhoneNumber) {
+      // TODO: 显示提示
+      return
+    }
+
+    if (passengers.length === 0) {
+      // TODO: 显示提示
+      return
+    }
+
+    if (!schedule) {
+      // TODO: 显示提示
+      console.error('路线数据未加载完成')
+      return
+    }
+
+    if (!isCharter && passengers.length > (schedule.availableSeats || 0)) {
+      // TODO: 显示提示
+      return
+    }
+
+    try {
+      // 创建订单
+      const orderData: OrderCreateRequest = {
+        routeId: Number(routeId),
+        passengerCount: passengers.length,
+        passengerSnapshots: passengers,
+        totalAmount: totalPrice,
+        routeSnapshot: {
+          id: schedule.id,
+          name: schedule.name || '',
+          pickupPoint: schedule.pickupPoint,
+          dropoffPoint: schedule.dropoffPoint,
+          departureTime: schedule.departureTime,
+          price: schedule.price,
+          vehicleType: schedule.vehicleType,
+          travelMode: schedule.travelMode
+        }
+      }
+
+      const order = await createOrderMutation.mutateAsync(orderData)
+
+      // 发起支付
+      await createPaymentMutation.mutateAsync({
+        orderId: order.id,
+        totalAmount: totalPrice,
+        description: `${activityName || '出行'}订单`
+      })
+
+      // 调用微信支付
+      // TODO: 实现微信支付调用
+
+      // 支付成功后跳转到支付成功页面
+      navigateTo({
+        url: `/pages/pay-success?orderId=${order.id}&totalPrice=${totalPrice}&passengerCount=${passengers.length}`
+      })
+
+    } catch (error) {
+      console.error('支付失败:', error)
+      // TODO: 显示错误提示
+    }
+  }
+
+  if (isLoadingRoute || !schedule) {
+    return (
+      <View className="flex-1 flex flex-col items-center justify-center bg-gradient-to-b from-gray-50 to-gray-100 min-h-screen">
+        <View className="flex flex-col items-center">
+          <View className="i-heroicons-arrow-path-20-solid animate-spin w-8 h-8 text-blue-500 mb-4" />
+          <Text className="text-gray-600">加载中...</Text>
+        </View>
+      </View>
+    )
+  }
+
+  return (
+    <View className="flex-1 bg-gradient-to-b from-gray-50 to-gray-100 min-h-screen pb-40">
+      <Navbar
+        title="订单确认"
+        leftIcon="i-heroicons-arrow-left-20-solid"
+        onClickLeft={() => Taro.navigateBack()}
+        {...NavbarPresets.primary}
+      />
+
+      <ScrollView className="flex-1">
+        {/* 活动信息 */}
+        <View className="bg-gradient-to-r from-primary to-primary-dark px-4 py-8 text-white shadow-lg">
+          <Text className="text-2xl font-bold text-center tracking-wide">{activityName || '活动'}</Text>
+        </View>
+
+        {/* 班次信息 */}
+        <View className="px-4 mt-4">
+          <Card className={`${isCharter ? 'bg-gradient-to-br from-charter-dark to-charter-bg border-2 border-charter shadow-charter' : 'bg-white/95 shadow-lg backdrop-blur-md border border-white/40'} rounded-2xl`}>
+            <CardContent className="p-6">
+              <View className="flex justify-between items-center mb-6 pb-4 border-b border-gray-200">
+                <Text className={`text-xl font-bold tracking-wide ${isCharter ? 'text-charter' : 'text-gray-900'}`}>
+                  {isCharter ? '包车服务' : '班次信息'}
+                </Text>
+                <View className="bg-primary text-white px-4 py-2 rounded-full text-xs font-semibold tracking-wide">
+                  {schedule.travelMode === 'charter' ? '专车包车' : (schedule.vehicleType === 'business' ? '商务拼车' : '大巴拼车')}
+                </View>
+              </View>
+
+              <View className="space-y-4 mb-6">
+                <View className="flex justify-between items-center">
+                  <Text className={`text-sm font-medium ${isCharter ? 'text-gray-300' : 'text-gray-600'}`}>出发时间</Text>
+                  <Text className={`text-sm font-semibold ${isCharter ? 'text-white' : 'text-gray-900'}`}>{schedule.departureTime}</Text>
+                </View>
+                <View className="flex justify-between items-center">
+                  <Text className={`text-sm font-medium ${isCharter ? 'text-gray-300' : 'text-gray-600'}`}>车辆型号</Text>
+                  <Text className={`text-sm font-semibold ${isCharter ? 'text-white' : 'text-gray-900'}`}>{schedule.vehicleType}</Text>
+                </View>
+                <View className="flex justify-between items-center">
+                  <Text className={`text-sm font-medium ${isCharter ? 'text-gray-300' : 'text-gray-600'}`}>上车地点</Text>
+                  <Text className={`text-sm font-semibold ${isCharter ? 'text-white' : 'text-gray-900'} text-right max-w-48`}>{schedule.pickupPoint}</Text>
+                </View>
+                <View className="flex justify-between items-center">
+                  <Text className={`text-sm font-medium ${isCharter ? 'text-gray-300' : 'text-gray-600'}`}>下车地点</Text>
+                  <Text className={`text-sm font-semibold ${isCharter ? 'text-white' : 'text-gray-900'} text-right max-w-48`}>{schedule.dropoffPoint}</Text>
+                </View>
+                <View className="flex justify-between items-center">
+                  <Text className={`text-sm font-medium ${isCharter ? 'text-gray-300' : 'text-gray-600'}`}>
+                    {isCharter ? '包车价格' : '单人票价'}
+                  </Text>
+                  <Text className={`text-base font-bold ${isCharter ? 'text-charter' : 'text-primary'}`}>
+                    ¥{schedule.price}{isCharter ? '/车' : '/人'}
+                  </Text>
+                </View>
+                {!isCharter && (
+                  <View className="flex justify-between items-center">
+                    <Text className="text-sm font-medium text-gray-600">剩余座位</Text>
+                    <Text className="text-sm font-semibold text-gray-900">{schedule.availableSeats}个</Text>
+                  </View>
+                )}
+              </View>
+
+              <View className="bg-blue-50 p-4 rounded-xl border-l-4 border-primary">
+                <Text className="text-xs text-blue-700 font-medium">如需退票,请提前联系客服处理</Text>
+              </View>
+            </CardContent>
+          </Card>
+        </View>
+
+        {/* 手机号获取 */}
+        <View className="px-4 mt-4">
+          <Card className={`${isCharter ? 'bg-gradient-to-br from-charter-dark to-charter-bg border-2 border-charter shadow-charter' : 'bg-white/95 shadow-lg backdrop-blur-md border border-white/40'} rounded-2xl`}>
+            <CardContent className="p-6">
+              <Text className={`text-xl font-bold tracking-wide mb-6 ${isCharter ? 'text-charter' : 'text-gray-900'}`}>联系方式</Text>
+
+              {hasPhoneNumber ? (
+                <View className="bg-green-50 p-4 rounded-xl border-2 border-green-200">
+                  <View className="flex items-center gap-4">
+                    <View className="i-heroicons-phone-20-solid w-5 h-5 text-green-600" />
+                    <Text className="text-base font-semibold text-gray-900 flex-1">{phoneNumber}</Text>
+                    <View className="bg-green-100 text-green-800 px-3 py-1 rounded-full text-xs font-medium">已验证</View>
+                  </View>
+                </View>
+              ) : (
+                <Button
+                  variant="default"
+                  size="lg"
+                  openType="getPhoneNumber"
+                  onGetPhoneNumber={handleGetPhoneNumber}
+                  className="w-full"
+                >
+                  <View className="flex items-center justify-center">
+                    <View className="i-heroicons-phone-20-solid w-5 h-5 mr-2" />
+                    微信一键获取手机号
+                  </View>
+                </Button>
+              )}
+            </CardContent>
+          </Card>
+        </View>
+
+        {/* 乘车人信息 */}
+        <View className="px-4 mt-4">
+          <Card className={`${isCharter ? 'bg-gradient-to-br from-charter-dark to-charter-bg border-2 border-charter shadow-charter' : 'bg-white/95 shadow-lg backdrop-blur-md border border-white/40'} rounded-2xl`}>
+            <CardContent className="p-6">
+              <View className="flex justify-between items-center mb-6">
+                <Text className={`text-xl font-bold tracking-wide ${isCharter ? 'text-charter' : 'text-gray-900'}`}>
+                  {isCharter ? '乘车人信息' : '购票人信息'}
+                </Text>
+                <View className="bg-primary text-white px-3 py-1 rounded-full text-xs font-semibold">
+                  {passengers.length}{isCharter ? '人' : '张票'}
+                </View>
+              </View>
+
+              {passengers.length > 0 && (
+                <View className="mb-6 space-y-3">
+                  {passengers.map((passenger, index) => (
+                    <Card key={index} className="bg-blue-50/80 border border-blue-100 rounded-xl">
+                      <CardContent className="p-4">
+                        <View className="flex justify-between items-start">
+                          <View className="flex-1">
+                            <Text className="text-base font-semibold text-gray-900 mb-2">{passenger.name}</Text>
+                            <View className="flex items-center gap-3 mb-2">
+                              <View className="bg-primary text-white px-3 py-1 rounded-full text-xs font-medium">
+                                {passenger.idType}
+                              </View>
+                              <Text className="text-xs text-gray-600 font-mono">{passenger.idNumber}</Text>
+                            </View>
+                            <Text className="text-xs text-gray-600 font-mono">{passenger.phone}</Text>
+                          </View>
+                          <Button
+                            variant="destructive"
+                            size="sm"
+                            onClick={() => handleDeletePassenger(index)}
+                            className="min-w-16"
+                          >
+                            删除
+                          </Button>
+                        </View>
+                      </CardContent>
+                    </Card>
+                  ))}
+                </View>
+              )}
+
+              <Button
+                variant="outline"
+                size="lg"
+                onClick={handleAddPassenger}
+                className="w-full"
+              >
+                <View className="flex items-center justify-center">
+                  <View className="i-heroicons-plus-20-solid w-5 h-5 mr-2" />
+                  {passengers.length === 0 ? '添加乘车人' : '继续添加'}
+                </View>
+              </Button>
+
+              {!isCharter && passengers.length > 0 && (
+                <View className="text-center mt-4">
+                  <Text className="text-xs text-gray-500">最多可购买{schedule.availableSeats}张票</Text>
+                </View>
+              )}
+            </CardContent>
+          </Card>
+        </View>
+
+        {/* 价格统计 */}
+        <View className="px-4 mt-4 mb-6">
+          <Card className={`${isCharter ? 'bg-gradient-to-br from-charter-dark to-charter-bg border-2 border-charter shadow-charter' : 'bg-white/95 shadow-lg backdrop-blur-md border border-white/40'} rounded-2xl`}>
+            <CardContent className="p-6">
+              <View className="mb-6 space-y-3">
+                {isCharter ? (
+                  <View className="flex justify-between items-center">
+                    <Text className={`text-sm font-medium ${isCharter ? 'text-gray-300' : 'text-gray-600'}`}>包车费用</Text>
+                    <Text className={`text-sm font-semibold ${isCharter ? 'text-white' : 'text-gray-900'}`}>¥{schedule.price}</Text>
+                  </View>
+                ) : (
+                  <View className="flex justify-between items-center">
+                    <Text className="text-sm font-medium text-gray-600">票价 × {passengers.length}人</Text>
+                    <Text className="text-sm font-semibold text-gray-900">¥{schedule.price} × {passengers.length}</Text>
+                  </View>
+                )}
+
+                <View className="flex justify-between items-center">
+                  <Text className={`text-sm font-medium ${isCharter ? 'text-gray-300' : 'text-gray-600'}`}>原价小计</Text>
+                  <Text className={`text-sm font-semibold ${isCharter ? 'text-white' : 'text-gray-900'}`}>¥{originalPrice}</Text>
+                </View>
+              </View>
+
+              <View className="flex justify-between items-center pt-6 border-t border-gray-200">
+                <Text className={`text-xl font-bold tracking-wide ${isCharter ? 'text-charter' : 'text-gray-900'}`}>实付金额</Text>
+                <Text className={`text-3xl font-bold ${isCharter ? 'text-charter' : 'text-primary'}`}>¥{totalPrice}</Text>
+              </View>
+            </CardContent>
+          </Card>
+        </View>
+      </ScrollView>
+
+      {/* 支付按钮 */}
+      <View className="fixed bottom-4 left-4 right-4 z-100">
+        <Button
+          variant="default"
+          size="lg"
+          onClick={handlePay}
+          disabled={createOrderMutation.isPending || createPaymentMutation.isPending}
+          className="w-full"
+        >
+          {createOrderMutation.isPending || createPaymentMutation.isPending ? (
+            <View className="flex items-center justify-center">
+              <View className="i-heroicons-arrow-path-20-solid animate-spin w-5 h-5 mr-2" />
+              处理中...
+            </View>
+          ) : (
+            <View className="flex items-center justify-center">
+              <View className="i-heroicons-credit-card-20-solid w-5 h-5 mr-2" />
+              {isCharter ? '立即包车支付' : '立即购票支付'} ¥{totalPrice}
+            </View>
+          )}
+        </Button>
+      </View>
+
+      {/* 乘车人选择器模态框 */}
+      <Dialog open={showPassengerSelector} onOpenChange={setShowPassengerSelector}>
+        <DialogContent className="max-w-md max-h-80vh overflow-hidden">
+          <DialogHeader>
+            <DialogTitle className="text-lg font-semibold text-gray-900">选择乘车人</DialogTitle>
+          </DialogHeader>
+
+          <ScrollView className="max-h-60vh overflow-y-auto p-1">
+            {!savedPassengers || savedPassengers.length === 0 ? (
+              <View className="text-center py-20">
+                <View className="text-6xl mb-3 opacity-60">👥</View>
+                <Text className="text-lg text-gray-600 mb-1 font-semibold">暂无已保存的乘车人</Text>
+                <Text className="text-sm text-gray-500">添加后可快速选择</Text>
+              </View>
+            ) : (
+              <View className="space-y-3">
+                {savedPassengers.map((passenger) => (
+                  <Card
+                    key={passenger.id}
+                    className="bg-blue-50/80 border border-blue-100 rounded-xl transition-all duration-300 active:translate-y-0.5 active:shadow-md"
+                    onClick={() => handleSelectSavedPassenger(passenger)}
+                  >
+                    <CardContent className="p-4">
+                      <View className="flex items-center justify-between">
+                        <View className="flex-1">
+                          <Text className="text-base font-semibold text-gray-900 mb-2">{passenger.name}</Text>
+                          <View className="flex items-center mb-2">
+                            <View className="bg-primary text-white px-3 py-1 rounded-full text-xs font-medium mr-3">
+                              {passenger.idType}
+                            </View>
+                            <Text className="text-xs text-gray-600 font-mono">{passenger.idNumber}</Text>
+                          </View>
+                          <Text className="text-xs text-gray-600 font-mono">{passenger.phone}</Text>
+                        </View>
+                        <View className="i-heroicons-plus-20-solid w-5 h-5 text-primary ml-4" />
+                      </View>
+                    </CardContent>
+                  </Card>
+                ))}
+              </View>
+            )}
+          </ScrollView>
+
+          <DialogFooter className="pt-4 border-t border-gray-100 bg-gray-50/50 flex flex-row gap-4">
+            <Button
+              variant="outline"
+              onClick={handleAddNewPassenger}
+              className="flex-1"
+            >
+              <View className="flex items-center justify-center">
+                <View className="i-heroicons-plus-20-solid w-4 h-4 mr-2" />
+                添加新乘车人
+              </View>
+            </Button>
+            {savedPassengers && savedPassengers.length > 0 && (
+              <Button
+                onClick={handleManagePassengers}
+                className="flex-1"
+              >
+                <View className="flex items-center justify-center">
+                  <View className="i-heroicons-cog-6-tooth-20-solid w-4 h-4 mr-2" />
+                  管理乘车人
+                </View>
+              </Button>
+            )}
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+    </View>
+  )
+}

+ 5 - 0
mini/src/pages/pay-success/index.config.ts

@@ -0,0 +1,5 @@
+export default definePageConfig({
+  navigationBarTitleText: '支付成功',
+  enableShareAppMessage: true,
+  enableShareTimeline: true
+})

+ 175 - 0
mini/src/pages/pay-success/index.tsx

@@ -0,0 +1,175 @@
+import { View, Text, Button, Image } from '@tarojs/components'
+import { useLoad, useRouter, navigateTo } from '@tarojs/taro'
+import { useState, useEffect } from 'react'
+
+export default function PaySuccessPage() {
+  const router = useRouter()
+  const {
+    totalPrice = '0',
+    passengerCount = '0',
+    orderId = '',
+    orderType = 'bus'
+  } = router.params
+
+  const [assigningDriver, setAssigningDriver] = useState(false)
+  const [driverAssigned, setDriverAssigned] = useState(false)
+  const [driverInfo, setDriverInfo] = useState<any>(null)
+
+  useLoad(() => {
+    // 支付成功后自动分配司机
+    if (orderId) {
+      assignDriver()
+    }
+  })
+
+  // 分配司机
+  const assignDriver = async () => {
+    if (!orderId) {
+      console.warn('订单ID为空,跳过司机分配')
+      return
+    }
+
+    setAssigningDriver(true)
+
+    try {
+      // TODO: 待司机分配API开发完成后替换为真实API调用
+      // const result = await api.drivers.assignToOrder(orderId, orderType)
+      // setDriverInfo(result.data)
+
+      // 模拟司机分配过程
+      setTimeout(() => {
+        const mockDriverInfo = {
+          driverName: '李师傅',
+          rating: 4.9,
+          carNumber: '京A12345',
+          carModel: '奔驰V级商务车',
+          estimatedArrival: {
+            display: '约15分钟'
+          }
+        }
+
+        setAssigningDriver(false)
+        setDriverAssigned(true)
+        setDriverInfo(mockDriverInfo)
+      }, 2000)
+
+    } catch (error) {
+      setAssigningDriver(false)
+      console.error('司机分配失败:', error)
+    }
+  }
+
+  // 查看订单
+  const handleViewOrder = () => {
+    navigateTo({
+      url: '/pages/orders/orders'
+    })
+  }
+
+  // 返回首页
+  const handleBackToHome = () => {
+    navigateTo({
+      url: '/pages/home/index'
+    })
+  }
+
+  return (
+    <View className="min-h-screen bg-gradient-to-b from-blue-500 to-blue-600 p-8 text-center">
+      {/* 成功图标 */}
+      <Image
+        src="/static/success.png"
+        className="w-30 h-30 mb-8 mx-auto"
+        mode="aspectFit"
+      />
+      <Text className="text-white text-4xl font-bold mb-12">支付成功!</Text>
+
+      {/* 支付信息 */}
+      <View className="bg-white/95 rounded-card p-8 mb-8 backdrop-blur-sm">
+        <View className="flex justify-between items-center mb-4">
+          <Text className="text-sm text-gray-600">支付金额</Text>
+          <Text className="text-base font-bold text-gray-900">¥{totalPrice}</Text>
+        </View>
+        <View className="flex justify-between items-center">
+          <Text className="text-sm text-gray-600">购票数量</Text>
+          <Text className="text-base font-bold text-gray-900">{passengerCount}张</Text>
+        </View>
+      </View>
+
+      {/* 积分奖励 */}
+      <View className="bg-white/95 rounded-card p-8 mb-12 backdrop-blur-sm">
+        <Text className="text-base font-bold text-gray-900 mb-6">本次获得奖励</Text>
+        <View className="flex justify-center gap-12 mb-6">
+          <View className="flex flex-col items-center">
+            <Text className="text-4xl mb-3">💰</Text>
+            <Text className="text-3xl font-bold text-blue-600 mb-2">+{Math.floor(parseFloat(totalPrice))}</Text>
+            <Text className="text-xs text-gray-600">积分</Text>
+          </View>
+          <View className="flex flex-col items-center">
+            <Text className="text-4xl mb-3">💎</Text>
+            <Text className="text-3xl font-bold text-blue-600 mb-2">+{Math.floor(parseFloat(totalPrice))}</Text>
+            <Text className="text-xs text-gray-600">会员值</Text>
+          </View>
+        </View>
+        <Text className="text-xs text-gray-500 leading-5">
+          消费1元得1积分+1会员值,积分可兑换优惠券
+        </Text>
+      </View>
+
+      {/* 司机分配状态 */}
+      <View className="mx-8 mb-12">
+        {assigningDriver && (
+          <View className="bg-white/95 rounded-card p-12 text-center backdrop-blur-sm border border-white/40 shadow-lg">
+            <Text className="text-8xl mb-6 animate-bounce">🚗</Text>
+            <Text className="text-base font-semibold text-gray-900 mb-3">正在为您分配司机...</Text>
+            <Text className="text-xs text-gray-600">预计需要1-2分钟</Text>
+          </View>
+        )}
+
+        {driverAssigned && driverInfo && (
+          <View className="bg-white/95 rounded-card p-8 backdrop-blur-sm border border-white/40 shadow-lg animate-slideInUp">
+            <View className="flex items-center justify-center mb-6">
+              <Text className="text-base mr-3">✅</Text>
+              <Text className="text-base font-semibold text-success">司机分配成功</Text>
+            </View>
+            <View className="bg-blue-50 rounded-medium p-6 border border-blue-100">
+              <View className="flex items-center justify-between mb-5 pb-4 border-b border-blue-100">
+                <Text className="text-lg font-semibold text-gray-900">{driverInfo.driverName}</Text>
+                <Text className="text-sm text-warning">⭐ {driverInfo.rating}</Text>
+              </View>
+              <View className="mt-4">
+                <View className="flex items-center mb-3">
+                  <Text className="text-sm text-gray-600 w-28 flex-shrink-0">车牌号:</Text>
+                  <Text className="text-sm text-gray-900 font-medium flex-1">{driverInfo.carNumber}</Text>
+                </View>
+                <View className="flex items-center mb-3">
+                  <Text className="text-sm text-gray-600 w-28 flex-shrink-0">车型:</Text>
+                  <Text className="text-sm text-gray-900 font-medium flex-1">{driverInfo.carModel}</Text>
+                </View>
+                <View className="flex items-center">
+                  <Text className="text-sm text-gray-600 w-28 flex-shrink-0">预计到达:</Text>
+                  <Text className="text-sm text-gray-900 font-medium flex-1">{driverInfo.estimatedArrival.display}</Text>
+                </View>
+              </View>
+            </View>
+          </View>
+        )}
+      </View>
+
+      {/* 操作按钮 */}
+      <View className="flex gap-6">
+        <Button
+          className="flex-1 bg-white/90 text-blue-600 border-2 border-blue-600 rounded-button py-6 text-base font-bold backdrop-blur-sm"
+          onClick={handleBackToHome}
+        >
+          返回首页
+        </Button>
+        <Button
+          className="flex-1 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-button py-6 text-base font-bold shadow-lg"
+          onClick={handleViewOrder}
+        >
+          查看订单
+        </Button>
+      </View>
+    </View>
+  )
+}

+ 5 - 8
mini/src/pages/schedule-list/ScheduleListPage.tsx

@@ -1,6 +1,6 @@
 import React, { useState, useEffect } from 'react'
 import React, { useState, useEffect } from 'react'
 import { View, Text, ScrollView, Button } from '@tarojs/components'
 import { View, Text, ScrollView, Button } from '@tarojs/components'
-import { useRouter } from '@tarojs/taro'
+import { useRouter, navigateTo } from '@tarojs/taro'
 import { useQuery } from '@tanstack/react-query'
 import { useQuery } from '@tanstack/react-query'
 import { routeClient } from '../../api'
 import { routeClient } from '../../api'
 
 
@@ -124,13 +124,10 @@ const ScheduleListPage: React.FC = () => {
 
 
   // 预订路线
   // 预订路线
   const handleBookRoute = (route: Route) => {
   const handleBookRoute = (route: Route) => {
-    // 这里可以导航到订单确认页面
-    console.log('预订路线:', route)
-    // 临时提示
-    // Taro.showToast({
-    //   title: '预订功能开发中',
-    //   icon: 'none'
-    // })
+    // 导航到订单确认页面
+    navigateTo({
+      url: `/pages/order/index?routeId=${route.id}&activityName=${encodeURIComponent(activityName)}&type=${route.travelMode}`
+    })
   }
   }
 
 
   // 获取车辆类型显示名称
   // 获取车辆类型显示名称

+ 80 - 0
mini/src/types/order.types.ts

@@ -0,0 +1,80 @@
+// 订单状态枚举
+export enum OrderStatus {
+  PENDING_PAYMENT = '待支付',
+  WAITING_DEPARTURE = '待出发',
+  IN_PROGRESS = '行程中',
+  COMPLETED = '已完成',
+  CANCELLED = '已取消'
+}
+
+// 支付状态枚举
+export enum PaymentStatus {
+  PENDING = '待支付',
+  PAID = '已支付',
+  FAILED = '支付失败',
+  REFUNDED = '已退款'
+}
+
+// 乘客信息接口
+export interface Passenger {
+  id?: number
+  name: string
+  idType: string
+  idNumber: string
+  phone: string
+}
+
+// 路线快照接口
+export interface RouteSnapshot {
+  id: number
+  price: number
+  availableSeats: number
+  departureTime: string
+  vehicleModel: string
+  startLocation: string
+  endLocation: string
+  duration: string
+  refundPolicy: string
+  type?: string
+}
+
+// 订单接口
+export interface Order {
+  id: number
+  userId: number
+  routeId: number
+  passengerCount: number
+  totalAmount: number
+  status: OrderStatus
+  paymentStatus: PaymentStatus
+  passengerSnapshots: Passenger[]
+  routeSnapshot: RouteSnapshot
+  createdAt: Date
+  updatedAt: Date
+}
+
+// 订单创建请求接口
+export interface OrderCreateRequest {
+  routeId: number
+  passengerCount: number
+  passengerSnapshots: Passenger[]
+  totalAmount: number
+}
+
+// 支付创建请求接口
+export interface PaymentCreateRequest {
+  orderId: number
+  totalAmount: number
+  description: string
+  openid?: string
+}
+
+// 支付创建响应接口
+export interface PaymentCreateResponse {
+  paymentId: string
+  timeStamp: string
+  nonceStr: string
+  package: string
+  signType: string
+  paySign: string
+}

+ 23 - 0
mini/src/types/route.types.ts

@@ -0,0 +1,23 @@
+// 路线接口
+export interface Route {
+  id: number
+  price: number
+  availableSeats: number
+  departureTime: string
+  vehicleModel: string
+  startLocation: string
+  endLocation: string
+  duration: string
+  refundPolicy: string
+  type?: string
+}
+
+// 路线查询参数接口
+export interface RouteQueryParams {
+  activityId?: number
+  startPoint?: string
+  endPoint?: string
+  date?: Date
+  sortBy?: 'price' | 'departureTime'
+  sortOrder?: 'asc' | 'desc'
+}

+ 193 - 1
packages/server/src/api/routes/index.ts

@@ -271,6 +271,169 @@ const searchRoutesRoute = createRoute({
   }
   }
 });
 });
 
 
+// 获取单个路线详情Schema
+const getRouteSchema = z.object({
+  id: z.coerce.number<number>().int().positive('路线ID必须为正整数')
+});
+
+// 单个路线响应Schema
+const routeResponseSchema = z.object({
+  success: z.boolean(),
+  data: z.object({
+    id: z.number(),
+    name: z.string(),
+    description: z.string().nullable(),
+    startLocationId: z.number(),
+    endLocationId: z.number(),
+    startLocation: z.object({
+      id: z.number(),
+      name: z.string(),
+      provinceId: z.number(),
+      cityId: z.number(),
+      districtId: z.number(),
+      address: z.string(),
+      province: z.object({
+        id: z.number(),
+        name: z.string(),
+        level: z.number(),
+        code: z.string(),
+        isDisabled: z.number(),
+        isDeleted: z.number(),
+        createdBy: z.number().nullable(),
+        updatedBy: z.number().nullable(),
+        createdAt: z.string(),
+        updatedAt: z.string()
+      }).nullable(),
+      city: z.object({
+        id: z.number(),
+        name: z.string(),
+        level: z.number(),
+        code: z.string(),
+        isDisabled: z.number(),
+        isDeleted: z.number(),
+        createdBy: z.number().nullable(),
+        updatedBy: z.number().nullable(),
+        createdAt: z.string(),
+        updatedAt: z.string()
+      }).nullable(),
+      district: z.object({
+        id: z.number(),
+        name: z.string(),
+        level: z.number(),
+        code: z.string(),
+        isDisabled: z.number(),
+        isDeleted: z.number(),
+        createdBy: z.number().nullable(),
+        updatedBy: z.number().nullable(),
+        createdAt: z.string(),
+        updatedAt: z.string()
+      }).nullable()
+    }),
+    endLocation: z.object({
+      id: z.number(),
+      name: z.string(),
+      provinceId: z.number(),
+      cityId: z.number(),
+      districtId: z.number(),
+      address: z.string(),
+      province: z.object({
+        id: z.number(),
+        name: z.string(),
+        level: z.number(),
+        code: z.string(),
+        isDisabled: z.number(),
+        isDeleted: z.number(),
+        createdBy: z.number().nullable(),
+        updatedBy: z.number().nullable(),
+        createdAt: z.string(),
+        updatedAt: z.string()
+      }).nullable(),
+      city: z.object({
+        id: z.number(),
+        name: z.string(),
+        level: z.number(),
+        code: z.string(),
+        isDisabled: z.number(),
+        isDeleted: z.number(),
+        createdBy: z.number().nullable(),
+        updatedBy: z.number().nullable(),
+        createdAt: z.string(),
+        updatedAt: z.string()
+      }).nullable(),
+      district: z.object({
+        id: z.number(),
+        name: z.string(),
+        level: z.number(),
+        code: z.string(),
+        isDisabled: z.number(),
+        isDeleted: z.number(),
+        createdBy: z.number().nullable(),
+        updatedBy: z.number().nullable(),
+        createdAt: z.string(),
+        updatedAt: z.string()
+      }).nullable()
+    }),
+    pickupPoint: z.string(),
+    dropoffPoint: z.string(),
+    departureTime: z.string(),
+    vehicleType: z.nativeEnum(VehicleType),
+    travelMode: z.nativeEnum(TravelMode),
+    price: z.number(),
+    seatCount: z.number(),
+    availableSeats: z.number(),
+    activityId: z.number(),
+    activity: z.object({
+      id: z.number(),
+      name: z.string(),
+      description: z.string().nullable(),
+      venueLocationId: z.number(),
+      venueLocation: z.object({
+        id: z.number(),
+        name: z.string(),
+        provinceId: z.number(),
+        cityId: z.number(),
+        districtId: z.number(),
+        address: z.string()
+      }),
+      startDate: z.string(),
+      endDate: z.string()
+    }),
+    routeType: z.enum(['departure', 'return']),
+    isDisabled: z.number(),
+    isDeleted: z.number(),
+    createdBy: z.number().nullable(),
+    updatedBy: z.number().nullable(),
+    createdAt: z.string(),
+    updatedAt: z.string()
+  }),
+  message: z.string()
+});
+
+// 创建获取单个路线详情路由
+const getRouteRoute = createRoute({
+  method: 'get',
+  path: '/{id}',
+  request: {
+    params: getRouteSchema
+  },
+  responses: {
+    200: {
+      description: '获取路线详情成功',
+      content: {
+        'application/json': { schema: routeResponseSchema }
+      }
+    },
+    404: {
+      description: '路线不存在',
+      content: { 'application/json': { schema: errorSchema } }
+    },
+    500: {
+      description: '获取路线详情失败',
+      content: { 'application/json': { schema: errorSchema } }
+    }
+  }
+});
+
 const app = new OpenAPIHono()
 const app = new OpenAPIHono()
   .openapi(searchRoutesRoute, async (c) => {
   .openapi(searchRoutesRoute, async (c) => {
     try {
     try {
@@ -340,6 +503,35 @@ const app = new OpenAPIHono()
         message: error instanceof Error ? error.message : '搜索路线失败'
         message: error instanceof Error ? error.message : '搜索路线失败'
       }, 500);
       }, 500);
     }
     }
-  });
+  })
+  .openapi(getRouteRoute, async (c) => {
+  try {
+    const { id } = c.req.valid('param');
+
+    // 初始化路线服务
+    const routeService = new RouteService(AppDataSource);
+
+    // 获取路线详情
+    const route = await routeService.getRouteById(id);
+    if (!route) {
+      return c.json({
+        code: 404,
+        message: '路线不存在'
+      }, 404);
+    }
+
+    return c.json({
+      success: true,
+      data: route,
+      message: '获取路线详情成功'
+    }, 200);
+  } catch (error) {
+    console.error('获取路线详情失败:', error);
+    return c.json({
+      code: 500,
+      message: error instanceof Error ? error.message : '获取路线详情失败'
+    }, 500);
+  }
+});
 
 
 export default app;
 export default app;