为 PluginCard 组件添加实时日志弹窗功能

This commit is contained in:
jxxghp
2025-05-26 12:32:22 +08:00
parent 6353d56beb
commit d81120ab8f
7 changed files with 263 additions and 177 deletions

View File

@@ -1,84 +1,99 @@
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
import { useI18n } from "vue-i18n";
// 定义输入变量
const props = defineProps<{
logfile: string;
}>();
// 国际化
const { t } = useI18n()
const { t } = useI18n();
// 已解析的日志列表
const parsedLogs = ref<{ level: string; time: string; program: string; content: string }[]>([])
const parsedLogs = ref<
{ level: string; time: string; program: string; content: string }[]
>([]);
// 表头
const headers = [
{ title: t('logging.level'), value: 'level' },
{ title: t('logging.time'), value: 'time' },
{ title: t('logging.program'), value: 'program' },
{ title: t('logging.content'), value: 'content' },
]
{ title: t("logging.level"), value: "level" },
{ title: t("logging.time"), value: "time" },
{ title: t("logging.program"), value: "program" },
{ title: t("logging.content"), value: "content" },
];
// SSE消息对象
let eventSource: EventSource | null = null
let eventSource: EventSource | null = null;
// 日志颜色映射表
const logColorMap: Record<string, string> = {
DEBUG: 'secondary',
INFO: 'info',
WARNING: 'warning',
ERROR: 'error',
}
DEBUG: "secondary",
INFO: "info",
WARNING: "warning",
ERROR: "error",
};
// 获取日志颜色
function getLogColor(level: string): string {
return logColorMap[level] || 'secondary'
return logColorMap[level] || "secondary";
}
// SSE持续获取日志
function startSSELogging() {
eventSource = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/logging`)
const buffer: string[] = []
let timeoutId: number | null = null
eventSource = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/logging?logfile=${
encodeURIComponent(props.logfile) ?? "moviepilot.log"
}`
);
const buffer: string[] = [];
let timeoutId: number | null = null;
eventSource.addEventListener('message', event => {
const message = event.data
eventSource.addEventListener("message", (event) => {
const message = event.data;
if (message) {
buffer.push(message)
buffer.push(message);
if (!timeoutId) {
timeoutId = window.setTimeout(() => {
// 解析新日志
const newParsedLogs = buffer
.map(log => {
const logPattern = /^【(.*?)】[0-9\-:]*\s(.*?)\s-\s(.*?)\s-\s(.*)$/
const matches = log.match(logPattern)
.map((log) => {
const logPattern = /^【(.*?)】[0-9\-:]*\s(.*?)\s-\s(.*?)\s-\s(.*)$/;
const matches = log.match(logPattern);
if (matches) {
const [, level, time, program, content] = matches
return { level, time, program, content }
const [, level, time, program, content] = matches;
return { level, time, program, content };
}
return null
return null;
})
.filter(Boolean)
.filter(Boolean);
// 倒序后插入parsedLogs顶部
parsedLogs.value.unshift(...(newParsedLogs.reverse() as any[]))
parsedLogs.value.unshift(...(newParsedLogs.reverse() as any[]));
// 保留最新的200条日志
parsedLogs.value = parsedLogs.value.slice(0, 200)
parsedLogs.value = parsedLogs.value.slice(0, 200);
// 重置buffer
buffer.length = 0
timeoutId = null
}, 100)
buffer.length = 0;
timeoutId = null;
}, 100);
}
}
})
});
}
onMounted(() => {
startSSELogging()
})
startSSELogging();
});
onBeforeUnmount(() => {
if (eventSource) eventSource.close()
})
if (eventSource) eventSource.close();
});
</script>
<template>
<LoadingBanner v-if="parsedLogs.length === 0" class="mt-12" :text="t('logging.refreshing') + ' ...'" />
<LoadingBanner
v-if="parsedLogs.length === 0"
class="mt-12"
:text="t('logging.refreshing') + ' ...'"
/>
<div v-else>
<VTable class="table-rounded" hide-default-footer disable-sort>
<tbody>
@@ -91,7 +106,12 @@ onBeforeUnmount(() => {
hide-default-header
>
<template #item.level="{ item }">
<VChip size="small" :color="getLogColor(item.level)" variant="elevated" v-text="item.level" />
<VChip
size="small"
:color="getLogColor(item.level)"
variant="elevated"
v-text="item.level"
/>
</template>
<template #item.time="{ item }">
<span class="text-sm">{{ item.time }}</span>