Files
novalon-website/src/app/(marketing)/news/page.tsx
T
张翔 1f591fe2b4 feat: 完成网站功能开发与优化
- 完善产品页面布局与交互
- 优化服务详情页用户体验
- 增强新闻模块内容展示
- 改进团队页面设计
- 优化全局样式和响应式布局
- 添加分页组件支持
- 提升性能与SEO优化
- 修复已知问题与改进代码质量
2026-04-27 20:53:39 +08:00

198 lines
8.5 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 { NEWS } from '@/lib/constants';
import { Card, CardContent } 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, Calendar, Filter, ArrowRight, SearchX } from 'lucide-react';
import { StaticLink } from '@/components/ui/static-link';
import { motion } from 'framer-motion';
import { Pagination } from '@/components/ui/pagination';
const categories = ['全部', '公司新闻', '产品发布', '研发动态'];
const ITEMS_PER_PAGE = 9;
export default function NewsListPage() {
const [selectedCategory, setSelectedCategory] = useState('全部');
const [searchQuery, setSearchQuery] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const contentRef = useRef(null);
const isContentInView = useInView(contentRef, { once: true, margin: '-100px' });
const filteredNews = useMemo(() => {
return NEWS.filter((newsItem) => {
const matchesCategory = selectedCategory === '全部' || newsItem.category === selectedCategory;
const matchesSearch =
newsItem.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
newsItem.excerpt.toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch;
}).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
}, [selectedCategory, searchQuery]);
const totalPages = Math.ceil(filteredNews.length / ITEMS_PER_PAGE);
const paginatedNews = useMemo(() => {
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
const endIndex = startIndex + ITEMS_PER_PAGE;
return filteredNews.slice(startIndex, endIndex);
}, [filteredNews, currentPage]);
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
const handleCategoryChange = (category: string) => {
setSelectedCategory(category);
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-12" ref={contentRef} id="page-content">
<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">
{categories.map((category) => (
<Button
key={category}
variant={selectedCategory === category ? 'default' : 'outline'}
onClick={() => handleCategoryChange(category)}
className={
selectedCategory === category
? 'bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary-hover)] text-white'
: ''
}
>
{category}
</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>
{paginatedNews.length === 0 ? (
<div className="text-center py-20">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-[#F5F5F5] flex items-center justify-center">
<SearchX className="w-8 h-8 text-[#5C5C5C]" />
</div>
<p className="text-lg text-[#5C5C5C] mb-4"></p>
<button
onClick={() => { setSearchQuery(''); setSelectedCategory('全部'); setCurrentPage(1); }}
className="text-sm text-[var(--color-brand-primary)] hover:text-[var(--color-brand-primary-hover)] font-medium transition-colors"
>
</button>
</div>
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{paginatedNews.map((newsItem, index) => (
<motion.div
key={newsItem.id}
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.2 + index * 0.1 }}
>
<StaticLink href={`/news/${newsItem.id}`}>
<Card className="h-full hover:shadow-lg transition-shadow cursor-pointer border-[#E5E5E5] hover:border-[var(--color-brand-primary)]">
<CardContent className="p-0">
{newsItem.image ? (
<div className="aspect-video bg-gray-100 overflow-hidden">
<img
src={newsItem.image}
alt={newsItem.title}
className="w-full h-full object-cover"
/>
</div>
) : (
<div className="aspect-video bg-gradient-to-br from-[var(--color-brand-primary)]/10 to-[#1C1C1C]/10 flex items-center justify-center mb-4">
<span className="text-4xl">📰</span>
</div>
)}
<div className="p-6">
<div className="flex items-center gap-2 mb-3">
<Badge variant="secondary">{newsItem.category}</Badge>
<div className="flex items-center gap-1 text-sm text-[#5C5C5C]">
<Calendar className="w-4 h-4" />
{newsItem.date}
</div>
</div>
<h3 className="text-xl font-semibold text-[#1C1C1C] mb-3 line-clamp-2">
{newsItem.title}
</h3>
<p className="text-[#5C5C5C] text-sm line-clamp-3 mb-4">
{newsItem.excerpt}
</p>
<div className="flex items-center text-[var(--color-brand-primary)] text-sm font-medium group">
<ArrowRight className="ml-1 w-4 h-4" />
</div>
</div>
</CardContent>
</Card>
</StaticLink>
</motion.div>
))}
</div>
<Pagination currentPage={currentPage} totalPages={totalPages} onPageChange={handlePageChange} scrollTargetId="page-content" />
<div className="text-center mt-4 text-[#5C5C5C] text-sm">
{paginatedNews.length} {filteredNews.length}
</div>
</>
)}
{/* Bottom CTA */}
<div className="mt-12 text-center py-16 bg-[#F5F5F5] rounded-2xl">
<h3 className="text-xl md:text-2xl font-semibold text-[#1C1C1C] mb-3">
</h3>
<p className="text-[#5C5C5C] mb-6 max-w-lg mx-auto">
</p>
<Button size="lg" className="bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary-hover)] text-white" asChild>
<StaticLink href="/contact">
<ArrowRight className="ml-2 w-4 h-4" />
</StaticLink>
</Button>
</div>
</div>
</div>
);
}