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( /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/, '$1' ); 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*\|/, '| ' + m[1] + ' |' ); } 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: `