子仓库提交

This commit is contained in:
2025-11-10 09:19:56 +08:00
parent 62f92213f7
commit 5feb24e4e2
733 changed files with 141413 additions and 0 deletions

View File

@@ -0,0 +1 @@
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

@@ -0,0 +1,404 @@
/**
* filekorolheader: 登录日志页面 - 用户登录行为监控页面
* 功能:登录日志查询、统计、导出、筛选
* 路径:/central-config/monitor/login-log
* 规范遵循crop-x/docs/开发项目规范.md使用SearchFormPagination重构事件驱动模式
*/
'use client';
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 { SearchFormPagination, type SearchFieldConfig, type TableColumnConfig } from '@/components/common/searchFormPagination';
import {
fetchLoginLogs,
transformLoginLogsList,
LoginLog,
PaginationState,
LoginLogsQueryParams,
fetchLoginStatistics,
exportLoginLogs
} from './components/loginLogApi';
export default function LoginLogPage() {
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);
// 搜索字段配置
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 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 loadLoginLogs = useCallback(async (params?: {
filters?: Record<string, string>;
pagination?: { page: number; size: number };
resetPage?: boolean;
}) => {
try {
setLoading(true);
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 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.total === 0
? 0
: Math.floor(response.total / response.size) + 1,
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]);
// 事件处理器 - 事件驱动模式
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} disabled={loading}>
<Download className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 统计卡片 */}
{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>
)}
{/* 搜索、表格和分页 */}
<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]}
/>
</div>
);
}

View File

