Ver código fonte

✨ feat(passengers): 新增乘车人管理功能

- 添加乘车人管理页面,支持增删改查操作
- 新增添加乘车人页面,支持表单验证和保存
- 扩展UI组件库,新增Dialog、Select组件和CardTitle组件
- 在个人资料页面添加乘车人管理入口
- 更新API客户端,添加乘客相关接口支持
- 重构证件类型枚举,提取到共享类型文件中
- 优化webpack配置,修复未使用变量警告
yourname 3 meses atrás
pai
commit
d6d517e974

+ 1 - 1
mini/config/dev.ts

@@ -18,7 +18,7 @@ export default {
       },
       open: false
     },
-    webpackChain(chain, webpack) {  
+    webpackChain(chain, _webpack) {  
       // 确保在 HtmlWebpackPlugin 之后添加  
       chain  
         .plugin('iframeCommunicationPlugin')  

+ 4 - 2
mini/src/api.ts

@@ -1,5 +1,5 @@
 // @ts-ignore
-import type { AuthRoutes, UserRoutes, RoleRoutes, FileRoutes, AreasUserRoutes, LocationsUserRoutes, RoutesRoutes } from '@/server/api'
+import type { AuthRoutes, UserRoutes, RoleRoutes, FileRoutes, AreasUserRoutes, LocationsUserRoutes, RoutesRoutes, PassengersRoutes } from '@/server/api'
 import { rpcClient } from './utils/rpc-client'
 
 // 创建各个模块的RPC客户端
@@ -16,4 +16,6 @@ export const areaClient = rpcClient<AreasUserRoutes>().api.v1.areas
 // @ts-ignore
 export const locationClient = rpcClient<LocationsUserRoutes>().api.v1.locations
 // @ts-ignore
-export const routeClient = rpcClient<RoutesRoutes>().api.v1.routes
+export const routeClient = rpcClient<RoutesRoutes>().api.v1.routes
+// @ts-ignore
+export const passengerClient = rpcClient<PassengersRoutes>().api.v1.passengers

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

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

+ 13 - 0
mini/src/components/ui/card.tsx

@@ -51,4 +51,17 @@ export function CardFooter({ className, children }: CardFooterProps) {
       {children}
     </View>
   )
+}
+
+interface CardTitleProps {
+  className?: string
+  children: React.ReactNode
+}
+
+export function CardTitle({ className, children }: CardTitleProps) {
+  return (
+    <View className={cn("text-lg font-semibold", className)}>
+      {children}
+    </View>
+  )
 }

+ 81 - 0
mini/src/components/ui/dialog.tsx

@@ -0,0 +1,81 @@
+import { useState, useEffect } from 'react'
+import { View, Text } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { cn } from '@/utils/cn'
+
+interface DialogProps {
+  open: boolean
+  onOpenChange: (open: boolean) => void
+  children: React.ReactNode
+}
+
+export function Dialog({ open, onOpenChange, children }: DialogProps) {
+  useEffect(() => {
+    if (open) {
+      // 在 Taro 中,我们可以使用模态框或者自定义弹窗
+      // 这里使用自定义实现
+    }
+  }, [open])
+
+  if (!open) return null
+
+  return (
+    <View className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
+      <View className="relative bg-white rounded-lg shadow-lg max-w-md w-full mx-4">
+        {children}
+      </View>
+    </View>
+  )
+}
+
+interface DialogContentProps {
+  className?: string
+  children: React.ReactNode
+}
+
+export function DialogContent({ className, children }: DialogContentProps) {
+  return (
+    <View className={cn("p-6", className)}>
+      {children}
+    </View>
+  )
+}
+
+interface DialogHeaderProps {
+  className?: string
+  children: React.ReactNode
+}
+
+export function DialogHeader({ className, children }: DialogHeaderProps) {
+  return (
+    <View className={cn("mb-4", className)}>
+      {children}
+    </View>
+  )
+}
+
+interface DialogTitleProps {
+  className?: string
+  children: React.ReactNode
+}
+
+export function DialogTitle({ className, children }: DialogTitleProps) {
+  return (
+    <Text className={cn("text-lg font-semibold text-gray-900", className)}>
+      {children}
+    </Text>
+  )
+}
+
+interface DialogFooterProps {
+  className?: string
+  children: React.ReactNode
+}
+
+export function DialogFooter({ className, children }: DialogFooterProps) {
+  return (
+    <View className={cn("flex justify-end space-x-2", className)}>
+      {children}
+    </View>
+  )
+}

+ 96 - 0
mini/src/components/ui/select.tsx

@@ -0,0 +1,96 @@
+import { useState } from 'react'
+import { View, Text } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { cn } from '@/utils/cn'
+
+interface SelectProps {
+  value: string
+  onValueChange: (value: string) => void
+  children: React.ReactNode
+}
+
+export function Select({ value, onValueChange, children }: SelectProps) {
+  const [open, setOpen] = useState(false)
+
+  const handleSelect = (newValue: string) => {
+    onValueChange(newValue)
+    setOpen(false)
+  }
+
+  return (
+    <View className="relative">
+      {children}
+    </View>
+  )
+}
+
+interface SelectTriggerProps {
+  className?: string
+  children: React.ReactNode
+}
+
+export function SelectTrigger({ className, children }: SelectTriggerProps) {
+  return (
+    <View
+      className={cn(
+        "flex h-10 w-full items-center justify-between rounded-md border border-gray-300 bg-white px-3 py-2 text-sm",
+        className
+      )}
+    >
+      {children}
+    </View>
+  )
+}
+
+interface SelectValueProps {
+  placeholder?: string
+}
+
+export function SelectValue({ placeholder }: SelectValueProps) {
+  return (
+    <Text className="text-gray-700">
+      {placeholder || '选择...'}
+    </Text>
+  )
+}
+
+interface SelectContentProps {
+  className?: string
+  children: React.ReactNode
+}
+
+export function SelectContent({ className, children }: SelectContentProps) {
+  return (
+    <View
+      className={cn(
+        "absolute z-50 mt-1 w-full rounded-md border border-gray-300 bg-white shadow-lg",
+        className
+      )}
+    >
+      {children}
+    </View>
+  )
+}
+
+interface SelectItemProps {
+  value: string
+  className?: string
+  children: React.ReactNode
+}
+
+export function SelectItem({ value, className, children }: SelectItemProps) {
+  return (
+    <View
+      className={cn(
+        "relative flex w-full cursor-pointer select-none items-center rounded-sm py-2 px-3 text-sm outline-none hover:bg-gray-100",
+        className
+      )}
+      onClick={() => {
+        // 这里需要从父组件传递onValueChange
+        console.log('Selected:', value)
+      }}
+    >
+      {children}
+    </View>
+  )
+}

+ 6 - 0
mini/src/pages/passengers/add-passenger.config.ts

@@ -0,0 +1,6 @@
+export default definePageConfig({
+  navigationBarTitleText: '添加乘车人',
+  enablePullDownRefresh: false,
+  backgroundColor: '#f8f9fa',
+  navigationStyle: 'default'
+})

+ 236 - 0
mini/src/pages/passengers/add-passenger.tsx

@@ -0,0 +1,236 @@
+import { useState, useEffect } from 'react'
+import { View, Text, ScrollView } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { Navbar } from '@/components/ui/navbar'
+import { Button } from '@/components/ui/button'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Input } from '@/components/ui/input'
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
+import { Label } from '@/components/ui/label'
+import { useAuth } from '@/utils/auth'
+import { passengerClient } from '@/api'
+import { IdType } from '@/types/passenger.types'
+
+interface PassengerFormData {
+  name: string
+  idType: IdType
+  idNumber: string
+  phone: string
+}
+
+const AddPassengerPage: React.FC = () => {
+  const { user } = useAuth()
+  const queryClient = useQueryClient()
+
+  const [formData, setFormData] = useState<PassengerFormData>({
+    name: '',
+    idType: IdType.ID_CARD,
+    idNumber: '',
+    phone: ''
+  })
+
+  const [from, setFrom] = useState<string>('order')
+
+  // 获取页面参数
+  useEffect(() => {
+    const currentPage = Taro.getCurrentPages().pop()
+    if (currentPage?.options?.from) {
+      setFrom(currentPage.options.from)
+    }
+  }, [])
+
+  // 添加乘客
+  const addMutation = useMutation({
+    mutationFn: async (passengerData: PassengerFormData) => {
+      const response = await passengerClient.$post({ json: passengerData })
+      return await response.json()
+    },
+    onSuccess: (savedPassenger) => {
+      queryClient.invalidateQueries({ queryKey: ['passengers'] })
+      Taro.showToast({ title: '保存成功', icon: 'success' })
+
+      // 如果来自订单页面,传递数据回上一页
+      if (from === 'order') {
+        const pages = Taro.getCurrentPages()
+        const prevPage = pages[pages.length - 2]
+
+        if (prevPage) {
+          // 通过事件总线或全局状态传递数据
+          // 这里使用简单的回调方式
+          const eventChannel = Taro.getCurrentInstance().page?.getOpenerEventChannel?.()
+          if (eventChannel) {
+            eventChannel.emit('passengerAdded', savedPassenger)
+          }
+        }
+      }
+
+      setTimeout(() => {
+        Taro.navigateBack()
+      }, 1500)
+    },
+    onError: (error) => {
+      Taro.showToast({
+        title: '保存失败',
+        icon: 'none'
+      })
+      console.error('保存乘客失败:', error)
+    }
+  })
+
+  // 保存乘客
+  const savePassenger = () => {
+    // 基本验证
+    if (!formData.name.trim()) {
+      Taro.showToast({ title: '请输入姓名', icon: 'none' })
+      return
+    }
+
+    if (!formData.idNumber.trim()) {
+      Taro.showToast({ title: '请输入证件号码', icon: 'none' })
+      return
+    }
+
+    if (!formData.phone.trim()) {
+      Taro.showToast({ title: '请输入手机号', icon: 'none' })
+      return
+    }
+
+    // 手机号格式验证
+    const phoneRegex = /^1[3-9]\d{9}$/
+    if (!phoneRegex.test(formData.phone)) {
+      Taro.showToast({ title: '请输入正确的手机号', icon: 'none' })
+      return
+    }
+
+    addMutation.mutate(formData)
+  }
+
+  if (!user) {
+    return (
+      <View className="flex-1 flex flex-col items-center justify-center">
+        <View className="flex flex-col items-center">
+          <View className="i-heroicons-exclamation-circle-20-solid w-12 h-12 text-gray-400 mx-auto mb-4" />
+          <Text className="text-gray-600 mb-4">请先登录</Text>
+          <Button
+            variant="default"
+            size="lg"
+            onClick={() => Taro.navigateTo({ url: '/pages/login/index' })}
+          >
+            去登录
+          </Button>
+        </View>
+      </View>
+    )
+  }
+
+  return (
+    <View className="flex-1 bg-gray-50 min-h-screen">
+      <Navbar
+        title="添加乘车人"
+        leftIcon="i-heroicons-arrow-left-20-solid"
+        onClickLeft={() => Taro.navigateBack()}
+      />
+
+      <ScrollView className="flex-1 px-4 py-4">
+        <Card className="bg-white rounded-2xl shadow-sm">
+          <CardHeader>
+            <CardTitle className="text-lg">乘车人信息</CardTitle>
+          </CardHeader>
+          <CardContent className="space-y-4">
+            {/* 姓名 */}
+            <View className="space-y-2">
+              <Label htmlFor="name">姓名</Label>
+              <Input
+                id="name"
+                placeholder="请输入姓名"
+                value={formData.name}
+                onChange={(e) => setFormData({ ...formData, name: e.target.value })}
+                className="w-full"
+              />
+            </View>
+
+            {/* 证件类型 */}
+            <View className="space-y-2">
+              <Label htmlFor="idType">证件类型</Label>
+              <Select
+                value={formData.idType}
+                onValueChange={(value: IdType) => setFormData({ ...formData, idType: value })}
+              >
+                <SelectTrigger>
+                  <SelectValue placeholder="选择证件类型" />
+                </SelectTrigger>
+                <SelectContent>
+                  {Object.values(IdType).map((type) => (
+                    <SelectItem key={type} value={type}>
+                      {type}
+                    </SelectItem>
+                  ))}
+                </SelectContent>
+              </Select>
+            </View>
+
+            {/* 证件号码 */}
+            <View className="space-y-2">
+              <Label htmlFor="idNumber">证件号码</Label>
+              <Input
+                id="idNumber"
+                placeholder="请输入证件号码"
+                value={formData.idNumber}
+                onChange={(e) => setFormData({ ...formData, idNumber: e.target.value })}
+                className="w-full"
+              />
+            </View>
+
+            {/* 手机号 */}
+            <View className="space-y-2">
+              <Label htmlFor="phone">手机号</Label>
+              <Input
+                id="phone"
+                placeholder="请输入手机号"
+                value={formData.phone}
+                onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
+                type="number"
+                maxLength={11}
+                className="w-full"
+              />
+            </View>
+          </CardContent>
+        </Card>
+
+        {/* 提示信息 */}
+        <View className="mt-4 p-4 bg-blue-50 rounded-xl border border-blue-200">
+          <Text className="text-sm text-blue-700">
+            • 请确保填写的信息准确无误
+          </Text>
+          <Text className="text-sm text-blue-700 mt-1">
+            • 证件号码将用于实名认证
+          </Text>
+          <Text className="text-sm text-blue-700 mt-1">
+            • 手机号将用于接收出行通知
+          </Text>
+        </View>
+      </ScrollView>
+
+      {/* 保存按钮 */}
+      <View className="p-4 bg-white border-t border-gray-200">
+        <Button
+          className="w-full"
+          onClick={savePassenger}
+          disabled={addMutation.isPending}
+        >
+          {addMutation.isPending ? (
+            <View className="flex items-center justify-center">
+              <View className="i-heroicons-arrow-path-20-solid animate-spin w-4 h-4 mr-2" />
+              保存中...
+            </View>
+          ) : (
+            '保存'
+          )}
+        </Button>
+      </View>
+    </View>
+  )
+}
+
+export default AddPassengerPage

