diff --git a/src/app/(marketing)/layout.tsx b/src/app/(marketing)/layout.tsx index 24c4a16..1d75650 100644 --- a/src/app/(marketing)/layout.tsx +++ b/src/app/(marketing)/layout.tsx @@ -1,3 +1,5 @@ +import { ErrorBoundary } from '@/components/ui/error-boundary'; + export default function MarketingLayout({ children, }: { @@ -5,7 +7,9 @@ export default function MarketingLayout({ }) { return (
-
{children}
+ +
{children}
+
); } diff --git a/src/app/(marketing)/page.tsx b/src/app/(marketing)/page.tsx index 42705c8..364520a 100644 --- a/src/app/(marketing)/page.tsx +++ b/src/app/(marketing)/page.tsx @@ -1,13 +1,50 @@ "use client"; +import dynamic from 'next/dynamic'; import { Header } from "@/components/layout/header"; import { Footer } from "@/components/layout/footer"; import { HeroSection } from "@/components/sections/hero-section"; -import { ServicesSection } from "@/components/sections/services-section"; -import { ProductsSection } from "@/components/sections/products-section"; -import { AboutSection } from "@/components/sections/about-section"; -import { NewsSection } from "@/components/sections/news-section"; -import { ContactSection } from "@/components/sections/contact-section"; +import { SectionSkeleton } from "@/components/ui/loading-skeleton"; + +const ServicesSection = dynamic( + () => import('@/components/sections/services-section').then(mod => ({ default: mod.ServicesSection })), + { + loading: () => , + ssr: true + } +); + +const ProductsSection = dynamic( + () => import('@/components/sections/products-section').then(mod => ({ default: mod.ProductsSection })), + { + loading: () => , + ssr: true + } +); + +const AboutSection = dynamic( + () => import('@/components/sections/about-section').then(mod => ({ default: mod.AboutSection })), + { + loading: () => , + ssr: true + } +); + +const NewsSection = dynamic( + () => import('@/components/sections/news-section').then(mod => ({ default: mod.NewsSection })), + { + loading: () => , + ssr: true + } +); + +const ContactSection = dynamic( + () => import('@/components/sections/contact-section').then(mod => ({ default: mod.ContactSection })), + { + loading: () => , + ssr: true + } +); export default function HomePage() { return ( diff --git a/src/components/sections/contact-section.tsx b/src/components/sections/contact-section.tsx index a8fe408..b4e2924 100644 --- a/src/components/sections/contact-section.tsx +++ b/src/components/sections/contact-section.tsx @@ -5,6 +5,7 @@ import { z } from 'zod'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; +import { Toast } from '@/components/ui/toast'; import { Mail, Phone, MapPin, Send, Loader2, Clock, HeadphonesIcon, CheckCircle2 } from 'lucide-react'; import { COMPANY_INFO } from '@/lib/constants'; @@ -28,6 +29,9 @@ export function ContactSection() { const [isVisible, setIsVisible] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitted, setIsSubmitted] = useState(false); + const [showToast, setShowToast] = useState(false); + const [toastMessage, setToastMessage] = useState(''); + const [toastType, setToastType] = useState<'success' | 'error'>('success'); const [formData, setFormData] = useState({ name: '', phone: '', @@ -96,14 +100,31 @@ export function ContactSection() { setIsSubmitting(true); - await new Promise(resolve => setTimeout(resolve, 1500)); - - setIsSubmitting(false); - setIsSubmitted(true); + try { + await new Promise(resolve => setTimeout(resolve, 1500)); + + setIsSubmitting(false); + setIsSubmitted(true); + setToastMessage('表单提交成功!我们会尽快与您联系。'); + setToastType('success'); + setShowToast(true); + } catch (error) { + setIsSubmitting(false); + setToastMessage('提交失败,请稍后重试。'); + setToastType('error'); + setShowToast(true); + } } return (
+ {showToast && ( + setShowToast(false)} + /> + )}
diff --git a/src/components/ui/error-boundary.tsx b/src/components/ui/error-boundary.tsx new file mode 100644 index 0000000..612b7c0 --- /dev/null +++ b/src/components/ui/error-boundary.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { Component, ReactNode } from 'react'; + +interface Props { + children: ReactNode; + fallback?: ReactNode; +} + +interface State { + hasError: boolean; + error?: Error; +} + +export class ErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error) { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('Error caught by boundary:', error, errorInfo); + } + + render() { + if (this.state.hasError) { + return this.props.fallback || ( +
+
+
+ + + +
+

出错了

+

+ 抱歉,页面出现了问题。请刷新页面重试。 +

+ +
+
+ ); + } + + return this.props.children; + } +} diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx new file mode 100644 index 0000000..9e76566 --- /dev/null +++ b/src/components/ui/toast.tsx @@ -0,0 +1,64 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { CheckCircle2, X, AlertCircle, Info } from 'lucide-react'; + +interface ToastProps { + message: string; + type?: 'success' | 'error' | 'info'; + duration?: number; + onClose: () => void; +} + +export function Toast({ + message, + type = 'success', + duration = 3000, + onClose +}: ToastProps) { + const [isVisible, setIsVisible] = useState(true); + + useEffect(() => { + const timer = setTimeout(() => { + setIsVisible(false); + setTimeout(onClose, 300); + }, duration); + + return () => clearTimeout(timer); + }, [duration, onClose]); + + const icons = { + success: , + error: , + info: + }; + + const bgColors = { + success: 'bg-green-50 border-green-200', + error: 'bg-red-50 border-red-200', + info: 'bg-blue-50 border-blue-200' + }; + + return ( +
+ {icons[type]} +

{message}

+ +
+ ); +}