@@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
44import { motion } from "framer-motion" ;
55import { useTranslations } from "./LocaleProvider" ;
66import { parseStringPromise } from "xml2js" ;
7+ import { IoMdRefresh } from "react-icons/io" ;
78
89interface RssItem {
910 title : string ;
@@ -37,68 +38,121 @@ const SkeletonCard = () => (
3738) ;
3839
3940export default function RssFeed ( ) {
40- const { t } = useTranslations ( ) ;
41+ const { t, locale } = useTranslations ( ) ;
4142 const [ items , setItems ] = useState < RssItem [ ] > ( [ ] ) ;
4243 const [ loading , setLoading ] = useState ( true ) ;
4344 const [ error , setError ] = useState < string | null > ( null ) ;
45+ const [ currentFeedTitle , setCurrentFeedTitle ] = useState ( "" ) ;
46+ const [ refreshing , setRefreshing ] = useState ( false ) ;
4447
45- useEffect ( ( ) => {
46- const fetchRss = async ( ) => {
47- try {
48- setLoading ( true ) ;
49- const rssToJsonUrl = "https://api.rss2json.com/v1/api.json?rss_url=" ;
50- const techNewsRss = "https://news.mit.edu/rss/feed" ;
51- const response = await fetch (
52- `${ rssToJsonUrl } ${ encodeURIComponent ( techNewsRss ) } ` ,
53- {
54- cache : "no-store" ,
55- }
56- ) ;
57-
58- if ( ! response . ok ) {
59- throw new Error ( `获取RSS失败: ${ response . status } ` ) ;
60- }
48+ // RSS源列表
49+ const chineseRssSources = [
50+ { url : "https://www.oschina.net/news/rss" , title : "开源中国" } ,
51+ {
52+ url : "http://www.ruanyifeng.com/blog/atom.xml" ,
53+ title : "阮一峰的网络日志" ,
54+ } ,
55+ { url : "https://coolshell.cn/feed" , title : "酷壳" } ,
56+ {
57+ url : "https://www.zhangxinxu.com/wordpress/feed/" ,
58+ title : "张鑫旭的博客" ,
59+ } ,
60+ { url : "https://tech.meituan.com/feed/" , title : "美团技术团队" } ,
61+ ] ;
62+
63+ const englishRssSources = [
64+ { url : "https://news.ycombinator.com/rss" , title : "Hacker News" } ,
65+ {
66+ url : "http://feeds.arstechnica.com/arstechnica/index/" ,
67+ title : "Ars Technica" ,
68+ } ,
69+ { url : "https://techcrunch.com/feed/" , title : "TechCrunch" } ,
70+ { url : "https://lobste.rs/rss" , title : "Lobsters" } ,
71+ { url : "https://dev.to/feed" , title : "DEV Community" } ,
72+ { url : "https://stackoverflow.blog/feed/" , title : "Stack Overflow Blog" } ,
73+ ] ;
74+
75+ // 判断是否为中文环境
76+ const isChineseLocale = locale === "zh" ;
77+
78+ // 根据语言选择对应的RSS源列表
79+ const rssSources = isChineseLocale ? chineseRssSources : englishRssSources ;
80+
81+ // 随机选择一个RSS源
82+ const getRandomRssSource = ( ) => {
83+ const randomIndex = Math . floor ( Math . random ( ) * rssSources . length ) ;
84+ return rssSources [ randomIndex ] ;
85+ } ;
6186
62- const data = await response . json ( ) ;
63- // 打印data
64-
65- // RSS2JSON 返回的是已解析好的JSON格式,不需要进一步解析XML
66- if ( data . status === "ok" && data . items && data . items . length > 0 ) {
67- const parsedItems = data . items
68- . map ( ( item : any ) => ( {
69- title : item . title ,
70- link : item . link ,
71- description : item . description || "" ,
72- category :
73- item . categories && item . categories . length > 0
74- ? item . categories [ 0 ]
75- : "Technology" ,
76- pubDate : item . pubDate ,
77- creator : item . author ,
78- thumbnail : item . thumbnail || "" ,
79- } ) )
80- . slice ( 0 , 10 ) ; // 只显示前10条
81-
82- setItems ( parsedItems ) ;
83- } else {
84- throw new Error ( "RSS 源返回数据格式不正确" ) ;
87+ const fetchRss = async ( ) => {
88+ try {
89+ setLoading ( true ) ;
90+ setError ( null ) ;
91+
92+ // 随机选择一个RSS源
93+ const selectedSource = getRandomRssSource ( ) ;
94+ setCurrentFeedTitle ( selectedSource . title ) ;
95+
96+ const rssToJsonUrl = "https://api.rss2json.com/v1/api.json?rss_url=" ;
97+ const response = await fetch (
98+ `${ rssToJsonUrl } ${ encodeURIComponent ( selectedSource . url ) } ` ,
99+ {
100+ cache : "no-store" ,
85101 }
86- } catch ( err ) {
87- console . error ( "获取RSS feed失败:" , err ) ;
88- setError ( err instanceof Error ? err . message : "获取RSS feed失败" ) ;
89- } finally {
90- setLoading ( false ) ;
102+ ) ;
103+
104+ if ( ! response . ok ) {
105+ throw new Error ( `${ t ( "rssFeed.error" ) } : ${ response . status } ` ) ;
91106 }
92- } ;
93107
108+ const data = await response . json ( ) ;
109+
110+ // RSS2JSON 返回的是已解析好的JSON格式,不需要进一步解析XML
111+ if ( data . status === "ok" && data . items && data . items . length > 0 ) {
112+ const parsedItems = data . items
113+ . map ( ( item : any ) => ( {
114+ title : item . title ,
115+ link : item . link ,
116+ description : item . description || "" ,
117+ category :
118+ item . categories && item . categories . length > 0
119+ ? item . categories [ 0 ]
120+ : "Technology" ,
121+ pubDate : item . pubDate ,
122+ creator : item . author ,
123+ thumbnail : item . thumbnail || "" ,
124+ } ) )
125+ . slice ( 0 , 10 ) ; // 只显示前10条
126+
127+ setItems ( parsedItems ) ;
128+ } else {
129+ throw new Error ( "RSS 源返回数据格式不正确" ) ;
130+ }
131+ } catch ( err ) {
132+ console . error ( "获取RSS feed失败:" , err ) ;
133+ setError ( err instanceof Error ? err . message : t ( "rssFeed.error" ) ) ;
134+ } finally {
135+ setLoading ( false ) ;
136+ setRefreshing ( false ) ;
137+ }
138+ } ;
139+
140+ // 点击刷新按钮
141+ const handleRefresh = ( ) => {
142+ if ( refreshing ) return ;
143+ setRefreshing ( true ) ;
144+ fetchRss ( ) ;
145+ } ;
146+
147+ useEffect ( ( ) => {
94148 fetchRss ( ) ;
95- } , [ ] ) ;
149+ } , [ locale ] ) ; // 当语言变化时,重新获取RSS
96150
97151 // 格式化发布日期
98152 const formatDate = ( dateString : string ) => {
99153 try {
100154 const date = new Date ( dateString ) ;
101- return new Intl . DateTimeFormat ( "zh-CN" , {
155+ return new Intl . DateTimeFormat ( isChineseLocale ? "zh-CN" : "en-US ", {
102156 year : "numeric" ,
103157 month : "short" ,
104158 day : "numeric" ,
@@ -134,22 +188,31 @@ export default function RssFeed() {
134188 if ( loading ) {
135189 return (
136190 < div className = "w-full mt-6" >
137- < h2 className = "text-xl font-semibold mb-4 text-gray-800 dark:text-gray-200 flex items-center" >
138- < svg
139- xmlns = "http://www.w3.org/2000/svg"
140- className = "h-5 w-5 mr-2 text-red-600"
141- fill = "none"
142- viewBox = "0 0 24 24"
143- stroke = "currentColor"
191+ < h2 className = "text-xl font-semibold mb-4 text-gray-800 dark:text-gray-200 flex items-center justify-between" >
192+ < div className = "flex items-center" >
193+ < svg
194+ xmlns = "http://www.w3.org/2000/svg"
195+ className = "h-5 w-5 mr-2 text-red-600"
196+ fill = "none"
197+ viewBox = "0 0 24 24"
198+ stroke = "currentColor"
199+ >
200+ < path
201+ strokeLinecap = "round"
202+ strokeLinejoin = "round"
203+ strokeWidth = { 2 }
204+ d = "M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"
205+ />
206+ </ svg >
207+ { t ( "rssFeed.loading" ) }
208+ </ div >
209+ < button
210+ className = "p-2 rounded-full bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
211+ disabled = { true }
212+ aria-label = "刷新"
144213 >
145- < path
146- strokeLinecap = "round"
147- strokeLinejoin = "round"
148- strokeWidth = { 2 }
149- d = "M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"
150- />
151- </ svg >
152- MIT Technology News
214+ < IoMdRefresh className = "h-5 w-5 animate-spin" />
215+ </ button >
153216 </ h2 >
154217 < div className = "grid grid-cols-1 sm:grid-cols-2 gap-6" >
155218 { Array ( 4 )
@@ -164,30 +227,73 @@ export default function RssFeed() {
164227
165228 if ( error ) {
166229 return (
167- < div className = "w-full mt-6 bg-red-50 dark:bg-red-900/20 border border-red-100 dark:border-red-800/30 rounded-lg p-4" >
168- < p className = "text-red-600 dark:text-red-400" > { error } </ p >
230+ < div className = "w-full mt-6" >
231+ < h2 className = "text-xl font-semibold mb-4 text-gray-800 dark:text-gray-200 flex items-center justify-between" >
232+ < div className = "flex items-center" >
233+ < svg
234+ xmlns = "http://www.w3.org/2000/svg"
235+ className = "h-5 w-5 mr-2 text-red-600"
236+ fill = "none"
237+ viewBox = "0 0 24 24"
238+ stroke = "currentColor"
239+ >
240+ < path
241+ strokeLinecap = "round"
242+ strokeLinejoin = "round"
243+ strokeWidth = { 2 }
244+ d = "M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"
245+ />
246+ </ svg >
247+ { t ( "rssFeed.title" ) }
248+ </ div >
249+ < button
250+ onClick = { handleRefresh }
251+ disabled = { refreshing }
252+ className = "p-2 rounded-full bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
253+ aria-label = "刷新"
254+ >
255+ < IoMdRefresh
256+ className = { `h-5 w-5 ${ refreshing ? "animate-spin" : "" } ` }
257+ />
258+ </ button >
259+ </ h2 >
260+ < div className = "bg-red-50 dark:bg-red-900/20 border border-red-100 dark:border-red-800/30 rounded-lg p-4" >
261+ < p className = "text-red-600 dark:text-red-400" > { error } </ p >
262+ </ div >
169263 </ div >
170264 ) ;
171265 }
172266
173267 return (
174268 < div className = "w-full mt-6" >
175- < h2 className = "text-xl font-semibold mb-4 text-gray-800 dark:text-gray-200 flex items-center" >
176- < svg
177- xmlns = "http://www.w3.org/2000/svg"
178- className = "h-5 w-5 mr-2 text-red-600"
179- fill = "none"
180- viewBox = "0 0 24 24"
181- stroke = "currentColor"
269+ < h2 className = "text-xl font-semibold mb-4 text-gray-800 dark:text-gray-200 flex items-center justify-between" >
270+ < div className = "flex items-center" >
271+ < svg
272+ xmlns = "http://www.w3.org/2000/svg"
273+ className = "h-5 w-5 mr-2 text-red-600"
274+ fill = "none"
275+ viewBox = "0 0 24 24"
276+ stroke = "currentColor"
277+ >
278+ < path
279+ strokeLinecap = "round"
280+ strokeLinejoin = "round"
281+ strokeWidth = { 2 }
282+ d = "M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"
283+ />
284+ </ svg >
285+ { currentFeedTitle }
286+ </ div >
287+ < button
288+ onClick = { handleRefresh }
289+ disabled = { refreshing }
290+ className = "p-2 rounded-full bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
291+ aria-label = "刷新"
182292 >
183- < path
184- strokeLinecap = "round"
185- strokeLinejoin = "round"
186- strokeWidth = { 2 }
187- d = "M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"
293+ < IoMdRefresh
294+ className = { `h-5 w-5 ${ refreshing ? "animate-spin" : "" } ` }
188295 />
189- </ svg >
190- MIT Technology News
296+ </ button >
191297 </ h2 >
192298
193299 < div className = "grid grid-cols-1 sm:grid-cols-2 gap-6" >
0 commit comments