feat: 优化SSE连接延迟,添加初始化状态提示

This commit is contained in:
jxxghp
2025-08-17 08:39:02 +08:00
parent 276948dd68
commit 2a9ea81ad4
7 changed files with 137 additions and 41 deletions
@@ -127,7 +127,7 @@ const progressSSE = useProgressSSE(
`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer`, `${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer`,
handleProgressMessage, handleProgressMessage,
'transfer-queue-progress', 'transfer-queue-progress',
progressActive progressActive,
) )
// 使用SSE监听加载进度 // 使用SSE监听加载进度
@@ -166,6 +166,7 @@ onUnmounted(() => {
:value="progressValue" :value="progressValue"
color="primary" color="primary"
indeterminate indeterminate
:height="2"
/> />
<VCardItem v-if="dataList.length > 0 && progressValue > 0" class="text-center pt-2"> <VCardItem v-if="dataList.length > 0 && progressValue > 0" class="text-center pt-2">
<span class="text-sm">{{ progressText }}</span> <span class="text-sm">{{ progressText }}</span>
+17 -1
View File
@@ -22,22 +22,38 @@ export function useBackgroundOptimization() {
backgroundCloseDelay?: number backgroundCloseDelay?: number
reconnectDelay?: number reconnectDelay?: number
maxReconnectAttempts?: number maxReconnectAttempts?: number
connectDelay?: number // 新增:连接延迟
}, },
) => { ) => {
const manager = sseManagerSingleton.getManager(url, options) const manager = sseManagerSingleton.getManager(url, options)
const isConnected = ref(false)
onMounted(() => { onMounted(() => {
manager.addMessageListener(listenerId, messageHandler) // 延迟建立连接,确保组件完全挂载
const connectDelay = options?.connectDelay || 100
setTimeout(() => {
try {
manager.addMessageListener(listenerId, event => {
messageHandler(event)
isConnected.value = true
})
} catch (error) {
console.error('SSE连接建立失败:', error)
}
}, connectDelay)
}) })
onUnmounted(() => { onUnmounted(() => {
manager.removeMessageListener(listenerId) manager.removeMessageListener(listenerId)
isConnected.value = false
}) })
return { return {
manager, manager,
readyState: () => manager.readyState, readyState: () => manager.readyState,
close: () => manager.removeMessageListener(listenerId), close: () => manager.removeMessageListener(listenerId),
isConnected,
forceReconnect: () => manager.forceReconnect(),
} }
} }
+1
View File
@@ -1079,6 +1079,7 @@ export default {
program: 'Program', program: 'Program',
content: 'Content', content: 'Content',
refreshing: 'Refreshing', refreshing: 'Refreshing',
initializing: 'Initializing',
}, },
moduleTest: { moduleTest: {
normal: 'Normal', normal: 'Normal',
+1
View File
@@ -1075,6 +1075,7 @@ export default {
program: '程序', program: '程序',
content: '内容', content: '内容',
refreshing: '正在刷新', refreshing: '正在刷新',
initializing: '正在初始化',
}, },
moduleTest: { moduleTest: {
normal: '正常', normal: '正常',
+1
View File
@@ -1074,6 +1074,7 @@ export default {
program: '程序', program: '程序',
content: '內容', content: '內容',
refreshing: '正在刷新', refreshing: '正在刷新',
initializing: '正在初始化',
}, },
moduleTest: { moduleTest: {
normal: '正常', normal: '正常',
+63 -13
View File
@@ -14,6 +14,8 @@ export class SSEManager {
reconnectDelay: number reconnectDelay: number
maxReconnectAttempts: number maxReconnectAttempts: number
} }
private reconnectAttempts = 0
private isConnecting = false
constructor(url: string, options: Partial<typeof SSEManager.prototype.options> = {}) { constructor(url: string, options: Partial<typeof SSEManager.prototype.options> = {}) {
this.url = url this.url = url
@@ -21,7 +23,7 @@ export class SSEManager {
backgroundCloseDelay: 5000, // 5秒后关闭后台连接 backgroundCloseDelay: 5000, // 5秒后关闭后台连接
reconnectDelay: 3000, // 3秒后重连 reconnectDelay: 3000, // 3秒后重连
maxReconnectAttempts: 3, maxReconnectAttempts: 3,
...options ...options,
} }
this.setupVisibilityListener() this.setupVisibilityListener()
@@ -52,7 +54,6 @@ export class SSEManager {
this.backgroundCloseTimer = window.setTimeout(() => { this.backgroundCloseTimer = window.setTimeout(() => {
if (this.isBackground && this.eventSource) { if (this.isBackground && this.eventSource) {
console.log('SSE: 后台关闭连接')
this.eventSource.close() this.eventSource.close()
this.eventSource = null this.eventSource = null
} }
@@ -70,26 +71,32 @@ export class SSEManager {
// 立即重新建立连接 // 立即重新建立连接
if (!this.eventSource || this.eventSource.readyState === EventSource.CLOSED) { if (!this.eventSource || this.eventSource.readyState === EventSource.CLOSED) {
console.log('SSE: 前台恢复连接')
this.reconnectSSE() this.reconnectSSE()
} }
} }
private reconnectSSE(attemptCount = 0) { private reconnectSSE(attemptCount = 0) {
if (attemptCount >= this.options.maxReconnectAttempts) { if (attemptCount >= this.options.maxReconnectAttempts) {
console.warn('SSE: 达到最大重连次数')
return return
} }
if (this.isConnecting) {
return
}
this.isConnecting = true
this.reconnectAttempts = attemptCount
try { try {
this.eventSource = new EventSource(this.url) this.eventSource = new EventSource(this.url)
this.eventSource.onopen = () => { this.eventSource.onopen = () => {
console.log('SSE: 连接已建立') this.isConnecting = false
this.reconnectAttempts = 0
} }
this.eventSource.onerror = (error) => { this.eventSource.onerror = error => {
console.error('SSE: 连接错误', error) this.isConnecting = false
if (this.eventSource?.readyState === EventSource.CLOSED) { if (this.eventSource?.readyState === EventSource.CLOSED) {
// 连接已关闭,尝试重连 // 连接已关闭,尝试重连
@@ -99,13 +106,13 @@ export class SSEManager {
this.reconnectTimer = window.setTimeout(() => { this.reconnectTimer = window.setTimeout(() => {
if (!this.isBackground) { if (!this.isBackground) {
this.reconnectSSE(attemptCount + 1) this.reconnectSSE(this.reconnectAttempts + 1)
} }
}, this.options.reconnectDelay) }, this.options.reconnectDelay)
} }
} }
this.eventSource.onmessage = (event) => { this.eventSource.onmessage = event => {
// 分发消息给所有监听器 // 分发消息给所有监听器
this.listeners.forEach(listener => { this.listeners.forEach(listener => {
try { try {
@@ -115,9 +122,19 @@ export class SSEManager {
} }
}) })
} }
} catch (error) { } catch (error) {
console.error('SSE: 创建连接失败', error) this.isConnecting = false
// 连接创建失败,尝试重连
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer)
}
this.reconnectTimer = window.setTimeout(() => {
if (!this.isBackground) {
this.reconnectSSE(this.reconnectAttempts + 1)
}
}, this.options.reconnectDelay)
} }
} }
@@ -127,8 +144,8 @@ export class SSEManager {
addMessageListener(id: string, listener: (event: MessageEvent) => void) { addMessageListener(id: string, listener: (event: MessageEvent) => void) {
this.listeners.set(id, listener) this.listeners.set(id, listener)
// 如果还没有连接,现在建立连接 // 如果还没有连接且不在后台,现在建立连接
if (!this.eventSource && !this.isBackground) { if (!this.eventSource && !this.isBackground && !this.isConnecting) {
this.reconnectSSE() this.reconnectSSE()
} }
} }
@@ -165,6 +182,8 @@ export class SSEManager {
} }
this.listeners.clear() this.listeners.clear()
this.isConnecting = false
this.reconnectAttempts = 0
} }
/** /**
@@ -180,6 +199,37 @@ export class SSEManager {
get connectionUrl(): string { get connectionUrl(): string {
return this.url return this.url
} }
/**
* 强制重新连接
*/
forceReconnect() {
this.close()
if (!this.isBackground) {
this.reconnectSSE()
}
}
/**
* 检查是否有活跃的监听器
*/
get hasActiveListeners(): boolean {
return this.listeners.size > 0
}
/**
* 获取当前重连次数
*/
get currentReconnectAttempts(): number {
return this.reconnectAttempts
}
/**
* 检查是否达到最大重连次数
*/
get hasReachedMaxAttempts(): boolean {
return this.reconnectAttempts >= this.options.maxReconnectAttempts
}
} }
/** /**
+37 -11
View File
@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { isToday } from '@/@core/utils/index' import { isToday } from '@/@core/utils/index'
import dayjs from 'dayjs'; import dayjs from 'dayjs'
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization' import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
// 定义输入变量 // 定义输入变量
@@ -16,6 +16,9 @@ const { useSSE } = useBackgroundOptimization()
// 已解析的日志列表 // 已解析的日志列表
const parsedLogs = ref<{ level: string; date: string; time: string; program: string; content: string }[]>([]) const parsedLogs = ref<{ level: string; date: string; time: string; program: string; content: string }[]>([])
// 组件是否已挂载
const isMounted = ref(false)
// 表头 // 表头
const headers = [ const headers = [
{ title: t('logging.level'), value: 'level' }, { title: t('logging.level'), value: 'level' },
@@ -72,23 +75,40 @@ function handleSSEMessage(event: MessageEvent) {
} }
} }
// 使用优化的SSE连接 // 使用优化的SSE连接,添加延迟确保弹窗完全打开
useSSE( const { manager, isConnected } = useSSE(
`${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'
}`,
handleSSEMessage, handleSSEMessage,
`logging-${props.logfile}`, `logging-${props.logfile}`,
{ {
backgroundCloseDelay: 5000, backgroundCloseDelay: 5000,
reconnectDelay: 3000, reconnectDelay: 3000,
maxReconnectAttempts: 3 maxReconnectAttempts: 3,
} connectDelay: 300, // 延迟300ms建立连接,确保弹窗完全打开
},
) )
// 监听弹窗状态变化,确保弹窗完全打开后再建立连接
onMounted(() => {
// 延迟标记组件已挂载,确保弹窗完全渲染
setTimeout(() => {
isMounted.value = true
}, 200)
})
// 监听连接状态变化
watch(isConnected, connected => {})
// 监听日志数据变化
watch(parsedLogs, logs => {}, { deep: true })
</script> </script>
<template> <template>
<LoadingBanner v-if="parsedLogs.length === 0" class="mt-12" :text="t('logging.refreshing') + ' ...'" /> <LoadingBanner
v-if="!isMounted || !isConnected || parsedLogs.length === 0"
class="mt-12"
:text="!isMounted ? t('logging.initializing') + ' ...' : 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>
@@ -104,8 +124,14 @@ useSSE(
<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>
<template #item.time="{ item }"> <template #item.time="{ item }">
<span class="text-sm">{{ isToday(dayjs(item.date).toDate()) ? item.time : `${item.date} <span class="text-sm">
${item.time}` }}</span> {{
isToday(dayjs(item.date).toDate())
? item.time
: `${item.date}
${item.time}`
}}
</span>
</template> </template>
<template #item.program="{ item }"> <template #item.program="{ item }">
<h6 class="text-sm font-weight-medium">{{ item.program }}</h6> <h6 class="text-sm font-weight-medium">{{ item.program }}</h6>