feat: 统一全站设计风格、导航组件与文案逻辑自洽性修复

- 新增 InkGlowCard 墨韵流光卡片组件,统一全站卡片交互风格
- 新增 PageNav 面包屑组件,统一全站页面导航
- 统一色彩体系、排版层级、间距节奏和动画风格
- 修复 CTA 区品牌名称错误(诺瓦隆→睿新致遠)
- 修复 ERP 产品卖点与年费制定价矛盾
- 导航下拉补充 SDS 和 OA 产品
- 统一全站数据指标为 12+年核心团队经验、6自研产品、10+团队成员
- 移除不可靠的 100%客户满意度和 30+行业专家指标
- 修复新闻时间线不合理问题,调整里程碑节奏
- 统一响应承诺为工作日快速响应
- 服务第4项重命名为行业方案实施,厘清概念
- 服务详情页效果数据改为定性描述
- 删除 cases 模块,精简代码库
This commit is contained in:
张翔
2026-05-02 08:11:47 +08:00
parent 307e0a654e
commit a272e58aaa
64 changed files with 1934 additions and 2995 deletions
@@ -1,209 +0,0 @@
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { CaseDetailClient } from './client';
interface TestCaseItem {
id: string;
title: string;
excerpt: string;
content: string;
category: string;
slug: string;
date: string;
image?: string;
challenge: string;
solution: string;
keyMoments: { title: string; description: string }[];
results: { label: string; value: string }[];
testimonial: { quote: string; author: string; role: string };
duration: string;
}
jest.mock('next/navigation', () => ({
useRouter: () => ({
push: jest.fn(),
back: jest.fn(),
forward: jest.fn(),
}),
}));
jest.mock('next/link', () => {
const MockLink = ({ children, href }: { children: React.ReactNode; href: string }) => {
return <a href={href}>{children}</a>;
};
MockLink.displayName = 'MockLink';
return MockLink;
});
const mockCaseItem: TestCaseItem = {
id: 'test-case',
title: '测试案例标题',
excerpt: '这是一个测试案例的描述',
content: '这是测试案例的详细内容',
category: '制造业',
slug: 'test-case',
date: '2026-03-27',
challenge: '这是客户面临的挑战描述',
solution: '这是我们的解决方案描述',
keyMoments: [
{ title: '关键时刻一', description: '关键时刻一的详细描述' },
{ title: '关键时刻二', description: '关键时刻二的详细描述' },
],
results: [
{ label: '运营成本', value: '降低25%' },
{ label: '设备故障响应', value: '缩短85%' },
{ label: '排产周期', value: '从1周缩至半天' },
],
testimonial: {
quote: '这是客户证言内容',
author: '测试客户',
role: 'CTO',
},
duration: '2年',
};
describe('CaseDetailClient', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('Rendering', () => {
it('should render case detail page', () => {
const { container } = render(<CaseDetailClient caseItem={mockCaseItem} />);
expect(container.firstChild).toBeInTheDocument();
});
it('should render case title', () => {
render(<CaseDetailClient caseItem={mockCaseItem} />);
const title = screen.getByRole('heading', { level: 1 });
expect(title).toBeInTheDocument();
expect(title).toHaveTextContent('测试案例标题');
});
it('should render case excerpt', () => {
render(<CaseDetailClient caseItem={mockCaseItem} />);
const excerpts = screen.getAllByText('这是一个测试案例的描述');
expect(excerpts.length).toBeGreaterThan(0);
});
it('should render case industry badge', () => {
render(<CaseDetailClient caseItem={mockCaseItem} />);
const categories = screen.getAllByText('制造业');
expect(categories.length).toBeGreaterThan(0);
});
it('should render challenge content', () => {
render(<CaseDetailClient caseItem={mockCaseItem} />);
expect(screen.getByText('这是客户面临的挑战描述')).toBeInTheDocument();
});
it('should render solution content', () => {
render(<CaseDetailClient caseItem={mockCaseItem} />);
expect(screen.getByText('这是我们的解决方案描述')).toBeInTheDocument();
});
it('should render results data', () => {
render(<CaseDetailClient caseItem={mockCaseItem} />);
expect(screen.getByText('降低25%')).toBeInTheDocument();
expect(screen.getByText('缩短85%')).toBeInTheDocument();
});
it('should render testimonial', () => {
render(<CaseDetailClient caseItem={mockCaseItem} />);
expect(screen.getByText('这是客户证言内容')).toBeInTheDocument();
const authors = screen.getAllByText('测试客户');
expect(authors.length).toBeGreaterThan(0);
const roles = screen.getAllByText('CTO');
expect(roles.length).toBeGreaterThan(0);
});
it('should render contact button', () => {
render(<CaseDetailClient caseItem={mockCaseItem} />);
const contactButton = screen.getByRole('link', { name: /联系我们/i });
expect(contactButton).toBeInTheDocument();
});
it('should render duration in sidebar', () => {
render(<CaseDetailClient caseItem={mockCaseItem} />);
expect(screen.getByText('2年')).toBeInTheDocument();
});
});
describe('Sections', () => {
it('should render customer challenges section', () => {
render(<CaseDetailClient caseItem={mockCaseItem} />);
const section = screen.getByText('客户遇到的成长瓶颈');
expect(section).toBeInTheDocument();
});
it('should render solution section', () => {
render(<CaseDetailClient caseItem={mockCaseItem} />);
const section = screen.getByText('我们如何智连未来');
expect(section).toBeInTheDocument();
});
it('should render growth story section with key moments', () => {
render(<CaseDetailClient caseItem={mockCaseItem} />);
const section = screen.getByText('共同成长的故事');
expect(section).toBeInTheDocument();
expect(screen.getByText('关键时刻一')).toBeInTheDocument();
expect(screen.getByText('关键时刻二')).toBeInTheDocument();
});
it('should render achievements section with results', () => {
render(<CaseDetailClient caseItem={mockCaseItem} />);
const section = screen.getByText('今天,他们走到了哪里');
expect(section).toBeInTheDocument();
});
it('should render testimonial section', () => {
render(<CaseDetailClient caseItem={mockCaseItem} />);
const section = screen.getByText('客户证言精选');
expect(section).toBeInTheDocument();
});
});
describe('Conditional Rendering', () => {
it('should not render key moments section when empty', () => {
const caseWithoutMoments = { ...mockCaseItem, keyMoments: [] };
render(<CaseDetailClient caseItem={caseWithoutMoments} />);
expect(screen.queryByText('共同成长的故事')).not.toBeInTheDocument();
});
it('should not render results section when empty', () => {
const caseWithoutResults = { ...mockCaseItem, results: [] };
render(<CaseDetailClient caseItem={caseWithoutResults} />);
expect(screen.queryByText('今天,他们走到了哪里')).not.toBeInTheDocument();
});
it('should not render testimonial section when absent', () => {
const caseWithoutTestimonial = { ...mockCaseItem, testimonial: undefined };
render(<CaseDetailClient caseItem={caseWithoutTestimonial} />);
expect(screen.queryByText('客户证言精选')).not.toBeInTheDocument();
});
});
describe('Navigation', () => {
it('should have back button', () => {
render(<CaseDetailClient caseItem={mockCaseItem} />);
const backButton = screen.getByRole('button', { name: /返回/i });
expect(backButton).toBeInTheDocument();
});
});
describe('Accessibility', () => {
it('should have container element', () => {
const { container } = render(<CaseDetailClient caseItem={mockCaseItem} />);
expect(container.firstChild).toBeInTheDocument();
});
it('should have proper heading hierarchy', () => {
render(<CaseDetailClient caseItem={mockCaseItem} />);
const h1 = screen.getByRole('heading', { level: 1 });
expect(h1).toBeInTheDocument();
const h2s = screen.getAllByRole('heading', { level: 2 });
expect(h2s.length).toBeGreaterThan(0);
});
});
});
-286
View File
@@ -1,286 +0,0 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { StaticLink } from '@/components/ui/static-link';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { BackButton } from '@/components/ui/back-button';
import {
Target,
Quote,
Clock,
MessageCircle,
Award,
TrendingUp,
} from 'lucide-react';
interface CaseKeyMoment {
title: string;
description: string;
}
interface CaseResult {
label: string;
value: string;
}
interface CaseTestimonial {
quote: string;
author: string;
role: string;
}
interface CaseItem {
id: string;
title: string;
excerpt: string;
content: string;
category: string;
slug: string;
date: string;
image?: string;
/** 客户面临的挑战 */
challenge: string;
/** 我们的解决方案 */
solution: string;
/** 关键时刻 */
keyMoments: CaseKeyMoment[];
/** 成果数据 */
results: CaseResult[];
/** 客户证言 */
testimonial?: CaseTestimonial;
/** 合作时长 */
duration: string;
}
interface CaseDetailClientProps {
caseItem: CaseItem;
}
export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
const [isVisible, setIsVisible] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry?.isIntersecting) {
setIsVisible(true);
}
},
{ threshold: 0.1 }
);
if (contentRef.current) {
observer.observe(contentRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div className="min-h-screen bg-white">
<div className="relative overflow-hidden bg-gradient-to-b from-[#FAFAFA] to-white">
<div className="container-wide relative z-10 pt-32 pb-20">
<BackButton />
<div className="max-w-4xl mt-8">
<Badge className="mb-4 bg-[#C41E3A]/10 text-[#C41E3A] hover:bg-[#C41E3A]/20">
{caseItem.category}
</Badge>
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-semibold text-[#1C1C1C] mb-2">
{caseItem.title}
</h1>
<p className="text-lg text-[#5C5C5C]">{caseItem.excerpt}</p>
</div>
</div>
</div>
<div ref={contentRef} className="container-wide py-12 md:py-16">
<div
className={`
grid lg:grid-cols-3 gap-8 lg:gap-12
opacity-0 translate-y-4
${isVisible ? 'animate-fade-in-up' : ''}
`}
>
<div className="lg:col-span-2 space-y-12">
{/* 客户遇到的成长瓶颈 */}
<section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[#C41E3A]/20">
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 bg-[#C41E3A] rounded-xl flex items-center justify-center">
<MessageCircle className="w-6 h-6 text-white" />
</div>
<h2 className="text-2xl font-semibold text-[#1C1C1C]">
客户遇到的成长瓶颈
</h2>
</div>
<p className="text-[#5C5C5C] leading-relaxed text-lg">
{caseItem.challenge}
</p>
</section>
{/* 我们如何智连未来 */}
<section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[#C41E3A]/20">
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 bg-[#C41E3A] rounded-xl flex items-center justify-center">
<Target className="w-6 h-6 text-white" />
</div>
<h2 className="text-2xl font-semibold text-[#1C1C1C]">
我们如何智连未来
</h2>
</div>
<div className="prose prose-base max-w-none [&_h3]:text-xl [&_h3]:font-semibold [&_h3]:text-[#1C1C1C] [&_h3]:mt-8 [&_h3]:mb-4 [&_p]:text-[#5C5C5C] [&_p]:leading-[1.8] [&_p]:mb-4 [&_p]:text-base">
<p className="text-[#5C5C5C] leading-relaxed text-lg">
{caseItem.solution}
</p>
</div>
</section>
{/* 共同成长的故事 */}
{caseItem.keyMoments && caseItem.keyMoments.length > 0 && (
<section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[#C41E3A]/20">
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 bg-[#C41E3A] rounded-xl flex items-center justify-center">
<Clock className="w-6 h-6 text-white" />
</div>
<h2 className="text-2xl font-semibold text-[#1C1C1C]">
共同成长的故事
</h2>
</div>
<div className="space-y-4">
{caseItem.keyMoments.map((moment, index) => (
<div
key={index}
className="p-4 bg-white rounded-lg border border-[#E5E5E5]"
>
<div className="flex items-start gap-3">
<Quote className="w-5 h-5 text-[#C41E3A] flex-shrink-0 mt-0.5" />
<div>
<h4 className="font-semibold text-[#1C1C1C] mb-2">
{moment.title}
</h4>
<p className="text-sm text-[#737373]">
{moment.description}
</p>
</div>
</div>
</div>
))}
</div>
</section>
)}
{/* 今天,他们走到了哪里 */}
{caseItem.results && caseItem.results.length > 0 && (
<section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[#C41E3A]/20">
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 bg-[#C41E3A] rounded-xl flex items-center justify-center">
<Award className="w-6 h-6 text-white" />
</div>
<h2 className="text-2xl font-semibold text-[#1C1C1C]">
今天,他们走到了哪里
</h2>
</div>
<div className="grid sm:grid-cols-3 gap-4">
{caseItem.results.map((result, index) => (
<div
key={index}
className="p-6 bg-white rounded-lg border border-[#E5E5E5] hover:border-[#C41E3A] transition-colors"
>
<TrendingUp className="w-8 h-8 text-[#C41E3A] mb-3" />
<div className="text-2xl font-semibold text-[#C41E3A] mb-1">
{result.value}
</div>
<div className="text-sm text-[#737373]">
{result.label}
</div>
</div>
))}
</div>
</section>
)}
{/* 客户证言精选 */}
{caseItem.testimonial && (
<section className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-8 border border-[#C41E3A]/20">
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 bg-[#C41E3A] rounded-xl flex items-center justify-center">
<Quote className="w-6 h-6 text-white" />
</div>
<h2 className="text-2xl font-semibold text-[#1C1C1C]">
客户证言精选
</h2>
</div>
<div className="p-6 bg-white rounded-lg border border-[#E5E5E5]">
<Quote className="w-8 h-8 text-[#C41E3A] mb-4" />
<p className="text-lg text-[#1C1C1C] leading-relaxed mb-4">
{caseItem.testimonial.quote}
</p>
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-[#C41E3A] rounded-full flex items-center justify-center">
<span className="text-white font-semibold">客</span>
</div>
<div>
<p className="font-semibold text-[#1C1C1C]">
{caseItem.testimonial.author}
</p>
<p className="text-sm text-[#737373]">
{caseItem.testimonial.role}
</p>
</div>
</div>
</div>
</section>
)}
</div>
{/* 侧边栏 */}
<div className="space-y-6">
<div className="p-6 bg-[#FAFAFA] rounded-lg border border-[#E5E5E5]">
<h3 className="text-lg font-semibold text-[#1C1C1C] mb-4">
项目信息
</h3>
<dl className="space-y-3">
<div>
<dt className="text-sm text-[#737373]">客户名称</dt>
<dd className="text-[#1C1C1C] font-medium">
{caseItem.testimonial?.author || '客户企业'}
</dd>
</div>
<div>
<dt className="text-sm text-[#737373]">行业领域</dt>
<dd className="text-[#1C1C1C] font-medium">
{caseItem.category}
</dd>
</div>
<div>
<dt className="text-sm text-[#737373]">合作时长</dt>
<dd className="text-[#1C1C1C] font-medium">
{caseItem.duration || '3年'}
</dd>
</div>
<div>
<dt className="text-sm text-[#737373]">发布时间</dt>
<dd className="text-[#1C1C1C] font-medium">{caseItem.date}</dd>
</div>
</dl>
</div>
<div className="p-6 bg-gradient-to-br from-[#C41E3A] to-[#8B1429] rounded-lg text-white">
<h3 className="text-lg font-semibold mb-2">想要了解更多?</h3>
<p className="text-sm text-white/80 mb-4">
联系我们的专家团队,获取定制化解决方案
</p>
<Button
className="w-full bg-white text-[#C41E3A] hover:bg-white/90"
asChild
>
<StaticLink href="/contact">联系我们</StaticLink>
</Button>
</div>
</div>
</div>
</div>
</div>
);
}
-52
View File
@@ -1,52 +0,0 @@
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { CASES } from '@/lib/constants';
import { CaseDetailClient } from './client';
export async function generateStaticParams() {
return CASES.map((caseItem) => ({
id: caseItem.id,
}));
}
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
const { id } = await params;
const caseItem = CASES.find((c) => c.id === id);
if (!caseItem) {
return {
title: '案例未找到',
};
}
return {
title: `${caseItem.title} - 睿新致远`,
description: caseItem.description,
};
}
export default async function CaseDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const caseItem = CASES.find((c) => c.id === id);
if (!caseItem) {
notFound();
}
return <CaseDetailClient caseItem={{
id: caseItem.id,
title: caseItem.title,
excerpt: caseItem.description,
content: caseItem.solution || '',
category: caseItem.industry,
slug: caseItem.id,
date: caseItem.date,
image: caseItem.image,
challenge: caseItem.challenge,
solution: caseItem.solution,
keyMoments: caseItem.keyMoments,
results: caseItem.results,
testimonial: caseItem.testimonial,
duration: caseItem.duration,
}} />;
}
-14
View File
@@ -1,14 +0,0 @@
import { Metadata } from 'next';
export const metadata: Metadata = {
title: '客户案例 - 睿新致远',
description: '与谁同行,决定能走多远',
};
export default function CasesLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}
-255
View File
@@ -1,255 +0,0 @@
'use client';
import { useState, useMemo, useRef, ChangeEvent } from 'react';
import { useInView } from 'framer-motion';
import { CASES } from '@/lib/constants';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { PageHeader } from '@/components/ui/page-header';
import { Search, ArrowLeft, Building2, Calendar, TrendingUp, ChevronLeft, ChevronRight, Filter } from 'lucide-react';
import { StaticLink } from '@/components/ui/static-link';
import { motion } from 'framer-motion';
const industries = ['全部', ...Array.from(new Set(CASES.map((c) => c.industry)))];
const ITEMS_PER_PAGE = 6;
export default function CasesPage() {
const [selectedIndustry, setSelectedIndustry] = useState('全部');
const [searchQuery, setSearchQuery] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const contentRef = useRef(null);
const isContentInView = useInView(contentRef, { once: true, margin: '-100px' });
const filteredCases = useMemo(() => {
return CASES.filter((caseItem) => {
const matchesIndustry = selectedIndustry === '全部' || caseItem.industry === selectedIndustry;
const matchesSearch =
caseItem.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
caseItem.description.toLowerCase().includes(searchQuery.toLowerCase());
return matchesIndustry && matchesSearch;
});
}, [selectedIndustry, searchQuery]);
const totalPages = Math.ceil(filteredCases.length / ITEMS_PER_PAGE);
const paginatedCases = useMemo(() => {
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
const endIndex = startIndex + ITEMS_PER_PAGE;
return filteredCases.slice(startIndex, endIndex);
}, [filteredCases, currentPage]);
const handlePageChange = (page: number) => {
setCurrentPage(page);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const handleIndustryChange = (industry: string) => {
setSelectedIndustry(industry);
setCurrentPage(1);
};
const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
setSearchQuery(e.target.value);
setCurrentPage(1);
};
return (
<div className="min-h-screen bg-white">
<PageHeader
title="与谁同行,决定能走多远"
description="我们与优秀的企业同行,共同成长,共创未来"
/>
<div className="container-wide relative z-10 py-16" ref={contentRef}>
<div className="max-w-6xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="mb-8 space-y-4"
>
<div className="flex flex-col md:flex-row gap-4 items-start md:items-center">
<div className="flex items-center gap-2 text-[#1C1C1C]">
<Filter className="w-5 h-5" />
<span className="font-medium">行业筛选:</span>
</div>
<div className="flex flex-wrap gap-2">
{industries.map((industry) => (
<Button
key={industry}
variant={selectedIndustry === industry ? 'default' : 'outline'}
onClick={() => handleIndustryChange(industry)}
className={
selectedIndustry === industry
? 'bg-[#C41E3A] hover:bg-[#A01830] text-white'
: ''
}
>
{industry}
</Button>
))}
</div>
</div>
<div className="relative max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[#5C5C5C] w-5 h-5" />
<Input
type="text"
placeholder="搜索案例..."
value={searchQuery}
onChange={handleSearchChange}
className="pl-10"
aria-label="搜索案例"
/>
</div>
</motion.div>
{paginatedCases.length === 0 ? (
<div className="text-center py-20">
<p className="text-xl text-[#5C5C5C]">没有找到相关案例</p>
</div>
) : (
<>
<div className="grid md:grid-cols-2 gap-8">
{paginatedCases.map((caseItem, index) => (
<motion.div
key={caseItem.id}
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<StaticLink
href={`/cases/${caseItem.id}`}
className="group bg-white rounded-2xl border border-[#E5E5E5] overflow-hidden hover:shadow-xl transition-all duration-300 block"
>
<div className="relative h-48 bg-gradient-to-br from-[#F5F5F5] to-[#E5E5E5] overflow-hidden">
<div className="absolute inset-0 flex items-center justify-center">
<Building2 className="w-24 h-24 text-[#C41E3A]/20 group-hover:scale-110 transition-transform duration-300" />
</div>
<div className="absolute top-4 right-4">
<Badge className="bg-white/90 text-[#1C1C1C] hover:bg-white">
{caseItem.industry}
</Badge>
</div>
</div>
<div className="p-6">
<div className="flex items-center gap-2 mb-3">
<Building2 className="w-5 h-5 text-[#C41E3A]" />
<span className="font-semibold text-[#1C1C1C]">{caseItem.client}</span>
</div>
<h3 className="text-xl font-bold text-[#1C1C1C] mb-3 group-hover:text-[#C41E3A] transition-colors">
{caseItem.title}
</h3>
<div className="flex flex-wrap gap-2 mb-4">
<Badge variant="secondary" className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
{caseItem.duration}合作
</Badge>
{caseItem.tags.slice(0, 1).map((tag) => (
<Badge key={tag} variant="secondary" className="flex items-center gap-1">
<TrendingUp className="w-3 h-3" />
{tag}
</Badge>
))}
</div>
<p className="text-[#5C5C5C] text-sm line-clamp-2 mb-4">
{caseItem.description}
</p>
<div className="flex items-center text-[#C41E3A] font-medium group-hover:translate-x-2 transition-transform">
查看详情
<ArrowLeft className="w-4 h-4 ml-2 rotate-180" />
</div>
</div>
</StaticLink>
</motion.div>
))}
</div>
{totalPages > 1 && (
<div className="flex justify-center items-center gap-2 mt-8">
<Button
variant="outline"
size="icon"
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
>
<ChevronLeft className="w-4 h-4" />
</Button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<Button
key={page}
variant={currentPage === page ? 'default' : 'outline'}
size="icon"
onClick={() => handlePageChange(page)}
className={
currentPage === page
? 'bg-[#C41E3A] hover:bg-[#A01830] text-white'
: ''
}
>
{page}
</Button>
))}
<Button
variant="outline"
size="icon"
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
)}
<div className="text-center mt-4 text-[#5C5C5C] text-sm">
显示 {paginatedCases.length} 条,共 {filteredCases.length} 条案例
</div>
</>
)}
</div>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.4 }}
className="bg-[#F5F5F5] py-16"
>
<div className="container-wide text-center">
<h2 className="text-3xl font-bold text-[#1C1C1C] mb-6">
准备开始您的数字化转型之旅?
</h2>
<p className="text-lg text-[#5C5C5C] mb-8 max-w-2xl mx-auto">
让我们与您同行,共创美好未来
</p>
<div className="flex justify-center gap-4">
<StaticLink href="/contact">
<Button
size="lg"
variant="outline"
>
联系我们
</Button>
</StaticLink>
<StaticLink href="/contact">
<Button
size="lg"
className="bg-[#C41E3A] hover:bg-[#A01830] text-white"
>
立即咨询
<ArrowLeft className="ml-2 w-4 h-4 rotate-180" />
</Button>
</StaticLink>
</div>
</div>
</motion.div>
</div>
);
}