Browse Source

feat: 完善协议日志功能

feature/x1-web-request
root 2 days ago
parent
commit
f4b1963400
  1. 5
      src/X1.Application/Features/ProtocolLogs/Queries/GetProtocolLogsByDevice/GetProtocolLogsByDeviceQuery.cs
  2. 4
      src/X1.Application/Features/ProtocolLogs/Queries/GetProtocolLogsByDevice/GetProtocolLogsByDeviceQueryHandler.cs
  3. 4
      src/X1.Domain/Repositories/Logging/IProtocolLogRepository.cs
  4. 35
      src/X1.Infrastructure/Repositories/Logging/ProtocolLogRepository.cs
  5. 4
      src/X1.Presentation/Controllers/ProtocolLogsController.cs
  6. 295
      src/X1.WebUI/src/components/protocol-logs/ProtocolLogsTable.tsx
  7. 2
      src/X1.WebUI/src/constants/api.ts
  8. 24
      src/X1.WebUI/src/constants/menuConfig.ts
  9. 71
      src/X1.WebUI/src/constants/protocolLayerOptions.ts
  10. 4
      src/X1.WebUI/src/contexts/AuthContext.tsx
  11. 7
      src/X1.WebUI/src/lib/http-client.ts
  12. 300
      src/X1.WebUI/src/pages/online-protocol-logs/OnlineProtocolLogsView.tsx
  13. 322
      src/X1.WebUI/src/pages/protocol-logs/HistoryProtocolLogsView.tsx
  14. 23
      src/X1.WebUI/src/routes/AppRouter.tsx
  15. 18
      src/X1.WebUI/src/services/protocolLogsService.ts

5
src/X1.Application/Features/ProtocolLogs/Queries/GetProtocolLogsByDevice/GetProtocolLogsByDeviceQuery.cs

@ -27,10 +27,9 @@ public class GetProtocolLogsByDeviceQuery : IRequest<OperationResult<GetProtocol
public long? EndTimestamp { get; set; }
/// <summary>
/// 协议层类型
/// 协议层类型数组
/// </summary>
[MaxLength(50, ErrorMessage = "协议层类型不能超过50个字符")]
public string? LayerType { get; set; }
public int[] LayerTypes { get; set; } = Array.Empty<int>();
/// <summary>
/// 设备运行时状态

4
src/X1.Application/Features/ProtocolLogs/Queries/GetProtocolLogsByDevice/GetProtocolLogsByDeviceQueryHandler.cs

@ -58,8 +58,8 @@ public class GetProtocolLogsByDeviceQueryHandler : IRequestHandler<GetProtocolLo
request.RuntimeCodes, // 直接使用请求中的运行时代码数组
request.StartTimestamp,
request.EndTimestamp,
request.LayerType,
request.RuntimeStatuses.Any() ? request.RuntimeStatuses : null, // 使用前端传入的运行时状态数组
request.LayerTypes.Any() ? request.LayerTypes : null, // 使用协议层类型数组
request.RuntimeStatuses.Any() ? request.RuntimeStatuses : null, // 直接使用前端传入的运行时状态数组
request.OrderByDescending,
cancellationToken);

4
src/X1.Domain/Repositories/Logging/IProtocolLogRepository.cs

@ -58,7 +58,7 @@ public interface IProtocolLogRepository : IBaseRepository<ProtocolLog>
/// <param name="runtimeCodes">运行时代码集合</param>
/// <param name="startTimestamp">开始时间戳</param>
/// <param name="endTimestamp">结束时间戳</param>
/// <param name="layerType">协议层类型</param>
/// <param name="layerTypes">协议层类型数组</param>
/// <param name="runtimeStatuses">运行时状态过滤(可选,支持多个状态)</param>
/// <param name="orderByDescending">是否按时间戳降序排序</param>
/// <param name="cancellationToken">取消令牌</param>
@ -68,7 +68,7 @@ public interface IProtocolLogRepository : IBaseRepository<ProtocolLog>
IEnumerable<string>? runtimeCodes = null,
long? startTimestamp = null,
long? endTimestamp = null,
string? layerType = null,
IEnumerable<int>? layerTypes = null,
IEnumerable<int>? runtimeStatuses = null,
bool orderByDescending = true,
CancellationToken cancellationToken = default);

35
src/X1.Infrastructure/Repositories/Logging/ProtocolLogRepository.cs

