生产管理系统 - 系统参数配置集成于首页loading

This commit is contained in:
2025-11-07 14:31:42 +08:00
parent 588f55552d
commit c34f4c8503
12 changed files with 1610 additions and 436 deletions

View File

@@ -1,59 +0,0 @@
import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Search } from 'lucide-react'
interface LoginLogFiltersProps {
searchKeyword: string
onSearchChange: (value: string) => void
statusFilter: string
onStatusFilterChange: (value: string) => void
dateRange: string
onDateRangeChange: (value: string) => void
}
export function LoginLogFilters({
searchKeyword,
onSearchChange,
statusFilter,
onStatusFilterChange,
dateRange,
onDateRangeChange
}: LoginLogFiltersProps) {
return (
<Card className="p-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="搜索用户名、IP地址、位置..."
value={searchKeyword}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-10"
/>
</div>
<Select value={statusFilter} onValueChange={onStatusFilterChange}>
<SelectTrigger>
<SelectValue placeholder="登录状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="success"></SelectItem>
<SelectItem value="failed"></SelectItem>
</SelectContent>
</Select>
<Select value={dateRange} onValueChange={onDateRangeChange}>
<SelectTrigger>
<SelectValue placeholder="时间范围" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="today"></SelectItem>
<SelectItem value="week">7</SelectItem>
<SelectItem value="month">30</SelectItem>
</SelectContent>
</Select>
</div>
</Card>
)
}

View File

@@ -1,20 +0,0 @@
import { Card } from '@/components/ui/card'
import { Shield } from 'lucide-react'
export function LoginLogInfo() {
return (
<Card className="p-4 bg-blue-50 border-blue-200">
<h4 className="text-blue-900 mb-2">
<Shield className="w-4 h-4 inline mr-2" />
</h4>
<ul className="space-y-1 text-sm text-blue-800">
<li> </li>
<li> IP地址</li>
<li> 便</li>
<li> 访</li>
<li> </li>
</ul>
</Card>
)
}

View File

@@ -1,50 +0,0 @@
import { Card } from '@/components/ui/card'
import { LoginLog } from '@/types/monitor'
interface LoginLogStatsProps {
logs: LoginLog[]
}
export function LoginLogStats({ logs }: LoginLogStatsProps) {
const stats = [
{
label: '总登录次数',
value: logs.length,
color: 'text-blue-600',
bg: 'bg-blue-100',
},
{
label: '成功登录',
value: logs.filter(l => l.status === 'success').length,
color: 'text-green-600',
bg: 'bg-green-100',
},
{
label: '失败登录',
value: logs.filter(l => l.status === 'failed').length,
color: 'text-red-600',
bg: 'bg-red-100',
},
{
label: '今日登录',
value: logs.filter(l => {
const logDate = new Date(l.loginTime)
const today = new Date()
return logDate.toDateString() === today.toDateString()
}).length,
color: 'text-purple-600',
bg: 'bg-purple-100',
},
]
return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{stats.map((stat, index) => (
<Card key={index} className="p-4">
<div className="text-sm text-muted-foreground">{stat.label}</div>
<div className={`mt-2 ${stat.color}`}>{stat.value}</div>
</Card>
))}
</div>
)
}

View File

