Skip to content

Commit ce2f290

Browse files
committed
refactor: 外部化文章数据获取和类型,并提高BlogArticles carousel响应性
1 parent 68d56c1 commit ce2f290

File tree

4 files changed

+165
-265
lines changed

4 files changed

+165
-265
lines changed

app/components/BlogArticles.tsx

Lines changed: 15 additions & 234 deletions
Original file line numberDiff line numberDiff line change
@@ -1,252 +1,27 @@
11
import { Button } from 'antd'
22
import { LeftOutlined, RightOutlined, ArrowRightOutlined } from '@ant-design/icons'
33
import { useState, useEffect } from 'react'
4-
import { config } from '../config'
5-
6-
interface Article {
7-
id: number
8-
title: string
9-
category: string
10-
description: string
11-
date: string
12-
link: string
13-
styleName: string
14-
tags: string[]
15-
}
16-
17-
interface RSSItem {
18-
title: string
19-
link: string
20-
description: string
21-
pubDate: string
22-
category: string
23-
tags: string[]
24-
}
4+
import type { Article } from '../types/article'
255

266
interface BlogArticlesProps {
277
title?: string
288
subtitle?: string
9+
articles?: Article[]
2910
}
3011

31-
// RSS 解析函数
32-
const parseRSSFeed = async (rssUrl: string): Promise<RSSItem[]> => {
33-
try {
34-
const response = await fetch(rssUrl)
35-
if (!response.ok) {
36-
throw new Error(`HTTP error! status: ${response.status}`)
37-
}
38-
39-
const xmlText = await response.text()
40-
41-
// 创建 DOMParser 来解析 XML
42-
const parser = new DOMParser()
43-
const xmlDoc = parser.parseFromString(xmlText, 'text/xml')
44-
45-
// 检查解析错误
46-
const parseError = xmlDoc.querySelector('parsererror')
47-
if (parseError) {
48-
throw new Error('XML 解析错误')
49-
}
50-
51-
// 获取所有 item 元素
52-
const items = xmlDoc.querySelectorAll('item')
53-
54-
const rssItems: RSSItem[] = []
55-
56-
items.forEach((item) => {
57-
// 处理 CDATA 包装的内容
58-
const extractCDATA = (content: string | null | undefined): string => {
59-
if (!content) return ''
60-
return content.replace(/^<!\[CDATA\[|\]\]>$/g, '').trim()
61-
}
62-
63-
const titleElement = item.querySelector('title')
64-
const title = extractCDATA(titleElement?.textContent) || ''
65-
66-
const link = item.querySelector('link')?.textContent?.trim() || ''
67-
68-
const descriptionElement = item.querySelector('description')
69-
const description = extractCDATA(descriptionElement?.textContent) || ''
70-
71-
const pubDate = item.querySelector('pubDate')?.textContent?.trim() || ''
72-
const category = item.querySelector('category')?.textContent?.trim() || '技术'
73-
74-
// 处理标签,可能有多个 tag 元素
75-
const tagElements = item.querySelectorAll('tag')
76-
let tags: string[] = []
77-
78-
if (tagElements.length > 0) {
79-
tagElements.forEach((tagEl) => {
80-
const tagContent = tagEl.textContent?.trim()
81-
if (tagContent) {
82-
// 如果标签包含逗号,按逗号分割
83-
const splitTags = tagContent
84-
.split(',')
85-
.map((tag) => tag.trim())
86-
.filter((tag) => tag)
87-
tags.push(...splitTags)
88-
}
89-
})
90-
}
91-
92-
// 去重标签
93-
tags = [...new Set(tags)]
94-
95-
if (title && link) {
96-
rssItems.push({
97-
title,
98-
link,
99-
description,
100-
pubDate,
101-
category,
102-
tags
103-
})
104-
}
105-
})
106-
107-
return rssItems
108-
} catch (error) {
109-
console.error('解析 RSS 失败:', error)
110-
return []
111-
}
112-
}
113-
114-
// 根据标签生成图标和渐变色的 className
115-
const getArticleStyle = (tags: string[]) => {
116-
const mainTag = tags[0].toLowerCase()
117-
118-
return 'weiz-icon-' + mainTag
119-
}
120-
121-
// 格式化日期
122-
const formatDate = (dateString: string): string => {
123-
try {
124-
const date = new Date(dateString)
125-
126-
// 检查日期是否有效
127-
if (isNaN(date.getTime())) {
128-
return dateString
129-
}
130-
131-
const year = date.getFullYear()
132-
const month = String(date.getMonth() + 1).padStart(2, '0')
133-
const day = String(date.getDate()).padStart(2, '0')
134-
135-
return `${year}${month}${day}日`
136-
} catch {
137-
return dateString
138-
}
139-
}
140-
141-
// 转换 RSS 数据为组件需要的格式
142-
const convertRSSToArticles = (rssItems: RSSItem[]): Article[] => {
143-
return rssItems.slice(0, 12).map((item, index) => {
144-
const styleName = getArticleStyle(item.tags)
145-
146-
return {
147-
id: index + 1,
148-
title: item.title,
149-
category: item.category,
150-
description: item.description,
151-
date: formatDate(item.pubDate),
152-
link: item.link,
153-
styleName,
154-
tags: item.tags
155-
}
156-
})
157-
}
158-
159-
export function BlogArticles({ title = '我的文章', subtitle = '来自博客的最新动态,发现更多精彩内容' }: BlogArticlesProps) {
160-
const [articles, setArticles] = useState<Article[]>([])
161-
const [loading, setLoading] = useState(true)
162-
163-
// 备用文章数据
164-
const fallbackArticles: Article[] = [
165-
{
166-
id: 1,
167-
title: '如何快速无缝的从 vscode 转向AI编辑器 cursor、kiro、trae 等',
168-
category: '资源',
169-
description: '本文介绍了如何从 VSCode 快速无缝转向 AI 编辑器,如 kiro、cursor、trae 等',
170-
date: '2025年07月25日',
171-
link: config.blog.url + '/editor/ai/to-kiro',
172-
styleName: 'weiz-icon-ai',
173-
tags: ['AI', 'VSCode']
174-
},
175-
{
176-
id: 2,
177-
title: 'MacOS Sequoia系统优化',
178-
category: '资源',
179-
description: '本文介绍了 MacOS Sequoia 系统的基础优化设置,包括修改截屏保存位置、修复启动图标错乱、关闭安装来源限制等系统级操作',
180-
date: '2025年04月26日',
181-
link: config.blog.url + '/macos/setting/base-init',
182-
styleName: 'weiz-icon-macos',
183-
tags: ['MacOS']
184-
},
185-
{
186-
id: 3,
187-
title: 'VitePress 建站资源汇总',
188-
category: '资源',
189-
description:
190-
'本文汇总了使用 VitePress 搭建博客的资源与配置方法,包括暗黑模式切换动画、DocSearch 搜索、Fancybox 图片查看器、GitHub Giscus 评论系统、Cloudflare R2 图床配置等内容',
191-
date: '2025年04月18日',
192-
link: config.blog.url + '/vitepress/all/resource-all',
193-
styleName: 'weiz-icon-vitepress',
194-
tags: ['VitePress', '网站']
195-
}
196-
]
197-
198-
// 获取 RSS 数据
199-
useEffect(() => {
200-
const fetchArticles = async () => {
201-
try {
202-
setLoading(true)
203-
204-
// 使用 CORS 代理获取 RSS 数据
205-
const rssItems = await parseRSSFeed(config.api.rss)
206-
207-
if (rssItems.length > 0) {
208-
const convertedArticles = convertRSSToArticles(rssItems)
209-
setArticles(convertedArticles)
210-
} else {
211-
// 如果 RSS 没有数据,使用备用数据
212-
setArticles(fallbackArticles)
213-
}
214-
} catch (error) {
215-
console.error('获取文章失败:', error)
216-
// 如果获取失败,使用备用数据
217-
setArticles(fallbackArticles)
218-
} finally {
219-
setLoading(false)
220-
}
221-
}
12+
export function BlogArticles({ title = '我的文章', subtitle = '来自博客的最新动态,发现更多精彩内容', articles = [] }: BlogArticlesProps) {
22213

223-
fetchArticles()
224-
}, [])
22514

22615
const [currentSlide, setCurrentSlide] = useState(0)
22716
const [touchStart, setTouchStart] = useState(0)
22817
const [touchEnd, setTouchEnd] = useState(0)
229-
const [isMobile, setIsMobile] = useState(false)
23018
const [isPaused, setIsPaused] = useState(false)
23119

232-
// 检测是否为移动设备
233-
useEffect(() => {
234-
const checkIsMobile = () => {
235-
setIsMobile(window.innerWidth < 768)
236-
}
237-
238-
checkIsMobile()
239-
window.addEventListener('resize', checkIsMobile)
240-
241-
return () => {
242-
window.removeEventListener('resize', checkIsMobile)
243-
}
244-
}, [])
245-
24620
// 计算PC端的最大滑动位置
24721
const maxSlidePC = Math.max(0, articles.length - 3)
24822

24923
const nextSlide = () => {
24+
const isMobile = window.innerWidth < 768;
25025
if (isMobile) {
25126
// 移动端:一次滚动一个模块
25227
setCurrentSlide((prev) => (prev === articles.length - 1 ? 0 : prev + 1))
@@ -262,6 +37,7 @@ export function BlogArticles({ title = '我的文章', subtitle = '来自博客
26237
}
26338

26439
const prevSlide = () => {
40+
const isMobile = window.innerWidth < 768;
26541
if (isMobile) {
26642
// 移动端:一次滚动一个模块
26743
setCurrentSlide((prev) => (prev === 0 ? articles.length - 1 : prev - 1))
@@ -311,7 +87,7 @@ export function BlogArticles({ title = '我的文章', subtitle = '来自博客
31187
}, [currentSlide, articles.length, isPaused])
31288

31389
// 如果没有文章数据,不渲染组件
314-
if (!loading && articles.length === 0) {
90+
if (articles.length === 0) {
31591
return null
31692
}
31793

@@ -321,22 +97,27 @@ export function BlogArticles({ title = '我的文章', subtitle = '来自博客
32197
<h2 className='text-2xl sm:text-3xl md:text-4xl font-bold text-center mb-3 md:mb-4 px-4'>{title}</h2>
32298
<p className='text-gray-500 text-center mb-8 md:mb-12 text-sm md:text-base px-4'>{subtitle}</p>
32399

324-
{loading ? (
100+
{false ? (
325101
<div className='flex justify-center items-center py-20'>
326102
<div className='animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500'></div>
327103
</div>
328104
) : (
329105
<div className='relative'>
330106
<div className='overflow-hidden pb-10' onTouchStart={handleTouchStart} onTouchMove={handleTouchMove} onTouchEnd={handleTouchEnd}>
331107
<div
332-
className='flex transition-transform duration-500 ease-in-out'
108+
className='flex transition-transform duration-500 ease-in-out md:[--slide-percentage:33.33333%]'
333109
style={{
334-
transform: isMobile ? `translateX(-${currentSlide * 100}%)` : `translateX(-${currentSlide * 33.33}%)`
110+
// 使用 CSS 变量处理移动端/桌面端差异
111+
// 移动端: 100% (1 item)
112+
// 桌面端: 33.33% (3 items)
113+
// @ts-ignore
114+
'--slide-percentage': '100%',
115+
transform: `translateX(calc(-${currentSlide} * var(--slide-percentage)))`
335116
}}>
336117
{articles.map((article) => (
337118
<div
338119
key={article.id}
339-
className={`${isMobile ? 'w-full' : 'w-1/3'} flex-shrink-0 md:px-3`}
120+
className={`w-full md:w-1/3 flex-shrink-0 md:px-3`}
340121
onMouseEnter={() => setIsPaused(true)}
341122
onMouseLeave={() => setIsPaused(false)}>
342123
<div className='bg-white/80 backdrop-blur-sm rounded-2xl md:rounded-3xl border-1 border-slate-200 shadow-md shadow-slate-200 h-full hover:shadow-xl transition-all duration-300 overflow-hidden'>

app/components/PersonalHomepage.tsx

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@ import { NoteShowcase } from "./NoteShowcase";
1313
import { About } from "./About";
1414
import { ProjectShowcase } from "./ProjectShowcase";
1515
import { Footer } from "./Footer";
16+
import type { Article } from "../types/article";
1617

17-
export function PersonalHomepage() {
18+
interface PersonalHomepageProps {
19+
articles?: Article[];
20+
}
21+
22+
export function PersonalHomepage({ articles = [] }: PersonalHomepageProps) {
1823
// 导航项数据
1924
const navItems = [
2025
{ title: "推荐", id: "recommend" },
@@ -32,21 +37,8 @@ export function PersonalHomepage() {
3237

3338
// 移动端菜单状态
3439
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
35-
const [isMobile, setIsMobile] = useState(false);
36-
37-
// 检测是否为移动设备
38-
useEffect(() => {
39-
const checkIsMobile = () => {
40-
setIsMobile(window.innerWidth < 768);
41-
};
42-
43-
checkIsMobile();
44-
window.addEventListener("resize", checkIsMobile);
4540

46-
return () => {
47-
window.removeEventListener("resize", checkIsMobile);
48-
};
49-
}, []);
41+
// 处理鼠标进入导航项
5042

5143
// 处理鼠标进入导航项
5244
const handleMouseEnter = (index: number) => {
@@ -96,18 +88,15 @@ export function PersonalHomepage() {
9688
?.scrollIntoView({ behavior: "smooth" })
9789
}
9890
/>
99-
{isMobile && (
100-
<span className="ml-2 text-lg font-semibold">weizwz</span>
101-
)}
91+
<span className="ml-2 text-lg font-semibold md:hidden">weizwz</span>
10292
</div>
10393

10494
{/* 桌面导航 */}
105-
{!isMobile && (
106-
<div
107-
className="flex items-center relative"
108-
ref={navRef}
109-
onMouseLeave={handleMouseLeave}
110-
>
95+
<div
96+
className="hidden md:flex items-center relative"
97+
ref={navRef}
98+
onMouseLeave={handleMouseLeave}
99+
>
111100
{/* 滑动背景指示器 */}
112101
<div
113102
className="absolute bg-white/80 border border-blue-500 rounded-full transition-all duration-500"
@@ -137,18 +126,16 @@ export function PersonalHomepage() {
137126
</a>
138127
))}
139128
</div>
140-
)}
141-
142129
<div className="flex items-center gap-4">
143130
{/* 移动端汉堡菜单按钮 */}
144-
{isMobile && (
131+
<div className="md:hidden flex items-center">
145132
<Button
146133
type="text"
147134
icon={<MenuOutlined />}
148135
onClick={() => setMobileMenuOpen(true)}
149136
className="flex items-center justify-center"
150137
/>
151-
)}
138+
</div>
152139

153140
{/* 博客按钮 */}
154141
<Button
@@ -294,7 +281,7 @@ export function PersonalHomepage() {
294281
<ProjectShowcase />
295282

296283
{/* Blog Articles Section */}
297-
<BlogArticles />
284+
<BlogArticles articles={articles} />
298285

299286
{/* Note Showcase Section */}
300287
<NoteShowcase />

0 commit comments

Comments
 (0)