From f5a7249969ef1ac2a489dcd18f659b662500cc7b Mon Sep 17 00:00:00 2001 From: kongkx Date: Sat, 11 Jul 2026 14:20:37 +0800 Subject: [PATCH] feat(moderation): resource-centric reported comments/events pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rework 内容审核 pages around reported resources instead of individual reports, matching the new backend endpoints: rows group all reports on one comment/event, with a slide-over listing report details and per-row actions 通过并屏蔽 (approve) / 驳回举报 (reject) / 撤销屏蔽 (revert). Filters: 待处理 / 已屏蔽 / 全部. Co-Authored-By: Claude Fable 5 --- src/api/modules/comments.ts | 12 +- src/api/modules/filex.ts | 13 +- src/api/types.gen.ts | 418 +++++++++++-------- src/api/types.ts | 44 +- src/components/layout/nav.ts | 4 +- src/features/reports/report-status-badge.tsx | 17 + src/features/reports/reports-sheet.tsx | 55 +++ src/routes/_authed/comment-reports.tsx | 168 ++++---- src/routes/_authed/event-reports.tsx | 182 ++++---- 9 files changed, 532 insertions(+), 381 deletions(-) create mode 100644 src/features/reports/reports-sheet.tsx diff --git a/src/api/modules/comments.ts b/src/api/modules/comments.ts index 1836b30..eefa619 100644 --- a/src/api/modules/comments.ts +++ b/src/api/modules/comments.ts @@ -1,14 +1,14 @@ import { api } from '@/api/client' -import type { ApiResponse, CommentReport, ListParams, PaginatedResponse } from '@/api/types' +import type { ApiResponse, ListParams, PaginatedResponse, ReportedComment } from '@/api/types' const BASE = '/api/admin/v1/comments' -export type CommentReportAction = 'hide' | 'delete' | 'dismiss' +export type ModerationAction = 'approve' | 'reject' | 'revert' -export function fetchCommentReports(params: ListParams) { - return api.get>(`${BASE}/reports`, params) +export function fetchReportedComments(params: ListParams) { + return api.get>(`${BASE}/reported`, params) } -export function handleCommentReport(id: number, action: CommentReportAction) { - return api.patch>(`${BASE}/reports/${id}`, { action }) +export function moderateReportedComment(id: number, action: ModerationAction) { + return api.patch>(`${BASE}/reported/${id}`, { action }) } diff --git a/src/api/modules/filex.ts b/src/api/modules/filex.ts index e9eff8e..49a66ac 100644 --- a/src/api/modules/filex.ts +++ b/src/api/modules/filex.ts @@ -1,14 +1,13 @@ import { api } from '@/api/client' -import type { ApiResponse, FileXEventReport, ListParams, PaginatedResponse } from '@/api/types' +import type { ApiResponse, ListParams, PaginatedResponse, ReportedEvent } from '@/api/types' +import type { ModerationAction } from '@/api/modules/comments' const BASE = '/api/admin/v1/file-x' -export type EventReportAction = 'hide' | 'delete' | 'resolve' | 'dismiss' - -export function fetchEventReports(params: ListParams) { - return api.get>(`${BASE}/event-reports`, params) +export function fetchReportedEvents(params: ListParams) { + return api.get>(`${BASE}/reported-events`, params) } -export function handleEventReport(id: number, action: EventReportAction) { - return api.patch>(`${BASE}/event-reports/${id}`, { action }) +export function moderateReportedEvent(id: number, action: ModerationAction) { + return api.patch>(`${BASE}/reported-events/${id}`, { action }) } diff --git a/src/api/types.gen.ts b/src/api/types.gen.ts index fc44d57..9e0e482 100644 --- a/src/api/types.gen.ts +++ b/src/api/types.gen.ts @@ -218,30 +218,33 @@ export interface paths { patch?: never; trace?: never; }; - "/api/admin/v1/comments/reports": { + "/api/admin/v1/comments/reported": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** [评论举报] 获取举报列表 */ + /** + * [评论审核] 被举报评论列表 + * @description 按被举报的评论分组列出举报,附带举报明细与屏蔽状态。 + */ get: { parameters: { query?: { /** - * @description optional Filter by status: open, resolved, dismissed. - * @example open + * @description 可选。按状态过滤: pending(有待处理举报), blocked(已屏蔽), all(默认)。 示例: pending + * @example architecto */ status?: string; /** - * @description optional Page number. - * @example 1 + * @description 可选。页码。 示例: 1 + * @example 16 */ page?: number; /** - * @description optional Page size (max 100). - * @example 20 + * @description 可选。每页数量,最大100。 示例: 20 + * @example 16 */ page_size?: number; }; @@ -253,10 +256,10 @@ export interface paths { content: { "application/json": { /** - * @example resolved + * @example pending * @enum {string|null} */ - status?: "open" | "resolved" | "dismissed" | null; + status?: "pending" | "blocked" | "all" | null; /** * @description Must be at least 1. * @example 16 @@ -282,54 +285,103 @@ export interface paths { /** * @example [ * { - * "id": 1, - * "comment_id": 10, - * "reason": "spam", - * "message": "Contains spam links", - * "status": "open", - * "created_at": "2025-12-01T10:10:00Z", - * "handled_at": null, - * "handled_by": null, - * "reporter": { - * "uid": "01HXXXX", - * "display_name": "Alice" + * "kind": "reported_comment", + * "id": 10, + * "body": "这条评论包含垃圾推广链接", + * "body_format": "markdown", + * "status": "active", + * "is_blocked": false, + * "blocked_at": null, + * "blocked_by_uid": null, + * "author": { + * "uid": "01HYYYY", + * "display_name": "Bob" * }, - * "comment": { - * "id": 10, - * "body": "Root comment" - * } + * "open_reports_count": 2, + * "latest_report_at": "2026-07-11T10:10:00Z", + * "reports": [ + * { + * "id": 1, + * "reason": "spam", + * "message": "含有垃圾推广链接", + * "status": "open", + * "reporter": { + * "uid": "01HXXXX", + * "display_name": "Alice" + * }, + * "created_at": "2026-07-11T10:10:00Z", + * "handled_at": null + * } + * ], + * "created_at": "2026-07-10T09:00:00Z" * } * ] */ data?: { - /** @example 1 */ - id?: number; + /** @example reported_comment */ + kind?: string; /** @example 10 */ - comment_id?: number; - /** @example spam */ - reason?: string; - /** @example Contains spam links */ - message?: string; - /** @example open */ + id?: number; + /** @example 这条评论包含垃圾推广链接 */ + body?: string; + /** @example markdown */ + body_format?: string; + /** @example active */ status?: string; - /** @example 2025-12-01T10:10:00Z */ - created_at?: string; + /** @example false */ + is_blocked?: boolean; /** @example null */ - handled_at?: string; + blocked_at?: string; /** @example null */ - handled_by?: string; - reporter?: { - /** @example 01HXXXX */ + blocked_by_uid?: string; + author?: { + /** @example 01HYYYY */ uid?: string; - /** @example Alice */ + /** @example Bob */ display_name?: string; }; - comment?: { - /** @example 10 */ + /** @example 2 */ + open_reports_count?: number; + /** @example 2026-07-11T10:10:00Z */ + latest_report_at?: string; + /** + * @example [ + * { + * "id": 1, + * "reason": "spam", + * "message": "含有垃圾推广链接", + * "status": "open", + * "reporter": { + * "uid": "01HXXXX", + * "display_name": "Alice" + * }, + * "created_at": "2026-07-11T10:10:00Z", + * "handled_at": null + * } + * ] + */ + reports?: { + /** @example 1 */ id?: number; - /** @example Root comment */ - body?: string; - }; + /** @example spam */ + reason?: string; + /** @example 含有垃圾推广链接 */ + message?: string; + /** @example open */ + status?: string; + reporter?: { + /** @example 01HXXXX */ + uid?: string; + /** @example Alice */ + display_name?: string; + }; + /** @example 2026-07-11T10:10:00Z */ + created_at?: string; + /** @example null */ + handled_at?: string; + }[]; + /** @example 2026-07-10T09:00:00Z */ + created_at?: string; }[]; pagination?: { /** @example 1 */ @@ -378,31 +430,6 @@ export interface paths { }; }; }; - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @example false */ - success?: boolean; - /** @example VALIDATION_ERROR */ - code?: string; - /** @example The given data was invalid. */ - message?: string; - data?: { - errors?: { - /** - * @example [ - * "The selected status is invalid." - * ] - */ - status?: string[]; - }; - }; - }; - }; - }; }; }; put?: never; @@ -413,21 +440,16 @@ export interface paths { patch?: never; trace?: never; }; - "/api/admin/v1/comments/reports/{report_id}": { + "/api/admin/v1/comments/reported/{comment}": { parameters: { query?: never; header?: never; path: { /** - * @description The ID of the report. - * @example 3 + * @description 评论 ID。 + * @example 10 */ - report_id: number; - /** - * @description Report ID. - * @example 1 - */ - report: number; + comment: number; }; cookie?: never; }; @@ -437,22 +459,22 @@ export interface paths { delete?: never; options?: never; head?: never; - /** [评论举报] 处理举报 */ + /** + * [评论审核] 处理被举报评论 + * @description approve: 举报成立,屏蔽评论并将其全部待处理举报标记已处理; + * reject: 举报不成立,驳回全部待处理举报; + * revert: 撤销屏蔽(仅对已屏蔽评论有效),举报记录保留。 + */ patch: { parameters: { query?: never; header?: never; path: { /** - * @description The ID of the report. - * @example 3 + * @description 评论 ID。 + * @example 10 */ - report_id: number; - /** - * @description Report ID. - * @example 1 - */ - report: number; + comment: number; }; cookie?: never; }; @@ -460,8 +482,8 @@ export interface paths { content: { "application/json": { /** - * @description Action to take: hide, delete, dismiss. - * @example hide + * @description 处理动作: approve, reject, revert。 + * @example approve */ action: string; }; @@ -477,16 +499,20 @@ export interface paths { /** @example true */ success?: boolean; data?: { - /** @example 1 */ + /** @example reported_comment */ + kind?: string; + /** @example 10 */ id?: number; - /** @example resolved */ - status?: string; - /** @example 2025-12-01T10:20:00Z */ - handled_at?: string; + /** @example true */ + is_blocked?: boolean; + /** @example 2026-07-11T10:20:00Z */ + blocked_at?: string; /** @example 01HXXXX */ - handled_by_uid?: string; + blocked_by_uid?: string; + /** @example 0 */ + open_reports_count?: number; }; - /** @example Report handled */ + /** @example Moderation applied */ message?: string; }; }; @@ -521,21 +547,6 @@ export interface paths { }; }; }; - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @example false */ - success?: boolean; - /** @example NOT_FOUND */ - code?: string; - /** @example 举报不存在 */ - message?: string; - }; - }; - }; 422: { headers: { [name: string]: unknown; @@ -552,7 +563,7 @@ export interface paths { errors?: { /** * @example [ - * "The selected action is invalid." + * "Comment is not blocked." * ] */ action?: string[]; @@ -881,11 +892,11 @@ export interface paths { content: { "application/json": { /** - * @example ios + * @example android * @enum {string|null} */ platform?: "ios" | "android" | "web" | "desktop" | null; - /** @example false */ + /** @example true */ is_online?: boolean | null; /** * @description Must be at least 1. @@ -1311,19 +1322,22 @@ export interface paths { patch?: never; trace?: never; }; - "/api/admin/v1/file-x/event-reports": { + "/api/admin/v1/file-x/reported-events": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** [事件举报] 获取举报列表 */ + /** + * [事件审核] 被举报事件列表 + * @description 按被举报的事件分组列出举报,附带举报明细与屏蔽状态。 + */ get: { parameters: { query?: { /** - * @description 可选。按状态过滤: open, resolved, dismissed。 示例: open + * @description 可选。按状态过滤: pending(有待处理举报), blocked(已屏蔽), all(默认)。 示例: pending * @example architecto */ status?: string; @@ -1346,10 +1360,10 @@ export interface paths { content: { "application/json": { /** - * @example open + * @example blocked * @enum {string|null} */ - status?: "open" | "resolved" | "dismissed" | null; + status?: "pending" | "blocked" | "all" | null; /** * @description Must be at least 1. * @example 16 @@ -1375,47 +1389,100 @@ export interface paths { /** * @example [ * { - * "id": 1, - * "kind": "filex_event_report", - * "event_id": 25, - * "reason": "spam", - * "message": "含有大量垃圾推广链接", - * "status": "open", - * "created_at": "2026-06-06T10:00:00Z", - * "handled_at": null, - * "reporter": { - * "uid": "01HXXXX", - * "display_name": "Alice" + * "kind": "reported_event", + * "id": 25, + * "title": "空调清洗上门服务记录", + * "visibility": "public", + * "is_blocked": false, + * "blocked_at": null, + * "blocked_by_uid": null, + * "author": { + * "uid": "01HYYYY", + * "display_name": "Bob" * }, - * "handled_by": null + * "open_reports_count": 2, + * "latest_report_at": "2026-07-11T10:10:00Z", + * "reports": [ + * { + * "id": 1, + * "reason": "spam", + * "message": "疑似虚假事迹", + * "status": "open", + * "reporter": { + * "uid": "01HXXXX", + * "display_name": "Alice" + * }, + * "created_at": "2026-07-11T10:10:00Z", + * "handled_at": null + * } + * ], + * "created_at": "2026-07-10T09:00:00Z" * } * ] */ data?: { - /** @example 1 */ - id?: number; - /** @example filex_event_report */ + /** @example reported_event */ kind?: string; /** @example 25 */ - event_id?: number; - /** @example spam */ - reason?: string; - /** @example 含有大量垃圾推广链接 */ - message?: string; - /** @example open */ - status?: string; - /** @example 2026-06-06T10:00:00Z */ - created_at?: string; + id?: number; + /** @example 空调清洗上门服务记录 */ + title?: string; + /** @example public */ + visibility?: string; + /** @example false */ + is_blocked?: boolean; /** @example null */ - handled_at?: string; - reporter?: { - /** @example 01HXXXX */ + blocked_at?: string; + /** @example null */ + blocked_by_uid?: string; + author?: { + /** @example 01HYYYY */ uid?: string; - /** @example Alice */ + /** @example Bob */ display_name?: string; }; - /** @example null */ - handled_by?: string; + /** @example 2 */ + open_reports_count?: number; + /** @example 2026-07-11T10:10:00Z */ + latest_report_at?: string; + /** + * @example [ + * { + * "id": 1, + * "reason": "spam", + * "message": "疑似虚假事迹", + * "status": "open", + * "reporter": { + * "uid": "01HXXXX", + * "display_name": "Alice" + * }, + * "created_at": "2026-07-11T10:10:00Z", + * "handled_at": null + * } + * ] + */ + reports?: { + /** @example 1 */ + id?: number; + /** @example spam */ + reason?: string; + /** @example 疑似虚假事迹 */ + message?: string; + /** @example open */ + status?: string; + reporter?: { + /** @example 01HXXXX */ + uid?: string; + /** @example Alice */ + display_name?: string; + }; + /** @example 2026-07-11T10:10:00Z */ + created_at?: string; + /** @example null */ + handled_at?: string; + }[]; + /** @example 2026-07-10T09:00:00Z */ + created_at?: string; }[]; pagination?: { /** @example 1 */ @@ -1474,18 +1541,16 @@ export interface paths { patch?: never; trace?: never; }; - "/api/admin/v1/file-x/event-reports/{report_id}": { + "/api/admin/v1/file-x/reported-events/{event}": { parameters: { query?: never; header?: never; path: { /** - * @description The ID of the report. - * @example 1 + * @description 事件 ID。 + * @example 25 */ - report_id: number; - /** @description Optional parameter. 必需。举报ID。 示例: 1 */ - report: number; + event: number; }; cookie?: never; }; @@ -1495,30 +1560,33 @@ export interface paths { delete?: never; options?: never; head?: never; - /** [事件举报] 处理举报 */ + /** + * [事件审核] 处理被举报事件 + * @description approve: 举报成立,屏蔽事件并将其全部待处理举报标记已处理; + * reject: 举报不成立,驳回全部待处理举报; + * revert: 撤销屏蔽(仅对已屏蔽事件有效),举报记录保留。 + */ patch: { parameters: { query?: never; header?: never; path: { /** - * @description The ID of the report. - * @example 1 + * @description 事件 ID。 + * @example 25 */ - report_id: number; - /** @description Optional parameter. 必需。举报ID。 示例: 1 */ - report: number; + event: number; }; cookie?: never; }; - requestBody?: { + requestBody: { content: { "application/json": { /** - * @description 必需。处理动作: hide, delete, resolve, dismiss。 示例: hide - * @example architecto + * @description 处理动作: approve, reject, revert。 + * @example approve */ - action?: string; + action: string; }; }; }; @@ -1532,14 +1600,18 @@ export interface paths { /** @example true */ success?: boolean; data?: { - /** @example 1 */ + /** @example reported_event */ + kind?: string; + /** @example 25 */ id?: number; - /** @example resolved */ - status?: string; - /** @example 2026-06-06T10:20:00Z */ - handled_at?: string; + /** @example true */ + is_blocked?: boolean; + /** @example 2026-07-11T10:20:00Z */ + blocked_at?: string; /** @example 01HXXXX */ - handled_by_uid?: string; + blocked_by_uid?: string; + /** @example 0 */ + open_reports_count?: number; }; /** @example 举报已处理 */ message?: string; @@ -1607,7 +1679,7 @@ export interface paths { errors?: { /** * @example [ - * "The selected action is invalid." + * "Event is not blocked." * ] */ action?: string[]; diff --git a/src/api/types.ts b/src/api/types.ts index 88eb40b..6ee0b50 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -292,36 +292,38 @@ export interface ReportUser { display_name?: string | null } -export interface CommentReport { +export interface ResourceReport { id: number - comment_id: number reason: string message?: string | null status: ReportStatus + reporter?: ReportUser | null created_at: string handled_at?: string | null - handled_by?: ReportUser | null - reporter?: ReportUser | null - comment?: { - id: number - body?: string | null - status?: string - author?: ReportUser | null - } | null } -export interface FileXEventReport { - id: number +interface ReportedResourceBase { kind?: string - event_id: number - reason: string - message?: string | null - status: ReportStatus - created_at: string - handled_at?: string | null - handled_by?: ReportUser | null - reporter?: ReportUser | null - event?: { id: number; title?: string | null } | null + id: number + is_blocked: boolean + blocked_at?: string | null + blocked_by_uid?: string | null + author?: ReportUser | null + open_reports_count: number + latest_report_at?: string | null + reports: ResourceReport[] + created_at?: string +} + +export interface ReportedComment extends ReportedResourceBase { + body?: string | null + body_format?: string + status?: string +} + +export interface ReportedEvent extends ReportedResourceBase { + title?: string | null + visibility?: string } export type DimzouTranslationStatus = 'draft' | 'awaiting_publish' | 'published' diff --git a/src/components/layout/nav.ts b/src/components/layout/nav.ts index 7969b64..670c1c7 100644 --- a/src/components/layout/nav.ts +++ b/src/components/layout/nav.ts @@ -58,8 +58,8 @@ export const NAV_GROUPS: NavGroup[] = [ { label: '内容审核', items: [ - { label: '评论举报', to: '/comment-reports', icon: MessageSquareWarning }, - { label: '事件举报', to: '/event-reports', icon: Flag }, + { label: '被举报评论', to: '/comment-reports', icon: MessageSquareWarning }, + { label: '被举报事件', to: '/event-reports', icon: Flag }, { label: 'Dimzou 翻译', to: '/dimzou-translations', icon: Languages }, ], }, diff --git a/src/features/reports/report-status-badge.tsx b/src/features/reports/report-status-badge.tsx index 805957c..ba53609 100644 --- a/src/features/reports/report-status-badge.tsx +++ b/src/features/reports/report-status-badge.tsx @@ -1,6 +1,23 @@ import type { ReportStatus } from '@/api/types' import { Badge } from '@/components/ui/badge' +/** State of a reported resource, derived from is_blocked + open reports. */ +export function ResourceStateBadge({ + isBlocked, + openReports, +}: { + isBlocked: boolean + openReports: number +}) { + if (isBlocked) { + return 已屏蔽 + } + if (openReports > 0) { + return 待处理 + } + return 正常 +} + export function ReportStatusBadge({ status }: { status: ReportStatus }) { if (status === 'open') { return 待处理 diff --git a/src/features/reports/reports-sheet.tsx b/src/features/reports/reports-sheet.tsx new file mode 100644 index 0000000..db7d8ce --- /dev/null +++ b/src/features/reports/reports-sheet.tsx @@ -0,0 +1,55 @@ +import type { ResourceReport } from '@/api/types' +import { ReportStatusBadge } from '@/features/reports/report-status-badge' +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet' + +interface ReportsSheetProps { + open: boolean + onOpenChange: (open: boolean) => void + title: string + /** resource summary shown under the title */ + description?: string + reports: ResourceReport[] +} + +/** Slide-over listing the individual reports filed against one resource. */ +export function ReportsSheet({ open, onOpenChange, title, description, reports }: ReportsSheetProps) { + return ( + + + + {title} + {description && {description}} + +
+ {reports.map((report) => ( +
+
+ {report.reason} + +
+ {report.message ? ( +

{report.message}

+ ) : null} +
+ + {report.reporter?.display_name ?? '—'} + {report.reporter?.uid ?? ''} + + {new Date(report.created_at).toLocaleString('zh-CN')} +
+
+ ))} + {reports.length === 0 && ( +

没有举报记录

+ )} +
+
+
+ ) +} diff --git a/src/routes/_authed/comment-reports.tsx b/src/routes/_authed/comment-reports.tsx index 625a907..bfabcb9 100644 --- a/src/routes/_authed/comment-reports.tsx +++ b/src/routes/_authed/comment-reports.tsx @@ -2,15 +2,16 @@ import { createFileRoute } from '@tanstack/react-router' import { useState } from 'react' import type { ColumnDef } from '@tanstack/react-table' import { toast } from 'sonner' -import type { CommentReport } from '@/api/types' -import type { CommentReportAction } from '@/api/modules/comments' -import { fetchCommentReports, handleCommentReport } from '@/api/modules/comments' +import type { ReportedComment } from '@/api/types' +import type { ModerationAction } from '@/api/modules/comments' +import { fetchReportedComments, moderateReportedComment } from '@/api/modules/comments' import { handleApiError } from '@/lib/errors' import { useListPage } from '@/lib/list-page' import { DataTable } from '@/components/data-table/data-table' import { PageHeader } from '@/components/page-header' import { ConfirmDialog } from '@/components/confirm-dialog' -import { ReportStatusBadge } from '@/features/reports/report-status-badge' +import { ResourceStateBadge } from '@/features/reports/report-status-badge' +import { ReportsSheet } from '@/features/reports/reports-sheet' import { Button } from '@/components/ui/button' import { Select, @@ -21,20 +22,21 @@ import { } from '@/components/ui/select' export const Route = createFileRoute('/_authed/comment-reports')({ - component: CommentReportsPage, + component: ReportedCommentsPage, }) -function CommentReportsPage() { - const [status, setStatus] = useState('open') +function ReportedCommentsPage() { + const [status, setStatus] = useState('pending') + const [detail, setDetail] = useState(null) const list = useListPage( - 'comment-reports', - fetchCommentReports, + 'reported-comments', + fetchReportedComments, status === 'all' ? {} : { status }, ) - const act = async (row: CommentReport, action: CommentReportAction, done: string) => { + const act = async (row: ReportedComment, action: ModerationAction, done: string) => { try { - await handleCommentReport(row.id, action) + await moderateReportedComment(row.id, action) toast.success(done) void list.query.refetch() } catch (error) { @@ -42,111 +44,108 @@ function CommentReportsPage() { } } - const columns: ColumnDef[] = [ + const columns: ColumnDef[] = [ { - header: '举报原因', + header: '评论', cell: ({ row }) => ( -
-
{row.original.reason}
- {row.original.message ? ( -
- {row.original.message} -
- ) : null} -
- ), - }, - { - header: '被举报评论', - cell: ({ row }) => ( -
-
- {row.original.comment?.body ?? `#${row.original.comment_id}(已不存在)`} +
+
+ {row.original.body ?? '—'} +
+
+ #{row.original.id} + {row.original.author?.display_name ?? '—'}
- #{row.original.comment_id}
), }, { - header: '举报人', + header: '举报', cell: ({ row }) => ( -
-
{row.original.reporter?.display_name ?? '—'}
- - {row.original.reporter?.uid ?? ''} - -
+ ), }, { header: '状态', - cell: ({ row }) => , + cell: ({ row }) => ( + + ), }, { - header: '举报时间', + header: '最近举报', cell: ({ row }) => - row.original.created_at ? new Date(row.original.created_at).toLocaleString('zh-CN') : '—', + row.original.latest_report_at + ? new Date(row.original.latest_report_at).toLocaleString('zh-CN') + : '—', }, { header: '操作', - cell: ({ row }) => - row.original.status === 'open' ? ( -
- 隐藏评论} - title={`隐藏评论 #${row.original.comment_id}?`} - description="评论将对用户不可见,举报标记为已处理。" - onConfirm={() => act(row.original, 'hide', '已隐藏评论')} - /> + cell: ({ row }) => ( +
+ {row.original.is_blocked ? ( - 删除评论 + 撤销屏蔽 } - title={`删除评论 #${row.original.comment_id}?`} - description="评论将被删除,举报标记为已处理。" - confirmLabel="删除" - destructive - onConfirm={() => act(row.original, 'delete', '已删除评论')} + title={`撤销屏蔽评论 #${row.original.id}?`} + description="评论将重新对用户可见,举报记录保留。" + onConfirm={() => act(row.original, 'revert', '已撤销屏蔽')} /> - - 驳回 - - } - title="驳回该举报?" - description="评论保持原样,举报标记为已驳回。" - confirmLabel="驳回" - onConfirm={() => act(row.original, 'dismiss', '已驳回举报')} - /> -
- ) : ( - - {row.original.handled_at - ? new Date(row.original.handled_at).toLocaleString('zh-CN') - : '—'} - - ), + ) : ( + row.original.open_reports_count > 0 && ( + <> + 通过并屏蔽} + title={`屏蔽评论 #${row.original.id}?`} + description="举报成立:评论将对用户不可见(可撤销),其全部待处理举报标记为已处理。" + destructive + onConfirm={() => act(row.original, 'approve', '已屏蔽评论')} + /> + + 驳回举报 + + } + title="驳回该评论的全部待处理举报?" + description="举报不成立:评论保持原样。" + confirmLabel="驳回" + onConfirm={() => act(row.original, 'reject', '已驳回举报')} + /> + + ) + )} +
+ ), }, ] return (
- 全部状态 - 待处理 - 已处理 - 已驳回 + 待处理 + 已屏蔽 + 全部 } @@ -156,10 +155,17 @@ function CommentReportsPage() { data={list.rows} pagination={list.pagination} loading={list.loading} - emptyText="没有相关举报" + emptyText="没有被举报的评论" onPageChange={list.setPage} onPageSizeChange={list.setPageSize} /> + !open && setDetail(null)} + title={`评论 #${detail?.id ?? ''} 的举报`} + description={detail?.body ?? undefined} + reports={detail?.reports ?? []} + />
) } diff --git a/src/routes/_authed/event-reports.tsx b/src/routes/_authed/event-reports.tsx index 3401eea..6dfa321 100644 --- a/src/routes/_authed/event-reports.tsx +++ b/src/routes/_authed/event-reports.tsx @@ -2,15 +2,17 @@ import { createFileRoute } from '@tanstack/react-router' import { useState } from 'react' import type { ColumnDef } from '@tanstack/react-table' import { toast } from 'sonner' -import type { FileXEventReport } from '@/api/types' -import type { EventReportAction } from '@/api/modules/filex' -import { fetchEventReports, handleEventReport } from '@/api/modules/filex' +import type { ReportedEvent } from '@/api/types' +import type { ModerationAction } from '@/api/modules/comments' +import { fetchReportedEvents, moderateReportedEvent } from '@/api/modules/filex' import { handleApiError } from '@/lib/errors' import { useListPage } from '@/lib/list-page' import { DataTable } from '@/components/data-table/data-table' import { PageHeader } from '@/components/page-header' import { ConfirmDialog } from '@/components/confirm-dialog' -import { ReportStatusBadge } from '@/features/reports/report-status-badge' +import { ResourceStateBadge } from '@/features/reports/report-status-badge' +import { ReportsSheet } from '@/features/reports/reports-sheet' +import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Select, @@ -21,20 +23,21 @@ import { } from '@/components/ui/select' export const Route = createFileRoute('/_authed/event-reports')({ - component: EventReportsPage, + component: ReportedEventsPage, }) -function EventReportsPage() { - const [status, setStatus] = useState('open') +function ReportedEventsPage() { + const [status, setStatus] = useState('pending') + const [detail, setDetail] = useState(null) const list = useListPage( - 'event-reports', - fetchEventReports, + 'reported-events', + fetchReportedEvents, status === 'all' ? {} : { status }, ) - const act = async (row: FileXEventReport, action: EventReportAction, done: string) => { + const act = async (row: ReportedEvent, action: ModerationAction, done: string) => { try { - await handleEventReport(row.id, action) + await moderateReportedEvent(row.id, action) toast.success(done) void list.query.refetch() } catch (error) { @@ -42,121 +45,112 @@ function EventReportsPage() { } } - const columns: ColumnDef[] = [ + const columns: ColumnDef[] = [ { - header: '举报原因', + header: '事件', cell: ({ row }) => ( -
-
{row.original.reason}
- {row.original.message ? ( -
- {row.original.message} -
- ) : null} -
- ), - }, - { - header: '被举报事件', - cell: ({ row }) => ( -
-
- {row.original.event?.title ?? `#${row.original.event_id}(已删除)`} +
+
+ {row.original.title ?? '—'} +
+
+ #{row.original.id} + {row.original.author?.display_name ?? '—'}
- #{row.original.event_id}
), }, { - header: '举报人', + header: '可见性', + cell: ({ row }) => {row.original.visibility ?? '—'}, + }, + { + header: '举报', cell: ({ row }) => ( -
-
{row.original.reporter?.display_name ?? '—'}
- - {row.original.reporter?.uid ?? ''} - -
+ ), }, { header: '状态', - cell: ({ row }) => , + cell: ({ row }) => ( + + ), }, { - header: '举报时间', + header: '最近举报', cell: ({ row }) => - row.original.created_at ? new Date(row.original.created_at).toLocaleString('zh-CN') : '—', + row.original.latest_report_at + ? new Date(row.original.latest_report_at).toLocaleString('zh-CN') + : '—', }, { header: '操作', - cell: ({ row }) => - row.original.status === 'open' ? ( -
- 隐藏事件} - title={`隐藏事件 #${row.original.event_id}?`} - description="事件将对用户不可见,举报标记为已处理。" - onConfirm={() => act(row.original, 'hide', '已隐藏事件')} - /> + cell: ({ row }) => ( +
+ {row.original.is_blocked ? ( - 删除事件 + 撤销屏蔽 } - title={`删除事件 #${row.original.event_id}?`} - description="事件将被删除,举报标记为已处理。" - confirmLabel="删除" - destructive - onConfirm={() => act(row.original, 'delete', '已删除事件')} + title={`撤销屏蔽事件「${row.original.title ?? row.original.id}」?`} + description="事件将重新对用户可见,举报记录保留。" + onConfirm={() => act(row.original, 'revert', '已撤销屏蔽')} /> - - 标记已处理 - - } - title="标记该举报为已处理?" - description="事件保持原样,仅将举报标记为已处理。" - onConfirm={() => act(row.original, 'resolve', '已标记处理')} - /> - - 驳回 - - } - title="驳回该举报?" - description="事件保持原样,举报标记为已驳回。" - confirmLabel="驳回" - onConfirm={() => act(row.original, 'dismiss', '已驳回举报')} - /> -
- ) : ( - - {row.original.handled_at - ? new Date(row.original.handled_at).toLocaleString('zh-CN') - : '—'} - - ), + ) : ( + row.original.open_reports_count > 0 && ( + <> + 通过并屏蔽} + title={`屏蔽事件「${row.original.title ?? row.original.id}」?`} + description="举报成立:事件将对用户不可见(可撤销),其全部待处理举报标记为已处理。" + destructive + onConfirm={() => act(row.original, 'approve', '已屏蔽事件')} + /> + + 驳回举报 + + } + title="驳回该事件的全部待处理举报?" + description="举报不成立:事件保持原样。" + confirmLabel="驳回" + onConfirm={() => act(row.original, 'reject', '已驳回举报')} + /> + + ) + )} +
+ ), }, ] return (
- 全部状态 - 待处理 - 已处理 - 已驳回 + 待处理 + 已屏蔽 + 全部 } @@ -166,10 +160,16 @@ function EventReportsPage() { data={list.rows} pagination={list.pagination} loading={list.loading} - emptyText="没有相关举报" + emptyText="没有被举报的事件" onPageChange={list.setPage} onPageSizeChange={list.setPageSize} /> + !open && setDetail(null)} + title={`事件「${detail?.title ?? detail?.id ?? ''}」的举报`} + reports={detail?.reports ?? []} + />
) }