Add v2 comments support and align APIs
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
<!--这里没有有用的东西哦(-->
|
||||
<!--不过既然都看到这里了,有没有兴趣加入开发喵!-->
|
||||
<!--既然都看到这里了,有没有兴趣加入开发喵!-->
|
||||
<!--https://github.com/Sycamore-Whisper/v2-->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import { tokens } from '@fluentui/react-components';
|
||||
import { tokens, useToastController, Toast, ToastTitle } from '@fluentui/react-components';
|
||||
import { MainLayout } from './layouts/MainLayout';
|
||||
import About from './components/About';
|
||||
import CreatePost from './components/CreatePost';
|
||||
@@ -11,6 +11,8 @@ import './App.css';
|
||||
|
||||
const Home: React.FC = () => {
|
||||
const { refreshTrigger } = useLayout();
|
||||
const { toasterId } = useLayout();
|
||||
const { dispatchToast } = useToastController(toasterId);
|
||||
const [articles, setArticles] = useState<Article[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -81,6 +83,12 @@ const Home: React.FC = () => {
|
||||
setLoading(false);
|
||||
if (refreshing) {
|
||||
setRefreshing(false);
|
||||
dispatchToast(
|
||||
<Toast>
|
||||
<ToastTitle>刷新成功</ToastTitle>
|
||||
</Toast>,
|
||||
{ intent: 'success' }
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -211,3 +211,70 @@ export const voteArticle = async (id: number, type: 'up' | 'down'): Promise<void
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Comment {
|
||||
id: number;
|
||||
nickname: string;
|
||||
content: string;
|
||||
parent_comment_id: number;
|
||||
}
|
||||
|
||||
export const getComments = async (id: string | number): Promise<Comment[]> => {
|
||||
try {
|
||||
const response = await fetch(`/api/get/comment?id=${id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const json = await response.json();
|
||||
if (json.code === 1000 && Array.isArray(json.data)) {
|
||||
return json.data as Comment[];
|
||||
}
|
||||
throw new Error('Invalid response code or missing data');
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch comments:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export interface PostCommentRequest {
|
||||
content: string;
|
||||
submission_id: number;
|
||||
parent_comment_id: number;
|
||||
nickname: string;
|
||||
}
|
||||
|
||||
export interface PostCommentResponse {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export const postComment = async (commentData: PostCommentRequest): Promise<PostCommentResponse> => {
|
||||
try {
|
||||
const identity = await get_id_token();
|
||||
const response = await fetch('/api/comment', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...commentData,
|
||||
identity,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
if (json.code === 1001 && json.data?.id !== undefined) {
|
||||
return { id: Number(json.data.id) };
|
||||
}
|
||||
if (json.code === 2005) {
|
||||
throw new Error('评论包含违禁词');
|
||||
}
|
||||
throw new Error('Comment failed');
|
||||
} catch (error) {
|
||||
console.error('Failed to post comment:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
257
front/src/components/CommentSection.tsx
Normal file
257
front/src/components/CommentSection.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
makeStyles,
|
||||
Button,
|
||||
Input,
|
||||
Text,
|
||||
tokens,
|
||||
Card,
|
||||
Tooltip,
|
||||
Divider,
|
||||
useToastController,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
} from '@fluentui/react-components';
|
||||
import { Dismiss24Regular, ArrowReply24Regular } from '@fluentui/react-icons';
|
||||
import { getComments, postComment } from '../api';
|
||||
import type { Comment as CommentType } from '../api';
|
||||
import { useLayout } from '../context/LayoutContext';
|
||||
|
||||
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 { toasterId } = useLayout();
|
||||
const { dispatchToast } = useToastController(toasterId);
|
||||
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) {
|
||||
dispatchToast(
|
||||
<Toast>
|
||||
<ToastTitle>获取评论失败</ToastTitle>
|
||||
</Toast>,
|
||||
{ intent: 'error' }
|
||||
);
|
||||
console.error('Error fetching comments:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitComment = async () => {
|
||||
if (!content.trim() || !nickname.trim()) {
|
||||
dispatchToast(
|
||||
<Toast>
|
||||
<ToastTitle>评论内容和昵称不能为空</ToastTitle>
|
||||
</Toast>,
|
||||
{ intent: 'error' }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
await postComment({
|
||||
submission_id: postId,
|
||||
nickname,
|
||||
content,
|
||||
parent_comment_id: replyTo ? replyTo.id : 0,
|
||||
});
|
||||
dispatchToast(
|
||||
<Toast>
|
||||
<ToastTitle>评论成功</ToastTitle>
|
||||
</Toast>,
|
||||
{ intent: 'success' }
|
||||
);
|
||||
setContent('');
|
||||
if (replyTo) setReplyTo(null);
|
||||
fetchComments();
|
||||
} catch (error: any) {
|
||||
dispatchToast(
|
||||
<Toast>
|
||||
<ToastTitle>评论失败</ToastTitle>
|
||||
</Toast>,
|
||||
{ intent: '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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentSection;
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
Warning24Regular,
|
||||
} from '@fluentui/react-icons';
|
||||
import { useLayout } from '../context/LayoutContext';
|
||||
import CommentSection from './CommentSection';
|
||||
|
||||
// 自定义 remark 插件,用于高亮 #tag
|
||||
const remarkTagPlugin = () => {
|
||||
@@ -149,6 +150,11 @@ const useStyles = makeStyles({
|
||||
justifyItems: 'center',
|
||||
gap: '0 8px',
|
||||
},
|
||||
commentSection: {
|
||||
marginTop: tokens.spacingVerticalM,
|
||||
borderTop: `1px solid ${tokens.colorNeutralStroke1}`,
|
||||
paddingTop: tokens.spacingVerticalM,
|
||||
},
|
||||
});
|
||||
|
||||
interface PostCardProps {
|
||||
@@ -171,6 +177,7 @@ const PostCard = ({
|
||||
const { dispatchToast } = useToastController(toasterId);
|
||||
const [votes, setVotes] = React.useState({ upvotes, downvotes });
|
||||
const [hasVoted, setHasVoted] = React.useState(false);
|
||||
const [showComments, setShowComments] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setVotes({ upvotes, downvotes });
|
||||
@@ -267,7 +274,7 @@ const PostCard = ({
|
||||
<Button
|
||||
icon={<Comment24Regular />}
|
||||
appearance="transparent"
|
||||
onClick={() => {}}
|
||||
onClick={() => setShowComments(!showComments)}
|
||||
/>
|
||||
<Button
|
||||
icon={<Warning24Regular />}
|
||||
@@ -276,6 +283,11 @@ const PostCard = ({
|
||||
/>
|
||||
</div>
|
||||
</CardFooter>
|
||||
{showComments && (
|
||||
<div className={styles.commentSection}>
|
||||
<CommentSection postId={id} />
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user