Add timestamps and file upload precheck
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user