From f4b196340092726024f363e19baf25b75f2bcb53 Mon Sep 17 00:00:00 2001 From: root <295172551@qq.com> Date: Sat, 2 Aug 2025 03:45:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E5=8D=8F=E8=AE=AE?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetProtocolLogsByDeviceQuery.cs | 5 +- .../GetProtocolLogsByDeviceQueryHandler.cs | 4 +- .../Logging/IProtocolLogRepository.cs | 4 +- .../Logging/ProtocolLogRepository.cs | 35 +- .../Controllers/ProtocolLogsController.cs | 4 +- .../protocol-logs/ProtocolLogsTable.tsx | 295 ++++++++++++++++ src/X1.WebUI/src/constants/api.ts | 2 +- src/X1.WebUI/src/constants/menuConfig.ts | 24 +- .../src/constants/protocolLayerOptions.ts | 71 ++++ src/X1.WebUI/src/contexts/AuthContext.tsx | 4 +- src/X1.WebUI/src/lib/http-client.ts | 7 +- .../OnlineProtocolLogsView.tsx | 300 ++++++++++++++++ .../protocol-logs/HistoryProtocolLogsView.tsx | 322 ++++++++++++++++++ src/X1.WebUI/src/routes/AppRouter.tsx | 23 ++ .../src/services/protocolLogsService.ts | 18 +- 15 files changed, 1082 insertions(+), 36 deletions(-) create mode 100644 src/X1.WebUI/src/components/protocol-logs/ProtocolLogsTable.tsx create mode 100644 src/X1.WebUI/src/constants/protocolLayerOptions.ts create mode 100644 src/X1.WebUI/src/pages/online-protocol-logs/OnlineProtocolLogsView.tsx create mode 100644 src/X1.WebUI/src/pages/protocol-logs/HistoryProtocolLogsView.tsx diff --git a/src/X1.Application/Features/ProtocolLogs/Queries/GetProtocolLogsByDevice/GetProtocolLogsByDeviceQuery.cs b/src/X1.Application/Features/ProtocolLogs/Queries/GetProtocolLogsByDevice/GetProtocolLogsByDeviceQuery.cs index 9f0ab89..88750cf 100644 --- a/src/X1.Application/Features/ProtocolLogs/Queries/GetProtocolLogsByDevice/GetProtocolLogsByDeviceQuery.cs +++ b/src/X1.Application/Features/ProtocolLogs/Queries/GetProtocolLogsByDevice/GetProtocolLogsByDeviceQuery.cs @@ -27,10 +27,9 @@ public class GetProtocolLogsByDeviceQuery : IRequest - /// 协议层类型 + /// 协议层类型数组 /// - [MaxLength(50, ErrorMessage = "协议层类型不能超过50个字符")] - public string? LayerType { get; set; } + public int[] LayerTypes { get; set; } = Array.Empty(); /// /// 设备运行时状态 diff --git a/src/X1.Application/Features/ProtocolLogs/Queries/GetProtocolLogsByDevice/GetProtocolLogsByDeviceQueryHandler.cs b/src/X1.Application/Features/ProtocolLogs/Queries/GetProtocolLogsByDevice/GetProtocolLogsByDeviceQueryHandler.cs index 2c9ecbc..08df9e1 100644 --- a/src/X1.Application/Features/ProtocolLogs/Queries/GetProtocolLogsByDevice/GetProtocolLogsByDeviceQueryHandler.cs +++ b/src/X1.Application/Features/ProtocolLogs/Queries/GetProtocolLogsByDevice/GetProtocolLogsByDeviceQueryHandler.cs @@ -58,8 +58,8 @@ public class GetProtocolLogsByDeviceQueryHandler : IRequestHandler /// 运行时代码集合 /// 开始时间戳 /// 结束时间戳 - /// 协议层类型 + /// 协议层类型数组 /// 运行时状态过滤(可选,支持多个状态) /// 是否按时间戳降序排序 /// 取消令牌 @@ -68,7 +68,7 @@ public interface IProtocolLogRepository : IBaseRepository IEnumerable? runtimeCodes = null, long? startTimestamp = null, long? endTimestamp = null, - string? layerType = null, + IEnumerable? layerTypes = null, IEnumerable? runtimeStatuses = null, bool orderByDescending = true, CancellationToken cancellationToken = default); diff --git a/src/X1.Infrastructure/Repositories/Logging/ProtocolLogRepository.cs b/src/X1.Infrastructure/Repositories/Logging/ProtocolLogRepository.cs index 7fd6ddd..e382be8 100644 --- a/src/X1.Infrastructure/Repositories/Logging/ProtocolLogRepository.cs +++ b/src/X1.Infrastructure/Repositories/Logging/ProtocolLogRepository.cs @@ -156,7 +156,7 @@ public class ProtocolLogRepository : BaseRepository, IProtocolLogRe /// 运行时代码集合 /// 开始时间戳 /// 结束时间戳 - /// 协议层类型 + /// 协议层类型数组 /// 运行时状态过滤(可选,支持多个状态) /// 是否按时间戳降序排序 /// 取消令牌 @@ -166,7 +166,7 @@ public class ProtocolLogRepository : BaseRepository, IProtocolLogRe IEnumerable? runtimeCodes = null, long? startTimestamp = null, long? endTimestamp = null, - string? layerType = null, + IEnumerable? layerTypes = null, IEnumerable? runtimeStatuses = null, bool orderByDescending = true, CancellationToken cancellationToken = default) @@ -178,16 +178,17 @@ public class ProtocolLogRepository : BaseRepository, IProtocolLogRe SELECT pl.* FROM ""tb_protocol_logs"" pl INNER JOIN ""tb_cellular_device_runtimes"" cdr - ON pl.""DeviceCode"" = cdr.""DeviceCode"" - AND pl.""RuntimeCode"" = cdr.""RuntimeCode"""; + ON pl.""DeviceCode"" = cdr.""DeviceCode"""; var parameters = new List(); + var paramIndex = 0; // 添加设备代码过滤(如果提供) if (!string.IsNullOrEmpty(deviceCode)) { - sql += " WHERE pl.\"DeviceCode\" = @deviceCode"; + sql += $" WHERE pl.\"DeviceCode\" = {{{paramIndex}}}"; parameters.Add(deviceCode); + paramIndex++; } else { @@ -198,37 +199,43 @@ public class ProtocolLogRepository : BaseRepository, IProtocolLogRe if (runtimeStatuses != null && runtimeStatuses.Any()) { // 支持多个运行时状态过滤 - var statusList = string.Join(",", runtimeStatuses.Select((_, i) => $"@runtimeStatus{i}")); - sql += $" AND cdr.\"RuntimeStatus\" IN ({statusList})"; - parameters.AddRange(runtimeStatuses.Cast()); + var statusList = string.Join(" OR ", runtimeStatuses.Select(status => $"cdr.\"RuntimeStatus\" = {status}")); + sql += $" AND ({statusList})"; } // 添加运行时编码过滤 if (runtimeCodes != null && runtimeCodes.Any()) { - var runtimeCodeList = string.Join(",", runtimeCodes.Select((_, i) => $"@runtimeCode{i}")); + var runtimeCodeList = string.Join(",", runtimeCodes.Select((_, i) => $"{{{paramIndex + i}}}")); sql += $" AND pl.\"RuntimeCode\" IN ({runtimeCodeList})"; parameters.AddRange(runtimeCodes); + paramIndex += runtimeCodes.Count(); } // 添加时间范围过滤 if (startTimestamp.HasValue) { - sql += " AND pl.\"Timestamp\" >= @startTimestamp"; + sql += $" AND pl.\"Timestamp\" >= {{{paramIndex}}}"; parameters.Add(startTimestamp.Value); + paramIndex++; } if (endTimestamp.HasValue) { - sql += " AND pl.\"Timestamp\" <= @endTimestamp"; + sql += $" AND pl.\"Timestamp\" <= {{{paramIndex}}}"; parameters.Add(endTimestamp.Value); + paramIndex++; } // 添加协议层类型过滤 - if (!string.IsNullOrEmpty(layerType)) + if (layerTypes != null && layerTypes.Any()) { - sql += " AND pl.\"LayerType\" = @layerType"; - parameters.Add(layerType); + var layerTypeList = string.Join(",", layerTypes.Select((_, i) => $"{{{paramIndex + i}}}")); + sql += $" AND pl.\"LayerType\" IN ({layerTypeList})"; + foreach (var layerType in layerTypes) + { + parameters.Add(layerType); + } } // 添加排序 diff --git a/src/X1.Presentation/Controllers/ProtocolLogsController.cs b/src/X1.Presentation/Controllers/ProtocolLogsController.cs index c3045cd..66b5d08 100644 --- a/src/X1.Presentation/Controllers/ProtocolLogsController.cs +++ b/src/X1.Presentation/Controllers/ProtocolLogsController.cs @@ -41,8 +41,8 @@ public class ProtocolLogsController : ApiController public async Task> GetProtocolLogs( [FromBody] GetProtocolLogsByDeviceQuery query) { - _logger.LogInformation("开始获取协议日志,设备代码: {DeviceCode}, 开始时间戳: {StartTimestamp}, 结束时间戳: {EndTimestamp}, 协议层类型: {LayerType}, 运行时状态: {DeviceRuntimeStatus}, 运行时代码数量: {RuntimeCodesCount}, 运行时状态数量: {RuntimeStatusesCount}", - query.DeviceCode, query.StartTimestamp, query.EndTimestamp, query.LayerType, query.DeviceRuntimeStatus, query.RuntimeCodes?.Length ?? 0, query.RuntimeStatuses?.Length ?? 0); + _logger.LogInformation("开始获取协议日志,设备代码: {DeviceCode}, 开始时间戳: {StartTimestamp}, 结束时间戳: {EndTimestamp}, 协议层类型数量: {LayerTypesCount}, 运行时状态: {DeviceRuntimeStatus}, 运行时代码数量: {RuntimeCodesCount}, 运行时状态数量: {RuntimeStatusesCount}", + query.DeviceCode, query.StartTimestamp, query.EndTimestamp, query.LayerTypes?.Length ?? 0, query.DeviceRuntimeStatus, query.RuntimeCodes?.Length ?? 0, query.RuntimeStatuses?.Length ?? 0); var result = await mediator.Send(query); if (!result.IsSuccess) diff --git a/src/X1.WebUI/src/components/protocol-logs/ProtocolLogsTable.tsx b/src/X1.WebUI/src/components/protocol-logs/ProtocolLogsTable.tsx new file mode 100644 index 0000000..ab19215 --- /dev/null +++ b/src/X1.WebUI/src/components/protocol-logs/ProtocolLogsTable.tsx @@ -0,0 +1,295 @@ +import React, { useState } from 'react'; +import { ProtocolLogDto } from '@/services/protocolLogsService'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Drawer, DrawerContent, DrawerHeader } from '@/components/ui/drawer'; +import { Eye, X } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { DensityType } from '@/components/ui/TableToolbar'; +import { getProtocolLayerLabel } from '@/constants/protocolLayerOptions'; +import ConfigContentViewer from '@/components/ui/ConfigContentViewer'; + +interface ProtocolLogsTableProps { + protocolLogs: ProtocolLogDto[]; + loading: boolean; + columns: { key: string; title: string; visible: boolean }[]; + density: DensityType; +} + +export default function ProtocolLogsTable({ + protocolLogs, + loading, + columns, + density, +}: ProtocolLogsTableProps) { + const [selectedMessageDetail, setSelectedMessageDetail] = useState(''); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + + // 密度样式映射 + const densityStyles = { + relaxed: 'py-3', + compact: 'py-1', + default: 'py-2', + }; + + // 格式化时间戳 + const formatTimestamp = (timestamp: number) => { + try { + return new Date(timestamp).toLocaleString('zh-CN'); + } catch { + return timestamp.toString(); + } + }; + + // 获取方向描述 + const getDirectionDescription = (direction: number) => { + switch (direction) { + case 0: + return '上行'; + case 1: + return '下行'; + default: + return '未知'; + } + }; + + // 获取方向颜色 + const getDirectionColor = (direction: number) => { + switch (direction) { + case 0: + return 'bg-blue-100 text-blue-800 hover:bg-blue-100'; + case 1: + return 'bg-green-100 text-green-800 hover:bg-green-100'; + default: + return 'bg-gray-100 text-gray-800 hover:bg-gray-100'; + } + }; + + + + // 查看消息详情 + const handleViewMessageDetail = (messageDetailJson: string) => { + // 尝试格式化 JSON 数据 + let formattedContent = messageDetailJson; + try { + // 如果是 JSON 字符串,尝试解析并格式化 + if (messageDetailJson && (messageDetailJson.startsWith('[') || messageDetailJson.startsWith('{'))) { + const parsed = JSON.parse(messageDetailJson); + formattedContent = JSON.stringify(parsed, null, 2); + } + } catch (error) { + // 如果解析失败,保持原始内容 + console.log('messageDetailJson 不是有效的 JSON 格式,使用原始内容'); + } + setSelectedMessageDetail(formattedContent); + setIsDrawerOpen(true); + }; + + // 复制消息详情 + const handleCopyMessageDetail = async () => { + try { + await navigator.clipboard.writeText(selectedMessageDetail); + // 可以添加一个 toast 提示 + } catch (error) { + console.error('复制失败:', error); + } + }; + + // 下载消息详情 + const handleDownloadMessageDetail = () => { + const blob = new Blob([selectedMessageDetail], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `message-detail-${Date.now()}.txt`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + // 渲染单元格内容 + const renderCell = (log: ProtocolLogDto, columnKey: string) => { + switch (columnKey) { + case 'layerType': + return ( +
+ {getProtocolLayerLabel(log.layerType)} +
+ ); + + case 'time': + return ( +
+ {log.time ? log.time.substring(0, 12) : '-'} +
+ ); + + case 'plmn': + return ( +
+ {(log as any).plmn || log.PLMN || '-'} +
+ ); + + case 'info': + return ( +
+ {log.info || '-'} +
+ ); + + case 'ueid': + return ( +
+ {(log as any).ueid || log.UEID || '-'} +
+ ); + + case 'imsi': + return ( +
+ {(log as any).imsi || log.IMSI || '-'} +
+ ); + + case 'cellID': + return ( +
+ {(log as any).cellID || log.CellID || '-'} +
+ ); + + case 'direction': + const directionDesc = getDirectionDescription(log.direction); + const directionColor = getDirectionColor(log.direction); + return ( + + {directionDesc} + + ); + + case 'message': + return ( +
+ {log.message || '-'} +
+ ); + + case 'MessageDetailJson': + return ( +
+ +
+ ); + + default: + return null; + } + }; + + // 过滤可见列 + const visibleColumns = columns.filter(col => col.visible); + + return ( + <> +
+
+ {/* 固定的表头 */} +
+ + + + {visibleColumns.map((column) => ( + + {column.title} + + ))} + + +
+
+ + {/* 可滚动的表体 */} +
+ + + {loading ? ( + + +
+
+ 加载中... +
+
+
+ ) : protocolLogs.length === 0 ? ( + + +
+ 暂无协议日志数据 +
+
+
+ ) : ( + protocolLogs.map((log, index) => ( + + {visibleColumns.map((column) => ( + + {renderCell(log, column.key)} + + ))} + + )) + )} +
+
+
+
+
+ + {/* 消息详情抽屉 */} + +
+ +
+

