diff --git a/back/main.py b/back/main.py index 6d6e94a..35e16af 100644 --- a/back/main.py +++ b/back/main.py @@ -52,6 +52,7 @@ class Submission(db.Model): identity_token = db.Column(db.String(36), nullable=True) status = db.Column(db.String(20), default='Pending') created_at = db.Column(db.DateTime, default=lambda: datetime.now()) + updated_at = db.Column(db.DateTime, default=lambda: datetime.now()) upvotes = db.Column(db.Integer, default=0) downvotes = db.Column(db.Integer, default=0) @@ -153,7 +154,9 @@ def get_settings(): "footer_text": settings.footer_text, "repo_link": settings.repo_link, "enable_repo_button": settings.enable_repo_button, - "enable_notice": settings.enable_notice + "enable_notice": settings.enable_notice, + "file_size_limit": settings.file_size_limit, + "file_formats": settings.file_formats } return jsonify({ "code": 1000, @@ -252,10 +255,13 @@ def submit_post(): identity_token = None # 保存 + now = datetime.now() new_post = Submission( content=content, identity_token=identity_token, - status='Pending' if NEED_AUDIT else 'Pass' + status='Pending' if NEED_AUDIT else 'Pass', + created_at=now, + updated_at=now, ) db.session.add(new_post) db.session.commit() @@ -349,6 +355,10 @@ def submit_report(): submission_id = data.get('id') title = data.get('title') content = data.get('content') + title = str(title).strip() if title is not None else '' + content = str(content).strip() if content is not None else '' + if not title or not content: + return jsonify({"code": 2000, "data": "参数错误"}) submission = db.session.get(Submission, submission_id) if not submission: @@ -363,8 +373,8 @@ def submit_report(): report = Report( submission_id=submission_id, - title=str(title).strip() if title is not None else '', - content=str(content).strip() if content is not None else '', + title=title, + content=content, identity_token=identity_token, status='Pending', ) @@ -399,7 +409,8 @@ def get_comments(): "id": c.id, "nickname": c.nickname, "content": c.content, - "parent_comment_id": c.parent_comment_id if c.parent_comment_id is not None else 0 + "parent_comment_id": c.parent_comment_id if c.parent_comment_id is not None else 0, + "time": c.created_at.isoformat() if c.created_at else None } for c in pagination.items] return jsonify({"code": 1000, "data": {"comments": data, "total_pages": pagination.pages}}) @@ -549,6 +560,8 @@ def get_posts_info(): "upvotes": s.upvotes, "downvotes": s.downvotes, "created_at": s.created_at.isoformat() if s.created_at else None, + "time": s.created_at.isoformat() if s.created_at else None, + "modified": 0 if (not s.updated_at or not s.created_at or s.updated_at == s.created_at) else 1, "comment_count": len(s.comments), "total_pages": pagination.total, }) @@ -577,6 +590,8 @@ def get_post_info(): "upvotes": submission.upvotes, "downvotes": submission.downvotes, "created_at": submission.created_at.isoformat() if submission.created_at else None, + "time": submission.created_at.isoformat() if submission.created_at else None, + "modified": 0 if (not submission.updated_at or not submission.created_at or submission.updated_at == submission.created_at) else 1, "comment_count": len(submission.comments), } diff --git a/front/src/App.tsx b/front/src/App.tsx index 8c04d65..7a22fb3 100644 --- a/front/src/App.tsx +++ b/front/src/App.tsx @@ -115,6 +115,8 @@ const Home: React.FC<{ onPreviewImage: (src: string, alt?: string) => void }> = content={article.content} upvotes={article.upvotes} downvotes={article.downvotes} + time={article.time} + modified={article.modified} onPreviewImage={onPreviewImage} /> @@ -127,6 +129,8 @@ const Home: React.FC<{ onPreviewImage: (src: string, alt?: string) => void }> = content={article.content} upvotes={article.upvotes} downvotes={article.downvotes} + time={article.time} + modified={article.modified} onPreviewImage={onPreviewImage} /> ); diff --git a/front/src/api.ts b/front/src/api.ts index ce6e233..2b37abb 100644 --- a/front/src/api.ts +++ b/front/src/api.ts @@ -4,6 +4,8 @@ export interface SiteSettings { enableCodeIcon: boolean; repoUrl: string; favicon: string; + fileSizeLimit?: number; + fileFormats?: string[]; } export const getSettings = async (): Promise => { @@ -15,12 +17,29 @@ export const getSettings = async (): Promise => { const json = await response.json(); if (json.code === 1000 && json.data) { const data = json.data; + let fileFormats: string[] | undefined; + if (data.file_formats) { + if (Array.isArray(data.file_formats)) { + fileFormats = data.file_formats.map((x: any) => String(x).trim().replace(/^\./, '').toLowerCase()).filter((x: string) => x); + } else if (typeof data.file_formats === 'string') { + try { + const parsed = JSON.parse(data.file_formats); + if (Array.isArray(parsed)) { + fileFormats = parsed.map((x: any) => String(x).trim().replace(/^\./, '').toLowerCase()).filter((x: string) => x); + } + } catch { + // ignore parse error + } + } + } return { siteTitle: data.title, siteFooter: data.footer_text, enableCodeIcon: data.enable_repo_button ?? true, repoUrl: data.repo_link, favicon: data.icon, + fileSizeLimit: data.file_size_limit != null ? Number(data.file_size_limit) : undefined, + fileFormats, }; } else { throw new Error('Invalid response code or missing data'); @@ -213,6 +232,8 @@ export interface Article { upvotes: number; downvotes: number; created_at?: string; + time?: string; + modified?: number; comment_count?: number; total_pages?: number; } @@ -261,6 +282,7 @@ export interface Comment { nickname: string; content: string; parent_comment_id: number; + time?: string; } export interface CommentPage { diff --git a/front/src/components/CommentSection.tsx b/front/src/components/CommentSection.tsx index eab9768..c3cce4b 100644 --- a/front/src/components/CommentSection.tsx +++ b/front/src/components/CommentSection.tsx @@ -61,6 +61,11 @@ const useStyles = makeStyles({ alignItems: 'center', marginTop: tokens.spacingVerticalXS, }, + commentTime: { + marginRight: tokens.spacingHorizontalS, + color: tokens.colorNeutralForeground3, + fontSize: tokens.fontSizeBase200, + }, replyButton: { cursor: 'pointer', color: tokens.colorBrandForeground1, @@ -236,6 +241,29 @@ useEffect(() => { setReplyTo(null); }; + const formatTimeLabel = (iso?: string) => { + if (!iso) return ''; + const date = new Date(iso); + if (Number.isNaN(date.getTime())) return ''; + const now = new Date(); + let diffMs = now.getTime() - date.getTime(); + if (diffMs < 0) diffMs = 0; + const diffMinutes = Math.floor(diffMs / 60000); + if (diffMinutes < 1) return '现在'; + if (diffMinutes < 60) return `${diffMinutes}分钟前`; + const diffHours = Math.floor(diffMs / 3600000); + const sameDay = now.toDateString() === date.toDateString(); + if (sameDay) return `${diffHours}小时前`; + const diffDays = Math.floor(diffMs / 86400000); + const diffMonths = Math.floor(diffDays / 30); + if (diffMonths >= 1 && diffMonths < 12) return `${diffMonths}个月前`; + if (diffDays >= 1 && diffMonths < 1) return `${diffDays}天前`; + const yyyy = date.getFullYear(); + const mm = String(date.getMonth() + 1).padStart(2, '0'); + const dd = String(date.getDate()).padStart(2, '0'); + return `${yyyy}年${mm}月${dd}日`; + }; + const renderComments = (parentId: number = 0, level: number = 0) => { return comments .filter(comment => comment.parent_comment_id === parentId) @@ -267,6 +295,10 @@ useEffect(() => {
+ {(() => { + const timeLabel = formatTimeLabel(comment.time); + return timeLabel ? {timeLabel} : null; + })()}
{ const CreatePost: React.FC = () => { const styles = useStyles(); const navigate = useNavigate(); - const { isDarkMode, toasterId, triggerRefresh } = useLayout(); + const { isDarkMode, toasterId, triggerRefresh, settings } = useLayout(); const { dispatchToast } = useToastController(toasterId); const [value, setValue] = useState(""); const [lastSaved, setLastSaved] = useState(() => new Date().toLocaleTimeString('zh-CN', { hour12: false })); @@ -226,6 +226,38 @@ const CreatePost: React.FC = () => { const handleFileChange: React.ChangeEventHandler = async (e) => { const file = e.target.files?.[0]; if (!file) return; + const limitMb = settings?.fileSizeLimit; + if (typeof limitMb === 'number' && limitMb > 0) { + const limitBytes = limitMb * 1024 * 1024; + if (file.size > limitBytes) { + dispatchToast( + + 上传的文件超出{limitMb}MB限制 + , + { intent: 'error' } + ); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + return; + } + } + const formats = settings?.fileFormats; + if (formats && formats.length > 0) { + const ext = file.name.split('.').pop()?.toLowerCase() || ''; + if (!ext || !formats.includes(ext)) { + dispatchToast( + + 上传的文件类型不支持 + , + { intent: 'error' } + ); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + return; + } + } try { const url = await uploadImage(file); insertImageMarkdown(url, file.name || 'image'); diff --git a/front/src/components/PostCard.tsx b/front/src/components/PostCard.tsx index cf31cb4..0b3b405 100644 --- a/front/src/components/PostCard.tsx +++ b/front/src/components/PostCard.tsx @@ -151,6 +151,18 @@ const useStyles = makeStyles({ justifyItems: 'center', gap: '0 8px', }, + footerRow: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + width: '100%', + gap: tokens.spacingHorizontalM, + }, + timeRow: { + color: tokens.colorNeutralForeground3, + fontSize: tokens.fontSizeBase200, + whiteSpace: 'nowrap', + }, actionButton: { '&:focus': { outlineStyle: 'none', @@ -194,6 +206,8 @@ interface PostCardProps { content: string; upvotes: number; downvotes: number; + time?: string; + modified?: number; onPreviewImage?: (src: string, alt?: string) => void; } @@ -202,6 +216,8 @@ const PostCard = ({ content, upvotes, downvotes, + time, + modified, onPreviewImage, }: PostCardProps) => { const styles = useStyles(); @@ -217,6 +233,32 @@ const PostCard = ({ setVotes({ upvotes, downvotes }); }, [upvotes, downvotes]); + const formatTimeLabel = (iso?: string) => { + if (!iso) return ''; + const date = new Date(iso); + if (Number.isNaN(date.getTime())) return ''; + const now = new Date(); + let diffMs = now.getTime() - date.getTime(); + if (diffMs < 0) diffMs = 0; + const diffMinutes = Math.floor(diffMs / 60000); + if (diffMinutes < 1) return '现在'; + if (diffMinutes < 60) return `${diffMinutes}分钟前`; + const diffHours = Math.floor(diffMs / 3600000); + const sameDay = now.toDateString() === date.toDateString(); + if (sameDay) return `${diffHours}小时前`; + const diffDays = Math.floor(diffMs / 86400000); + const diffMonths = Math.floor(diffDays / 30); + if (diffMonths >= 1 && diffMonths < 12) return `${diffMonths}个月前`; + if (diffDays >= 1 && diffMonths < 1) return `${diffDays}天前`; + const yyyy = date.getFullYear(); + const mm = String(date.getMonth() + 1).padStart(2, '0'); + const dd = String(date.getDate()).padStart(2, '0'); + return `${yyyy}年${mm}月${dd}日`; + }; + + const timeLabel = formatTimeLabel(time); + const showModified = modified === 1; + return (
@@ -244,7 +286,8 @@ const PostCard = ({
-
+
+
+ {timeLabel && ( +
+ {showModified ? `已修改 · ${timeLabel}` : timeLabel} +
+ )}
{showComments && ( diff --git a/front/src/components/ReportPost.tsx b/front/src/components/ReportPost.tsx index 3e927a0..ddcd2f4 100644 --- a/front/src/components/ReportPost.tsx +++ b/front/src/components/ReportPost.tsx @@ -41,6 +41,15 @@ const ReportPost: React.FC = ({ onClose, postId }) => { const [content, setContent] = React.useState(''); const handleSubmit = async () => { + if (!title.trim() || !content.trim()) { + dispatchToast( + + 投诉标题和内容不能为空 + , + { intent: 'error' } + ); + return; + } try { const response = await reportPost({ id: postId, title, content }); dispatchToast(