Add report flow and post_info by id

This commit is contained in:
LeonspaceX
2026-01-27 13:19:18 +08:00
parent 72204c74b0
commit 0bce1d49da
7 changed files with 228 additions and 4 deletions

View File

@@ -67,6 +67,16 @@ class Comment(db.Model):
created_at = db.Column(db.DateTime, default=lambda: datetime.now()) created_at = db.Column(db.DateTime, default=lambda: datetime.now())
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 Report(db.Model):
__tablename__ = 'reports'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
submission_id = db.Column(db.Integer, nullable=False)
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text, nullable=False)
identity_token = db.Column(db.String(36), nullable=True)
status = db.Column(db.String(20), default='Pending')
created_at = db.Column(db.DateTime, default=lambda: datetime.now())
class Hashtag(db.Model): class Hashtag(db.Model):
__tablename__ = 'hashtags' __tablename__ = 'hashtags'
id = db.Column(db.Integer, primary_key=True, autoincrement=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
@@ -329,6 +339,42 @@ 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/report', methods=['POST'])
def submit_report():
try:
data = request.get_json()
if not data or 'id' not in data or 'title' not in data or 'content' not in data:
return jsonify({"code": 2000, "data": "参数错误"})
submission_id = data.get('id')
title = data.get('title')
content = data.get('content')
submission = db.session.get(Submission, submission_id)
if not submission:
return jsonify({"code": 2002, "data": "投稿不存在"})
identity_token = data.get('identity')
if identity_token:
if not Identity.query.filter_by(token=identity_token).first():
return jsonify({"code": 2004, "data": "无效的Identity Token"})
else:
identity_token = None
report = Report(
submission_id=submission_id,
title=str(title).strip() if title is not None else '',
content=str(content).strip() if content is not None else '',
identity_token=identity_token,
status='Pending',
)
db.session.add(report)
db.session.commit()
return jsonify({"code": 1001, "data": {"id": report.id}})
except Exception as e:
return jsonify({"code": 2003, "data": f"投诉失败: {str(e)}"})
@app.route('/api/get/comment', methods=['GET']) @app.route('/api/get/comment', methods=['GET'])
def get_comments(): def get_comments():
try: try:
@@ -511,6 +557,30 @@ def get_posts_info():
}) })
except Exception as e: except Exception as e:
return jsonify({"code": 2003, "data": str(e)}) return jsonify({"code": 2003, "data": str(e)})
@app.route('/api/post_info', methods=['GET'])
def get_post_info():
try:
post_id = request.args.get("id", type=int)
if not post_id:
return jsonify({"code": 2000, "data": "参数错误"})
submission = Submission.query.filter_by(id=post_id, status='Pass').first()
if not submission:
return jsonify({"code": 2002, "data": "投稿不存在"})
data = {
"id": submission.id,
"content": submission.content,
"upvotes": submission.upvotes,
"downvotes": submission.downvotes,
"created_at": submission.created_at.isoformat() if submission.created_at else None,
"comment_count": len(submission.comments),
}
return jsonify({"code": 1000, "data": data})
except Exception as e:
return jsonify({"code": 2003, "data": str(e)})
# --- 彩蛋 --- # --- 彩蛋 ---
@app.route('/api/teapot', methods=['GET']) @app.route('/api/teapot', methods=['GET'])

View File