@ -156,7 +156,7 @@ public class ProtocolLogRepository : BaseRepository<ProtocolLog>, IProtocolLogRe
/// <param name="runtimeCodes">运行时代码集合</param>
/// <param name="startTimestamp">开始时间戳</param>
/// <param name="endTimestamp">结束时间戳</param>
/// <param name="layerType">协议层类型</param>
/// <param name="layerTypes">协议层类型数组</param>
/// <param name="runtimeStatuses">运行时状态过滤(可选,支持多个状态)</param>
/// <param name="orderByDescending">是否按时间戳降序排序</param>
/// <param name="cancellationToken">取消令牌</param>
@ -166,7 +166,7 @@ public class ProtocolLogRepository : BaseRepository<ProtocolLog>, IProtocolLogRe
IEnumerable<string>? runtimeCodes = null,
long? startTimestamp = null,
long? endTimestamp = null,
string? layerType = null,
IEnumerable<int>? layerTypes = null,
IEnumerable<int>? runtimeStatuses = null,
bool orderByDescending = true,
CancellationToken cancellationToken = default)
@ -178,16 +178,17 @@ public class ProtocolLogRepository : BaseRepository<ProtocolLog>, 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<object>();
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<ProtocolLog>, 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<object>());
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);
}
}
// 添加排序

4
src/X1.Presentation/Controllers/ProtocolLogsController.cs

@ -41,8 +41,8 @@ public class ProtocolLogsController : ApiController
public async Task<OperationResult<GetProtocolLogsByDeviceResponse>> 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)

295
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<string>('');
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 (
<div className="text-sm">
{getProtocolLayerLabel(log.layerType)}
</div>
);
case 'time':
return (
<div className="text-sm text-muted-foreground">
{log.time ? log.time.substring(0, 12) : '-'}
</div>
);
case 'plmn':
return (
<div className="text-sm font-mono">
{(log as any).plmn || log.PLMN || '-'}
</div>
);
case 'info':
return (
<div className="text-sm max-w-xs truncate" title={log.info}>
{log.info || '-'}
</div>
);
case 'ueid':
return (
<div className="text-sm text-muted-foreground">
{(log as any).ueid || log.UEID || '-'}
</div>
);
case 'imsi':
return (
<div className="text-sm font-mono">
{(log as any).imsi || log.IMSI || '-'}
</div>
);
case 'cellID':
return (
<div className="text-sm text-muted-foreground">
{(log as any).cellID || log.CellID || '-'}
</div>
);
case 'direction':
const directionDesc = getDirectionDescription(log.direction);
const directionColor = getDirectionColor(log.direction);
return (
<Badge
variant="outline"
className={cn(directionColor)}
>
{directionDesc}
</Badge>
);
case 'message':
return (
<div className="text-sm max-w-xs truncate" title={log.message}>
{log.message || '-'}
</div>
);
case 'MessageDetailJson':
return (
<div className="text-center">
<Button
variant="ghost"
size="sm"
onClick={() => handleViewMessageDetail(log.messageDetailJson || '')}
disabled={!log.messageDetailJson}
className="h-8 w-8 p-0 hover:bg-accent"
title="查看详情"
>
<Eye className="h-4 w-4 text-muted-foreground hover:text-foreground" />
</Button>
</div>
);
default:
return null;
}
};
// 过滤可见列
const visibleColumns = columns.filter(col => col.visible);
return (
<>
<div className="rounded-md border overflow-hidden">
<div className="overflow-x-auto">
{/* 固定的表头 */}
<div className="bg-background border-b shadow-sm">
<Table className="w-full table-fixed">
<TableHeader>
<TableRow>
{visibleColumns.map((column) => (
<TableHead key={column.key} className={cn("text-center whitespace-nowrap bg-background", densityStyles[density])} style={{ width: `${100 / visibleColumns.length}%` }}>
{column.title}
</TableHead>
))}
</TableRow>
</TableHeader>
</Table>
</div>
{/* 可滚动的表体 */}
<div className="max-h-[600px] overflow-auto">
<Table className="w-full table-fixed">
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={visibleColumns.length} className="text-center py-8">
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
<span className="ml-2">...</span>
</div>
</TableCell>
</TableRow>
) : protocolLogs.length === 0 ? (
<TableRow>
<TableCell colSpan={visibleColumns.length} className="text-center py-8">
<div className="text-muted-foreground">
</div>
</TableCell>
</TableRow>
) : (
protocolLogs.map((log, index) => (
<TableRow key={`${log.id}-${index}`}>
{visibleColumns.map((column) => (
<TableCell key={column.key} className={cn("text-center whitespace-nowrap", densityStyles[density])} style={{ width: `${100 / visibleColumns.length}%` }}>
{renderCell(log, column.key)}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
</div>
{/* 消息详情抽屉 */}
<Drawer open={isDrawerOpen} onOpenChange={setIsDrawerOpen}>
<div className="flex flex-col h-full">
<DrawerHeader className="flex-shrink-0">
<div className="flex items-center justify-between w-full">
<h2 className="text-lg font-semibold">Message Detail</h2>
<Button
variant="ghost"
size="sm"
onClick={() => setIsDrawerOpen(false)}
className="h-8 w-8 p-0"
>
<X className="h-4 w-4" />
</Button>
</div>
</DrawerHeader>
<DrawerContent className="flex flex-col flex-1 min-h-0">
<div className="flex-1 p-4">
<ConfigContentViewer
content={selectedMessageDetail || 'No message detail available'}
onCopy={handleCopyMessageDetail}
onDownload={handleDownloadMessageDetail}
className="h-full"
/>
</div>
</DrawerContent>
</div>
</Drawer>
</>
);
}

2
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',

24
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',
},
],
},
{

71
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;
};

4
src/X1.WebUI/src/contexts/AuthContext.tsx

@ -84,7 +84,9 @@ const getDefaultPermissions = (userPermissions: Record<string, boolean> = {}) =>
// 设备运行时管理权限
'deviceruntimes.view',
'deviceruntimes.manage',
// 协议日志管理权限
'protocollogs.view',
'protocollogs.manage',
])
];

