Files
novalon-website/src/app/(marketing)/cases/page.tsx
T
张翔 3ea5cf849e feat(cases): 新增智慧农业案例并优化政府案例数据
- 新增农业种植灌溉信息化建设咨询项目案例
- 更新政府案例:添加关键成果数据、优化客户评价
- 同步更新测试用例和页面组件
2026-04-21 16:57:20 +08:00

255 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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"
/>
</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>
);
}