Files
23ff5c49-3ffc-4a02-b409-140…/src/pages/Contact.tsx
2026-01-05 11:18:07 +08:00

577 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import { motion } from 'framer-motion';
import {
MapPin,
Phone,
Mail,
Clock,
Send,
User,
MessageSquare,
Building2,
Briefcase,
ArrowRight,
CheckCircle,
} from 'lucide-react';
import { Header } from '../components/Header';
import { Footer } from '../components/Footer';
import { COMPANY_INFO, CONTACT_INFO } from '../lib/constants';
import { usePageTitle } from '../hooks/usePageTitle';
/**
* Contact 组件 - 联系我们页面
*/
// 招聘信息数据
const jobPositions = [
{
id: 1,
title: '高级投资经理',
department: '投资部',
location: '北京',
type: '全职',
salary: '30K-50K/月',
},
{
id: 2,
title: 'Java 开发工程师',
department: '技术部',
location: '北京',
type: '全职',
salary: '25K-40K/月',
},
{
id: 3,
title: '财务顾问',
department: '金融部',
location: '上海',
type: '全职',
salary: '20K-35K/月',
},
{
id: 4,
title: '产品经理',
department: '产品部',
location: '北京',
type: '全职',
salary: '28K-45K/月',
},
];
// 表单状态类型
interface ContactFormData {
name: string;
email: string;
subject: string;
message: string;
}
interface FormErrors {
name?: string;
email?: string;
subject?: string;
message?: string;
}
/**
* Contact 组件
*/
export const Contact: React.FC = () => {
usePageTitle('联系我们');
const [formData, setFormData] = useState<ContactFormData>({
name: '',
email: '',
subject: '',
message: '',
});
const [errors, setErrors] = useState<FormErrors>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitSuccess, setSubmitSuccess] = useState(false);
// 表单验证
const validateForm = (): boolean => {
const newErrors: FormErrors = {};
if (!formData.name.trim()) {
newErrors.name = '请输入您的姓名';
}
if (!formData.email.trim()) {
newErrors.email = '请输入您的邮箱';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = '请输入有效的邮箱地址';
}
if (!formData.subject.trim()) {
newErrors.subject = '请输入邮件主题';
}
if (!formData.message.trim()) {
newErrors.message = '请输入留言内容';
} else if (formData.message.trim().length < 10) {
newErrors.message = '留言内容至少需要 10 个字符';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// 处理输入变化
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
// 清除对应字段的错误
if (errors[name as keyof FormErrors]) {
setErrors((prev) => ({ ...prev, [name]: undefined }));
}
};
// 处理表单提交
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) return;
setIsSubmitting(true);
// 模拟表单提交
await new Promise((resolve) => setTimeout(resolve, 1500));
setIsSubmitting(false);
setSubmitSuccess(true);
setFormData({ name: '', email: '', subject: '', message: '' });
// 3秒后重置成功状态
setTimeout(() => setSubmitSuccess(false), 3000);
};
return (
<motion.div
className="min-h-screen bg-background"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
{/* 顶部导航 */}
<Header />
{/* 主内容 */}
<main>
{/* 页面标题 */}
<section className="pt-32 pb-16 bg-gradient-to-br from-primary to-primary-light">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
className="text-center text-white"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h1 className="text-4xl md:text-5xl font-bold mb-4">
</h1>
<p className="text-xl text-white/80 max-w-3xl mx-auto">
</p>
</motion.div>
</div>
</section>
{/* 联系信息 */}
<section className="py-16 -mt-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
{CONTACT_INFO.map((info, index) => {
const Icon = info.icon;
return (
<motion.div
key={info.id}
className="bg-white rounded-xl shadow-lg p-6 text-center hover:shadow-xl transition-shadow"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1, duration: 0.5 }}
whileHover={{ y: -5 }}
>
<div className="inline-flex items-center justify-center w-14 h-14 rounded-full bg-accent/20 mb-4">
<Icon size={28} className="text-accent" />
</div>
<h3 className="text-sm text-gray-500 mb-2">{info.title}</h3>
<p className="font-medium text-primary-dark">{info.content}</p>
</motion.div>
);
})}
</div>
</div>
</section>
{/* 联系表单和地图 */}
<section className="py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid lg:grid-cols-2 gap-12">
{/* 联系表单 */}
<motion.div
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-8">
<h2 className="text-2xl font-bold text-primary-dark mb-2">
</h2>
<p className="text-gray-600 mb-8">
</p>
{/* 成功提示 */}
{submitSuccess && (
<motion.div
className="mb-6 p-4 bg-green-50 border border-green-200 rounded-xl flex items-center gap-3"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
>
<CheckCircle size={24} className="text-green-600" />
<div>
<p className="font-medium text-green-800"></p>
<p className="text-sm text-green-600">
</p>
</div>
</motion.div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
{/* 姓名 */}
<div>
<label
htmlFor="name"
className="block text-sm font-medium text-gray-700 mb-2"
>
<span className="text-red-500">*</span>
</label>
<div className="relative">
<User
size={20}
className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"
/>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleInputChange}
placeholder="请输入您的姓名"
className={`w-full pl-10 pr-4 py-3 border rounded-lg focus:ring-2 focus:ring-primary/20 outline-none transition-all ${
errors.name
? 'border-red-300 focus:border-red-500'
: 'border-gray-200 focus:border-primary'
}`}
/>
</div>
{errors.name && (
<p className="mt-1 text-sm text-red-500">{errors.name}</p>
)}
</div>
{/* 邮箱 */}
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700 mb-2"
>
<span className="text-red-500">*</span>
</label>
<div className="relative">
<Mail
size={20}
className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"
/>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleInputChange}
placeholder="请输入您的邮箱"
className={`w-full pl-10 pr-4 py-3 border rounded-lg focus:ring-2 focus:ring-primary/20 outline-none transition-all ${
errors.email
? 'border-red-300 focus:border-red-500'
: 'border-gray-200 focus:border-primary'
}`}
/>
</div>
{errors.email && (
<p className="mt-1 text-sm text-red-500">{errors.email}</p>
)}
</div>
{/* 主题 */}
<div>
<label
htmlFor="subject"
className="block text-sm font-medium text-gray-700 mb-2"
>
<span className="text-red-500">*</span>
</label>
<div className="relative">
<MessageSquare
size={20}
className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"
/>
<select
id="subject"
name="subject"
value={formData.subject}
onChange={handleInputChange}
className={`w-full pl-10 pr-4 py-3 border rounded-lg focus:ring-2 focus:ring-primary/20 outline-none transition-all appearance-none bg-white ${
errors.subject
? 'border-red-300 focus:border-red-500'
: 'border-gray-200 focus:border-primary'
}`}
>
<option value=""></option>
<option value="业务咨询"></option>
<option value="投资合作"></option>
<option value="技术支持"></option>
<option value="媒体合作"></option>
<option value="其他"></option>
</select>
</div>
{errors.subject && (
<p className="mt-1 text-sm text-red-500">{errors.subject}</p>
)}
</div>
{/* 留言内容 */}
<div>
<label
htmlFor="message"
className="block text-sm font-medium text-gray-700 mb-2"
>
<span className="text-red-500">*</span>
</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleInputChange}
rows={5}
placeholder="请详细描述您的需求..."
className={`w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-primary/20 outline-none transition-all resize-none ${
errors.message
? 'border-red-300 focus:border-red-500'
: 'border-gray-200 focus:border-primary'
}`}
/>
{errors.message && (
<p className="mt-1 text-sm text-red-500">{errors.message}</p>
)}
</div>
{/* 提交按钮 */}
<motion.button
type="submit"
disabled={isSubmitting}
className={`w-full py-4 rounded-lg font-medium text-white transition-all flex items-center justify-center gap-2 ${
isSubmitting
? 'bg-gray-400 cursor-not-allowed'
: 'bg-primary hover:bg-primary-light'
}`}
whileHover={!isSubmitting ? { scale: 1.02 } : {}}
whileTap={!isSubmitting ? { scale: 0.98 } : {}}
>
{isSubmitting ? (
<>
<span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
...
</>
) : (
<>
<Send size={20} />
</>
)}
</motion.button>
</form>
</div>
</motion.div>
{/* 地图和地址信息 */}
<motion.div
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
{/* 地图和办公环境 */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-2 mb-6">
<div className="aspect-[4/3] rounded-xl overflow-hidden relative">
<img
src="https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?w=1200&h=900&fit=crop"
alt="诚裕集团总部大楼 - 现代化办公大厦"
className="w-full h-full object-cover"
/>
<div className="absolute bottom-4 left-4 right-4 bg-white/95 backdrop-blur-sm rounded-lg p-3 shadow-lg">
<p className="text-primary-dark font-medium text-sm mb-1">
📍
</p>
<p className="text-gray-600 text-xs">
88
</p>
<motion.button
className="mt-2 px-4 py-1 bg-primary text-white rounded text-xs hover:bg-primary-light transition-colors"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
</motion.button>
</div>
</div>
</div>
{/* 办公环境展示 */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden mb-6">
<div className="aspect-[16/9] overflow-hidden">
<img
src="https://images.unsplash.com/photo-1497366216548-37526070297c?w=1200&h=675&fit=crop"
alt="诚裕集团办公环境 - 现代化工作空间"
className="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
/>
</div>
</div>
{/* 公司详细信息 */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
<h3 className="text-lg font-semibold text-primary-dark mb-4">
</h3>
<div className="space-y-4">
<div className="flex items-start gap-3">
<MapPin size={20} className="text-accent mt-0.5" />
<div>
<p className="text-sm text-gray-500"></p>
<p className="text-primary-dark">
{COMPANY_INFO.headquarters}
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Phone size={20} className="text-accent mt-0.5" />
<div>
<p className="text-sm text-gray-500">线</p>
<p className="text-primary-dark">{COMPANY_INFO.phone}</p>
</div>
</div>
<div className="flex items-start gap-3">
<Mail size={20} className="text-accent mt-0.5" />
<div>
<p className="text-sm text-gray-500"></p>
<p className="text-primary-dark">{COMPANY_INFO.email}</p>
</div>
</div>
<div className="flex items-start gap-3">
<Clock size={20} className="text-accent mt-0.5" />
<div>
<p className="text-sm text-gray-500"></p>
<p className="text-primary-dark">{COMPANY_INFO.workingHours}</p>
</div>
</div>
</div>
</div>
</motion.div>
</div>
</div>
</section>
{/* 招聘信息 */}
<section className="py-20 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
className="text-center mb-12"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2 className="text-3xl font-bold text-primary-dark mb-4">
</h2>
<p className="text-lg text-gray-600">
</p>
</motion.div>
<div className="grid md:grid-cols-2 gap-6">
{jobPositions.map((job, index) => (
<motion.div
key={job.id}
className="p-6 bg-background rounded-xl border border-gray-100 hover:shadow-md transition-shadow"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1, duration: 0.5 }}
whileHover={{ y: -3 }}
>
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-primary-dark mb-1">
{job.title}
</h3>
<p className="text-sm text-gray-500 flex items-center gap-4">
<span className="flex items-center gap-1">
<Building2 size={14} />
{job.department}
</span>
<span className="flex items-center gap-1">
<MapPin size={14} />
{job.location}
</span>
</p>
</div>
<span className="px-3 py-1 bg-accent/20 text-accent-dark text-sm font-medium rounded-full">
{job.salary}
</span>
</div>
<div className="flex items-center justify-between pt-4 border-t border-gray-100">
<span className="text-sm text-gray-500">{job.type}</span>
<motion.button
className="flex items-center gap-1 text-primary font-medium text-sm hover:text-primary-light"
whileHover={{ x: 3 }}
>
<ArrowRight size={16} />
</motion.button>
</div>
</motion.div>
))}
</div>
<motion.div
className="text-center mt-8"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ delay: 0.3 }}
>
<motion.button
className="inline-flex items-center gap-2 px-8 py-3 border-2 border-primary text-primary font-medium rounded-lg hover:bg-primary hover:text-white transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Briefcase size={20} />
</motion.button>
</motion.div>
</div>
</section>
</main>
{/* 页脚 */}
<Footer />
</motion.div>
);
};
export default Contact;