6 changed files with 440 additions and 73 deletions
@ -0,0 +1,268 @@ |
|||
@{ |
|||
ViewData["Title"] = "实时日志"; |
|||
} |
|||
<style> |
|||
/* 页面布局 */ |
|||
.log-container { |
|||
display: flex; |
|||
height: calc(100vh - 120px); |
|||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace; |
|||
} |
|||
|
|||
/* 左侧日志列表 */ |
|||
.log-list-panel { |
|||
flex: 0 0 65%; |
|||
border-right: 2px solid #ddd; |
|||
display: flex; |
|||
flex-direction: column; |
|||
overflow: hidden; /* 防止内部元素溢出 */ |
|||
} |
|||
|
|||
#log-scroll-area { |
|||
flex-grow: 1; |
|||
overflow-y: auto; |
|||
} |
|||
|
|||
#log-content-area { |
|||
width: 100%; |
|||
} |
|||
|
|||
.log-item { |
|||
padding: 6px 10px; |
|||
border-bottom: 1px solid #f0f0f0; |
|||
cursor: pointer; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
.log-item:hover { |
|||
background-color: #e8f4ff; |
|||
} |
|||
.log-item.selected { |
|||
background-color: #cce7ff; |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.log-item > span { |
|||
text-overflow: ellipsis; |
|||
overflow: hidden; |
|||
padding-right: 15px; |
|||
} |
|||
.log-timestamp { flex-basis: 18%; } |
|||
.log-layer { flex-basis: 10%; } |
|||
.log-direction { flex-basis: 8%; } |
|||
.log-message { flex-basis: 24%; } |
|||
.log-info { flex-basis: 40%; } |
|||
|
|||
|
|||
/* 右侧日志详情 */ |
|||
.log-detail-panel { |
|||
flex-grow: 1; |
|||
padding: 0 20px; |
|||
overflow-y: auto; |
|||
} |
|||
|
|||
.log-detail-panel h4 { |
|||
margin-top: 10px; |
|||
border-bottom: 2px solid #007bff; |
|||
padding-bottom: 5px; |
|||
color: #0056b3; |
|||
} |
|||
|
|||
.detail-item { |
|||
margin-bottom: 12px; |
|||
} |
|||
|
|||
.detail-item-label { |
|||
font-weight: bold; |
|||
color: #333; |
|||
} |
|||
|
|||
.detail-item-value { |
|||
background-color: #f7f7f7; |
|||
padding: 8px; |
|||
border-radius: 4px; |
|||
word-break: break-all; |
|||
white-space: pre-wrap; |
|||
margin-top: 4px; |
|||
} |
|||
|
|||
#detail-placeholder { |
|||
color: #999; |
|||
text-align: center; |
|||
padding-top: 50px; |
|||
font-size: 1.2em; |
|||
} |
|||
|
|||
/* 状态栏 */ |
|||
.status-bar { |
|||
padding: 8px 10px; |
|||
background-color: #f8f9fa; |
|||
border-top: 1px solid #ddd; |
|||
font-size: 0.9em; |
|||
color: #666; |
|||
} |
|||
</style> |
|||
|
|||
<div class="log-container"> |
|||
<div class="log-list-panel"> |
|||
<div id="log-scroll-area"> |
|||
<div id="log-content-area"></div> |
|||
</div> |
|||
<div class="status-bar"> |
|||
总日志条数: <span id="total-logs">0</span> |
|||
</div> |
|||
</div> |
|||
<div class="log-detail-panel"> |
|||
<div id="detail-placeholder"> |
|||
<p>请从左侧选择一条日志以查看详情</p> |
|||
</div> |
|||
<div id="detail-content" class="d-none"> |
|||
<h4>日志详情</h4> |
|||
<!-- 详情将在这里动态生成 --> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
@section Scripts { |
|||
<script src="https://cdnjs.cloudflare.com/ajax/libs/clusterize.js/0.19.0/clusterize.min.js"></script> |
|||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/clusterize.js/0.19.0/clusterize.min.css" /> |
|||
<script> |
|||
document.addEventListener("DOMContentLoaded", function () { |
|||
const scrollArea = document.getElementById('log-scroll-area'); |
|||
const contentArea = document.getElementById('log-content-area'); |
|||
const totalLogsEl = document.getElementById('total-logs'); |
|||
const detailPlaceholder = document.getElementById('detail-placeholder'); |
|||
const detailContent = document.getElementById('detail-content'); |
|||
|
|||
let allLogsData = []; |
|||
let clusterize = new Clusterize({ |
|||
rows: [], |
|||
scrollId: 'log-scroll-area', |
|||
contentId: 'log-content-area', |
|||
tag: 'div', |
|||
rows_in_block: 50, |
|||
blocks_in_cluster: 4 |
|||
}); |
|||
|
|||
// 美化方向 |
|||
function formatDirection(dir) { |
|||
return dir === 0 ? "Uplink" : "Downlink"; |
|||
} |
|||
|
|||
// 格式化日志条目为 HTML 字符串 |
|||
function formatLogItem(log, index) { |
|||
const timestamp = new Date(log.timestamp).toISOString(); |
|||
return `<div class="log-item" data-index="${index}"> |
|||
<span class="log-timestamp" title="${timestamp}">${timestamp}</span> |
|||
<span class="log-layer" title="${log.layer}">${log.layer}</span> |
|||
<span class="log-direction" title="${formatDirection(log.direction)}">${formatDirection(log.direction)}</span> |
|||
<span class="log-message" title="${log.message}">${log.message}</span> |
|||
<span class="log-info" title="${log.info}">${log.info}</span> |
|||
</div>`; |
|||
} |
|||
|
|||
// 更新日志列表 |
|||
function updateLogList(logs, prepend = false) { |
|||
const newRows = logs.map((log, i) => formatLogItem(log, allLogsData.length + i)); |
|||
|
|||
if (prepend) { |
|||
clusterize.prepend(newRows); |
|||
} else { |
|||
clusterize.append(newRows); |
|||
} |
|||
allLogsData.push(...logs); |
|||
totalLogsEl.textContent = allLogsData.length; |
|||
} |
|||
|
|||
// 显示日志详情 |
|||
function showLogDetail(index) { |
|||
const log = allLogsData[index]; |
|||
if (!log) return; |
|||
|
|||
detailPlaceholder.classList.add('d-none'); |
|||
|
|||
const detailHtml = ` |
|||
<div class="detail-item"> |
|||
<div class="detail-item-label">Timestamp</div> |
|||
<div class="detail-item-value">${new Date(log.timestamp).toISOString()}</div> |
|||
</div> |
|||
<div class="detail-item"> |
|||
<div class="detail-item-label">Layer</div> |
|||
<div class="detail-item-value">${log.layer}</div> |
|||
</div> |
|||
<div class="detail-item"> |
|||
<div class="detail-item-label">Direction</div> |
|||
<div class="detail-item-value">${formatDirection(log.direction)} (${log.direction})</div> |
|||
</div> |
|||
<div class="detail-item"> |
|||
<div class="detail-item-label">UeId</div> |
|||
<div class="detail-item-value">${log.ueId || 'N/A'}</div> |
|||
</div> |
|||
<div class="detail-item"> |
|||
<div class="detail-item-label">Rnti</div> |
|||
<div class="detail-item-value">${log.rnti || 'N/A'}</div> |
|||
</div> |
|||
<div class="detail-item"> |
|||
<div class="detail-item-label">Client</div> |
|||
<div class="detail-item-value">${log.client || 'N/A'}</div> |
|||
</div> |
|||
<div class="detail-item"> |
|||
<div class="detail-item-label">Message</div> |
|||
<div class="detail-item-value">${log.message}</div> |
|||
</div> |
|||
<div class="detail-item"> |
|||
<div class="detail-item-label">Info</div> |
|||
<div class="detail-item-value">${log.info}</div> |
|||
</div> |
|||
<div class="detail-item"> |
|||
<div class="detail-item-label">Data</div> |
|||
<div class="detail-item-value">${log.data || 'N/A'}</div> |
|||
</div> |
|||
`; |
|||
|
|||
detailContent.innerHTML = '<h4>日志详情</h4>' + detailHtml; |
|||
detailContent.classList.remove('d-none'); |
|||
} |
|||
|
|||
// 事件委托处理点击事件 |
|||
contentArea.addEventListener('click', function(e) { |
|||
const item = e.target.closest('.log-item'); |
|||
if (item) { |
|||
// 移除之前选中的 |
|||
const selected = contentArea.querySelector('.log-item.selected'); |
|||
if (selected) { |
|||
selected.classList.remove('selected'); |
|||
} |
|||
// 添加选中样式 |
|||
item.classList.add('selected'); |
|||
|
|||
const index = parseInt(item.dataset.index, 10); |
|||
showLogDetail(index); |
|||
} |
|||
}); |
|||
|
|||
// SSE 连接 |
|||
const eventSource = new EventSource('/api/websocket/logs/stream'); |
|||
|
|||
eventSource.addEventListener('history', function(event) { |
|||
console.log("接收到历史日志..."); |
|||
const data = JSON.parse(event.data); |
|||
updateLogList(data.logs); |
|||
}); |
|||
|
|||
eventSource.addEventListener('new_logs', function(event) { |
|||
console.log("接收到新日志..."); |
|||
const data = JSON.parse(event.data); |
|||
updateLogList(data.logs); |
|||
}); |
|||
|
|||
eventSource.onerror = function (err) { |
|||
console.error("SSE 错误:", err); |
|||
totalLogsEl.textContent += " (连接已断开)"; |
|||
eventSource.close(); |
|||
}; |
|||
}); |
|||
</script> |
|||
} |
Loading…
Reference in new issue