Skip to content

Commit 7f79e99

Browse files
committed
更新RssFeed组件,添加随机选择RSS源功能,并引入刷新按钮以提升用户体验。同时,更新国际化文本以反映新的标题和刷新提示,确保多语言支持的准确性和一致性。更新package.json和package-lock.json以包含react-hot-toast和react-icons依赖。
1 parent 55a16c7 commit 7f79e99

5 files changed

Lines changed: 230 additions & 83 deletions

File tree

package-lock.json

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
"react": "^18",
3030
"react-diff-viewer-continued": "^3.4.0",
3131
"react-dom": "^18",
32+
"react-hot-toast": "^2.5.2",
33+
"react-icons": "^5.5.0",
3234
"react-markdown": "^10.1.0",
3335
"react-syntax-highlighter": "^15.6.1",
3436
"react-tooltip": "^5.28.1",

src/components/RssFeed.tsx

Lines changed: 185 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
44
import { motion } from "framer-motion";
55
import { useTranslations } from "./LocaleProvider";
66
import { parseStringPromise } from "xml2js";
7+
import { IoMdRefresh } from "react-icons/io";
78

89
interface RssItem {
910
title: string;
@@ -37,68 +38,121 @@ const SkeletonCard = () => (
3738
);
3839

3940
export 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">

src/messages/en.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,11 +395,12 @@
395395
"generateFailed": "Generation failed: {message}"
396396
},
397397
"rssFeed": {
398-
"title": "MIT Technology News",
398+
"title": "Tech News",
399399
"loading": "Loading...",
400400
"readMore": "Read More",
401401
"author": "Author",
402402
"uncategorized": "Technology",
403-
"error": "Failed to fetch RSS feed"
403+
"error": "Failed to fetch RSS feed",
404+
"refresh": "Refresh news"
404405
}
405406
}

0 commit comments

Comments
 (0)