import { useEffect, useState } from 'react'
import { Link, useSearchParams } from 'react-router'
import { useList } from '@refinedev/core'
import { Alert, Button, PageHeader, PageState, Table } from './lib/ui.jsx'
import { useTitle } from './lib/hooks.js'
const startOfDay = value => (value ? new Date(`${value}T00:00:00`).toISOString() : '')
const endOfDay = value => (value ? new Date(`${value}T23:59:59.999`).toISOString() : '')
const csvColumns = ['id', 'event_type', 'account_id', 'task_id', 'attempt_id', 'browser_env_alias', 'network_exit_id', 'binding_version', 'reason_code', 'outcome', 'created_at']
const csvCell = value => `"${String(value ?? '').replaceAll('"', '""')}"`
// 导出仅含关联字段的脱敏白名单,不带 details。
export function auditCSV(events) {
return [csvColumns, ...events.map(event => csvColumns.map(column => event[column]))]
.map(row => row.map(csvCell).join(','))
.join('\n')
}
function downloadCSV(events) {
const url = URL.createObjectURL(new Blob([auditCSV(events)], { type: 'text/csv;charset=utf-8' }))
const anchor = document.createElement('a')
anchor.href = url
anchor.download = 'creatorhub-audit.csv'
anchor.click()
URL.revokeObjectURL(url)
}
const filterInput = 'h-9 w-full rounded-md border border-[#c4ccd9] bg-white px-3 text-sm focus:border-primary focus:outline-none'
function FilterField({ id, label, ...props }) {
return (
)
}
export function AuditPage() {
const [searchParams] = useSearchParams()
const [account, setAccount] = useState(searchParams.get('account_id') || '')
const [task, setTask] = useState(searchParams.get('task_id') || '')
const [attempt, setAttempt] = useState(searchParams.get('attempt_id') || '')
const [env, setEnv] = useState(searchParams.get('browser_env_alias') || '')
const [exit, setExit] = useState(searchParams.get('network_exit_id') || '')
const [eventType, setEventType] = useState('')
const [from, setFrom] = useState('')
const [to, setTo] = useState('')
const [page, setPage] = useState(1)
const filters = [
{ field: 'account_id', value: account },
{ field: 'task_id', value: task },
{ field: 'attempt_id', value: attempt },
{ field: 'browser_env_alias', value: env },
{ field: 'network_exit_id', value: exit },
{ field: 'event_type', value: eventType },
{ field: 'from', value: startOfDay(from) },
{ field: 'to', value: endOfDay(to) },
]
const { result, query } = useList({ resource: 'audit', pagination: { currentPage: page, pageSize: 25 }, filters, queryOptions: { retry: false } })
const error = query.error
const isPending = query.isPending
const events = result.data ?? []
const total = result.total ?? 0
useTitle('CreatorHub · 审计记录')
useEffect(() => { setPage(1) }, [account, task, attempt, env, exit, eventType, from, to])
const hasFilters = !!(account || task || attempt || env || exit || eventType || from || to)
return (
setAccount(event.target.value)} />
setTask(event.target.value)} />
setAttempt(event.target.value)} />
setEnv(event.target.value)} />
setExit(event.target.value)} />
setEventType(event.target.value)} />
setFrom(event.target.value)} />
setTo(event.target.value)} />
{hasFilters ? (
) : null}
{error ?
{error.message} : null}
(
{event.event_type}
#{event.id}
) },
{ header: '关联', width: '27%', render: event => (
{event.account_id ?
账号:{event.account_id}
: null}
{event.task_id ?
任务:{event.task_id}
: null}
{event.attempt_id ?
Attempt:{event.attempt_id}
: null}
) },
{ header: '环境 / 出口', width: '24%', render: event => (
{event.browser_env_alias ? {event.browser_env_alias} : '—'}
{' / '}
{event.network_exit_id ? {event.network_exit_id} : '—'}
绑定版本 {event.binding_version || '—'}
) },
{ header: '原因 / 时间', width: '29%', render: event => (
{event.reason_code || event.outcome || '—'}
{new Date(event.created_at).toLocaleString('zh-CN')}
) },
]}
rows={events} rowKey={event => event.id} minWidth="1000px"
/>
{total > 25 ? (
第 {page} / {Math.ceil(total / 25)} 页
) : null}
)
}