From b075495540f73d57b0e3b25ee4ef20878eb5a333 Mon Sep 17 00:00:00 2001 From: freefire Date: Thu, 6 Aug 2026 01:06:02 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=AF=84=E8=AE=BA=E7=82=B9?= =?UTF-8?q?=E8=B5=9E=E4=B8=8E=E4=B8=BE=E6=8A=A5=EF=BC=8C=E5=B9=B6=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E5=B8=96=E5=AD=90/=E8=AF=84=E8=AE=BA=E7=9A=84?= =?UTF-8?q?=E4=B8=BE=E6=8A=A5=E5=85=A5=E5=8F=A3=E4=BA=A4=E4=BA=92=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- frontend/src/api/client.ts | 7 +- frontend/src/api/types.ts | 6 +- frontend/src/components/CommentThreadList.tsx | 170 +++++++++++++++++- frontend/src/pages/PostDetailPage.tsx | 57 ++++-- frontend/src/pages/admin/AdminReportsPage.tsx | 137 +++++++++----- frontend/src/styles/global.css | 62 ++++++- handler/handlers.go | 10 ++ handler/report.go | 19 ++ model/db.go | 2 +- model/models.go | 20 ++- router/router.go | 4 +- service/comment.go | 59 ++++++ service/report.go | 132 ++++++++++++-- 13 files changed, 595 insertions(+), 90 deletions(-) diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 4d417d0..cd3ee2b 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -107,7 +107,7 @@ export const api = { }>(`/api/admin/reports${qs ? `?${qs}` : ''}`); }, adminHandleReport: (id: number, body: { - action: 'dismiss' | 'resolve' | 'reject_post'; + action: 'dismiss' | 'resolve' | 'reject_post' | 'reject_comment'; handle_note?: string; reject_reason?: string; }) => @@ -333,11 +333,16 @@ export const api = { captcha: () => request<{ id: string; image: string }>('/api/captcha'), logout: () => request('/api/logout', { method: 'POST' }), like: (id: number) => request<{ liked: boolean; like_count: number }>(`/api/posts/${id}/like`, { method: 'POST' }), + likeComment: (id: number) => request<{ liked: boolean; like_count: number }>(`/api/comments/${id}/like`, { method: 'POST' }), favorite: (id: number) => request<{ favorited: boolean }>(`/api/posts/${id}/favorite`, { method: 'POST' }), reportPost: (id: number, body: { reason: ReportReason; detail?: string }) => request<{ message: string; report: PostReport }>(`/api/posts/${id}/report`, { method: 'POST', body: JSON.stringify(body), }), + reportComment: (id: number, body: { reason: ReportReason; detail?: string }) => + request<{ message: string; report: PostReport }>(`/api/comments/${id}/report`, { + method: 'POST', body: JSON.stringify(body), + }), messageConversations: (params?: { page?: number; size?: number }) => { const q = new URLSearchParams(); if (params?.page) q.set('page', String(params.page)); diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index c076eab..398a1ba 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -130,6 +130,8 @@ export interface Comment { is_private?: boolean; status?: 'pending' | 'published' | 'rejected' | string; content_hidden?: boolean; + like_count?: number; + liked?: boolean; created_at: string; updated_at?: string; user?: User; @@ -392,10 +394,11 @@ export interface MessageConversation { export type ReportReason = 'spam' | 'abuse' | 'illegal' | 'irrelevant' | 'other'; export type ReportStatus = 'pending' | 'resolved' | 'dismissed'; -/** 帖子举报 */ +/** 帖子/评论举报(有 comment_id 时为评论举报) */ export interface PostReport { id: number; post_id: number; + comment_id?: number; reporter_id: number; reason: ReportReason | string; detail: string; @@ -405,6 +408,7 @@ export interface PostReport { created_at: string; handled_at?: string; post?: PostItem; + comment?: Comment; reporter?: User; handler?: User; } diff --git a/frontend/src/components/CommentThreadList.tsx b/frontend/src/components/CommentThreadList.tsx index fb62dd5..1f7efb6 100644 --- a/frontend/src/components/CommentThreadList.tsx +++ b/frontend/src/components/CommentThreadList.tsx @@ -1,7 +1,11 @@ import { useState, useEffect } from 'react'; -import { Check, Clock, History, MessageSquare, X, Pencil, Trash2 } from 'lucide-react'; +import { + Check, Clock, History, MessageSquare, X, Pencil, Trash2, + ThumbsUp, MoreHorizontal, Flag, +} from 'lucide-react'; import type { ReactNode } from 'react'; -import type { Comment, User } from '../api/types'; +import type { Comment, ReportReason, User } from '../api/types'; +import { api } from '../api/client'; import CommentContent from './CommentContent'; import CommentRevisionDialog from './CommentRevisionDialog'; import { @@ -15,6 +19,22 @@ import { AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Button } from '@/components/ui/button'; +import { notify } from '@/lib/notify'; import { commentNick, commentInitial, @@ -24,9 +44,11 @@ import { type CommentNode, } from '../utils/comment'; import { isTimeDiffSignificant } from '../utils/content'; +import { REPORT_REASON_OPTIONS } from '../utils/report'; import { useForumLimits } from '../hooks/useForumLimits'; import { Tooltip } from './ui/Tooltip'; import UserLink from './UserLink'; +import { cn } from '@/lib/utils'; function isCommentAuthor(c: Comment, user?: User | null): boolean { return !!user && c.user_id > 0 && c.user_id === user.id; @@ -56,6 +78,8 @@ interface ItemProps { onSaveEdit: (comment: Comment, content: string) => Promise; onDelete: (comment: Comment) => Promise; onApprove?: (comment: Comment) => Promise; + onRequireLogin?: (actionLabel: string) => void; + onLikeUpdate?: (commentId: number, liked: boolean, likeCount: number) => void; renderReplyBox?: (comment: Comment) => ReactNode; } @@ -74,6 +98,8 @@ function CommentItem({ onSaveEdit, onDelete, onApprove, + onRequireLogin, + onLikeUpdate, renderReplyBox, }: ItemProps) { const { limits } = useForumLimits(); @@ -105,16 +131,29 @@ function CommentItem({ && (c.status === 'pending' || c.status === 'rejected') && !!onApprove; const showEdited = !hidden && !!c.updated_at && isTimeDiffSignificant(c.created_at, c.updated_at); + const canReport = !hidden && !isEditing && !isCommentAuthor(c, currentUser); const [editText, setEditText] = useState(c.content); const [saving, setSaving] = useState(false); const [deleting, setDeleting] = useState(false); const [approving, setApproving] = useState(false); + const [liking, setLiking] = useState(false); + const [liked, setLiked] = useState(!!c.liked); + const [likeCount, setLikeCount] = useState(c.like_count ?? 0); const [revOpen, setRevOpen] = useState(false); + const [reportOpen, setReportOpen] = useState(false); + const [reportReason, setReportReason] = useState('spam'); + const [reportDetail, setReportDetail] = useState(''); + const [reporting, setReporting] = useState(false); useEffect(() => { if (isEditing) setEditText(c.content); }, [isEditing, c.content, c.id]); + useEffect(() => { + setLiked(!!c.liked); + setLikeCount(c.like_count ?? 0); + }, [c.id, c.liked, c.like_count]); + const handleSave = async () => { const next = editText.trim(); if (!next) return; @@ -126,6 +165,51 @@ function CommentItem({ } }; + const handleLike = async () => { + if (!currentUser) { + onRequireLogin?.('点赞'); + return; + } + if (liking) return; + setLiking(true); + try { + const r = await api.likeComment(c.id); + setLiked(r.liked); + setLikeCount(r.like_count); + onLikeUpdate?.(c.id, r.liked, r.like_count); + } catch (e: unknown) { + notify.error(e instanceof Error ? e.message : '点赞失败'); + } finally { + setLiking(false); + } + }; + + const openReport = () => { + if (!currentUser) { + onRequireLogin?.('举报'); + return; + } + setReportReason('spam'); + setReportDetail(''); + setReportOpen(true); + }; + + const handleReport = async () => { + setReporting(true); + try { + const r = await api.reportComment(c.id, { + reason: reportReason, + detail: reportDetail.trim() || undefined, + }); + notify.success(r.message); + setReportOpen(false); + } catch (e: unknown) { + notify.error(e instanceof Error ? e.message : '举报失败'); + } finally { + setReporting(false); + } + }; + return (
{nick} )} - {!c.reply_to && ( - - #{c.floor} - + {!hidden && ( + )}
@@ -295,12 +387,70 @@ function CommentItem({ )} + {canReport && ( + + + + + + + + 举报 + + + + )} {isAdmin && ( )} + + + + 举报评论 + 请选择原因,管理员将尽快处理。 + +
+ +