feat(filex): add admin event monitoring
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
import { api } from '@/api/client'
|
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'
|
import type { ModerationAction } from '@/api/modules/comments'
|
||||||
|
|
||||||
const BASE = '/api/admin/v1/file-x'
|
const BASE = '/api/admin/v1/file-x'
|
||||||
@@ -11,3 +18,11 @@ export function fetchReportedEvents(params: ListParams) {
|
|||||||
export function moderateReportedEvent(id: number, action: ModerationAction) {
|
export function moderateReportedEvent(id: number, action: ModerationAction) {
|
||||||
return api.patch<ApiResponse<ReportedEvent>>(`${BASE}/reported-events/${id}`, { action })
|
return api.patch<ApiResponse<ReportedEvent>>(`${BASE}/reported-events/${id}`, { action })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchFileXEvents(params: ListParams) {
|
||||||
|
return api.get<PaginatedResponse<FileXAdminEvent>>(`${BASE}/events`, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchFileXEvent(id: number) {
|
||||||
|
return api.get<ApiResponse<FileXAdminEventDetail>>(`${BASE}/events/${id}`)
|
||||||
|
}
|
||||||
|
|||||||
+22
-19
@@ -50,7 +50,7 @@ export interface paths {
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
/**
|
/**
|
||||||
* @example all
|
* @example active
|
||||||
* @enum {string|null}
|
* @enum {string|null}
|
||||||
*/
|
*/
|
||||||
state?: "active" | "archived" | "trashed" | "all" | null;
|
state?: "active" | "archived" | "trashed" | "all" | null;
|
||||||
@@ -346,7 +346,7 @@ export interface paths {
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
/**
|
/**
|
||||||
* @example group_only
|
* @example public
|
||||||
* @enum {string|null}
|
* @enum {string|null}
|
||||||
*/
|
*/
|
||||||
visibility?: "public" | "group_only" | null;
|
visibility?: "public" | "group_only" | null;
|
||||||
@@ -539,7 +539,7 @@ export interface paths {
|
|||||||
*/
|
*/
|
||||||
language_code?: string;
|
language_code?: string;
|
||||||
/**
|
/**
|
||||||
* @example published
|
* @example awaiting_publish
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
status?: "draft" | "awaiting_publish" | "published";
|
status?: "draft" | "awaiting_publish" | "published";
|
||||||
@@ -4691,7 +4691,7 @@ export interface paths {
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
/**
|
/**
|
||||||
* @example rejected
|
* @example ignored
|
||||||
* @enum {string|null}
|
* @enum {string|null}
|
||||||
*/
|
*/
|
||||||
status?: "sent" | "delivered" | "viewed" | "accepted" | "rejected" | "ignored" | "expired" | "taken_by_other" | null;
|
status?: "sent" | "delivered" | "viewed" | "accepted" | "rejected" | "ignored" | "expired" | "taken_by_other" | null;
|
||||||
@@ -4885,12 +4885,12 @@ export interface paths {
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
/**
|
/**
|
||||||
* @example fulfilled
|
* @example started
|
||||||
* @enum {string|null}
|
* @enum {string|null}
|
||||||
*/
|
*/
|
||||||
status?: "created" | "confirmed" | "started" | "consumed" | "paid" | "fulfilled" | "canceled" | null;
|
status?: "created" | "confirmed" | "started" | "consumed" | "paid" | "fulfilled" | "canceled" | null;
|
||||||
/**
|
/**
|
||||||
* @example UNPAID
|
* @example PAID
|
||||||
* @enum {string|null}
|
* @enum {string|null}
|
||||||
*/
|
*/
|
||||||
payment_status?: "UNPAID" | "PAID" | null;
|
payment_status?: "UNPAID" | "PAID" | null;
|
||||||
@@ -5466,35 +5466,35 @@ export interface paths {
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
/**
|
/**
|
||||||
* @example public
|
* @example friends
|
||||||
* @enum {string|null}
|
* @enum {string|null}
|
||||||
*/
|
*/
|
||||||
visibility?: "public" | "friends" | "private" | null;
|
visibility?: "public" | "friends" | "private" | null;
|
||||||
/**
|
/**
|
||||||
* @example visible
|
* @example blocked
|
||||||
* @enum {string|null}
|
* @enum {string|null}
|
||||||
*/
|
*/
|
||||||
status?: "blocked" | "visible" | "all" | null;
|
status?: "blocked" | "visible" | "all" | null;
|
||||||
/** @example architecto */
|
/** @example null */
|
||||||
user_uid?: string | null;
|
user_uid?: string | null;
|
||||||
/**
|
/**
|
||||||
* @description Must not be greater than 100 characters.
|
* @description Must not be greater than 100 characters.
|
||||||
* @example n
|
* @example b
|
||||||
*/
|
*/
|
||||||
primary_topic?: string | null;
|
primary_topic?: string | null;
|
||||||
/**
|
/**
|
||||||
* @description Must not be greater than 100 characters.
|
* @description Must not be greater than 100 characters.
|
||||||
* @example g
|
* @example n
|
||||||
*/
|
*/
|
||||||
q?: string | null;
|
q?: string | null;
|
||||||
/**
|
/**
|
||||||
* @description Must be at least 1.
|
* @description Must be at least 1.
|
||||||
* @example 66
|
* @example 67
|
||||||
*/
|
*/
|
||||||
page?: number | null;
|
page?: number | null;
|
||||||
/**
|
/**
|
||||||
* @description Must be at least 1. Must not be greater than 100.
|
* @description Must be at least 1. Must not be greater than 100.
|
||||||
* @example 17
|
* @example 16
|
||||||
*/
|
*/
|
||||||
page_size?: number | null;
|
page_size?: number | null;
|
||||||
};
|
};
|
||||||
@@ -5528,7 +5528,8 @@ export interface paths {
|
|||||||
* "comments_count": 3,
|
* "comments_count": 3,
|
||||||
* "open_reports_count": 0,
|
* "open_reports_count": 0,
|
||||||
* "event_day": "2026-07-10",
|
* "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;
|
event_day?: string;
|
||||||
/** @example 2026-07-10T09:00:00Z */
|
/** @example 2026-07-10T09:00:00Z */
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
|
/** @example null */
|
||||||
|
deleted_at?: string;
|
||||||
}[];
|
}[];
|
||||||
pagination?: {
|
pagination?: {
|
||||||
/** @example 1 */
|
/** @example 1 */
|
||||||
@@ -5797,7 +5800,7 @@ export interface paths {
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
/**
|
/**
|
||||||
* @example all
|
* @example blocked
|
||||||
* @enum {string|null}
|
* @enum {string|null}
|
||||||
*/
|
*/
|
||||||
status?: "pending" | "blocked" | "all" | null;
|
status?: "pending" | "blocked" | "all" | null;
|
||||||
@@ -7682,9 +7685,9 @@ export interface paths {
|
|||||||
* @example b
|
* @example b
|
||||||
*/
|
*/
|
||||||
q?: string | null;
|
q?: string | null;
|
||||||
/** @example true */
|
|
||||||
status?: boolean | null;
|
|
||||||
/** @example false */
|
/** @example false */
|
||||||
|
status?: boolean | null;
|
||||||
|
/** @example true */
|
||||||
is_robot?: boolean | null;
|
is_robot?: boolean | null;
|
||||||
/**
|
/**
|
||||||
* @description Must not be greater than 100 characters.
|
* @description Must not be greater than 100 characters.
|
||||||
@@ -9762,7 +9765,7 @@ export interface paths {
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
/**
|
/**
|
||||||
* @example pending
|
* @example blocked
|
||||||
* @enum {string|null}
|
* @enum {string|null}
|
||||||
*/
|
*/
|
||||||
status?: "pending" | "blocked" | "all" | null;
|
status?: "pending" | "blocked" | "all" | null;
|
||||||
@@ -10129,7 +10132,7 @@ export interface paths {
|
|||||||
* @enum {string|null}
|
* @enum {string|null}
|
||||||
*/
|
*/
|
||||||
platform?: "ios" | "android" | "web" | "desktop" | null;
|
platform?: "ios" | "android" | "web" | "desktop" | null;
|
||||||
/** @example true */
|
/** @example false */
|
||||||
is_online?: boolean | null;
|
is_online?: boolean | null;
|
||||||
/**
|
/**
|
||||||
* @description Must be at least 1.
|
* @description Must be at least 1.
|
||||||
|
|||||||
@@ -441,6 +441,36 @@ export interface ReportedEvent extends ReportedResourceBase {
|
|||||||
visibility?: string
|
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 type DimzouTranslationStatus = 'draft' | 'awaiting_publish' | 'published'
|
||||||
|
|
||||||
export interface DimzouTranslation {
|
export interface DimzouTranslation {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
FileJson,
|
FileJson,
|
||||||
FileUp,
|
FileUp,
|
||||||
Flag,
|
Flag,
|
||||||
|
GalleryVerticalEnd,
|
||||||
FolderTree,
|
FolderTree,
|
||||||
Hash,
|
Hash,
|
||||||
HardDrive,
|
HardDrive,
|
||||||
@@ -100,6 +101,7 @@ export const NAV_GROUPS: NavGroup[] = [
|
|||||||
{
|
{
|
||||||
label: '内容审核',
|
label: '内容审核',
|
||||||
items: [
|
items: [
|
||||||
|
{ label: '事件监控', to: '/filex-events', icon: GalleryVerticalEnd, permission: 'admin:filex:read' },
|
||||||
{ label: '被举报评论', to: '/comment-reports', icon: MessageSquareWarning, permission: 'admin:comment:read' },
|
{ label: '被举报评论', to: '/comment-reports', icon: MessageSquareWarning, permission: 'admin:comment:read' },
|
||||||
{ label: '被举报事件', to: '/event-reports', icon: Flag, permission: 'admin:filex:read' },
|
{ label: '被举报事件', to: '/event-reports', icon: Flag, permission: 'admin:filex:read' },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { Route as AuthedRolesRouteImport } from './routes/_authed/roles'
|
|||||||
import { Route as AuthedRobotsRouteImport } from './routes/_authed/robots'
|
import { Route as AuthedRobotsRouteImport } from './routes/_authed/robots'
|
||||||
import { Route as AuthedMinioConsoleRouteImport } from './routes/_authed/minio-console'
|
import { Route as AuthedMinioConsoleRouteImport } from './routes/_authed/minio-console'
|
||||||
import { Route as AuthedLocalesRouteImport } from './routes/_authed/locales'
|
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 AuthedEventReportsRouteImport } from './routes/_authed/event-reports'
|
||||||
import { Route as AuthedDimzouTranslationsRouteImport } from './routes/_authed/dimzou-translations'
|
import { Route as AuthedDimzouTranslationsRouteImport } from './routes/_authed/dimzou-translations'
|
||||||
import { Route as AuthedDevicesRouteImport } from './routes/_authed/devices'
|
import { Route as AuthedDevicesRouteImport } from './routes/_authed/devices'
|
||||||
@@ -86,6 +87,11 @@ const AuthedLocalesRoute = AuthedLocalesRouteImport.update({
|
|||||||
path: '/locales',
|
path: '/locales',
|
||||||
getParentRoute: () => AuthedRoute,
|
getParentRoute: () => AuthedRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AuthedFilexEventsRoute = AuthedFilexEventsRouteImport.update({
|
||||||
|
id: '/filex-events',
|
||||||
|
path: '/filex-events',
|
||||||
|
getParentRoute: () => AuthedRoute,
|
||||||
|
} as any)
|
||||||
const AuthedEventReportsRoute = AuthedEventReportsRouteImport.update({
|
const AuthedEventReportsRoute = AuthedEventReportsRouteImport.update({
|
||||||
id: '/event-reports',
|
id: '/event-reports',
|
||||||
path: '/event-reports',
|
path: '/event-reports',
|
||||||
@@ -218,6 +224,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/devices': typeof AuthedDevicesRoute
|
'/devices': typeof AuthedDevicesRoute
|
||||||
'/dimzou-translations': typeof AuthedDimzouTranslationsRoute
|
'/dimzou-translations': typeof AuthedDimzouTranslationsRoute
|
||||||
'/event-reports': typeof AuthedEventReportsRoute
|
'/event-reports': typeof AuthedEventReportsRoute
|
||||||
|
'/filex-events': typeof AuthedFilexEventsRoute
|
||||||
'/locales': typeof AuthedLocalesRoute
|
'/locales': typeof AuthedLocalesRoute
|
||||||
'/minio-console': typeof AuthedMinioConsoleRoute
|
'/minio-console': typeof AuthedMinioConsoleRoute
|
||||||
'/robots': typeof AuthedRobotsRoute
|
'/robots': typeof AuthedRobotsRoute
|
||||||
@@ -250,6 +257,7 @@ export interface FileRoutesByTo {
|
|||||||
'/devices': typeof AuthedDevicesRoute
|
'/devices': typeof AuthedDevicesRoute
|
||||||
'/dimzou-translations': typeof AuthedDimzouTranslationsRoute
|
'/dimzou-translations': typeof AuthedDimzouTranslationsRoute
|
||||||
'/event-reports': typeof AuthedEventReportsRoute
|
'/event-reports': typeof AuthedEventReportsRoute
|
||||||
|
'/filex-events': typeof AuthedFilexEventsRoute
|
||||||
'/locales': typeof AuthedLocalesRoute
|
'/locales': typeof AuthedLocalesRoute
|
||||||
'/minio-console': typeof AuthedMinioConsoleRoute
|
'/minio-console': typeof AuthedMinioConsoleRoute
|
||||||
'/robots': typeof AuthedRobotsRoute
|
'/robots': typeof AuthedRobotsRoute
|
||||||
@@ -285,6 +293,7 @@ export interface FileRoutesById {
|
|||||||
'/_authed/devices': typeof AuthedDevicesRoute
|
'/_authed/devices': typeof AuthedDevicesRoute
|
||||||
'/_authed/dimzou-translations': typeof AuthedDimzouTranslationsRoute
|
'/_authed/dimzou-translations': typeof AuthedDimzouTranslationsRoute
|
||||||
'/_authed/event-reports': typeof AuthedEventReportsRoute
|
'/_authed/event-reports': typeof AuthedEventReportsRoute
|
||||||
|
'/_authed/filex-events': typeof AuthedFilexEventsRoute
|
||||||
'/_authed/locales': typeof AuthedLocalesRoute
|
'/_authed/locales': typeof AuthedLocalesRoute
|
||||||
'/_authed/minio-console': typeof AuthedMinioConsoleRoute
|
'/_authed/minio-console': typeof AuthedMinioConsoleRoute
|
||||||
'/_authed/robots': typeof AuthedRobotsRoute
|
'/_authed/robots': typeof AuthedRobotsRoute
|
||||||
@@ -321,6 +330,7 @@ export interface FileRouteTypes {
|
|||||||
| '/devices'
|
| '/devices'
|
||||||
| '/dimzou-translations'
|
| '/dimzou-translations'
|
||||||
| '/event-reports'
|
| '/event-reports'
|
||||||
|
| '/filex-events'
|
||||||
| '/locales'
|
| '/locales'
|
||||||
| '/minio-console'
|
| '/minio-console'
|
||||||
| '/robots'
|
| '/robots'
|
||||||
@@ -353,6 +363,7 @@ export interface FileRouteTypes {
|
|||||||
| '/devices'
|
| '/devices'
|
||||||
| '/dimzou-translations'
|
| '/dimzou-translations'
|
||||||
| '/event-reports'
|
| '/event-reports'
|
||||||
|
| '/filex-events'
|
||||||
| '/locales'
|
| '/locales'
|
||||||
| '/minio-console'
|
| '/minio-console'
|
||||||
| '/robots'
|
| '/robots'
|
||||||
@@ -387,6 +398,7 @@ export interface FileRouteTypes {
|
|||||||
| '/_authed/devices'
|
| '/_authed/devices'
|
||||||
| '/_authed/dimzou-translations'
|
| '/_authed/dimzou-translations'
|
||||||
| '/_authed/event-reports'
|
| '/_authed/event-reports'
|
||||||
|
| '/_authed/filex-events'
|
||||||
| '/_authed/locales'
|
| '/_authed/locales'
|
||||||
| '/_authed/minio-console'
|
| '/_authed/minio-console'
|
||||||
| '/_authed/robots'
|
| '/_authed/robots'
|
||||||
@@ -483,6 +495,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AuthedLocalesRouteImport
|
preLoaderRoute: typeof AuthedLocalesRouteImport
|
||||||
parentRoute: typeof AuthedRoute
|
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': {
|
'/_authed/event-reports': {
|
||||||
id: '/_authed/event-reports'
|
id: '/_authed/event-reports'
|
||||||
path: '/event-reports'
|
path: '/event-reports'
|
||||||
@@ -654,6 +673,7 @@ interface AuthedRouteChildren {
|
|||||||
AuthedDevicesRoute: typeof AuthedDevicesRoute
|
AuthedDevicesRoute: typeof AuthedDevicesRoute
|
||||||
AuthedDimzouTranslationsRoute: typeof AuthedDimzouTranslationsRoute
|
AuthedDimzouTranslationsRoute: typeof AuthedDimzouTranslationsRoute
|
||||||
AuthedEventReportsRoute: typeof AuthedEventReportsRoute
|
AuthedEventReportsRoute: typeof AuthedEventReportsRoute
|
||||||
|
AuthedFilexEventsRoute: typeof AuthedFilexEventsRoute
|
||||||
AuthedLocalesRoute: typeof AuthedLocalesRoute
|
AuthedLocalesRoute: typeof AuthedLocalesRoute
|
||||||
AuthedMinioConsoleRoute: typeof AuthedMinioConsoleRoute
|
AuthedMinioConsoleRoute: typeof AuthedMinioConsoleRoute
|
||||||
AuthedRobotsRoute: typeof AuthedRobotsRoute
|
AuthedRobotsRoute: typeof AuthedRobotsRoute
|
||||||
@@ -686,6 +706,7 @@ const AuthedRouteChildren: AuthedRouteChildren = {
|
|||||||
AuthedDevicesRoute: AuthedDevicesRoute,
|
AuthedDevicesRoute: AuthedDevicesRoute,
|
||||||
AuthedDimzouTranslationsRoute: AuthedDimzouTranslationsRoute,
|
AuthedDimzouTranslationsRoute: AuthedDimzouTranslationsRoute,
|
||||||
AuthedEventReportsRoute: AuthedEventReportsRoute,
|
AuthedEventReportsRoute: AuthedEventReportsRoute,
|
||||||
|
AuthedFilexEventsRoute: AuthedFilexEventsRoute,
|
||||||
AuthedLocalesRoute: AuthedLocalesRoute,
|
AuthedLocalesRoute: AuthedLocalesRoute,
|
||||||
AuthedMinioConsoleRoute: AuthedMinioConsoleRoute,
|
AuthedMinioConsoleRoute: AuthedMinioConsoleRoute,
|
||||||
AuthedRobotsRoute: AuthedRobotsRoute,
|
AuthedRobotsRoute: AuthedRobotsRoute,
|
||||||
|
|||||||
@@ -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<string, string> = {
|
||||||
|
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<number | null>(null)
|
||||||
|
const filters: Record<string, string> = {}
|
||||||
|
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<FileXAdminEvent>[] = [
|
||||||
|
{
|
||||||
|
header: '事件',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
className="h-auto max-w-80 justify-start p-0 text-left"
|
||||||
|
onClick={() => setDetailId(row.original.id)}
|
||||||
|
>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="truncate font-medium">{row.original.title ?? '—'}</div>
|
||||||
|
<code className="font-mono text-xs text-muted-foreground">#{row.original.id}</code>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ header: '作者', cell: ({ row }) => row.original.author?.display_name ?? '—' },
|
||||||
|
{
|
||||||
|
header: '可见性',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Badge variant="secondary">
|
||||||
|
{VISIBILITY_LABELS[row.original.visibility] ?? row.original.visibility}
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: '状态',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Badge variant={row.original.is_blocked ? 'destructive' : 'outline'}>
|
||||||
|
{row.original.is_blocked ? '已屏蔽' : row.original.deleted_at ? '已删除' : '正常'}
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
<PageHeader
|
||||||
|
title="事件监控"
|
||||||
|
description="全站事件、屏蔽状态及互动与举报情况(只读)"
|
||||||
|
actions={
|
||||||
|
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||||
|
<Input className="h-8 w-36" placeholder="搜索标题" value={query} onChange={(event) => setQuery(event.target.value)} />
|
||||||
|
<Input className="h-8 w-32" placeholder="主话题" value={topic} onChange={(event) => setTopic(event.target.value)} />
|
||||||
|
<Input className="h-8 w-52" placeholder="作者 UID" value={authorUid} onChange={(event) => setAuthorUid(event.target.value)} />
|
||||||
|
<Select value={status} onValueChange={setStatus}>
|
||||||
|
<SelectTrigger size="sm" className="w-28"><SelectValue /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">全部状态</SelectItem>
|
||||||
|
<SelectItem value="visible">未屏蔽</SelectItem>
|
||||||
|
<SelectItem value="blocked">已屏蔽</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select value={visibility} onValueChange={setVisibility}>
|
||||||
|
<SelectTrigger size="sm" className="w-28"><SelectValue /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">全部可见性</SelectItem>
|
||||||
|
<SelectItem value="public">公开</SelectItem>
|
||||||
|
<SelectItem value="friends">好友</SelectItem>
|
||||||
|
<SelectItem value="private">私密</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
data={list.rows}
|
||||||
|
pagination={list.pagination}
|
||||||
|
loading={list.loading}
|
||||||
|
emptyText="没有事件记录"
|
||||||
|
onPageChange={list.setPage}
|
||||||
|
onPageSizeChange={list.setPageSize}
|
||||||
|
/>
|
||||||
|
<DetailSheet
|
||||||
|
open={detailId !== null}
|
||||||
|
onOpenChange={(open) => !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 },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user