|
|
@ -1,14 +1,15 @@ |
|
|
|
import React, { useState } from 'react'; |
|
|
|
import { ProtocolLogDto } from '@/services/protocolLogsService'; |
|
|
|
import { ProtocolLogDto, protocolLogsService } 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 { Eye, X, Loader2 } 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'; |
|
|
|
import { useToast } from '@/components/ui/use-toast'; |
|
|
|
|
|
|
|
interface ProtocolLogsTableProps { |
|
|
|
protocolLogs: ProtocolLogDto[]; |
|
|
@ -25,12 +26,42 @@ export default function ProtocolLogsTable({ |
|
|
|
}: ProtocolLogsTableProps) { |
|
|
|
const [selectedMessageDetail, setSelectedMessageDetail] = useState<string>(''); |
|
|
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false); |
|
|
|
const [loadingMessageDetail, setLoadingMessageDetail] = useState(false); |
|
|
|
const { toast } = useToast(); |
|
|
|
|
|
|
|
// 密度样式映射
|
|
|
|
// 密度样式映射 - 更紧凑的样式
|
|
|
|
const densityStyles = { |
|
|
|
relaxed: 'py-3', |
|
|
|
compact: 'py-1', |
|
|
|
default: 'py-2', |
|
|
|
relaxed: 'py-2 px-2', |
|
|
|
compact: 'py-1 px-1', |
|
|
|
default: 'py-1.5 px-2', |
|
|
|
}; |
|
|
|
|
|
|
|
// 列宽度配置 - 为Message列分配更多空间
|
|
|
|
const getColumnWidth = (columnKey: string) => { |
|
|
|
switch (columnKey) { |
|
|
|
case 'layerType': |
|
|
|
return 'w-16'; // 固定宽度
|
|
|
|
case 'time': |
|
|
|
return 'w-24'; // 时间列
|
|
|
|
case 'plmn': |
|
|
|
return 'w-20'; // PLMN列
|
|
|
|
case 'info': |
|
|
|
return 'w-24'; // Info列
|
|
|
|
case 'ueid': |
|
|
|
return 'w-16'; // UEID列
|
|
|
|
case 'imsi': |
|
|
|
return 'w-28'; // IMSI列
|
|
|
|
case 'cellID': |
|
|
|
return 'w-16'; // CellID列
|
|
|
|
case 'direction': |
|
|
|
return 'w-16'; // 方向列
|
|
|
|
case 'message': |
|
|
|
return 'flex-1 min-w-0'; // Message列占用剩余空间
|
|
|
|
case 'MessageDetailJson': |
|
|
|
return 'w-12'; // 详情按钮列
|
|
|
|
default: |
|
|
|
return 'w-auto'; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
// 格式化时间戳
|
|
|
@ -69,94 +100,108 @@ export default function ProtocolLogsTable({ |
|
|
|
|
|
|
|
|
|
|
|
// 查看消息详情
|
|
|
|
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); |
|
|
|
const handleViewMessageDetail = async (log: ProtocolLogDto) => { |
|
|
|
// 如果已经有消息详情,直接显示
|
|
|
|
if (log.messageDetailJson) { |
|
|
|
let formattedContent = log.messageDetailJson; |
|
|
|
try { |
|
|
|
// 如果是 JSON 字符串,尝试解析并格式化
|
|
|
|
if (log.messageDetailJson && (log.messageDetailJson.startsWith('[') || log.messageDetailJson.startsWith('{'))) { |
|
|
|
const parsed = JSON.parse(log.messageDetailJson); |
|
|
|
formattedContent = JSON.stringify(parsed, null, 2); |
|
|
|
} |
|
|
|
} catch (error) { |
|
|
|
// 如果解析失败,保持原始内容
|
|
|
|
console.log('messageDetailJson 不是有效的 JSON 格式,使用原始内容'); |
|
|
|
} |
|
|
|
} catch (error) { |
|
|
|
// 如果解析失败,保持原始内容
|
|
|
|
console.log('messageDetailJson 不是有效的 JSON 格式,使用原始内容'); |
|
|
|
setSelectedMessageDetail(formattedContent); |
|
|
|
setIsDrawerOpen(true); |
|
|
|
return; |
|
|
|
} |
|
|
|
setSelectedMessageDetail(formattedContent); |
|
|
|
setIsDrawerOpen(true); |
|
|
|
}; |
|
|
|
|
|
|
|
// 复制消息详情
|
|
|
|
const handleCopyMessageDetail = async () => { |
|
|
|
// 如果没有消息详情,从API获取
|
|
|
|
setLoadingMessageDetail(true); |
|
|
|
try { |
|
|
|
await navigator.clipboard.writeText(selectedMessageDetail); |
|
|
|
// 可以添加一个 toast 提示
|
|
|
|
const result = await protocolLogsService.getMessageDetailJsonById(log.id); |
|
|
|
if (result.isSuccess && result.data?.found) { |
|
|
|
let formattedContent = result.data.messageDetailJson || ''; |
|
|
|
try { |
|
|
|
if (formattedContent && (formattedContent.startsWith('[') || formattedContent.startsWith('{'))) { |
|
|
|
const parsed = JSON.parse(formattedContent); |
|
|
|
formattedContent = JSON.stringify(parsed, null, 2); |
|
|
|
} |
|
|
|
} catch (error) { |
|
|
|
console.log('messageDetailJson 不是有效的 JSON 格式,使用原始内容'); |
|
|
|
} |
|
|
|
setSelectedMessageDetail(formattedContent); |
|
|
|
setIsDrawerOpen(true); |
|
|
|
} else { |
|
|
|
toast({ |
|
|
|
title: "获取失败", |
|
|
|
description: result.errorMessages?.join(', ') || "未找到消息详情", |
|
|
|
variant: "destructive", |
|
|
|
}); |
|
|
|
} |
|
|
|
} catch (error) { |
|
|
|
console.error('复制失败:', error); |
|
|
|
toast({ |
|
|
|
title: "获取失败", |
|
|
|
description: "获取消息详情时发生错误", |
|
|
|
variant: "destructive", |
|
|
|
}); |
|
|
|
} finally { |
|
|
|
setLoadingMessageDetail(false); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
// 下载消息详情
|
|
|
|
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"> |
|
|
|
<div className="text-xs font-medium"> |
|
|
|
{getProtocolLayerLabel(log.layerType)} |
|
|
|
</div> |
|
|
|
); |
|
|
|
|
|
|
|
case 'time': |
|
|
|
return ( |
|
|
|
<div className="text-sm text-muted-foreground"> |
|
|
|
<div className="text-xs text-muted-foreground font-mono"> |
|
|
|
{log.time ? log.time.substring(0, 12) : '-'} |
|
|
|
</div> |
|
|
|
); |
|
|
|
|
|
|
|
case 'plmn': |
|
|
|
return ( |
|
|
|
<div className="text-sm font-mono"> |
|
|
|
<div className="text-xs font-mono"> |
|
|
|
{(log as any).plmn || log.PLMN || '-'} |
|
|
|
</div> |
|
|
|
); |
|
|
|
|
|
|
|
case 'info': |
|
|
|
return ( |
|
|
|
<div className="text-sm max-w-xs truncate" title={log.info}> |
|
|
|
<div className="text-xs max-w-full truncate" title={log.info}> |
|
|
|
{log.info || '-'} |
|
|
|
</div> |
|
|
|
); |
|
|
|
|
|
|
|
case 'ueid': |
|
|
|
return ( |
|
|
|
<div className="text-sm text-muted-foreground"> |
|
|
|
<div className="text-xs text-muted-foreground"> |
|
|
|
{(log as any).ueid || log.UEID || '-'} |
|
|
|
</div> |
|
|
|
); |
|
|
|
|
|
|
|
case 'imsi': |
|
|
|
return ( |
|
|
|
<div className="text-sm font-mono"> |
|
|
|
<div className="text-xs font-mono"> |
|
|
|
{(log as any).imsi || log.IMSI || '-'} |
|
|
|
</div> |
|
|
|
); |
|
|
|
|
|
|
|
case 'cellID': |
|
|
|
return ( |
|
|
|
<div className="text-sm text-muted-foreground"> |
|
|
|
<div className="text-xs text-muted-foreground"> |
|
|
|
{(log as any).cellID || log.CellID || '-'} |
|
|
|
</div> |
|
|
|
); |
|
|
@ -167,7 +212,7 @@ export default function ProtocolLogsTable({ |
|
|
|
return ( |
|
|
|
<Badge |
|
|
|
variant="outline" |
|
|
|
className={cn(directionColor)} |
|
|
|
className={cn(directionColor, "text-xs px-1 py-0.5")} |
|
|
|
> |
|
|
|
{directionDesc} |
|
|
|
</Badge> |
|
|
@ -175,7 +220,7 @@ export default function ProtocolLogsTable({ |
|
|
|
|
|
|
|
case 'message': |
|
|
|
return ( |
|
|
|
<div className="text-sm max-w-xs truncate" title={log.message}> |
|
|
|
<div className="text-xs break-words whitespace-normal text-left" title={log.message}> |
|
|
|
{log.message || '-'} |
|
|
|
</div> |
|
|
|
); |
|
|
@ -186,12 +231,16 @@ export default function ProtocolLogsTable({ |
|
|
|
<Button |
|
|
|
variant="ghost" |
|
|
|
size="sm" |
|
|
|
onClick={() => handleViewMessageDetail(log.messageDetailJson || '')} |
|
|
|
disabled={!log.messageDetailJson} |
|
|
|
className="h-8 w-8 p-0 hover:bg-accent" |
|
|
|
onClick={() => handleViewMessageDetail(log)} |
|
|
|
disabled={loadingMessageDetail} |
|
|
|
className="h-6 w-6 p-0 hover:bg-accent" |
|
|
|
title="查看详情" |
|
|
|
> |
|
|
|
<Eye className="h-4 w-4 text-muted-foreground hover:text-foreground" /> |
|
|
|
{loadingMessageDetail ? ( |
|
|
|
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" /> |
|
|
|
) : ( |
|
|
|
<Eye className="h-3 w-3 text-muted-foreground hover:text-foreground" /> |
|
|
|
)} |
|
|
|
</Button> |
|
|
|
</div> |
|
|
|
); |
|
|
@ -210,11 +259,18 @@ export default function ProtocolLogsTable({ |
|
|
|
<div className="overflow-x-auto"> |
|
|
|
{/* 固定的表头 */} |
|
|
|
<div className="bg-background border-b shadow-sm"> |
|
|
|
<Table className="w-full table-fixed"> |
|
|
|
<Table className="w-full"> |
|
|
|
<TableHeader> |
|
|
|
<TableRow> |
|
|
|
<TableRow className="border-b-0"> |
|
|
|
{visibleColumns.map((column) => ( |
|
|
|
<TableHead key={column.key} className={cn("text-center whitespace-nowrap bg-background", densityStyles[density])} style={{ width: `${100 / visibleColumns.length}%` }}> |
|
|
|
<TableHead |
|
|
|
key={column.key} |
|
|
|
className={cn( |
|
|
|
"text-center whitespace-nowrap bg-background text-xs font-medium", |
|
|
|
densityStyles[density], |
|
|
|
getColumnWidth(column.key) |
|
|
|
)} |
|
|
|
> |
|
|
|
{column.title} |
|
|
|
</TableHead> |
|
|
|
))} |
|
|
@ -225,7 +281,7 @@ export default function ProtocolLogsTable({ |
|
|
|
|
|
|
|
{/* 可滚动的表体 */} |
|
|
|
<div className="max-h-[600px] overflow-auto"> |
|
|
|
<Table className="w-full table-fixed"> |
|
|
|
<Table className="w-full"> |
|
|
|
<TableBody> |
|
|
|
{loading ? ( |
|
|
|
<TableRow> |
|
|
@ -246,9 +302,17 @@ export default function ProtocolLogsTable({ |
|
|
|
</TableRow> |
|
|
|
) : ( |
|
|
|
protocolLogs.map((log, index) => ( |
|
|
|
<TableRow key={`${log.id}-${index}`}> |
|
|
|
<TableRow key={`${log.id}-${index}`} className="hover:bg-muted/50"> |
|
|
|
{visibleColumns.map((column) => ( |
|
|
|
<TableCell key={column.key} className={cn("text-center whitespace-nowrap", densityStyles[density])} style={{ width: `${100 / visibleColumns.length}%` }}> |
|
|
|
<TableCell |
|
|
|
key={column.key} |
|
|
|
className={cn( |
|
|
|
"text-center whitespace-nowrap border-b", |
|
|
|
densityStyles[density], |
|
|
|
getColumnWidth(column.key), |
|
|
|
column.key === 'message' ? 'text-left' : 'text-center' |
|
|
|
)} |
|
|
|
> |
|
|
|
{renderCell(log, column.key)} |
|
|
|
</TableCell> |
|
|
|
))} |
|
|
|