Files
novalon-website/src/components/sections/news-section.tsx
T
张翔 ac2672729f feat: 实现内容管理API及相关功能
refactor(services-section): 重构服务展示组件使用API数据
refactor(news-section): 重构新闻展示组件使用API数据
refactor(products-section): 重构产品展示组件使用API数据

test: 添加API客户端和服务钩子的单元测试
test(e2e): 添加配置验证和API响应格式的端到端测试

ci: 更新Playwright测试配置
2026-03-13 18:55:25 +08:00

151 lines
5.6 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 { motion } from 'framer-motion';
import { useInView } from 'framer-motion';
import { useRef, useMemo } from 'react';
import Link from 'next/link';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { ArrowRight, Calendar } from 'lucide-react';
import { useNews } from '@/hooks/use-news';
interface NewsConfig {
enabled?: boolean;
displayCount?: number;
categories?: string[];
sortOrder?: 'asc' | 'desc';
}
interface NewsSectionProps {
config?: NewsConfig;
}
export function NewsSection({ config }: NewsSectionProps) {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: '-100px' });
const { news, loading, error } = useNews();
const displayedNews = useMemo(() => {
if (!news || news.length === 0) {
return [];
}
let filtered = news;
if (config?.categories && config.categories.length > 0) {
filtered = filtered.filter(newsItem => config.categories?.includes(newsItem.category));
}
if (config?.sortOrder === 'asc') {
filtered = [...filtered].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
} else {
filtered = [...filtered].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
}
const count = config?.displayCount || 4;
return filtered.slice(0, count);
}, [news, config]);
if (loading) {
return (
<section id="news" role="region" aria-labelledby="news-heading" className="py-24 bg-[#F5F5F5]" ref={ref}>
<div className="container-custom">
<div className="text-center">
<p className="text-lg text-[#5C5C5C]">...</p>
</div>
</div>
</section>
);
}
if (error) {
return (
<section id="news" role="region" aria-labelledby="news-heading" className="py-24 bg-[#F5F5F5]" ref={ref}>
<div className="container-custom">
<div className="text-center">
<p className="text-lg text-red-600"></p>
</div>
</div>
</section>
);
}
return (
<section id="news" role="region" aria-labelledby="news-heading" className="py-24 bg-[#F5F5F5]" ref={ref}>
<div className="container-custom">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5 }}
className="text-center max-w-3xl mx-auto mb-16"
>
<h2 id="news-heading" className="text-3xl sm:text-4xl lg:text-5xl font-bold text-[#1C1C1C] mb-6">
<span className="text-[#C41E3A]"></span>
</h2>
<p className="text-lg text-[#5C5C5C]">
</p>
</motion.div>
{displayedNews.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-5xl mx-auto">
{displayedNews.map((newsItem, idx) => (
<motion.div
key={newsItem.id}
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.4, delay: idx * 0.08 }}
>
<Card className="h-full flex flex-col group cursor-pointer border-[#E5E5E5] hover:border-[#1C1C1C]">
<CardHeader>
<div className="flex items-center gap-2 mb-3">
<span className="inline-block px-2 py-0.5 rounded-full bg-[#F5F5F5] text-[#1C1C1C] text-xs font-medium">
{newsItem.category}
</span>
<span className="text-sm text-[#5C5C5C] flex items-center gap-1">
<Calendar className="w-3 h-3" />
{newsItem.date}
</span>
</div>
<CardTitle className="text-xl leading-tight">{newsItem.title}</CardTitle>
</CardHeader>
<CardContent className="flex-1 flex flex-col">
<CardDescription className="text-base leading-relaxed mb-6 flex-1">
{newsItem.excerpt}
</CardDescription>
<a
href={`/news/${newsItem.id}`}
className="inline-flex items-center text-sm font-medium text-[#1C1C1C] hover:text-[#C41E3A] transition-colors group/link"
>
<ArrowRight className="ml-1 w-4 h-4 transition-transform group-hover/link:translate-x-1" />
</a>
</CardContent>
</Card>
</motion.div>
))}
</div>
) : (
<div className="text-center py-12">
<p className="text-lg text-[#5C5C5C]"></p>
</div>
)}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.5 }}
className="mt-12 text-center"
>
<Link
href="/news"
className="inline-flex items-center text-sm font-medium text-[#1C1C1C] hover:text-[#C41E3A] transition-colors bg-transparent border-none cursor-pointer group"
>
<ArrowRight className="ml-1 w-4 h-4 transition-transform group-hover:translate-x-1" />
</Link>
</motion.div>
</div>
</section>
);
}