Add v2 comments support and align APIs
This commit is contained in:
72
back/main.py
72
back/main.py
@@ -1,6 +1,6 @@
|
|||||||
# 这里是Sycamore whisper的后端代码喵!
|
# 这里是Sycamore whisper的后端代码喵!
|
||||||
# 但愿比V1写的好喵(逃
|
# 但愿比V1写的好喵(逃
|
||||||
from flask import Flask, jsonify, request
|
from flask import Flask, jsonify, request, abort
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
import os
|
import os
|
||||||
@@ -56,11 +56,10 @@ class Comment(db.Model):
|
|||||||
__tablename__ = 'comments'
|
__tablename__ = 'comments'
|
||||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
submission_id = db.Column(db.Integer, db.ForeignKey('submissions.id'), nullable=False)
|
submission_id = db.Column(db.Integer, db.ForeignKey('submissions.id'), nullable=False)
|
||||||
|
nickname = db.Column(db.String(50), default='匿名用户')
|
||||||
content = db.Column(db.Text, nullable=False)
|
content = db.Column(db.Text, nullable=False)
|
||||||
identity_token = db.Column(db.String(36), nullable=True)
|
identity_token = db.Column(db.String(36), nullable=True)
|
||||||
created_at = db.Column(db.DateTime, default=lambda: datetime.now())
|
created_at = db.Column(db.DateTime, default=lambda: datetime.now())
|
||||||
upvotes = db.Column(db.Integer, default=0)
|
|
||||||
downvotes = db.Column(db.Integer, default=0)
|
|
||||||
parent_comment_id = db.Column(db.Integer, db.ForeignKey('comments.id'), nullable=True)
|
parent_comment_id = db.Column(db.Integer, db.ForeignKey('comments.id'), nullable=True)
|
||||||
|
|
||||||
class Hashtag(db.Model):
|
class Hashtag(db.Model):
|
||||||
@@ -192,6 +191,7 @@ def submit_post():
|
|||||||
return jsonify({"code": 2000, "data": "内容不能为空"})
|
return jsonify({"code": 2000, "data": "内容不能为空"})
|
||||||
|
|
||||||
content = data['content']
|
content = data['content']
|
||||||
|
nickname = data.get('nickname') or '匿名用户'
|
||||||
hashtopic = data.get('hashtopic', [])
|
hashtopic = data.get('hashtopic', [])
|
||||||
|
|
||||||
if not isinstance(hashtopic, list):
|
if not isinstance(hashtopic, list):
|
||||||
@@ -245,6 +245,12 @@ def submit_comment():
|
|||||||
|
|
||||||
submission_id = data['submission_id']
|
submission_id = data['submission_id']
|
||||||
content = data['content']
|
content = data['content']
|
||||||
|
nickname = data.get('nickname') or '匿名用户'
|
||||||
|
parent_comment_id = data.get('parent_comment_id', 0)
|
||||||
|
try:
|
||||||
|
parent_comment_id = int(parent_comment_id)
|
||||||
|
except Exception:
|
||||||
|
parent_comment_id = 0
|
||||||
hashtopic = data.get('hashtopic', [])
|
hashtopic = data.get('hashtopic', [])
|
||||||
|
|
||||||
if not isinstance(hashtopic, list):
|
if not isinstance(hashtopic, list):
|
||||||
@@ -253,8 +259,8 @@ def submit_comment():
|
|||||||
identity_token = data.get('identity')
|
identity_token = data.get('identity')
|
||||||
|
|
||||||
# 检查 submission 是否存在
|
# 检查 submission 是否存在
|
||||||
if not Submission.query.get(submission_id):
|
if not db.session.get(Submission, submission_id):
|
||||||
return jsonify({"code": 2004, "data": "投稿不存在"})
|
return jsonify({"code": 2002, "data": "投稿不存在"})
|
||||||
|
|
||||||
# 违禁词检测
|
# 违禁词检测
|
||||||
for word in DENY_WORDS_CACHE:
|
for word in DENY_WORDS_CACHE:
|
||||||
@@ -270,8 +276,10 @@ def submit_comment():
|
|||||||
|
|
||||||
new_comment = Comment(
|
new_comment = Comment(
|
||||||
submission_id=submission_id,
|
submission_id=submission_id,
|
||||||
|
nickname=nickname,
|
||||||
content=content,
|
content=content,
|
||||||
identity_token=identity_token
|
identity_token=identity_token,
|
||||||
|
parent_comment_id=None if parent_comment_id == 0 else parent_comment_id
|
||||||
)
|
)
|
||||||
db.session.add(new_comment)
|
db.session.add(new_comment)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@@ -291,6 +299,28 @@ def submit_comment():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"code": 2003, "data": f"评论失败: {str(e)}"})
|
return jsonify({"code": 2003, "data": f"评论失败: {str(e)}"})
|
||||||
|
|
||||||
|
@app.route('/api/get/comment', methods=['GET'])
|
||||||
|
def get_comments():
|
||||||
|
try:
|
||||||
|
submission_id = request.args.get("id", type=int)
|
||||||
|
if not submission_id:
|
||||||
|
return jsonify({"code": 2000, "data": "参数错误"})
|
||||||
|
|
||||||
|
submission = db.session.get(Submission, submission_id)
|
||||||
|
if not submission or submission.status != "Pass":
|
||||||
|
return jsonify({"code": 2002, "data": "投稿不存在"})
|
||||||
|
|
||||||
|
data = [{
|
||||||
|
"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
|
||||||
|
} for c in submission.comments]
|
||||||
|
|
||||||
|
return jsonify({"code": 1000, "data": data})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"code": 2003, "data": str(e)})
|
||||||
|
|
||||||
@app.route('/api/up', methods=['POST'])
|
@app.route('/api/up', methods=['POST'])
|
||||||
def upvote():
|
def upvote():
|
||||||
try:
|
try:
|
||||||
@@ -301,17 +331,14 @@ def upvote():
|
|||||||
item_id = data['id']
|
item_id = data['id']
|
||||||
item_type = data['type']
|
item_type = data['type']
|
||||||
|
|
||||||
item = None
|
|
||||||
if item_type == 'submission':
|
if item_type == 'submission':
|
||||||
item = db.session.get(Submission, item_id)
|
item = db.session.get(Submission, item_id)
|
||||||
elif item_type == 'comment':
|
if not item:
|
||||||
item = db.session.get(Comment, item_id)
|
return jsonify({"code": 2002, "data": "对象不存在"})
|
||||||
|
item.upvotes += 1
|
||||||
if not item:
|
db.session.commit()
|
||||||
return jsonify({"code": 2004, "data": "对象不存在"})
|
else:
|
||||||
|
return jsonify({"code": 2000, "data": "参数错误"})
|
||||||
item.upvotes += 1
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
return jsonify({"code": 1000, "data": ""})
|
return jsonify({"code": 1000, "data": ""})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -327,17 +354,14 @@ def downvote():
|
|||||||
item_id = data['id']
|
item_id = data['id']
|
||||||
item_type = data['type']
|
item_type = data['type']
|
||||||
|
|
||||||
item = None
|
|
||||||
if item_type == 'submission':
|
if item_type == 'submission':
|
||||||
item = db.session.get(Submission, item_id)
|
item = db.session.get(Submission, item_id)
|
||||||
elif item_type == 'comment':
|
if not item:
|
||||||
item = db.session.get(Comment, item_id)
|
return jsonify({"code": 2002, "data": "对象不存在"})
|
||||||
|
item.downvotes += 1
|
||||||
if not item:
|
db.session.commit()
|
||||||
return jsonify({"code": 2004, "data": "对象不存在"})
|
else:
|
||||||
|
return jsonify({"code": 2000, "data": "参数错误"})
|
||||||
item.downvotes += 1
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
return jsonify({"code": 1000, "data": ""})
|
return jsonify({"code": 1000, "data": ""})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
| 2001 | 失败。未初始化。不应该在成功初始化后继续使用该code。 |
|
| 2001 | 失败。未初始化。不应该在成功初始化后继续使用该code。 |
|
||||||
| 2002 | 失败。数据不存在。 |
|
| 2002 | 失败。数据不存在。 |
|
||||||
| 2003 | 失败。服务器内部错误。 |
|
| 2003 | 失败。服务器内部错误。 |
|
||||||
| 2004 | 失败。试图请求不存在的资源或使用不存在的Identity。 |
|
| 2004 | 失败。试图使用不存在的Identity。 |
|
||||||
| 2005 | 失败。提交内容包含违禁词。 |
|
| 2005 | 失败。提交内容包含违禁词。 |
|
||||||
| 404 | api端点不存在。 |
|
| 404 | api端点不存在。 |
|
||||||
| | |
|
| | |
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<!--这里没有有用的东西哦(-->
|
<!--既然都看到这里了,有没有兴趣加入开发喵!-->
|
||||||
<!--不过既然都看到这里了,有没有兴趣加入开发喵!-->
|
|
||||||
<!--https://github.com/Sycamore-Whisper/v2-->
|
<!--https://github.com/Sycamore-Whisper/v2-->
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
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 { MainLayout } from './layouts/MainLayout';
|
||||||
import About from './components/About';
|
import About from './components/About';
|
||||||
import CreatePost from './components/CreatePost';
|
import CreatePost from './components/CreatePost';
|
||||||
@@ -11,6 +11,8 @@ import './App.css';
|
|||||||
|
|
||||||
const Home: React.FC = () => {
|
const Home: React.FC = () => {
|
||||||
const { refreshTrigger } = useLayout();
|
const { refreshTrigger } = useLayout();
|
||||||
|
const { toasterId } = useLayout();
|
||||||
|
const { dispatchToast } = useToastController(toasterId);
|
||||||
const [articles, setArticles] = useState<Article[]>([]);
|
const [articles, setArticles] = useState<Article[]>([]);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -81,6 +83,12 @@ const Home: React.FC = () => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
if (refreshing) {
|
if (refreshing) {
|
||||||
setRefreshing(false);
|
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;
|
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,
|
Warning24Regular,
|
||||||
} from '@fluentui/react-icons';
|
} from '@fluentui/react-icons';
|
||||||
import { useLayout } from '../context/LayoutContext';
|
import { useLayout } from '../context/LayoutContext';
|
||||||
|
import CommentSection from './CommentSection';
|
||||||
|
|
||||||
// 自定义 remark 插件,用于高亮 #tag
|
// 自定义 remark 插件,用于高亮 #tag
|
||||||
const remarkTagPlugin = () => {
|
const remarkTagPlugin = () => {
|
||||||
@@ -149,6 +150,11 @@ const useStyles = makeStyles({
|
|||||||
justifyItems: 'center',
|
justifyItems: 'center',
|
||||||
gap: '0 8px',
|
gap: '0 8px',
|
||||||
},
|
},
|
||||||
|
commentSection: {
|
||||||
|
marginTop: tokens.spacingVerticalM,
|
||||||
|
borderTop: `1px solid ${tokens.colorNeutralStroke1}`,
|
||||||
|
paddingTop: tokens.spacingVerticalM,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface PostCardProps {
|
interface PostCardProps {
|
||||||
@@ -171,6 +177,7 @@ const PostCard = ({
|
|||||||
const { dispatchToast } = useToastController(toasterId);
|
const { dispatchToast } = useToastController(toasterId);
|
||||||
const [votes, setVotes] = React.useState({ upvotes, downvotes });
|
const [votes, setVotes] = React.useState({ upvotes, downvotes });
|
||||||
const [hasVoted, setHasVoted] = React.useState(false);
|
const [hasVoted, setHasVoted] = React.useState(false);
|
||||||
|
const [showComments, setShowComments] = React.useState(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setVotes({ upvotes, downvotes });
|
setVotes({ upvotes, downvotes });
|
||||||
@@ -267,7 +274,7 @@ const PostCard = ({
|
|||||||
<Button
|
<Button
|
||||||
icon={<Comment24Regular />}
|
icon={<Comment24Regular />}
|
||||||
appearance="transparent"
|
appearance="transparent"
|
||||||
onClick={() => {}}
|
onClick={() => setShowComments(!showComments)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
icon={<Warning24Regular />}
|
icon={<Warning24Regular />}
|
||||||
@@ -276,6 +283,11 @@ const PostCard = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
|
{showComments && (
|
||||||
|
<div className={styles.commentSection}>
|
||||||
|
<CommentSection postId={id} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user