生产管理系统 - 操作日志页面提交
This commit is contained in:
@@ -247,7 +247,9 @@ export default function LoginLogPage() {
|
||||
page: response.page,
|
||||
size: response.size,
|
||||
total: response.total,
|
||||
totalPages: response.totalPages,
|
||||
totalPages: response.total === 0
|
||||
? 0
|
||||
: Math.floor(response.total / response.size) + 1,
|
||||
hasNext: response.hasNext,
|
||||
hasPrev: response.hasPrev,
|
||||
});
|
||||
@@ -260,25 +262,6 @@ export default function LoginLogPage() {
|
||||
}
|
||||
}, [pagination.page, pagination.size, searchFilters]);
|
||||
|
||||
// 加载统计数据
|
||||
const loadStatistics = useCallback(async () => {
|
||||
try {
|
||||
const stats = await fetchLoginStatistics();
|
||||
setStatistics(stats);
|
||||
} catch (error) {
|
||||
console.error('Failed to load login statistics:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 初始化数据 - 只在组件挂载时执行一次
|
||||
useEffect(() => {
|
||||
if (isFirstLoad.current) {
|
||||
isFirstLoad.current = false;
|
||||
loadLoginLogs({ resetPage: true });
|
||||
loadStatistics();
|
||||
}
|
||||
}, [loadLoginLogs, loadStatistics]);
|
||||
|
||||
// 事件处理器 - 事件驱动模式
|
||||
const handleSearch = useCallback((filters: Record<string, string>) => {
|
||||
setSearchFilters(filters);
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { OperationLog } from '@/types/monitor'
|
||||
import { FileText } from 'lucide-react'
|
||||
|
||||
interface OperationLogDetailDialogProps {
|
||||
log: OperationLog | null
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
export function OperationLogDetailDialog({
|
||||
log,
|
||||
isOpen,
|
||||
onClose,
|
||||
isLoading = false
|
||||
}: OperationLogDetailDialogProps) {
|
||||
const getModuleLabel = (module: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
user: '用户管理',
|
||||
role: '角色管理',
|
||||
permission: '权限管理',
|
||||
machinery: '农机管理',
|
||||
driver: '驾驶员管理',
|
||||
task: '任务管理',
|
||||
system: '系统配置',
|
||||
other: '其他',
|
||||
}
|
||||
return labels[module] || module
|
||||
}
|
||||
|
||||
const getActionLabel = (action: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
create: '新增',
|
||||
update: '修改',
|
||||
delete: '删除',
|
||||
view: '查看',
|
||||
export: '导出',
|
||||
import: '导入',
|
||||
login: '登录',
|
||||
logout: '登出',
|
||||
}
|
||||
return labels[action] || action
|
||||
}
|
||||
|
||||
const getActionBadge = (action: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
create: 'bg-green-100 text-green-700',
|
||||
update: 'bg-blue-100 text-blue-700',
|
||||
delete: 'bg-red-100 text-red-700',
|
||||
view: 'bg-gray-100 text-gray-700',
|
||||
export: 'bg-purple-100 text-purple-700',
|
||||
import: 'bg-yellow-100 text-yellow-700',
|
||||
}
|
||||
return colors[action] || 'bg-gray-100 text-gray-700'
|
||||
}
|
||||
|
||||
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">
|
||||
<FileText className="w-5 h-5 text-green-600" />
|
||||
操作日志详情
|
||||
</div>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="space-y-4">
|
||||
{Array.from({ length: 6 }).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 font-medium">{log.username}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">操作时间</p>
|
||||
<p className="mt-1">{new Date(log.operationTime).toLocaleString('zh-CN')}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">操作模块</p>
|
||||
<p className="mt-1">
|
||||
<Badge variant="outline">{getModuleLabel(log.module)}</Badge>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">操作类型</p>
|
||||
<p className="mt-1">
|
||||
<Badge className={getActionBadge(log.action)}>
|
||||
{getActionLabel(log.action)}
|
||||
</Badge>
|
||||
</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.ipAddress}
|
||||
</code>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">耗时</p>
|
||||
<p className="mt-1">{log.duration ? `${log.duration}ms` : '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">操作描述</p>
|
||||
<p className="mt-1">{log.description}</p>
|
||||
</div>
|
||||
|
||||
{log.requestUrl && (
|
||||
<div>
|
||||
<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">
|
||||
{log.requestMethod} {log.requestUrl}
|
||||
</code>
|
||||
</p>
|
||||
</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">
|
||||
{(() => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(log.requestParams), null, 2)
|
||||
} catch {
|
||||
return log.requestParams
|
||||
}
|
||||
})()}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{log.responseData && (
|
||||
<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.responseData), null, 2)
|
||||
} catch {
|
||||
return log.responseData
|
||||
}
|
||||
})()}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{log.errorMessage && (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">错误信息</p>
|
||||
<p className="mt-1 text-red-600 bg-red-50 p-3 rounded">{log.errorMessage}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
关闭
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,81 +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 OperationLogFiltersProps {
|
||||
searchKeyword: string
|
||||
onSearchChange: (value: string) => void
|
||||
moduleFilter: string
|
||||
onModuleFilterChange: (value: string) => void
|
||||
actionFilter: string
|
||||
onActionFilterChange: (value: string) => void
|
||||
statusFilter: string
|
||||
onStatusFilterChange: (value: string) => void
|
||||
}
|
||||
|
||||
export function OperationLogFilters({
|
||||
searchKeyword,
|
||||
onSearchChange,
|
||||
moduleFilter,
|
||||
onModuleFilterChange,
|
||||
actionFilter,
|
||||
onActionFilterChange,
|
||||
statusFilter,
|
||||
onStatusFilterChange
|
||||
}: OperationLogFiltersProps) {
|
||||
return (
|
||||
<Card className="p-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 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="搜索用户名、操作描述..."
|
||||
value={searchKeyword}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<Select value={moduleFilter} onValueChange={onModuleFilterChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="操作模块" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部模块</SelectItem>
|
||||
<SelectItem value="user">用户管理</SelectItem>
|
||||
<SelectItem value="role">角色管理</SelectItem>
|
||||
<SelectItem value="permission">权限管理</SelectItem>
|
||||
<SelectItem value="machinery">农机管理</SelectItem>
|
||||
<SelectItem value="driver">驾驶员管理</SelectItem>
|
||||
<SelectItem value="task">任务管理</SelectItem>
|
||||
<SelectItem value="system">系统配置</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={actionFilter} onValueChange={onActionFilterChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="操作类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部类型</SelectItem>
|
||||
<SelectItem value="create">新增</SelectItem>
|
||||
<SelectItem value="update">修改</SelectItem>
|
||||
<SelectItem value="delete">删除</SelectItem>
|
||||
<SelectItem value="view">查看</SelectItem>
|
||||
<SelectItem value="export">导出</SelectItem>
|
||||
<SelectItem value="import">导入</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={statusFilter} onValueChange={onStatusFilterChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="操作状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部状态</SelectItem>
|
||||
<SelectItem value="success">成功</SelectItem>
|
||||
<SelectItem value="failed">失败</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Activity } from 'lucide-react'
|
||||
|
||||
export function OperationLogInfo() {
|
||||
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>• 记录用户在系统中的所有关键操作,包括数据修改、流程触发、配置变更等</li>
|
||||
<li>• 详细记录操作人、时间、模块、动作及请求详情</li>
|
||||
<li>• 支持按多个维度进行复杂条件搜索和过滤</li>
|
||||
<li>• 可查看原始请求信息,满足运维审计需求</li>
|
||||
<li>• 失败操作会记录错误信息,便于事故追溯</li>
|
||||
</ul>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,314 +0,0 @@
|
||||
import { OperationLog } from '@/types/monitor'
|
||||
import { ApiResponse, PaginatedResponse, PaginationParams } from '@/types'
|
||||
|
||||
export interface OperationLogFilters {
|
||||
searchKeyword?: string
|
||||
module?: string
|
||||
action?: string
|
||||
status?: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
export interface OperationLogListParams extends PaginationParams {
|
||||
filters?: OperationLogFilters
|
||||
}
|
||||
|
||||
export class OperationLogService {
|
||||
private static baseUrl = '/api/monitor/operation-logs'
|
||||
|
||||
/**
|
||||
* 获取操作日志列表
|
||||
*/
|
||||
static async getOperationLogs(params: OperationLogListParams = {}): Promise<PaginatedResponse<OperationLog>> {
|
||||
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.module && params.filters.module !== 'all') queryParams.append('module', params.filters.module)
|
||||
if (params.filters.action && params.filters.action !== 'all') queryParams.append('action', params.filters.action)
|
||||
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)
|
||||
}
|
||||
|
||||
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 operation logs:', error)
|
||||
// 降级处理:返回mock数据
|
||||
return this.getMockData(params)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作日志详情
|
||||
*/
|
||||
static async getOperationLogDetail(id: string): Promise<ApiResponse<OperationLog>> {
|
||||
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 operation log detail:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出操作日志
|
||||
*/
|
||||
static async exportOperationLogs(filters?: OperationLogFilters): Promise<Blob> {
|
||||
try {
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
if (filters) {
|
||||
if (filters.searchKeyword) queryParams.append('searchKeyword', filters.searchKeyword)
|
||||
if (filters.module && filters.module !== 'all') queryParams.append('module', filters.module)
|
||||
if (filters.action && filters.action !== 'all') queryParams.append('action', filters.action)
|
||||
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}/export?${queryParams}`)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
return response.blob()
|
||||
} catch (error) {
|
||||
console.error('Failed to export operation logs:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作日志统计信息
|
||||
*/
|
||||
static async getOperationLogStats(filters?: OperationLogFilters): Promise<ApiResponse<{
|
||||
total: number
|
||||
success: number
|
||||
failed: number
|
||||
averageDuration: number
|
||||
moduleStats: Array<{ module: string, count: number }>
|
||||
actionStats: Array<{ action: string, count: number }>
|
||||
}>> {
|
||||
try {
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
if (filters) {
|
||||
if (filters.module && filters.module !== 'all') queryParams.append('module', filters.module)
|
||||
if (filters.action && filters.action !== 'all') queryParams.append('action', filters.action)
|
||||
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 operation log stats:', error)
|
||||
// 降级处理:返回mock统计数据
|
||||
return this.getMockStats()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock数据 - 用于降级处理
|
||||
*/
|
||||
private static getMockData(params: OperationLogListParams): PaginatedResponse<OperationLog> {
|
||||
const mockLogs: OperationLog[] = [
|
||||
{
|
||||
id: 'op-1',
|
||||
userId: 'user-1',
|
||||
username: 'admin',
|
||||
operationTime: '2024-10-21T09:35:00',
|
||||
module: 'user',
|
||||
action: 'create',
|
||||
description: '创建用户账号: zhangsan',
|
||||
ipAddress: '192.168.1.100',
|
||||
requestUrl: '/api/users',
|
||||
requestMethod: 'POST',
|
||||
requestParams: JSON.stringify({ username: 'zhangsan', name: '张三' }),
|
||||
duration: 150,
|
||||
status: 'success',
|
||||
},
|
||||
{
|
||||
id: 'op-2',
|
||||
userId: 'user-2',
|
||||
username: 'zhangsan',
|
||||
operationTime: '2024-10-21T10:20:00',
|
||||
module: 'machinery',
|
||||
action: 'update',
|
||||
description: '更新农机信息: 约翰迪尔拖拉机',
|
||||
ipAddress: '192.168.1.101',
|
||||
requestUrl: '/api/machinery/123',
|
||||
requestMethod: 'PUT',
|
||||
duration: 89,
|
||||
status: 'success',
|
||||
},
|
||||
{
|
||||
id: 'op-3',
|
||||
userId: 'user-3',
|
||||
username: 'lisi',
|
||||
operationTime: '2024-10-21T11:25:00',
|
||||
module: 'role',
|
||||
action: 'delete',
|
||||
description: '删除角色: 临时操作员',
|
||||
ipAddress: '192.168.1.102',
|
||||
requestUrl: '/api/roles/456',
|
||||
requestMethod: 'DELETE',
|
||||
duration: 120,
|
||||
status: 'failed',
|
||||
errorMessage: '该角色下仍有关联用户,无法删除',
|
||||
},
|
||||
{
|
||||
id: 'op-4',
|
||||
userId: 'user-1',
|
||||
username: 'admin',
|
||||
operationTime: '2024-10-21T14:50:00',
|
||||
module: 'system',
|
||||
action: 'update',
|
||||
description: '修改系统配置: 会话超时时间',
|
||||
ipAddress: '192.168.1.100',
|
||||
requestUrl: '/api/system/settings',
|
||||
requestMethod: 'PUT',
|
||||
duration: 95,
|
||||
status: 'success',
|
||||
},
|
||||
{
|
||||
id: 'op-5',
|
||||
userId: 'user-2',
|
||||
username: 'zhangsan',
|
||||
operationTime: '2024-10-21T15:35:00',
|
||||
module: 'task',
|
||||
action: 'create',
|
||||
description: '创建作业任务: 小麦播种作业',
|
||||
ipAddress: '192.168.1.101',
|
||||
requestUrl: '/api/tasks',
|
||||
requestMethod: 'POST',
|
||||
duration: 180,
|
||||
status: 'success',
|
||||
},
|
||||
{
|
||||
id: 'op-6',
|
||||
userId: 'user-1',
|
||||
username: 'admin',
|
||||
operationTime: '2024-10-21T16:15:00',
|
||||
module: 'user',
|
||||
action: 'export',
|
||||
description: '导出用户列表数据',
|
||||
ipAddress: '192.168.1.100',
|
||||
requestUrl: '/api/users/export',
|
||||
requestMethod: 'GET',
|
||||
duration: 1250,
|
||||
status: 'success',
|
||||
},
|
||||
]
|
||||
|
||||
// 应用筛选器
|
||||
let filteredLogs = mockLogs.filter(log => {
|
||||
if (params.filters?.searchKeyword) {
|
||||
const keyword = params.filters.searchKeyword.toLowerCase()
|
||||
if (!log.username.toLowerCase().includes(keyword) &&
|
||||
!log.description.toLowerCase().includes(keyword) &&
|
||||
!log.module.toLowerCase().includes(keyword)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (params.filters?.module && params.filters.module !== 'all') {
|
||||
if (log.module !== params.filters.module) return false
|
||||
}
|
||||
|
||||
if (params.filters?.action && params.filters.action !== 'all') {
|
||||
if (log.action !== params.filters.action) return false
|
||||
}
|
||||
|
||||
if (params.filters?.status && params.filters.status !== 'all') {
|
||||
if (log.status !== params.filters.status) 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
|
||||
failed: number
|
||||
averageDuration: number
|
||||
moduleStats: Array<{ module: string, count: number }>
|
||||
actionStats: Array<{ action: string, count: number }>
|
||||
}> {
|
||||
return {
|
||||
code: 200,
|
||||
message: 'success',
|
||||
success: true,
|
||||
data: {
|
||||
total: 6,
|
||||
success: 5,
|
||||
failed: 1,
|
||||
averageDuration: 314,
|
||||
moduleStats: [
|
||||
{ module: 'user', count: 2 },
|
||||
{ module: 'machinery', count: 1 },
|
||||
{ module: 'role', count: 1 },
|
||||
{ module: 'system', count: 1 },
|
||||
{ module: 'task', count: 1 }
|
||||
],
|
||||
actionStats: [
|
||||
{ action: 'create', count: 2 },
|
||||
{ action: 'update', count: 2 },
|
||||
{ action: 'delete', count: 1 },
|
||||
{ action: 'export', count: 1 }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { OperationLog } from '@/types/monitor'
|
||||
|
||||
interface OperationLogStatsProps {
|
||||
logs: OperationLog[]
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
export function OperationLogStats({ logs, isLoading = false }: OperationLogStatsProps) {
|
||||
const stats = [
|
||||
{
|
||||
label: '总操作数',
|
||||
value: logs.length,
|
||||
color: 'text-blue-600',
|
||||
},
|
||||
{
|
||||
label: '成功操作',
|
||||
value: logs.filter(l => l.status === 'success').length,
|
||||
color: 'text-green-600',
|
||||
},
|
||||
{
|
||||
label: '失败操作',
|
||||
value: logs.filter(l => l.status === 'failed').length,
|
||||
color: 'text-red-600',
|
||||
},
|
||||
{
|
||||
label: '平均耗时',
|
||||
value: logs.length > 0
|
||||
? Math.round(logs.reduce((sum, l) => sum + (l.duration || 0), 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>
|
||||
)
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
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 { OperationLog } from '@/types/monitor'
|
||||
import { Eye } from 'lucide-react'
|
||||
|
||||
interface OperationLogTableProps {
|
||||
logs: OperationLog[]
|
||||
isLoading?: boolean
|
||||
onViewDetail: (log: OperationLog) => void
|
||||
}
|
||||
|
||||
export function OperationLogTable({ logs, isLoading = false, onViewDetail }: OperationLogTableProps) {
|
||||
const getModuleLabel = (module: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
user: '用户管理',
|
||||
role: '角色管理',
|
||||
permission: '权限管理',
|
||||
machinery: '农机管理',
|
||||
driver: '驾驶员管理',
|
||||
task: '任务管理',
|
||||
system: '系统配置',
|
||||
other: '其他',
|
||||
}
|
||||
return labels[module] || module
|
||||
}
|
||||
|
||||
const getActionLabel = (action: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
create: '新增',
|
||||
update: '修改',
|
||||
delete: '删除',
|
||||
view: '查看',
|
||||
export: '导出',
|
||||
import: '导入',
|
||||
login: '登录',
|
||||
logout: '登出',
|
||||
}
|
||||
return labels[action] || action
|
||||
}
|
||||
|
||||
const getActionBadge = (action: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
create: 'bg-green-100 text-green-700',
|
||||
update: 'bg-blue-100 text-blue-700',
|
||||
delete: 'bg-red-100 text-red-700',
|
||||
view: 'bg-gray-100 text-gray-700',
|
||||
export: 'bg-purple-100 text-purple-700',
|
||||
import: 'bg-yellow-100 text-yellow-700',
|
||||
}
|
||||
return colors[action] || 'bg-gray-100 text-gray-700'
|
||||
}
|
||||
|
||||
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-32 rounded"></div>
|
||||
<div className="bg-gray-200 h-4 w-16 rounded"></div>
|
||||
<div className="bg-gray-200 h-4 w-24 rounded"></div>
|
||||
<div className="bg-gray-200 h-4 w-40 rounded"></div>
|
||||
<div className="bg-gray-200 h-4 w-16 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>模块</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>{log.username}</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">
|
||||
{new Date(log.operationTime).toLocaleString('zh-CN')}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{getModuleLabel(log.module)}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={getActionBadge(log.action)}>
|
||||
{getActionLabel(log.action)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="max-w-xs truncate" title={log.description}>
|
||||
{log.description}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
{log.duration ? `${log.duration}ms` : '-'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{log.status === 'success' ? (
|
||||
<Badge className="bg-green-100 text-green-700">成功</Badge>
|
||||
) : (
|
||||
<Badge className="bg-red-100 text-red-700">失败</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onViewDetail(log)}
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1 @@
|
||||
export { OperationLogService } from './OperationLogService'
|
||||
export { OperationLogStats } from './OperationLogStats'
|
||||
export { OperationLogFilters } from './OperationLogFilters'
|
||||
export { OperationLogTable } from './OperationLogTable'
|
||||
export { OperationLogDetailDialog } from './OperationLogDetailDialog'
|
||||
export { OperationLogInfo } from './OperationLogInfo'
|
||||
|
||||
export type { OperationLogListParams } from './OperationLogService'
|
||||
export type { OperationLogFilters as OperationLogFiltersType } from './OperationLogService'
|
||||
export * from './operationLogApi'
|
||||
@@ -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);
|
||||
};
|
||||
@@ -1,152 +1,378 @@
|
||||
'use client'
|
||||
/**
|
||||
* filekorolheader: 操作日志页面 - 用户操作行为监控页面
|
||||
* 功能:操作日志查询、统计、导出、筛选
|
||||
* 路径:/central-config/monitor/operation-log
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用SearchFormPagination重构,事件驱动模式
|
||||
*/
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { OperationLog } from '@/types/monitor'
|
||||
import { Download } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
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 modular components
|
||||
import { SearchFormPagination, type SearchFieldConfig, type TableColumnConfig } from '@/components/common/searchFormPagination';
|
||||
import {
|
||||
OperationLogService,
|
||||
OperationLogStats,
|
||||
OperationLogFilters,
|
||||
OperationLogTable,
|
||||
OperationLogDetailDialog,
|
||||
OperationLogInfo
|
||||
} from './components'
|
||||
fetchOperationLogs,
|
||||
transformOperationLogsList,
|
||||
OperationLog,
|
||||
PaginationState,
|
||||
OperationLogsQueryParams,
|
||||
exportOperationLogs
|
||||
} from './components';
|
||||
|
||||
export default function OperationLogPage() {
|
||||
const [logs, setLogs] = useState<OperationLog[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [searchKeyword, setSearchKeyword] = useState('')
|
||||
const [moduleFilter, setModuleFilter] = useState<string>('all')
|
||||
const [actionFilter, setActionFilter] = useState<string>('all')
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all')
|
||||
const [showDetailDialog, setShowDetailDialog] = useState(false)
|
||||
const [selectedLog, setSelectedLog] = useState<OperationLog | null>(null)
|
||||
const [isDetailLoading, setIsDetailLoading] = useState(false)
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
loadLogs()
|
||||
}, [searchKeyword, moduleFilter, actionFilter, statusFilter])
|
||||
// 搜索字段配置
|
||||
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 loadLogs = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const response = await OperationLogService.getOperationLogs({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
filters: {
|
||||
searchKeyword,
|
||||
module: moduleFilter,
|
||||
action: actionFilter,
|
||||
status: statusFilter
|
||||
// 表格列配置
|
||||
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>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (response.success) {
|
||||
setLogs(response.data)
|
||||
} else {
|
||||
throw new Error(response.message || '加载操作日志失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load operation logs:', error)
|
||||
toast.error('加载操作日志失败,请稍后重试')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleViewDetail = async (log: OperationLog) => {
|
||||
setSelectedLog(log)
|
||||
setShowDetailDialog(true)
|
||||
|
||||
setIsDetailLoading(true)
|
||||
// 加载操作日志数据 - 事件驱动模式
|
||||
const loadOperationLogs = useCallback(async (params?: {
|
||||
filters?: Record<string, string>;
|
||||
pagination?: { page: number; size: number };
|
||||
resetPage?: boolean;
|
||||
}) => {
|
||||
try {
|
||||
const response = await OperationLogService.getOperationLogDetail(log.id)
|
||||
if (response.success) {
|
||||
setSelectedLog(response.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch log detail:', error)
|
||||
} finally {
|
||||
setIsDetailLoading(false)
|
||||
}
|
||||
}
|
||||
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 {
|
||||
const blob = await OperationLogService.exportOperationLogs({
|
||||
searchKeyword,
|
||||
module: moduleFilter,
|
||||
action: actionFilter,
|
||||
status: statusFilter
|
||||
})
|
||||
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 url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `operation_logs_${new Date().getTime()}.json`
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
// 创建下载链接
|
||||
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('导出成功')
|
||||
toast.success('导出成功');
|
||||
} catch (error) {
|
||||
console.error('Failed to export logs:', error)
|
||||
toast.error('导出失败,请稍后重试')
|
||||
console.error('Failed to export operation logs:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '导出失败';
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadLogs()
|
||||
}
|
||||
};
|
||||
|
||||
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 gap-2">
|
||||
<Button variant="outline" onClick={handleRefresh} disabled={isLoading}>
|
||||
刷新
|
||||
</Button>
|
||||
<Button onClick={handleExport} disabled={isLoading || logs.length === 0}>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
导出日志
|
||||
</Button>
|
||||
<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>
|
||||
|
||||
<OperationLogStats logs={logs} isLoading={isLoading} />
|
||||
|
||||
<OperationLogFilters
|
||||
searchKeyword={searchKeyword}
|
||||
onSearchChange={setSearchKeyword}
|
||||
moduleFilter={moduleFilter}
|
||||
onModuleFilterChange={setModuleFilter}
|
||||
actionFilter={actionFilter}
|
||||
onActionFilterChange={setActionFilter}
|
||||
statusFilter={statusFilter}
|
||||
onStatusFilterChange={setStatusFilter}
|
||||
{/* 搜索、表格和分页 */}
|
||||
<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]}
|
||||
/>
|
||||
|
||||
<OperationLogTable
|
||||
logs={logs}
|
||||
isLoading={isLoading}
|
||||
onViewDetail={handleViewDetail}
|
||||
/>
|
||||
|
||||
<OperationLogDetailDialog
|
||||
log={selectedLog}
|
||||
isOpen={showDetailDialog}
|
||||
onClose={() => setShowDetailDialog(false)}
|
||||
isLoading={isDetailLoading}
|
||||
/>
|
||||
|
||||
<OperationLogInfo />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -277,7 +277,7 @@ export default function TenantUserManagementPage() {
|
||||
const loadUsers = useCallback(async (resetPage = false) => {
|
||||
try {
|
||||
dispatch({ type: 'SET_LOADING', payload: true });
|
||||
|
||||
debugger
|
||||
const params: UsersQueryParams = {
|
||||
page: resetPage ? 1 : state.pagination.page,
|
||||
size: state.pagination.size,
|
||||
@@ -327,7 +327,7 @@ export default function TenantUserManagementPage() {
|
||||
payload: error instanceof Error ? error.message : '加载用户数据失败'
|
||||
});
|
||||
}
|
||||
}, [state.pagination.page, state.pagination.size, state.sortBy, state.sortOrder, searchFilters]);
|
||||
}, []);
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = useCallback((filters: Record<string, string>) => {
|
||||
@@ -411,7 +411,7 @@ export default function TenantUserManagementPage() {
|
||||
// 加载数据
|
||||
useEffect(() => {
|
||||
loadUsers();
|
||||
}, [loadUsers]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
Reference in New Issue
Block a user