فهرست منبع

✨ 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
Apple\Apple 9 ماه پیش
والد
کامیت
e85f687c6b
4فایلهای تغییر یافته به همراه125 افزوده شده و 50 حذف شده
  1. 17 0
      web/src/components/HeaderBar.js
  2. 94 0
      web/src/components/NoticeModal.js
  3. 3 1
      web/src/i18n/locales/en.json
  4. 11 49
      web/src/pages/Home/index.js

+ 17 - 0
web/src/components/HeaderBar.js

@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
 import { API, getLogo, getSystemName, showSuccess } from '../helpers';
 import fireworks from 'react-fireworks';
 import { CN, GB } from 'country-flag-icons/react/3x2';
+import NoticeModal from './NoticeModal';
 
 import {
   IconClose,
@@ -18,6 +19,7 @@ import {
   IconUserSetting,
   IconCreditCard,
   IconKey,
+  IconBell,
 } from '@douyinfe/semi-icons';
 import {
   Avatar,
@@ -41,6 +43,7 @@ const HeaderBar = () => {
   const [currentLang, setCurrentLang] = useState(i18n.language);
   const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
   const location = useLocation();
+  const [noticeVisible, setNoticeVisible] = useState(false);
 
   const systemName = getSystemName();
   const logo = getLogo();
@@ -357,6 +360,11 @@ const HeaderBar = () => {
 
   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">
+      <NoticeModal
+        visible={noticeVisible}
+        onClose={() => setNoticeVisible(false)}
+        isMobile={styleState.isMobile}
+      />
       <div className="w-full px-4">
         <div className="flex items-center justify-between h-16">
           <div className="flex items-center">
@@ -456,6 +464,15 @@ const HeaderBar = () => {
               </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
               icon={theme === 'dark' ? <IconSun size="large" className="text-yellow-500" /> : <IconMoon size="large" className="text-gray-300" />}
               aria-label={t('切换主题')}

+ 94 - 0
web/src/components/NoticeModal.js

@@ -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; 

+ 3 - 1
web/src/i18n/locales/en.json

@@ -1529,5 +1529,7 @@
   "系统公告": "System Notice",
   "今日关闭": "Close Today",
   "关闭公告": "Close Notice",
-  "搜索条件": "Search Conditions"
+  "搜索条件": "Search Conditions",
+  "加载中...": "Loading...",
+  "暂无公告": "No Notice"
 }

+ 11 - 49
web/src/pages/Home/index.js

@@ -1,5 +1,5 @@
 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 { StatusContext } from '../../context/Status';
 import { marked } from 'marked';
@@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next';
 import { IconGithubLogo } from '@douyinfe/semi-icons';
 import exampleImage from '../../images/example.png';
 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';
 
 const { Text } = Typography;
@@ -17,38 +18,16 @@ const Home = () => {
   const [homePageContentLoaded, setHomePageContentLoaded] = useState(false);
   const [homePageContent, setHomePageContent] = useState('');
   const [noticeVisible, setNoticeVisible] = useState(false);
-  const [noticeContent, setNoticeContent] = useState('');
 
   const isDemoSiteMode = statusState?.status?.demo_site_enabled || false;
 
-  const handleCloseNotice = () => {
-    setNoticeVisible(false);
-  };
-
-  const handleCloseTodayNotice = () => {
+  useEffect(() => {
+    const lastCloseDate = localStorage.getItem('notice_close_date');
     const today = new Date().toDateString();
-    localStorage.setItem('notice_close_date', today);
-    setNoticeVisible(false);
-  };
-
-  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);
+    if (lastCloseDate !== today) {
+      setNoticeVisible(true);
     }
-  };
+  }, []);
 
   const displayHomePageContent = async () => {
     setHomePageContent(localStorage.getItem('home_page_content') || '');
@@ -81,33 +60,16 @@ const Home = () => {
   };
 
   useEffect(() => {
-    displayNotice().then();
     displayHomePageContent().then();
   }, []);
 
   return (
     <div className="w-full overflow-x-hidden">
-      <Modal
-        title={t('系统公告')}
+      <NoticeModal
         visible={noticeVisible}
-        onCancel={handleCloseNotice}
-        footer={(
-          <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>
+        onClose={() => setNoticeVisible(false)}
+        isMobile={isMobile()}
+      />
       {homePageContentLoaded && homePageContent === '' ? (
         <div className="w-full overflow-x-hidden">
           {/* Banner 部分 */}