| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import React, { useEffect, useState, useContext, useMemo } from 'react'; |
| import { |
| Button, |
| Modal, |
| Empty, |
| Tabs, |
| TabPane, |
| Timeline, |
| } from '@douyinfe/semi-ui'; |
| import { useTranslation } from 'react-i18next'; |
| import { API, showError, getRelativeTime } from '../../helpers'; |
| import { marked } from 'marked'; |
| import { |
| IllustrationNoContent, |
| IllustrationNoContentDark, |
| } from '@douyinfe/semi-illustrations'; |
| import { StatusContext } from '../../context/Status'; |
| import { Bell, Megaphone } from 'lucide-react'; |
|
|
| const NoticeModal = ({ |
| visible, |
| onClose, |
| isMobile, |
| defaultTab = 'inApp', |
| unreadKeys = [], |
| }) => { |
| const { t } = useTranslation(); |
| const [noticeContent, setNoticeContent] = useState(''); |
| const [loading, setLoading] = useState(false); |
| const [activeTab, setActiveTab] = useState(defaultTab); |
|
|
| const [statusState] = useContext(StatusContext); |
|
|
| const announcements = statusState?.status?.announcements || []; |
|
|
| const unreadSet = useMemo(() => new Set(unreadKeys), [unreadKeys]); |
|
|
| const getKeyForItem = (item) => |
| `${item?.publishDate || ''}-${(item?.content || '').slice(0, 30)}`; |
|
|
| const processedAnnouncements = useMemo(() => { |
| return (announcements || []).slice(0, 20).map((item) => { |
| const pubDate = item?.publishDate ? new Date(item.publishDate) : null; |
| const absoluteTime = |
| pubDate && !isNaN(pubDate.getTime()) |
| ? `${pubDate.getFullYear()}-${String(pubDate.getMonth() + 1).padStart(2, '0')}-${String(pubDate.getDate()).padStart(2, '0')} ${String(pubDate.getHours()).padStart(2, '0')}:${String(pubDate.getMinutes()).padStart(2, '0')}` |
| : item?.publishDate || ''; |
| return { |
| key: getKeyForItem(item), |
| type: item.type || 'default', |
| time: absoluteTime, |
| content: item.content, |
| extra: item.extra, |
| relative: getRelativeTime(item.publishDate), |
| isUnread: unreadSet.has(getKeyForItem(item)), |
| }; |
| }); |
| }, [announcements, unreadSet]); |
|
|
| const handleCloseTodayNotice = () => { |
| const today = new Date().toDateString(); |
| localStorage.setItem('notice_close_date', today); |
| onClose(); |
| }; |
|
|
| const displayNotice = async () => { |
| setLoading(true); |
| try { |
| const res = await API.get('/api/notice'); |
| const { success, message, data } = res.data; |
| if (success) { |
| if (data !== '') { |
| const htmlNotice = marked.parse(data); |
| setNoticeContent(htmlNotice); |
| } else { |
| setNoticeContent(''); |
| } |
| } else { |
| showError(message); |
| } |
| } catch (error) { |
| showError(error.message); |
| } finally { |
| setLoading(false); |
| } |
| }; |
|
|
| useEffect(() => { |
| if (visible) { |
| displayNotice(); |
| } |
| }, [visible]); |
|
|
| useEffect(() => { |
| if (visible) { |
| setActiveTab(defaultTab); |
| } |
| }, [defaultTab, visible]); |
|
|
| const renderMarkdownNotice = () => { |
| if (loading) { |
| return ( |
| <div className='py-12'> |
| <Empty description={t('ε θ½½δΈ...')} /> |
| </div> |
| ); |
| } |
|
|
| if (!noticeContent) { |
| return ( |
| <div className='py-12'> |
| <Empty |
| image={ |
| <IllustrationNoContent style={{ width: 150, height: 150 }} /> |
| } |
| darkModeImage={ |
| <IllustrationNoContentDark style={{ width: 150, height: 150 }} /> |
| } |
| description={t('ζζ ε
¬ε')} |
| /> |
| </div> |
| ); |
| } |
|
|
| return ( |
| <div |
| dangerouslySetInnerHTML={{ __html: noticeContent }} |
| className='notice-content-scroll max-h-[55vh] overflow-y-auto pr-2' |
| /> |
| ); |
| }; |
|
|
| const renderAnnouncementTimeline = () => { |
| if (processedAnnouncements.length === 0) { |
| return ( |
| <div className='py-12'> |
| <Empty |
| image={ |
| <IllustrationNoContent style={{ width: 150, height: 150 }} /> |
| } |
| darkModeImage={ |
| <IllustrationNoContentDark style={{ width: 150, height: 150 }} /> |
| } |
| description={t('ζζ η³»η»ε
¬ε')} |
| /> |
| </div> |
| ); |
| } |
|
|
| return ( |
| <div className='max-h-[55vh] overflow-y-auto pr-2 card-content-scroll'> |
| <Timeline mode='left'> |
| {processedAnnouncements.map((item, idx) => { |
| const htmlContent = marked.parse(item.content || ''); |
| const htmlExtra = item.extra ? marked.parse(item.extra) : ''; |
| return ( |
| <Timeline.Item |
| key={idx} |
| type={item.type} |
| time={`${item.relative ? item.relative + ' ' : ''}${item.time}`} |
| extra={ |
| item.extra ? ( |
| <div |
| className='text-xs text-gray-500' |
| dangerouslySetInnerHTML={{ __html: htmlExtra }} |
| /> |
| ) : null |
| } |
| className={item.isUnread ? '' : ''} |
| > |
| <div> |
| <div |
| className={item.isUnread ? 'shine-text' : ''} |
| dangerouslySetInnerHTML={{ __html: htmlContent }} |
| /> |
| </div> |
| </Timeline.Item> |
| ); |
| })} |
| </Timeline> |
| </div> |
| ); |
| }; |
|
|
| const renderBody = () => { |
| if (activeTab === 'inApp') { |
| return renderMarkdownNotice(); |
| } |
| return renderAnnouncementTimeline(); |
| }; |
|
|
| return ( |
| <Modal |
| title={ |
| <div className='flex items-center justify-between w-full'> |
| <span>{t('η³»η»ε
¬ε')}</span> |
| <Tabs activeKey={activeTab} onChange={setActiveTab} type='button'> |
| <TabPane |
| tab={ |
| <span className='flex items-center gap-1'> |
| <Bell size={14} /> {t('ιη₯')} |
| </span> |
| } |
| itemKey='inApp' |
| /> |
| <TabPane |
| tab={ |
| <span className='flex items-center gap-1'> |
| <Megaphone size={14} /> {t('η³»η»ε
¬ε')} |
| </span> |
| } |
| itemKey='system' |
| /> |
| </Tabs> |
| </div> |
| } |
| visible={visible} |
| onCancel={onClose} |
| footer={ |
| <div className='flex justify-end'> |
| <Button type='secondary' onClick={handleCloseTodayNotice}> |
| {t('δ»ζ₯ε
³ι')} |
| </Button> |
| <Button type='primary' onClick={onClose}> |
| {t('ε
³ιε
¬ε')} |
| </Button> |
| </div> |
| } |
| size={isMobile ? 'full-width' : 'large'} |
| > |
| {renderBody()} |
| </Modal> |
| ); |
| }; |
|
|
| export default NoticeModal; |
|
|