优化日志显示

This commit is contained in:
Wenjie Zhang 2025-03-11 00:03:29 +08:00
parent ab611018d9
commit fcbf4e2283
3 changed files with 306 additions and 54 deletions

View File

@ -17,6 +17,7 @@
"ant-design-vue": "^4.2.3", "ant-design-vue": "^4.2.3",
"axios": "^1.3.4", "axios": "^1.3.4",
"d3": "^7.8.3", "d3": "^7.8.3",
"dayjs": "^1.11.13",
"echarts": "^5.4.2", "echarts": "^5.4.2",
"echarts-gl": "^2.0.9", "echarts-gl": "^2.0.9",
"highlight.js": "^11.10.0", "highlight.js": "^11.10.0",

View File

@ -987,7 +987,7 @@ watch(
background-color: #666; background-color: #666;
border-radius: 50%; border-radius: 50%;
opacity: 0.3; opacity: 0.3;
animation: pulse 1.4s infinite ease-in-out both; animation: pulse 0.5s infinite ease-in-out both;
} }
.loading-dots div:nth-child(1) { .loading-dots div:nth-child(1) {

View File

@ -1,52 +1,169 @@
<template> <template>
<div class="log-viewer"> <div :class="['log-viewer', { fullscreen: state.isFullscreen }]" ref="logViewer">
<div class="button-group"> <div class="control-panel">
<a-button @click="fetchLogs" :loading="state.fetching">刷新</a-button> <div class="button-group">
<a-button @click="printConfig">打印配置</a-button> <a-button @click="fetchLogs" :loading="state.fetching">
<a-switch v-model:checked="state.autoRefresh" @change="toggleAutoRefresh"> <template #icon><ReloadOutlined /></template>
自动刷新 刷新
</a-switch> </a-button>
<a-button @click="clearLogs">
<template #icon><ClearOutlined /></template>
清空
</a-button>
<a-button @click="printConfig">
<template #icon><SettingOutlined /></template>
打印配置
</a-button>
<a-button @click="toggleFullscreen">
<template #icon>
<FullscreenOutlined v-if="!state.isFullscreen" />
<FullscreenExitOutlined v-else />
</template>
{{ state.isFullscreen ? '退出全屏' : '全屏' }}
</a-button>
<a-tooltip :title="state.autoRefresh ? '点击停止自动刷新' : '点击开启自动刷新'">
<a-button
:type="state.autoRefresh ? 'primary' : 'default'"
@click="toggleAutoRefresh(!state.autoRefresh)"
>
<template #icon>
<SyncOutlined :spin="state.autoRefresh" />
</template>
自动刷新
<span v-if="state.autoRefresh" class="refresh-interval">(5s)</span>
</a-button>
</a-tooltip>
</div>
<div class="filter-group">
<a-input-search
v-model:value="state.searchText"
placeholder="搜索日志..."
style="width: 200px"
@search="onSearch"
/>
<a-select
v-model:value="state.selectedLevels"
mode="multiple"
placeholder="选择日志级别"
:options="logLevels"
@change="filterLogs"
/>
</div>
</div> </div>
<div ref="logContainer" class="log-container"> <div ref="logContainer" class="log-container">
<pre v-if="logs">{{ logs }}</pre> <div v-if="processedLogs.length" class="log-lines">
<div
v-for="(log, index) in processedLogs"
:key="index"
:class="['log-line', `level-${log.level.toLowerCase()}`]"
>
<span class="timestamp">{{ formatTimestamp(log.timestamp) }}</span>
<span class="level">{{ log.level }}</span>
<span class="module">{{ log.module }}</span>
<span class="message">{{ log.message }}</span>
</div>
</div>
<div v-else class="empty-logs">暂无日志</div>
</div> </div>
<p v-if="error" class="error">{{ error }}</p> <p v-if="error" class="error">{{ error }}</p>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onActivated, onUnmounted, nextTick, reactive } from 'vue'; import { ref, onMounted, onActivated, onUnmounted, nextTick, reactive, computed } from 'vue';
import { useConfigStore } from '@/stores/config'; import { useConfigStore } from '@/stores/config';
import { useThrottleFn } from '@vueuse/core'; import { useThrottleFn } from '@vueuse/core';
import {
FullscreenOutlined,
FullscreenExitOutlined,
ReloadOutlined,
ClearOutlined,
SettingOutlined,
SyncOutlined
} from '@ant-design/icons-vue';
import dayjs from 'dayjs';
const configStore = useConfigStore() const configStore = useConfigStore()
const config = configStore.config; const config = configStore.config;
// ref //
const logs = ref(''); const logLevels = [
const error = ref(''); { value: 'INFO', label: 'INFO' },
{ value: 'ERROR', label: 'ERROR' },
{ value: 'DEBUG', label: 'DEBUG' },
{ value: 'WARNING', label: 'WARNING' }
];
const logViewer = ref(null);
//
const state = reactive({ const state = reactive({
fetching: false, fetching: false,
autoRefresh: false, autoRefresh: false,
}) searchText: '',
selectedLevels: logLevels.map(l => l.value),
rawLogs: [],
isFullscreen: false,
});
// ref DOM const logs = ref('');
const error = ref('');
const logContainer = ref(null); const logContainer = ref(null);
let autoRefreshInterval = null;
// //
const parseLogLine = (line) => {
const match = line.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}) - (\w+) - ([^-]+) - (.+)$/);
if (match) {
return {
timestamp: match[1],
level: match[2],
module: match[3].trim(),
message: match[4].trim(),
raw: line
};
}
return null;
};
//
const formatTimestamp = (timestamp) => {
try {
// "2025-03-10 08:26:37,269" "2025-03-10 08:26:37.269"
const normalizedTimestamp = timestamp.replace(',', '.');
const date = dayjs(normalizedTimestamp);
return date.isValid() ? date.format('HH:mm:ss.SSS') : timestamp;
} catch (err) {
console.error('时间戳格式化错误:', err);
return timestamp;
}
};
//
const processedLogs = computed(() => {
return state.rawLogs
.map(parseLogLine)
.filter(log => log !== null)
.filter(log => state.selectedLevels.includes(log.level))
.filter(log => {
if (!state.searchText) return true;
return log.raw.toLowerCase().includes(state.searchText.toLowerCase());
});
});
//
const fetchLogs = async () => { const fetchLogs = async () => {
state.fetching = true; state.fetching = true;
try { try {
error.value = ''; error.value = '';
const response = await fetch('/api/log'); const response = await fetch('/api/log');
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to fetch logs'); throw new Error('获取日志失败');
} }
const data = await response.json(); const data = await response.json();
logs.value = data.log; state.rawLogs = data.log.split('\n').filter(line => line.trim());
// 使
await nextTick(); await nextTick();
const scrollToBottom = useThrottleFn(() => { const scrollToBottom = useThrottleFn(() => {
if (logContainer.value) { if (logContainer.value) {
@ -55,19 +172,82 @@ const fetchLogs = async () => {
}, 100); }, 100);
scrollToBottom(); scrollToBottom();
} catch (err) { } catch (err) {
// error.value = `错误: ${err.message}`;
error.value = `Error: ${err.message}`;
} finally { } finally {
state.fetching = false; state.fetching = false;
} }
}; };
// //
const clearLogs = () => {
state.rawLogs = [];
};
//
const onSearch = () => {
// computed
};
//
const filterLogs = () => {
// computed
};
//
const toggleAutoRefresh = (value) => {
if (value) {
autoRefreshInterval = setInterval(fetchLogs, 5000);
state.autoRefresh = true;
} else {
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
autoRefreshInterval = null;
}
state.autoRefresh = false;
}
};
//
const toggleFullscreen = async () => {
try {
if (!state.isFullscreen) {
if (logViewer.value.requestFullscreen) {
await logViewer.value.requestFullscreen();
} else if (logViewer.value.webkitRequestFullscreen) {
await logViewer.value.webkitRequestFullscreen();
} else if (logViewer.value.msRequestFullscreen) {
await logViewer.value.msRequestFullscreen();
}
} else {
if (document.exitFullscreen) {
await document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
await document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
await document.msExitFullscreen();
}
}
} catch (err) {
console.error('全屏切换失败:', err);
}
};
//
const handleFullscreenChange = () => {
state.isFullscreen = Boolean(
document.fullscreenElement ||
document.webkitFullscreenElement ||
document.msFullscreenElement
);
};
onMounted(() => { onMounted(() => {
fetchLogs(); fetchLogs();
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
document.addEventListener('msfullscreenchange', handleFullscreenChange);
}); });
// keep-alive
onActivated(() => { onActivated(() => {
if (state.autoRefresh) { if (state.autoRefresh) {
toggleAutoRefresh(true); toggleAutoRefresh(true);
@ -76,37 +256,51 @@ onActivated(() => {
} }
}); });
//
const toggleAutoRefresh = (value) => {
if (value) {
let autoRefreshInterval = setInterval(fetchLogs, 5000);
state.autoRefresh = true;
} else {
clearInterval(autoRefreshInterval);
state.autoRefresh = false;
}
};
onUnmounted(() => { onUnmounted(() => {
clearInterval(autoRefreshInterval); if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
autoRefreshInterval = null;
}
document.removeEventListener('fullscreenchange', handleFullscreenChange);
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange);
document.removeEventListener('msfullscreenchange', handleFullscreenChange);
}); });
//
const printConfig = () => { const printConfig = () => {
console.log('Current config:', config); console.log('Current config:', config);
}; };
</script> </script>
<style scoped> <style scoped>
.log-viewer { .log-viewer {
background: white; background: white;
height: 100%;
}
.log-viewer.fullscreen {
padding: 16px;
}
.control-panel {
margin-bottom: 16px;
} }
.button-group { .button-group {
display: flex; display: flex;
gap: 10px; gap: 10px;
margin-bottom: 10px; margin-bottom: 10px;
.refresh-interval {
font-size: 12px;
opacity: 0.8;
margin-left: 2px;
}
}
.filter-group {
display: flex;
gap: 10px;
margin-bottom: 10px;
} }
.error { .error {
@ -114,32 +308,89 @@ const printConfig = () => {
} }
.log-container { .log-container {
height: calc(100vh - 150px); /* 设置最大高度 */ height: calc(100vh - 200px);
overflow-y: auto; /* 启用垂直滚动 */ overflow-y: auto;
background-color: #f6f7f7;
padding-bottom: 0;
border-radius: 5px;
margin-top: 10px;
white-space: pre-wrap; /* 使日志内容自动换行 */
word-wrap: break-word;
font-size: small;
background: #0C0C0C; background: #0C0C0C;
color: #D1D1D1; color: #D1D1D1;
border-radius: 5px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
}
pre { .log-lines {
margin: 0; padding: 8px;
padding: 10px; }
height: 100%;
}
transition: background-color 0.3s ease; .log-line {
padding: 2px 4px;
display: flex;
gap: 8px;
line-height: 1.4;
}
.log-line:hover {
background: rgba(255, 255, 255, 0.05);
}
.timestamp {
color: #6A9955;
min-width: 80px;
}
.level {
min-width: 40px;
font-weight: bold;
}
.module {
color: #569CD6;
min-width: 30px;
}
.message {
flex: 1;
white-space: pre-wrap;
word-break: break-all;
}
.level-info {
.level { color: #4EC9B0; }
}
.level-error {
.level { color: #F14C4C; }
}
.level-debug {
.level { color: #9CDCFE; }
}
.level-warning {
.level { color: #DCD900; }
}
.empty-logs {
padding: 16px;
text-align: center;
color: #666;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.log-container { .log-container {
background: #1E1E1E; background: #1E1E1E;
color: #D4D4D4;
} }
} }
:fullscreen .log-container {
height: calc(100vh - 120px);
}
:-webkit-full-screen .log-container {
height: calc(100vh - 120px);
}
:-ms-fullscreen .log-container {
height: calc(100vh - 120px);
}
</style> </style>