mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-07 00:36:41 +08:00
PluginCard 组件中的实时日志弹窗代码
This commit is contained in:
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user