From c84fc90c3d483025de42308754c27683d9449e43 Mon Sep 17 00:00:00 2001 From: kongkx Date: Wed, 9 Sep 2026 22:36:22 +0800 Subject: [PATCH] feat(filex): add admin event monitoring --- src/api/modules/filex.ts | 17 ++- src/api/types.gen.ts | 41 +++---- src/api/types.ts | 30 +++++ src/components/layout/nav.ts | 2 + src/routeTree.gen.ts | 21 ++++ src/routes/_authed/filex-events.tsx | 163 ++++++++++++++++++++++++++++ 6 files changed, 254 insertions(+), 20 deletions(-) create mode 100644 src/routes/_authed/filex-events.tsx diff --git a/src/api/modules/filex.ts b/src/api/modules/filex.ts index 49a66ac..603d4c4 100644 --- a/src/api/modules/filex.ts +++ b/src/api/modules/filex.ts @@ -1,5 +1,12 @@ import { api } from '@/api/client' -import type { ApiResponse, ListParams, PaginatedResponse, ReportedEvent } from '@/api/types' +import type { + ApiResponse, + FileXAdminEvent, + FileXAdminEventDetail, + ListParams, + PaginatedResponse, + ReportedEvent, +} from '@/api/types' import type { ModerationAction } from '@/api/modules/comments' const BASE = '/api/admin/v1/file-x' @@ -11,3 +18,11 @@ export function fetchReportedEvents(params: ListParams) { export function moderateReportedEvent(id: number, action: ModerationAction) { return api.patch>(`${BASE}/reported-events/${id}`, { action }) } + +export function fetchFileXEvents(params: ListParams) { + return api.get>(`${BASE}/events`, params) +} + +export function fetchFileXEvent(id: number) { + return api.get>(`${BASE}/events/${id}`) +} diff --git a/src/api/types.gen.ts b/src/api/types.gen.ts index c79d791..d412541 100644 --- a/src/api/types.gen.ts +++ b/src/api/types.gen.ts @@ -50,7 +50,7 @@ export interface paths { content: { "application/json": { /** - * @example all + * @example active * @enum {string|null} */ state?: "active" | "archived" | "trashed" | "all" | null; @@ -346,7 +346,7 @@ export interface paths { content: { "application/json": { /** - * @example group_only + * @example public * @enum {string|null} */ visibility?: "public" | "group_only" | null; @@ -539,7 +539,7 @@ export interface paths { */ language_code?: string; /** - * @example published + * @example awaiting_publish * @enum {string} */ status?: "draft" | "awaiting_publish" | "published"; @@ -4691,7 +4691,7 @@ export interface paths { content: { "application/json": { /** - * @example rejected + * @example ignored * @enum {string|null} */ status?: "sent" | "delivered" | "viewed" | "accepted" | "rejected" | "ignored" | "expired" | "taken_by_other" | null; @@ -4885,12 +4885,12 @@ export interface paths { content: { "application/json": { /** - * @example fulfilled + * @example started * @enum {string|null} */ status?: "created" | "confirmed" | "started" | "consumed" | "paid" | "fulfilled" | "canceled" | null; /** - * @example UNPAID + * @example PAID * @enum {string|null} */ payment_status?: "UNPAID" | "PAID" | null; @@ -5466,35 +5466,35 @@ export interface paths { content: { "application/json": { /** - * @example public + * @example friends * @enum {string|null} */ visibility?: "public" | "friends" | "private" | null; /** - * @example visible + * @example blocked * @enum {string|null} */ status?: "blocked" | "visible" | "all" | null; - /** @example architecto */ + /** @example null */ user_uid?: string | null; /** * @description Must not be greater than 100 characters. - * @example n + * @example b */ primary_topic?: string | null; /** * @description Must not be greater than 100 characters. - * @example g + * @example n */ q?: string | null; /** * @description Must be at least 1. - * @example 66 + * @example 67 */ page?: number | null; /** * @description Must be at least 1. Must not be greater than 100. - * @example 17 + * @example 16 */ page_size?: number | null; }; @@ -5528,7 +5528,8 @@ export interface paths { * "comments_count": 3, * "open_reports_count": 0, * "event_day": "2026-07-10", - * "created_at": "2026-07-10T09:00:00Z" + * "created_at": "2026-07-10T09:00:00Z", + * "deleted_at": null * } * ] */ @@ -5565,6 +5566,8 @@ export interface paths { event_day?: string; /** @example 2026-07-10T09:00:00Z */ created_at?: string; + /** @example null */ + deleted_at?: string; }[]; pagination?: { /** @example 1 */ @@ -5797,7 +5800,7 @@ export interface paths { content: { "application/json": { /** - * @example all + * @example blocked * @enum {string|null} */ status?: "pending" | "blocked" | "all" | null; @@ -7682,9 +7685,9 @@ export interface paths { * @example b */ q?: string | null; - /** @example true */ - status?: boolean | null; /** @example false */ + status?: boolean | null; + /** @example true */ is_robot?: boolean | null; /** * @description Must not be greater than 100 characters. @@ -9762,7 +9765,7 @@ export interface paths { content: { "application/json": { /** - * @example pending + * @example blocked * @enum {string|null} */ status?: "pending" | "blocked" | "all" | null; @@ -10129,7 +10132,7 @@ export interface paths { * @enum {string|null} */ platform?: "ios" | "android" | "web" | "desktop" | null; - /** @example true */ + /** @example false */ is_online?: boolean | null; /** * @description Must be at least 1. diff --git a/src/api/types.ts b/src/api/types.ts index 275e9e5..a7d814a 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -441,6 +441,36 @@ export interface ReportedEvent extends ReportedResourceBase { visibility?: string } +export interface FileXAdminEvent { + kind: 'filex_event' + id: number + title?: string | null + visibility: 'public' | 'friends' | 'private' + primary_topic?: string | null + is_blocked: boolean + blocked_at?: string | null + blocked_by_uid?: string | null + author?: ReportUser | null + likes_count: number + comments_count: number + open_reports_count: number + event_day?: string | null + created_at?: string | null + deleted_at?: string | null +} + +export interface FileXAdminEventDetail extends FileXAdminEvent { + content?: string | null + topics?: string[] | null + topic_confidence?: number | string | null + location_query?: string | null + location_name?: string | null + latitude?: number | null + longitude?: number | null + assets: unknown[] + updated_at?: string | null +} + export type DimzouTranslationStatus = 'draft' | 'awaiting_publish' | 'published' export interface DimzouTranslation { diff --git a/src/components/layout/nav.ts b/src/components/layout/nav.ts index 6d90c63..7780ac9 100644 --- a/src/components/layout/nav.ts +++ b/src/components/layout/nav.ts @@ -9,6 +9,7 @@ import { FileJson, FileUp, Flag, + GalleryVerticalEnd, FolderTree, Hash, HardDrive, @@ -100,6 +101,7 @@ export const NAV_GROUPS: NavGroup[] = [ { label: '内容审核', items: [ + { label: '事件监控', to: '/filex-events', icon: GalleryVerticalEnd, permission: 'admin:filex:read' }, { label: '被举报评论', to: '/comment-reports', icon: MessageSquareWarning, permission: 'admin:comment:read' }, { label: '被举报事件', to: '/event-reports', icon: Flag, permission: 'admin:filex:read' }, ], diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index eefc42b..b8185c0 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -18,6 +18,7 @@ import { Route as AuthedRolesRouteImport } from './routes/_authed/roles' import { Route as AuthedRobotsRouteImport } from './routes/_authed/robots' import { Route as AuthedMinioConsoleRouteImport } from './routes/_authed/minio-console' import { Route as AuthedLocalesRouteImport } from './routes/_authed/locales' +import { Route as AuthedFilexEventsRouteImport } from './routes/_authed/filex-events' import { Route as AuthedEventReportsRouteImport } from './routes/_authed/event-reports' import { Route as AuthedDimzouTranslationsRouteImport } from './routes/_authed/dimzou-translations' import { Route as AuthedDevicesRouteImport } from './routes/_authed/devices' @@ -86,6 +87,11 @@ const AuthedLocalesRoute = AuthedLocalesRouteImport.update({ path: '/locales', getParentRoute: () => AuthedRoute, } as any) +const AuthedFilexEventsRoute = AuthedFilexEventsRouteImport.update({ + id: '/filex-events', + path: '/filex-events', + getParentRoute: () => AuthedRoute, +} as any) const AuthedEventReportsRoute = AuthedEventReportsRouteImport.update({ id: '/event-reports', path: '/event-reports', @@ -218,6 +224,7 @@ export interface FileRoutesByFullPath { '/devices': typeof AuthedDevicesRoute '/dimzou-translations': typeof AuthedDimzouTranslationsRoute '/event-reports': typeof AuthedEventReportsRoute + '/filex-events': typeof AuthedFilexEventsRoute '/locales': typeof AuthedLocalesRoute '/minio-console': typeof AuthedMinioConsoleRoute '/robots': typeof AuthedRobotsRoute @@ -250,6 +257,7 @@ export interface FileRoutesByTo { '/devices': typeof AuthedDevicesRoute '/dimzou-translations': typeof AuthedDimzouTranslationsRoute '/event-reports': typeof AuthedEventReportsRoute + '/filex-events': typeof AuthedFilexEventsRoute '/locales': typeof AuthedLocalesRoute '/minio-console': typeof AuthedMinioConsoleRoute '/robots': typeof AuthedRobotsRoute @@ -285,6 +293,7 @@ export interface FileRoutesById { '/_authed/devices': typeof AuthedDevicesRoute '/_authed/dimzou-translations': typeof AuthedDimzouTranslationsRoute '/_authed/event-reports': typeof AuthedEventReportsRoute + '/_authed/filex-events': typeof AuthedFilexEventsRoute '/_authed/locales': typeof AuthedLocalesRoute '/_authed/minio-console': typeof AuthedMinioConsoleRoute '/_authed/robots': typeof AuthedRobotsRoute @@ -321,6 +330,7 @@ export interface FileRouteTypes { | '/devices' | '/dimzou-translations' | '/event-reports' + | '/filex-events' | '/locales' | '/minio-console' | '/robots' @@ -353,6 +363,7 @@ export interface FileRouteTypes { | '/devices' | '/dimzou-translations' | '/event-reports' + | '/filex-events' | '/locales' | '/minio-console' | '/robots' @@ -387,6 +398,7 @@ export interface FileRouteTypes { | '/_authed/devices' | '/_authed/dimzou-translations' | '/_authed/event-reports' + | '/_authed/filex-events' | '/_authed/locales' | '/_authed/minio-console' | '/_authed/robots' @@ -483,6 +495,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthedLocalesRouteImport parentRoute: typeof AuthedRoute } + '/_authed/filex-events': { + id: '/_authed/filex-events' + path: '/filex-events' + fullPath: '/filex-events' + preLoaderRoute: typeof AuthedFilexEventsRouteImport + parentRoute: typeof AuthedRoute + } '/_authed/event-reports': { id: '/_authed/event-reports' path: '/event-reports' @@ -654,6 +673,7 @@ interface AuthedRouteChildren { AuthedDevicesRoute: typeof AuthedDevicesRoute AuthedDimzouTranslationsRoute: typeof AuthedDimzouTranslationsRoute AuthedEventReportsRoute: typeof AuthedEventReportsRoute + AuthedFilexEventsRoute: typeof AuthedFilexEventsRoute AuthedLocalesRoute: typeof AuthedLocalesRoute AuthedMinioConsoleRoute: typeof AuthedMinioConsoleRoute AuthedRobotsRoute: typeof AuthedRobotsRoute @@ -686,6 +706,7 @@ const AuthedRouteChildren: AuthedRouteChildren = { AuthedDevicesRoute: AuthedDevicesRoute, AuthedDimzouTranslationsRoute: AuthedDimzouTranslationsRoute, AuthedEventReportsRoute: AuthedEventReportsRoute, + AuthedFilexEventsRoute: AuthedFilexEventsRoute, AuthedLocalesRoute: AuthedLocalesRoute, AuthedMinioConsoleRoute: AuthedMinioConsoleRoute, AuthedRobotsRoute: AuthedRobotsRoute, diff --git a/src/routes/_authed/filex-events.tsx b/src/routes/_authed/filex-events.tsx new file mode 100644 index 0000000..8db8cc8 --- /dev/null +++ b/src/routes/_authed/filex-events.tsx @@ -0,0 +1,163 @@ +import { createFileRoute } from '@tanstack/react-router' +import { useQuery } from '@tanstack/react-query' +import { useState } from 'react' +import type { ColumnDef } from '@tanstack/react-table' +import type { FileXAdminEvent } from '@/api/types' +import { fetchFileXEvent, fetchFileXEvents } from '@/api/modules/filex' +import { PERM, requirePermission } from '@/auth/permissions' +import { DataTable } from '@/components/data-table/data-table' +import { PageHeader } from '@/components/page-header' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { DetailSheet } from '@/features/studio/detail-sheet' +import { useListPage } from '@/lib/list-page' + +export const Route = createFileRoute('/_authed/filex-events')({ + beforeLoad: () => requirePermission(PERM.FILEX_READ), + component: FileXEventsPage, +}) + +const VISIBILITY_LABELS: Record = { + public: '公开', + friends: '好友', + private: '私密', +} + +function formatTime(value?: string | null) { + return value ? new Date(value).toLocaleString('zh-CN') : '—' +} + +function FileXEventsPage() { + const [status, setStatus] = useState('all') + const [visibility, setVisibility] = useState('all') + const [query, setQuery] = useState('') + const [topic, setTopic] = useState('') + const [authorUid, setAuthorUid] = useState('') + const [detailId, setDetailId] = useState(null) + const filters: Record = {} + if (status !== 'all') filters.status = status + if (visibility !== 'all') filters.visibility = visibility + if (query.trim()) filters.q = query.trim() + if (topic.trim()) filters.primary_topic = topic.trim() + if (authorUid.trim()) filters.user_uid = authorUid.trim() + + const list = useListPage('filex-events', fetchFileXEvents, filters) + const detail = useQuery({ + queryKey: ['filex-event', detailId], + queryFn: () => fetchFileXEvent(detailId as number), + enabled: detailId !== null, + }) + const event = detail.data?.data + + const columns: ColumnDef[] = [ + { + header: '事件', + cell: ({ row }) => ( + + ), + }, + { header: '作者', cell: ({ row }) => row.original.author?.display_name ?? '—' }, + { + header: '可见性', + cell: ({ row }) => ( + + {VISIBILITY_LABELS[row.original.visibility] ?? row.original.visibility} + + ), + }, + { + header: '状态', + cell: ({ row }) => ( + + {row.original.is_blocked ? '已屏蔽' : row.original.deleted_at ? '已删除' : '正常'} + + ), + }, + { + header: '互动 / 待处理举报', + cell: ({ row }) => + `${row.original.likes_count} 赞 · ${row.original.comments_count} 评论 · ${row.original.open_reports_count} 举报`, + }, + { header: '创建时间', cell: ({ row }) => formatTime(row.original.created_at) }, + ] + + return ( +
+ + setQuery(event.target.value)} /> + setTopic(event.target.value)} /> + setAuthorUid(event.target.value)} /> + + +
+ } + /> + + !open && setDetailId(null)} + title={event?.title ?? `事件 #${detailId ?? ''}`} + description={event ? `${VISIBILITY_LABELS[event.visibility] ?? event.visibility} · ${event.is_blocked ? '已屏蔽' : event.deleted_at ? '已删除' : '正常'}` : undefined} + fields={[ + { label: '正文', value: event?.content ?? '—' }, + { label: '作者', value: event?.author?.display_name ?? '—' }, + { label: '主话题', value: event?.primary_topic ?? '—' }, + { label: '事件日期', value: event?.event_day ?? '—' }, + { label: '位置', value: event?.location_name ?? event?.location_query ?? '—' }, + { label: '经纬度', value: event?.latitude != null && event.longitude != null ? `${event.latitude}, ${event.longitude}` : '—' }, + { label: '点赞 / 评论 / 待处理举报', value: event ? `${event.likes_count} / ${event.comments_count} / ${event.open_reports_count}` : '—' }, + { label: '屏蔽时间', value: formatTime(event?.blocked_at) }, + { label: '删除时间', value: formatTime(event?.deleted_at) }, + ]} + jsonBlocks={[ + { label: '话题', value: event?.topics?.length ? event.topics : null }, + { label: '附件', value: event?.assets?.length ? event.assets : null }, + ]} + /> + + ) +}