@@ -800,6 +800,10 @@ <h1><i class="fas fa-plug"></i> WebSocket 专业测试控制台</h1>
800800 实时日志控制台
801801 </ div >
802802 < div class ="terminal-controls ">
803+ < button class ="terminal-btn " onclick ="toggleHeartbeatVisibility() ">
804+ < i class ="fas fa-heartbeat " id ="heartbeatIcon "> </ i >
805+ < span id ="heartbeatText "> 显示心跳</ span >
806+ </ button >
803807 < button class ="terminal-btn " onclick ="toggleDisplayMode() ">
804808 < i class ="fas fa-list-ul " id ="displayModeIcon "> </ i >
805809 < span id ="displayModeText "> 日志模式</ span >
@@ -855,6 +859,7 @@ <h1><i class="fas fa-plug"></i> WebSocket 专业测试控制台</h1>
855859 let currentFilter = 'all' ;
856860 let logs = [ ] ;
857861 let displayMode = 'log' ; // 'raw' 或 'log'
862+ let heartbeatVisible = false ;
858863
859864 // 初始化
860865 window . onload = function ( ) {
@@ -921,72 +926,146 @@ <h1><i class="fas fa-plug"></i> WebSocket 专业测试控制台</h1>
921926
922927 // 限制日志数量
923928 if ( logs . length > 1000 ) {
924- logs = logs . slice ( - 1000 ) ;
929+ logs . shift ( ) ;
925930 }
926931
927- renderLogs ( ) ;
932+ // 优化:直接追加DOM而不是重绘
933+ if ( shouldShowLog ( logEntry ) ) {
934+ const terminalContent = document . getElementById ( 'terminalContent' ) ;
935+ const logElement = createLogElement ( logEntry ) ;
936+ terminalContent . appendChild ( logElement ) ;
937+
938+ // 保持DOM数量合理
939+ if ( terminalContent . children . length > 1000 ) {
940+ terminalContent . removeChild ( terminalContent . firstChild ) ;
941+ }
942+
943+ if ( autoScrollEnabled ) {
944+ terminalContent . scrollTop = terminalContent . scrollHeight ;
945+ }
946+ }
947+
928948 updateStats ( ) ;
929949 }
930950
931951 // 渲染日志
932952 function renderLogs ( ) {
933953 const terminalContent = document . getElementById ( 'terminalContent' ) ;
934- const filteredLogs = currentFilter === 'all' ? logs : logs . filter ( log => log . type === currentFilter ) ;
935-
936954 terminalContent . innerHTML = '' ;
937955
956+ const fragment = document . createDocumentFragment ( ) ;
957+ const filteredLogs = logs . filter ( shouldShowLog ) ;
958+
938959 filteredLogs . forEach ( log => {
939- const logElement = document . createElement ( 'div' ) ;
940-
941- if ( displayMode === 'log' ) {
942- // 日志模式:单行显示
943- logElement . className = 'log-entry log-mode' ;
944- const logLine = formatAsLogLine ( log ) ;
945-
946- // 解析日志行,分离前缀和内容
947- const logMatch = logLine . match ( / ^ ( \[ .* ?\] ) \s * ( \[ .* ?\] ) \s * ( .* ) $ / ) ;
948- if ( logMatch ) {
949- const [ , timestamp , logType , content ] = logMatch ;
950- logElement . innerHTML = `
951- <div class="log-line ${ log . type } ">
952- <span class="log-prefix">
953- <span class="log-timestamp-tag">${ timestamp } </span>
954- <span class="log-type-tag ${ log . type } ">${ logType } </span>
955- </span>
956- <span class="log-message">${ content } </span>
957- </div>
958- ` ;
959- } else {
960- // 如果无法解析,使用原始格式
961- logElement . innerHTML = `<div class="log-line ${ log . type } ">${ logLine } </div>` ;
962- }
963- } else {
964- // 原始数据模式:原有的显示方式
965- logElement . className = 'log-entry' ;
966-
967- let contentHtml = log . content ;
968- if ( log . data ) {
969- try {
970- const jsonStr = JSON . stringify ( log . data , null , 2 ) ;
971- contentHtml += `<div class="log-json">${ jsonStr } </div>` ;
972- } catch ( e ) {
973- contentHtml += `<div class="log-json">${ log . data } </div>` ;
974- }
975- }
960+ fragment . appendChild ( createLogElement ( log ) ) ;
961+ } ) ;
962+
963+ terminalContent . appendChild ( fragment ) ;
964+
965+ if ( autoScrollEnabled ) {
966+ terminalContent . scrollTop = terminalContent . scrollHeight ;
967+ }
968+ }
976969
970+ // 创建日志元素
971+ function createLogElement ( log ) {
972+ const logElement = document . createElement ( 'div' ) ;
973+
974+ if ( displayMode === 'log' ) {
975+ // 日志模式:单行显示
976+ logElement . className = 'log-entry log-mode' ;
977+ const logLine = formatAsLogLine ( log ) ;
978+
979+ // 解析日志行,分离前缀和内容
980+ const logMatch = logLine . match ( / ^ ( \[ .* ?\] ) \s * ( \[ .* ?\] ) \s * ( .* ) $ / ) ;
981+ if ( logMatch ) {
982+ const [ , timestamp , logType , content ] = logMatch ;
977983 logElement . innerHTML = `
978- <div class="log-timestamp">${ log . timestamp } </div>
979- <div class="log-type ${ log . type } ">${ log . type } </div>
980- <div class="log-content">${ contentHtml } </div>
984+ <div class="log-line ${ log . type } ">
985+ <span class="log-prefix">
986+ <span class="log-timestamp-tag">${ timestamp } </span>
987+ <span class="log-type-tag ${ log . type } ">${ logType } </span>
988+ </span>
989+ <span class="log-message">${ content } </span>
990+ </div>
981991 ` ;
992+ } else {
993+ // 如果无法解析,使用原始格式
994+ logElement . innerHTML = `<div class="log-line ${ log . type } ">${ logLine } </div>` ;
982995 }
996+ } else {
997+ // 原始数据模式:原有的显示方式
998+ logElement . className = 'log-entry' ;
983999
984- terminalContent . appendChild ( logElement ) ;
985- } ) ;
1000+ let contentHtml = log . content ;
1001+ if ( log . data ) {
1002+ try {
1003+ const jsonStr = JSON . stringify ( log . data , null , 2 ) ;
1004+ contentHtml += `<div class="log-json">${ jsonStr } </div>` ;
1005+ } catch ( e ) {
1006+ contentHtml += `<div class="log-json">${ log . data } </div>` ;
1007+ }
1008+ }
9861009
987- if ( autoScrollEnabled ) {
988- terminalContent . scrollTop = terminalContent . scrollHeight ;
1010+ logElement . innerHTML = `
1011+ <div class="log-timestamp">${ log . timestamp } </div>
1012+ <div class="log-type ${ log . type } ">${ log . type } </div>
1013+ <div class="log-content">${ contentHtml } </div>
1014+ ` ;
9891015 }
1016+ return logElement ;
1017+ }
1018+
1019+ // 判断日志是否应该显示
1020+ function shouldShowLog ( log ) {
1021+ if ( currentFilter !== 'all' && log . type !== currentFilter ) return false ;
1022+ if ( ! heartbeatVisible && isHeartbeat ( log ) ) return false ;
1023+ return true ;
1024+ }
1025+
1026+ // 判断是否是心跳包
1027+ function isHeartbeat ( log ) {
1028+ if ( ! log . data ) return false ;
1029+
1030+ // 直接心跳类型
1031+ if ( log . data . type === 'heartbeat' ) return true ;
1032+
1033+ // ECHO类型中包含心跳
1034+ if ( log . data . type === 'echo' ) {
1035+ let content = log . data . originalMessage || log . data . message || log . data . data ;
1036+ // 尝试解析字符串内容
1037+ if ( typeof content === 'string' ) {
1038+ try {
1039+ // 简单判断字符串是否包含心跳特征,避免解析开销
1040+ if ( content . includes ( '"type":"heartbeat"' ) ) return true ;
1041+ const parsed = JSON . parse ( content ) ;
1042+ if ( parsed && parsed . type === 'heartbeat' ) return true ;
1043+ } catch ( e ) { }
1044+ }
1045+ // 如果是对象
1046+ if ( content && typeof content === 'object' && content . type === 'heartbeat' ) {
1047+ return true ;
1048+ }
1049+ }
1050+ return false ;
1051+ }
1052+
1053+ // 切换心跳显示
1054+ function toggleHeartbeatVisibility ( ) {
1055+ heartbeatVisible = ! heartbeatVisible ;
1056+ const icon = document . getElementById ( 'heartbeatIcon' ) ;
1057+ const text = document . getElementById ( 'heartbeatText' ) ;
1058+
1059+ if ( heartbeatVisible ) {
1060+ icon . className = 'fas fa-heart-broken' ;
1061+ text . textContent = '隐藏心跳' ;
1062+ } else {
1063+ icon . className = 'fas fa-heartbeat' ;
1064+ text . textContent = '显示心跳' ;
1065+ }
1066+
1067+ renderLogs ( ) ;
1068+ addLog ( `心跳显示已${ heartbeatVisible ? '启用' : '禁用' } ` , 'system' ) ;
9901069 }
9911070
9921071 // 过滤日志
0 commit comments