✨ feat: add notice modal component with empty state support
This commit introduces the following changes: - Create a reusable NoticeModal component to handle system announcements - Extract notice functionality from Home and HeaderBar components - Add loading and empty states using Semi UI illustrations - Implement "close for today" feature with localStorage - Support both light and dark mode for empty state illustrations - Add proper error handling and loading states - Improve code reusability and maintainability Breaking changes: None Related components: HeaderBar.js, Home/index.js, NoticeModal.js
This commit is contained in:
@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { API, getLogo, getSystemName, showSuccess } from '../helpers';
|
import { API, getLogo, getSystemName, showSuccess } from '../helpers';
|
||||||
import fireworks from 'react-fireworks';
|
import fireworks from 'react-fireworks';
|
||||||
import { CN, GB } from 'country-flag-icons/react/3x2';
|
import { CN, GB } from 'country-flag-icons/react/3x2';
|
||||||
|
import NoticeModal from './NoticeModal';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IconClose,
|
IconClose,
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
IconUserSetting,
|
IconUserSetting,
|
||||||
IconCreditCard,
|
IconCreditCard,
|
||||||
IconKey,
|
IconKey,
|
||||||
|
IconBell,
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
@@ -41,6 +43,7 @@ const HeaderBar = () => {
|
|||||||
const [currentLang, setCurrentLang] = useState(i18n.language);
|
const [currentLang, setCurrentLang] = useState(i18n.language);
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const [noticeVisible, setNoticeVisible] = useState(false);
|
||||||
|
|
||||||
const systemName = getSystemName();
|
const systemName = getSystemName();
|
||||||
const logo = getLogo();
|
const logo = getLogo();
|
||||||
@@ -357,6 +360,11 @@ const HeaderBar = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="text-semi-color-text-0 sticky top-0 z-50 transition-colors duration-300 bg-white/75 dark:bg-zinc-900/75 backdrop-blur-lg">
|
<header className="text-semi-color-text-0 sticky top-0 z-50 transition-colors duration-300 bg-white/75 dark:bg-zinc-900/75 backdrop-blur-lg">
|
||||||
|
<NoticeModal
|
||||||
|
visible={noticeVisible}
|
||||||
|
onClose={() => setNoticeVisible(false)}
|
||||||
|
isMobile={styleState.isMobile}
|
||||||
|
/>
|
||||||
<div className="w-full px-4">
|
<div className="w-full px-4">
|
||||||
<div className="flex items-center justify-between h-16">
|
<div className="flex items-center justify-between h-16">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@@ -456,6 +464,15 @@ const HeaderBar = () => {
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
icon={<IconBell className="text-lg" />}
|
||||||
|
aria-label={t('系统公告')}
|
||||||
|
onClick={() => setNoticeVisible(true)}
|
||||||
|
theme="borderless"
|
||||||
|
type="tertiary"
|
||||||
|
className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full bg-semi-color-fill-0 dark:bg-semi-color-fill-1 hover:bg-semi-color-fill-1 dark:hover:bg-semi-color-fill-2"
|
||||||
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
icon={theme === 'dark' ? <IconSun size="large" className="text-yellow-500" /> : <IconMoon size="large" className="text-gray-300" />}
|
icon={theme === 'dark' ? <IconSun size="large" className="text-yellow-500" /> : <IconMoon size="large" className="text-gray-300" />}
|
||||||
aria-label={t('切换主题')}
|
aria-label={t('切换主题')}
|
||||||
|
|||||||
94
web/src/components/NoticeModal.js
Normal file
94
web/src/components/NoticeModal.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Button, Modal, Empty } from '@douyinfe/semi-ui';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { API, showError } from '../helpers';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations';
|
||||||
|
|
||||||
|
const NoticeModal = ({ visible, onClose, isMobile }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [noticeContent, setNoticeContent] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
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="max-h-[60vh] overflow-y-auto pr-2"
|
||||||
|
style={{
|
||||||
|
scrollbarWidth: 'thin',
|
||||||
|
scrollbarColor: 'var(--semi-color-tertiary) transparent'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t('系统公告')}
|
||||||
|
visible={visible}
|
||||||
|
onCancel={onClose}
|
||||||
|
footer={(
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button type='secondary' className='!rounded-full' onClick={handleCloseTodayNotice}>{t('今日关闭')}</Button>
|
||||||
|
<Button type="primary" className='!rounded-full' onClick={onClose}>{t('关闭公告')}</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
size={isMobile ? 'full-width' : 'large'}
|
||||||
|
>
|
||||||
|
{renderContent()}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NoticeModal;
|
||||||
@@ -1529,5 +1529,7 @@
|
|||||||
"系统公告": "System Notice",
|
"系统公告": "System Notice",
|
||||||
"今日关闭": "Close Today",
|
"今日关闭": "Close Today",
|
||||||
"关闭公告": "Close Notice",
|
"关闭公告": "Close Notice",
|
||||||
"搜索条件": "Search Conditions"
|
"搜索条件": "Search Conditions",
|
||||||
|
"加载中...": "Loading...",
|
||||||
|
"暂无公告": "No Notice"
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { Button, Typography, Tag, Modal } from '@douyinfe/semi-ui';
|
import { Button, Typography, Tag } from '@douyinfe/semi-ui';
|
||||||
import { API, showError, isMobile } from '../../helpers';
|
import { API, showError, isMobile } from '../../helpers';
|
||||||
import { StatusContext } from '../../context/Status';
|
import { StatusContext } from '../../context/Status';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
@@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { IconGithubLogo } from '@douyinfe/semi-icons';
|
import { IconGithubLogo } from '@douyinfe/semi-icons';
|
||||||
import exampleImage from '../../images/example.png';
|
import exampleImage from '../../images/example.png';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import NoticeModal from '../../components/NoticeModal';
|
||||||
import { Moonshot, OpenAI, XAI, Zhipu, Volcengine, Cohere, Claude, Gemini, Suno, Minimax, Wenxin, Spark, Qingyan, DeepSeek, Qwen, Midjourney, Grok, AzureAI, Hunyuan, Xinference } from '@lobehub/icons';
|
import { Moonshot, OpenAI, XAI, Zhipu, Volcengine, Cohere, Claude, Gemini, Suno, Minimax, Wenxin, Spark, Qingyan, DeepSeek, Qwen, Midjourney, Grok, AzureAI, Hunyuan, Xinference } from '@lobehub/icons';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
@@ -17,38 +18,16 @@ const Home = () => {
|
|||||||
const [homePageContentLoaded, setHomePageContentLoaded] = useState(false);
|
const [homePageContentLoaded, setHomePageContentLoaded] = useState(false);
|
||||||
const [homePageContent, setHomePageContent] = useState('');
|
const [homePageContent, setHomePageContent] = useState('');
|
||||||
const [noticeVisible, setNoticeVisible] = useState(false);
|
const [noticeVisible, setNoticeVisible] = useState(false);
|
||||||
const [noticeContent, setNoticeContent] = useState('');
|
|
||||||
|
|
||||||
const isDemoSiteMode = statusState?.status?.demo_site_enabled || false;
|
const isDemoSiteMode = statusState?.status?.demo_site_enabled || false;
|
||||||
|
|
||||||
const handleCloseNotice = () => {
|
useEffect(() => {
|
||||||
setNoticeVisible(false);
|
const lastCloseDate = localStorage.getItem('notice_close_date');
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseTodayNotice = () => {
|
|
||||||
const today = new Date().toDateString();
|
const today = new Date().toDateString();
|
||||||
localStorage.setItem('notice_close_date', today);
|
if (lastCloseDate !== today) {
|
||||||
setNoticeVisible(false);
|
setNoticeVisible(true);
|
||||||
};
|
|
||||||
|
|
||||||
const displayNotice = async () => {
|
|
||||||
const res = await API.get('/api/notice');
|
|
||||||
const { success, message, data } = res.data;
|
|
||||||
if (success) {
|
|
||||||
if (data !== '') {
|
|
||||||
const htmlNotice = marked.parse(data);
|
|
||||||
setNoticeContent(htmlNotice);
|
|
||||||
const lastCloseDate = localStorage.getItem('notice_close_date');
|
|
||||||
const today = new Date().toDateString();
|
|
||||||
|
|
||||||
if (lastCloseDate !== today) {
|
|
||||||
setNoticeVisible(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const displayHomePageContent = async () => {
|
const displayHomePageContent = async () => {
|
||||||
setHomePageContent(localStorage.getItem('home_page_content') || '');
|
setHomePageContent(localStorage.getItem('home_page_content') || '');
|
||||||
@@ -81,33 +60,16 @@ const Home = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
displayNotice().then();
|
|
||||||
displayHomePageContent().then();
|
displayHomePageContent().then();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full overflow-x-hidden">
|
<div className="w-full overflow-x-hidden">
|
||||||
<Modal
|
<NoticeModal
|
||||||
title={t('系统公告')}
|
|
||||||
visible={noticeVisible}
|
visible={noticeVisible}
|
||||||
onCancel={handleCloseNotice}
|
onClose={() => setNoticeVisible(false)}
|
||||||
footer={(
|
isMobile={isMobile()}
|
||||||
<div className="flex justify-end">
|
/>
|
||||||
<Button type='secondary' className='!rounded-full' onClick={handleCloseTodayNotice}>{t('今日关闭')}</Button>
|
|
||||||
<Button type="primary" className='!rounded-full' onClick={handleCloseNotice}>{t('关闭公告')}</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
size={isMobile() ? 'full-width' : 'large'}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
dangerouslySetInnerHTML={{ __html: noticeContent }}
|
|
||||||
className="max-h-[60vh] overflow-y-auto pr-2"
|
|
||||||
style={{
|
|
||||||
scrollbarWidth: 'thin',
|
|
||||||
scrollbarColor: 'var(--semi-color-tertiary) transparent'
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
</Modal>
|
|
||||||
{homePageContentLoaded && homePageContent === '' ? (
|
{homePageContentLoaded && homePageContent === '' ? (
|
||||||
<div className="w-full overflow-x-hidden">
|
<div className="w-full overflow-x-hidden">
|
||||||
{/* Banner 部分 */}
|
{/* Banner 部分 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user