优化日志显示
This commit is contained in:
parent
ab611018d9
commit
fcbf4e2283
@ -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",
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user