Message Detail

+ +
+
+ + +
+ +
+
+
+
+ + ); +} \ No newline at end of file diff --git a/src/X1.WebUI/src/constants/api.ts b/src/X1.WebUI/src/constants/api.ts index 8f8c965..ed1fb98 100644 --- a/src/X1.WebUI/src/constants/api.ts +++ b/src/X1.WebUI/src/constants/api.ts @@ -6,7 +6,7 @@ export const API_PATHS = { // 协议相关 PROTOCOLS: '/protocolversions', - PROTOCOL_LOGS: '/protocol-logs', + PROTOCOL_LOGS: '/protocolLogs', // RAN配置相关 RAN_CONFIGURATIONS: '/ranconfigurations', diff --git a/src/X1.WebUI/src/constants/menuConfig.ts b/src/X1.WebUI/src/constants/menuConfig.ts index f69dd6d..029d2bc 100644 --- a/src/X1.WebUI/src/constants/menuConfig.ts +++ b/src/X1.WebUI/src/constants/menuConfig.ts @@ -57,6 +57,9 @@ export type Permission = // 设备运行时管理权限 | 'deviceruntimes.view' | 'deviceruntimes.manage' + // 协议日志管理权限 + | 'protocollogs.view' + | 'protocollogs.manage' export interface MenuItem { @@ -195,7 +198,26 @@ export const menuItems: MenuItem[] = [ title: '启动设备网络', href: '/dashboard/instruments/device-runtimes/list', permission: 'deviceruntimes.view', - } + }, + + ], + }, + { + title: '信令分析', + icon: FileText, + href: '/dashboard/protocol-logs', + permission: 'protocollogs.view', + children: [ + { + title: '在线协议日志', + href: '/dashboard/protocol-logs/online-logs', + permission: 'protocollogs.view', + }, + { + title: '历史协议日志', + href: '/dashboard/protocol-logs/history-logs', + permission: 'protocollogs.view', + }, ], }, { diff --git a/src/X1.WebUI/src/constants/protocolLayerOptions.ts b/src/X1.WebUI/src/constants/protocolLayerOptions.ts new file mode 100644 index 0000000..d5ee4e6 --- /dev/null +++ b/src/X1.WebUI/src/constants/protocolLayerOptions.ts @@ -0,0 +1,71 @@ +// 完整的协议层类型枚举选项(用于标签显示) +export const allProtocolLayerOptions = [ + { value: '0', label: 'NONE' }, + { value: '1', label: 'GTPU' }, + { value: '2', label: 'LPPa' }, + { value: '3', label: 'M2AP' }, + { value: '4', label: 'MAC' }, + { value: '5', label: 'NAS' }, + { value: '6', label: 'NGAP' }, + { value: '7', label: 'NRPPa' }, + { value: '8', label: 'PDCP' }, + { value: '9', label: 'PROD' }, + { value: '10', label: 'PHY' }, + { value: '11', label: 'RLC' }, + { value: '12', label: 'RRC' }, + { value: '13', label: 'S1AP' }, + { value: '14', label: 'TRX' }, + { value: '15', label: 'X2AP' }, + { value: '16', label: 'XnAP' }, + { value: '17', label: 'IP' }, + { value: '18', label: 'IMS' }, + { value: '19', label: 'CX' }, + { value: '20', label: 'RX' }, + { value: '21', label: 'S6' }, + { value: '22', label: 'S13' }, + { value: '23', label: 'SGsAP' }, + { value: '24', label: 'SBcAP' }, + { value: '25', label: 'LCSAP' }, + { value: '26', label: 'N12' }, + { value: '27', label: 'N8' }, + { value: '28', label: 'N17' }, + { value: '29', label: 'N50' }, + { value: '30', label: 'N13' }, + { value: '31', label: 'NL1' }, + { value: '32', label: 'HTTP2' }, + { value: '33', label: 'EPDG' }, + { value: '34', label: 'IKEV2' }, + { value: '35', label: 'IPSEC' }, + { value: '36', label: 'MEDIA' }, + { value: '37', label: 'MMS' }, + { value: '38', label: 'SIP' }, +]; + +// 根据实际数据动态生成协议层类型选项 +export const generateProtocolLayerOptions = (protocolLogs: any[]): { value: string; label: string }[] => { + // 从数据中提取所有唯一的 layerType 值 + const uniqueLayerTypes = [...new Set(protocolLogs.map(log => log.layerType))].sort((a, b) => a - b); + + // 构建选项数组,始终包含"全部"选项 + const options = [{ value: 'ALL', label: '全部' }]; + + // 为每个唯一的 layerType 添加对应的选项 + uniqueLayerTypes.forEach(layerType => { + const option = allProtocolLayerOptions.find(opt => opt.value === String(layerType)); + if (option) { + options.push(option); + } + }); + + return options; +}; + +// 保持向后兼容的 protocolLayerOptions(现在使用动态生成) +export const protocolLayerOptions = allProtocolLayerOptions; + +// 获取协议层类型标签的辅助函数 +export const getProtocolLayerLabel = (layerType: string | number): string => { + const layerTypeStr = String(layerType); + const option = allProtocolLayerOptions.find(opt => opt.value === layerTypeStr); + return option ? option.label : layerTypeStr; +}; \ No newline at end of file diff --git a/src/X1.WebUI/src/contexts/AuthContext.tsx b/src/X1.WebUI/src/contexts/AuthContext.tsx index 1033fa5..54cabc3 100644 --- a/src/X1.WebUI/src/contexts/AuthContext.tsx +++ b/src/X1.WebUI/src/contexts/AuthContext.tsx @@ -84,7 +84,9 @@ const getDefaultPermissions = (userPermissions: Record = {}) => // 设备运行时管理权限 'deviceruntimes.view', 'deviceruntimes.manage', - + // 协议日志管理权限 + 'protocollogs.view', + 'protocollogs.manage', ]) ]; diff --git a/src/X1.WebUI/src/lib/http-client.ts b/src/X1.WebUI/src/lib/http-client.ts index 1c10be3..91ae2cc 100644 --- a/src/X1.WebUI/src/lib/http-client.ts +++ b/src/X1.WebUI/src/lib/http-client.ts @@ -129,7 +129,12 @@ export class HttpClient { // 封装POST请求 public async post(url: string, data?: any, config?: AxiosRequestConfig): Promise> { - const response = await this.instance.post>(url, data, config); + // 将 undefined 值转换为 null,确保字段在JSON中保留 + const processedData = data ? JSON.parse(JSON.stringify(data, (key, value) => + value === undefined ? null : value + )) : data; + + const response = await this.instance.post>(url, processedData, config); return response.data; } diff --git a/src/X1.WebUI/src/pages/online-protocol-logs/OnlineProtocolLogsView.tsx b/src/X1.WebUI/src/pages/online-protocol-logs/OnlineProtocolLogsView.tsx new file mode 100644 index 0000000..a06a788 --- /dev/null +++ b/src/X1.WebUI/src/pages/online-protocol-logs/OnlineProtocolLogsView.tsx @@ -0,0 +1,300 @@ +import React, { useState, useEffect } from 'react'; +import { + ProtocolLogDto, + GetProtocolLogsRequest, + protocolLogsService +} from '@/services/protocolLogsService'; +import ProtocolLogsTable from '@/components/protocol-logs/ProtocolLogsTable'; +import { Input } from '@/components/ui/input'; +import TableToolbar, { DensityType } from '@/components/ui/TableToolbar'; +import { Button } from '@/components/ui/button'; +import { useToast } from '@/components/ui/use-toast'; +import { generateProtocolLayerOptions } from '@/constants/protocolLayerOptions'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Badge } from '@/components/ui/badge'; +import { ChevronDown, X } from 'lucide-react'; + +const defaultColumns = [ + { key: 'layerType', title: 'Layer Type', visible: true }, + { key: 'time', title: 'Time', visible: true }, + { key: 'plmn', title: 'PLMN', visible: true }, + { key: 'info', title: 'Info', visible: true }, + { key: 'ueid', title: 'UEID', visible: true }, + { key: 'imsi', title: 'IMSI', visible: true }, + { key: 'cellID', title: 'CellID', visible: true }, + { key: 'direction', title: 'Direction', visible: true }, + { key: 'message', title: 'Message', visible: true }, + { key: 'MessageDetailJson', title: 'Detail', visible: true }, +]; + +export default function OnlineProtocolLogsView() { + const [protocolLogs, setProtocolLogs] = useState([]); + const [loading, setLoading] = useState(false); + const [density, setDensity] = useState('default'); + const [columns, setColumns] = useState(defaultColumns); + const [layerTypeOpen, setLayerTypeOpen] = useState(false); + + // 动态生成协议层类型选项 + const protocolLayerOptions = generateProtocolLayerOptions(protocolLogs); + + const [searchParams, setSearchParams] = useState({ + deviceCode: '', + startTimestamp: undefined, + endTimestamp: undefined, + layerTypes: [], + deviceRuntimeStatus: undefined, + runtimeCodes: [], + runtimeStatuses: [1], // 默认只显示运行中的设备日志 + orderByDescending: true, + }); + + const { toast } = useToast(); + + const fetchProtocolLogs = async (params: GetProtocolLogsRequest = searchParams) => { + setLoading(true); + try { + // 确保所有字段都被包含在请求中,即使为空 + const requestParams = { + deviceCode: params.deviceCode || '', + startTimestamp: params.startTimestamp, + endTimestamp: params.endTimestamp, + layerTypes: params.layerTypes || [], + deviceRuntimeStatus: params.deviceRuntimeStatus, + runtimeCodes: params.runtimeCodes || [], + runtimeStatuses: params.runtimeStatuses || [], + orderByDescending: params.orderByDescending ?? true, + }; + + // 调试:打印请求参数 + console.log('发送在线协议日志请求参数:', JSON.stringify(requestParams, null, 2)); + + const result = await protocolLogsService.getProtocolLogs(requestParams); + + if (result.isSuccess && result.data) { + setProtocolLogs(result.data.items || []); + } else { + toast({ + title: '获取在线协议日志失败', + description: result.errorMessages?.join(', ') || '未知错误', + variant: 'destructive', + }); + } + } catch (error) { + console.error('获取在线协议日志失败:', error); + toast({ + title: '获取在线协议日志失败', + description: '网络错误或服务器异常', + variant: 'destructive', + }); + } finally { + setLoading(false); + } + }; + + const handleQuery = () => { + fetchProtocolLogs(); + }; + + const handleReset = () => { + const resetParams: GetProtocolLogsRequest = { + deviceCode: '', + startTimestamp: undefined, + endTimestamp: undefined, + layerTypes: [], + deviceRuntimeStatus: undefined, + runtimeCodes: [], + runtimeStatuses: [1], // 重置时也保持只显示运行中的设备日志 + orderByDescending: true, + }; + setSearchParams(resetParams); + fetchProtocolLogs(resetParams); + }; + + useEffect(() => { + fetchProtocolLogs(); + }, []); + + // 点击外部关闭下拉框 + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Element; + if (!target.closest('.layer-type-dropdown')) { + setLayerTypeOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + return ( +
+
+
+

在线协议日志

+

+ 实时监控协议日志记录 +

+
+ +
+
{ + e.preventDefault(); + handleQuery(); + }} + > +
+ + ) => + setSearchParams(prev => ({ ...prev, deviceCode: e.target.value })) + } + /> +
+ +
+ +
+ + + {layerTypeOpen && ( +
+
+
+ 选择协议层类型 + +
+ {protocolLayerOptions.map((option) => ( +
+ { + const currentTypes = searchParams.layerTypes || []; + if (checked) { + setSearchParams(prev => ({ + ...prev, + layerTypes: [...currentTypes, parseInt(option.value)] + })); + } else { + setSearchParams(prev => ({ + ...prev, + layerTypes: currentTypes.filter(type => type !== parseInt(option.value)) + })); + } + }} + /> + +
+ ))} +
+
+ )} +
+
+ + + +
+ + ) => + setSearchParams(prev => ({ + ...prev, + startTimestamp: e.target.value ? parseInt(e.target.value) : undefined + })) + } + /> +
+ +
+ + ) => + setSearchParams(prev => ({ + ...prev, + endTimestamp: e.target.value ? parseInt(e.target.value) : undefined + })) + } + /> +
+ +
+ + +
+
+
+ +
+
+
+ + 共 {protocolLogs.length} 条记录 + +
+ fetchProtocolLogs()} + onDensityChange={setDensity} + onColumnsChange={setColumns} + onColumnsReset={() => setColumns(defaultColumns)} + columns={columns} + density={density} + /> +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/X1.WebUI/src/pages/protocol-logs/HistoryProtocolLogsView.tsx b/src/X1.WebUI/src/pages/protocol-logs/HistoryProtocolLogsView.tsx new file mode 100644 index 0000000..8beb85a --- /dev/null +++ b/src/X1.WebUI/src/pages/protocol-logs/HistoryProtocolLogsView.tsx @@ -0,0 +1,322 @@ +import React, { useState, useEffect } from 'react'; +import { + ProtocolLogDto, + GetProtocolLogsRequest, + protocolLogsService +} from '@/services/protocolLogsService'; +import ProtocolLogsTable from '@/components/protocol-logs/ProtocolLogsTable'; +import { Input } from '@/components/ui/input'; +import TableToolbar, { DensityType } from '@/components/ui/TableToolbar'; +import { Button } from '@/components/ui/button'; +import { useToast } from '@/components/ui/use-toast'; +import { generateProtocolLayerOptions } from '@/constants/protocolLayerOptions'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Badge } from '@/components/ui/badge'; +import { ChevronDown, X } from 'lucide-react'; + +const defaultColumns = [ + { key: 'layerType', title: 'Layer Type', visible: true }, + { key: 'time', title: 'Time', visible: true }, + { key: 'plmn', title: 'PLMN', visible: true }, + { key: 'info', title: 'Info', visible: true }, + { key: 'ueid', title: 'UEID', visible: true }, + { key: 'imsi', title: 'IMSI', visible: true }, + { key: 'cellID', title: 'CellID', visible: true }, + { key: 'direction', title: 'Direction', visible: true }, + { key: 'message', title: 'Message', visible: true }, + { key: 'MessageDetailJson', title: 'Detail', visible: true }, +]; + +export default function HistoryProtocolLogsView() { + const [protocolLogs, setProtocolLogs] = useState([]); + const [loading, setLoading] = useState(false); + const [density, setDensity] = useState('default'); + const [columns, setColumns] = useState(defaultColumns); + + // 动态生成协议层类型选项 + const protocolLayerOptions = generateProtocolLayerOptions(protocolLogs); + + const [searchParams, setSearchParams] = useState({ + deviceCode: '', + startTimestamp: undefined, + endTimestamp: undefined, + layerTypes: [], + deviceRuntimeStatus: undefined, + runtimeCodes: [], + runtimeStatuses: [0,2,3], + orderByDescending: true, + }); + + const { toast } = useToast(); + + const fetchProtocolLogs = async (params: GetProtocolLogsRequest = searchParams) => { + setLoading(true); + try { + // 确保所有字段都被包含在请求中,即使为空 + const requestParams = { + deviceCode: params.deviceCode || '', + startTimestamp: params.startTimestamp, + endTimestamp: params.endTimestamp, + layerTypes: params.layerTypes || [], + deviceRuntimeStatus: params.deviceRuntimeStatus, + runtimeCodes: params.runtimeCodes || [], + runtimeStatuses: params.runtimeStatuses || [], + orderByDescending: params.orderByDescending ?? true, + }; + + // 调试:打印请求参数 + console.log('发送协议日志请求参数:', JSON.stringify(requestParams, null, 2)); + + const result = await protocolLogsService.getProtocolLogs(requestParams); + + if (result.isSuccess && result.data) { + setProtocolLogs(result.data.items || []); + } else { + toast({ + title: '获取历史协议日志失败', + description: result.errorMessages?.join(', ') || '未知错误', + variant: 'destructive', + }); + } + } catch (error) { + console.error('获取历史协议日志失败:', error); + toast({ + title: '获取历史协议日志失败', + description: '网络错误或服务器异常', + variant: 'destructive', + }); + } finally { + setLoading(false); + } + }; + + const handleQuery = () => { + fetchProtocolLogs(); + }; + + const handleReset = () => { + const resetParams: GetProtocolLogsRequest = { + deviceCode: '', + startTimestamp: undefined, + endTimestamp: undefined, + layerTypes: [], + deviceRuntimeStatus: undefined, + runtimeCodes: [], + runtimeStatuses: [0,2,3], // 重置时显示非运行状态的设备日志 + orderByDescending: true, + }; + setSearchParams(resetParams); + fetchProtocolLogs(resetParams); + }; + + useEffect(() => { + fetchProtocolLogs(); + }, []); + + // 点击外部关闭下拉框 + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const dropdown = document.getElementById('layerTypeDropdown'); + const button = document.querySelector('[data-layer-type-button]'); + if (dropdown && !dropdown.contains(event.target as Node) && !button?.contains(event.target as Node)) { + dropdown.classList.add('hidden'); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + return ( +
+
+ + +
+
{ + e.preventDefault(); + handleQuery(); + }} + > +
+ + ) => + setSearchParams(prev => ({ ...prev, deviceCode: e.target.value })) + } + /> +
+ +
+ +
+ + +
+
+
+ { + setSearchParams(prev => ({ ...prev, layerTypes: [] })); + }} + /> + +
+ + {protocolLayerOptions.filter(option => option.value !== 'ALL').map((option) => ( +
+ { + const currentTypes = searchParams.layerTypes || []; + if (checked) { + setSearchParams(prev => ({ + ...prev, + layerTypes: [...currentTypes, parseInt(option.value)] + })); + } else { + setSearchParams(prev => ({ + ...prev, + layerTypes: currentTypes.filter(type => type !== parseInt(option.value)) + })); + } + }} + /> + +
+ ))} +
+
+ + {(searchParams.layerTypes?.length || 0) > 0 && ( +
+ {(searchParams.layerTypes || []).map((type) => { + const option = protocolLayerOptions.find(opt => opt.value === String(type)); + return ( + + {option?.label || type} + { + setSearchParams(prev => ({ + ...prev, + layerTypes: prev.layerTypes?.filter(t => t !== type) || [] + })); + }} + /> + + ); + })} +
+ )} +
+
+ + + +
+ + ) => + setSearchParams(prev => ({ + ...prev, + startTimestamp: e.target.value ? parseInt(e.target.value) : undefined + })) + } + /> +
+ +
+ + ) => + setSearchParams(prev => ({ + ...prev, + endTimestamp: e.target.value ? parseInt(e.target.value) : undefined + })) + } + /> +
+ +
+ + +
+
+
+ +
+
+
+ + 共 {protocolLogs.length} 条记录 + +
+ fetchProtocolLogs()} + onDensityChange={setDensity} + onColumnsChange={setColumns} + onColumnsReset={() => setColumns(defaultColumns)} + columns={columns} + density={density} + /> +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/X1.WebUI/src/routes/AppRouter.tsx b/src/X1.WebUI/src/routes/AppRouter.tsx index 4121f63..565750e 100644 --- a/src/X1.WebUI/src/routes/AppRouter.tsx +++ b/src/X1.WebUI/src/routes/AppRouter.tsx @@ -35,6 +35,10 @@ const DevicesView = lazy(() => import('@/pages/instruments/DevicesView')); const DeviceRuntimesView = lazy(() => import('@/pages/device-runtimes/DeviceRuntimesView')); // 协议管理页面 const ProtocolsView = lazy(() => import('@/pages/protocols/ProtocolsView')); +// 在线协议日志页面 +const OnlineProtocolLogsView = lazy(() => import('@/pages/online-protocol-logs/OnlineProtocolLogsView')); +// 历史协议日志页面 +const HistoryProtocolLogsView = lazy(() => import('@/pages/protocol-logs/HistoryProtocolLogsView')); // RAN配置管理页面 const RANConfigurationsView = lazy(() => import('@/pages/ran-configurations/RANConfigurationsView')); // IMS配置管理页面 @@ -222,6 +226,25 @@ export function AppRouter() { + {/* 信令分析路由 */} + + } /> + + + + + + } /> + + + + + + } /> + + {/* 网络栈配置管理路由 */} } /> diff --git a/src/X1.WebUI/src/services/protocolLogsService.ts b/src/X1.WebUI/src/services/protocolLogsService.ts index 42ab801..df8cf33 100644 --- a/src/X1.WebUI/src/services/protocolLogsService.ts +++ b/src/X1.WebUI/src/services/protocolLogsService.ts @@ -15,7 +15,7 @@ export interface GetProtocolLogsRequest { deviceCode?: string; startTimestamp?: number; endTimestamp?: number; - layerType?: string; + layerTypes?: number[]; deviceRuntimeStatus?: DeviceRuntimeStatus; runtimeCodes?: string[]; runtimeStatuses?: number[]; @@ -28,11 +28,11 @@ export interface ProtocolLogDto { messageId: number; layerType: number; messageDetailJson?: string; - cellID?: number; - imsi?: string; + CellID?: number; + IMSI?: string; direction: number; - ueid?: number; - plmn?: string; + UEID?: number; + PLMN?: string; timeMs: number; timestamp: number; info?: string; @@ -110,16 +110,16 @@ class ProtocolLogsService { /** * 按协议层类型获取协议日志(便捷方法) - * @param layerType 协议层类型 + * @param layerTypes 协议层类型数组 * @param options 其他查询选项 * @returns 协议日志列表 */ async getProtocolLogsByLayerType( - layerType: string, - options: Omit = {} + layerTypes: number[], + options: Omit = {} ): Promise> { return this.getProtocolLogs({ - layerType, + layerTypes, ...options }); }