"use client" import {useCallback, useEffect, useMemo, useState} from "react" import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query" import {Area, AreaChart, CartesianGrid, XAxis, YAxis} from "recharts" import {RefreshCw, ScrollText, Trash2} from "lucide-react" import {toast} from "sonner" import {EmptyStateWithBorder} from "@/components/layout/empty" import {ErrorInline} from "@/components/layout/error" import {LoadingStateWithBorder} from "@/components/layout/loading" import {Badge} from "@/components/ui/badge" import {Button} from "@/components/ui/button" import {ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent,} from "@/components/ui/chart" import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/components/ui/select" import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from "@/components/ui/table" import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs" import {AccessLogService} from "@/lib/services/openflare" import {formatDateTime} from "@/lib/utils" import {AccessLogFilters} from "./components/access-log-filters" import { type AccessLogTab, DETAIL_SORT_OPTIONS, FOLD_SORT_OPTIONS, formatCompactNumber, IP_SORT_OPTIONS, parseSortValue, type SearchDraft, } from "./components/access-log-utils" import {CleanupDialog} from "./components/cleanup-dialog" const trendChartConfig = { requests: { label: "请求数", color: "hsl(var(--primary))" }, } satisfies ChartConfig const emptyDraft: SearchDraft = { nodeId: "", remoteAddr: "", host: "", path: "", } function PaginationBar({ page, hasMore, loading, onPrev, onNext, }: { page: number hasMore: boolean loading: boolean onPrev: () => void onNext: () => void }) { return (

当前第 {page + 1} 页

) } export default function AccessLogsPage() { const queryClient = useQueryClient() const [tab, setTab] = useState("list") 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 [foldSort, setFoldSort] = useState("bucket_started_at:desc") const [ipSort, setIpSort] = useState("total_requests:desc") const [foldMinutes, setFoldMinutes] = useState<3 | 5>(3) const [trendIp, setTrendIp] = useState("") const [appliedTrendIp, setAppliedTrendIp] = useState("") const [cleanupOpen, setCleanupOpen] = useState(false) const detailSortState = parseSortValue(detailSort) const foldSortState = parseSortValue(foldSort) const ipSortState = parseSortValue(ipSort) 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 foldsQuery = useQuery({ queryKey: ["openflare", "access-logs", "folds", filters, page, pageSize, foldSort, foldMinutes], queryFn: () => AccessLogService.listFolds({ 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: foldSortState.sortBy, sort_order: foldSortState.sortOrder, fold_minutes: foldMinutes, }), enabled: tab === "folds", }) const ipSummaryQuery = useQuery({ queryKey: ["openflare", "access-logs", "ip-summary", filters, page, pageSize, ipSort], queryFn: () => AccessLogService.listIPSummaries({ node_id: filters.nodeId || undefined, remote_addr: filters.remoteAddr || undefined, host: filters.host || undefined, p: page, page_size: pageSize, sort_by: ipSortState.sortBy, sort_order: ipSortState.sortOrder, }), enabled: tab === "ip-summary", }) const ipTrendQuery = useQuery({ queryKey: ["openflare", "access-logs", "ip-trend", filters, appliedTrendIp], queryFn: () => AccessLogService.getIPTrend({ node_id: filters.nodeId || undefined, remote_addr: appliedTrendIp, host: filters.host || undefined, hours: 24, bucket_minutes: 30, }), enabled: tab === "ip-trend" && appliedTrendIp !== "", }) 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 activeSummary = useMemo(() => { if (tab === "list" && listQuery.data) { return { totalRecord: listQuery.data.total_record, totalIp: listQuery.data.total_ip, } } if (tab === "folds" && foldsQuery.data) { return { totalRecord: foldsQuery.data.total_record, totalIp: foldsQuery.data.total_ip, } } if (tab === "ip-summary" && ipSummaryQuery.data) { return { totalRecord: 0, totalIp: ipSummaryQuery.data.total_ip, } } return { totalRecord: 0, totalIp: 0 } }, [tab, listQuery.data, foldsQuery.data, ipSummaryQuery.data]) const trendChartData = useMemo(() => { return (ipTrendQuery.data?.points ?? []).map((point) => ({ label: formatDateTime(point.bucket_started_at).slice(5), requests: point.request_count, })) }, [ipTrendQuery.data?.points]) 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) setTrendIp("") setAppliedTrendIp("") } useEffect(() => { setPage(0) }, [tab, pageSize]) const refreshActive = () => { if (tab === "list") void listQuery.refetch() if (tab === "folds") void foldsQuery.refetch() if (tab === "ip-summary") void ipSummaryQuery.refetch() if (tab === "ip-trend") void ipTrendQuery.refetch() } const isFetching = listQuery.isFetching || foldsQuery.isFetching || ipSummaryQuery.isFetching || ipTrendQuery.isFetching return (

访问日志

按节点、IP、域名与路径检索,支持时间折叠、IP 汇总与趋势分析。

{[ { label: "访问记录", value: formatCompactNumber(activeSummary.totalRecord) }, { label: "来源 IP", value: formatCompactNumber(activeSummary.totalIp) }, { label: "当前视图", value: tab === "list" ? "明细日志" : tab === "folds" ? "时间折叠" : tab === "ip-summary" ? "IP 汇总" : "IP 趋势", }, ].map((item) => (

{item.label}

{item.value}

))}
setTab(value as AccessLogTab)}> 明细列表 时间折叠 IP 汇总 IP 趋势

