From 5924a7d493ea9e2f495760b5123863ca7fe868ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Fri, 27 Feb 2026 20:30:36 +0800 Subject: [PATCH] feat: add skeleton screen components and optimize image config --- next.config.ts | 8 +- src/app/(marketing)/services/page.tsx | 103 ++++++++++++++----------- src/components/ui/loading-skeleton.tsx | 80 +++++++++++++++---- 3 files changed, 132 insertions(+), 59 deletions(-) diff --git a/next.config.ts b/next.config.ts index 274b130..6d86003 100644 --- a/next.config.ts +++ b/next.config.ts @@ -4,10 +4,16 @@ const nextConfig: NextConfig = { output: 'export', distDir: 'dist', images: { + remotePatterns: [ + { + protocol: 'https', + hostname: '**', + }, + ], unoptimized: true, + formats: ['image/avif', 'image/webp'], deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], - formats: ['image/webp'], }, compress: true, poweredByHeader: false, diff --git a/src/app/(marketing)/services/page.tsx b/src/app/(marketing)/services/page.tsx index fc305c9..34c9ec2 100644 --- a/src/app/(marketing)/services/page.tsx +++ b/src/app/(marketing)/services/page.tsx @@ -3,10 +3,11 @@ import Link from 'next/link'; import { motion } 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 { Badge } from '@/components/ui/badge'; import { PageHeader } from '@/components/ui/page-header'; +import { ServiceCardSkeleton } from '@/components/ui/loading-skeleton'; import { ArrowRight, Code, Cloud, BarChart3, Shield } from 'lucide-react'; import { SERVICES } from '@/lib/constants'; @@ -20,6 +21,12 @@ const iconMap: Record> = { export default function ServicesPage() { const contentRef = useRef(null); const isContentInView = useInView(contentRef, { once: true, margin: '-100px' }); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const timer = setTimeout(() => setIsLoading(false), 1000); + return () => clearTimeout(timer); + }, []); return (
@@ -30,54 +37,62 @@ export default function ServicesPage() {
-
- {SERVICES.map((service, index) => { - const Icon = iconMap[service.icon]; - return ( - - + {Array.from({ length: 4 }).map((_, index) => ( + + ))} +
+ ) : ( +
+ {SERVICES.map((service, index) => { + const Icon = iconMap[service.icon]; + return ( + -
-
-
- {Icon && } + +
+
+
+ {Icon && } +
+
+

+ {service.title} +

+

+ {service.description} +

+
-
-

- {service.title} -

-

- {service.description} -

-
-
-
-
- {service.features.slice(0, 3).map((feature, idx) => ( - - {feature.split(':')[0]} - - ))} -
-
- 了解详情 - +
+
+ {service.features.slice(0, 3).map((feature, idx) => ( + + {feature.split(':')[0]} + + ))} +
+
+ 了解详情 + +
-
- - - ); - })} -
+ + + ); + })} +
+ )}
diff --git a/src/components/ui/loading-skeleton.tsx b/src/components/ui/loading-skeleton.tsx index 8db7feb..47eda81 100644 --- a/src/components/ui/loading-skeleton.tsx +++ b/src/components/ui/loading-skeleton.tsx @@ -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 ( -
+
); } -export function FormSkeleton() { +export function CardSkeleton() { return ( -
- - - - +
+ + + +
); } -export function SectionSkeleton() { +export function ServiceCardSkeleton() { return ( -
-
- - - +
+ + + + +
+ ); +} + +export function CaseCardSkeleton() { + return ( +
+ +
+ + + + +
+
+ ); +} + +export function ProductCardSkeleton() { + return ( +
+ + + + + +
+ ); +} + +export function NewsCardSkeleton() { + return ( +
+ +
+ + + +
);