|
|
@@ -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
|