'use client'; import { Suspense, useCallback, useEffect, useState } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { RefreshCw, ScrollText, Trash2 } from 'lucide-react'; import { toast } from 'sonner'; import { LoadingStateWithBorder } from '@/components/layout/loading'; import { Button } from '@/components/ui/button'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { AccessLogService } from '@/lib/services/openflare'; import { AccessLogFilters } from './components/access-log-filters'; import { type AccessLogTab, type OverviewRangeHours, parseSortValue, type SearchDraft, } from './components/access-log-utils'; import { CleanupDialog } from './components/cleanup-dialog'; import { DetailTab } from './components/detail-tab'; import { OverviewTab } from './components/overview-tab'; const emptyDraft: SearchDraft = { nodeId: '', remoteAddr: '', host: '', path: '', }; function resolveTab(value: string | null): AccessLogTab { return value === 'list' ? 'list' : 'overview'; } function AccessLogsPageContent() { const router = useRouter(); const searchParams = useSearchParams(); const queryClient = useQueryClient(); const tab = resolveTab(searchParams.get('tab')); const [draft, setDraft] = useState(emptyDraft); const [filters, setFilters] = useState(emptyDraft); const [pageSize, setPageSize] = useState(20); const [page, setPage] = useState(0); const [detailSort, setDetailSort] = useState('logged_at:desc'); const [overviewHours, setOverviewHours] = useState(24); const [cleanupOpen, setCleanupOpen] = useState(false); const detailSortState = parseSortValue(detailSort); const handleTabChange = (value: string) => { const next = resolveTab(value); router.replace( next === 'overview' ? '/access-logs' : `/access-logs?tab=${next}`, ); }; const overviewQuery = useQuery({ queryKey: ['openflare', 'access-logs', 'overview', overviewHours], queryFn: () => AccessLogService.getOverview({ hours: overviewHours, }), enabled: tab === 'overview', }); const listQuery = useQuery({ queryKey: [ 'openflare', 'access-logs', 'list', filters, page, pageSize, detailSort, ], queryFn: () => AccessLogService.list({ node_id: filters.nodeId || undefined, remote_addr: filters.remoteAddr || undefined, host: filters.host || undefined, path: filters.path || undefined, p: page, page_size: pageSize, sort_by: detailSortState.sortBy, sort_order: detailSortState.sortOrder, }), enabled: tab === 'list', }); const cleanupMutation = useMutation({ mutationFn: (retentionDays: number) => AccessLogService.cleanup({ retention_days: retentionDays }), onSuccess: async (result) => { toast.success(`已清理 ${result.deleted_count} 条日志`); setCleanupOpen(false); await queryClient.invalidateQueries({ queryKey: ['openflare', 'access-logs'], }); }, onError: (error) => { toast.error(error instanceof Error ? error.message : '清理失败'); }, }); const handleSearch = useCallback(() => { setFilters({ nodeId: draft.nodeId.trim(), remoteAddr: draft.remoteAddr.trim(), host: draft.host.trim(), path: draft.path.trim(), }); setPage(0); }, [draft]); const handleReset = () => { setDraft(emptyDraft); setFilters(emptyDraft); setPage(0); }; useEffect(() => { setPage(0); }, [tab, pageSize]); const refreshActive = () => { if (tab === 'overview') void overviewQuery.refetch(); if (tab === 'list') void listQuery.refetch(); }; const isFetching = overviewQuery.isFetching || listQuery.isFetching; return (

访问日志

查看访问概览、排行榜与明细日志。

概览 日志明细 void overviewQuery.refetch()} />
void listQuery.refetch()} onPrevPage={() => setPage((p) => Math.max(0, p - 1))} onNextPage={() => setPage((p) => p + 1)} isFetching={listQuery.isFetching} />
cleanupMutation.mutate(days)} loading={cleanupMutation.isPending} />
); } export default function AccessLogsPage() { return ( } > ); }