|
|
@ -265,21 +265,105 @@ |
|
|
|
} |
|
|
|
|
|
|
|
/* Layer过滤器样式 */ |
|
|
|
.layer-filter-dropdown { |
|
|
|
.layer-filter-container { |
|
|
|
position: relative; |
|
|
|
margin-left: 8px; |
|
|
|
padding: 2px 6px; |
|
|
|
font-size: 0.8em; |
|
|
|
} |
|
|
|
|
|
|
|
.layer-filter-trigger { |
|
|
|
padding: 4px 8px; |
|
|
|
border: 1px solid #ced4da; |
|
|
|
border-radius: 3px; |
|
|
|
background-color: #fff; |
|
|
|
cursor: pointer; |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
gap: 4px; |
|
|
|
min-width: 80px; |
|
|
|
font-size: 0.8em; |
|
|
|
} |
|
|
|
|
|
|
|
.layer-filter-dropdown:focus { |
|
|
|
outline: none; |
|
|
|
.layer-filter-trigger:hover { |
|
|
|
border-color: #007bff; |
|
|
|
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); |
|
|
|
} |
|
|
|
|
|
|
|
.filter-text { |
|
|
|
font-size: 0.8em; |
|
|
|
} |
|
|
|
|
|
|
|
.filter-arrow { |
|
|
|
font-size: 0.8em; |
|
|
|
transition: transform 0.2s; |
|
|
|
} |
|
|
|
|
|
|
|
.layer-filter-container.open .filter-arrow { |
|
|
|
transform: rotate(180deg); |
|
|
|
} |
|
|
|
|
|
|
|
.layer-filter-dropdown { |
|
|
|
position: absolute; |
|
|
|
top: 100%; |
|
|
|
left: 0; |
|
|
|
width: 150px; |
|
|
|
background-color: #fff; |
|
|
|
border: 1px solid #ced4da; |
|
|
|
border-radius: 3px; |
|
|
|
max-height: 200px; |
|
|
|
overflow-y: auto; |
|
|
|
z-index: 1000; |
|
|
|
display: none; |
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1); |
|
|
|
} |
|
|
|
|
|
|
|
.layer-filter-container.open .layer-filter-dropdown { |
|
|
|
display: block; |
|
|
|
} |
|
|
|
|
|
|
|
.filter-header { |
|
|
|
padding: 8px 10px; |
|
|
|
background-color: #f8f9fa; |
|
|
|
border-bottom: 1px solid #dee2e6; |
|
|
|
} |
|
|
|
|
|
|
|
.select-all-label { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
gap: 4px; |
|
|
|
font-size: 0.8em; |
|
|
|
cursor: pointer; |
|
|
|
} |
|
|
|
|
|
|
|
.select-all-label input { |
|
|
|
margin: 0; |
|
|
|
} |
|
|
|
|
|
|
|
.filter-options { |
|
|
|
padding: 8px 10px; |
|
|
|
} |
|
|
|
|
|
|
|
.filter-option { |
|
|
|
margin-bottom: 6px; |
|
|
|
} |
|
|
|
|
|
|
|
.filter-option:last-child { |
|
|
|
margin-bottom: 0; |
|
|
|
} |
|
|
|
|
|
|
|
.filter-option label { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
gap: 4px; |
|
|
|
font-size: 0.8em; |
|
|
|
cursor: pointer; |
|
|
|
padding: 2px 0; |
|
|
|
} |
|
|
|
|
|
|
|
.filter-option label:hover { |
|
|
|
background-color: #f8f9fa; |
|
|
|
} |
|
|
|
|
|
|
|
.filter-option input { |
|
|
|
margin: 0; |
|
|
|
} |
|
|
|
|
|
|
|
.log-layer { |
|
|
@ -299,9 +383,22 @@ |
|
|
|
<span class="log-timestamp">Timestamp</span> |
|
|
|
<span class="log-layer"> |
|
|
|
Layer |
|
|
|
<select id="layer-filter" class="layer-filter-dropdown"> |
|
|
|
<option value="">全部</option> |
|
|
|
</select> |
|
|
|
<div class="layer-filter-container"> |
|
|
|
<div class="layer-filter-trigger" id="layer-filter-trigger"> |
|
|
|
<span class="filter-text">全部</span> |
|
|
|
<span class="filter-arrow">▼</span> |
|
|
|
</div> |
|
|
|
<div class="layer-filter-dropdown" id="layer-filter-dropdown"> |
|
|
|
<div class="filter-header"> |
|
|
|
<label class="select-all-label"> |
|
|
|
<input type="checkbox" id="select-all-layers"> 全选 |
|
|
|
</label> |
|
|
|
</div> |
|
|
|
<div class="filter-options" id="layer-filter-options"> |
|
|
|
<!-- 选项将动态生成 --> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</span> |
|
|
|
<span class="log-direction">Direction</span> |
|
|
|
<span class="log-message">Message</span> |
|
|
@ -362,10 +459,15 @@ |
|
|
|
const reconnectBtn = document.getElementById('reconnect-btn'); |
|
|
|
const logListPanel = document.querySelector('.log-list-panel'); |
|
|
|
const resizer = document.getElementById('drag-resizer'); |
|
|
|
const layerFilter = document.getElementById('layer-filter'); |
|
|
|
const layerFilter = document.getElementById('layer-filter-dropdown'); |
|
|
|
const layerFilterTrigger = document.getElementById('layer-filter-trigger'); |
|
|
|
const layerFilterOptions = document.getElementById('layer-filter-options'); |
|
|
|
const selectAllLayers = document.getElementById('select-all-layers'); |
|
|
|
const filterText = document.querySelector('.filter-text'); |
|
|
|
|
|
|
|
let allLogsData = []; |
|
|
|
let availableLayers = new Set(); // 用于跟踪可用的日志层 |
|
|
|
let availableLayers = new Set(['PHY', 'MAC', 'RLC', 'PDCP', 'RRC', 'NAS']); // 初始化标准LTE层 |
|
|
|
let selectedLayers = new Set(); // 用于跟踪选中的日志层 |
|
|
|
let eventSource = null; |
|
|
|
let reconnectAttempts = 0; |
|
|
|
const maxReconnectAttempts = 5; |
|
|
@ -381,29 +483,86 @@ |
|
|
|
no_data_text: "正在等待日志..." |
|
|
|
}); |
|
|
|
|
|
|
|
// 更新过滤器显示文本 |
|
|
|
function updateFilterText() { |
|
|
|
if (selectedLayers.size === 0) { |
|
|
|
filterText.textContent = '全部'; |
|
|
|
} else if (selectedLayers.size === availableLayers.size) { |
|
|
|
filterText.textContent = '全部'; |
|
|
|
} else if (selectedLayers.size === 1) { |
|
|
|
filterText.textContent = Array.from(selectedLayers)[0]; |
|
|
|
} else { |
|
|
|
filterText.textContent = `${selectedLayers.size}个层`; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 更新全选复选框状态 |
|
|
|
function updateSelectAllState() { |
|
|
|
if (availableLayers.size === 0) { |
|
|
|
selectAllLayers.checked = false; |
|
|
|
selectAllLayers.indeterminate = false; |
|
|
|
} else if (selectedLayers.size === 0) { |
|
|
|
selectAllLayers.checked = false; |
|
|
|
selectAllLayers.indeterminate = false; |
|
|
|
} else if (selectedLayers.size === availableLayers.size) { |
|
|
|
selectAllLayers.checked = true; |
|
|
|
selectAllLayers.indeterminate = false; |
|
|
|
} else { |
|
|
|
selectAllLayers.checked = false; |
|
|
|
selectAllLayers.indeterminate = true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 更新Layer过滤器选项 |
|
|
|
function updateLayerFilter() { |
|
|
|
const currentValue = layerFilter.value; |
|
|
|
const options = ['<option value="">全部</option>']; |
|
|
|
const options = []; |
|
|
|
|
|
|
|
// 按字母顺序排序 |
|
|
|
const sortedLayers = Array.from(availableLayers).sort(); |
|
|
|
|
|
|
|
sortedLayers.forEach(layer => { |
|
|
|
const selected = layer === currentValue ? ' selected' : ''; |
|
|
|
options.push(`<option value="${layer}"${selected}>${layer}</option>`); |
|
|
|
const isChecked = selectedLayers.has(layer) ? ' checked' : ''; |
|
|
|
options.push(`<div class="filter-option"> |
|
|
|
<label> |
|
|
|
<input type="checkbox" value="${layer}"${isChecked}> |
|
|
|
${layer} |
|
|
|
</label> |
|
|
|
</div>`); |
|
|
|
}); |
|
|
|
|
|
|
|
layerFilter.innerHTML = options.join(''); |
|
|
|
layerFilterOptions.innerHTML = options.join(''); |
|
|
|
|
|
|
|
// 重新绑定事件 |
|
|
|
bindFilterEvents(); |
|
|
|
|
|
|
|
// 更新显示文本和全选状态 |
|
|
|
updateFilterText(); |
|
|
|
updateSelectAllState(); |
|
|
|
} |
|
|
|
|
|
|
|
// 绑定过滤器事件 |
|
|
|
function bindFilterEvents() { |
|
|
|
// 绑定单个复选框事件 |
|
|
|
layerFilterOptions.querySelectorAll('input[type="checkbox"]').forEach(checkbox => { |
|
|
|
checkbox.addEventListener('change', function() { |
|
|
|
if (this.checked) { |
|
|
|
selectedLayers.add(this.value); |
|
|
|
} else { |
|
|
|
selectedLayers.delete(this.value); |
|
|
|
} |
|
|
|
updateFilterText(); |
|
|
|
updateSelectAllState(); |
|
|
|
refreshLogList(); |
|
|
|
}); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// 根据当前过滤器重新渲染日志列表 |
|
|
|
function refreshLogList() { |
|
|
|
const selectedLayer = layerFilter.value; |
|
|
|
let filteredLogs = allLogsData; |
|
|
|
|
|
|
|
if (selectedLayer) { |
|
|
|
filteredLogs = allLogsData.filter(log => log.Layer === selectedLayer); |
|
|
|
if (selectedLayers.size > 0 && selectedLayers.size < availableLayers.size) { |
|
|
|
filteredLogs = allLogsData.filter(log => selectedLayers.has(log.Layer)); |
|
|
|
} |
|
|
|
|
|
|
|
const rows = filteredLogs.map((log, i) => formatLogItem(log, i)); |
|
|
@ -442,6 +601,7 @@ |
|
|
|
function clearLogsDisplay() { |
|
|
|
allLogsData = []; |
|
|
|
availableLayers.clear(); |
|
|
|
selectedLayers.clear(); |
|
|
|
clusterize.clear(); |
|
|
|
totalLogsEl.textContent = '0'; |
|
|
|
newLogsCountEl.textContent = ''; |
|
|
@ -668,11 +828,51 @@ |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
// Layer过滤器变化事件 |
|
|
|
layerFilter.addEventListener('change', function() { |
|
|
|
// 切换下拉框显示状态 |
|
|
|
function toggleFilterDropdown() { |
|
|
|
const container = layerFilterTrigger.parentElement; |
|
|
|
container.classList.toggle('open'); |
|
|
|
} |
|
|
|
|
|
|
|
// 关闭下拉框 |
|
|
|
function closeFilterDropdown() { |
|
|
|
const container = layerFilterTrigger.parentElement; |
|
|
|
container.classList.remove('open'); |
|
|
|
} |
|
|
|
|
|
|
|
// 事件监听器 |
|
|
|
layerFilterTrigger.addEventListener('click', function(e) { |
|
|
|
e.stopPropagation(); |
|
|
|
toggleFilterDropdown(); |
|
|
|
}); |
|
|
|
|
|
|
|
// 全选/取消全选 |
|
|
|
selectAllLayers.addEventListener('change', function() { |
|
|
|
const isChecked = selectAllLayers.checked; |
|
|
|
if (isChecked) { |
|
|
|
// 全选 |
|
|
|
selectedLayers.clear(); |
|
|
|
availableLayers.forEach(layer => selectedLayers.add(layer)); |
|
|
|
} else { |
|
|
|
// 取消全选 |
|
|
|
selectedLayers.clear(); |
|
|
|
} |
|
|
|
updateLayerFilter(); |
|
|
|
refreshLogList(); |
|
|
|
}); |
|
|
|
|
|
|
|
// 点击外部关闭下拉框 |
|
|
|
document.addEventListener('click', function(e) { |
|
|
|
if (!layerFilterTrigger.contains(e.target) && !layerFilter.contains(e.target)) { |
|
|
|
closeFilterDropdown(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// 阻止下拉框内部点击事件冒泡 |
|
|
|
layerFilter.addEventListener('click', function(e) { |
|
|
|
e.stopPropagation(); |
|
|
|
}); |
|
|
|
|
|
|
|
// 事件委托处理点击事件 |
|
|
|
contentArea.addEventListener('click', function(e) { |
|
|
|
const item = e.target.closest('.log-item'); |
|
|
@ -785,6 +985,9 @@ |
|
|
|
|
|
|
|
// 初始化连接 |
|
|
|
connectSSE(); |
|
|
|
|
|
|
|
// 初始化Layer过滤器,显示标准LTE层 |
|
|
|
updateLayerFilter(); |
|
|
|
}); |
|
|
|
</script> |
|
|
|
} |
|
|
|