7
src/X1.WebUI/src/lib/http-client.ts

@ -129,7 +129,12 @@ export class HttpClient {
// 封装POST请求
public async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<OperationResult<T>> {
const response = await this.instance.post<OperationResult<T>>(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<OperationResult<T>>(url, processedData, config);
return response.data;
}

300
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<ProtocolLogDto[]>([]);
const [loading, setLoading] = useState(false);
const [density, setDensity] = useState<DensityType>('default');
const [columns, setColumns] = useState(defaultColumns);
const [layerTypeOpen, setLayerTypeOpen] = useState(false);
// 动态生成协议层类型选项
const protocolLayerOptions = generateProtocolLayerOptions(protocolLogs);
const [searchParams, setSearchParams] = useState<GetProtocolLogsRequest>({
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 (
<main className="flex-1 p-4 transition-all duration-300 ease-in-out sm:p-6">
<div className="w-full space-y-4">
<div>
<h1 className="text-2xl font-bold text-foreground">线</h1>
<p className="text-sm text-muted-foreground mt-1">
</p>
</div>
<div className="flex flex-col bg-background p-4 rounded-md border mb-2">
<form
className="flex gap-x-8 gap-y-4 items-center flex-wrap"
onSubmit={e => {
e.preventDefault();
handleQuery();
}}
>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right" style={{ width: 80, minWidth: 80 }}>
</label>
<Input
className="flex-1 bg-background text-foreground placeholder:text-muted-foreground border border-border focus:outline-none focus:ring-0 focus:border-border transition-all"
placeholder="请输入设备编号"
value={searchParams.deviceCode || ''}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setSearchParams(prev => ({ ...prev, deviceCode: e.target.value }))
}
/>
</div>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right" style={{ width: 80, minWidth: 80 }}>
</label>
<div className="flex-1 relative layer-type-dropdown">
<Button
variant="outline"
className="w-full justify-between bg-background text-foreground border border-border focus:outline-none focus:ring-0 focus:border-border transition-all"
onClick={() => setLayerTypeOpen(!layerTypeOpen)}
>
<span className="truncate">
{searchParams.layerTypes?.length ? `${searchParams.layerTypes.length} 个类型` : '请选择协议层类型'}
</span>
<ChevronDown className="h-4 w-4 opacity-50" />
</Button>
{layerTypeOpen && (
<div className="absolute top-full left-0 right-0 z-50 mt-1 bg-background border border-border rounded-md shadow-lg max-h-60 overflow-y-auto">
<div className="p-2">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium"></span>
<Button
variant="ghost"
size="sm"
onClick={() => setSearchParams(prev => ({ ...prev, layerTypes: [] }))}
className="h-6 px-2 text-xs"
>
</Button>
</div>
{protocolLayerOptions.map((option) => (
<div key={option.value} className="flex items-center space-x-2 py-1">
<Checkbox
id={`layer-${option.value}`}
checked={searchParams.layerTypes?.includes(parseInt(option.value)) || false}
onCheckedChange={(checked) => {
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))
}));
}
}}
/>
<label
htmlFor={`layer-${option.value}`}
className="text-sm cursor-pointer flex-1"
>
{option.label}
</label>
</div>
))}
</div>
</div>
)}
</div>
</div>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right" style={{ width: 80, minWidth: 80 }}>
</label>
<Input
type="number"
className="flex-1 bg-background text-foreground placeholder:text-muted-foreground border border-border focus:outline-none focus:ring-0 focus:border-border transition-all"
placeholder="请输入开始时间戳"
value={searchParams.startTimestamp || ''}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setSearchParams(prev => ({
...prev,
startTimestamp: e.target.value ? parseInt(e.target.value) : undefined
}))
}
/>
</div>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right" style={{ width: 80, minWidth: 80 }}>
</label>
<Input
type="number"
className="flex-1 bg-background text-foreground placeholder:text-muted-foreground border border-border focus:outline-none focus:ring-0 focus:border-border transition-all"
placeholder="请输入结束时间戳"
value={searchParams.endTimestamp || ''}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setSearchParams(prev => ({
...prev,
endTimestamp: e.target.value ? parseInt(e.target.value) : undefined
}))
}
/>
</div>
<div className="flex flex-row items-center gap-2">
<Button variant="outline" onClick={handleReset}></Button>
<Button onClick={handleQuery}></Button>
</div>
</form>
</div>
<div className="rounded-md border bg-background p-4">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">
{protocolLogs.length}
</span>
</div>
<TableToolbar
onRefresh={() => fetchProtocolLogs()}
onDensityChange={setDensity}
onColumnsChange={setColumns}
onColumnsReset={() => setColumns(defaultColumns)}
columns={columns}
density={density}
/>
</div>
<ProtocolLogsTable
protocolLogs={protocolLogs}
loading={loading}
columns={columns}
density={density}
/>
</div>
</div>
</main>
);
}

