class LogMonitorAdaptive { constructor(container, opts = {}) { this.container = typeof container === 'string' ? document.querySelector(container) : container; if (!this.container) throw new Error('容器未找到'); /* ========== 配置项及其含义 ========== */ this.cfg = { /* --- 尺寸相关 --- */ width: null, // 日志容器宽度。null=100%,可填数字(px)或字符串('80%'/'20rem') height: null, // 日志容器高度。null=100vh,可填数字(px)或字符串('400px'/'50vh') /* --- 日志条数与滚动 --- */ maxLines: 5000, // 最大保留日志条数,超出后自动删除最旧日志 autoScroll: true, // 出现新日志时是否自动滚动到底部 /* --- 主题与外观 --- */ theme: 'dark', // 主题:'dark' | 'light',控制整体配色 fontSize: 14, // 日志文字大小,单位 px wordWrap: false, // 日志内容是否自动换行(true=换行,false=横向滚动) /* --- 日志内容格式 --- */ showTimestamp: true, // 是否显示时间戳 showLevel: true, // 是否显示日志级别标签 timeFormat: 'HH:mm:ss.SSS', // 时间戳格式,可自定义 /* --- 日志级别 & 过滤 --- */ levels: ['debug', 'info', 'warn', 'error', 'success', 'system'], // 可用日志级别 // 过滤功能基于 levels;如只想显示 info/warn,可在这里删减 /* --- 功能开关 --- */ enableFilter: true, // 顶部是否显示级别过滤复选框 enableSearch: true, // 是否启用搜索框与高亮功能 enableExport: true, // 是否允许导出日志文件 enableClear: true, // 是否提供“清空”按钮 enablePause: true, // 是否提供“暂停/继续”按钮 enableThemeToggle: true, // 是否提供“切换主题”按钮 enableFullscreen: true, // 是否提供“全屏”按钮 enableFontSize: true, // 是否提供“字体大小 +/-”按钮 enableWordWrap: true, // 是否提供“换行/不换行”切换按钮 /* --- 暂停/继续 的回调函数 --- */ onTogglePause: () => {}, /* --- 创建完成 的回调函数 --- */ onCreated: () => {}, ...opts }; /* 强制确保 levels 是数组,防止 forEach 报错 */ if (!Array.isArray(this.cfg.levels)) { this.cfg.levels = ['info', 'warn', 'error']; } this.isPaused = false; this.isFullscreen = false; this.logs = []; this.filters = new Set(); this.searchTerm = ''; this.highlightIndex = -1; this.highlightEls = []; this.initDOM(); this.bindResize(); this.bindGlobalEvents(); //执行回调函数 if(this.cfg.onCreated && typeof this.cfg.onCreated === 'function'){ this.cfg.onCreated(); } } /* ------------------------ 初始化 ------------------------ */ initDOM() { this.container.innerHTML = ''; this.setContainerSize(); // 宽高自适应 this.applyContainerStyle(); // 顶部导航栏 this.navbar = document.createElement('div'); Object.assign(this.navbar.style, { display: 'flex', alignItems: 'center', padding: '8px 12px', backgroundColor: this.cfg.theme === 'dark' ? '#2d2d30' : '#f3f3f3', borderBottom: `1px solid ${this.cfg.theme === 'dark' ? '#555' : '#ccc'}`, gap: '10px', fontSize: '14px', flexWrap: 'wrap' }); // 按钮组 if (this.cfg.enablePause) { this.pauseBtn = this.createBtn('⏸ 暂停', () => this.togglePause(), {w: '72px'}); this.navbar.appendChild(this.pauseBtn); } if (this.cfg.enableClear) { this.navbar.appendChild(this.createBtn('🗑 清空', () => this.clear(), {w: '72px'})); } if (this.cfg.enableExport) { this.navbar.appendChild(this.createBtn('💾 导出', () => this.export(), {w: '72px'})); } // 主题切换按钮 if (this.cfg.enableThemeToggle) { this.themeBtn = this.createBtn( this.cfg.theme === 'dark' ? '☀️ 明亮' : '🌙 暗黑', () => this.toggleTheme(), {w: '80px'} ); this.navbar.appendChild(this.themeBtn); } // 字体大小 if (this.cfg.enableFontSize) { this.navbar.appendChild(this.createBtn('A-', () => this.changeFontSize(-1), {w: '36px'})); this.navbar.appendChild(this.createBtn('A+', () => this.changeFontSize(1), {w: '36px'})); } // 换行 if (this.cfg.enableWordWrap) { this.wrapBtn = this.createBtn( this.cfg.wordWrap ? '⏎ 换行' : '⏎ 不换行', () => this.toggleWordWrap(), {w: '90px'} ); this.navbar.appendChild(this.wrapBtn); } // 全屏 if (this.cfg.enableFullscreen) { this.navbar.appendChild(this.createBtn('⛶ 全屏', () => this.toggleFullscreen(), {w: '72px'})); } // 过滤复选框 if (this.cfg.enableFilter) { this.cfg.levels.forEach(lvl => { this.navbar.appendChild(this.createCheckbox(lvl)); }); } // 搜索框 if (this.cfg.enableSearch) { const searchWrap = document.createElement('span'); searchWrap.style.marginLeft = 'auto'; searchWrap.style.display = 'flex'; searchWrap.style.alignItems = 'center'; searchWrap.style.gap = '4px'; this.searchInput = document.createElement('input'); Object.assign(this.searchInput.style, { padding: '6px 8px', fontSize: '14px', border: `1px solid ${this.cfg.theme === 'dark' ? '#555' : '#bbb'}`, borderRadius: '4px', backgroundColor: this.cfg.theme === 'dark' ? '#3c3c3c' : '#fff', color: 'inherit', width: '260px' }); this.searchInput.placeholder = '搜索…'; this.searchInput.oninput = () => { this.searchTerm = this.searchInput.value.trim(); this.renderVisible(); }; searchWrap.appendChild(this.searchInput); ['↑', '↓'].forEach((arr, idx) => { searchWrap.appendChild( this.createBtn(arr, () => this.navigateHighlight(idx), {w: '30px'}) ); }); this.navbar.appendChild(searchWrap); } // 日志视窗 this.viewport = document.createElement('div'); this.applyViewportStyle(); this.container.appendChild(this.navbar); this.container.appendChild(this.viewport); this.renderVisible(); } /* ------------------------ 样式 ------------------------ */ setContainerSize() { // 如果用户未指定宽高,则自适应 if (this.cfg.width === null) { this.container.style.width = '100%'; } else { this.container.style.width = typeof this.cfg.width === 'number' ? `${this.cfg.width}px` : this.cfg.width; } if (this.cfg.height === null) { //this.container.style.height = '100vh'; const bodyMargin = parseFloat(getComputedStyle(document.body).marginTop || 0) + parseFloat(getComputedStyle(document.body).marginBottom || 0); this.container.style.height = `calc(100vh - ${bodyMargin}px - 2px)`; } else { this.container.style.height = typeof this.cfg.height === 'number' ? `${this.cfg.height}px` : this.cfg.height; } } applyContainerStyle() { Object.assign(this.container.style, { display: 'flex', flexDirection: 'column', border: `1px solid ${this.cfg.theme === 'dark' ? '#444' : '#ccc'}`, borderRadius: '6px', fontFamily: 'Consolas, "Courier New", monospace', fontSize: `${this.cfg.fontSize}px`, lineHeight: 1.45, backgroundColor: this.cfg.theme === 'dark' ? '#1e1e1e' : '#fafafa', color: this.cfg.theme === 'dark' ? '#d4d4d4' : '#222', overflow: 'hidden' }); } applyViewportStyle() { Object.assign(this.viewport.style, { flex: 1, overflowY: 'auto', padding: '8px', whiteSpace: this.cfg.wordWrap ? 'pre-wrap' : 'pre', wordBreak: 'break-word' }); } /* ------------------------ 工具 ------------------------ */ createBtn(text, handler, {w} = {}) { const btn = document.createElement('button'); btn.textContent = text; Object.assign(btn.style, { padding: '6px 10px', fontSize: '14px', backgroundColor: this.cfg.theme === 'dark' ? '#444' : '#e7e7e7', color: 'inherit', border: `1px solid ${this.cfg.theme === 'dark' ? '#666' : '#ccc'}`, borderRadius: '4px', cursor: 'pointer', minWidth: w || 'auto' }); btn.onmouseenter = () => btn.style.backgroundColor = this.cfg.theme === 'dark' ? '#555' : '#d0d0d0'; btn.onmouseleave = () => btn.style.backgroundColor = this.cfg.theme === 'dark' ? '#444' : '#e7e7e7'; btn.onclick = handler; return btn; } createCheckbox(level) { const lbl = document.createElement('label'); lbl.style.color = this.cfg.theme === 'dark' ? '#ccc' : '#333'; lbl.style.fontSize = '13px'; lbl.style.display = 'flex'; lbl.style.alignItems = 'center'; const chk = document.createElement('input'); chk.type = 'checkbox'; chk.checked = true; chk.style.margin = '0 4px'; chk.onchange = () => { if (chk.checked) this.filters.delete(level); else this.filters.add(level); this.renderVisible(); }; lbl.appendChild(chk); lbl.append(level.toUpperCase()); return lbl; } /* ------------------------ 日志渲染 ------------------------ */ log(message, level = 'info', ts = this.formatTime(new Date())) { if (!this.cfg.levels.includes(level)) level = 'info'; if (this.isPaused) return; ts = this.cfg.showTimestamp ? ts : ''; this.logs.push({message, level, ts, id: Date.now() + Math.random()}); if (this.logs.length > this.cfg.maxLines) this.logs.shift(); this.renderVisible(); } renderVisible() { this.viewport.innerHTML = ''; this.highlightEls = []; this.highlightIndex = -1; const filtered = this.logs.filter(l => !this.filters.has(l.level)); const frag = document.createDocumentFragment(); const regex = this.searchTerm ? new RegExp(`(${this.searchTerm})`, 'gi') : null; filtered.forEach(entry => { const line = document.createElement('div'); line.style.display = 'flex'; line.style.margin = '1px 0'; if (this.cfg.showTimestamp && entry.ts) { const tsSpan = document.createElement('span'); tsSpan.textContent = entry.ts + ' '; tsSpan.style.color = this.cfg.theme === 'dark' ? '#6a9955' : '#008000'; tsSpan.style.minWidth = '90px'; line.appendChild(tsSpan); } if (this.cfg.showLevel) { const lvlSpan = document.createElement('span'); lvlSpan.textContent = `[${entry.level.toUpperCase()}]`; const colors = { debug: '#9c27b0', info: '#2196f3', warn: '#ff9800', error: '#f44336', success: '#4caf50', system: '#00bcd4' }; lvlSpan.style.color = colors[entry.level] || colors.info; lvlSpan.style.minWidth = '70px'; lvlSpan.style.fontWeight = 'bold'; line.appendChild(lvlSpan); } const msgSpan = document.createElement('span'); msgSpan.style.flex = 1; let msg = entry.message; if (regex) { if (regex.test(entry.message)) { line.style.backgroundColor = 'rgba(255,235,59,0.25)'; this.highlightEls.push(line); } msg = entry.message.replace(regex, '$1'); } msgSpan.innerHTML = msg; line.appendChild(msgSpan); frag.appendChild(line); }); this.viewport.appendChild(frag); if (this.cfg.autoScroll && !this.isPaused) { this.viewport.scrollTop = this.viewport.scrollHeight; } } /* ------------------------ 交互 ------------------------ */ togglePause() { this.isPaused = !this.isPaused; this.pauseBtn.textContent = this.isPaused ? '▶ 继续' : '⏸ 暂停'; this.pauseBtn.style.backgroundColor = this.isPaused ? (this.cfg.theme === 'dark' ? '#d32f2f' : '#ff5252') : (this.cfg.theme === 'dark' ? '#444' : '#e7e7e7'); //执行回调函数 if(this.cfg.onTogglePause && typeof this.cfg.onTogglePause === 'function'){ this.cfg.onTogglePause(this.isPaused); } } toggleTheme() { this.cfg.theme = this.cfg.theme === 'dark' ? 'light' : 'dark'; this.themeBtn.textContent = this.cfg.theme === 'dark' ? '☀️ 明亮' : '🌙 暗黑'; this.applyContainerStyle(); this.navbar.style.backgroundColor = this.cfg.theme === 'dark' ? '#2d2d30' : '#f3f3f3'; this.renderVisible(); } changeFontSize(delta) { this.cfg.fontSize = Math.max(10, Math.min(24, this.cfg.fontSize + delta)); this.container.style.fontSize = `${this.cfg.fontSize}px`; } toggleWordWrap() { this.cfg.wordWrap = !this.cfg.wordWrap; this.wrapBtn.textContent = this.cfg.wordWrap ? '⏎ 不换行' : '⏎ 换行'; this.applyViewportStyle(); } toggleFullscreen() { if (!this.isFullscreen) { if (this.container.requestFullscreen) this.container.requestFullscreen(); else if (this.container.webkitRequestFullscreen) this.container.webkitRequestFullscreen(); else if (this.container.msRequestFullscreen) this.container.msRequestFullscreen(); } else { if (document.exitFullscreen) document.exitFullscreen(); else if (document.webkitExitFullscreen) document.webkitExitFullscreen(); else if (document.msExitFullscreen) document.msExitFullscreen(); } this.isFullscreen = !this.isFullscreen; } navigateHighlight(dir) { if (!this.highlightEls.length) return; this.highlightIndex = (this.highlightIndex + (dir === 0 ? -1 : 1) + this.highlightEls.length) % this.highlightEls.length; this.highlightEls.forEach((el, idx) => { el.style.outline = idx === this.highlightIndex ? '2px solid #ffeb3b' : 'none'; }); this.highlightEls[this.highlightIndex].scrollIntoView({block: 'nearest', behavior: 'smooth'}); } clear() { this.logs = []; this.renderVisible(); } export() { const lines = this.logs.map(l => `${l.ts} [${l.level.toUpperCase()}] ${l.message}`); const blob = new Blob([lines.join('\n')], {type: 'text/plain;charset=utf-8'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `log_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.txt`; a.click(); URL.revokeObjectURL(url); } formatTime(date) { const t = date.toTimeString().slice(0, 8); const ms = String(date.getMilliseconds()).padStart(3, '0'); return `${t}.${ms}`; } bindResize() { window.addEventListener('resize', () => { this.setContainerSize(); // 响应窗口变化 }); } bindGlobalEvents() { document.addEventListener('keydown', e => { if (e.key === 'Escape' && this.searchInput) { this.searchInput.value = ''; this.searchTerm = ''; this.renderVisible(); } if (e.ctrlKey && e.key === 'k') { e.preventDefault(); this.clear(); } }); } /* 快捷方法 */ info(msg, ts = this.formatTime(new Date())) { this.log(msg, 'info', ts); } warn(msg, ts = this.formatTime(new Date())) { this.log(msg, 'warn', ts); } error(msg, ts = this.formatTime(new Date())) { this.log(msg, 'error', ts); } success(msg, ts = this.formatTime(new Date())) { this.log(msg, 'success', ts); } debug(msg, ts = this.formatTime(new Date())) { this.log(msg, 'debug', ts); } system(msg, ts = this.formatTime(new Date())) { this.log(msg, 'system', ts); } }