Add site notice and timestamp display updates
This commit is contained in:
34
back/main.py
34
back/main.py
@@ -97,6 +97,15 @@ class ImgFile(db.Model):
|
|||||||
name = db.Column(db.String(255), nullable=True)
|
name = db.Column(db.String(255), nullable=True)
|
||||||
identity_token = db.Column(db.String(36), nullable=True)
|
identity_token = db.Column(db.String(36), nullable=True)
|
||||||
|
|
||||||
|
class SiteNotice(db.Model):
|
||||||
|
__tablename__ = 'site_notice'
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
type = db.Column(db.String(10), default='md', nullable=False)
|
||||||
|
content = db.Column(db.Text, default='', nullable=False)
|
||||||
|
version = db.Column(db.Integer, default=0, nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=lambda: datetime.now())
|
||||||
|
updated_at = db.Column(db.DateTime, default=lambda: datetime.now(), onupdate=datetime.now)
|
||||||
|
|
||||||
# 初始化数据库函数
|
# 初始化数据库函数
|
||||||
def init_db():
|
def init_db():
|
||||||
if not os.path.exists('./data'):
|
if not os.path.exists('./data'):
|
||||||
@@ -105,6 +114,14 @@ def init_db():
|
|||||||
os.makedirs(IMG_DIR)
|
os.makedirs(IMG_DIR)
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
try:
|
||||||
|
existing = SiteNotice.query.first()
|
||||||
|
if not existing:
|
||||||
|
n = SiteNotice(type='md', content='', version=0)
|
||||||
|
db.session.add(n)
|
||||||
|
db.session.commit()
|
||||||
|
except Exception:
|
||||||
|
db.session.rollback()
|
||||||
|
|
||||||
def load_config():
|
def load_config():
|
||||||
global NEED_AUDIT, FILE_SIZE_LIMIT_MB, FILE_FORMATS
|
global NEED_AUDIT, FILE_SIZE_LIMIT_MB, FILE_FORMATS
|
||||||
@@ -180,6 +197,23 @@ def get_about():
|
|||||||
"data": "# 默认关于页面\n关于页面未设置,请前往管理面板操作。"
|
"data": "# 默认关于页面\n关于页面未设置,请前往管理面板操作。"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@app.route('/api/site_notice', methods=['GET'])
|
||||||
|
def get_site_notice():
|
||||||
|
try:
|
||||||
|
notice = SiteNotice.query.order_by(SiteNotice.id.asc()).first()
|
||||||
|
if not notice:
|
||||||
|
notice = SiteNotice(type='md', content='', version=0)
|
||||||
|
db.session.add(notice)
|
||||||
|
db.session.commit()
|
||||||
|
data = {
|
||||||
|
"type": notice.type if notice.type in ['md', 'url'] else 'md',
|
||||||
|
"content": notice.content or '',
|
||||||
|
"version": int(notice.version or 0),
|
||||||
|
}
|
||||||
|
return jsonify({"code": 1000, "data": data})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"code": 2003, "data": str(e)})
|
||||||
|
|
||||||
@app.route('/api/get_id_token', methods=['GET'])
|
@app.route('/api/get_id_token', methods=['GET'])
|
||||||
def get_id_token():
|
def get_id_token():
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import About from './components/About';
|
|||||||
import CreatePost from './components/CreatePost';
|
import CreatePost from './components/CreatePost';
|
||||||
import PostCard from './components/PostCard';
|
import PostCard from './components/PostCard';
|
||||||
import ImageViewer from './components/ImageViewer';
|
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 { useLayout } from './context/LayoutContext';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import { Eye24Regular, EyeOff24Regular, Copy24Regular } from '@fluentui/react-icons';
|
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 Home: React.FC<{ onPreviewImage: (src: string, alt?: string) => void }> = ({ onPreviewImage }) => {
|
||||||
const { refreshTrigger } = useLayout();
|
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 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() {
|
function App() {
|
||||||
const [imageViewer, setImageViewer] = useState<{ open: boolean; src?: string; alt?: string }>({ open: false });
|
const [imageViewer, setImageViewer] = useState<{ open: boolean; src?: string; alt?: string }>({ open: false });
|
||||||
const openImageViewer = (src?: string, alt?: string) => {
|
const openImageViewer = (src?: string, alt?: string) => {
|
||||||
@@ -254,6 +305,7 @@ function App() {
|
|||||||
</DialogBody>
|
</DialogBody>
|
||||||
</DialogSurface>
|
</DialogSurface>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
<NoticeOverlay />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export interface SiteSettings {
|
|||||||
enableCodeIcon: boolean;
|
enableCodeIcon: boolean;
|
||||||
repoUrl: string;
|
repoUrl: string;
|
||||||
favicon: string;
|
favicon: string;
|
||||||
|
enableNotice?: boolean;
|
||||||
fileSizeLimit?: number;
|
fileSizeLimit?: number;
|
||||||
fileFormats?: string[];
|
fileFormats?: string[];
|
||||||
}
|
}
|
||||||
@@ -38,6 +39,7 @@ export const getSettings = async (): Promise<SiteSettings> => {
|
|||||||
enableCodeIcon: data.enable_repo_button ?? true,
|
enableCodeIcon: data.enable_repo_button ?? true,
|
||||||
repoUrl: data.repo_link,
|
repoUrl: data.repo_link,
|
||||||
favicon: data.icon,
|
favicon: data.icon,
|
||||||
|
enableNotice: data.enable_notice ?? false,
|
||||||
fileSizeLimit: data.file_size_limit != null ? Number(data.file_size_limit) : undefined,
|
fileSizeLimit: data.file_size_limit != null ? Number(data.file_size_limit) : undefined,
|
||||||
fileFormats,
|
fileFormats,
|
||||||
};
|
};
|
||||||
@@ -74,6 +76,12 @@ export interface StaticsData {
|
|||||||
images: number;
|
images: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SiteNoticeData {
|
||||||
|
type: 'md' | 'url';
|
||||||
|
content: string;
|
||||||
|
version: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface HotTopicItem {
|
export interface HotTopicItem {
|
||||||
name: string;
|
name: string;
|
||||||
count: number;
|
count: number;
|
||||||
@@ -141,6 +149,27 @@ export const get_id_token = async (): Promise<string> => {
|
|||||||
return token;
|
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 = () => {
|
const notifyInvalidIdentity = () => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.dispatchEvent(new CustomEvent('identity_invalid'));
|
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