@@ -1,87 +0,0 @@
import { Badge } from '@/components/ui/badge'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Card } from '@/components/ui/card'
import { LoginLog } from '@/types/monitor'
import { Shield, Monitor, MapPin } from 'lucide-react'
interface LoginLogTableProps {
logs: LoginLog[]
}
export function LoginLogTable({ logs }: LoginLogTableProps) {
return (
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>IP地址</TableHead>
<TableHead>/</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{logs.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
</TableCell>
</TableRow>
) : (
logs.map((log) => (
<TableRow key={log.id}>
<TableCell>
<div className="flex items-center gap-2">
<Shield className="w-4 h-4 text-gray-400" />
{log.username}
</div>
</TableCell>
<TableCell className="text-muted-foreground">
{new Date(log.loginTime).toLocaleString('zh-CN')}
</TableCell>
<TableCell>
<code className="text-xs px-2 py-1 rounded">
{log.ipAddress}
</code>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Monitor className="w-4 h-4 text-gray-400" />
<div className="text-sm">
<div>{log.device}</div>
{log.browser && (
<div className="text-xs text-muted-foreground">{log.browser}</div>
)}
</div>
</div>
</TableCell>
<TableCell>
{log.location && (
<div className="flex items-center gap-1 text-sm">
<MapPin className="w-3 h-3 text-gray-400" />
{log.location}
</div>
)}
</TableCell>
<TableCell>
{log.status === 'success' ? (
<Badge className="bg-green-100 text-green-700"></Badge>
) : (
<div>
<Badge className="bg-red-100 text-red-700"></Badge>
{log.failReason && (
<p className="text-xs text-red-600 mt-1">{log.failReason}</p>
)}
</div>
)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</Card>
)
}

View File

@@ -1,4 +1 @@
export { LoginLogStats } from './LoginLogStats'
export { LoginLogFilters } from './LoginLogFilters'
export { LoginLogTable } from './LoginLogTable'
export { LoginLogInfo } from './LoginLogInfo'
export * from './loginLogApi'

View File

@@ -0,0 +1,196 @@
/**
* filekorolheader: 登录日志API - 登录日志相关接口调用
* 功能:获取登录日志列表、统计、导出等功能
* 路径:/central-config/monitor/login-log/components/loginLogApi
* 规范遵循crop-x/docs/开发项目规范.md使用SDK生成的API接口
*/
import {
listLoginLogsApiV1LogsLoginLoginLogsGet,
getLoginStatisticsApiV1LogsLoginLoginLogsStatisticsGet,
exportLoginLogsApiV1LogsLoginLoginLogsExportGet
} from '@/lib/api/sdk.gen';
// 登录日志接口
export interface LoginLog {
id: string;
created_at: string;
updated_at: string;
username: string;
user_id: string | null;
status: 'success' | 'failed';
method: string;
ip_address: string;
user_agent: string;
location: string | null;
failure_reason: string | null;
attempt_count: number;
is_suspicious: boolean;
}
// 分页参数接口
export interface LoginLogsQueryParams {
page?: number;
size?: number;
username?: string;
status?: string;
start_time?: string;
end_time?: string;
ip_address?: string;
sort_order?: 'asc' | 'desc';
order_by?: string;
}
// 分页状态接口
export interface PaginationState {
page: number;
size: number;
total: number;
totalPages?: number;
hasNext?: boolean;
hasPrev?: boolean;
}
// 统计数据接口
export interface LoginStatistics {
total_logins: number;
successful_logins: number;
failed_logins: number;
unique_users: number;
success_rate: number;
suspicious_logins: number;
}
/**
* 获取登录日志列表
*/
export const fetchLoginLogs = async (params: LoginLogsQueryParams = {}) => {
try {
// Get token from localStorage
const storedUser = localStorage.getItem('user');
let headers = {};
if (storedUser) {
const userData = JSON.parse(storedUser);
if (userData.token) {
headers = {
'Authorization': `Bearer ${userData.token}`
};
}
}
const response = await listLoginLogsApiV1LogsLoginLoginLogsGet({
headers,
query: {
page: params.page || 1,
size: params.size || 10,
username: params.username,
status: params.status,
start_time: params.start_time,
end_time: params.end_time,
ip_address: params.ip_address,
sort_order: params.sort_order || 'desc',
order_by: params.order_by || 'created_at',
}
});
return {
data: response.data?.data || [],
page: response.data?.page || 1,
size: response.data?.size || 10,
total: response.data?.total || 0,
totalPages: response.data?.total_pages || 0,
hasNext: response.data?.has_next || false,
hasPrev: response.data?.has_prev || false,
};
} catch (error) {
console.error('Failed to fetch login logs:', error);
throw error;
}
};
/**
* 获取登录统计信息
*/
export const fetchLoginStatistics = async () => {
try {
// Get token from localStorage
const storedUser = localStorage.getItem('user');
let headers = {};
if (storedUser) {
const userData = JSON.parse(storedUser);
if (userData.token) {
headers = {
'Authorization': `Bearer ${userData.token}`
};
}
}
const response = await getLoginStatisticsApiV1LogsLoginLoginLogsStatisticsGet({
headers
});
return response.data;
} catch (error) {
console.error('Failed to fetch login statistics:', error);
throw error;
}
};
/**
* 导出登录日志
*/
export const exportLoginLogs = async (params: LoginLogsQueryParams = {}) => {
try {
// Get token from localStorage
const storedUser = localStorage.getItem('user');
let headers = {};
if (storedUser) {
const userData = JSON.parse(storedUser);
if (userData.token) {
headers = {
'Authorization': `Bearer ${userData.token}`
};
}
}
const response = await exportLoginLogsApiV1LogsLoginLoginLogsExportGet({
headers,
query: {
start_time: params.start_time,
end_time: params.end_time,
user_id: params.username, // Map username to user_id for export
}
});
return response.data;
} catch (error) {
console.error('Failed to export login logs:', error);
throw error;
}
};
/**
* 转换登录日志数据 - 适配组件使用
*/
export const transformLoginLogData = (log: any): LoginLog => ({
id: log.id,
created_at: log.created_at,
updated_at: log.updated_at,
username: log.username,
user_id: log.user_id,
status: log.status,
method: log.method,
ip_address: log.ip_address,
user_agent: log.user_agent,
location: log.location,
failure_reason: log.failure_reason,
attempt_count: log.attempt_count,
is_suspicious: log.is_suspicious,
});
/**
* 批量转换登录日志数据
*/
export const transformLoginLogsList = (logs: any[]): LoginLog[] => {
return logs.map(transformLoginLogData);
};

View File

@@ -1,202 +1,421 @@
'use client'
/**
* filekorolheader: 登录日志页面 - 用户登录行为监控页面
* 功能:登录日志查询、统计、导出、筛选
* 路径:/central-config/monitor/login-log
* 规范遵循crop-x/docs/开发项目规范.md使用SearchFormPagination重构事件驱动模式
*/
'use client';
import { useState, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { LoginLog } from '@/types/monitor'
import { Download } from 'lucide-react'
import { toast } from 'sonner'
import { useState, useEffect, useCallback, useRef } from 'react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Download, Eye, AlertTriangle, CheckCircle, XCircle } from 'lucide-react';
// Import modular components
import { SearchFormPagination, type SearchFieldConfig, type TableColumnConfig } from '@/components/common/searchFormPagination';
import {
LoginLogStats,
LoginLogFilters,
LoginLogTable,
LoginLogInfo
} from './components'
fetchLoginLogs,
transformLoginLogsList,
LoginLog,
PaginationState,
LoginLogsQueryParams,
fetchLoginStatistics,
exportLoginLogs
} from './components/loginLogApi';
export default function LoginLogPage() {
const [logs, setLogs] = useState<LoginLog[]>([])
const [searchKeyword, setSearchKeyword] = useState('')
const [statusFilter, setStatusFilter] = useState<string>('all')
const [dateRange, setDateRange] = useState<string>('all')
const [logs, setLogs] = useState<LoginLog[]>([]);
const [loading, setLoading] = useState(false);
const [statistics, setStatistics] = useState<any>(null);
const [pagination, setPagination] = useState<PaginationState>({
page: 1,
size: 10,
total: 0,
totalPages: 0,
hasNext: false,
hasPrev: false,
});
const [searchFilters, setSearchFilters] = useState<Record<string, string>>({
search: '',
status: 'all'
});
const isFirstLoad = useRef(true);
useEffect(() => {
loadLogs()
}, [])
// 搜索字段配置
const searchFields: SearchFieldConfig[] = [
{
key: 'search',
label: '搜索',
type: 'text',
placeholder: '搜索用户名、IP地址...',
},
{
key: 'status',
label: '登录状态',
type: 'select',
defaultValue: 'all',
options: [
{ value: 'all', label: '全部状态' },
{ value: 'success', label: '成功' },
{ value: 'failed', label: '失败' },
],
},
{
key: 'dateRange',
label: '时间范围',
type: 'select',
defaultValue: 'all',
options: [
{ value: 'all', label: '全部时间' },
{ value: 'today', label: '今天' },
{ value: 'week', label: '最近7天' },
{ value: 'month', label: '最近30天' },
],
},
];
const loadLogs = () => {
// 强制重新加载mock数据以解决显示问题
localStorage.removeItem('smart_agriculture_login_logs')
// 表格列配置
const columns: TableColumnConfig[] = [
{
key: 'created_at',
label: '登录时间',
render: (value: string) => (
<div className="text-sm text-muted-foreground">
{value ? new Date(value).toLocaleString('zh-CN') : '-'}
</div>
),
},
{
key: 'username',
label: '用户名',
render: (value: string) => (
<div className="font-medium text-foreground">{value || '-'}</div>
),
},
{
key: 'ip_address',
label: 'IP地址',
render: (value: string) => (
<div className="font-mono text-sm">{value || '-'}</div>
),
},
{
key: 'user_agent',
label: '设备信息',
render: (value: string) => (
<div className="text-sm text-muted-foreground max-w-[200px] truncate" title={value}>
{value || '-'}
</div>
),
},
{
key: 'location',
label: '登录地点',
render: (value: string) => (
<div className="text-sm text-muted-foreground">{value || '-'}</div>
),
},
{
key: 'status',
label: '状态',
render: (value: string) => {
if (value === 'success') {
return (
<Badge className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
<CheckCircle className="w-3 h-3 mr-1" />
</Badge>
);
} else {
return (
<Badge className="bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">
<XCircle className="w-3 h-3 mr-1" />
</Badge>
);
}
},
},
{
key: 'is_suspicious',
label: '安全状态',
render: (value: boolean) => {
if (value) {
return (
<Badge className="bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200">
<AlertTriangle className="w-3 h-3 mr-1" />
</Badge>
);
} else {
return (
<Badge className="bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
<CheckCircle className="w-3 h-3 mr-1" />
</Badge>
);
}
},
},
{
key: 'failure_reason',
label: '失败原因',
render: (value: string) => (
<div className="text-sm text-muted-foreground max-w-[150px] truncate" title={value}>
{value || '-'}
</div>
),
},
{
key: 'actions',
label: '操作',
render: (_: any, row: LoginLog) => (
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => handleViewDetail(row)}
className="h-8 px-2"
title="查看详情"
>
<Eye className="w-3 h-3 mr-1" />
</Button>
</div>
),
},
];
const mockLogs: LoginLog[] = [
{
id: 'log-1',
userId: 'user-1',
username: 'admin',
loginTime: '2024-10-21T09:30:00',
ipAddress: '192.168.1.100',
device: 'Windows 11',
browser: 'Chrome 118',
os: 'Windows',
location: '北京市海淀区',
status: 'success',
sessionId: 'sess-001',
},
{
id: 'log-2',
userId: 'user-2',
username: 'zhangsan',
loginTime: '2024-10-21T10:15:00',
ipAddress: '192.168.1.101',
device: 'macOS 14',
browser: 'Safari 17',
os: 'macOS',
location: '上海市浦东新区',
status: 'success',
sessionId: 'sess-002',
},
{
id: 'log-3',
userId: 'user-3',
username: 'lisi',
loginTime: '2024-10-21T11:20:00',
ipAddress: '192.168.1.102',
device: 'Android 13',
browser: 'Chrome Mobile 118',
os: 'Android',
location: '广州市天河区',
status: 'failed',
failReason: '密码错误',
},
{
id: 'log-4',
userId: 'user-1',
username: 'admin',
loginTime: '2024-10-21T14:45:00',
ipAddress: '192.168.1.100',
device: 'Windows 11',
browser: 'Chrome 118',
os: 'Windows',
location: '北京市海淀区',
status: 'success',
sessionId: 'sess-003',
},
{
id: 'log-5',
userId: 'user-4',
username: 'wangwu',
loginTime: '2024-10-21T15:30:00',
ipAddress: '192.168.1.103',
device: 'iOS 17',
browser: 'Safari 17',
os: 'iOS',
location: '深圳市南山区',
status: 'failed',
failReason: '账号被锁定',
},
{
id: 'log-6',
userId: 'user-5',
username: 'zhaoliu',
loginTime: '2024-10-21T16:20:00',
ipAddress: '192.168.1.104',
device: 'Windows 10',
browser: 'Edge 118',
os: 'Windows',
location: '杭州市西湖区',
status: 'success',
sessionId: 'sess-004',
},
{
id: 'log-7',
userId: 'user-6',
username: 'chenqi',
loginTime: '2024-10-21T17:10:00',
ipAddress: '192.168.1.105',
device: 'Ubuntu 22.04',
browser: 'Firefox 119',
os: 'Linux',
location: '成都市武侯区',
status: 'success',
sessionId: 'sess-005',
},
]
localStorage.setItem('smart_agriculture_login_logs', JSON.stringify(mockLogs))
setLogs(mockLogs)
}
// 加载登录日志数据 - 事件驱动模式
const loadLoginLogs = useCallback(async (params?: {
filters?: Record<string, string>;
pagination?: { page: number; size: number };
resetPage?: boolean;
}) => {
try {
setLoading(true);
const filteredLogs = logs.filter(log => {
const matchKeyword = !searchKeyword ||
log.username.includes(searchKeyword) ||
log.ipAddress.includes(searchKeyword) ||
(log.location && log.location.includes(searchKeyword))
const queryParams: LoginLogsQueryParams = {
page: params?.resetPage ? 1 : (params?.pagination?.page || pagination.page),
size: params?.pagination?.size || pagination.size,
sort_order: 'desc',
order_by: 'created_at',
};
const matchStatus = statusFilter === 'all' || log.status === statusFilter
let matchDate = true
if (dateRange !== 'all') {
const logDate = new Date(log.loginTime)
const now = new Date()
const diffDays = Math.floor((now.getTime() - logDate.getTime()) / (1000 * 60 * 60 * 24))
switch (dateRange) {
case 'today':
matchDate = diffDays === 0
break
case 'week':
matchDate = diffDays <= 7
break
case 'month':
matchDate = diffDays <= 30
break
// 处理搜索条件
const searchKeyword = params?.filters?.search ?? searchFilters.search;
if (searchKeyword) {
queryParams.username = searchKeyword;
queryParams.ip_address = searchKeyword;
}
// 处理状态筛选
const status = params?.filters?.status ?? searchFilters.status;
if (status !== 'all') {
queryParams.status = status;
}
// 处理时间范围
const dateRange = params?.filters?.dateRange ?? searchFilters.dateRange;
if (dateRange !== 'all') {
const now = new Date();
let startTime: string;
switch (dateRange) {
case 'today':
startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0).toISOString();
break;
case 'week':
startTime = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
break;
case 'month':
startTime = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
break;
}
if (startTime) {
queryParams.start_time = startTime;
queryParams.end_time = now.toISOString();
}
}
const response = await fetchLoginLogs(queryParams);
const transformedLogs = transformLoginLogsList(response.data);
setLogs(transformedLogs);
setPagination({
page: response.page,
size: response.size,
total: response.total,
totalPages: response.totalPages,
hasNext: response.hasNext,
hasPrev: response.hasPrev,
});
} catch (error) {
console.error('Failed to load login logs:', error);
const errorMessage = error instanceof Error ? error.message : '加载登录日志失败';
toast.error(errorMessage);
} finally {
setLoading(false);
}
}, [pagination.page, pagination.size, searchFilters]);
return matchKeyword && matchStatus && matchDate
})
// 加载统计数据
const loadStatistics = useCallback(async () => {
try {
const stats = await fetchLoginStatistics();
setStatistics(stats);
} catch (error) {
console.error('Failed to load login statistics:', error);
}
}, []);
const handleExport = () => {
const dataStr = JSON.stringify(filteredLogs, null, 2)
const dataBlob = new Blob([dataStr], { type: 'application/json' })
const url = URL.createObjectURL(dataBlob)
const link = document.createElement('a')
link.href = url
link.download = `login_logs_${new Date().getTime()}.json`
link.click()
toast.success('导出成功')
}
// 初始化数据 - 只在组件挂载时执行一次
useEffect(() => {
if (isFirstLoad.current) {
isFirstLoad.current = false;
loadLoginLogs({ resetPage: true });
loadStatistics();
}
}, [loadLoginLogs, loadStatistics]);
// 事件处理器 - 事件驱动模式
const handleSearch = useCallback((filters: Record<string, string>) => {
setSearchFilters(filters);
loadLoginLogs({
filters,
pagination: { page: 1, size: pagination.size }
});
}, [loadLoginLogs, pagination.size]);
const handlePageChange = useCallback((page: number) => {
setPagination(prev => ({ ...prev, page }));
loadLoginLogs({
filters: searchFilters,
pagination: { page, size: pagination.size }
});
}, [loadLoginLogs, searchFilters, pagination.size]);
const handleSizeChange = useCallback((size: number) => {
setPagination(prev => ({ ...prev, size, page: 1 }));
loadLoginLogs({
filters: searchFilters,
pagination: { page: 1, size }
});
}, [loadLoginLogs, searchFilters]);
// 查看详情
const handleViewDetail = (log: LoginLog) => {
toast.info(`查看登录日志详情: ${log.username} - ${log.ip_address}`);
};
// 导出日志
const handleExport = async () => {
try {
setLoading(true);
const exportData = await exportLoginLogs({
username: searchFilters.search,
status: searchFilters.status === 'all' ? undefined : searchFilters.status,
start_time: searchFilters.dateRange === 'all' ? undefined :
searchFilters.dateRange === 'today' ? new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate(), 0, 0, 0).toISOString() :
searchFilters.dateRange === 'week' ? new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString() :
searchFilters.dateRange === 'month' ? new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString() : undefined,
end_time: new Date().toISOString(),
});
// 创建下载链接
const dataStr = JSON.stringify(exportData, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `login_logs_${new Date().getTime()}.json`;
link.click();
URL.revokeObjectURL(url);
toast.success('导出成功');
} catch (error) {
console.error('Failed to export login logs:', error);
const errorMessage = error instanceof Error ? error.message : '导出失败';
toast.error(errorMessage);
} finally {
setLoading(false);
}
};
return (
<div className="space-y-6">
{/* 页面标题 */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-green-800"></h2>
<p className="text-muted-foreground"></p>
</div>
<Button onClick={handleExport}>
<Button onClick={handleExport} disabled={loading}>
<Download className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 统计卡片 */}
<LoginLogStats logs={logs} />
{statistics && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="p-6 bg-card hover:bg-muted transition-colors border rounded-lg">
<div className="flex items-center justify-between mb-2">
<div className="text-sm text-muted-foreground"></div>
<div className="text-2xl font-bold text-blue-600">{statistics.total_logins}</div>
</div>
<div className="text-xs text-muted-foreground">
</div>
</div>
<div className="p-6 bg-card hover:bg-muted transition-colors border rounded-lg">
<div className="flex items-center justify-between mb-2">
<div className="text-sm text-muted-foreground"></div>
<div className="text-2xl font-bold text-green-600">{statistics.successful_logins}</div>
</div>
<div className="text-xs text-green-600">
</div>
</div>
<div className="p-6 bg-card hover:bg-muted transition-colors border rounded-lg">
<div className="flex items-center justify-between mb-2">
<div className="text-sm text-muted-foreground"></div>
<div className="text-2xl font-bold text-red-600">{statistics.failed_logins}</div>
</div>
<div className="text-xs text-red-600">
</div>
</div>
<div className="p-6 bg-card hover:bg-muted transition-colors border rounded-lg">
<div className="flex items-center justify-between mb-2">
<div className="text-sm text-muted-foreground"></div>
<div className="text-2xl font-bold text-purple-600">{Math.round(statistics.success_rate * 100)}%</div>
</div>
<div className="text-xs text-purple-600">
</div>
</div>
</div>
)}
{/* 搜索和筛选 */}
<LoginLogFilters
searchKeyword={searchKeyword}
onSearchChange={setSearchKeyword}
statusFilter={statusFilter}
onStatusFilterChange={setStatusFilter}
dateRange={dateRange}
onDateRangeChange={setDateRange}
{/* 搜索、表格和分页 */}
<SearchFormPagination
formTitle="登录日志列表"
searchFields={searchFields}
columns={columns}
data={logs}
loading={loading}
error={null}
pagination={pagination}
onPageChange={handlePageChange}
onSizeChange={handleSizeChange}
onSearch={handleSearch}
emptyText="暂无登录日志数据"
showSizeSelector={true}
showPageInfo={true}
sizeOptions={[10, 20, 50, 100]}
/>
{/* 日志列表 */}
<LoginLogTable logs={filteredLogs} />
{/* 使用说明 */}
<LoginLogInfo />
</div>
)
);
}