news.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. "use client";
  2. import React, {useState, useEffect, useMemo} from 'react';
  3. import './NewsPage.css';
  4. import Link from "next/link";
  5. // 1. 抽离类型定义,提升可维护性
  6. interface Article {
  7. id: string;
  8. newsUrl: string;
  9. newsName: string;
  10. newsDesc: string;
  11. newsAuthor?: string; // 可选字段
  12. content: string;
  13. newsCategory: string; // 补充分类字段(原逻辑依赖)
  14. }
  15. // 2. 定义Props类型,避免隐式any
  16. interface NewsItemProps {
  17. news: Article,
  18. key?: string
  19. }
  20. interface NewsListProps {
  21. newsList: Article[];
  22. }
  23. interface NewsPageProps {
  24. newsList: Article[];
  25. }
  26. // 3. 抽离常量,便于统一维护
  27. const CATEGORY_MAP = Object.freeze({
  28. company: '公司新闻',
  29. industry: '行内新闻'
  30. } as const); // 常量断言,锁定类型
  31. // 4. 新闻列表项组件(纯组件,优化性能)
  32. const NewsItem: React.FC<NewsItemProps> = ({news: article, key}) => {
  33. // 环境变量默认值兜底,避免undefined
  34. const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || '';
  35. // 图片地址处理抽离,提升可读性
  36. const coverImageUrl = article.newsUrl ? `${BASE_URL}${article.newsUrl}` : '';
  37. return (
  38. <div className="news-item">
  39. <div className="news-cover">
  40. <img
  41. src={coverImageUrl}
  42. alt={article.newsName} // 补充有意义的alt属性,优化可访问性
  43. loading="lazy"
  44. onError={(e) => {
  45. // 图片加载失败兜底,提升用户体验
  46. (e.target as HTMLImageElement).src = '/default-news-cover.png';
  47. }}
  48. />
  49. </div>
  50. <div className="news-content-wrapper">
  51. <h3 className="news-title">{article.newsName}</h3>
  52. <div className="news-meta">
  53. <span className="news-date">{article.newsDesc}</span>
  54. {/* 可选字段兜底,避免空值展示 */}
  55. <span className="news-author">作者:{article.newsAuthor || '未知'}</span>
  56. </div>
  57. <p className="news-content">{article.content}</p>
  58. </div>
  59. <Link
  60. href={`/news/${article.id}`}
  61. className="text-blue-500 hover:underline mt-4 self-start text-sm sm:text-base"
  62. aria-label={`查看${article.newsName}详情`} // 优化可访问性
  63. >
  64. 了解更多 &gt;
  65. </Link>
  66. </div>
  67. );
  68. };
  69. // 5. 新闻列表组件(添加空状态优化)
  70. const NewsList: React.FC<NewsListProps> = ({newsList}) => {
  71. // 空数组判断优化,避免length判断出错
  72. if (!Array.isArray(newsList) || newsList.length === 0) {
  73. return <div className="no-news">暂无相关新闻</div>;
  74. }
  75. return (
  76. <div className="news-list">
  77. {newsList.map((news) => (
  78. <NewsItem
  79. key={news.id}
  80. news={news}
  81. />
  82. ))}
  83. </div>
  84. );
  85. };
  86. // 6. 主新闻页面组件
  87. const NewsPage: React.FC<NewsPageProps> = ({newsList = []}) => {
  88. // 类型守卫,确保newsList是数组类型
  89. const safeNewsList = useMemo(() =>
  90. Array.isArray(newsList) ? newsList : [],
  91. [newsList]);
  92. const [activeTab, setActiveTab] = useState<keyof typeof CATEGORY_MAP>('company'); // 严格类型约束
  93. const [filteredNews, setFilteredNews] = useState<Article[]>([]);
  94. const [isLoading, setIsLoading] = useState(true);
  95. // 7. 优化筛选逻辑:使用useMemo减少重复计算
  96. useEffect(() => {
  97. setIsLoading(true);
  98. const timer = setTimeout(() => {
  99. const targetCategory = CATEGORY_MAP[activeTab];
  100. const filtered = safeNewsList.filter(
  101. (news) => news.newsCategory === targetCategory
  102. );
  103. setFilteredNews(filtered);
  104. setIsLoading(false);
  105. }, 300);
  106. return () => clearTimeout(timer);
  107. }, [safeNewsList, activeTab]);
  108. // 8. 抽离标签切换逻辑,简化代码
  109. const handleTabChange = (tab: keyof typeof CATEGORY_MAP) => {
  110. setIsLoading(true);
  111. setActiveTab(tab);
  112. };
  113. return (
  114. <div className="news-page">
  115. <div className="news-tabs">
  116. <button
  117. className={`tab-btn ${activeTab === 'company' ? 'active' : ''}`}
  118. onClick={() => handleTabChange('company')}
  119. aria-pressed={activeTab === 'company'} // 优化可访问性
  120. >
  121. 公司新闻
  122. </button>
  123. <button
  124. className={`tab-btn ${activeTab === 'industry' ? 'active' : ''}`}
  125. onClick={() => handleTabChange('industry')}
  126. aria-pressed={activeTab === 'industry'}
  127. >
  128. 行内新闻
  129. </button>
  130. </div>
  131. {isLoading && <div className="loading">加载中...</div>}
  132. {!isLoading && <NewsList newsList={filteredNews}/>}
  133. </div>
  134. );
  135. };
  136. export default NewsPage;