-
每页条数
+
+
+
每页条数
-
- )
+ );
}
diff --git a/frontend/app/(main)/access-logs/components/access-log-utils.ts b/frontend/app/(main)/access-logs/components/access-log-utils.ts
index ac5dcacd..426a1da8 100644
--- a/frontend/app/(main)/access-logs/components/access-log-utils.ts
+++ b/frontend/app/(main)/access-logs/components/access-log-utils.ts
@@ -1,48 +1,48 @@
-export type AccessLogTab = "list" | "folds" | "ip-summary" | "ip-trend"
+export type AccessLogTab = 'list' | 'folds' | 'ip-summary' | 'ip-trend';
export type SearchDraft = {
- nodeId: string
- remoteAddr: string
- host: string
- path: string
-}
+ nodeId: string;
+ remoteAddr: string;
+ host: string;
+ path: string;
+};
-export const PAGE_SIZE_OPTIONS = [20, 50, 100, 200]
+export const PAGE_SIZE_OPTIONS = [20, 50, 100, 200];
export const DETAIL_SORT_OPTIONS = [
- { value: "logged_at:desc", label: "时间从新到旧" },
- { value: "logged_at:asc", label: "时间从旧到新" },
- { value: "status_code:desc", label: "状态码从高到低" },
- { value: "status_code:asc", label: "状态码从低到高" },
- { value: "remote_addr:asc", label: "IP 正序" },
- { value: "remote_addr:desc", label: "IP 倒序" },
-]
+ { value: 'logged_at:desc', label: '时间从新到旧' },
+ { value: 'logged_at:asc', label: '时间从旧到新' },
+ { value: 'status_code:desc', label: '状态码从高到低' },
+ { value: 'status_code:asc', label: '状态码从低到高' },
+ { value: 'remote_addr:asc', label: 'IP 正序' },
+ { value: 'remote_addr:desc', label: 'IP 倒序' },
+];
export const FOLD_SORT_OPTIONS = [
- { value: "bucket_started_at:desc", label: "时间桶从新到旧" },
- { value: "bucket_started_at:asc", label: "时间桶从旧到新" },
- { value: "request_count:desc", label: "访问次数从高到低" },
- { value: "request_count:asc", label: "访问次数从低到高" },
-]
+ { value: 'bucket_started_at:desc', label: '时间桶从新到旧' },
+ { value: 'bucket_started_at:asc', label: '时间桶从旧到新' },
+ { value: 'request_count:desc', label: '访问次数从高到低' },
+ { value: 'request_count:asc', label: '访问次数从低到高' },
+];
export const IP_SORT_OPTIONS = [
- { value: "total_requests:desc", label: "总访问次数从高到低" },
- { value: "total_requests:asc", label: "总访问次数从低到高" },
- { value: "recent_requests:desc", label: "3 小时访问次数从高到低" },
- { value: "last_seen_at:desc", label: "最后访问时间从新到旧" },
-]
+ { value: 'total_requests:desc', label: '总访问次数从高到低' },
+ { value: 'total_requests:asc', label: '总访问次数从低到高' },
+ { value: 'recent_requests:desc', label: '3 小时访问次数从高到低' },
+ { value: 'last_seen_at:desc', label: '最后访问时间从新到旧' },
+];
export function parseSortValue(value: string) {
- const [sortBy = "logged_at", sortOrder = "desc"] = value.split(":")
+ const [sortBy = 'logged_at', sortOrder = 'desc'] = value.split(':');
return {
sortBy,
- sortOrder: sortOrder === "asc" ? ("asc" as const) : ("desc" as const),
- }
+ sortOrder: sortOrder === 'asc' ? ('asc' as const) : ('desc' as const),
+ };
}
export function formatCompactNumber(value: number) {
- return new Intl.NumberFormat("zh-CN", {
- notation: value >= 10000 ? "compact" : "standard",
+ return new Intl.NumberFormat('zh-CN', {
+ notation: value >= 10000 ? 'compact' : 'standard',
maximumFractionDigits: 1,
- }).format(value)
-}
\ No newline at end of file
+ }).format(value);
+}
diff --git a/frontend/app/(main)/access-logs/components/cleanup-dialog.tsx b/frontend/app/(main)/access-logs/components/cleanup-dialog.tsx
index 54b2c2e1..5253f77f 100644
--- a/frontend/app/(main)/access-logs/components/cleanup-dialog.tsx
+++ b/frontend/app/(main)/access-logs/components/cleanup-dialog.tsx
@@ -1,7 +1,7 @@
-"use client"
+'use client';
-import {useState} from "react"
-import {Loader2} from "lucide-react"
+import {useState} from 'react';
+import {Loader2} from 'lucide-react';
import {
AlertDialog,
@@ -11,19 +11,19 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
-} from "@/components/ui/alert-dialog"
-import {Button} from "@/components/ui/button"
-import {Input} from "@/components/ui/input"
-import {Label} from "@/components/ui/label"
-import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/components/ui/select"
+} from '@/components/ui/alert-dialog';
+import {Button} from '@/components/ui/button';
+import {Input} from '@/components/ui/input';
+import {Label} from '@/components/ui/label';
+import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/components/ui/select';
-const CLEANUP_PRESETS = [3, 7, 30]
+const CLEANUP_PRESETS = [3, 7, 30];
interface CleanupDialogProps {
- open: boolean
- onOpenChange: (open: boolean) => void
- onConfirm: (retentionDays: number) => void
- loading: boolean
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onConfirm: (retentionDays: number) => void;
+ loading: boolean;
}
export function CleanupDialog({
@@ -32,24 +32,24 @@ export function CleanupDialog({
onConfirm,
loading,
}: CleanupDialogProps) {
- const [mode, setMode] = useState
("7")
- const [customDays, setCustomDays] = useState("14")
- const [error, setError] = useState(null)
+ const [mode, setMode] = useState('7');
+ const [customDays, setCustomDays] = useState('14');
+ const [error, setError] = useState(null);
const handleConfirm = () => {
const retentionDays =
- mode === "custom"
+ mode === 'custom'
? Number.parseInt(customDays, 10)
- : Number.parseInt(mode, 10)
+ : Number.parseInt(mode, 10);
if (!Number.isFinite(retentionDays) || retentionDays < 1) {
- setError("保留天数必须大于 0")
- return
+ setError('保留天数必须大于 0');
+ return;
}
- setError(null)
- onConfirm(retentionDays)
- }
+ setError(null);
+ onConfirm(retentionDays);
+ };
return (
@@ -61,8 +61,8 @@ export function CleanupDialog({
-
-
+
+
- {mode === "custom" ? (
-
-
+ {mode === 'custom' ? (
+
+
setCustomDays(e.target.value)}
@@ -93,23 +93,27 @@ export function CleanupDialog({
) : null}
- {error ?
{error}
: null}
+ {error ?
{error}
: null}
取消
-
- )
+ );
}
diff --git a/frontend/app/(main)/access-logs/page.tsx b/frontend/app/(main)/access-logs/page.tsx
index 0b544c1f..9974c4af 100644
--- a/frontend/app/(main)/access-logs/page.tsx
+++ b/frontend/app/(main)/access-logs/page.tsx
@@ -1,24 +1,42 @@
-"use client"
+'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 { 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 { 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 { AccessLogFilters } from './components/access-log-filters';
import {
type AccessLogTab,
DETAIL_SORT_OPTIONS,
@@ -27,19 +45,19 @@ import {
IP_SORT_OPTIONS,
parseSortValue,
type SearchDraft,
-} from "./components/access-log-utils"
-import {CleanupDialog} from "./components/cleanup-dialog"
+} from './components/access-log-utils';
+import { CleanupDialog } from './components/cleanup-dialog';
const trendChartConfig = {
- requests: { label: "请求数", color: "hsl(var(--primary))" },
-} satisfies ChartConfig
+ requests: { label: '请求数', color: 'hsl(var(--primary))' },
+} satisfies ChartConfig;
const emptyDraft: SearchDraft = {
- nodeId: "",
- remoteAddr: "",
- host: "",
- path: "",
-}
+ nodeId: '',
+ remoteAddr: '',
+ host: '',
+ path: '',
+};
function PaginationBar({
page,
@@ -48,48 +66,66 @@ function PaginationBar({
onPrev,
onNext,
}: {
- page: number
- hasMore: boolean
- loading: boolean
- onPrev: () => void
- onNext: () => void
+ page: number;
+ hasMore: boolean;
+ loading: boolean;
+ onPrev: () => void;
+ onNext: () => void;
}) {
return (
-
-
当前第 {page + 1} 页
-
-
+
+
当前第 {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 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 detailSortState = parseSortValue(detailSort);
+ const foldSortState = parseSortValue(foldSort);
+ const ipSortState = parseSortValue(ipSort);
const listQuery = useQuery({
- queryKey: ["openflare", "access-logs", "list", filters, page, pageSize, detailSort],
+ queryKey: [
+ 'openflare',
+ 'access-logs',
+ 'list',
+ filters,
+ page,
+ pageSize,
+ detailSort,
+ ],
queryFn: () =>
AccessLogService.list({
node_id: filters.nodeId || undefined,
@@ -101,11 +137,20 @@ export default function AccessLogsPage() {
sort_by: detailSortState.sortBy,
sort_order: detailSortState.sortOrder,
}),
- enabled: tab === "list",
- })
+ enabled: tab === 'list',
+ });
const foldsQuery = useQuery({
- queryKey: ["openflare", "access-logs", "folds", filters, page, pageSize, foldSort, foldMinutes],
+ queryKey: [
+ 'openflare',
+ 'access-logs',
+ 'folds',
+ filters,
+ page,
+ pageSize,
+ foldSort,
+ foldMinutes,
+ ],
queryFn: () =>
AccessLogService.listFolds({
node_id: filters.nodeId || undefined,
@@ -118,11 +163,19 @@ export default function AccessLogsPage() {
sort_order: foldSortState.sortOrder,
fold_minutes: foldMinutes,
}),
- enabled: tab === "folds",
- })
+ enabled: tab === 'folds',
+ });
const ipSummaryQuery = useQuery({
- queryKey: ["openflare", "access-logs", "ip-summary", filters, page, pageSize, ipSort],
+ queryKey: [
+ 'openflare',
+ 'access-logs',
+ 'ip-summary',
+ filters,
+ page,
+ pageSize,
+ ipSort,
+ ],
queryFn: () =>
AccessLogService.listIPSummaries({
node_id: filters.nodeId || undefined,
@@ -133,11 +186,11 @@ export default function AccessLogsPage() {
sort_by: ipSortState.sortBy,
sort_order: ipSortState.sortOrder,
}),
- enabled: tab === "ip-summary",
- })
+ enabled: tab === 'ip-summary',
+ });
const ipTrendQuery = useQuery({
- queryKey: ["openflare", "access-logs", "ip-trend", filters, appliedTrendIp],
+ queryKey: ['openflare', 'access-logs', 'ip-trend', filters, appliedTrendIp],
queryFn: () =>
AccessLogService.getIPTrend({
node_id: filters.nodeId || undefined,
@@ -146,50 +199,52 @@ export default function AccessLogsPage() {
hours: 24,
bucket_minutes: 30,
}),
- enabled: tab === "ip-trend" && appliedTrendIp !== "",
- })
+ 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"] })
+ toast.success(`已清理 ${result.deleted_count} 条日志`);
+ setCleanupOpen(false);
+ await queryClient.invalidateQueries({
+ queryKey: ['openflare', 'access-logs'],
+ });
},
onError: (error) => {
- toast.error(error instanceof Error ? error.message : "清理失败")
+ toast.error(error instanceof Error ? error.message : '清理失败');
},
- })
+ });
const activeSummary = useMemo(() => {
- if (tab === "list" && listQuery.data) {
+ if (tab === 'list' && listQuery.data) {
return {
totalRecord: listQuery.data.total_record,
totalIp: listQuery.data.total_ip,
- }
+ };
}
- if (tab === "folds" && foldsQuery.data) {
+ if (tab === 'folds' && foldsQuery.data) {
return {
totalRecord: foldsQuery.data.total_record,
totalIp: foldsQuery.data.total_ip,
- }
+ };
}
- if (tab === "ip-summary" && ipSummaryQuery.data) {
+ 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])
+ 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])
+ }));
+ }, [ipTrendQuery.data?.points]);
const handleSearch = useCallback(() => {
setFilters({
@@ -197,93 +252,116 @@ export default function AccessLogsPage() {
remoteAddr: draft.remoteAddr.trim(),
host: draft.host.trim(),
path: draft.path.trim(),
- })
- setPage(0)
- }, [draft])
+ });
+ setPage(0);
+ }, [draft]);
const handleReset = () => {
- setDraft(emptyDraft)
- setFilters(emptyDraft)
- setPage(0)
- setTrendIp("")
- setAppliedTrendIp("")
- }
+ setDraft(emptyDraft);
+ setFilters(emptyDraft);
+ setPage(0);
+ setTrendIp('');
+ setAppliedTrendIp('');
+ };
useEffect(() => {
- setPage(0)
- }, [tab, pageSize])
+ 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()
- }
+ 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
+ ipTrendQuery.isFetching;
return (
-
-
-
-
+
+
+
+
-
访问日志
-
+
访问日志
+
按节点、IP、域名与路径检索,支持时间折叠、IP 汇总与趋势分析。
-
-
-
+
+
+
刷新
- setCleanupOpen(true)}>
-
+ setCleanupOpen(true)}
+ >
+
清理日志
-
+
{[
- { label: "访问记录", value: formatCompactNumber(activeSummary.totalRecord) },
- { label: "来源 IP", value: formatCompactNumber(activeSummary.totalIp) },
{
- label: "当前视图",
+ label: '访问记录',
+ value: formatCompactNumber(activeSummary.totalRecord),
+ },
+ {
+ label: '来源 IP',
+ value: formatCompactNumber(activeSummary.totalIp),
+ },
+ {
+ label: '当前视图',
value:
- tab === "list"
- ? "明细日志"
- : tab === "folds"
- ? "时间折叠"
- : tab === "ip-summary"
- ? "IP 汇总"
- : "IP 趋势",
+ tab === 'list'
+ ? '明细日志'
+ : tab === 'folds'
+ ? '时间折叠'
+ : tab === 'ip-summary'
+ ? 'IP 汇总'
+ : 'IP 趋势',
},
].map((item) => (
-
-
+
+
{item.label}
-
{item.value}
+
{item.value}
))}
-
setTab(value as AccessLogTab)}>
-
- 明细列表
- 时间折叠
- IP 汇总
- IP 趋势
+ setTab(value as AccessLogTab)}
+ >
+
+ 明细列表
+ 时间折叠
+ IP 汇总
+ IP 趋势
-
+
-
-
-
-
明细日志
+
+
+
+
明细日志
{listQuery.isError ? (
-
+
void listQuery.refetch()}
/>
) : listQuery.isLoading ? (
) : (listQuery.data?.items ?? []).length === 0 ? (
-
+
) : (
-
-
- 时间
- 节点
- IP
- 域名
- 路径
- 状态码
+
+
+ 时间
+ 节点
+ IP
+ 域名
+ 路径
+ 状态码
{(listQuery.data?.items ?? []).map((item) => (
-
- {formatDateTime(item.logged_at)}
- {item.node_name || item.node_id}
- {item.remote_addr}
- {item.host}
- {item.path}
+
+
+ {formatDateTime(item.logged_at)}
+
+
+ {item.node_name || item.node_id}
+
+
+ {item.remote_addr}
+
+ {item.host}
+
+ {item.path}
+
-
+
{item.status_code}
@@ -363,26 +453,28 @@ export default function AccessLogsPage() {
-
-
-
-
-
时间折叠
+
+
+
+
+
时间折叠
{foldsQuery.isError ? (
-
+
void foldsQuery.refetch()}
/>
@@ -408,32 +500,47 @@ export default function AccessLogsPage() {
) : foldsQuery.isLoading ? (
) : (foldsQuery.data?.items ?? []).length === 0 ? (
-
+
) : (
-
-
- 时间桶
- 请求数
- 独立 IP
- 独立域名
- 2xx
- 4xx
- 5xx
+
+
+ 时间桶
+ 请求数
+ 独立 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}
+
+ {item.request_count}
+
+
+ {item.unique_ip_count}
+
+
+ {item.unique_host_count}
+
+
+ {item.success_count}
+
+
+ {item.client_error_count}
+
+
+ {item.server_error_count}
+
))}
@@ -449,12 +556,12 @@ export default function AccessLogsPage() {
-
-
-
-
IP 汇总
+
+
+
+
IP 汇总
{ipSummaryQuery.isError ? (
-
+
void ipSummaryQuery.refetch()}
/>
@@ -480,24 +587,30 @@ export default function AccessLogsPage() {
) : ipSummaryQuery.isLoading ? (
) : (ipSummaryQuery.data?.items ?? []).length === 0 ? (
-
+
) : (
-
-
- IP
- 总请求数
- 近 3 小时
- 最后访问
+
+
+ IP
+ 总请求数
+ 近 3 小时
+ 最后访问
{(ipSummaryQuery.data?.items ?? []).map((item) => (
-
- {item.remote_addr}
- {item.total_requests}
- {item.recent_requests}
-
+
+
+ {item.remote_addr}
+
+
+ {item.total_requests}
+
+
+ {item.recent_requests}
+
+
{formatDateTime(item.last_seen_at)}
@@ -515,19 +628,21 @@ export default function AccessLogsPage() {
-
-
-
-
趋势 IP
+
+
+
+
+ 趋势 IP
+
setTrendIp(e.target.value)}
- placeholder="输入要分析趋势的 IP 地址"
+ placeholder='输入要分析趋势的 IP 地址'
/>
setAppliedTrendIp(trendIp.trim())}
disabled={!trendIp.trim()}
>
@@ -535,9 +650,9 @@ export default function AccessLogsPage() {
-
+
{!appliedTrendIp ? (
-
+
) : ipTrendQuery.isLoading ? (
) : ipTrendQuery.isError ? (
@@ -545,28 +660,41 @@ export default function AccessLogsPage() {
message={
ipTrendQuery.error instanceof Error
? ipTrendQuery.error.message
- : "加载失败"
+ : '加载失败'
}
onRetry={() => void ipTrendQuery.refetch()}
/>
) : trendChartData.length === 0 ? (
-
+
) : (
-
-
+
+
{appliedTrendIp} · 近 {ipTrendQuery.data?.hours ?? 24} 小时
-
+
-
-
+
+
} />
@@ -584,5 +712,5 @@ export default function AccessLogsPage() {
loading={cleanupMutation.isPending}
/>
- )
+ );
}
diff --git a/frontend/app/(main)/admin/database/components/cache-manager.tsx b/frontend/app/(main)/admin/database/components/cache-manager.tsx
index b0214fdf..0d46e599 100644
--- a/frontend/app/(main)/admin/database/components/cache-manager.tsx
+++ b/frontend/app/(main)/admin/database/components/cache-manager.tsx
@@ -1,16 +1,16 @@
-"use client"
+'use client';
-import * as React from "react"
-import {useCallback, useEffect, useState} from "react"
-import {toast} from "sonner"
-import {HardDrive, RefreshCw, Trash2} from "lucide-react"
+import * as React from 'react';
+import {useCallback, useEffect, useState} from 'react';
+import {toast} from 'sonner';
+import {HardDrive, RefreshCw, Trash2} from 'lucide-react';
-import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"
-import {Button} from "@/components/ui/button"
-import {Skeleton} from "@/components/ui/skeleton"
-import {Label} from "@/components/ui/label"
-import {Input} from "@/components/ui/input"
-import {Switch} from "@/components/ui/switch"
+import {Card, CardContent, CardDescription, CardHeader, CardTitle,} from '@/components/ui/card';
+import {Button} from '@/components/ui/button';
+import {Skeleton} from '@/components/ui/skeleton';
+import {Label} from '@/components/ui/label';
+import {Input} from '@/components/ui/input';
+import {Switch} from '@/components/ui/switch';
import {
Dialog,
DialogContent,
@@ -18,183 +18,205 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
-} from "@/components/ui/dialog"
-import type {CacheStatus} from "@/lib/services/admin"
-import services from "@/lib/services"
+} from '@/components/ui/dialog';
+import type {CacheStatus} from '@/lib/services/admin';
+import services from '@/lib/services';
/**
* 格式化字节大小
*/
const formatBytes = (bytes: number) => {
- if (bytes === 0) return "0 Bytes"
- const k = 1024
- const sizes = ["Bytes", "KB", "MB", "GB", "TB"]
- const i = Math.floor(Math.log(bytes) / Math.log(k))
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
-}
+ if (bytes === 0) return '0 Bytes';
+ const k = 1024;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+};
/**
* 格式化数字,每3位加逗号
*/
const formatNumber = (num: number | string) => {
- if (num === undefined || num === null) return "0"
- return num.toString().replace(/\B(?=(\d{3})+(?!\B))/g, ",")
-}
+ if (num === undefined || num === null) return '0';
+ return num.toString().replace(/\B(?=(\d{3})+(?!\B))/g, ',');
+};
interface CacheManagerProps {
- refreshTrigger: number
+ refreshTrigger: number;
}
export function CacheManager({ refreshTrigger }: CacheManagerProps) {
- const [cacheStatus, setCacheStatus] = useState
(null)
- const [loadingCache, setLoadingCache] = useState(true)
- const [savingConfig, setSavingConfig] = useState(false)
- const [clearingCache, setClearingCache] = useState(false)
- const [showClearConfirm, setShowClearConfirm] = useState(false)
+ const [cacheStatus, setCacheStatus] = useState(null);
+ const [loadingCache, setLoadingCache] = useState(true);
+ const [savingConfig, setSavingConfig] = useState(false);
+ const [clearingCache, setClearingCache] = useState(false);
+ const [showClearConfirm, setShowClearConfirm] = useState(false);
// 策略配置表单状态
- const [maxSizeMB, setMaxSizeMB] = useState("100")
- const [ttlMinutes, setTtlMinutes] = useState("60")
- const [lruEnabled, setLruEnabled] = useState(true)
+ const [maxSizeMB, setMaxSizeMB] = useState('100');
+ const [ttlMinutes, setTtlMinutes] = useState('60');
+ const [lruEnabled, setLruEnabled] = useState(true);
// 获取磁盘缓存状态
const fetchCacheStatus = useCallback(async (isSilent = false) => {
- if (!isSilent) setLoadingCache(true)
+ if (!isSilent) setLoadingCache(true);
try {
- const data = await services.adminCache.getCacheStatus()
- setCacheStatus(data)
- setMaxSizeMB(data.max_size_mb.toString())
- setTtlMinutes(data.ttl_minutes.toString())
- setLruEnabled(data.lru_enabled)
+ const data = await services.adminCache.getCacheStatus();
+ setCacheStatus(data);
+ setMaxSizeMB(data.max_size_mb.toString());
+ setTtlMinutes(data.ttl_minutes.toString());
+ setLruEnabled(data.lru_enabled);
} catch (err) {
- toast.error("获取磁盘缓存状态失败", {
- description: err instanceof Error ? err.message : "未知错误",
- })
+ toast.error('获取磁盘缓存状态失败', {
+ description: err instanceof Error ? err.message : '未知错误',
+ });
} finally {
- setLoadingCache(false)
+ setLoadingCache(false);
}
- }, [])
+ }, []);
// 保存配置
const handleSaveConfig = async (e: React.FormEvent) => {
- e.preventDefault()
- const size = parseInt(maxSizeMB, 10)
- const ttl = parseInt(ttlMinutes, 10)
+ e.preventDefault();
+ const size = parseInt(maxSizeMB, 10);
+ const ttl = parseInt(ttlMinutes, 10);
if (isNaN(size) || size < 1) {
- toast.error("保存失败", { description: "最大容量限制必须是大于等于 1 的整数" })
- return
+ toast.error('保存失败', {
+ description: '最大容量限制必须是大于等于 1 的整数',
+ });
+ return;
}
if (isNaN(ttl) || ttl < 0) {
- toast.error("保存失败", { description: "默认过期时间必须是大于等于 0 的整数" })
- return
+ toast.error('保存失败', {
+ description: '默认过期时间必须是大于等于 0 的整数',
+ });
+ return;
}
- setSavingConfig(true)
+ setSavingConfig(true);
try {
await services.adminCache.updateCacheConfig({
max_size_mb: size,
ttl_minutes: ttl,
lru_enabled: lruEnabled,
- })
- toast.success("保存成功", { description: "磁盘缓存策略已热更新" })
- await fetchCacheStatus(true)
+ });
+ toast.success('保存成功', { description: '磁盘缓存策略已热更新' });
+ await fetchCacheStatus(true);
} catch (err) {
- toast.error("保存配置失败", {
- description: err instanceof Error ? err.message : "未知错误",
- })
+ toast.error('保存配置失败', {
+ description: err instanceof Error ? err.message : '未知错误',
+ });
} finally {
- setSavingConfig(false)
+ setSavingConfig(false);
}
- }
+ };
// 清空磁盘缓存数据
const handleClearCache = async () => {
- setClearingCache(true)
+ setClearingCache(true);
try {
- await services.adminCache.clearCache()
- toast.success("清空成功", { description: "缓存数据已全部清除" })
- setShowClearConfirm(false)
- await fetchCacheStatus(true)
+ await services.adminCache.clearCache();
+ toast.success('清空成功', { description: '缓存数据已全部清除' });
+ setShowClearConfirm(false);
+ await fetchCacheStatus(true);
} catch (err) {
- toast.error("清空缓存失败", {
- description: err instanceof Error ? err.message : "未知错误",
- })
+ toast.error('清空缓存失败', {
+ description: err instanceof Error ? err.message : '未知错误',
+ });
} finally {
- setClearingCache(false)
+ setClearingCache(false);
}
- }
+ };
// 初始化拉取
useEffect(() => {
- fetchCacheStatus()
- }, [fetchCacheStatus])
+ fetchCacheStatus();
+ }, [fetchCacheStatus]);
// 当外部刷新触发器改变时自动刷新
useEffect(() => {
- fetchCacheStatus(true)
- }, [refreshTrigger, fetchCacheStatus])
+ fetchCacheStatus(true);
+ }, [refreshTrigger, fetchCacheStatus]);
return (
-
-
-
-
-
-
缓存管理
+
+
+
+
+
+ 缓存管理
-
管理和监控系统级磁盘缓存的资源占用、生命周期及淘汰策略
+
+ 管理和监控系统级磁盘缓存的资源占用、生命周期及淘汰策略
+
fetchCacheStatus(true)}
disabled={loadingCache}
>
-
+
刷新状态
-
+
{loadingCache && !cacheStatus ? (
-
-
-
-
+
+
+
+
) : cacheStatus ? (
-
+
{/* 左边:状态区 (2/5 cols) */}
-
-
运行状态
-
+
+
+ 运行状态
+
+
{/* 已占空间 */}
-
-
已用空间
-
+
+
+ 已用空间
+
+
{formatBytes(cacheStatus.total_size)}
{/* Key数量 */}
-
-
缓存键数量
-
- {formatNumber(cacheStatus.keys_count)} 个文件
+
+
+ 缓存键数量
+
+
+ {formatNumber(cacheStatus.keys_count)}{' '}
+
+ 个文件
+
{/* 存储路径 */}
-
-
缓存基准目录
-
+
+
+ 缓存基准目录
+
+
{cacheStatus.base_path}
setShowClearConfirm(true)}
>
立即清空缓存
@@ -202,111 +224,125 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
{/* 右边:配置区 (3/5 cols) */}
-