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