@@ -0,0 +1,191 @@
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { NetworkLog } from '@/types/monitor'
import { Globe } from 'lucide-react'
interface NetworkLogDetailDialogProps {
log: NetworkLog | null
isOpen: boolean
onClose: () => void
isLoading?: boolean
}
export function NetworkLogDetailDialog({
log,
isOpen,
onClose,
isLoading = false
}: NetworkLogDetailDialogProps) {
const getMethodBadge = (method: string) => {
const colors: Record<string, string> = {
GET: 'bg-blue-100 text-blue-700',
POST: 'bg-green-100 text-green-700',
PUT: 'bg-yellow-100 text-yellow-700',
DELETE: 'bg-red-100 text-red-700',
PATCH: 'bg-purple-100 text-purple-700',
}
return colors[method] || 'bg-gray-100 text-gray-700'
}
const getStatusBadge = (status: number) => {
if (status >= 200 && status < 300) {
return 'bg-green-100 text-green-700'
} else if (status >= 400 && status < 500) {
return 'bg-yellow-100 text-yellow-700'
} else if (status >= 500) {
return 'bg-red-100 text-red-700'
}
return 'bg-gray-100 text-gray-700'
}
const formatBytes = (bytes: number) => {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`
}
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
<div className="flex items-center gap-2">
<Globe className="w-5 h-5 text-green-600" />
</div>
</DialogTitle>
</DialogHeader>
{isLoading ? (
<div className="space-y-4">
{Array.from({ length: 8 }).map((_, index) => (
<div key={index} className="animate-pulse">
<div className="bg-gray-200 h-4 w-24 rounded mb-2"></div>
<div className="bg-gray-200 h-6 w-40 rounded"></div>
</div>
))}
</div>
) : log ? (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">{new Date(log.timestamp).toLocaleString('zh-CN')}</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">
<Badge className={getMethodBadge(log.method)}>
{log.method}
</Badge>
</p>
</div>
<div className="col-span-2">
<p className="text-sm text-muted-foreground">URL</p>
<p className="mt-1">
<code className="text-xs bg-gray-100 px-2 py-1 rounded block break-all">
{log.url}
</code>
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">IP</p>
<p className="mt-1">
<code className="text-xs bg-gray-100 px-2 py-1 rounded">
{log.clientIp}
</code>
</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">{log.username || '未登录'}</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">
<Badge className={getStatusBadge(log.responseStatus)}>
{log.responseStatus}
</Badge>
</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">{log.responseTime}ms</p>
</div>
{log.responseSize && (
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">{formatBytes(log.responseSize)}</p>
</div>
)}
</div>
{log.requestParams && (
<div>
<p className="text-sm text-muted-foreground"></p>
<pre className="mt-1 p-3 bg-gray-50 rounded text-xs overflow-x-auto">
{log.requestParams}
</pre>
</div>
)}
{log.requestHeaders && (
<div>
<p className="text-sm text-muted-foreground"></p>
<pre className="mt-1 p-3 bg-gray-50 rounded text-xs overflow-x-auto">
{(() => {
try {
return JSON.stringify(JSON.parse(log.requestHeaders), null, 2)
} catch {
return log.requestHeaders
}
})()}
</pre>
</div>
)}
{log.requestBody && (
<div>
<p className="text-sm text-muted-foreground"></p>
<pre className="mt-1 p-3 bg-gray-50 rounded text-xs overflow-x-auto">
{log.requestBody}
</pre>
</div>
)}
{log.responseBody && (
<div>
<p className="text-sm text-muted-foreground"></p>
<pre className="mt-1 p-3 bg-gray-50 rounded text-xs overflow-x-auto max-h-40 overflow-y-auto">
{(() => {
try {
return JSON.stringify(JSON.parse(log.responseBody), null, 2)
} catch {
return log.responseBody
}
})()}
</pre>
</div>
)}
{log.userAgent && (
<div>
<p className="text-sm text-muted-foreground">User Agent</p>
<p className="mt-1 text-xs text-muted-foreground break-all">
{log.userAgent}
</p>
</div>
)}
</div>
) : null}
<DialogFooter>
<Button variant="outline" onClick={onClose}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,62 @@
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 NetworkLogFiltersProps {
searchKeyword: string
onSearchChange: (value: string) => void
methodFilter: string
onMethodFilterChange: (value: string) => void
statusFilter: string
onStatusFilterChange: (value: string) => void
}
export function NetworkLogFilters({
searchKeyword,
onSearchChange,
methodFilter,
onMethodFilterChange,
statusFilter,
onStatusFilterChange
}: NetworkLogFiltersProps) {
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="搜索URL、用户名..."
value={searchKeyword}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-10"
/>
</div>
<Select value={methodFilter} onValueChange={onMethodFilterChange}>
<SelectTrigger>
<SelectValue placeholder="请求方法" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="GET">GET</SelectItem>
<SelectItem value="POST">POST</SelectItem>
<SelectItem value="PUT">PUT</SelectItem>
<SelectItem value="DELETE">DELETE</SelectItem>
<SelectItem value="PATCH">PATCH</SelectItem>
</SelectContent>
</Select>
<Select value={statusFilter} onValueChange={onStatusFilterChange}>
<SelectTrigger>
<SelectValue placeholder="响应状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="2xx">2xx </SelectItem>
<SelectItem value="4xx">4xx </SelectItem>
<SelectItem value="5xx">5xx </SelectItem>
</SelectContent>
</Select>
</div>
</Card>
)
}

View File

@@ -0,0 +1,20 @@
import { Card } from '@/components/ui/card'
import { Globe } from 'lucide-react'
export function NetworkLogInfo() {
return (
<Card className="p-4 bg-blue-50 border-blue-200">
<h4 className="text-blue-900 mb-2">
<Globe className="w-4 h-4 inline mr-2" />
</h4>
<ul className="space-y-1 text-sm text-blue-800">
<li> HTTP请求的详细信息</li>
<li> 1</li>
<li> 4xx状态码通常表示客户端错误5xx表示服务器错误</li>
<li> </li>
<li> </li>
</ul>
</Card>
)
}

View File

@@ -0,0 +1,338 @@
import { NetworkLog } from '@/types/monitor'
import { ApiResponse, PaginatedResponse, PaginationParams } from '@/types'
export interface NetworkLogFilters {
searchKeyword?: string
method?: string
status?: string
startDate?: string
endDate?: string
minResponseTime?: number
maxResponseTime?: number
}
export interface NetworkLogListParams extends PaginationParams {
filters?: NetworkLogFilters
}
export class NetworkLogService {
private static baseUrl = '/api/monitor/network-logs'
/**
* 获取网络日志列表
*/
static async getNetworkLogs(params: NetworkLogListParams = {}): Promise<PaginatedResponse<NetworkLog>> {
try {
const queryParams = new URLSearchParams()
// 添加分页参数
if (params.page) queryParams.append('page', params.page.toString())
if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString())
// 添加筛选参数
if (params.filters) {
if (params.filters.searchKeyword) queryParams.append('searchKeyword', params.filters.searchKeyword)
if (params.filters.method && params.filters.method !== 'all') queryParams.append('method', params.filters.method)
if (params.filters.status && params.filters.status !== 'all') queryParams.append('status', params.filters.status)
if (params.filters.startDate) queryParams.append('startDate', params.filters.startDate)
if (params.filters.endDate) queryParams.append('endDate', params.filters.endDate)
if (params.filters.minResponseTime) queryParams.append('minResponseTime', params.filters.minResponseTime.toString())
if (params.filters.maxResponseTime) queryParams.append('maxResponseTime', params.filters.maxResponseTime.toString())
}
const response = await fetch(`${this.baseUrl}?${queryParams}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
return data
} catch (error) {
console.error('Failed to fetch network logs:', error)
// 降级处理返回mock数据
return this.getMockData(params)
}
}
/**
* 获取网络日志详情
*/
static async getNetworkLogDetail(id: string): Promise<ApiResponse<NetworkLog>> {
try {
const response = await fetch(`${this.baseUrl}/${id}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
return data
} catch (error) {
console.error('Failed to fetch network log detail:', error)
throw error
}
}
/**
* 导出网络日志
*/
static async exportNetworkLogs(filters?: NetworkLogFilters): Promise<Blob> {
try {
const queryParams = new URLSearchParams()
if (filters) {
if (filters.searchKeyword) queryParams.append('searchKeyword', filters.searchKeyword)
if (filters.method && filters.method !== 'all') queryParams.append('method', filters.method)
if (filters.status && filters.status !== 'all') queryParams.append('status', filters.status)
if (filters.startDate) queryParams.append('startDate', filters.startDate)
if (filters.endDate) queryParams.append('endDate', filters.endDate)
if (filters.minResponseTime) queryParams.append('minResponseTime', filters.minResponseTime.toString())
if (filters.maxResponseTime) queryParams.append('maxResponseTime', filters.maxResponseTime.toString())
}
const response = await fetch(`${this.baseUrl}/export?${queryParams}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return response.blob()
} catch (error) {
console.error('Failed to export network logs:', error)
throw error
}
}
/**
* 获取网络日志统计信息
*/
static async getNetworkLogStats(filters?: NetworkLogFilters): Promise<ApiResponse<{
total: number
success: number
clientError: number
serverError: number
averageResponseTime: number
totalResponseSize: number
methodStats: Array<{ method: string, count: number }>
statusStats: Array<{ status: number, count: number }>
topSlowRequests: Array<{ url: string, responseTime: number, count: number }>
}>> {
try {
const queryParams = new URLSearchParams()
if (filters) {
if (filters.method && filters.method !== 'all') queryParams.append('method', filters.method)
if (filters.status && filters.status !== 'all') queryParams.append('status', filters.status)
if (filters.startDate) queryParams.append('startDate', filters.startDate)
if (filters.endDate) queryParams.append('endDate', filters.endDate)
}
const response = await fetch(`${this.baseUrl}/stats?${queryParams}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
return data
} catch (error) {
console.error('Failed to fetch network log stats:', error)
// 降级处理返回mock统计数据
return this.getMockStats()
}
}
/**
* Mock数据 - 用于降级处理
*/
private static getMockData(params: NetworkLogListParams): PaginatedResponse<NetworkLog> {
const mockLogs: NetworkLog[] = [
{
id: 'net-1',
timestamp: '2024-10-21T09:35:00',
method: 'POST',
url: '/api/users',
requestParams: 'username=zhangsan&name=张三',
responseStatus: 200,
responseTime: 150,
responseSize: 256,
clientIp: '192.168.1.100',
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/118.0.0.0',
userId: 'user-1',
username: 'admin',
},
{
id: 'net-2',
timestamp: '2024-10-21T10:20:00',
method: 'GET',
url: '/api/machinery/list',
requestParams: 'page=1&size=10',
responseStatus: 200,
responseTime: 89,
responseSize: 4096,
clientIp: '192.168.1.101',
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Safari/17.0',
userId: 'user-2',
username: 'zhangsan',
},
{
id: 'net-3',
timestamp: '2024-10-21T11:25:00',
method: 'DELETE',
url: '/api/roles/456',
responseStatus: 400,
responseTime: 120,
responseSize: 128,
clientIp: '192.168.1.102',
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) Chrome/118.0.0.0',
userId: 'user-3',
username: 'lisi',
},
{
id: 'net-4',
timestamp: '2024-10-21T14:50:00',
method: 'PUT',
url: '/api/system/settings',
requestParams: 'sessionTimeout=30',
responseStatus: 200,
responseTime: 95,
responseSize: 512,
clientIp: '192.168.1.100',
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/118.0.0.0',
userId: 'user-1',
username: 'admin',
},
{
id: 'net-5',
timestamp: '2024-10-21T15:35:00',
method: 'POST',
url: '/api/tasks',
responseStatus: 201,
responseTime: 180,
responseSize: 1024,
clientIp: '192.168.1.101',
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Safari/17.0',
userId: 'user-2',
username: 'zhangsan',
},
{
id: 'net-6',
timestamp: '2024-10-21T16:15:00',
method: 'GET',
url: '/api/users/export',
requestParams: 'format=excel',
responseStatus: 200,
responseTime: 1250,
responseSize: 102400,
clientIp: '192.168.1.100',
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/118.0.0.0',
userId: 'user-1',
username: 'admin',
},
]
// 应用筛选器
let filteredLogs = mockLogs.filter(log => {
if (params.filters?.searchKeyword) {
const keyword = params.filters.searchKeyword.toLowerCase()
if (!log.url.toLowerCase().includes(keyword) &&
!(log.username && log.username.toLowerCase().includes(keyword))) {
return false
}
}
if (params.filters?.method && params.filters.method !== 'all') {
if (log.method !== params.filters.method) return false
}
if (params.filters?.status && params.filters.status !== 'all') {
const status = log.responseStatus
switch (params.filters.status) {
case '2xx':
if (status < 200 || status >= 300) return false
break
case '4xx':
if (status < 400 || status >= 500) return false
break
case '5xx':
if (status < 500) return false
break
}
}
if (params.filters?.minResponseTime && log.responseTime < params.filters.minResponseTime) {
return false
}
if (params.filters?.maxResponseTime && log.responseTime > params.filters.maxResponseTime) {
return false
}
return true
})
// 应用分页
const page = params.page || 1
const pageSize = params.pageSize || 10
const startIndex = (page - 1) * pageSize
const endIndex = startIndex + pageSize
const paginatedLogs = filteredLogs.slice(startIndex, endIndex)
return {
code: 200,
message: 'success',
success: true,
data: paginatedLogs,
pagination: {
page,
pageSize,
total: filteredLogs.length,
totalPages: Math.ceil(filteredLogs.length / pageSize)
}
}
}
private static getMockStats(): ApiResponse<{
total: number
success: number
clientError: number
serverError: number
averageResponseTime: number
totalResponseSize: number
methodStats: Array<{ method: string, count: number }>
statusStats: Array<{ status: number, count: number }>
topSlowRequests: Array<{ url: string, responseTime: number, count: number }>
}> {
return {
code: 200,
message: 'success',
success: true,
data: {
total: 6,
success: 4,
clientError: 1,
serverError: 1,
averageResponseTime: 314,
totalResponseSize: 108416,
methodStats: [
{ method: 'GET', count: 2 },
{ method: 'POST', count: 2 },
{ method: 'PUT', count: 1 },
{ method: 'DELETE', count: 1 }
],
statusStats: [
{ status: 200, count: 3 },
{ status: 201, count: 1 },
{ status: 400, count: 1 },
{ status: 500, count: 1 }
],
topSlowRequests: [
{ url: '/api/users/export', responseTime: 1250, count: 1 },
{ url: '/api/tasks', responseTime: 180, count: 1 }
]
}
}
}
}

View File

@@ -0,0 +1,51 @@
import { Card } from '@/components/ui/card'
import { NetworkLog } from '@/types/monitor'
interface NetworkLogStatsProps {
logs: NetworkLog[]
isLoading?: boolean
}
export function NetworkLogStats({ logs, isLoading = false }: NetworkLogStatsProps) {
const stats = [
{
label: '总请求数',
value: logs.length,
color: 'text-blue-600',
},
{
label: '成功请求',
value: logs.filter(l => l.responseStatus >= 200 && l.responseStatus < 300).length,
color: 'text-green-600',
},
{
label: '失败请求',
value: logs.filter(l => l.responseStatus >= 400).length,
color: 'text-red-600',
},
{
label: '平均响应时间',
value: logs.length > 0
? Math.round(logs.reduce((sum, l) => sum + l.responseTime, 0) / logs.length) + 'ms'
: '0ms',
color: 'text-purple-600',
},
]
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 text-2xl font-semibold ${stat.color}`}>
{isLoading ? (
<div className="animate-pulse bg-gray-200 h-8 w-16 rounded"></div>
) : (
stat.value
)}
</div>
</Card>
))}
</div>
)
}

