|
@@ -1,14 +1,36 @@
|
|
|
-import React, { useEffect, useState } from 'react';
|
|
|
|
|
-import { Button, Modal, Empty } from '@douyinfe/semi-ui';
|
|
|
|
|
|
|
+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 { useTranslation } from 'react-i18next';
|
|
|
-import { API, showError } from '../../helpers';
|
|
|
|
|
|
|
+import { API, showError, getRelativeTime } from '../../helpers';
|
|
|
import { marked } from 'marked';
|
|
import { marked } from 'marked';
|
|
|
import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations';
|
|
import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations';
|
|
|
|
|
+import { StatusContext } from '../../context/Status/index.js';
|
|
|
|
|
+import { Bell, Megaphone } from 'lucide-react';
|
|
|
|
|
|
|
|
-const NoticeModal = ({ visible, onClose, isMobile }) => {
|
|
|
|
|
|
|
+const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadKeys = [] }) => {
|
|
|
const { t } = useTranslation();
|
|
const { t } = useTranslation();
|
|
|
const [noticeContent, setNoticeContent] = useState('');
|
|
const [noticeContent, setNoticeContent] = useState('');
|
|
|
const [loading, setLoading] = useState(false);
|
|
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 => ({
|
|
|
|
|
+ key: getKeyForItem(item),
|
|
|
|
|
+ type: item.type || 'default',
|
|
|
|
|
+ time: getRelativeTime(item.publishDate),
|
|
|
|
|
+ content: item.content,
|
|
|
|
|
+ extra: item.extra,
|
|
|
|
|
+ isUnread: unreadSet.has(getKeyForItem(item))
|
|
|
|
|
+ }));
|
|
|
|
|
+ }, [announcements, unreadSet]);
|
|
|
|
|
|
|
|
const handleCloseTodayNotice = () => {
|
|
const handleCloseTodayNotice = () => {
|
|
|
const today = new Date().toDateString();
|
|
const today = new Date().toDateString();
|
|
@@ -44,7 +66,13 @@ const NoticeModal = ({ visible, onClose, isMobile }) => {
|
|
|
}
|
|
}
|
|
|
}, [visible]);
|
|
}, [visible]);
|
|
|
|
|
|
|
|
- const renderContent = () => {
|
|
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ if (visible) {
|
|
|
|
|
+ setActiveTab(defaultTab);
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [defaultTab, visible]);
|
|
|
|
|
+
|
|
|
|
|
+ const renderMarkdownNotice = () => {
|
|
|
if (loading) {
|
|
if (loading) {
|
|
|
return <div className="py-12"><Empty description={t('加载中...')} /></div>;
|
|
return <div className="py-12"><Empty description={t('加载中...')} /></div>;
|
|
|
}
|
|
}
|
|
@@ -64,14 +92,74 @@ const NoticeModal = ({ visible, onClose, isMobile }) => {
|
|
|
return (
|
|
return (
|
|
|
<div
|
|
<div
|
|
|
dangerouslySetInnerHTML={{ __html: noticeContent }}
|
|
dangerouslySetInnerHTML={{ __html: noticeContent }}
|
|
|
- className="notice-content-scroll max-h-[60vh] overflow-y-auto pr-2"
|
|
|
|
|
|
|
+ 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="alternate">
|
|
|
|
|
+ {processedAnnouncements.map((item, idx) => (
|
|
|
|
|
+ <Timeline.Item
|
|
|
|
|
+ key={idx}
|
|
|
|
|
+ type={item.type}
|
|
|
|
|
+ time={item.time}
|
|
|
|
|
+ className={item.isUnread ? '' : ''}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div>
|
|
|
|
|
+ {item.isUnread ? (
|
|
|
|
|
+ <span className="shine-text">
|
|
|
|
|
+ {item.content}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ item.content
|
|
|
|
|
+ )}
|
|
|
|
|
+ {item.extra && <div className="text-xs text-gray-500">{item.extra}</div>}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Timeline.Item>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </Timeline>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const renderBody = () => {
|
|
|
|
|
+ if (activeTab === 'inApp') {
|
|
|
|
|
+ return renderMarkdownNotice();
|
|
|
|
|
+ }
|
|
|
|
|
+ return renderAnnouncementTimeline();
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<Modal
|
|
<Modal
|
|
|
- title={t('系统公告')}
|
|
|
|
|
|
|
+ title={
|
|
|
|
|
+ <div className="flex items-center justify-between w-full">
|
|
|
|
|
+ <span>{t('系统公告')}</span>
|
|
|
|
|
+ <Tabs
|
|
|
|
|
+ activeKey={activeTab}
|
|
|
|
|
+ onChange={setActiveTab}
|
|
|
|
|
+ type='card'
|
|
|
|
|
+ size='small'
|
|
|
|
|
+ >
|
|
|
|
|
+ <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}
|
|
visible={visible}
|
|
|
onCancel={onClose}
|
|
onCancel={onClose}
|
|
|
footer={(
|
|
footer={(
|
|
@@ -82,7 +170,7 @@ const NoticeModal = ({ visible, onClose, isMobile }) => {
|
|
|
)}
|
|
)}
|
|
|
size={isMobile ? 'full-width' : 'large'}
|
|
size={isMobile ? 'full-width' : 'large'}
|
|
|
>
|
|
>
|
|
|
- {renderContent()}
|
|
|
|
|
|
|
+ {renderBody()}
|
|
|
</Modal>
|
|
</Modal>
|
|
|
);
|
|
);
|
|
|
};
|
|
};
|