rwadurian/frontend/mining-admin-web/src/app/(dashboard)/audit-logs/page.tsx

166 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState } from 'react';
import { PageHeader } from '@/components/layout/page-header';
import { useQuery } from '@tanstack/react-query';
import { apiClient } from '@/lib/api/client';
import { formatDateTime } from '@/lib/utils/date';
import { Card, CardContent } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Skeleton } from '@/components/ui/skeleton';
import { Search, ChevronLeft, ChevronRight } from 'lucide-react';
import type { PaginatedResponse } from '@/types/api';
interface AuditLog {
id: string;
adminId: string;
adminUsername: string;
action: string;
resource: string;
resourceId: string | null;
details: string | null;
ipAddress: string;
createdAt: string;
}
const actionLabels: Record<string, { label: string; className: string }> = {
CREATE: { label: '创建', className: 'bg-green-100 text-green-700' },
UPDATE: { label: '更新', className: 'bg-blue-100 text-blue-700' },
DELETE: { label: '删除', className: 'bg-red-100 text-red-700' },
LOGIN: { label: '登录', className: 'bg-purple-100 text-purple-700' },
LOGOUT: { label: '登出', className: 'bg-gray-100 text-gray-600' },
};
export default function AuditLogsPage() {
const [page, setPage] = useState(1);
const [action, setAction] = useState<string>('all');
const [keyword, setKeyword] = useState('');
const pageSize = 20;
const { data, isLoading, error } = useQuery({
queryKey: ['audit-logs', page, action, keyword],
queryFn: async () => {
const response = await apiClient.get('/audit', {
params: { page, pageSize, action: action === 'all' ? undefined : action, keyword: keyword || undefined },
});
return response.data.data as PaginatedResponse<AuditLog>;
},
});
const items = data?.items ?? [];
return (
<div className="space-y-6">
<PageHeader title="审计日志" description="查看系统操作日志" />
<Card>
<CardContent className="p-4">
<div className="flex gap-4">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="搜索管理员、资源..."
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
className="pl-10"
/>
</div>
<Select value={action} onValueChange={setAction}>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="操作类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="CREATE"></SelectItem>
<SelectItem value="UPDATE"></SelectItem>
<SelectItem value="DELETE"></SelectItem>
<SelectItem value="LOGIN"></SelectItem>
</SelectContent>
</Select>
<Button onClick={() => setPage(1)}></Button>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead>IP地址</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoading ? (
[...Array(10)].map((_, i) => (
<TableRow key={i}>
{[...Array(7)].map((_, j) => (
<TableCell key={j}>
<Skeleton className="h-4 w-full" />
</TableCell>
))}
</TableRow>
))
) : items.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-center py-8 text-muted-foreground">
</TableCell>
</TableRow>
) : (
items.map((log) => {
const actionInfo = actionLabels[log.action] || { label: log.action, className: '' };
return (
<TableRow key={log.id}>
<TableCell className="text-sm">{formatDateTime(log.createdAt)}</TableCell>
<TableCell>{log.adminUsername}</TableCell>
<TableCell>
<span
className={`inline-flex items-center px-2 py-1 rounded text-xs font-medium ${actionInfo.className}`}
>
{actionInfo.label}
</span>
</TableCell>
<TableCell>{log.resource}</TableCell>
<TableCell className="font-mono text-xs">{log.resourceId || '-'}</TableCell>
<TableCell className="max-w-[200px] truncate" title={log.details || ''}>
{log.details || '-'}
</TableCell>
<TableCell className="font-mono text-xs">{log.ipAddress}</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
{data && data.totalPages > 1 && (
<div className="flex items-center justify-between p-4 border-t">
<p className="text-sm text-muted-foreground">
{data.total} {page} / {data.totalPages}
</p>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => setPage(page - 1)} disabled={page <= 1}>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button variant="outline" size="sm" onClick={() => setPage(page + 1)} disabled={page >= data.totalPages}>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
)}
</CardContent>
</Card>
</div>
);
}