|
@@ -0,0 +1,274 @@
|
|
|
|
|
+import React, { useState } from 'react';
|
|
|
|
|
+import { useForm } from 'react-hook-form';
|
|
|
|
|
+import { zodResolver } from '@hookform/resolvers/zod';
|
|
|
|
|
+import { z } from 'zod';
|
|
|
|
|
+import { Button } from '@/client/components/ui/button';
|
|
|
|
|
+import { Input } from '@/client/components/ui/input';
|
|
|
|
|
+import { Textarea } from '@/client/components/ui/textarea';
|
|
|
|
|
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
|
|
|
|
|
+import { Label } from '@/client/components/ui/label';
|
|
|
|
|
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/client/components/ui/select';
|
|
|
|
|
+import { toast } from 'react-toastify';
|
|
|
|
|
+import { publicConsultationRequestClient } from '@/client/api';
|
|
|
|
|
+import type { InferRequestType } from 'hono/client';
|
|
|
|
|
+
|
|
|
|
|
+// 表单验证Schema
|
|
|
|
|
+const ConsultationRequestFormSchema = z.object({
|
|
|
|
|
+ customerName: z.string().min(2, '姓名至少2个字符').max(255),
|
|
|
|
|
+ companyName: z.string().max(255).optional(),
|
|
|
|
|
+ phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号'),
|
|
|
|
|
+ email: z.string().email('请输入正确的邮箱地址').optional(),
|
|
|
|
|
+ projectType: z.string().min(1, '请选择项目类型'),
|
|
|
|
|
+ projectDescription: z.string().min(10, '项目描述至少10个字符').max(2000),
|
|
|
|
|
+ budgetRange: z.string().max(100).optional(),
|
|
|
|
|
+ timeline: z.string().max(100).optional(),
|
|
|
|
|
+ isGuest: z.boolean().default(true)
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+type ConsultationRequestFormData = z.infer<typeof ConsultationRequestFormSchema>;
|
|
|
|
|
+
|
|
|
|
|
+// 项目类型选项
|
|
|
|
|
+const projectTypes = [
|
|
|
|
|
+ { value: '企业ERP系统', label: '企业ERP系统' },
|
|
|
|
|
+ { value: '智慧政务平台', label: '智慧政务平台' },
|
|
|
|
|
+ { value: '医疗信息化系统', label: '医疗信息化系统' },
|
|
|
|
|
+ { value: '教育信息化平台', label: '教育信息化平台' },
|
|
|
|
|
+ { value: '电商平台', label: '电商平台' },
|
|
|
|
|
+ { value: '移动应用开发', label: '移动应用开发' },
|
|
|
|
|
+ { value: '大数据分析平台', label: '大数据分析平台' },
|
|
|
|
|
+ { value: '人工智能应用', label: '人工智能应用' },
|
|
|
|
|
+ { value: '物联网系统', label: '物联网系统' },
|
|
|
|
|
+ { value: '其他', label: '其他' }
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+// 预算范围选项
|
|
|
|
|
+const budgetRanges = [
|
|
|
|
|
+ { value: '10万以下', label: '10万以下' },
|
|
|
|
|
+ { value: '10-50万', label: '10-50万' },
|
|
|
|
|
+ { value: '50-100万', label: '50-100万' },
|
|
|
|
|
+ { value: '100-500万', label: '100-500万' },
|
|
|
|
|
+ { value: '500万以上', label: '500万以上' }
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+// 时间要求选项
|
|
|
|
|
+const timelines = [
|
|
|
|
|
+ { value: '1-3个月', label: '1-3个月' },
|
|
|
|
|
+ { value: '3-6个月', label: '3-6个月' },
|
|
|
|
|
+ { value: '6-12个月', label: '6-12个月' },
|
|
|
|
|
+ { value: '12个月以上', label: '12个月以上' }
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+interface ConsultationRequestFormProps {
|
|
|
|
|
+ onSuccess?: () => void;
|
|
|
|
|
+ onCancel?: () => void;
|
|
|
|
|
+ className?: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export default function ConsultationRequestForm({
|
|
|
|
|
+ onSuccess,
|
|
|
|
|
+ onCancel,
|
|
|
|
|
+ className = ''
|
|
|
|
|
+}: ConsultationRequestFormProps) {
|
|
|
|
|
+ const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
|
+
|
|
|
|
|
+ const {
|
|
|
|
|
+ register,
|
|
|
|
|
+ handleSubmit,
|
|
|
|
|
+ formState: { errors },
|
|
|
|
|
+ setValue,
|
|
|
|
|
+ watch
|
|
|
|
|
+ } = useForm<ConsultationRequestFormData>({
|
|
|
|
|
+ resolver: zodResolver(ConsultationRequestFormSchema),
|
|
|
|
|
+ defaultValues: {
|
|
|
|
|
+ isGuest: true
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const onSubmit = async (data: ConsultationRequestFormData) => {
|
|
|
|
|
+ setIsSubmitting(true);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await publicConsultationRequestClient.$post({
|
|
|
|
|
+ json: data
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (response.status === 200) {
|
|
|
|
|
+ const result = await response.json();
|
|
|
|
|
+ toast.success(result.message || '客户需求提交成功!');
|
|
|
|
|
+ onSuccess?.();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const error = await response.json();
|
|
|
|
|
+ toast.error(error.message || '提交失败,请稍后重试');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('提交客户需求失败:', error);
|
|
|
|
|
+ toast.error('网络错误,请检查网络连接后重试');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setIsSubmitting(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Card className={`w-full max-w-2xl mx-auto ${className}`}>
|
|
|
|
|
+ <CardHeader>
|
|
|
|
|
+ <CardTitle className="text-2xl font-bold">项目咨询需求</CardTitle>
|
|
|
|
|
+ <CardDescription>
|
|
|
|
|
+ 请填写您的项目需求信息,我们将尽快与您联系并提供专业的咨询服务
|
|
|
|
|
+ </CardDescription>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
|
|
|
|
+ {/* 基本信息 */}
|
|
|
|
|
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <Label htmlFor="customerName">客户姓名 *</Label>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ id="customerName"
|
|
|
|
|
+ placeholder="请输入您的姓名"
|
|
|
|
|
+ {...register('customerName')}
|
|
|
|
|
+ className={errors.customerName ? 'border-red-500' : ''}
|
|
|
|
|
+ />
|
|
|
|
|
+ {errors.customerName && (
|
|
|
|
|
+ <p className="text-sm text-red-500">{errors.customerName.message}</p>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <Label htmlFor="companyName">公司名称</Label>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ id="companyName"
|
|
|
|
|
+ placeholder="请输入公司名称(选填)"
|
|
|
|
|
+ {...register('companyName')}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <Label htmlFor="phone">手机号 *</Label>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ id="phone"
|
|
|
|
|
+ placeholder="请输入手机号"
|
|
|
|
|
+ {...register('phone')}
|
|
|
|
|
+ className={errors.phone ? 'border-red-500' : ''}
|
|
|
|
|
+ />
|
|
|
|
|
+ {errors.phone && (
|
|
|
|
|
+ <p className="text-sm text-red-500">{errors.phone.message}</p>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <Label htmlFor="email">邮箱地址</Label>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ id="email"
|
|
|
|
|
+ type="email"
|
|
|
|
|
+ placeholder="请输入邮箱地址(选填)"
|
|
|
|
|
+ {...register('email')}
|
|
|
|
|
+ className={errors.email ? 'border-red-500' : ''}
|
|
|
|
|
+ />
|
|
|
|
|
+ {errors.email && (
|
|
|
|
|
+ <p className="text-sm text-red-500">{errors.email.message}</p>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 项目信息 */}
|
|
|
|
|
+ <div className="space-y-4">
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <Label htmlFor="projectType">项目类型 *</Label>
|
|
|
|
|
+ <Select onValueChange={(value) => setValue('projectType', value)}>
|
|
|
|
|
+ <SelectTrigger className={errors.projectType ? 'border-red-500' : ''}>
|
|
|
|
|
+ <SelectValue placeholder="请选择项目类型" />
|
|
|
|
|
+ </SelectTrigger>
|
|
|
|
|
+ <SelectContent>
|
|
|
|
|
+ {projectTypes.map((type) => (
|
|
|
|
|
+ <SelectItem key={type.value} value={type.value}>
|
|
|
|
|
+ {type.label}
|
|
|
|
|
+ </SelectItem>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </SelectContent>
|
|
|
|
|
+ </Select>
|
|
|
|
|
+ {errors.projectType && (
|
|
|
|
|
+ <p className="text-sm text-red-500">{errors.projectType.message}</p>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <Label htmlFor="projectDescription">项目描述 *</Label>
|
|
|
|
|
+ <Textarea
|
|
|
|
|
+ id="projectDescription"
|
|
|
|
|
+ placeholder="请详细描述您的项目需求、目标和期望效果..."
|
|
|
|
|
+ rows={4}
|
|
|
|
|
+ {...register('projectDescription')}
|
|
|
|
|
+ className={errors.projectDescription ? 'border-red-500' : ''}
|
|
|
|
|
+ />
|
|
|
|
|
+ {errors.projectDescription && (
|
|
|
|
|
+ <p className="text-sm text-red-500">{errors.projectDescription.message}</p>
|
|
|
|
|
+ )}
|
|
|
|
|
+ <p className="text-sm text-gray-500">
|
|
|
|
|
+ 已输入 {watch('projectDescription')?.length || 0} / 2000 字符
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <Label htmlFor="budgetRange">预算范围</Label>
|
|
|
|
|
+ <Select onValueChange={(value) => setValue('budgetRange', value)}>
|
|
|
|
|
+ <SelectTrigger>
|
|
|
|
|
+ <SelectValue placeholder="请选择预算范围(选填)" />
|
|
|
|
|
+ </SelectTrigger>
|
|
|
|
|
+ <SelectContent>
|
|
|
|
|
+ {budgetRanges.map((range) => (
|
|
|
|
|
+ <SelectItem key={range.value} value={range.value}>
|
|
|
|
|
+ {range.label}
|
|
|
|
|
+ </SelectItem>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </SelectContent>
|
|
|
|
|
+ </Select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <Label htmlFor="timeline">时间要求</Label>
|
|
|
|
|
+ <Select onValueChange={(value) => setValue('timeline', value)}>
|
|
|
|
|
+ <SelectTrigger>
|
|
|
|
|
+ <SelectValue placeholder="请选择时间要求(选填)" />
|
|
|
|
|
+ </SelectTrigger>
|
|
|
|
|
+ <SelectContent>
|
|
|
|
|
+ {timelines.map((timeline) => (
|
|
|
|
|
+ <SelectItem key={timeline.value} value={timeline.value}>
|
|
|
|
|
+ {timeline.label}
|
|
|
|
|
+ </SelectItem>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </SelectContent>
|
|
|
|
|
+ </Select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 操作按钮 */}
|
|
|
|
|
+ <div className="flex gap-4 pt-4">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="submit"
|
|
|
|
|
+ className="flex-1 bg-blue-600 hover:bg-blue-700"
|
|
|
|
|
+ disabled={isSubmitting}
|
|
|
|
|
+ >
|
|
|
|
|
+ {isSubmitting ? '提交中...' : '提交咨询需求'}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ {onCancel && (
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ onClick={onCancel}
|
|
|
|
|
+ disabled={isSubmitting}
|
|
|
|
|
+ >
|
|
|
|
|
+ 取消
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <p className="text-sm text-gray-500 text-center">
|
|
|
|
|
+ 提交即表示您同意我们的服务条款和隐私政策
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </form>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|