PluginCard 组件中的实时日志弹窗代码

This commit is contained in:
jxxghp
2025-05-26 13:26:13 +08:00
parent 139eaa7016
commit 8552203d43
2 changed files with 46 additions and 84 deletions
+1 -29
View File
@@ -10,6 +10,7 @@ import VersionHistory from '@/components/misc/VersionHistory.vue'
import ProgressDialog from '../dialog/ProgressDialog.vue' import ProgressDialog from '../dialog/ProgressDialog.vue'
import PluginConfigDialog from '../dialog/PluginConfigDialog.vue' import PluginConfigDialog from '../dialog/PluginConfigDialog.vue'
import PluginDataDialog from '../dialog/PluginDataDialog.vue' import PluginDataDialog from '../dialog/PluginDataDialog.vue'
import LoggingView from '@/views/system/LoggingView.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
@@ -702,35 +703,6 @@ watch(
</VCardActions> </VCardActions>
</VCard> </VCard>
</VDialog> </VDialog>
<!-- 实时日志弹窗 -->
<VDialog
v-if="loggingDialog"
v-model="loggingDialog"
scrollable
max-width="60rem"
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VDialogCloseBtn @click="loggingDialog = false" />
<VCardItem>
<VCardTitle class="d-inline-flex">
<VIcon icon="mdi-file-document" class="me-2" />
{{ t('plugin.logTitle') }}
<a class="mx-2 d-inline-flex align-center cursor-pointer" @click="openLoggerWindow">
<VChip color="grey-darken-1" size="small" class="ml-2">
<VIcon icon="mdi-open-in-new" size="small" start />
{{ t('common.openInNewWindow') }}
</VChip>
</a>
</VCardTitle>
</VCardItem>
<VDivider />
<VCardText>
<LoggingView :logfile="`plugins/${props.plugin?.id?.toLowerCase()}.log`" />
</VCardText>
</VCard>
</VDialog>
</div> </div>
</template> </template>
+45 -55
View File
@@ -1,99 +1,94 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useI18n } from "vue-i18n"; import { useI18n } from 'vue-i18n'
// 定义输入变量 // 定义输入变量
const props = defineProps<{ const props = defineProps<{
logfile: string; logfile: string
}>(); }>()
// 国际化 // 国际化
const { t } = useI18n(); const { t } = useI18n()
// 已解析的日志列表 // 已解析的日志列表
const parsedLogs = ref< const parsedLogs = ref<{ level: string; time: string; program: string; content: string }[]>([])
{ level: string; time: string; program: string; content: string }[]
>([]);
// 表头 // 表头
const headers = [ const headers = [
{ title: t("logging.level"), value: "level" }, { title: t('logging.level'), value: 'level' },
{ title: t("logging.time"), value: "time" }, { title: t('logging.time'), value: 'time' },
{ title: t("logging.program"), value: "program" }, { title: t('logging.program'), value: 'program' },
{ title: t("logging.content"), value: "content" }, { title: t('logging.content'), value: 'content' },
]; ]
// SSE消息对象 // SSE消息对象
let eventSource: EventSource | null = null; let eventSource: EventSource | null = null
// 日志颜色映射表 // 日志颜色映射表
const logColorMap: Record<string, string> = { const logColorMap: Record<string, string> = {
DEBUG: "secondary", DEBUG: 'secondary',
INFO: "info", INFO: 'info',
WARNING: "warning", WARNING: 'warning',
ERROR: "error", ERROR: 'error',
}; }
// 获取日志颜色 // 获取日志颜色
function getLogColor(level: string): string { function getLogColor(level: string): string {
return logColorMap[level] || "secondary"; return logColorMap[level] || 'secondary'
} }
// SSE持续获取日志 // SSE持续获取日志
function startSSELogging() { function startSSELogging() {
console.log(props.logfile)
eventSource = new EventSource( eventSource = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/logging?logfile=${ `${import.meta.env.VITE_API_BASE_URL}system/logging?logfile=${
encodeURIComponent(props.logfile) ?? "moviepilot.log" encodeURIComponent(props.logfile) ?? 'moviepilot.log'
}` }`,
); )
const buffer: string[] = []; const buffer: string[] = []
let timeoutId: number | null = null; let timeoutId: number | null = null
eventSource.addEventListener("message", (event) => { eventSource.addEventListener('message', event => {
const message = event.data; const message = event.data
if (message) { if (message) {
buffer.push(message); buffer.push(message)
if (!timeoutId) { if (!timeoutId) {
timeoutId = window.setTimeout(() => { timeoutId = window.setTimeout(() => {
// 解析新日志 // 解析新日志
const newParsedLogs = buffer const newParsedLogs = buffer
.map((log) => { .map(log => {
const logPattern = /^【(.*?)】[0-9\-:]*\s(.*?)\s-\s(.*?)\s-\s(.*)$/; const logPattern = /^【(.*?)】[0-9\-:]*\s(.*?)\s-\s(.*?)\s-\s(.*)$/
const matches = log.match(logPattern); const matches = log.match(logPattern)
if (matches) { if (matches) {
const [, level, time, program, content] = matches; const [, level, time, program, content] = matches
return { level, time, program, content }; return { level, time, program, content }
} }
return null; return null
}) })
.filter(Boolean); .filter(Boolean)
// 倒序后插入parsedLogs顶部 // 倒序后插入parsedLogs顶部
parsedLogs.value.unshift(...(newParsedLogs.reverse() as any[])); parsedLogs.value.unshift(...(newParsedLogs.reverse() as any[]))
// 保留最新的200条日志 // 保留最新的200条日志
parsedLogs.value = parsedLogs.value.slice(0, 200); parsedLogs.value = parsedLogs.value.slice(0, 200)
// 重置buffer // 重置buffer
buffer.length = 0; buffer.length = 0
timeoutId = null; timeoutId = null
}, 100); }, 100)
} }
} }
}); })
} }
onMounted(() => { onMounted(() => {
startSSELogging(); startSSELogging()
}); })
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (eventSource) eventSource.close(); if (eventSource) eventSource.close()
}); })
</script> </script>
<template> <template>
<LoadingBanner <LoadingBanner v-if="parsedLogs.length === 0" class="mt-12" :text="t('logging.refreshing') + ' ...'" />
v-if="parsedLogs.length === 0"
class="mt-12"
:text="t('logging.refreshing') + ' ...'"
/>
<div v-else> <div v-else>
<VTable class="table-rounded" hide-default-footer disable-sort> <VTable class="table-rounded" hide-default-footer disable-sort>
<tbody> <tbody>
@@ -106,12 +101,7 @@ onBeforeUnmount(() => {
hide-default-header hide-default-header
> >
<template #item.level="{ item }"> <template #item.level="{ item }">
<VChip <VChip size="small" :color="getLogColor(item.level)" variant="elevated" v-text="item.level" />
size="small"
:color="getLogColor(item.level)"
variant="elevated"
v-text="item.level"
/>
</template> </template>
<template #item.time="{ item }"> <template #item.time="{ item }">
<span class="text-sm">{{ item.time }}</span> <span class="text-sm">{{ item.time }}</span>