first commit

This commit is contained in:
LeonspaceX
2025-10-18 17:34:11 +08:00
commit cb0fd04f59
43 changed files with 10922 additions and 0 deletions

View File

@@ -0,0 +1,233 @@
import React, { useState, useEffect, useRef } from 'react';
import {
makeStyles,
Button,
Input,
Text,
tokens,
Card,
Tooltip,
Divider
} from '@fluentui/react-components';
import { Dismiss24Regular, ArrowReply24Regular } from '@fluentui/react-icons';
import { getComments, postComment } from '../api';
import type { Comment as CommentType } from '../api';
import { toast, Toaster } from 'react-hot-toast';
const useStyles = makeStyles({
container: {
padding: tokens.spacingVerticalM,
width: '100%',
},
commentInput: {
marginBottom: tokens.spacingVerticalS,
width: '100%',
height: '40px',
fontSize: '16px',
},
commentButton: {
marginBottom: tokens.spacingVerticalM,
},
commentList: {
marginTop: tokens.spacingVerticalM,
},
commentCard: {
marginBottom: tokens.spacingVerticalS,
padding: tokens.spacingHorizontalM,
width: '100%',
},
childComment: {
marginLeft: tokens.spacingHorizontalL,
borderLeft: `2px solid ${tokens.colorNeutralStroke1}`,
paddingLeft: tokens.spacingHorizontalM,
},
commentHeader: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: tokens.spacingVerticalXS,
},
nickname: {
fontWeight: 'bold',
},
commentFooter: {
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
marginTop: tokens.spacingVerticalXS,
},
replyButton: {
cursor: 'pointer',
color: tokens.colorBrandForeground1,
display: 'flex',
alignItems: 'center',
gap: tokens.spacingHorizontalXS,
},
replyInfo: {
display: 'flex',
alignItems: 'center',
marginBottom: tokens.spacingVerticalXS,
},
cancelReply: {
marginLeft: tokens.spacingHorizontalS,
cursor: 'pointer',
},
inputContainer: {
display: 'flex',
flexDirection: 'column',
gap: tokens.spacingVerticalS,
},
nicknameInput: {
width: '100%',
height: '40px',
fontSize: '16px',
},
});
interface CommentSectionProps {
postId: number;
}
const CommentSection: React.FC<CommentSectionProps> = ({ postId }) => {
const styles = useStyles();
const [comments, setComments] = useState<CommentType[]>([]);
const [content, setContent] = useState('');
const [nickname, setNickname] = useState('');
const [replyTo, setReplyTo] = useState<CommentType | null>(null);
const [loading, setLoading] = useState(false);
const commentCardRefs = useRef<Map<number, HTMLDivElement>>(new Map());
useEffect(() => {
fetchComments();
}, [postId]);
const fetchComments = async () => {
try {
setLoading(true);
const data = await getComments(postId);
setComments(data as CommentType[]);
} catch (error) {
toast.error('获取评论失败');
console.error('Error fetching comments:', error);
} finally {
setLoading(false);
}
};
const handleSubmitComment = async () => {
if (!content.trim() || !nickname.trim()) {
toast.error('评论内容和昵称不能为空');
return;
}
try {
setLoading(true);
await postComment({
submission_id: postId,
nickname,
content,
parent_comment_id: replyTo ? replyTo.id : 0,
});
toast.success('评论成功');
setContent('');
if (replyTo) setReplyTo(null);
fetchComments();
} catch (error: any) {
toast.error('评论失败');
console.error('Error posting comment:', error);
} finally {
setLoading(false);
}
};
const handleReply = (comment: CommentType) => {
setReplyTo(comment);
};
const cancelReply = () => {
setReplyTo(null);
};
const renderComments = (parentId: number = 0, level: number = 0) => {
return comments
.filter(comment => comment.parent_comment_id === parentId)
.map(comment => (
<div
key={comment.id}
className={level > 0 ? styles.childComment : ''}
ref={el => {
if (el) commentCardRefs.current.set(comment.id, el);
}}
>
<Card className={styles.commentCard}>
<div className={styles.commentHeader}>
<Text className={styles.nickname}>{comment.nickname}</Text>
</div>
<Text>{comment.content}</Text>
<div className={styles.commentFooter}>
<Tooltip content="回复" relationship="label">
<div
className={styles.replyButton}
onClick={() => handleReply(comment)}
>
<ArrowReply24Regular />
<Text size={200}></Text>
</div>
</Tooltip>
</div>
</Card>
{renderComments(comment.id, level + 1)}
</div>
));
};
return (
<div className={styles.container}>
<div className={styles.inputContainer}>
<Input
className={styles.nicknameInput}
placeholder="输入昵称"
value={nickname}
onChange={(e) => setNickname(e.target.value)}
/>
{replyTo && (
<div className={styles.replyInfo}>
<Text>{replyTo.nickname}</Text>
<Dismiss24Regular
className={styles.cancelReply}
onClick={cancelReply}
/>
</div>
)}
<Input
className={styles.commentInput}
placeholder="输入评论"
value={content}
onChange={(e) => setContent(e.target.value)}
/>
<Button
className={styles.commentButton}
appearance="primary"
onClick={handleSubmitComment}
disabled={loading || !content.trim() || !nickname.trim()}
>
</Button>
</div>
<Divider />
<div className={styles.commentList}>
{loading && <Text>...</Text>}
{!loading && comments.length === 0 && <Text></Text>}
{renderComments()}
</div>
<Toaster position="top-center" />
</div>
);
};
export default CommentSection;