Add timestamps and file upload precheck

This commit is contained in:
LeonspaceX
2026-01-30 17:44:04 +08:00
parent fcc4b95f7a
commit abc8d58bf4
7 changed files with 170 additions and 7 deletions

View File

@@ -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}
/>
</div>
@@ -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}
/>
);

View File

@@ -4,6 +4,8 @@ export interface SiteSettings {
enableCodeIcon: boolean;
repoUrl: string;
favicon: string;
fileSizeLimit?: number;
fileFormats?: string[];
}
export const getSettings = async (): Promise<SiteSettings> => {
@@ -15,12 +17,29 @@ export const getSettings = async (): Promise<SiteSettings> => {
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 {

View File

@@ -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(() => {
</ReactMarkdown>
</div>
<div className={styles.commentFooter}>
{(() => {
const timeLabel = formatTimeLabel(comment.time);
return timeLabel ? <Text className={styles.commentTime}>{timeLabel}</Text> : null;
})()}
<Tooltip content="回复" relationship="label">
<div
className={styles.replyButton}

View File

@@ -131,7 +131,7 @@ const remarkTagPlugin = () => {
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<string | undefined>("");
const [lastSaved, setLastSaved] = useState<string>(() => new Date().toLocaleTimeString('zh-CN', { hour12: false }));
@@ -226,6 +226,38 @@ const CreatePost: React.FC = () => {
const handleFileChange: React.ChangeEventHandler<HTMLInputElement> = 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(
<Toast>
<ToastTitle>{limitMb}MB限制</ToastTitle>
</Toast>,
{ 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(
<Toast>
<ToastTitle></ToastTitle>
</Toast>,
{ intent: 'error' }
);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
return;
}
}
try {
const url = await uploadImage(file);
insertImageMarkdown(url, file.name || 'image');

View File

@@ -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 (
<Card className={styles.card}>
<div className={styles.content}>
@@ -244,7 +286,8 @@ const PostCard = ({
</div>
</div>
<CardFooter>
<div className={styles.actions}>
<div className={styles.footerRow}>
<div className={styles.actions}>
<Button
icon={<ArrowUp24Regular primaryFill={voteChoice === 'up' ? tokens.colorBrandForegroundLink : undefined} />}
appearance="transparent"
@@ -323,6 +366,12 @@ const PostCard = ({
className={styles.actionButton}
onClick={() => setShowReportModal(true)}
/>
</div>
{timeLabel && (
<div className={styles.timeRow}>
{showModified ? `已修改 · ${timeLabel}` : timeLabel}
</div>
)}
</div>
</CardFooter>
{showComments && (

View File

@@ -41,6 +41,15 @@ const ReportPost: React.FC<ReportPostProps> = ({ onClose, postId }) => {
const [content, setContent] = React.useState('');
const handleSubmit = async () => {
if (!title.trim() || !content.trim()) {
dispatchToast(
<Toast>
<ToastTitle></ToastTitle>
</Toast>,
{ intent: 'error' }
);
return;
}
try {
const response = await reportPost({ id: postId, title, content });
dispatchToast(