chore: sync current WechatHookBot workspace
This commit is contained in:
113
utils/webui_static/components/LogViewer.js
Normal file
113
utils/webui_static/components/LogViewer.js
Normal file
@@ -0,0 +1,113 @@
|
||||
window.LogViewer = {
|
||||
setup() {
|
||||
const { ref, computed, onMounted, onUnmounted, nextTick, watch } = Vue;
|
||||
const { connected, logs, paused, connect, clear, togglePause, destroy } = useWebSocket();
|
||||
|
||||
const filterText = ref('');
|
||||
const containerRef = ref(null);
|
||||
|
||||
function onWheel(event) {
|
||||
const el = containerRef.value;
|
||||
if (!el) return;
|
||||
|
||||
const atTop = el.scrollTop <= 0;
|
||||
const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 1;
|
||||
const scrollingUp = event.deltaY < 0;
|
||||
const scrollingDown = event.deltaY > 0;
|
||||
|
||||
// 防止日志容器触底/触顶时把滚轮事件传递给外层页面
|
||||
if ((scrollingUp && atTop) || (scrollingDown && atBottom)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
const filteredLogs = computed(() => {
|
||||
const kw = filterText.value.toLowerCase();
|
||||
if (!kw) return logs.value;
|
||||
return logs.value.filter(line => line.toLowerCase().includes(kw));
|
||||
});
|
||||
|
||||
watch(filteredLogs, () => {
|
||||
if (!paused.value) {
|
||||
nextTick(() => {
|
||||
if (containerRef.value) {
|
||||
containerRef.value.scrollTop = containerRef.value.scrollHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const levelColors = {
|
||||
DEBUG: 'var(--el-text-color-placeholder)',
|
||||
INFO: 'var(--el-color-primary)',
|
||||
SUCCESS: 'var(--el-color-success)',
|
||||
WARNING: 'var(--el-color-warning)',
|
||||
ERROR: 'var(--el-color-danger)',
|
||||
CRITICAL: '#b42318',
|
||||
};
|
||||
|
||||
function colorize(raw) {
|
||||
let s = raw
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(
|
||||
/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/,
|
||||
'<span style="color:var(--el-text-color-placeholder)">$1</span>'
|
||||
);
|
||||
const m = raw.match(/\|\s*(DEBUG|INFO|SUCCESS|WARNING|ERROR|CRITICAL)\s*\|/);
|
||||
if (m) {
|
||||
s = s.replace(
|
||||
/\|\s*(DEBUG|INFO|SUCCESS|WARNING|ERROR|CRITICAL)\s*\|/,
|
||||
'| <span style="color:' + levelColors[m[1]] + '">' + m[1] + '</span> |'
|
||||
);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
connect();
|
||||
if (containerRef.value) {
|
||||
containerRef.value.addEventListener('wheel', onWheel, { passive: false });
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (containerRef.value) {
|
||||
containerRef.value.removeEventListener('wheel', onWheel);
|
||||
}
|
||||
destroy();
|
||||
});
|
||||
|
||||
return {
|
||||
filterText, paused, connected, filteredLogs,
|
||||
containerRef, togglePause, clear, colorize
|
||||
};
|
||||
},
|
||||
template: `
|
||||
<div class="panel-page">
|
||||
<div class="log-toolbar">
|
||||
<el-input v-model="filterText" placeholder="搜索过滤..." clearable
|
||||
class="log-search" size="small">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-button size="small" :type="paused ? 'primary' : ''" @click="togglePause">
|
||||
{{ paused ? '恢复' : '暂停' }}
|
||||
</el-button>
|
||||
<el-button size="small" @click="clear">清空</el-button>
|
||||
<span class="log-status" :class="connected ? 'is-online' : 'is-offline'">
|
||||
<span class="status-dot"></span>
|
||||
{{ connected ? '已连接' : '已断开' }}
|
||||
</span>
|
||||
</div>
|
||||
<div ref="containerRef"
|
||||
class="log-stream">
|
||||
<div v-for="(line, i) in filteredLogs" :key="i"
|
||||
class="log-line"
|
||||
v-html="colorize(line)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
Reference in New Issue
Block a user