Add site notice and timestamp display updates

This commit is contained in:
LeonspaceX
2026-01-30 18:42:41 +08:00
parent abc8d58bf4
commit 183032aa90
4 changed files with 209 additions and 1 deletions

View File

@@ -6,10 +6,12 @@ import About from './components/About';
import CreatePost from './components/CreatePost';
import PostCard from './components/PostCard';
import ImageViewer from './components/ImageViewer';
import { fetchArticles, reset_identity_token, type Article } from './api';
import { fetchArticles, reset_identity_token, getSiteNotice, type Article } from './api';
import { useLayout } from './context/LayoutContext';
import './App.css';
import { Eye24Regular, EyeOff24Regular, Copy24Regular } from '@fluentui/react-icons';
import NoticeModal from './components/NoticeModal';
import type { NoticeData } from './components/NoticeModal';
const Home: React.FC<{ onPreviewImage: (src: string, alt?: string) => void }> = ({ onPreviewImage }) => {
const { refreshTrigger } = useLayout();
@@ -152,6 +154,55 @@ const Home: React.FC<{ onPreviewImage: (src: string, alt?: string) => void }> =
const NotFound = () => <h1>404 Not Found</h1>;
const NoticeOverlay: React.FC = () => {
const { settings } = useLayout();
const [noticeData, setNoticeData] = useState<NoticeData | null>(null);
const [showNotice, setShowNotice] = useState(false);
useEffect(() => {
if (!settings || settings.enableNotice !== true) {
setShowNotice(false);
return;
}
getSiteNotice().then(data => {
const savedVersion = localStorage.getItem('site_notice_version');
if (data.content && (!savedVersion || Number(savedVersion) < Number(data.version))) {
setNoticeData({
type: data.type,
content: data.content,
version: data.version,
});
setShowNotice(true);
}
}).catch(console.error);
}, [settings]);
if (!showNotice || !noticeData) return null;
return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: 2000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
<NoticeModal
data={noticeData}
onClose={() => setShowNotice(false)}
onNeverShow={(version) => {
localStorage.setItem('site_notice_version', String(version));
setShowNotice(false);
}}
/>
</div>
);
};
function App() {
const [imageViewer, setImageViewer] = useState<{ open: boolean; src?: string; alt?: string }>({ open: false });
const openImageViewer = (src?: string, alt?: string) => {
@@ -254,6 +305,7 @@ function App() {
</DialogBody>
</DialogSurface>
</Dialog>
<NoticeOverlay />
</>
}
/>

View File

@@ -4,6 +4,7 @@ export interface SiteSettings {
enableCodeIcon: boolean;
repoUrl: string;
favicon: string;
enableNotice?: boolean;
fileSizeLimit?: number;
fileFormats?: string[];
}
@@ -38,6 +39,7 @@ export const getSettings = async (): Promise<SiteSettings> => {
enableCodeIcon: data.enable_repo_button ?? true,
repoUrl: data.repo_link,
favicon: data.icon,
enableNotice: data.enable_notice ?? false,
fileSizeLimit: data.file_size_limit != null ? Number(data.file_size_limit) : undefined,
fileFormats,
};
@@ -74,6 +76,12 @@ export interface StaticsData {
images: number;
}
export interface SiteNoticeData {
type: 'md' | 'url';
content: string;
version: number;
}
export interface HotTopicItem {
name: string;
count: number;
@@ -141,6 +149,27 @@ export const get_id_token = async (): Promise<string> => {
return token;
};
export const getSiteNotice = async (): Promise<SiteNoticeData> => {
try {
const response = await fetch('/api/site_notice');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
if (json.code === 1000 && json.data) {
return {
type: (json.data.type === 'url' ? 'url' : 'md') as 'md' | 'url',
content: String(json.data.content ?? ''),
version: Number(json.data.version ?? 0),
};
}
throw new Error('Invalid response code or missing data');
} catch (error) {
console.error('Failed to fetch site notice:', error);
throw error;
}
};
const notifyInvalidIdentity = () => {
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('identity_invalid'));

View File

@@ -0,0 +1,93 @@
import React from 'react';
import { makeStyles, Button, tokens, Text } from '@fluentui/react-components';
import { Dismiss24Regular } from '@fluentui/react-icons';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkIns from 'remark-ins';
import remarkBreaks from 'remark-breaks';
const useStyles = makeStyles({
modalContent: {
backgroundColor: tokens.colorNeutralBackground1,
padding: tokens.spacingHorizontalXXL,
borderRadius: tokens.borderRadiusXLarge,
boxShadow: tokens.shadow64,
display: 'flex',
flexDirection: 'column',
gap: tokens.spacingVerticalM,
width: '520px',
maxWidth: '90vw',
position: 'relative',
},
closeButton: {
position: 'absolute',
top: tokens.spacingVerticalS,
right: tokens.spacingHorizontalS,
},
title: {
fontSize: tokens.fontSizeBase500,
fontWeight: tokens.fontWeightSemibold,
marginBottom: tokens.spacingVerticalS,
},
contentBox: {
maxHeight: '60vh',
overflowY: 'auto',
},
iframe: {
width: '100%',
height: '360px',
border: 'none',
borderRadius: tokens.borderRadiusMedium,
backgroundColor: tokens.colorNeutralBackground2,
},
actions: {
display: 'flex',
justifyContent: 'flex-end',
gap: tokens.spacingHorizontalS,
},
});
export interface NoticeData {
type: 'md' | 'url';
content: string;
version: number;
}
interface NoticeModalProps {
data: NoticeData;
onClose: () => void;
onNeverShow: (version: number) => void;
}
const NoticeModal: React.FC<NoticeModalProps> = ({ data, onClose, onNeverShow }) => {
const styles = useStyles();
const { type, content, version } = data;
return (
<div className={styles.modalContent} role="dialog" aria-modal="true" aria-label="公告">
<Button
icon={<Dismiss24Regular />}
appearance="transparent"
className={styles.closeButton}
onClick={onClose}
aria-label="关闭"
/>
<Text as="h2" className={styles.title}></Text>
<div className={styles.contentBox}>
{type === 'md' ? (
<ReactMarkdown remarkPlugins={[remarkGfm, remarkIns, remarkBreaks]}>
{content}
</ReactMarkdown>
) : (
<iframe className={styles.iframe} src={content} title={`公告-${version}`} />
)}
</div>
<div className={styles.actions}>
<Button appearance="primary" onClick={onClose}></Button>
<Button appearance="subtle" onClick={() => onNeverShow(version)}></Button>
</div>
</div>
);
};
export default NoticeModal;