View File

@@ -0,0 +1,133 @@
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Card } from '@/components/ui/card'
import { NetworkLog } from '@/types/monitor'
import { Eye, Clock } from 'lucide-react'
interface NetworkLogTableProps {
logs: NetworkLog[]
isLoading?: boolean
onViewDetail: (log: NetworkLog) => void
}
export function NetworkLogTable({ logs, isLoading = false, onViewDetail }: NetworkLogTableProps) {
const getMethodBadge = (method: string) => {
const colors: Record<string, string> = {
GET: 'bg-blue-100 text-blue-700',
POST: 'bg-green-100 text-green-700',
PUT: 'bg-yellow-100 text-yellow-700',
DELETE: 'bg-red-100 text-red-700',
PATCH: 'bg-purple-100 text-purple-700',
}
return colors[method] || 'bg-gray-100 text-gray-700'
}
const getStatusBadge = (status: number) => {
if (status >= 200 && status < 300) {
return 'bg-green-100 text-green-700'
} else if (status >= 400 && status < 500) {
return 'bg-yellow-100 text-yellow-700'
} else if (status >= 500) {
return 'bg-red-100 text-red-700'
}
return 'bg-gray-100 text-gray-700'
}
const formatBytes = (bytes: number) => {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`
}
if (isLoading) {
return (
<Card>
<div className="p-8 space-y-4">
{Array.from({ length: 5 }).map((_, index) => (
<div key={index} className="animate-pulse">
<div className="flex space-x-4">
<div className="bg-gray-200 h-4 w-20 rounded"></div>
<div className="bg-gray-200 h-4 w-16 rounded"></div>
<div className="bg-gray-200 h-4 w-32 rounded"></div>
<div className="bg-gray-200 h-4 w-20 rounded"></div>
<div className="bg-gray-200 h-4 w-16 rounded"></div>
<div className="bg-gray-200 h-4 w-20 rounded"></div>
<div className="bg-gray-200 h-4 w-16 rounded"></div>
<div className="bg-gray-200 h-8 w-8 rounded"></div>
</div>
</div>
))}
</div>
</Card>
)
}
return (
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>URL</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{logs.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="text-center text-muted-foreground py-8">
</TableCell>
</TableRow>
) : (
logs.map((log) => (
<TableRow key={log.id}>
<TableCell className="text-sm text-muted-foreground">
{new Date(log.timestamp).toLocaleTimeString('zh-CN')}
</TableCell>
<TableCell>
<Badge className={getMethodBadge(log.method)}>
{log.method}
</Badge>
</TableCell>
<TableCell className="max-w-xs truncate text-sm" title={log.url}>
{log.url}
</TableCell>
<TableCell className="text-sm">{log.username || '-'}</TableCell>
<TableCell>
<Badge className={getStatusBadge(log.responseStatus)}>
{log.responseStatus}
</Badge>
</TableCell>
<TableCell className="text-sm">
<div className="flex items-center gap-1">
<Clock className="w-3 h-3 text-gray-400" />
{log.responseTime}ms
</div>
</TableCell>
<TableCell className="text-sm">
{log.responseSize ? formatBytes(log.responseSize) : '-'}
</TableCell>
<TableCell>
<Button
variant="ghost"
size="sm"
onClick={() => onViewDetail(log)}
>
<Eye className="w-4 h-4" />
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</Card>
)
}

View File

@@ -0,0 +1,8 @@
export { NetworkLogService } from './NetworkLogService'
export { NetworkLogStats } from './NetworkLogStats'
export { NetworkLogFilters } from './NetworkLogFilters'
export { NetworkLogTable } from './NetworkLogTable'
export { NetworkLogDetailDialog } from './NetworkLogDetailDialog'
export { NetworkLogInfo } from './NetworkLogInfo'
export type { NetworkLogFilters as NetworkLogFiltersType, NetworkLogListParams } from './NetworkLogService'

View File

@@ -0,0 +1,139 @@
'use client'
import { useState, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { NetworkLog } from '@/types/monitor'
import { Download } from 'lucide-react'
import { toast } from 'sonner'
// Import modular components
import {
NetworkLogService,
NetworkLogStats,
NetworkLogFilters,
NetworkLogTable,
NetworkLogDetailDialog,
NetworkLogInfo
} from './components'
export default function NetworkLogPage() {
const [logs, setLogs] = useState<NetworkLog[]>([])
const [isLoading, setIsLoading] = useState(false)
const [searchKeyword, setSearchKeyword] = useState('')
const [methodFilter, setMethodFilter] = useState<string>('all')
const [statusFilter, setStatusFilter] = useState<string>('all')
const [showDetailDialog, setShowDetailDialog] = useState(false)
const [selectedLog, setSelectedLog] = useState<NetworkLog | null>(null)
const [isDetailLoading, setIsDetailLoading] = useState(false)
useEffect(() => {
loadLogs()
}, [searchKeyword, methodFilter, statusFilter])
const loadLogs = async () => {
setIsLoading(true)
try {
const response = await NetworkLogService.getNetworkLogs({
page: 1,
pageSize: 100,
filters: {
searchKeyword,
method: methodFilter,
status: statusFilter
}
})
if (response.success) {
setLogs(response.data)
} else {
throw new Error(response.message || '加载网络日志失败')
}
} catch (error) {
console.error('Failed to load network logs:', error)
toast.error('加载网络日志失败,请稍后重试')
} finally {
setIsLoading(false)
}
}
const handleViewDetail = async (log: NetworkLog) => {
setSelectedLog(log)
setShowDetailDialog(true)
setIsDetailLoading(true)
try {
const response = await NetworkLogService.getNetworkLogDetail(log.id)
if (response.success) {
setSelectedLog(response.data)
}
} catch (error) {
console.error('Failed to fetch log detail:', error)
} finally {
setIsDetailLoading(false)
}
}
const handleExport = async () => {
try {
const blob = await NetworkLogService.exportNetworkLogs({
searchKeyword,
method: methodFilter,
status: statusFilter
})
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `network_logs_${new Date().getTime()}.json`
link.click()
URL.revokeObjectURL(url)
toast.success('导出成功')
} catch (error) {
console.error('Failed to export logs:', error)
toast.error('导出失败,请稍后重试')
}
}
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} disabled={isLoading || logs.length === 0}>
<Download className="w-4 h-4 mr-2" />
</Button>
</div>
<NetworkLogStats logs={logs} isLoading={isLoading} />
<NetworkLogFilters
searchKeyword={searchKeyword}
onSearchChange={setSearchKeyword}
methodFilter={methodFilter}
onMethodFilterChange={setMethodFilter}
statusFilter={statusFilter}
onStatusFilterChange={setStatusFilter}
/>
<NetworkLogTable
logs={logs}
isLoading={isLoading}
onViewDetail={handleViewDetail}
/>
<NetworkLogDetailDialog
log={selectedLog}
isOpen={showDetailDialog}
onClose={() => setShowDetailDialog(false)}
isLoading={isDetailLoading}
/>
<NetworkLogInfo />
</div>
)
}

View File

@@ -0,0 +1 @@
export * from './operationLogApi'

View File

@@ -0,0 +1,212 @@
/**
* filekorolheader: 操作日志API - 操作日志相关接口调用
* 功能:获取操作日志列表、统计、导出等功能
* 路径:/central-config/monitor/operation-log/components/operationLogApi
* 规范遵循crop-x/docs/开发项目规范.md使用SDK生成的API接口
*/
import {
listOperationLogsApiV1LogsOperationOperationLogsGet,
getOperationStatisticsApiV1LogsOperationOperationLogsStatisticsGet,
exportOperationLogsApiV1LogsOperationOperationLogsExportGet
} from '@/lib/api/sdk.gen';
// 操作日志接口
export interface OperationLog {
id: string;
created_at: string;
updated_at: string;
username: string;
user_id: string | null;
operation_type: string;
module: string;
action: string;
request_method: string;
request_url: string;
request_headers: any | null;
request_body: any | null;
request_params: any | null;
response_status: number;
response_body: any | null;
error_message: string | null;
processing_time: number;
}
// 分页参数接口
export interface OperationLogsQueryParams {
page?: number;
size?: number;
username?: string;
module?: string;
action?: string;
operation_type?: string;
response_status?: number;
start_time?: string;
end_time?: 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 OperationLogStatistics {
total_operations: number;
successful_operations: number;
failed_operations: number;
unique_users: number;
success_rate: number;
average_processing_time: number;
}
/**
* 获取操作日志列表
*/
export const fetchOperationLogs = async (params: OperationLogsQueryParams = {}) => {
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 listOperationLogsApiV1LogsOperationOperationLogsGet({
headers,
query: {
page: params.page || 1,
size: params.size || 10,
username: params.username,
module: params.module,
action: params.action,
operation_type: params.operation_type,
response_status: params.response_status,
start_time: params.start_time,
end_time: params.end_time,
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 operation logs:', error);
throw error;
}
};
/**
* 获取操作统计信息
*/
export const fetchOperationStatistics = 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 getOperationStatisticsApiV1LogsOperationOperationLogsStatisticsGet({
headers
});
return response.data;
} catch (error) {
console.error('Failed to fetch operation statistics:', error);
throw error;
}
};
/**
* 导出操作日志
*/
export const exportOperationLogs = async (params: OperationLogsQueryParams = {}) => {
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 exportOperationLogsApiV1LogsOperationOperationLogsExportGet({
headers,
query: {
username: params.username,
module: params.module,
action: params.action,
operation_type: params.operation_type,
response_status: params.response_status,
start_time: params.start_time,
end_time: params.end_time,
}
});
return response.data;
} catch (error) {
console.error('Failed to export operation logs:', error);
throw error;
}
};
/**
* 转换操作日志数据 - 适配组件使用
*/
export const transformOperationLogData = (log: any): OperationLog => ({
id: log.id,
created_at: log.created_at,
updated_at: log.updated_at,
username: log.username,
user_id: log.user_id,
operation_type: log.operation_type,
module: log.module,
action: log.action,
request_method: log.request_method,
request_url: log.request_url,
request_headers: log.request_headers,
request_body: log.request_body,
request_params: log.request_params,
response_status: log.response_status,
response_body: log.response_body,
error_message: log.error_message,
processing_time: log.processing_time,
});
/**
* 批量转换操作日志数据
*/
export const transformOperationLogsList = (logs: any[]): OperationLog[] => {
return logs.map(transformOperationLogData);
};

View File

@@ -0,0 +1,378 @@
/**
* filekorolheader: 操作日志页面 - 用户操作行为监控页面
* 功能:操作日志查询、统计、导出、筛选
* 路径:/central-config/monitor/operation-log
* 规范遵循crop-x/docs/开发项目规范.md使用SearchFormPagination重构事件驱动模式
*/
'use client';
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Download, Eye, CheckCircle, XCircle, Clock, AlertTriangle } from 'lucide-react';
import { SearchFormPagination, type SearchFieldConfig, type TableColumnConfig } from '@/components/common/searchFormPagination';
import {
fetchOperationLogs,
transformOperationLogsList,
OperationLog,
PaginationState,
OperationLogsQueryParams,
exportOperationLogs
} from './components';
export default function OperationLogPage() {
const [logs, setLogs] = useState<OperationLog[]>([]);
const [loading, setLoading] = useState(false);
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: '',
module: 'all',
status: 'all'
});
const isFirstLoad = useRef(true);
// 搜索字段配置
const searchFields: SearchFieldConfig[] = [
{
key: 'search',
label: '搜索',
type: 'text',
placeholder: '搜索操作人、模块、操作...',
},
{
key: 'module',
label: '模块',
type: 'select',
defaultValue: 'all',
options: [
{ value: 'all', label: '全部模块' },
{ value: '用户管理', label: '用户管理' },
{ value: '企业管理', label: '企业管理' },
{ value: '系统配置', label: '系统配置' },
{ value: '数据管理', label: '数据管理' },
],
},
{
key: 'status',
label: '状态',
type: 'select',
defaultValue: 'all',
options: [
{ value: 'all', label: '全部状态' },
{ value: 'success', label: '成功' },
{ value: 'failed', label: '失败' },
],
},
];
// 表格列配置
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: 'module',
label: '模块',
render: (value: string) => (
<Badge variant="outline" className="bg-blue-50 dark:bg-blue-950 text-blue-600 dark:text-blue-400">
{value || '-'}
</Badge>
),
},
{
key: 'action',
label: '操作',
render: (value: string) => (
<div className="text-sm font-medium">{value || '-'}</div>
),
},
{
key: 'processing_time',
label: '耗时',
render: (value: number) => (
<div className="flex items-center gap-1 text-sm text-muted-foreground">
<Clock className="w-3 h-3" />
{value ? `${(value * 1000).toFixed(0)}ms` : '-'}
</div>
),
},
{
key: 'response_status',
label: '状态',
render: (value: number) => {
if (value >= 200 && value < 300) {
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: 'actions',
label: '操作',
render: (_: any, row: OperationLog) => (
<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 loadOperationLogs = useCallback(async (params?: {
filters?: Record<string, string>;
pagination?: { page: number; size: number };
resetPage?: boolean;
}) => {
try {
setLoading(true);
const queryParams: OperationLogsQueryParams = {
page: params?.resetPage ? 1 : (params?.pagination?.page || pagination.page),
size: params?.pagination?.size || pagination.size,
sort_order: 'desc',
order_by: 'created_at',
};
// 处理搜索条件
const searchKeyword = params?.filters?.search ?? searchFilters.search;
if (searchKeyword) {
queryParams.username = searchKeyword;
queryParams.module = searchKeyword;
queryParams.action = searchKeyword;
}
// 处理模块筛选
const module = params?.filters?.module ?? searchFilters.module;
if (module !== 'all') {
queryParams.module = module;
}
// 处理状态筛选
const status = params?.filters?.status ?? searchFilters.status;
if (status !== 'all') {
queryParams.response_status = status === 'success' ? 200 : 400;
}
const response = await fetchOperationLogs(queryParams);
const transformedLogs = transformOperationLogsList(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 operation logs:', error);
const errorMessage = error instanceof Error ? error.message : '加载操作日志失败';
toast.error(errorMessage);
} finally {
setLoading(false);
}
}, [pagination.page, pagination.size, searchFilters]);
// 初始化数据 - 只在组件挂载时执行一次
useEffect(() => {
if (isFirstLoad.current) {
isFirstLoad.current = false;
loadOperationLogs({ resetPage: true });
}
}, [loadOperationLogs]);
// 事件处理器 - 事件驱动模式
const handleSearch = useCallback((filters: Record<string, string>) => {
setSearchFilters(filters);
loadOperationLogs({
filters,
pagination: { page: 1, size: pagination.size }
});
}, [loadOperationLogs, pagination.size]);
const handlePageChange = useCallback((page: number) => {
setPagination(prev => ({ ...prev, page }));
loadOperationLogs({
filters: searchFilters,
pagination: { page, size: pagination.size }
});
}, [loadOperationLogs, searchFilters, pagination.size]);
const handleSizeChange = useCallback((size: number) => {
setPagination(prev => ({ ...prev, size, page: 1 }));
loadOperationLogs({
filters: searchFilters,
pagination: { page: 1, size }
});
}, [loadOperationLogs, searchFilters]);
// 计算统计数据
const stats = useMemo(() => {
const totalOperations = logs.length;
const successfulOperations = logs.filter(log => log.response_status >= 200 && log.response_status < 300).length;
const failedOperations = totalOperations - successfulOperations;
const averageProcessingTime = logs.length > 0
? logs.reduce((sum, log) => sum + log.processing_time, 0) / logs.length
: 0;
return {
totalOperations,
successfulOperations,
failedOperations,
averageProcessingTime
};
}, [logs]);
// 查看详情
const handleViewDetail = (log: OperationLog) => {
toast.info(`查看操作日志详情: ${log.username} - ${log.action}`);
};
// 导出日志
const handleExport = async () => {
try {
setLoading(true);
const exportData = await exportOperationLogs({
username: searchFilters.search,
module: searchFilters.module === 'all' ? undefined : searchFilters.module,
response_status: searchFilters.status === 'all' ? undefined :
searchFilters.status === 'success' ? 200 : 400,
});
// 创建下载链接
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 = `operation_logs_${new Date().getTime()}.json`;
link.click();
URL.revokeObjectURL(url);
toast.success('导出成功');
} catch (error) {
console.error('Failed to export operation 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} disabled={loading}>
<Download className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 统计卡片 */}
<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">{stats.totalOperations}</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">{stats.successfulOperations}</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">{stats.failedOperations}</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-orange-600">{Math.round(stats.averageProcessingTime * 1000)}ms</div>
</div>
<div className="text-xs text-orange-600">
</div>
</div>
</div>
{/* 搜索、表格和分页 */}
<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]}
/>
</div>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import React from 'react';
import Link from 'next/link';
export default function MonitorPage() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4"></h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Link href="/central-config/monitor/login-log" className="bg-white rounded-lg shadow p-4 hover:shadow-md transition-shadow">
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-gray-600 text-sm"></p>
</Link>
<Link href="/central-config/monitor/operation-log" className="bg-white rounded-lg shadow p-4 hover:shadow-md transition-shadow">
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-gray-600 text-sm"></p>
</Link>
<Link href="/central-config/monitor/performance" className="bg-white rounded-lg shadow p-4 hover:shadow-md transition-shadow">
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-gray-600 text-sm"></p>
</Link>
<Link href="/central-config/monitor/network-log" className="bg-white rounded-lg shadow p-4 hover:shadow-md transition-shadow">
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-gray-600 text-sm">访</p>
</Link>
</div>
</div>
);
}

View File

@@ -0,0 +1,40 @@
import { Card } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { Badge } from '@/components/ui/badge';
import { Cpu } from 'lucide-react';
import { SystemPerformance } from '@/types/monitor';
interface CpuMetricCardProps {
performance: SystemPerformance;
getUsageColor: (usage: number) => string;
getUsageStatus: (usage: number) => string;
}
export function CpuMetricCard({ performance, getUsageColor, getUsageStatus }: CpuMetricCardProps) {
return (
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Cpu className="w-5 h-5 text-blue-600" />
<h3>CPU使用率</h3>
</div>
<Badge className={getUsageColor(performance.cpu.usage)}>
{getUsageStatus(performance.cpu.usage)}
</Badge>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">使</span>
<span className={`${getUsageColor(performance.cpu.usage)}`}>
{performance.cpu.usage.toFixed(1)}%
</span>
</div>
<Progress value={performance.cpu.usage} className="h-2" />
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground"></span>
<span>{performance.cpu.cores} </span>
</div>
</div>
</Card>
);
}

View File

@@ -0,0 +1,51 @@
import { Card } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { Badge } from '@/components/ui/badge';
import { HardDrive } from 'lucide-react';
import { SystemPerformance } from '@/types/monitor';
interface DiskMetricCardProps {
performance: SystemPerformance;
getUsageColor: (usage: number) => string;
getUsageStatus: (usage: number) => string;
formatBytes: (bytes: number) => string;
}
export function DiskMetricCard({ performance, getUsageColor, getUsageStatus, formatBytes }: DiskMetricCardProps) {
return (
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<HardDrive className="w-5 h-5 text-purple-600" />
<h3>使</h3>
</div>
<Badge className={getUsageColor(performance.disk.usage)}>
{getUsageStatus(performance.disk.usage)}
</Badge>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">使</span>
<span className={getUsageColor(performance.disk.usage)}>
{performance.disk.usage.toFixed(1)}%
</span>
</div>
<Progress value={performance.disk.usage} className="h-2" />
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<span className="text-muted-foreground"></span>
<p>{formatBytes(performance.disk.used)}</p>
</div>
<div>
<span className="text-muted-foreground"></span>
<p>{formatBytes(performance.disk.free)}</p>
</div>
<div>
<span className="text-muted-foreground"></span>
<p>{formatBytes(performance.disk.total)}</p>
</div>
</div>
</div>
</Card>
);
}

View File

@@ -0,0 +1,53 @@
import { Card } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { Server } from 'lucide-react';
import { SystemPerformance } from '@/types/monitor';
interface JvmInfoCardProps {
performance: SystemPerformance;
formatBytes: (bytes: number) => string;
}
export function JvmInfoCard({ performance, formatBytes }: JvmInfoCardProps) {
if (!performance.jvm) {
return null;
}
return (
<Card className="p-6">
<div className="flex items-center gap-2 mb-4">
<Server className="w-5 h-5 text-orange-600" />
<h3>JVM信息</h3>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<div>
<p className="text-sm text-muted-foreground">使</p>
<p className="mt-1">{performance.jvm.heapUsage.toFixed(1)}%</p>
<Progress value={performance.jvm.heapUsage} className="h-1 mt-2" />
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">
{formatBytes(performance.jvm.heapUsed)} / {formatBytes(performance.jvm.heapMax)}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">{formatBytes(performance.jvm.nonHeapUsed)}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">线</p>
<p className="mt-1">{performance.jvm.threadCount}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">GC次数</p>
<p className="mt-1">{performance.jvm.gcCount}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">GC耗时</p>
<p className="mt-1">{performance.jvm.gcTime}ms</p>
</div>
</div>
</Card>
);
}

View File

@@ -0,0 +1,47 @@
import { Card } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { Badge } from '@/components/ui/badge';
import { MemoryStick } from 'lucide-react';
import { SystemPerformance } from '@/types/monitor';
interface MemoryMetricCardProps {
performance: SystemPerformance;
getUsageColor: (usage: number) => string;
getUsageStatus: (usage: number) => string;
formatBytes: (bytes: number) => string;
}
export function MemoryMetricCard({ performance, getUsageColor, getUsageStatus, formatBytes }: MemoryMetricCardProps) {
return (
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<MemoryStick className="w-5 h-5 text-green-600" />
<h3>使</h3>
</div>
<Badge className={getUsageColor(performance.memory.usage)}>
{getUsageStatus(performance.memory.usage)}
</Badge>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">使</span>
<span className={getUsageColor(performance.memory.usage)}>
{performance.memory.usage.toFixed(1)}%
</span>
</div>
<Progress value={performance.memory.usage} className="h-2" />
<div className="grid grid-cols-2 gap-2 text-sm">
<div>
<span className="text-muted-foreground"></span>
<p>{formatBytes(performance.memory.used)}</p>
</div>
<div>
<span className="text-muted-foreground"></span>
<p>{formatBytes(performance.memory.total)}</p>
</div>
</div>
</div>
</Card>
);
}

View File

@@ -0,0 +1,20 @@
import { Card } from '@/components/ui/card';
import { Activity } from 'lucide-react';
export function PerformanceInstructions() {
return (
<Card className="p-4 bg-blue-50 border-blue-200">
<h4 className="text-blue-900 mb-2">
<Activity className="w-4 h-4 inline mr-2" />
</h4>
<ul className="space-y-1 text-sm text-blue-800">
<li> 5</li>
<li> 使60%80%</li>
<li> 20</li>
<li> 使</li>
<li> JVM和Tomcat信息仅在Java环境下可用</li>
</ul>
</Card>
);
}

View File

@@ -0,0 +1,42 @@
import { Card } from '@/components/ui/card';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { SystemPerformance } from '@/types/monitor';
interface PerformanceTrendChartProps {
history: SystemPerformance[];
}
export function PerformanceTrendChart({ history }: PerformanceTrendChartProps) {
if (history.length <= 1) {
return null;
}
const chartData = history.map((item) => ({
time: new Date(item.timestamp).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}),
CPU: item.cpu.usage.toFixed(1),
内存: item.memory.usage.toFixed(1),
磁盘: item.disk.usage.toFixed(1),
}));
return (
<Card className="p-6">
<h3 className="mb-4"></h3>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="CPU" stroke="#3b82f6" strokeWidth={2} />
<Line type="monotone" dataKey="内存" stroke="#10b981" strokeWidth={2} />
<Line type="monotone" dataKey="磁盘" stroke="#a855f7" strokeWidth={2} />
</LineChart>
</ResponsiveContainer>
</Card>
);
}

View File

@@ -0,0 +1,48 @@
import { Card } from '@/components/ui/card';
import { Activity } from 'lucide-react';
import { SystemPerformance } from '@/types/monitor';
interface TomcatInfoCardProps {
performance: SystemPerformance;
}
export function TomcatInfoCard({ performance }: TomcatInfoCardProps) {
if (!performance.tomcat) {
return null;
}
return (
<Card className="p-6">
<div className="flex items-center gap-2 mb-4">
<Activity className="w-5 h-5 text-red-600" />
<h3>Tomcat信息</h3>
</div>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
<div>
<p className="text-sm text-muted-foreground">线</p>
<p className="mt-1">
{performance.tomcat.threadCount} / {performance.tomcat.maxThreads}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">{performance.tomcat.connectionCount}</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">{performance.tomcat.requestCount.toLocaleString()}</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1 text-red-600">{performance.tomcat.errorCount}</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1">
{((performance.tomcat.errorCount / performance.tomcat.requestCount) * 100).toFixed(2)}%
</p>
</div>
</div>
</Card>
);
}

View File

@@ -0,0 +1,7 @@
export { CpuMetricCard } from './CpuMetricCard';
export { MemoryMetricCard } from './MemoryMetricCard';
export { DiskMetricCard } from './DiskMetricCard';
export { JvmInfoCard } from './JvmInfoCard';
export { TomcatInfoCard } from './TomcatInfoCard';
export { PerformanceTrendChart } from './PerformanceTrendChart';
export { PerformanceInstructions } from './PerformanceInstructions';

View File

@@ -0,0 +1,274 @@
'use client';
import { useState, useEffect } from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { RefreshCw } from 'lucide-react';
import { SystemPerformance } from '@/types/monitor';
import {
CpuMetricCard,
MemoryMetricCard,
DiskMetricCard,
JvmInfoCard,
TomcatInfoCard,
PerformanceTrendChart,
PerformanceInstructions
} from './components';
// API服务函数
const performanceApi = {
// 获取当前性能数据
getCurrentPerformance: async (): Promise<SystemPerformance> => {
try {
const response = await fetch('/api/monitor/performance/current');
if (!response.ok) {
throw new Error('Failed to fetch performance data');
}
return await response.json();
} catch (error) {
console.warn('Failed to fetch performance data, using mock data:', error);
// 如果API调用失败返回模拟数据
return getMockPerformanceData();
}
},
// 获取历史性能数据
getPerformanceHistory: async (limit: number = 20): Promise<SystemPerformance[]> => {
try {
const response = await fetch(`/api/monitor/performance/history?limit=${limit}`);
if (!response.ok) {
throw new Error('Failed to fetch performance history');
}
return await response.json();
} catch (error) {
console.warn('Failed to fetch performance history, using mock data:', error);
// 如果API调用失败返回模拟历史数据
return Array.from({ length: Math.min(5, limit) }, (_, i) => {
const mockData = getMockPerformanceData();
mockData.timestamp = new Date(Date.now() - (4 - i) * 5000).toISOString();
return mockData;
});
}
}
};
// 模拟数据生成函数
function getMockPerformanceData(): SystemPerformance {
const mockData: SystemPerformance = {
timestamp: new Date().toISOString(),
cpu: {
usage: Math.random() * 60 + 20, // 20-80%
cores: 8,
},
memory: {
total: 16384, // 16GB
used: Math.random() * 8192 + 4096, // 4-12GB
free: 0,
usage: 0,
},
disk: {
total: 512, // 512GB
used: Math.random() * 102 + 204, // 204-306GB
free: 0,
usage: 0,
},
jvm: {
heapUsed: Math.random() * 1024 + 512, // 512-1536MB
heapMax: 2048, // 2GB
heapUsage: 0,
nonHeapUsed: Math.random() * 100 + 50,
threadCount: Math.floor(Math.random() * 50 + 100),
gcCount: Math.floor(Math.random() * 10 + 50),
gcTime: Math.floor(Math.random() * 200 + 100),
},
tomcat: {
threadCount: Math.floor(Math.random() * 50 + 50),
maxThreads: 200,
connectionCount: Math.floor(Math.random() * 100 + 50),
requestCount: Math.floor(Math.random() * 10000 + 50000),
errorCount: Math.floor(Math.random() * 10),
},
};
// 计算百分比
mockData.memory.free = mockData.memory.total - mockData.memory.used;
mockData.memory.usage = (mockData.memory.used / mockData.memory.total) * 100;
mockData.disk.free = mockData.disk.total - mockData.disk.used;
mockData.disk.usage = (mockData.disk.used / mockData.disk.total) * 100;
if (mockData.jvm) {
mockData.jvm.heapUsage = (mockData.jvm.heapUsed / mockData.jvm.heapMax) * 100;
}
return mockData;
}
export default function PerformanceMonitorPage() {
const [performance, setPerformance] = useState<SystemPerformance | null>(null);
const [history, setHistory] = useState<SystemPerformance[]>([]);
const [autoRefresh, setAutoRefresh] = useState(true);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadPerformance();
const interval = setInterval(() => {
if (autoRefresh) {
loadPerformance();
}
}, 5000); // 每5秒刷新一次
return () => clearInterval(interval);
}, [autoRefresh]);
const loadPerformance = async () => {
try {
setError(null);
setLoading(true);
// 获取当前数据
const currentData = await performanceApi.getCurrentPerformance();
setPerformance(currentData);
// 保存历史数据最多保留20条
setHistory(prev => {
const newHistory = [...prev, currentData].slice(-20);
return newHistory;
});
} catch (err) {
setError(err instanceof Error ? err.message : '加载性能数据失败');
console.error('Failed to load performance data:', err);
} finally {
setLoading(false);
}
};
const getUsageColor = (usage: number) => {
if (usage < 60) return 'text-green-600';
if (usage < 80) return 'text-yellow-600';
return 'text-red-600';
};
const getUsageStatus = (usage: number) => {
if (usage < 60) return '正常';
if (usage < 80) return '警告';
return '危险';
};
const formatBytes = (bytes: number) => {
if (bytes < 1024) return `${bytes.toFixed(2)} MB`;
return `${(bytes / 1024).toFixed(2)} GB`;
};
if (loading && !performance) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<RefreshCw className="w-8 h-8 animate-spin text-green-600 mx-auto mb-2" />
<p className="text-muted-foreground">...</p>
</div>
</div>
);
}
if (error && !performance) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<p className="text-red-600 mb-4">: {error}</p>
<Button onClick={loadPerformance} variant="outline">
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
);
}
if (!performance) {
return null;
}
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>
<div className="flex items-center gap-2">
<Badge variant={autoRefresh ? 'default' : 'outline'}>
{autoRefresh ? '自动刷新' : '已暂停'}
</Badge>
<Button
variant="outline"
size="sm"
onClick={() => setAutoRefresh(!autoRefresh)}
>
{autoRefresh ? '暂停' : '启动'}
</Button>
<Button
variant="outline"
size="sm"
onClick={loadPerformance}
disabled={loading}
>
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
</Button>
</div>
</div>
{error && (
<div className="p-4 border border-yellow-200 bg-yellow-50 rounded-md">
<p className="text-yellow-800 text-sm">
: {error} ()
</p>
</div>
)}
{/* CPU和内存卡片 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<CpuMetricCard
performance={performance}
getUsageColor={getUsageColor}
getUsageStatus={getUsageStatus}
/>
<MemoryMetricCard
performance={performance}
getUsageColor={getUsageColor}
getUsageStatus={getUsageStatus}
formatBytes={formatBytes}
/>
</div>
{/* 磁盘卡片 */}
<DiskMetricCard
performance={performance}
getUsageColor={getUsageColor}
getUsageStatus={getUsageStatus}
formatBytes={formatBytes}
/>
{/* JVM信息卡片 */}
<JvmInfoCard
performance={performance}
formatBytes={formatBytes}
/>
{/* Tomcat信息卡片 */}
<TomcatInfoCard
performance={performance}
/>
{/* 性能趋势图 */}
<PerformanceTrendChart
history={history}
/>
{/* 使用说明 */}
<PerformanceInstructions />
</div>
);
}