|
@@ -2,12 +2,15 @@ import { useState } from 'react'
|
|
|
import { View, Text, ScrollView, Picker } from '@tarojs/components'
|
|
import { View, Text, ScrollView, Picker } from '@tarojs/components'
|
|
|
import Taro from '@tarojs/taro'
|
|
import Taro from '@tarojs/taro'
|
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
|
|
|
+import { useForm } from 'react-hook-form'
|
|
|
|
|
+import { zodResolver } from '@hookform/resolvers/zod'
|
|
|
|
|
+import { z } from 'zod'
|
|
|
import { Navbar, NavbarPresets } from '@/components/ui/navbar'
|
|
import { Navbar, NavbarPresets } from '@/components/ui/navbar'
|
|
|
import { Button } from '@/components/ui/button'
|
|
import { Button } from '@/components/ui/button'
|
|
|
import { Card, CardContent } from '@/components/ui/card'
|
|
import { Card, CardContent } from '@/components/ui/card'
|
|
|
import { Input as ShadcnInput } from '@/components/ui/input'
|
|
import { Input as ShadcnInput } from '@/components/ui/input'
|
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
|
|
-import { Label } from '@/components/ui/label'
|
|
|
|
|
|
|
+import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'
|
|
|
import { useAuth } from '@/utils/auth'
|
|
import { useAuth } from '@/utils/auth'
|
|
|
import { passengerClient } from '@/api'
|
|
import { passengerClient } from '@/api'
|
|
|
import type { InferResponseType } from 'hono/client'
|
|
import type { InferResponseType } from 'hono/client'
|
|
@@ -16,6 +19,46 @@ import { IdType } from '@/types/passenger.types'
|
|
|
// 使用RPC方式提取类型
|
|
// 使用RPC方式提取类型
|
|
|
type Passenger = InferResponseType<typeof passengerClient.$get, 200>['data'][0]
|
|
type Passenger = InferResponseType<typeof passengerClient.$get, 200>['data'][0]
|
|
|
|
|
|
|
|
|
|
+// 证件号码验证函数
|
|
|
|
|
+const validateIdNumber = (idNumber: string, idType: IdType) => {
|
|
|
|
|
+ switch (idType) {
|
|
|
|
|
+ case IdType.ID_CARD:
|
|
|
|
|
+ return /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/.test(idNumber)
|
|
|
|
|
+ case IdType.HONG_KONG_MACAO_PASS:
|
|
|
|
|
+ return /^[HMhm]{1}([0-9]{10}|[0-9]{8})$/.test(idNumber)
|
|
|
|
|
+ case IdType.TAIWAN_PASS:
|
|
|
|
|
+ return /^[0-9]{8}$/.test(idNumber)
|
|
|
|
|
+ case IdType.PASSPORT:
|
|
|
|
|
+ return /^[EeKkGgDdSsPp]([0-9]{8}|[0-9]{7})$/.test(idNumber)
|
|
|
|
|
+ case IdType.OTHER:
|
|
|
|
|
+ return idNumber.length >= 1 && idNumber.length <= 30
|
|
|
|
|
+ default:
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 乘客表单验证schema
|
|
|
|
|
+const passengerFormSchema = z.object({
|
|
|
|
|
+ name: z
|
|
|
|
|
+ .string()
|
|
|
|
|
+ .min(1, '请输入姓名')
|
|
|
|
|
+ .min(2, '姓名至少2个字符')
|
|
|
|
|
+ .max(50, '姓名不能超过50个字符'),
|
|
|
|
|
+ idType: z.nativeEnum(IdType, {
|
|
|
|
|
+ message: '请选择证件类型'
|
|
|
|
|
+ }),
|
|
|
|
|
+ idNumber: z
|
|
|
|
|
+ .string()
|
|
|
|
|
+ .min(1, '请输入证件号码')
|
|
|
|
|
+ .max(30, '证件号码不能超过30个字符'),
|
|
|
|
|
+ phone: z
|
|
|
|
|
+ .string()
|
|
|
|
|
+ .min(1, '请输入手机号')
|
|
|
|
|
+ .regex(/^1[3-9]\d{9}$/, '手机号格式不正确')
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+type PassengerFormData = z.infer<typeof passengerFormSchema>
|
|
|
|
|
+
|
|
|
const PassengersPage: React.FC = () => {
|
|
const PassengersPage: React.FC = () => {
|
|
|
const { user } = useAuth()
|
|
const { user } = useAuth()
|
|
|
const queryClient = useQueryClient()
|
|
const queryClient = useQueryClient()
|
|
@@ -24,11 +67,16 @@ const PassengersPage: React.FC = () => {
|
|
|
const [showSearchBar, setShowSearchBar] = useState(false)
|
|
const [showSearchBar, setShowSearchBar] = useState(false)
|
|
|
const [showAddModal, setShowAddModal] = useState(false)
|
|
const [showAddModal, setShowAddModal] = useState(false)
|
|
|
const [editingPassenger, setEditingPassenger] = useState<Passenger | null>(null)
|
|
const [editingPassenger, setEditingPassenger] = useState<Passenger | null>(null)
|
|
|
- const [formData, setFormData] = useState({
|
|
|
|
|
- name: '',
|
|
|
|
|
- idType: IdType.ID_CARD,
|
|
|
|
|
- idNumber: '',
|
|
|
|
|
- phone: ''
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化react-hook-form
|
|
|
|
|
+ const form = useForm<PassengerFormData>({
|
|
|
|
|
+ resolver: zodResolver(passengerFormSchema),
|
|
|
|
|
+ defaultValues: {
|
|
|
|
|
+ name: '',
|
|
|
|
|
+ idType: IdType.ID_CARD,
|
|
|
|
|
+ idNumber: '',
|
|
|
|
|
+ phone: ''
|
|
|
|
|
+ }
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
// 证件类型选项
|
|
// 证件类型选项
|
|
@@ -41,7 +89,7 @@ const PassengersPage: React.FC = () => {
|
|
|
]
|
|
]
|
|
|
|
|
|
|
|
// 获取当前证件类型的索引
|
|
// 获取当前证件类型的索引
|
|
|
- const currentIdTypeIndex = idTypeOptions.findIndex(option => option.value === formData.idType)
|
|
|
|
|
|
|
+ const currentIdTypeIndex = idTypeOptions.findIndex(option => option.value === form.watch('idType'))
|
|
|
|
|
|
|
|
// 获取乘客列表
|
|
// 获取乘客列表
|
|
|
const { data: passengersResponse, isLoading } = useQuery({
|
|
const { data: passengersResponse, isLoading } = useQuery({
|
|
@@ -136,7 +184,7 @@ const PassengersPage: React.FC = () => {
|
|
|
|
|
|
|
|
// 重置表单
|
|
// 重置表单
|
|
|
const resetForm = () => {
|
|
const resetForm = () => {
|
|
|
- setFormData({
|
|
|
|
|
|
|
+ form.reset({
|
|
|
name: '',
|
|
name: '',
|
|
|
idType: IdType.ID_CARD,
|
|
idType: IdType.ID_CARD,
|
|
|
idNumber: '',
|
|
idNumber: '',
|
|
@@ -164,7 +212,7 @@ const PassengersPage: React.FC = () => {
|
|
|
|
|
|
|
|
// 编辑乘客
|
|
// 编辑乘客
|
|
|
const editPassenger = (passenger: Passenger) => {
|
|
const editPassenger = (passenger: Passenger) => {
|
|
|
- setFormData({
|
|
|
|
|
|
|
+ form.reset({
|
|
|
name: passenger.name,
|
|
name: passenger.name,
|
|
|
idType: passenger.idType,
|
|
idType: passenger.idType,
|
|
|
idNumber: passenger.idNumber,
|
|
idNumber: passenger.idNumber,
|
|
@@ -196,30 +244,20 @@ const PassengersPage: React.FC = () => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 保存乘客
|
|
// 保存乘客
|
|
|
- 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' })
|
|
|
|
|
|
|
+ const onSubmit = (data: PassengerFormData) => {
|
|
|
|
|
+ // 额外的证件号码格式验证
|
|
|
|
|
+ if (!validateIdNumber(data.idNumber, data.idType)) {
|
|
|
|
|
+ Taro.showToast({ title: '证件号码格式不正确', icon: 'none' })
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (editingPassenger) {
|
|
if (editingPassenger) {
|
|
|
updateMutation.mutate({
|
|
updateMutation.mutate({
|
|
|
id: editingPassenger.id,
|
|
id: editingPassenger.id,
|
|
|
- data: formData
|
|
|
|
|
|
|
+ data: data
|
|
|
})
|
|
})
|
|
|
} else {
|
|
} else {
|
|
|
- addMutation.mutate(formData)
|
|
|
|
|
|
|
+ addMutation.mutate(data)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -401,78 +439,110 @@ const PassengersPage: React.FC = () => {
|
|
|
</DialogTitle>
|
|
</DialogTitle>
|
|
|
</DialogHeader>
|
|
</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={(value) => setFormData({ ...formData, name: value })}
|
|
|
|
|
|
|
+ <Form {...form}>
|
|
|
|
|
+ <ScrollView className="max-h-60vh overflow-y-auto p-1">
|
|
|
|
|
+ <View className="space-y-4">
|
|
|
|
|
+ <FormField
|
|
|
|
|
+ control={form.control}
|
|
|
|
|
+ name="name"
|
|
|
|
|
+ render={({ field }) => (
|
|
|
|
|
+ <FormItem>
|
|
|
|
|
+ <FormLabel>姓名</FormLabel>
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <ShadcnInput
|
|
|
|
|
+ placeholder="请输入姓名"
|
|
|
|
|
+ {...field}
|
|
|
|
|
+ />
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ <FormMessage />
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ )}
|
|
|
/>
|
|
/>
|
|
|
- </View>
|
|
|
|
|
|
|
|
|
|
- <View className="space-y-2">
|
|
|
|
|
- <Label htmlFor="idType">证件类型</Label>
|
|
|
|
|
- <Picker
|
|
|
|
|
- mode="selector"
|
|
|
|
|
- range={idTypeOptions.map(option => option.label)}
|
|
|
|
|
- value={currentIdTypeIndex}
|
|
|
|
|
- onChange={(e) => {
|
|
|
|
|
- const index = Number(e.detail.value)
|
|
|
|
|
- setFormData({ ...formData, idType: idTypeOptions[index].value })
|
|
|
|
|
- }}
|
|
|
|
|
- >
|
|
|
|
|
- <View className="border border-gray-300 rounded-lg px-3 py-2 bg-white">
|
|
|
|
|
- <Text className="text-gray-900">
|
|
|
|
|
- {idTypeOptions[currentIdTypeIndex]?.label || '选择证件类型'}
|
|
|
|
|
- </Text>
|
|
|
|
|
- </View>
|
|
|
|
|
- </Picker>
|
|
|
|
|
- </View>
|
|
|
|
|
|
|
+ <FormField
|
|
|
|
|
+ control={form.control}
|
|
|
|
|
+ name="idType"
|
|
|
|
|
+ render={({ field }) => (
|
|
|
|
|
+ <FormItem>
|
|
|
|
|
+ <FormLabel>证件类型</FormLabel>
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <Picker
|
|
|
|
|
+ mode="selector"
|
|
|
|
|
+ range={idTypeOptions.map(option => option.label)}
|
|
|
|
|
+ value={currentIdTypeIndex}
|
|
|
|
|
+ onChange={(e) => {
|
|
|
|
|
+ const index = Number(e.detail.value)
|
|
|
|
|
+ field.onChange(idTypeOptions[index].value)
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <View className="border border-gray-300 rounded-lg px-3 py-2 bg-white">
|
|
|
|
|
+ <Text className="text-gray-900">
|
|
|
|
|
+ {idTypeOptions[currentIdTypeIndex]?.label || '选择证件类型'}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </Picker>
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ <FormMessage />
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+ />
|
|
|
|
|
|
|
|
- <View className="space-y-2">
|
|
|
|
|
- <Label htmlFor="idNumber">证件号码</Label>
|
|
|
|
|
- <ShadcnInput
|
|
|
|
|
- id="idNumber"
|
|
|
|
|
- placeholder="请输入证件号码"
|
|
|
|
|
- value={formData.idNumber}
|
|
|
|
|
- onChange={(value) => setFormData({ ...formData, idNumber: value })}
|
|
|
|
|
|
|
+ <FormField
|
|
|
|
|
+ control={form.control}
|
|
|
|
|
+ name="idNumber"
|
|
|
|
|
+ render={({ field }) => (
|
|
|
|
|
+ <FormItem>
|
|
|
|
|
+ <FormLabel>证件号码</FormLabel>
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <ShadcnInput
|
|
|
|
|
+ placeholder="请输入证件号码"
|
|
|
|
|
+ {...field}
|
|
|
|
|
+ />
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ <FormMessage />
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ )}
|
|
|
/>
|
|
/>
|
|
|
- </View>
|
|
|
|
|
|
|
|
|
|
- <View className="space-y-2">
|
|
|
|
|
- <Label htmlFor="phone">手机号</Label>
|
|
|
|
|
- <ShadcnInput
|
|
|
|
|
- id="phone"
|
|
|
|
|
- placeholder="请输入手机号"
|
|
|
|
|
- value={formData.phone}
|
|
|
|
|
- onChange={(value) => setFormData({ ...formData, phone: value })}
|
|
|
|
|
|
|
+ <FormField
|
|
|
|
|
+ control={form.control}
|
|
|
|
|
+ name="phone"
|
|
|
|
|
+ render={({ field }) => (
|
|
|
|
|
+ <FormItem>
|
|
|
|
|
+ <FormLabel>手机号</FormLabel>
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <ShadcnInput
|
|
|
|
|
+ placeholder="请输入手机号"
|
|
|
|
|
+ {...field}
|
|
|
|
|
+ />
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ <FormMessage />
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ )}
|
|
|
/>
|
|
/>
|
|
|
</View>
|
|
</View>
|
|
|
- </View>
|
|
|
|
|
- </ScrollView>
|
|
|
|
|
-
|
|
|
|
|
- <DialogFooter className="pt-4 border-t border-gray-100 bg-gray-50/50 flex flex-row gap-4">
|
|
|
|
|
- <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>
|
|
|
|
|
|
|
+ </ScrollView>
|
|
|
|
|
+
|
|
|
|
|
+ <DialogFooter className="pt-4 border-t border-gray-100 bg-gray-50/50 flex flex-row gap-4">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ onClick={() => setShowAddModal(false)}
|
|
|
|
|
+ className="flex-1"
|
|
|
|
|
+ >
|
|
|
|
|
+ 取消
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ onClick={() => form.handleSubmit(onSubmit)()}
|
|
|
|
|
+ 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>
|
|
|
|
|
+ </Form>
|
|
|
</DialogContent>
|
|
</DialogContent>
|
|
|
</Dialog>
|
|
</Dialog>
|
|
|
</View>
|
|
</View>
|