Add site notice and timestamp display updates
This commit is contained in:
@@ -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 />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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'));
|
||||
|
||||
93
front/src/components/NoticeModal.tsx
Normal file
93
front/src/components/NoticeModal.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user