+ 6 - 0
mini/src/pages/passengers/passengers.config.ts

@@ -0,0 +1,6 @@
+export default definePageConfig({
+  navigationBarTitleText: '乘车人管理',
+  enablePullDownRefresh: false,
+  backgroundColor: '#f8f9fa',
+  navigationStyle: 'default'
+})

+ 474 - 0
mini/src/pages/passengers/passengers.tsx

@@ -0,0 +1,474 @@
+import { useState, useEffect } from 'react'
+import { View, Text, ScrollView, Input, Button as TaroButton } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
+import { Navbar } from '@/components/ui/navbar'
+import { Button } from '@/components/ui/button'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Input as ShadcnInput } from '@/components/ui/input'
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
+import { Label } from '@/components/ui/label'
+import { useAuth } from '@/utils/auth'
+import { passengerClient } from '@/api'
+import { IdType } from '@/types/passenger.types'
+
+interface Passenger {
+  id: number
+  name: string
+  idType: IdType
+  idNumber: string
+  phone: string
+  isDefault: boolean
+  createdAt: string
+  updatedAt: string
+}
+
+const PassengersPage: React.FC = () => {
+  const { user } = useAuth()
+  const queryClient = useQueryClient()
+
+  const [searchKeyword, setSearchKeyword] = useState('')
+  const [showSearchBar, setShowSearchBar] = useState(false)
+  const [showAddModal, setShowAddModal] = useState(false)
+  const [editingPassenger, setEditingPassenger] = useState<Passenger | null>(null)
+  const [formData, setFormData] = useState({
+    name: '',
+    idType: IdType.ID_CARD,
+    idNumber: '',
+    phone: ''
+  })
+
+  // 获取乘客列表
+  const { data: passengers = [], isLoading } = useQuery({
+    queryKey: ['passengers'],
+    queryFn: async () => {
+      const response = await passengerClient.$get()
+      return await response.json()
+    },
+    enabled: !!user
+  })
+
+  // 添加乘客
+  const addMutation = useMutation({
+    mutationFn: async (passengerData: Omit<Passenger, 'id' | 'createdAt' | 'updatedAt' | 'isDefault'>) => {
+      const response = await passengerClient.$post({ json: passengerData })
+      return await response.json()
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['passengers'] })
+      setShowAddModal(false)
+      resetForm()
+      Taro.showToast({ title: '添加成功', icon: 'success' })
+    },
+    onError: (error) => {
+      Taro.showToast({ title: '添加失败', icon: 'none' })
+      console.error('添加乘客失败:', error)
+    }
+  })
+
+  // 更新乘客
+  const updateMutation = useMutation({
+    mutationFn: async ({ id, data }: { id: number; data: Partial<Passenger> }) => {
+      const response = await passengerClient[':id'].$patch({
+        param: { id: id.toString() },
+        json: data
+      })
+      return await response.json()
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['passengers'] })
+      setShowAddModal(false)
+      resetForm()
+      Taro.showToast({ title: '更新成功', icon: 'success' })
+    },
+    onError: (error) => {
+      Taro.showToast({ title: '更新失败', icon: 'none' })
+      console.error('更新乘客失败:', error)
+    }
+  })
+
+  // 删除乘客
+  const deleteMutation = useMutation({
+    mutationFn: async (id: number) => {
+      await passengerClient[':id'].$delete({ param: { id: id.toString() } })
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['passengers'] })
+      Taro.showToast({ title: '删除成功', icon: 'success' })
+    },
+    onError: (error) => {
+      Taro.showToast({ title: '删除失败', icon: 'none' })
+      console.error('删除乘客失败:', error)
+    }
+  })
+
+  // 设置默认乘客
+  const setDefaultMutation = useMutation({
+    mutationFn: async (id: number) => {
+      const response = await passengerClient[':id']['set-default'].$post({
+        param: { id: id.toString() }
+      })
+      return await response.json()
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['passengers'] })
+      Taro.showToast({ title: '设置成功', icon: 'success' })
+    },
+    onError: (error) => {
+      Taro.showToast({ title: '设置失败', icon: 'none' })
+      console.error('设置默认乘客失败:', error)
+    }
+  })
+
+  // 重置表单
+  const resetForm = () => {
+    setFormData({
+      name: '',
+      idType: IdType.ID_CARD,
+      idNumber: '',
+      phone: ''
+    })
+    setEditingPassenger(null)
+  }
+
+  // 过滤乘客列表
+  const filteredPassengers = passengers.filter(passenger => {
+    if (!searchKeyword) return true
+    const keyword = searchKeyword.toLowerCase()
+    return (
+      passenger.name.toLowerCase().includes(keyword) ||
+      passenger.phone.includes(keyword) ||
+      passenger.idNumber.includes(keyword)
+    )
+  })
+
+  // 显示添加模态框
+  const showAddPassenger = () => {
+    resetForm()
+    setShowAddModal(true)
+  }
+
+  // 编辑乘客
+  const editPassenger = (passenger: Passenger) => {
+    setFormData({
+      name: passenger.name,
+      idType: passenger.idType,
+      idNumber: passenger.idNumber,
+      phone: passenger.phone
+    })
+    setEditingPassenger(passenger)
+    setShowAddModal(true)
+  }
+
+  // 删除乘客
+  const deletePassenger = (passenger: Passenger) => {
+    Taro.showModal({
+      title: '确认删除',
+      content: `确定要删除乘车人"${passenger.name}"吗?`,
+      confirmText: '删除',
+      confirmColor: '#ff4d4f',
+      success: (res) => {
+        if (res.confirm) {
+          deleteMutation.mutate(passenger.id)
+        }
+      }
+    })
+  }
+
+  // 设置默认乘客
+  const setDefaultPassenger = (passenger: Passenger) => {
+    setDefaultMutation.mutate(passenger.id)
+  }
+
+  // 保存乘客
+  const savePassenger = () => {
+    // 基本验证
+    if (!formData.name.trim()) {
+      Taro.showToast({ title: '请输入姓名', icon: 'none' })
+      return
+    }
+
+    if (!formData.idNumber.trim()) {
+      Taro.showToast({ title: '请输入证件号码', icon: 'none' })
+      return
+    }
+
+    if (!formData.phone.trim()) {
+      Taro.showToast({ title: '请输入手机号', icon: 'none' })
+      return
+    }
+
+    if (editingPassenger) {
+      updateMutation.mutate({
+        id: editingPassenger.id,
+        data: formData
+      })
+    } else {
+      addMutation.mutate(formData)
+    }
+  }
+
+  // 查看乘客详情
+  const viewPassengerDetail = (passenger: Passenger) => {
+    Taro.showModal({
+      title: '乘车人详情',
+      content: `姓名:${passenger.name}\n证件类型:${passenger.idType}\n证件号码:${passenger.idNumber}\n手机号:${passenger.phone}\n创建时间:${new Date(passenger.createdAt).toLocaleString('zh-CN')}`,
+      showCancel: false,
+      confirmText: '知道了'
+    })
+  }
+
+  // 跳转到独立添加页面
+  const navigateToAddPage = () => {
+    Taro.navigateTo({
+      url: '/pages/passengers/add-passenger?from=management'
+    })
+  }
+
+  if (!user) {
+    return (
+      <View className="flex-1 flex flex-col items-center justify-center">
+        <View className="flex flex-col items-center">
+          <View className="i-heroicons-exclamation-circle-20-solid w-12 h-12 text-gray-400 mx-auto mb-4" />
+          <Text className="text-gray-600 mb-4">请先登录</Text>
+          <Button
+            variant="default"
+            size="lg"
+            onClick={() => Taro.navigateTo({ url: '/pages/login/index' })}
+          >
+            去登录
+          </Button>
+        </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()}
+      />
+
+      {/* 顶部操作栏 */}
+      <View className="px-4 py-4 bg-white shadow-sm">
+        <View className="mb-3">
+          <Button
+            variant="outline"
+            className="w-full"
+            onClick={() => setShowSearchBar(!showSearchBar)}
+          >
+            <View className="flex items-center justify-center">
+              <View className="i-heroicons-magnifying-glass-20-solid w-4 h-4 mr-2" />
+              搜索乘车人
+            </View>
+          </Button>
+        </View>
+
+        {showSearchBar && (
+          <View className="animate-slide-down">
+            <ShadcnInput
+              placeholder="输入姓名、手机号或证件号"
+              value={searchKeyword}
+              onChange={(e) => setSearchKeyword(e.target.value)}
+              className="w-full"
+            />
+          </View>
+        )}
+      </View>
+
+      {/* 乘车人列表 */}
+      <ScrollView className="px-4 mt-3 flex-1">
+        {isLoading ? (
+          <View className="flex items-center justify-center py-20">
+            <View className="i-heroicons-arrow-path-20-solid animate-spin w-8 h-8 text-blue-500" />
+            <Text className="text-gray-500 mt-2">加载中...</Text>
+          </View>
+        ) : filteredPassengers.length === 0 ? (
+          <View className="text-center py-20 bg-white/80 rounded-2xl my-4 backdrop-blur-sm border border-white/30">
+            <View className="text-6xl mb-3 opacity-60">👥</View>
+            <Text className="text-lg text-gray-600 mb-1 font-semibold">
+              {searchKeyword ? '没有找到相关乘车人' : '暂无乘车人'}
+            </Text>
+            <Text className="text-sm text-gray-500">
+              {searchKeyword ? '试试其他关键词' : '点击下方按钮添加常用乘车人'}
+            </Text>
+          </View>
+        ) : (
+          <View className="space-y-3">
+            {filteredPassengers.map((passenger) => (
+              <Card
+                key={passenger.id}
+                className="bg-white/95 rounded-2xl shadow-lg backdrop-blur-md border border-white/40 transition-all duration-300 active:translate-y-0.5 active:shadow-md"
+              >
+                <CardContent className="p-4">
+                  <View
+                    className="mb-3"
+                    onClick={() => viewPassengerDetail(passenger)}
+                  >
+                    <View className="flex items-center justify-between mb-2">
+                      <View className="flex items-center space-x-2">
+                        <Text className="text-lg font-semibold text-gray-900 tracking-wide">
+                          {passenger.name}
+                        </Text>
+                        {passenger.isDefault && (
+                          <View className="bg-green-100 text-green-800 text-xs px-2 py-1 rounded-full font-medium">
+                            默认
+                          </View>
+                        )}
+                      </View>
+                      <View className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full font-medium">
+                        {passenger.idType}
+                      </View>
+                    </View>
+
+                    <View className="bg-blue-50 rounded-xl p-3 border border-blue-100">
+                      <Text className="text-sm text-gray-700 mb-1 font-mono tracking-wide">
+                        {passenger.idNumber}
+                      </Text>
+                      <Text className="text-sm text-gray-600 font-mono tracking-wide">
+                        {passenger.phone}
+                      </Text>
+                    </View>
+                  </View>
+
+                  <View className="flex justify-end space-x-2">
+                    {!passenger.isDefault && (
+                      <Button
+                        variant="outline"
+                        size="sm"
+                        onClick={() => setDefaultPassenger(passenger)}
+                        className="min-w-20"
+                      >
+                        设默认
+                      </Button>
+                    )}
+                    <Button
+                      variant="default"
+                      size="sm"
+                      onClick={() => editPassenger(passenger)}
+                      className="min-w-20"
+                    >
+                      编辑
+                    </Button>
+                    <Button
+                      variant="destructive"
+                      size="sm"
+                      onClick={() => deletePassenger(passenger)}
+                      className="min-w-20"
+                    >
+                      删除
+                    </Button>
+                  </View>
+                </CardContent>
+              </Card>
+            ))}
+          </View>
+        )}
+      </ScrollView>
+
+      {/* 添加按钮 */}
+      <View className="fixed bottom-4 left-4 right-4 z-100">
+        <Button
+          className="w-full"
+          onClick={showAddPassenger}
+        >
+          <View className="flex items-center justify-center">
+            <View className="text-lg mr-2">➕</View>
+            添加乘车人
+          </View>
+        </Button>
+      </View>
+
+      {/* 添加/编辑乘车人模态框 */}
+      <Dialog open={showAddModal} onOpenChange={setShowAddModal}>
+        <DialogContent className="max-w-md max-h-80vh overflow-hidden">
+          <DialogHeader>
+            <DialogTitle>
+              {editingPassenger ? '编辑乘车人' : '添加乘车人'}
+            </DialogTitle>
+          </DialogHeader>
+
+          <ScrollView className="max-h-60vh overflow-y-auto p-1">
+            <View className="space-y-4">
+              <View className="space-y-2">
+                <Label htmlFor="name">姓名</Label>
+                <ShadcnInput
+                  id="name"
+                  placeholder="请输入姓名"
+                  value={formData.name}
+                  onChange={(e) => setFormData({ ...formData, name: e.target.value })}
+                />
+              </View>
+
+              <View className="space-y-2">
+                <Label htmlFor="idType">证件类型</Label>
+                <Select
+                  value={formData.idType}
+                  onValueChange={(value: IdType) => setFormData({ ...formData, idType: value })}
+                >
+                  <SelectTrigger>
+                    <SelectValue placeholder="选择证件类型" />
+                  </SelectTrigger>
+                  <SelectContent>
+                    {Object.values(IdType).map((type) => (
+                      <SelectItem key={type} value={type}>
+                        {type}
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+              </View>
+
+              <View className="space-y-2">
+                <Label htmlFor="idNumber">证件号码</Label>
+                <ShadcnInput
+                  id="idNumber"
+                  placeholder="请输入证件号码"
+                  value={formData.idNumber}
+                  onChange={(e) => setFormData({ ...formData, idNumber: e.target.value })}
+                />
+              </View>
+
+              <View className="space-y-2">
+                <Label htmlFor="phone">手机号</Label>
+                <ShadcnInput
+                  id="phone"
+                  placeholder="请输入手机号"
+                  value={formData.phone}
+                  onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
+                  type="number"
+                  maxLength={11}
+                />
+              </View>
+            </View>
+          </ScrollView>
+
+          <DialogFooter className="pt-4 border-t border-gray-100 bg-gray-50/50">
+            <Button
+              variant="outline"
+              onClick={() => setShowAddModal(false)}
+              className="flex-1"
+            >
+              取消
+            </Button>
+            <Button
+              onClick={savePassenger}
+              disabled={addMutation.isPending || updateMutation.isPending}
+              className="flex-1"
+            >
+              {addMutation.isPending || updateMutation.isPending ? (
+                <View className="i-heroicons-arrow-path-20-solid animate-spin w-4 h-4 mr-2" />
+              ) : null}
+              {editingPassenger ? '更新' : '保存'}
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+    </View>
+  )
+}
+
+export default PassengersPage

+ 6 - 0
mini/src/pages/profile/index.tsx

@@ -108,6 +108,12 @@ const ProfilePage: React.FC = () => {
       onClick: handleEditProfile,
       color: 'text-blue-500'
     },
+    {
+      icon: 'i-heroicons-users-20-solid',
+      title: '乘车人管理',
+      onClick: () => Taro.navigateTo({ url: '/pages/passengers/passengers' }),
+      color: 'text-orange-500'
+    },
     {
       icon: 'i-heroicons-cog-6-tooth-20-solid',
       title: '设置',

+ 52 - 0
mini/src/types/passenger.types.ts

@@ -0,0 +1,52 @@
+export enum IdType {
+  ID_CARD = '身份证',
+  HONG_KONG_MACAO_PASS = '港澳通行证',
+  TAIWAN_PASS = '台湾通行证',
+  PASSPORT = '护照',
+  OTHER = '其他证件'
+}
+
+export interface Passenger {
+  id: number;
+  userId: number;
+  name: string;
+  idType: IdType;
+  idNumber: string;
+  phone: string;
+  isDefault: boolean;
+  createdAt: Date;
+  updatedAt: Date;
+  createdBy?: number;
+  updatedBy?: number;
+}
+
+export interface PassengerCreateInput {
+  userId: number;
+  name: string;
+  idType: IdType;
+  idNumber: string;
+  phone: string;
+  isDefault?: boolean;
+}
+
+export interface PassengerUpdateInput {
+  name?: string;
+  idType?: IdType;
+  idNumber?: string;
+  phone?: string;
+  isDefault?: boolean;
+}
+
+export interface PassengerListParams {
+  page?: number;
+  pageSize?: number;
+  keyword?: string;
+  userId?: number;
+}
+
+export interface PassengerListResponse {
+  data: Passenger[];
+  total: number;
+  page: number;
+  pageSize: number;
+}

+ 1 - 8
src/server/modules/passengers/passenger.entity.ts

@@ -1,13 +1,6 @@
 import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn, ObjectLiteral } from 'typeorm';
 import { UserEntity } from '../users/user.entity';
-
-export enum IdType {
-  ID_CARD = '身份证',
-  HONG_KONG_MACAO_PASS = '港澳通行证',
-  TAIWAN_PASS = '台湾通行证',
-  PASSPORT = '护照',
-  OTHER = '其他证件'
-}
+import { IdType } from '@/share/passenger.types';
 
 @Entity('passengers')
 export class Passenger implements ObjectLiteral {

+ 2 - 1
src/server/modules/passengers/passenger.schema.ts

@@ -1,5 +1,6 @@
+import { IdType } from '@/share/passenger.types';
 import { z } from 'zod';
-import { IdType } from './passenger.entity';
+
 
 // 证件类型枚举schema
 export const IdTypeSchema = z.nativeEnum(IdType);

+ 7 - 1
src/share/passenger.types.ts

@@ -1,4 +1,10 @@
-import { IdType } from '../server/modules/passengers/passenger.entity';
+export enum IdType {
+  ID_CARD = '身份证',
+  HONG_KONG_MACAO_PASS = '港澳通行证',
+  TAIWAN_PASS = '台湾通行证',
+  PASSPORT = '护照',
+  OTHER = '其他证件'
+}
 
 export interface Passenger {
   id: number;