Files
fmt/src/main/resources/static/common/js/LogMonitorAdaptive.js
紫影233 e17e31edfd v2.1.0 更新:
1、引入metona-mq-mini-pro消息队列,重构实时日志获取方式
2025-07-23 23:26:56 +08:00

459 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.toLowerCase()] || 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');
//执行回调函数
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);
}
}