Add report flow and post_info by id
This commit is contained in:
70
back/main.py
70
back/main.py
@@ -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'])
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
92
front/src/components/ReportPost.tsx
Normal file
92
front/src/components/ReportPost.tsx
Normal 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;
|
||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user