import { useEffect, useState, useRef } from 'react' import { Card, List, Spin, Empty, Typography, Button, Avatar, Drawer } from 'antd' import { MessageOutlined, LinkOutlined, UpOutlined, DownOutlined, ReloadOutlined, UnorderedListOutlined } from '@ant-design/icons' import { useTranslation } from 'react-i18next' import { apiService } from '../services/api' import { useMediaQuery } from 'react-responsive' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' const { Title, Text } = Typography interface Reactions { plusOne?: number minusOne?: number laugh?: number confused?: number heart?: number hooray?: number eyes?: number rocket?: number total?: number } interface Announcement { id: number title: string body: string author: string authorAvatarUrl?: string createdAt: number updatedAt: number reactions?: Reactions } const Announcements: React.FC = () => { const { t } = useTranslation() const isMobile = useMediaQuery({ maxWidth: 768 }) const [announcements, setAnnouncements] = useState([]) const [selectedAnnouncement, setSelectedAnnouncement] = useState(null) const [loading, setLoading] = useState(false) const [loadingDetail, setLoadingDetail] = useState(false) const [hasMore, setHasMore] = useState(false) const [isExpanded, setIsExpanded] = useState(false) const [drawerVisible, setDrawerVisible] = useState(false) const contentRef = useRef(null) useEffect(() => { fetchAnnouncements() fetchLatestDetail() }, []) const fetchAnnouncements = async (forceRefresh: boolean = false) => { setLoading(true) try { const response = await apiService.announcements.list({ forceRefresh }) if (response.data.code === 0 && response.data.data) { setAnnouncements(response.data.data.list || []) setHasMore(response.data.data.hasMore || false) } else { console.error('获取公告列表失败:', response.data.msg) } } catch (error: any) { console.error('获取公告列表异常:', error) } finally { setLoading(false) } } const fetchLatestDetail = async (forceRefresh: boolean = false) => { setLoadingDetail(true) try { const response = await apiService.announcements.detail({ forceRefresh }) if (response.data.code === 0 && response.data.data) { setSelectedAnnouncement(response.data.data) } else { console.error('获取公告详情失败:', response.data.msg) } } catch (error: any) { console.error('获取公告详情异常:', error) } finally { setLoadingDetail(false) } } const handleSelectAnnouncement = async (id: number, forceRefresh: boolean = false) => { setLoadingDetail(true) try { const response = await apiService.announcements.detail({ id, forceRefresh }) if (response.data.code === 0 && response.data.data) { setSelectedAnnouncement(response.data.data) // 移动端选择公告后关闭抽屉 if (isMobile) { setDrawerVisible(false) } } else { console.error('获取公告详情失败:', response.data.msg) } } catch (error: any) { console.error('获取公告详情异常:', error) } finally { setLoadingDetail(false) } } const handleRefresh = async () => { await Promise.all([ fetchAnnouncements(true), fetchLatestDetail(true) ]) } const formatDate = (timestamp: number): string => { const date = new Date(timestamp) return date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) } // 计算内容行数(通过换行符计算) const getLineCount = (text: string): number => { if (!text) return 0 return text.split('\n').length } // 检查是否需要折叠(超过30行) const shouldCollapse = (body: string): boolean => { return getLineCount(body) > 30 } // 当选中公告改变时,重置展开状态 useEffect(() => { if (selectedAnnouncement) { const shouldCollapseContent = shouldCollapse(selectedAnnouncement.body) setIsExpanded(!shouldCollapseContent) // 如果超过30行,默认折叠(isExpanded = false) } }, [selectedAnnouncement]) // 渲染公告详情内容(带折叠功能) const renderAnnouncementContent = (announcement: Announcement, isMobileView: boolean) => { const lineCount = getLineCount(announcement.body) const needsCollapse = shouldCollapse(announcement.body) const showCollapseButton = needsCollapse return (
} size={isMobileView ? 'default' : 'large'} />
{announcement.author}
{formatDate(announcement.createdAt)}
{announcement.body} {needsCollapse && !isExpanded && (
)}
{showCollapseButton && (
)}
) } // 渲染 reactions(使用 emoji) const renderReactions = (reactions?: Reactions) => { if (!reactions || reactions.total === 0) { return null } const reactionItems: Array<{ emoji: string; count: number; key: string }> = [] if (reactions.plusOne && reactions.plusOne > 0) { reactionItems.push({ emoji: '👍', count: reactions.plusOne, key: 'plusOne' }) } if (reactions.minusOne && reactions.minusOne > 0) { reactionItems.push({ emoji: '👎', count: reactions.minusOne, key: 'minusOne' }) } if (reactions.laugh && reactions.laugh > 0) { reactionItems.push({ emoji: '😄', count: reactions.laugh, key: 'laugh' }) } if (reactions.confused && reactions.confused > 0) { reactionItems.push({ emoji: '😕', count: reactions.confused, key: 'confused' }) } if (reactions.heart && reactions.heart > 0) { reactionItems.push({ emoji: '❤️', count: reactions.heart, key: 'heart' }) } if (reactions.hooray && reactions.hooray > 0) { reactionItems.push({ emoji: '🎉', count: reactions.hooray, key: 'hooray' }) } if (reactions.eyes && reactions.eyes > 0) { reactionItems.push({ emoji: '👀', count: reactions.eyes, key: 'eyes' }) } if (reactions.rocket && reactions.rocket > 0) { reactionItems.push({ emoji: '🚀', count: reactions.rocket, key: 'rocket' }) } if (reactionItems.length === 0) { return null } return (
{reactionItems.map((item) => ( {item.emoji} {item.count} ))}
) } // 渲染公告列表(用于抽屉) const renderAnnouncementList = () => { return (
{loading ? (
) : announcements.length === 0 ? ( ) : (
{announcements.map((item) => { const isSelected = selectedAnnouncement?.id === item.id return ( handleSelectAnnouncement(item.id)} style={{ cursor: 'pointer', borderRadius: '12px', boxShadow: isSelected ? '0 4px 12px rgba(24, 144, 255, 0.2)' : '0 2px 8px rgba(0,0,0,0.08)', border: isSelected ? '2px solid #1890ff' : '1px solid #e8e8e8', backgroundColor: isSelected ? '#f0f8ff' : '#ffffff', transition: 'all 0.3s ease', transform: isSelected ? 'scale(1.02)' : 'scale(1)' }} bodyStyle={{ padding: '16px' }} hoverable >
{/* 标题 */}
{item.title || t('announcements.noTitle') || '无标题'}
{/* 时间和作者 */}
} size="small" style={{ flexShrink: 0 }} /> {item.author} {formatDate(item.createdAt)}
{/* Reactions */} {renderReactions(item.reactions)}
) })}
)} {hasMore && (
)}
) } if (isMobile) { // 移动端布局:详情在主要内容区,列表在侧边抽屉 return (
{t('announcements.title') || '公告'}
{/* 公告详情 */}
{loadingDetail ? (
) : selectedAnnouncement ? ( renderAnnouncementContent(selectedAnnouncement, true) ) : ( )}
{/* 侧边抽屉:公告列表 */} setDrawerVisible(false)} open={drawerVisible} width="85%" bodyStyle={{ padding: '16px' }} > {renderAnnouncementList()}
) } // 桌面端布局:左右结构 return (
{t('announcements.title') || '公告'}
{/* 左侧:公告列表 */}
}} renderItem={(item) => ( handleSelectAnnouncement(item.id)} > {item.title || t('announcements.noTitle') || '无标题'} } description={
{formatDate(item.createdAt)} {renderReactions(item.reactions)}
} />
)} /> {hasMore && (
)}
{/* 右侧:公告详情 */}
{loadingDetail ? (
) : selectedAnnouncement ? ( renderAnnouncementContent(selectedAnnouncement, false) ) : ( )}
) } export default Announcements