11import { Button } from 'antd'
22import { LeftOutlined , RightOutlined , ArrowRightOutlined } from '@ant-design/icons'
33import { 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
266interface 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 ( / ^ < ! \[ C D A T A \[ | \] \] > $ / 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' >
0 commit comments