@@ -299,6 +299,37 @@ export const postComment = async (commentData: PostCommentRequest): Promise<Post
} }
}; };
export interface ReportPostResponse {
id: number;
}
export const reportPost = async (reportData: { id: number; title: string; content: string }): Promise<ReportPostResponse> => {
try {
const identity = await get_id_token();
const response = await fetch('/api/report', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...reportData,
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) };
}
throw new Error(json.data || 'Report failed');
} catch (error) {
console.error('Failed to report post:', error);
throw error;
}
};
export const uploadImage = async (file: File): Promise<string> => { export const uploadImage = async (file: File): Promise<string> => {
try { try {
const identity_token = await get_id_token(); const identity_token = await get_id_token();

View File

@@ -139,7 +139,7 @@ interface CommentSectionProps {
const CommentSection: React.FC<CommentSectionProps> = ({ postId }) => { const CommentSection: React.FC<CommentSectionProps> = ({ postId }) => {
const styles = useStyles(); const styles = useStyles();
const { toasterId } = useLayout(); const { toasterId, triggerStaticsRefresh } = useLayout();
const { dispatchToast } = useToastController(toasterId); const { dispatchToast } = useToastController(toasterId);
const [comments, setComments] = useState<CommentType[]>([]); const [comments, setComments] = useState<CommentType[]>([]);
const [content, setContent] = useState(''); const [content, setContent] = useState('');
@@ -214,6 +214,7 @@ useEffect(() => {
setContent(''); setContent('');
if (replyTo) setReplyTo(null); if (replyTo) setReplyTo(null);
fetchComments(1, false); fetchComments(1, false);
triggerStaticsRefresh();
} catch (error: any) { } catch (error: any) {
dispatchToast( dispatchToast(
<Toast> <Toast>

View File

@@ -21,6 +21,7 @@ import {
} from '@fluentui/react-icons'; } from '@fluentui/react-icons';
import { useLayout } from '../context/LayoutContext'; import { useLayout } from '../context/LayoutContext';
import CommentSection from './CommentSection'; import CommentSection from './CommentSection';
import ReportPost from './ReportPost';
// 自定义 remark 插件,用于高亮 #tag // 自定义 remark 插件,用于高亮 #tag
const remarkTagPlugin = () => { const remarkTagPlugin = () => {
@@ -150,6 +151,19 @@ const useStyles = makeStyles({
justifyItems: 'center', justifyItems: 'center',
gap: '0 8px', gap: '0 8px',
}, },
modalOverlay: {
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
backdropFilter: 'blur(5px)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 999,
},
commentSection: { commentSection: {
marginTop: tokens.spacingVerticalM, marginTop: tokens.spacingVerticalM,
borderTop: `1px solid ${tokens.colorNeutralStroke1}`, borderTop: `1px solid ${tokens.colorNeutralStroke1}`,
@@ -178,6 +192,7 @@ const PostCard = ({
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); const [showComments, setShowComments] = React.useState(false);
const [showReportModal, setShowReportModal] = React.useState(false);
React.useEffect(() => { React.useEffect(() => {
setVotes({ upvotes, downvotes }); setVotes({ upvotes, downvotes });
@@ -279,7 +294,7 @@ const PostCard = ({
<Button <Button
icon={<Warning24Regular />} icon={<Warning24Regular />}
appearance="transparent" appearance="transparent"
onClick={() => {}} onClick={() => setShowReportModal(true)}
/> />
</div> </div>
</CardFooter> </CardFooter>
@@ -288,6 +303,11 @@ const PostCard = ({
<CommentSection postId={id} /> <CommentSection postId={id} />
</div> </div>
)} )}
{showReportModal && (
<div className={styles.modalOverlay}>
<ReportPost postId={id} onClose={() => setShowReportModal(false)} />
</div>
)}
</Card> </Card>
); );
}; };

View File

@@ -0,0 +1,92 @@
import { makeStyles, Button, Input, Textarea, tokens, useToastController, Toast, ToastTitle } from '@fluentui/react-components';
import { Dismiss24Regular } from '@fluentui/react-icons';
import React from 'react';
import { reportPost } from '../api';
import { useLayout } from '../context/LayoutContext';
const useStyles = makeStyles({
modalContent: {
backgroundColor: tokens.colorNeutralBackground1,
padding: tokens.spacingHorizontalXXL,
borderRadius: tokens.borderRadiusXLarge,
boxShadow: tokens.shadow64,
display: 'flex',
flexDirection: 'column',
gap: tokens.spacingVerticalM,
width: '400px',
position: 'relative',
},
closeButton: {
position: 'absolute',
top: tokens.spacingVerticalS,
right: tokens.spacingHorizontalS,
},
title: {
fontSize: tokens.fontSizeBase500,
fontWeight: tokens.fontWeightSemibold,
marginBottom: tokens.spacingVerticalS,
},
});
interface ReportPostProps {
onClose: () => void;
postId: number;
}
const ReportPost: React.FC<ReportPostProps> = ({ onClose, postId }) => {
const styles = useStyles();
const { toasterId } = useLayout();
const { dispatchToast } = useToastController(toasterId);
const [title, setTitle] = React.useState('');
const [content, setContent] = React.useState('');
const handleSubmit = async () => {
try {
const response = await reportPost({ id: postId, title, content });
dispatchToast(
<Toast>
<ToastTitle>id={response.id}</ToastTitle>
</Toast>,
{ intent: 'success' }
);
onClose();
} catch (error) {
console.error('Failed to report post:', error);
const message = error instanceof Error ? `投诉失败:${error.message}` : '投诉失败,请稍后重试';
dispatchToast(
<Toast>
<ToastTitle>{message}</ToastTitle>
</Toast>,
{ intent: 'error' }
);
}
};
return (
<div className={styles.modalContent}>
<Button
icon={<Dismiss24Regular />}
appearance="transparent"
className={styles.closeButton}
onClick={onClose}
/>
<h2 className={styles.title}></h2>
<Input
placeholder="简述投诉类型"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<Textarea
placeholder="投诉具体内容"
value={content}
onChange={(e) => setContent(e.target.value)}
rows={5}
/>
<Button appearance="primary" onClick={handleSubmit}>
</Button>
</div>
);
};
export default ReportPost;

View File

@@ -76,7 +76,7 @@ const useStyles = makeStyles({
const StatusDisplay: React.FC = () => { const StatusDisplay: React.FC = () => {
const styles = useStyles(); const styles = useStyles();
const { toasterId, refreshTrigger } = useLayout(); const { toasterId, refreshTrigger, staticsRefreshTrigger } = useLayout();
const { dispatchToast } = useToastController(toasterId); const { dispatchToast } = useToastController(toasterId);
const [isApiOnline, setIsApiOnline] = useState<boolean | null>(null); const [isApiOnline, setIsApiOnline] = useState<boolean | null>(null);
const [statics, setStatics] = useState<StaticsData | null>(null); const [statics, setStatics] = useState<StaticsData | null>(null);
@@ -128,6 +128,12 @@ const StatusDisplay: React.FC = () => {
} }
}, [refreshTrigger, refreshStatics]); }, [refreshTrigger, refreshStatics]);
useEffect(() => {
if (staticsRefreshTrigger > 0) {
refreshStatics(false);
}
}, [staticsRefreshTrigger, refreshStatics]);
return ( return (
<div className={styles.container}> <div className={styles.container}>
<Card className={styles.card}> <Card className={styles.card}>

View File

@@ -12,6 +12,8 @@ interface LayoutContextType {
toasterId: string; toasterId: string;
refreshTrigger: number; refreshTrigger: number;
triggerRefresh: () => void; triggerRefresh: () => void;
staticsRefreshTrigger: number;
triggerStaticsRefresh: () => void;
} }
const LayoutContext = createContext<LayoutContextType | undefined>(undefined); const LayoutContext = createContext<LayoutContextType | undefined>(undefined);
@@ -21,6 +23,7 @@ export const LayoutProvider: React.FC<{ children: React.ReactNode; toasterId: st
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const [isDarkMode, setIsDarkMode] = useState(false); const [isDarkMode, setIsDarkMode] = useState(false);
const [refreshTrigger, setRefreshTrigger] = useState(0); const [refreshTrigger, setRefreshTrigger] = useState(0);
const [staticsRefreshTrigger, setStaticsRefreshTrigger] = useState(0);
const { dispatchToast } = useToastController(toasterId); const { dispatchToast } = useToastController(toasterId);
useEffect(() => { useEffect(() => {
@@ -51,9 +54,10 @@ export const LayoutProvider: React.FC<{ children: React.ReactNode; toasterId: st
const toggleSidebar = () => setIsSidebarCollapsed(prev => !prev); const toggleSidebar = () => setIsSidebarCollapsed(prev => !prev);
const toggleTheme = () => setIsDarkMode(prev => !prev); const toggleTheme = () => setIsDarkMode(prev => !prev);
const triggerRefresh = () => setRefreshTrigger(prev => prev + 1); const triggerRefresh = () => setRefreshTrigger(prev => prev + 1);
const triggerStaticsRefresh = () => setStaticsRefreshTrigger(prev => prev + 1);
return ( return (
<LayoutContext.Provider value={{ settings, isSidebarCollapsed, toggleSidebar, isDarkMode, toggleTheme, toasterId, refreshTrigger, triggerRefresh }}> <LayoutContext.Provider value={{ settings, isSidebarCollapsed, toggleSidebar, isDarkMode, toggleTheme, toasterId, refreshTrigger, triggerRefresh, staticsRefreshTrigger, triggerStaticsRefresh }}>
{children} {children}
</LayoutContext.Provider> </LayoutContext.Provider>
); );