| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464 |
- import React from 'react';
- import { useForm } from 'react-hook-form';
- import { zodResolver } from '@hookform/resolvers/zod';
- import { Button } from '@/client/components/ui/button';
- import { Input } from '@/client/components/ui/input';
- import { Textarea } from '@/client/components/ui/textarea';
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/client/components/ui/select';
- import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
- import { MapPin, DollarSign, Users, Car } from 'lucide-react';
- import { format } from 'date-fns';
- import { createRouteSchema, updateRouteSchema } from '@/server/modules/routes/route.schema';
- import type { CreateRouteInput, UpdateRouteInput } from '@/server/modules/routes/route.schema';
- import { ActivitySelect } from './ActivitySelect';
- // 将ISO日期时间格式化为 datetime-local 输入框需要的格式
- const formatDateTimeForInput = (dateString: string): string => {
- const date = new Date(dateString);
- // 使用 date-fns 转换为 YYYY-MM-DDTHH:mm 格式
- return format(date, "yyyy-MM-dd'T'HH:mm");
- };
- interface RouteFormProps {
- initialData?: UpdateRouteInput & { id?: number };
- onSubmit: (data: CreateRouteInput | UpdateRouteInput) => Promise<void>;
- onCancel: () => void;
- isLoading?: boolean;
- }
- export const RouteForm: React.FC<RouteFormProps> = ({
- initialData,
- onSubmit,
- onCancel,
- isLoading = false
- }) => {
- const isEditing = !!initialData?.id;
- const form = useForm<CreateRouteInput | UpdateRouteInput>({
- resolver: zodResolver(isEditing ? updateRouteSchema : createRouteSchema),
- defaultValues: initialData ? {
- name: initialData.name || '',
- description: initialData.description || '',
- startPoint: initialData.startPoint || '',
- endPoint: initialData.endPoint || '',
- pickupPoint: initialData.pickupPoint || '',
- dropoffPoint: initialData.dropoffPoint || '',
- departureTime: initialData.departureTime ? formatDateTimeForInput(initialData.departureTime) : '',
- vehicleType: initialData.vehicleType || 'bus',
- price: initialData.price || 0,
- seatCount: initialData.seatCount || 1,
- availableSeats: initialData.availableSeats || 1,
- activityId: initialData.activityId || 0,
- isDisabled: initialData.isDisabled
- } : {
- name: '',
- description: '',
- startPoint: '',
- endPoint: '',
- pickupPoint: '',
- dropoffPoint: '',
- departureTime: '',
- vehicleType: 'bus',
- price: 0,
- seatCount: 1,
- availableSeats: 1,
- activityId: 0,
- }
- });
- const handleSubmit = async (data: CreateRouteInput | UpdateRouteInput) => {
- try {
- await onSubmit(data);
- } catch (error) {
- console.error('表单提交失败:', error);
- }
- };
- return (
- <Form {...form}>
- <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
- {/* 路线名称 */}
- <FormField
- control={form.control}
- name="name"
- render={({ field }) => (
- <FormItem>
- <FormLabel>路线名称 *</FormLabel>
- <FormControl>
- <Input
- placeholder="请输入路线名称"
- data-testid="route-name-input"
- {...field}
- />
- </FormControl>
- <FormDescription>
- 路线的显示名称,最多255个字符
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
- {/* 车型 */}
- <FormField
- control={form.control}
- name="vehicleType"
- render={({ field }) => (
- <FormItem>
- <FormLabel>车型 *</FormLabel>
- <Select onValueChange={field.onChange} defaultValue={field.value}>
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="选择车型" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectItem value="bus">
- <div className="flex items-center gap-2">
- <Car className="h-4 w-4 text-blue-500" />
- <span>大巴</span>
- </div>
- </SelectItem>
- <SelectItem value="van">
- <div className="flex items-center gap-2">
- <Car className="h-4 w-4 text-green-500" />
- <span>中巴</span>
- </div>
- </SelectItem>
- <SelectItem value="car">
- <div className="flex items-center gap-2">
- <Car className="h-4 w-4 text-orange-500" />
- <span>小车</span>
- </div>
- </SelectItem>
- </SelectContent>
- </Select>
- <FormDescription>
- 选择路线的车型
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
- </div>
- {/* 路线描述 */}
- <FormField
- control={form.control}
- name="description"
- render={({ field }) => (
- <FormItem>
- <FormLabel>路线描述</FormLabel>
- <FormControl>
- <Textarea
- placeholder="请输入路线描述(可选)"
- className="min-h-[100px]"
- {...field}
- value={field.value || ''}
- />
- </FormControl>
- <FormDescription>
- 路线的详细描述,最多1000个字符
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
- {/* 出发地 */}
- <FormField
- control={form.control}
- name="startPoint"
- render={({ field }) => (
- <FormItem>
- <FormLabel>出发地 *</FormLabel>
- <FormControl>
- <div className="relative">
- <MapPin className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
- <Input
- placeholder="请输入出发地"
- className="pl-8"
- data-testid="start-point-input"
- {...field}
- />
- </div>
- </FormControl>
- <FormDescription>
- 路线的出发地点
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
- {/* 目的地 */}
- <FormField
- control={form.control}
- name="endPoint"
- render={({ field }) => (
- <FormItem>
- <FormLabel>目的地 *</FormLabel>
- <FormControl>
- <div className="relative">
- <MapPin className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
- <Input
- placeholder="请输入目的地"
- className="pl-8"
- data-testid="end-point-input"
- {...field}
- />
- </div>
- </FormControl>
- <FormDescription>
- 路线的目的地
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
- </div>
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
- {/* 上车点 */}
- <FormField
- control={form.control}
- name="pickupPoint"
- render={({ field }) => (
- <FormItem>
- <FormLabel>上车点 *</FormLabel>
- <FormControl>
- <div className="relative">
- <MapPin className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
- <Input
- placeholder="请输入上车点"
- className="pl-8"
- data-testid="pickup-point-input"
- {...field}
- />
- </div>
- </FormControl>
- <FormDescription>
- 乘客上车的地点
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
- {/* 下车点 */}
- <FormField
- control={form.control}
- name="dropoffPoint"
- render={({ field }) => (
- <FormItem>
- <FormLabel>下车点 *</FormLabel>
- <FormControl>
- <div className="relative">
- <MapPin className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
- <Input
- placeholder="请输入下车点"
- className="pl-8"
- data-testid="dropoff-point-input"
- {...field}
- />
- </div>
- </FormControl>
- <FormDescription>
- 乘客下车的地点
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
- </div>
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
- {/* 出发时间 */}
- <FormField
- control={form.control}
- name="departureTime"
- render={({ field }) => (
- <FormItem>
- <FormLabel>出发时间 *</FormLabel>
- <FormControl>
- <Input
- type="datetime-local"
- data-testid="departure-time-input"
- {...field}
- />
- </FormControl>
- <FormDescription>
- 路线的出发时间
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
- {/* 价格 */}
- <FormField
- control={form.control}
- name="price"
- render={({ field }) => (
- <FormItem>
- <FormLabel>价格 *</FormLabel>
- <FormControl>
- <div className="relative">
- <DollarSign className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
- <Input
- type="number"
- step="0.01"
- min="0"
- placeholder="0.00"
- className="pl-8"
- data-testid="price-input"
- {...field}
- onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
- />
- </div>
- </FormControl>
- <FormDescription>
- 路线的价格(元)
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
- </div>
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
- {/* 座位数 */}
- <FormField
- control={form.control}
- name="seatCount"
- render={({ field }) => (
- <FormItem>
- <FormLabel>总座位数 *</FormLabel>
- <FormControl>
- <div className="relative">
- <Users className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
- <Input
- type="number"
- min="1"
- max="1000"
- placeholder="1"
- className="pl-8"
- data-testid="seat-count-input"
- {...field}
- onChange={(e) => field.onChange(parseInt(e.target.value) || 1)}
- />
- </div>
- </FormControl>
- <FormDescription>
- 车辆的总座位数
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
- {/* 可用座位数 */}
- <FormField
- control={form.control}
- name="availableSeats"
- render={({ field }) => (
- <FormItem>
- <FormLabel>可用座位数 *</FormLabel>
- <FormControl>
- <div className="relative">
- <Users className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
- <Input
- type="number"
- min="0"
- max="1000"
- placeholder="0"
- className="pl-8"
- data-testid="available-seats-input"
- {...field}
- onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
- />
- </div>
- </FormControl>
- <FormDescription>
- 当前可用的座位数
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
- </div>
- {/* 关联活动 */}
- <FormField
- control={form.control}
- name="activityId"
- render={({ field }) => (
- <FormItem>
- <FormLabel>关联活动 *</FormLabel>
- <FormControl>
- <ActivitySelect
- value={field.value}
- onValueChange={field.onChange}
- placeholder="请选择关联活动"
- data-testid="activity-select"
- />
- </FormControl>
- <FormDescription>
- 选择此路线关联的活动
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
- {/* 状态选择(仅在编辑时显示) */}
- {isEditing && (
- <FormField
- control={form.control}
- name="isDisabled"
- render={({ field }) => (
- <FormItem>
- <FormLabel>路线状态</FormLabel>
- <Select onValueChange={(value) => field.onChange(parseInt(value))} defaultValue={field.value?.toString()}>
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="选择路线状态" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectItem value="0">启用</SelectItem>
- <SelectItem value="1">禁用</SelectItem>
- </SelectContent>
- </Select>
- <FormDescription>
- 控制路线是否对用户可见
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
- )}
- {/* 操作按钮 */}
- <div className="flex justify-end gap-4 pt-6">
- <Button
- type="button"
- variant="outline"
- onClick={onCancel}
- disabled={isLoading}
- >
- 取消
- </Button>
- <Button
- type="submit"
- disabled={isLoading}
- >
- {isLoading ? '保存中...' : isEditing ? '更新路线' : '创建路线'}
- </Button>
- </div>
- </form>
- </Form>
- );
- };
|