Skip to content

Commit a87308f

Browse files
committed
新增对xml2js库的支持,更新package.json和package-lock.json以包含相关依赖。同时,在主页中引入RssFeed组件,展示Linux DO的最新话题,并更新国际化文本以支持RSS功能的多语言显示,提升用户体验。
1 parent 0d7f187 commit a87308f

6 files changed

Lines changed: 258 additions & 4 deletions

File tree

package-lock.json

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

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"@tailwindcss/typography": "^0.5.16",
1414
"@types/crypto-js": "^4.2.2",
1515
"@types/js-yaml": "^4.0.9",
16+
"@types/xml2js": "^0.4.14",
1617
"crypto-js": "^4.2.0",
1718
"diff": "^8.0.1",
1819
"framer-motion": "^12.11.3",
@@ -31,7 +32,8 @@
3132
"react-markdown": "^10.1.0",
3233
"react-syntax-highlighter": "^15.6.1",
3334
"react-tooltip": "^5.28.1",
34-
"reactflow": "^11.11.4"
35+
"reactflow": "^11.11.4",
36+
"xml2js": "^0.6.2"
3537
},
3638
"devDependencies": {
3739
"@types/node": "^20",

src/app/page.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import SettingsButton from "../components/SettingsModal";
2020
import BrowserCompatCheck from "../components/BrowserCompatCheck";
2121
import KnowledgeModal from "../components/KnowledgeModal";
2222
import MultiThreadScanAlert from "../components/MultiThreadScanAlert";
23+
import RssFeed from "../components/RssFeed";
2324

2425
export default function Home() {
2526
const [directoryHandle] = useAtom(directoryHandleAtom);
@@ -508,6 +509,9 @@ export default function Home() {
508509
</button>
509510
</div>
510511
</div>
512+
513+
{/* Linux.do RSS Feed */}
514+
<RssFeed />
511515
</div>
512516
</motion.div>
513517

src/components/RssFeed.tsx

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
"use client";
2+
3+
import { useState, useEffect } from "react";
4+
import { motion } from "framer-motion";
5+
import { useTranslations } from "./LocaleProvider";
6+
import { parseStringPromise } from "xml2js";
7+
8+
interface RssItem {
9+
title: string;
10+
link: string;
11+
description: string;
12+
category?: string;
13+
pubDate: string;
14+
creator?: string;
15+
}
16+
17+
export default function RssFeed() {
18+
const { t } = useTranslations();
19+
const [items, setItems] = useState<RssItem[]>([]);
20+
const [loading, setLoading] = useState(true);
21+
const [error, setError] = useState<string | null>(null);
22+
23+
useEffect(() => {
24+
const fetchRss = async () => {
25+
try {
26+
setLoading(true);
27+
const response = await fetch("https://linux.do/latest.rss", {
28+
cache: "no-store",
29+
});
30+
31+
if (!response.ok) {
32+
throw new Error(`获取RSS失败: ${response.status}`);
33+
}
34+
35+
const xmlText = await response.text();
36+
const result = await parseStringPromise(xmlText, {
37+
explicitArray: false,
38+
});
39+
40+
if (result?.rss?.channel?.item) {
41+
const rssItems = Array.isArray(result.rss.channel.item)
42+
? result.rss.channel.item
43+
: [result.rss.channel.item];
44+
45+
const parsedItems = rssItems
46+
.map((item: any) => ({
47+
title: item.title,
48+
link: item.link,
49+
description: item.description,
50+
category: item.category,
51+
pubDate: item.pubDate,
52+
creator: item["dc:creator"],
53+
}))
54+
.slice(0, 10); // 只显示前10条
55+
56+
setItems(parsedItems);
57+
}
58+
} catch (err) {
59+
console.error("获取RSS feed失败:", err);
60+
setError(err instanceof Error ? err.message : "获取RSS feed失败");
61+
} finally {
62+
setLoading(false);
63+
}
64+
};
65+
66+
fetchRss();
67+
}, []);
68+
69+
// 格式化发布日期
70+
const formatDate = (dateString: string) => {
71+
try {
72+
const date = new Date(dateString);
73+
return new Intl.DateTimeFormat("zh-CN", {
74+
year: "numeric",
75+
month: "short",
76+
day: "numeric",
77+
}).format(date);
78+
} catch (e) {
79+
return dateString;
80+
}
81+
};
82+
83+
// 处理CDATA内容
84+
const extractCdata = (text: string | undefined): string => {
85+
if (!text) return "";
86+
// 处理CDATA标签
87+
const cdataMatch = text.match(/<!\[CDATA\[(.*?)\]\]>/);
88+
if (cdataMatch && cdataMatch[1]) {
89+
return cdataMatch[1].trim();
90+
}
91+
return text.trim();
92+
};
93+
94+
// 提取描述中的第一段文本
95+
const extractFirstParagraph = (html: string) => {
96+
// 首先处理CDATA
97+
const content = extractCdata(html);
98+
// 然后移除HTML标签
99+
const text = content.replace(/<[^>]*>/g, " ").trim();
100+
return (
101+
text.split(/\s+/).slice(0, 15).join(" ") +
102+
(text.split(/\s+/).length > 15 ? "..." : "")
103+
);
104+
};
105+
106+
if (loading) {
107+
return (
108+
<div className="w-full mt-6">
109+
<div className="text-center py-8">
110+
<div className="inline-block animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
111+
<p className="mt-2 text-gray-600 dark:text-gray-400">
112+
{t("rssFeed.loading")}
113+
</p>
114+
</div>
115+
</div>
116+
);
117+
}
118+
119+
if (error) {
120+
return (
121+
<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">
122+
<p className="text-red-600 dark:text-red-400">{error}</p>
123+
</div>
124+
);
125+
}
126+
127+
return (
128+
<div className="w-full mt-6">
129+
<h2 className="text-xl font-semibold mb-4 text-gray-800 dark:text-gray-200 flex items-center">
130+
<svg
131+
xmlns="http://www.w3.org/2000/svg"
132+
className="h-5 w-5 mr-2 text-blue-500"
133+
fill="none"
134+
viewBox="0 0 24 24"
135+
stroke="currentColor"
136+
>
137+
<path
138+
strokeLinecap="round"
139+
strokeLinejoin="round"
140+
strokeWidth={2}
141+
d="M6 5c7.18 0 13 5.82 13 13M6 11a7 7 0 017 7m-6 0a1 1 0 11-2 0 1 1 0 012 0z"
142+
/>
143+
</svg>
144+
{t("rssFeed.title")}
145+
</h2>
146+
147+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
148+
{items.map((item, index) => (
149+
<motion.div
150+
key={index}
151+
initial={{ opacity: 0, y: 20 }}
152+
animate={{ opacity: 1, y: 0 }}
153+
transition={{ delay: index * 0.1 }}
154+
className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden hover:shadow-md transition-shadow duration-200"
155+
>
156+
<div className="p-4">
157+
<div className="flex justify-between items-start mb-2">
158+
<span className="inline-block px-2 py-1 text-xs font-medium bg-blue-100 dark:bg-blue-900/40 text-blue-800 dark:text-blue-300 rounded">
159+
{item.category || t("rssFeed.uncategorized")}
160+
</span>
161+
<span className="text-xs text-gray-500 dark:text-gray-400">
162+
{formatDate(item.pubDate)}
163+
</span>
164+
</div>
165+
166+
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2 line-clamp-2">
167+
{extractCdata(item.title)}
168+
</h3>
169+
170+
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3 line-clamp-2">
171+
{extractFirstParagraph(item.description)}
172+
</p>
173+
174+
<div className="flex justify-between items-center">
175+
<span className="text-xs text-gray-500 dark:text-gray-400">
176+
{item.creator
177+
? `${t("rssFeed.author")}: ${extractCdata(item.creator)}`
178+
: ""}
179+
</span>
180+
<a
181+
href={item.link}
182+
target="_blank"
183+
rel="noopener noreferrer"
184+
className="text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
185+
>
186+
{t("rssFeed.readMore")}
187+
</a>
188+
</div>
189+
</div>
190+
</motion.div>
191+
))}
192+
</div>
193+
</div>
194+
);
195+
}

src/messages/en.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,5 +393,13 @@
393393
"zipImportSuccess": "Successfully imported {total} Markdown files from ZIP",
394394
"searchGenerate": "Generate",
395395
"generateFailed": "Generation failed: {message}"
396+
},
397+
"rssFeed": {
398+
"title": "Linux DO - Latest Topics",
399+
"loading": "Loading...",
400+
"readMore": "Read More",
401+
"author": "Author",
402+
"uncategorized": "Uncategorized",
403+
"error": "Failed to fetch RSS feed"
396404
}
397405
}

src/messages/zh.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,5 +396,13 @@
396396
"zipImportSuccess": "成功从ZIP文件导入 {total} 个Markdown文件",
397397
"searchGenerate": "",
398398
"generateFailed": "生成失败: {message}"
399+
},
400+
"rssFeed": {
401+
"title": "Linux DO - 最新话题",
402+
"loading": "加载中...",
403+
"readMore": "阅读全文",
404+
"author": "作者",
405+
"uncategorized": "未分类",
406+
"error": "获取RSS feed失败"
399407
}
400408
}

0 commit comments

Comments
 (0)