322
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<ProtocolLogDto[]>([]);
const [loading, setLoading] = useState(false);
const [density, setDensity] = useState<DensityType>('default');
const [columns, setColumns] = useState(defaultColumns);
// 动态生成协议层类型选项
const protocolLayerOptions = generateProtocolLayerOptions(protocolLogs);
const [searchParams, setSearchParams] = useState<GetProtocolLogsRequest>({
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 (
<main className="flex-1 p-4 transition-all duration-300 ease-in-out sm:p-6">
<div className="w-full space-y-4">
<div className="flex flex-col bg-background p-4 rounded-md border mb-2">
<form
className="flex gap-x-8 gap-y-4 items-center flex-wrap"
onSubmit={e => {
e.preventDefault();
handleQuery();
}}
>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right" style={{ width: 80, minWidth: 80 }}>
</label>
<Input
className="flex-1 bg-background text-foreground placeholder:text-muted-foreground border border-border focus:outline-none focus:ring-0 focus:border-border transition-all"
placeholder="请输入设备编号"
value={searchParams.deviceCode || ''}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setSearchParams(prev => ({ ...prev, deviceCode: e.target.value }))
}
/>
</div>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right" style={{ width: 80, minWidth: 80 }}>
</label>
<div className="flex-1 relative">
<Button
variant="outline"
data-layer-type-button
className="w-full justify-between bg-background text-foreground border border-border focus:outline-none focus:ring-0 focus:border-border transition-all"
onClick={() => {
const dropdown = document.getElementById('layerTypeDropdown');
if (dropdown) {
dropdown.classList.toggle('hidden');
}
}}
>
<span className="truncate">
{searchParams.layerTypes?.length
? `${searchParams.layerTypes.length} 个已选择`
: '请选择协议层类型'
}
</span>
<ChevronDown className="h-4 w-4" />
</Button>
<div
id="layerTypeDropdown"
className="hidden absolute z-50 w-full mt-1 bg-background border border-border rounded-md shadow-lg max-h-60 overflow-auto"
>
<div className="p-2">
<div className="flex items-center space-x-2 p-2 hover:bg-accent rounded">
<Checkbox
id="select-all"
checked={(searchParams.layerTypes?.length || 0) === 0}
onCheckedChange={(checked) => {
setSearchParams(prev => ({ ...prev, layerTypes: [] }));
}}
/>
<label htmlFor="select-all" className="text-sm"></label>
</div>
{protocolLayerOptions.filter(option => option.value !== 'ALL').map((option) => (
<div key={option.value} className="flex items-center space-x-2 p-2 hover:bg-accent rounded">
<Checkbox
id={`layer-${option.value}`}
checked={searchParams.layerTypes?.includes(parseInt(option.value)) || false}
onCheckedChange={(checked) => {
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))
}));
}
}}
/>
<label htmlFor={`layer-${option.value}`} className="text-sm">{option.label}</label>
</div>
))}
</div>
</div>
{(searchParams.layerTypes?.length || 0) > 0 && (
<div className="flex flex-wrap gap-1 mt-1">
{(searchParams.layerTypes || []).map((type) => {
const option = protocolLayerOptions.find(opt => opt.value === String(type));
return (
<Badge key={type} variant="secondary" className="text-xs">
{option?.label || type}
<X
className="h-3 w-3 ml-1 cursor-pointer"
onClick={() => {
setSearchParams(prev => ({
...prev,
layerTypes: prev.layerTypes?.filter(t => t !== type) || []
}));
}}
/>
</Badge>
);
})}
</div>
)}
</div>
</div>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right" style={{ width: 80, minWidth: 80 }}>
</label>
<Input
type="number"
className="flex-1 bg-background text-foreground placeholder:text-muted-foreground border border-border focus:outline-none focus:ring-0 focus:border-border transition-all"
placeholder="请输入开始时间戳"
value={searchParams.startTimestamp || ''}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setSearchParams(prev => ({
...prev,
startTimestamp: e.target.value ? parseInt(e.target.value) : undefined
}))
}
/>
</div>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right" style={{ width: 80, minWidth: 80 }}>
</label>
<Input
type="number"
className="flex-1 bg-background text-foreground placeholder:text-muted-foreground border border-border focus:outline-none focus:ring-0 focus:border-border transition-all"
placeholder="请输入结束时间戳"
value={searchParams.endTimestamp || ''}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setSearchParams(prev => ({
...prev,
endTimestamp: e.target.value ? parseInt(e.target.value) : undefined
}))
}
/>
</div>
<div className="flex flex-row items-center gap-2">
<Button variant="outline" onClick={handleReset}></Button>
<Button onClick={handleQuery}></Button>
</div>
</form>
</div>
<div className="rounded-md border bg-background p-4">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">
{protocolLogs.length}
</span>
</div>
<TableToolbar
onRefresh={() => fetchProtocolLogs()}
onDensityChange={setDensity}
onColumnsChange={setColumns}
onColumnsReset={() => setColumns(defaultColumns)}
columns={columns}
density={density}
/>
</div>
<ProtocolLogsTable
protocolLogs={protocolLogs}
loading={loading}
columns={columns}
density={density}
/>
</div>
</div>
</main>
);
}

23
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() {
</Route>
</Route>
{/* 信令分析路由 */}
<Route path="protocol-logs">
<Route index element={<Navigate to="online-logs" replace />} />
<Route path="online-logs" element={
<ProtectedRoute requiredPermission="protocollogs.view">
<AnimatedContainer>
<OnlineProtocolLogsView />
</AnimatedContainer>
</ProtectedRoute>
} />
<Route path="history-logs" element={
<ProtectedRoute requiredPermission="protocollogs.view">
<AnimatedContainer>
<HistoryProtocolLogsView />
</AnimatedContainer>
</ProtectedRoute>
} />
</Route>
{/* 网络栈配置管理路由 */}
<Route path="network-stack-configs">
<Route index element={<Navigate to="ran-configurations" replace />} />

18
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<GetProtocolLogsRequest, 'layerType'> = {}
layerTypes: number[],
options: Omit<GetProtocolLogsRequest, 'layerTypes'> = {}
): Promise<OperationResult<GetProtocolLogsResponse>> {
return this.getProtocolLogs({
layerType,
layerTypes,
...options
});
}

Loading…
Cancel
Save