feat(perf,ux): implement performance and UX optimizations
Phase 2: Performance Optimizations - Implement dynamic imports for non-critical sections - Add loading skeletons for lazy-loaded components - Optimize bundle size with code splitting - Enable SSR for dynamic components Phase 3: UX Optimizations - Create ErrorBoundary component for graceful error handling - Add Toast notification component for user feedback - Implement success/error notifications in contact form - Add error handling with user-friendly messages Files modified: - src/app/(marketing)/page.tsx: Dynamic imports for sections - src/app/(marketing)/layout.tsx: Error boundary integration - src/components/sections/contact-section.tsx: Toast notifications - src/components/ui/error-boundary.tsx: New error boundary component - src/components/ui/toast.tsx: New toast notification component Impact: - Reduced initial bundle size - Faster page load times - Better error handling - Improved user feedback - Enhanced user experience
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import { ErrorBoundary } from '@/components/ui/error-boundary';
|
||||||
|
|
||||||
export default function MarketingLayout({
|
export default function MarketingLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
@@ -5,7 +7,9 @@ export default function MarketingLayout({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
|
<ErrorBoundary>
|
||||||
<main className="flex-1">{children}</main>
|
<main className="flex-1">{children}</main>
|
||||||
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,50 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
import { Header } from "@/components/layout/header";
|
import { Header } from "@/components/layout/header";
|
||||||
import { Footer } from "@/components/layout/footer";
|
import { Footer } from "@/components/layout/footer";
|
||||||
import { HeroSection } from "@/components/sections/hero-section";
|
import { HeroSection } from "@/components/sections/hero-section";
|
||||||
import { ServicesSection } from "@/components/sections/services-section";
|
import { SectionSkeleton } from "@/components/ui/loading-skeleton";
|
||||||
import { ProductsSection } from "@/components/sections/products-section";
|
|
||||||
import { AboutSection } from "@/components/sections/about-section";
|
const ServicesSection = dynamic(
|
||||||
import { NewsSection } from "@/components/sections/news-section";
|
() => import('@/components/sections/services-section').then(mod => ({ default: mod.ServicesSection })),
|
||||||
import { ContactSection } from "@/components/sections/contact-section";
|
{
|
||||||
|
loading: () => <SectionSkeleton />,
|
||||||
|
ssr: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const ProductsSection = dynamic(
|
||||||
|
() => import('@/components/sections/products-section').then(mod => ({ default: mod.ProductsSection })),
|
||||||
|
{
|
||||||
|
loading: () => <SectionSkeleton />,
|
||||||
|
ssr: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const AboutSection = dynamic(
|
||||||
|
() => import('@/components/sections/about-section').then(mod => ({ default: mod.AboutSection })),
|
||||||
|
{
|
||||||
|
loading: () => <SectionSkeleton />,
|
||||||
|
ssr: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const NewsSection = dynamic(
|
||||||
|
() => import('@/components/sections/news-section').then(mod => ({ default: mod.NewsSection })),
|
||||||
|
{
|
||||||
|
loading: () => <SectionSkeleton />,
|
||||||
|
ssr: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const ContactSection = dynamic(
|
||||||
|
() => import('@/components/sections/contact-section').then(mod => ({ default: mod.ContactSection })),
|
||||||
|
{
|
||||||
|
loading: () => <SectionSkeleton />,
|
||||||
|
ssr: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { z } from 'zod';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
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 { Mail, Phone, MapPin, Send, Loader2, Clock, HeadphonesIcon, CheckCircle2 } from 'lucide-react';
|
||||||
import { COMPANY_INFO } from '@/lib/constants';
|
import { COMPANY_INFO } from '@/lib/constants';
|
||||||
|
|
||||||
@@ -28,6 +29,9 @@ export function ContactSection() {
|
|||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [isSubmitted, setIsSubmitted] = 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<ContactFormData>({
|
const [formData, setFormData] = useState<ContactFormData>({
|
||||||
name: '',
|
name: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
@@ -96,14 +100,31 @@ export function ContactSection() {
|
|||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
|
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
setIsSubmitted(true);
|
setIsSubmitted(true);
|
||||||
|
setToastMessage('表单提交成功!我们会尽快与您联系。');
|
||||||
|
setToastType('success');
|
||||||
|
setShowToast(true);
|
||||||
|
} catch (error) {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
setToastMessage('提交失败,请稍后重试。');
|
||||||
|
setToastType('error');
|
||||||
|
setShowToast(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="contact" className="section-padding relative bg-white overflow-hidden" ref={sectionRef}>
|
<section id="contact" className="section-padding relative bg-white overflow-hidden" ref={sectionRef}>
|
||||||
|
{showToast && (
|
||||||
|
<Toast
|
||||||
|
message={toastMessage}
|
||||||
|
type={toastType}
|
||||||
|
onClose={() => setShowToast(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="absolute inset-0 pointer-events-none">
|
<div className="absolute inset-0 pointer-events-none">
|
||||||
<div className="absolute inset-0 bg-gradient-radial from-[rgba(79,70,229,0.03)] via-transparent to-transparent" />
|
<div className="absolute inset-0 bg-gradient-radial from-[rgba(79,70,229,0.03)] via-transparent to-transparent" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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<Props, State> {
|
||||||
|
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 || (
|
||||||
|
<div className="flex items-center justify-center min-h-[400px] p-8">
|
||||||
|
<div className="text-center max-w-md">
|
||||||
|
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<svg
|
||||||
|
className="w-8 h-8 text-red-500"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-[#1C1C1C] mb-4">出错了</h2>
|
||||||
|
<p className="text-[#5C5C5C] mb-6">
|
||||||
|
抱歉,页面出现了问题。请刷新页面重试。
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => this.setState({ hasError: false, error: undefined })}
|
||||||
|
className="px-6 py-2.5 bg-[#C41E3A] text-white rounded-lg hover:bg-[#A01830] transition-colors"
|
||||||
|
>
|
||||||
|
重试
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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: <CheckCircle2 className="h-5 w-5 text-green-500" />,
|
||||||
|
error: <AlertCircle className="h-5 w-5 text-red-500" />,
|
||||||
|
info: <Info className="h-5 w-5 text-blue-500" />
|
||||||
|
};
|
||||||
|
|
||||||
|
const bgColors = {
|
||||||
|
success: 'bg-green-50 border-green-200',
|
||||||
|
error: 'bg-red-50 border-red-200',
|
||||||
|
info: 'bg-blue-50 border-blue-200'
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`fixed bottom-4 right-4 z-50 flex items-center gap-3 px-4 py-3 bg-white rounded-lg shadow-lg border transition-all duration-300 ${
|
||||||
|
isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-2'
|
||||||
|
} ${bgColors[type]}`}
|
||||||
|
role="alert"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
{icons[type]}
|
||||||
|
<p className="text-sm font-medium text-[#1C1C1C]">{message}</p>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsVisible(false);
|
||||||
|
setTimeout(onClose, 300);
|
||||||
|
}}
|
||||||
|
className="ml-2 text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
aria-label="关闭提示"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user