明细日志

{listQuery.isError ? (
void listQuery.refetch()} />
) : listQuery.isLoading ? ( ) : (listQuery.data?.items ?? []).length === 0 ? ( ) : ( 时间 节点 IP 域名 路径 状态码 {(listQuery.data?.items ?? []).map((item) => ( {formatDateTime(item.logged_at)} {item.node_name || item.node_id} {item.remote_addr} {item.host} {item.path} {item.status_code} ))}
)} setPage((p) => Math.max(0, p - 1))} onNext={() => setPage((p) => p + 1)} />

时间折叠

{foldsQuery.isError ? (
void foldsQuery.refetch()} />
) : foldsQuery.isLoading ? ( ) : (foldsQuery.data?.items ?? []).length === 0 ? ( ) : ( 时间桶 请求数 独立 IP 独立域名 2xx 4xx 5xx {(foldsQuery.data?.items ?? []).map((item) => ( {formatDateTime(item.bucket_started_at)} {item.request_count} {item.unique_ip_count} {item.unique_host_count} {item.success_count} {item.client_error_count} {item.server_error_count} ))}
)} setPage((p) => Math.max(0, p - 1))} onNext={() => setPage((p) => p + 1)} />

IP 汇总

{ipSummaryQuery.isError ? (
void ipSummaryQuery.refetch()} />
) : ipSummaryQuery.isLoading ? ( ) : (ipSummaryQuery.data?.items ?? []).length === 0 ? ( ) : ( IP 总请求数 近 3 小时 最后访问 {(ipSummaryQuery.data?.items ?? []).map((item) => ( {item.remote_addr} {item.total_requests} {item.recent_requests} {formatDateTime(item.last_seen_at)} ))}
)} setPage((p) => Math.max(0, p - 1))} onNext={() => setPage((p) => p + 1)} />

趋势 IP

setTrendIp(e.target.value)} placeholder="输入要分析趋势的 IP 地址" />
{!appliedTrendIp ? ( ) : ipTrendQuery.isLoading ? ( ) : ipTrendQuery.isError ? ( void ipTrendQuery.refetch()} /> ) : trendChartData.length === 0 ? ( ) : (

{appliedTrendIp} · 近 {ipTrendQuery.data?.hours ?? 24} 小时

} />
)}
cleanupMutation.mutate(days)} loading={cleanupMutation.isPending} />
) }