feat: 添加管理后台页面和功能,优化测试和性能配置
refactor: 重构页面导航和滚动逻辑,提升用户体验 test: 更新测试配置和用例,增加覆盖率和稳定性 perf: 优化性能指标和阈值,适应开发环境需求 ci: 添加Lighthouse CI工作流,集成性能测试 docs: 更新API文档和健康检查端点 fix: 修复登录页面和表单提交问题 style: 调整响应式布局和可访问性改进 chore: 更新依赖项和脚本配置
This commit is contained in:
@@ -1,19 +1,120 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo, useRef, useEffect, ChangeEvent } from 'react';
|
||||
import { useInView } from 'framer-motion';
|
||||
import { contentService } from '@/lib/api/services';
|
||||
import { Card } from '@/components/ui/card';
|
||||
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 Link from 'next/link';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useInView } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { PageHeader } from '@/components/ui/page-header';
|
||||
import { ArrowLeft, ArrowRight, Building2, Calendar, TrendingUp } from 'lucide-react';
|
||||
import { CASES } from '@/lib/constants';
|
||||
|
||||
const industries = ['全部', '金融', '制造', '零售', '医疗', '教育'];
|
||||
const ITEMS_PER_PAGE = 6;
|
||||
|
||||
interface CaseItem {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
industry: string;
|
||||
client: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export default function CasesPage() {
|
||||
const [selectedIndustry, setSelectedIndustry] = useState('全部');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [cases, setCases] = useState<CaseItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const contentRef = useRef(null);
|
||||
const isContentInView = useInView(contentRef, { once: true, margin: '-100px' });
|
||||
|
||||
const fetchCases = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await contentService.getNews(['案例'], 100, 'desc');
|
||||
const caseItems: CaseItem[] = data.map(item => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
description: item.excerpt,
|
||||
industry: item.category,
|
||||
client: '客户企业',
|
||||
slug: item.slug,
|
||||
}));
|
||||
setCases(caseItems);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Failed to fetch cases'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCases();
|
||||
}, []);
|
||||
|
||||
const filteredCases = useMemo(() => {
|
||||
if (!cases || cases.length === 0) return [];
|
||||
|
||||
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;
|
||||
});
|
||||
}, [cases, 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);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#C41E3A] mx-auto mb-4"></div>
|
||||
<p className="text-[#5C5C5C]">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-red-600 mb-4">加载案例失败</p>
|
||||
<Button onClick={() => window.location.reload()}>重新加载</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
<PageHeader
|
||||
@@ -27,63 +128,155 @@ export default function CasesPage() {
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
返回首页
|
||||
</Link>
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
{CASES.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 }}
|
||||
>
|
||||
<Link
|
||||
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>
|
||||
<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>
|
||||
|
||||
<h3 className="text-xl font-bold text-[#1C1C1C] mb-3 group-hover:text-[#C41E3A] transition-colors">
|
||||
{caseItem.title}
|
||||
</h3>
|
||||
<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>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
<Badge variant="secondary" className="flex items-center gap-1">
|
||||
<Calendar className="w-3 h-3" />
|
||||
3年合作
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="flex items-center gap-1">
|
||||
<TrendingUp className="w-3 h-3" />
|
||||
数字化转型
|
||||
</Badge>
|
||||
</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 }}
|
||||
>
|
||||
<Link
|
||||
href={`/cases/${caseItem.slug}`}
|
||||
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>
|
||||
|
||||
<p className="text-[#5C5C5C] text-sm line-clamp-2 mb-4">
|
||||
{caseItem.description}
|
||||
</p>
|
||||
<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>
|
||||
|
||||
<div className="flex items-center text-[#C41E3A] font-medium group-hover:translate-x-2 transition-transform">
|
||||
查看详情
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
</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" />
|
||||
3年合作
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="flex items-center gap-1">
|
||||
<TrendingUp className="w-3 h-3" />
|
||||
数字化转型
|
||||
</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>
|
||||
</Link>
|
||||
</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>
|
||||
|
||||
@@ -115,7 +308,7 @@ export default function CasesPage() {
|
||||
className="bg-[#C41E3A] hover:bg-[#A01830] text-white"
|
||||
>
|
||||
立即咨询
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
<ArrowLeft className="ml-2 w-4 h-4 rotate-180" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user