feat: add skeleton screen components and optimize image config
This commit is contained in:
+7
-1
@@ -4,10 +4,16 @@ const nextConfig: NextConfig = {
|
|||||||
output: 'export',
|
output: 'export',
|
||||||
distDir: 'dist',
|
distDir: 'dist',
|
||||||
images: {
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: '**',
|
||||||
|
},
|
||||||
|
],
|
||||||
unoptimized: true,
|
unoptimized: true,
|
||||||
|
formats: ['image/avif', 'image/webp'],
|
||||||
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
||||||
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
||||||
formats: ['image/webp'],
|
|
||||||
},
|
},
|
||||||
compress: true,
|
compress: true,
|
||||||
poweredByHeader: false,
|
poweredByHeader: false,
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useInView } from 'framer-motion';
|
import { useInView } from 'framer-motion';
|
||||||
import { useRef } from 'react';
|
import { useRef, useState, useEffect } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { PageHeader } from '@/components/ui/page-header';
|
import { PageHeader } from '@/components/ui/page-header';
|
||||||
|
import { ServiceCardSkeleton } from '@/components/ui/loading-skeleton';
|
||||||
import { ArrowRight, Code, Cloud, BarChart3, Shield } from 'lucide-react';
|
import { ArrowRight, Code, Cloud, BarChart3, Shield } from 'lucide-react';
|
||||||
import { SERVICES } from '@/lib/constants';
|
import { SERVICES } from '@/lib/constants';
|
||||||
|
|
||||||
@@ -20,6 +21,12 @@ const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
|||||||
export default function ServicesPage() {
|
export default function ServicesPage() {
|
||||||
const contentRef = useRef(null);
|
const contentRef = useRef(null);
|
||||||
const isContentInView = useInView(contentRef, { once: true, margin: '-100px' });
|
const isContentInView = useInView(contentRef, { once: true, margin: '-100px' });
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => setIsLoading(false), 1000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-white">
|
||||||
@@ -30,54 +37,62 @@ export default function ServicesPage() {
|
|||||||
|
|
||||||
<div className="container-wide relative z-10 py-16" ref={contentRef}>
|
<div className="container-wide relative z-10 py-16" ref={contentRef}>
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-6xl mx-auto">
|
||||||
<div className="grid md:grid-cols-2 gap-8">
|
{isLoading ? (
|
||||||
{SERVICES.map((service, index) => {
|
<div className="grid md:grid-cols-2 gap-8">
|
||||||
const Icon = iconMap[service.icon];
|
{Array.from({ length: 4 }).map((_, index) => (
|
||||||
return (
|
<ServiceCardSkeleton key={index} />
|
||||||
<motion.div
|
))}
|
||||||
key={service.id}
|
</div>
|
||||||
initial={{ opacity: 0, y: 20 }}
|
) : (
|
||||||
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
|
<div className="grid md:grid-cols-2 gap-8">
|
||||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
{SERVICES.map((service, index) => {
|
||||||
>
|
const Icon = iconMap[service.icon];
|
||||||
<Link
|
return (
|
||||||
href={`/services/${service.id}`}
|
<motion.div
|
||||||
className="group bg-white rounded-2xl border border-[#E5E5E5] overflow-hidden hover:shadow-xl transition-all duration-300 block h-full"
|
key={service.id}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={isContentInView ? { opacity:1, y: 0 } : {}}
|
||||||
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||||
>
|
>
|
||||||
<div className="p-8">
|
<Link
|
||||||
<div className="flex items-start gap-4 mb-4">
|
href={`/services/${service.id}`}
|
||||||
<div className="w-14 h-14 rounded-xl bg-[#F5F5F5] flex items-center justify-center group-hover:bg-[#C41E3A] transition-all duration-300">
|
className="group bg-white rounded-2xl border border-[#E5E5E5] overflow-hidden hover:shadow-xl transition-all duration-300 block h-full"
|
||||||
{Icon && <Icon className="w-7 h-7 text-[#1C1C1C] group-hover:text-white transition-colors" />}
|
>
|
||||||
|
<div className="p-8">
|
||||||
|
<div className="flex items-start gap-4 mb-4">
|
||||||
|
<div className="w-14 h-14 rounded-xl bg-[#F5F5F5] flex items-center justify-center group-hover:bg-[#C41E3A] transition-all duration-300">
|
||||||
|
{Icon && <Icon className="w-7 h-7 text-[#1C1C1C] group-hover:text-white transition-colors" />}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-xl font-semibold text-[#1C1C1C] mb-2 group-hover:text-[#C41E3A] transition-colors">
|
||||||
|
{service.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-[#5C5C5C] text-sm leading-relaxed">
|
||||||
|
{service.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="text-xl font-semibold text-[#1C1C1C] mb-2 group-hover:text-[#C41E3A] transition-colors">
|
|
||||||
{service.title}
|
|
||||||
</h3>
|
|
||||||
<p className="text-[#5C5C5C] text-sm leading-relaxed">
|
|
||||||
{service.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 pt-4 border-t border-[#E5E5E5]">
|
<div className="mt-6 pt-4 border-t border-[#E5E5E5]">
|
||||||
<div className="flex flex-wrap gap-2 mb-4">
|
<div className="flex flex-wrap gap-2 mb-4">
|
||||||
{service.features.slice(0, 3).map((feature, idx) => (
|
{service.features.slice(0, 3).map((feature, idx) => (
|
||||||
<Badge key={idx} variant="secondary" className="text-xs">
|
<Badge key={idx} variant="secondary" className="text-xs">
|
||||||
{feature.split(':')[0]}
|
{feature.split(':')[0]}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center text-[#C41E3A] font-medium group-hover:translate-x-2 transition-transform">
|
<div className="flex items-center text-[#C41E3A] font-medium group-hover:translate-x-2 transition-transform">
|
||||||
了解详情
|
了解详情
|
||||||
<ArrowRight className="w-4 h-4 ml-2" />
|
<ArrowRight className="w-4 h-4 ml-2" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
</Link>
|
</motion.div>
|
||||||
</motion.div>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,79 @@
|
|||||||
export function LoadingSkeleton({ className = '' }: { className?: string }) {
|
'use client';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface SkeletonProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Skeleton({ className }: SkeletonProps) {
|
||||||
return (
|
return (
|
||||||
<div className={`animate-pulse bg-gray-200 rounded ${className}`} />
|
<div
|
||||||
|
className={cn(
|
||||||
|
'animate-pulse rounded-md bg-[#F5F5F5]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FormSkeleton() {
|
export function CardSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="bg-[#FAFAFA] rounded-xl border border-[#E5E5E5] p-6">
|
||||||
<LoadingSkeleton className="h-12 w-full" />
|
<Skeleton className="h-12 w-12 rounded-xl mb-4" />
|
||||||
<LoadingSkeleton className="h-12 w-full" />
|
<Skeleton className="h-6 w-3/4 mb-3" />
|
||||||
<LoadingSkeleton className="h-12 w-full" />
|
<Skeleton className="h-4 w-full mb-2" />
|
||||||
<LoadingSkeleton className="h-32 w-full" />
|
<Skeleton className="h-4 w-2/3" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SectionSkeleton() {
|
export function ServiceCardSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="py-24">
|
<div className="bg-white rounded-xl border border-[#E5E5E5] p-6 h-full">
|
||||||
<div className="container-wide">
|
<Skeleton className="h-14 w-14 rounded-xl mb-4" />
|
||||||
<LoadingSkeleton className="h-12 w-1/3 mb-8" />
|
<Skeleton className="h-6 w-3/4 mb-3" />
|
||||||
<LoadingSkeleton className="h-6 w-2/3 mb-4" />
|
<Skeleton className="h-4 w-full mb-2" />
|
||||||
<LoadingSkeleton className="h-6 w-1/2" />
|
<Skeleton className="h-4 w-2/3" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CaseCardSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-xl border border-[#E5E5E5] overflow-hidden">
|
||||||
|
<Skeleton className="h-48 w-full" />
|
||||||
|
<div className="p-6">
|
||||||
|
<Skeleton className="h-5 w-1/2 mb-3" />
|
||||||
|
<Skeleton className="h-6 w-3/4 mb-3" />
|
||||||
|
<Skeleton className="h-4 w-full mb-2" />
|
||||||
|
<Skeleton className="h-4 w-2/3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProductCardSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="bg-[#FAFAFA] rounded-xl border border-[#E5E5E5] p-6 h-full flex flex-col">
|
||||||
|
<Skeleton className="h-6 w-1/3 mb-3" />
|
||||||
|
<Skeleton className="h-7 w-3/4 mb-4" />
|
||||||
|
<Skeleton className="h-4 w-full mb-2" />
|
||||||
|
<Skeleton className="h-4 w-2/3 mb-4" />
|
||||||
|
<Skeleton className="h-10 w-full mt-auto" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NewsCardSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-xl border border-[#E5E5E5] overflow-hidden">
|
||||||
|
<Skeleton className="h-48 w-full" />
|
||||||
|
<div className="p-6">
|
||||||
|
<Skeleton className="h-5 w-1/4 mb-3" />
|
||||||
|
<Skeleton className="h-6 w-3/4 mb-3" />
|
||||||
|
<Skeleton className="h-4 w-full mb-2" />
|
||||||
|
<Skeleton className="h-4 w-2/3" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user