代码提交
This commit is contained in:
444
src/main/resources/static/common/js/LogMonitorAdaptive.js
Normal file
444
src/main/resources/static/common/js/LogMonitorAdaptive.js
Normal file
@@ -0,0 +1,444 @@
|
||||
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, // 是否提供“换行/不换行”切换按钮
|
||||
|
||||
...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();
|
||||
}
|
||||
|
||||
/* ------------------------ 初始化 ------------------------ */
|
||||
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') {
|
||||
if (!this.cfg.levels.includes(level)) level = 'info';
|
||||
if (this.isPaused) return;
|
||||
|
||||
const ts = this.cfg.showTimestamp ? this.formatTime(new Date()) : '';
|
||||
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, '<mark style="background:#ffeb3b;color:#000;">$1</mark>');
|
||||
}
|
||||
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');
|
||||
}
|
||||
|
||||
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) {
|
||||
this.log(msg, 'info');
|
||||
}
|
||||
|
||||
warn(msg) {
|
||||
this.log(msg, 'warn');
|
||||
}
|
||||
|
||||
error(msg) {
|
||||
this.log(msg, 'error');
|
||||
}
|
||||
|
||||
success(msg) {
|
||||
this.log(msg, 'success');
|
||||
}
|
||||
|
||||
debug(msg) {
|
||||
this.log(msg, 'debug');
|
||||
}
|
||||
|
||||
system(msg) {
|
||||
this.log(msg, 'system');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user