diff --git a/crop-x/src/app/(app)/central-config/monitor/login-log/page.tsx b/crop-x/src/app/(app)/central-config/monitor/login-log/page.tsx index 05f3658..cfd6879 100644 --- a/crop-x/src/app/(app)/central-config/monitor/login-log/page.tsx +++ b/crop-x/src/app/(app)/central-config/monitor/login-log/page.tsx @@ -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) => { setSearchFilters(filters); diff --git a/crop-x/src/app/(app)/central-config/monitor/operation-log/components/OperationLogDetailDialog.tsx b/crop-x/src/app/(app)/central-config/monitor/operation-log/components/OperationLogDetailDialog.tsx deleted file mode 100644 index e3292ae..0000000 --- a/crop-x/src/app/(app)/central-config/monitor/operation-log/components/OperationLogDetailDialog.tsx +++ /dev/null @@ -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 = { - user: '用户管理', - role: '角色管理', - permission: '权限管理', - machinery: '农机管理', - driver: '驾驶员管理', - task: '任务管理', - system: '系统配置', - other: '其他', - } - return labels[module] || module - } - - const getActionLabel = (action: string) => { - const labels: Record = { - create: '新增', - update: '修改', - delete: '删除', - view: '查看', - export: '导出', - import: '导入', - login: '登录', - logout: '登出', - } - return labels[action] || action - } - - const getActionBadge = (action: string) => { - const colors: Record = { - 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 ( - - - - -
- - 操作日志详情 -
-
-
- - {isLoading ? ( -
- {Array.from({ length: 6 }).map((_, index) => ( -
-
-
-
- ))} -
- ) : log ? ( -
-
-
-

操作人

-

{log.username}

-
-
-

操作时间

-

{new Date(log.operationTime).toLocaleString('zh-CN')}

-
-
-

操作模块

-

- {getModuleLabel(log.module)} -

-
-
-

操作类型

-

- - {getActionLabel(log.action)} - -

-
-
-

IP地址

-

- - {log.ipAddress} - -

-
-
-

耗时

-

{log.duration ? `${log.duration}ms` : '-'}

-
-
- -
-

操作描述

-

{log.description}

-
- - {log.requestUrl && ( -
-

请求URL

-

- - {log.requestMethod} {log.requestUrl} - -

-
- )} - - {log.requestParams && ( -
-

请求参数

-
-                  {(() => {
-                    try {
-                      return JSON.stringify(JSON.parse(log.requestParams), null, 2)
-                    } catch {
-                      return log.requestParams
-                    }
-                  })()}
-                
-
- )} - - {log.responseData && ( -
-

响应数据

-
-                  {(() => {
-                    try {
-                      return JSON.stringify(JSON.parse(log.responseData), null, 2)
-                    } catch {
-                      return log.responseData
-                    }
-                  })()}
-                
-
- )} - - {log.errorMessage && ( -
-

错误信息

-

{log.errorMessage}

-
- )} -
- ) : null} - - - - -
-
- ) -} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/monitor/operation-log/components/OperationLogFilters.tsx b/crop-x/src/app/(app)/central-config/monitor/operation-log/components/OperationLogFilters.tsx deleted file mode 100644 index 59610bd..0000000 --- a/crop-x/src/app/(app)/central-config/monitor/operation-log/components/OperationLogFilters.tsx +++ /dev/null @@ -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 ( - -
-
- - onSearchChange(e.target.value)} - className="pl-10" - /> -
- - - -
-
- ) -} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/monitor/operation-log/components/OperationLogInfo.tsx b/crop-x/src/app/(app)/central-config/monitor/operation-log/components/OperationLogInfo.tsx deleted file mode 100644 index 34f0a24..0000000 --- a/crop-x/src/app/(app)/central-config/monitor/operation-log/components/OperationLogInfo.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Card } from '@/components/ui/card' -import { Activity } from 'lucide-react' - -export function OperationLogInfo() { - return ( - -

- - 操作日志说明 -

-
    -
  • • 记录用户在系统中的所有关键操作,包括数据修改、流程触发、配置变更等
  • -
  • • 详细记录操作人、时间、模块、动作及请求详情
  • -
  • • 支持按多个维度进行复杂条件搜索和过滤
  • -
  • • 可查看原始请求信息,满足运维审计需求
  • -
  • • 失败操作会记录错误信息,便于事故追溯
  • -
-
- ) -} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/monitor/operation-log/components/OperationLogService.ts b/crop-x/src/app/(app)/central-config/monitor/operation-log/components/OperationLogService.ts deleted file mode 100644 index 7de95bc..0000000 --- a/crop-x/src/app/(app)/central-config/monitor/operation-log/components/OperationLogService.ts +++ /dev/null @@ -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> { - 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> { - 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 { - 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 - 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 { - 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 } - ] - } - } - } -} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/monitor/operation-log/components/OperationLogStats.tsx b/crop-x/src/app/(app)/central-config/monitor/operation-log/components/OperationLogStats.tsx deleted file mode 100644 index e075d35..0000000 --- a/crop-x/src/app/(app)/central-config/monitor/operation-log/components/OperationLogStats.tsx +++ /dev/null @@ -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 ( -
- {stats.map((stat, index) => ( - -
{stat.label}
-
- {isLoading ? ( -
- ) : ( - stat.value - )} -
-
- ))} -
- ) -} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/monitor/operation-log/components/OperationLogTable.tsx b/crop-x/src/app/(app)/central-config/monitor/operation-log/components/OperationLogTable.tsx deleted file mode 100644 index 336be68..0000000 --- a/crop-x/src/app/(app)/central-config/monitor/operation-log/components/OperationLogTable.tsx +++ /dev/null @@ -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 = { - user: '用户管理', - role: '角色管理', - permission: '权限管理', - machinery: '农机管理', - driver: '驾驶员管理', - task: '任务管理', - system: '系统配置', - other: '其他', - } - return labels[module] || module - } - - const getActionLabel = (action: string) => { - const labels: Record = { - create: '新增', - update: '修改', - delete: '删除', - view: '查看', - export: '导出', - import: '导入', - login: '登录', - logout: '登出', - } - return labels[action] || action - } - - const getActionBadge = (action: string) => { - const colors: Record = { - 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 ( - -
- {Array.from({ length: 5 }).map((_, index) => ( -
-
-
-
-
-
-
-
-
-
-
-
- ))} -
-
- ) - } - - return ( - - - - - 操作人 - 操作时间 - 模块 - 操作 - 描述 - 耗时 - 状态 - 操作 - - - - {logs.length === 0 ? ( - - - 暂无操作日志 - - - ) : ( - logs.map((log) => ( - - {log.username} - - {new Date(log.operationTime).toLocaleString('zh-CN')} - - - {getModuleLabel(log.module)} - - - - {getActionLabel(log.action)} - - - - {log.description} - - - {log.duration ? `${log.duration}ms` : '-'} - - - {log.status === 'success' ? ( - 成功 - ) : ( - 失败 - )} - - - - - - )) - )} - -
-
- ) -} \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/monitor/operation-log/components/index.ts b/crop-x/src/app/(app)/central-config/monitor/operation-log/components/index.ts index 774cae1..02cc813 100644 --- a/crop-x/src/app/(app)/central-config/monitor/operation-log/components/index.ts +++ b/crop-x/src/app/(app)/central-config/monitor/operation-log/components/index.ts @@ -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' \ No newline at end of file +export * from './operationLogApi' \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/monitor/operation-log/components/operationLogApi.ts b/crop-x/src/app/(app)/central-config/monitor/operation-log/components/operationLogApi.ts new file mode 100644 index 0000000..ab62200 --- /dev/null +++ b/crop-x/src/app/(app)/central-config/monitor/operation-log/components/operationLogApi.ts @@ -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); +}; \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/monitor/operation-log/page.tsx b/crop-x/src/app/(app)/central-config/monitor/operation-log/page.tsx index 196752e..428d40a 100644 --- a/crop-x/src/app/(app)/central-config/monitor/operation-log/page.tsx +++ b/crop-x/src/app/(app)/central-config/monitor/operation-log/page.tsx @@ -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([]) - const [isLoading, setIsLoading] = useState(false) - const [searchKeyword, setSearchKeyword] = useState('') - const [moduleFilter, setModuleFilter] = useState('all') - const [actionFilter, setActionFilter] = useState('all') - const [statusFilter, setStatusFilter] = useState('all') - const [showDetailDialog, setShowDetailDialog] = useState(false) - const [selectedLog, setSelectedLog] = useState(null) - const [isDetailLoading, setIsDetailLoading] = useState(false) + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(false); + const [pagination, setPagination] = useState({ + page: 1, + size: 10, + total: 0, + totalPages: 0, + hasNext: false, + hasPrev: false, + }); + const [searchFilters, setSearchFilters] = useState>({ + 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) => ( +
+ {value ? new Date(value).toLocaleString('zh-CN') : '-'} +
+ ), + }, + { + key: 'username', + label: '操作人', + render: (value: string) => ( +
{value || '-'}
+ ), + }, + { + key: 'module', + label: '模块', + render: (value: string) => ( + + {value || '-'} + + ), + }, + { + key: 'action', + label: '操作', + render: (value: string) => ( +
{value || '-'}
+ ), + }, + { + key: 'processing_time', + label: '耗时', + render: (value: number) => ( +
+ + {value ? `${(value * 1000).toFixed(0)}ms` : '-'} +
+ ), + }, + { + key: 'response_status', + label: '状态', + render: (value: number) => { + if (value >= 200 && value < 300) { + return ( + + + 成功 + + ); + } else { + return ( + + + 失败 + + ); } - }) + }, + }, + { + key: 'actions', + label: '操作', + render: (_: any, row: OperationLog) => ( +
+ +
+ ), + }, + ]; - 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; + 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) => { + 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 (
+ {/* 页面标题 */}

操作日志

详细追踪用户在系统中的关键操作行为

-
- - + +
+ + {/* 统计卡片 */} +
+
+
+
总操作数
+
{stats.totalOperations}
+
+
+ 系统总操作数 +
+
+
+
+
成功操作
+
{stats.successfulOperations}
+
+
+ 操作成功次数 +
+
+
+
+
失败操作
+
{stats.failedOperations}
+
+
+ 操作失败次数 +
+
+
+
+
平均耗时
+
{Math.round(stats.averageProcessingTime * 1000)}ms
+
+
+ 操作平均耗时 +
- - - - - - - setShowDetailDialog(false)} - isLoading={isDetailLoading} - /> - -
- ) + ); } \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/tenant/user-management/page.tsx b/crop-x/src/app/(app)/central-config/tenant/user-management/page.tsx index 1c54940..9a2b968 100644 --- a/crop-x/src/app/(app)/central-config/tenant/user-management/page.tsx +++ b/crop-x/src/app/(app)/central-config/tenant/user-management/page.tsx @@ -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) => { @@ -411,7 +411,7 @@ export default function TenantUserManagementPage() { // 加载数据 useEffect(() => { loadUsers(); - }, [loadUsers]); + }, []); return (