mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-07-06 23:11:43 +08:00
refactor: replace useBackgroundOptimization with unified useBackground composable and update Nginx SSE route configuration
This commit is contained in:
@@ -9,14 +9,14 @@ import { cloneDeep } from 'lodash-es'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { downloaderDict, storageAttributes } from '@/api/constants'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
import { useBackground } from '@/composables/useBackground'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 获取i18n实例
|
||||
const { t } = useI18n()
|
||||
const { useConditionalDataRefresh } = useBackgroundOptimization()
|
||||
const { useConditionalDataRefresh } = useBackground()
|
||||
|
||||
// 定义输入
|
||||
const props = defineProps({
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
TransferDirectoryConf,
|
||||
TransferForm,
|
||||
} from '@/api/types'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
import { useBackground } from '@/composables/useBackground'
|
||||
import MediaIdSelector from '../misc/MediaIdSelector.vue'
|
||||
import ProgressDialog from './ProgressDialog.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -24,7 +24,7 @@ import { useGlobalSettingsStore } from '@/stores'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
const { useProgressSSE } = useBackgroundOptimization()
|
||||
const { useProgressSSE } = useBackground()
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
@@ -5,7 +5,7 @@ import api from '@/api'
|
||||
import { FileItem, TransferQueue } from '@/api/types'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
import { useBackground } from '@/composables/useBackground'
|
||||
import CryptoJS from 'crypto-js'
|
||||
|
||||
type TransferTask = TransferQueue['tasks'][number]
|
||||
@@ -20,7 +20,7 @@ interface MediaTaskGroup {
|
||||
|
||||
// 多语言支持
|
||||
const { t } = useI18n()
|
||||
const { useProgressSSE } = useBackgroundOptimization()
|
||||
const { useProgressSSE } = useBackground()
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
@@ -11,14 +11,14 @@ import ProgressDialog from '../dialog/ProgressDialog.vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import MediaInfoDialog from '../dialog/MediaInfoDialog.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
import { useBackground } from '@/composables/useBackground'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
import { useAvailableHeight } from '@/composables/useAvailableHeight'
|
||||
import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
const { useProgressSSE } = useBackgroundOptimization()
|
||||
const { useProgressSSE } = useBackground()
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
@@ -652,7 +652,7 @@ function handleProgressMessage(event: MessageEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
// 使用优化的进度SSE连接
|
||||
// 使用进度SSE连接
|
||||
const progressSSE = useProgressSSE(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/progress/batchrename`,
|
||||
handleProgressMessage,
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { onMounted, onUnmounted, ref, type Ref } from 'vue'
|
||||
import { sseManagerSingleton } from '@/utils/sseManager'
|
||||
import { getCurrentInstance, onMounted, onUnmounted, ref, type Ref } from 'vue'
|
||||
import { sseManagerSingleton, type SSEManagerOptions } from '@/utils/sseManager'
|
||||
import { addBackgroundTimer, removeBackgroundTimer } from '@/utils/backgroundManager'
|
||||
|
||||
type UseSSEOptions = Partial<SSEManagerOptions> & {
|
||||
connectDelay?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台优化组合函数
|
||||
* 统一管理SSE连接和定时器,优化iOS后台性能
|
||||
* 后台任务组合函数
|
||||
* 统一管理SSE连接和定时器,减少后台常驻活动。
|
||||
*/
|
||||
export function useBackgroundOptimization() {
|
||||
export function useBackground() {
|
||||
/**
|
||||
* 使用优化的SSE连接
|
||||
* 使用SSE连接
|
||||
* @param url SSE连接地址
|
||||
* @param messageHandler 消息处理函数
|
||||
* @param listenerId 监听器ID(用于区分不同的监听器)
|
||||
@@ -18,24 +22,30 @@ export function useBackgroundOptimization() {
|
||||
url: string,
|
||||
messageHandler: (event: MessageEvent) => void,
|
||||
listenerId: string,
|
||||
options?: {
|
||||
backgroundCloseDelay?: number
|
||||
reconnectDelay?: number
|
||||
maxReconnectAttempts?: number
|
||||
connectDelay?: number // 新增:连接延迟
|
||||
},
|
||||
options?: UseSSEOptions,
|
||||
) => {
|
||||
// 使用独立的SSE管理器,确保每个监听器都有独立的连接
|
||||
const manager = sseManagerSingleton.getIndependentManager(url, listenerId, options)
|
||||
const isConnected = ref(false)
|
||||
let connectTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let isClosed = false
|
||||
const statusListenerId = `${listenerId}:status`
|
||||
|
||||
manager.addStatusListener(statusListenerId, status => {
|
||||
isConnected.value = status === 'open'
|
||||
})
|
||||
|
||||
const cleanup = () => {
|
||||
if (isClosed) return
|
||||
|
||||
isClosed = true
|
||||
|
||||
if (connectTimer) {
|
||||
clearTimeout(connectTimer)
|
||||
connectTimer = null
|
||||
}
|
||||
|
||||
manager.removeStatusListener(statusListenerId)
|
||||
manager.removeMessageListener(listenerId)
|
||||
sseManagerSingleton.closeIndependentManager(url, listenerId)
|
||||
isConnected.value = false
|
||||
@@ -46,11 +56,10 @@ export function useBackgroundOptimization() {
|
||||
const connectDelay = options?.connectDelay || 100
|
||||
connectTimer = setTimeout(() => {
|
||||
connectTimer = null
|
||||
if (isClosed) return
|
||||
|
||||
try {
|
||||
manager.addMessageListener(listenerId, event => {
|
||||
messageHandler(event)
|
||||
isConnected.value = true
|
||||
})
|
||||
manager.addMessageListener(listenerId, messageHandler)
|
||||
} catch (error) {
|
||||
console.error('SSE连接建立失败:', error)
|
||||
}
|
||||
@@ -69,7 +78,7 @@ export function useBackgroundOptimization() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用优化的定时器
|
||||
* 使用定时器
|
||||
* @param id 定时器ID
|
||||
* @param callback 回调函数
|
||||
* @param interval 间隔时间(毫秒)
|
||||
@@ -110,25 +119,40 @@ export function useBackgroundOptimization() {
|
||||
messageHandler: (event: MessageEvent) => void,
|
||||
listenerId: string,
|
||||
delay: number = 3000,
|
||||
options?: Parameters<typeof useSSE>[3],
|
||||
options?: UseSSEOptions,
|
||||
) => {
|
||||
// 使用独立的SSE管理器,确保每个监听器都有独立的连接
|
||||
const manager = sseManagerSingleton.getIndependentManager(url, listenerId, options)
|
||||
const isConnected = ref(false)
|
||||
let connectTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let isClosed = false
|
||||
const statusListenerId = `${listenerId}:status`
|
||||
|
||||
manager.addStatusListener(statusListenerId, status => {
|
||||
isConnected.value = status === 'open'
|
||||
})
|
||||
|
||||
const cleanup = () => {
|
||||
if (isClosed) return
|
||||
|
||||
isClosed = true
|
||||
|
||||
if (connectTimer) {
|
||||
clearTimeout(connectTimer)
|
||||
connectTimer = null
|
||||
}
|
||||
|
||||
manager.removeStatusListener(statusListenerId)
|
||||
manager.removeMessageListener(listenerId)
|
||||
sseManagerSingleton.closeIndependentManager(url, listenerId)
|
||||
isConnected.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
connectTimer = setTimeout(() => {
|
||||
connectTimer = null
|
||||
if (isClosed) return
|
||||
|
||||
manager.addMessageListener(listenerId, messageHandler)
|
||||
}, delay)
|
||||
})
|
||||
@@ -139,6 +163,7 @@ export function useBackgroundOptimization() {
|
||||
manager,
|
||||
readyState: () => manager.readyState,
|
||||
close: cleanup,
|
||||
isConnected,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,9 +214,12 @@ export function useBackgroundOptimization() {
|
||||
isListening = false
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
stopProgress(true)
|
||||
})
|
||||
// 进度监听有些场景会在用户操作后动态创建;只有 setup 阶段创建时才注册自动卸载钩子。
|
||||
if (getCurrentInstance()) {
|
||||
onUnmounted(() => {
|
||||
stopProgress(true)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
start: startProgress,
|
||||
@@ -2,10 +2,10 @@
|
||||
import { formatDateDifference } from '@core/utils/formatters'
|
||||
import { SystemNotification } from '@/api/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
import { useBackground } from '@/composables/useBackground'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { useDelayedSSE } = useBackgroundOptimization()
|
||||
const { useDelayedSSE } = useBackground()
|
||||
|
||||
// 是否有新消息
|
||||
const hasNewMessage = ref(false)
|
||||
@@ -39,7 +39,7 @@ function handleMessage(event: MessageEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
// 使用优化的SSE连接,延迟3秒启动,避免认证问题
|
||||
// 延迟3秒启动SSE连接,避免认证信息尚未准备好。
|
||||
useDelayedSSE(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/message`,
|
||||
handleMessage,
|
||||
|
||||
@@ -240,6 +240,8 @@ const progressEnabled = ref(false)
|
||||
// 进度是否激活
|
||||
const progressActive = ref(false)
|
||||
|
||||
let progressResetTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
// 是否显示搜索进度
|
||||
const isSearchProgressVisible = computed(
|
||||
() => progressActive.value || (!isRefreshed.value && (progressEnabled.value || progressValue.value > 0)),
|
||||
@@ -268,10 +270,12 @@ const errorTitle = ref(t('resource.noData'))
|
||||
const errorDescription = ref(t('resource.noResourceFound'))
|
||||
|
||||
let searchEventSource: EventSource | null = null
|
||||
let searchStreamIdleTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const streamPreviewLimit = 24
|
||||
const streamUiFlushDelay = 1000
|
||||
const streamPreviewBufferLimit = streamPreviewLimit * 4
|
||||
const searchStreamIdleTimeout = 90_000
|
||||
|
||||
const streamTotalCount = ref(0)
|
||||
const streamPreviewDataList = ref<Array<Context>>([])
|
||||
@@ -343,6 +347,7 @@ const watchProgressValue = watch(
|
||||
|
||||
// 使用SSE监听加载进度
|
||||
function startLoadingProgress() {
|
||||
clearProgressResetTimer()
|
||||
watchProgressValue.resume()
|
||||
progressText.value = t('resource.searching')
|
||||
progressValue.value = 0
|
||||
@@ -357,18 +362,41 @@ function stopLoadingProgress() {
|
||||
|
||||
// 确保进度显示100%,然后再渐进清零
|
||||
progressValue.value = 100
|
||||
setTimeout(() => {
|
||||
clearProgressResetTimer()
|
||||
progressResetTimer = setTimeout(() => {
|
||||
progressResetTimer = null
|
||||
progressValue.value = 0
|
||||
progressEnabled.value = false
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
function clearProgressResetTimer() {
|
||||
if (progressResetTimer) {
|
||||
clearTimeout(progressResetTimer)
|
||||
progressResetTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭SSE连接
|
||||
function closeSearchEventSource() {
|
||||
function closeSearchEventSource(source?: EventSource) {
|
||||
if (source && searchEventSource !== source) {
|
||||
source.close()
|
||||
return
|
||||
}
|
||||
|
||||
if (searchEventSource) {
|
||||
searchEventSource.close()
|
||||
searchEventSource = null
|
||||
}
|
||||
|
||||
clearSearchStreamIdleTimer()
|
||||
}
|
||||
|
||||
function clearSearchStreamIdleTimer() {
|
||||
if (searchStreamIdleTimer) {
|
||||
clearTimeout(searchStreamIdleTimer)
|
||||
searchStreamIdleTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
// 渐进式搜索期间只保留有限预览数据,避免每个批次都触发完整筛选和分组计算。
|
||||
@@ -607,36 +635,48 @@ function searchByStream(params: SearchParams, requestToken?: string) {
|
||||
const source = new EventSource(buildSearchStreamUrl(params, requestToken))
|
||||
searchEventSource = source
|
||||
|
||||
const settleSearchStream = (callback: () => void) => {
|
||||
if (settled) return
|
||||
|
||||
settled = true
|
||||
closeSearchEventSource(source)
|
||||
callback()
|
||||
}
|
||||
|
||||
const resetIdleTimeout = () => {
|
||||
clearSearchStreamIdleTimer()
|
||||
searchStreamIdleTimer = setTimeout(() => {
|
||||
settleSearchStream(() => reject(new Error(t('resource.noResourceFound'))))
|
||||
}, searchStreamIdleTimeout)
|
||||
}
|
||||
|
||||
resetIdleTimeout()
|
||||
|
||||
source.onmessage = event => {
|
||||
if (source !== searchEventSource || settled) return
|
||||
|
||||
try {
|
||||
resetIdleTimeout()
|
||||
const eventData = JSON.parse(event.data)
|
||||
handleSearchStreamMessage(eventData)
|
||||
|
||||
if (eventData.type === 'error') {
|
||||
settled = true
|
||||
closeSearchEventSource()
|
||||
resolve()
|
||||
settleSearchStream(resolve)
|
||||
return
|
||||
}
|
||||
|
||||
if (eventData.type === 'done') {
|
||||
settled = true
|
||||
closeSearchEventSource()
|
||||
resolve()
|
||||
settleSearchStream(resolve)
|
||||
}
|
||||
} catch (error) {
|
||||
settled = true
|
||||
closeSearchEventSource()
|
||||
reject(error)
|
||||
settleSearchStream(() => reject(error))
|
||||
}
|
||||
}
|
||||
|
||||
source.onerror = () => {
|
||||
if (settled) return
|
||||
if (source !== searchEventSource || settled) return
|
||||
|
||||
settled = true
|
||||
closeSearchEventSource()
|
||||
reject(new Error(t('resource.noResourceFound')))
|
||||
settleSearchStream(() => reject(new Error(t('resource.noResourceFound'))))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1011,6 +1051,7 @@ onMounted(async () => {
|
||||
onUnmounted(() => {
|
||||
closeSearchEventSource()
|
||||
stopLoadingProgress()
|
||||
clearProgressResetTimer()
|
||||
stopAiRecommendPolling()
|
||||
clearStreamPreviewState()
|
||||
})
|
||||
|
||||
@@ -1,21 +1,33 @@
|
||||
export type SSEConnectionStatus = 'idle' | 'connecting' | 'open' | 'error' | 'closed'
|
||||
|
||||
export interface SSEManagerOptions {
|
||||
backgroundCloseDelay: number
|
||||
reconnectDelay: number
|
||||
maxReconnectAttempts: number
|
||||
reconnectBackoffMultiplier: number
|
||||
maxReconnectDelay: number
|
||||
}
|
||||
|
||||
type SSEMessageListener = (event: MessageEvent) => void
|
||||
type SSEStatusListener = (status: SSEConnectionStatus) => void
|
||||
|
||||
/**
|
||||
* SSE连接管理器
|
||||
* 优化后台SSE连接,减少iOS系统杀掉应用的概率
|
||||
* 统一收口 EventSource 生命周期,避免后台常驻连接和重复重连。
|
||||
*/
|
||||
export class SSEManager {
|
||||
private eventSource: EventSource | null = null
|
||||
private url: string
|
||||
private isBackground = false
|
||||
private isBackground = document.hidden
|
||||
private reconnectTimer: number | null = null
|
||||
private backgroundCloseTimer: number | null = null
|
||||
private listeners: Map<string, (event: MessageEvent) => void> = new Map()
|
||||
private options: {
|
||||
backgroundCloseDelay: number
|
||||
reconnectDelay: number
|
||||
maxReconnectAttempts: number
|
||||
}
|
||||
private listeners: Map<string, SSEMessageListener> = new Map()
|
||||
private statusListeners: Map<string, SSEStatusListener> = new Map()
|
||||
private options: SSEManagerOptions
|
||||
private reconnectAttempts = 0
|
||||
private isConnecting = false
|
||||
private isDestroyed = false
|
||||
private connectionStatus: SSEConnectionStatus = 'idle'
|
||||
private readonly handleVisibilityChange = () => {
|
||||
if (document.hidden) {
|
||||
this.handleBackground()
|
||||
@@ -27,12 +39,14 @@ export class SSEManager {
|
||||
this.destroy()
|
||||
}
|
||||
|
||||
constructor(url: string, options: Partial<typeof SSEManager.prototype.options> = {}) {
|
||||
constructor(url: string, options: Partial<SSEManagerOptions> = {}) {
|
||||
this.url = url
|
||||
this.options = {
|
||||
backgroundCloseDelay: 5000, // 5秒后关闭后台连接
|
||||
reconnectDelay: 3000, // 3秒后重连
|
||||
maxReconnectAttempts: 3,
|
||||
reconnectBackoffMultiplier: 1.5,
|
||||
maxReconnectDelay: 30_000,
|
||||
...options,
|
||||
}
|
||||
|
||||
@@ -50,38 +64,44 @@ export class SSEManager {
|
||||
}
|
||||
|
||||
private handleBackground() {
|
||||
if (this.isDestroyed) return
|
||||
|
||||
this.isBackground = true
|
||||
this.clearReconnectTimer()
|
||||
|
||||
// 延迟关闭SSE连接,避免频繁切换
|
||||
if (this.backgroundCloseTimer) {
|
||||
clearTimeout(this.backgroundCloseTimer)
|
||||
}
|
||||
this.clearBackgroundCloseTimer()
|
||||
|
||||
this.backgroundCloseTimer = window.setTimeout(() => {
|
||||
if (this.isBackground && this.eventSource) {
|
||||
this.eventSource.close()
|
||||
this.eventSource = null
|
||||
this.closeCurrentEventSource()
|
||||
this.setConnectionStatus('closed')
|
||||
}
|
||||
}, this.options.backgroundCloseDelay)
|
||||
}
|
||||
|
||||
private handleForeground() {
|
||||
if (this.isDestroyed) return
|
||||
|
||||
this.isBackground = false
|
||||
|
||||
// 清除后台关闭定时器
|
||||
if (this.backgroundCloseTimer) {
|
||||
clearTimeout(this.backgroundCloseTimer)
|
||||
this.backgroundCloseTimer = null
|
||||
}
|
||||
this.clearBackgroundCloseTimer()
|
||||
|
||||
// 只有在有活跃监听器时才重新建立连接
|
||||
if (this.listeners.size > 0 && (!this.eventSource || this.eventSource.readyState === EventSource.CLOSED)) {
|
||||
this.reconnectSSE()
|
||||
this.reconnectSSE(0)
|
||||
}
|
||||
}
|
||||
|
||||
private reconnectSSE(attemptCount = 0) {
|
||||
if (attemptCount >= this.options.maxReconnectAttempts) {
|
||||
if (this.isDestroyed || this.isBackground || this.listeners.size === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (attemptCount > this.options.maxReconnectAttempts) {
|
||||
this.reconnectAttempts = this.options.maxReconnectAttempts
|
||||
this.setConnectionStatus('closed')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -89,40 +109,38 @@ export class SSEManager {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果没有活跃的监听器,不进行重连
|
||||
if (this.listeners.size === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
this.clearReconnectTimer()
|
||||
this.closeCurrentEventSource()
|
||||
this.isConnecting = true
|
||||
this.reconnectAttempts = attemptCount
|
||||
this.setConnectionStatus('connecting')
|
||||
|
||||
try {
|
||||
this.eventSource = new EventSource(this.url)
|
||||
const source = new EventSource(this.url)
|
||||
this.eventSource = source
|
||||
|
||||
this.eventSource.onopen = () => {
|
||||
source.onopen = () => {
|
||||
if (source !== this.eventSource) return
|
||||
this.isConnecting = false
|
||||
this.reconnectAttempts = 0
|
||||
this.setConnectionStatus('open')
|
||||
}
|
||||
|
||||
this.eventSource.onerror = error => {
|
||||
source.onerror = () => {
|
||||
if (source !== this.eventSource) return
|
||||
|
||||
this.isConnecting = false
|
||||
this.setConnectionStatus('error')
|
||||
|
||||
if (this.eventSource?.readyState === EventSource.CLOSED) {
|
||||
// 连接已关闭,尝试重连
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer)
|
||||
}
|
||||
|
||||
this.reconnectTimer = window.setTimeout(() => {
|
||||
if (!this.isBackground && this.listeners.size > 0) {
|
||||
this.reconnectSSE(this.reconnectAttempts + 1)
|
||||
}
|
||||
}, this.options.reconnectDelay)
|
||||
if (source.readyState === EventSource.CLOSED) {
|
||||
this.closeCurrentEventSource()
|
||||
this.scheduleReconnect(this.reconnectAttempts + 1)
|
||||
}
|
||||
}
|
||||
|
||||
this.eventSource.onmessage = event => {
|
||||
source.onmessage = event => {
|
||||
if (source !== this.eventSource || this.isDestroyed) return
|
||||
|
||||
// 分发消息给所有监听器
|
||||
this.listeners.forEach((listener, listenerId) => {
|
||||
try {
|
||||
@@ -135,29 +153,95 @@ export class SSEManager {
|
||||
}
|
||||
} catch (error) {
|
||||
this.isConnecting = false
|
||||
this.setConnectionStatus('error')
|
||||
|
||||
// 连接创建失败,尝试重连
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer)
|
||||
}
|
||||
|
||||
this.reconnectTimer = window.setTimeout(() => {
|
||||
if (!this.isBackground && this.listeners.size > 0) {
|
||||
this.reconnectSSE(this.reconnectAttempts + 1)
|
||||
}
|
||||
}, this.options.reconnectDelay)
|
||||
this.scheduleReconnect(this.reconnectAttempts + 1)
|
||||
console.error('SSE: 连接创建失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleReconnect(attemptCount: number) {
|
||||
if (this.isDestroyed || this.isBackground || this.listeners.size === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (attemptCount > this.options.maxReconnectAttempts) {
|
||||
this.reconnectAttempts = this.options.maxReconnectAttempts
|
||||
this.setConnectionStatus('closed')
|
||||
return
|
||||
}
|
||||
|
||||
this.clearReconnectTimer()
|
||||
this.reconnectAttempts = attemptCount
|
||||
|
||||
// 失败越多等待越久,避免网络波动时短时间内打满连接。
|
||||
const reconnectDelay = Math.min(
|
||||
this.options.reconnectDelay * this.options.reconnectBackoffMultiplier ** Math.max(0, attemptCount - 1),
|
||||
this.options.maxReconnectDelay,
|
||||
)
|
||||
|
||||
this.reconnectTimer = window.setTimeout(() => {
|
||||
this.reconnectTimer = null
|
||||
this.reconnectSSE(attemptCount)
|
||||
}, reconnectDelay)
|
||||
}
|
||||
|
||||
private closeCurrentEventSource() {
|
||||
if (!this.eventSource) {
|
||||
return
|
||||
}
|
||||
|
||||
this.eventSource.onopen = null
|
||||
this.eventSource.onerror = null
|
||||
this.eventSource.onmessage = null
|
||||
this.eventSource.close()
|
||||
this.eventSource = null
|
||||
this.isConnecting = false
|
||||
}
|
||||
|
||||
private clearReconnectTimer() {
|
||||
if (!this.reconnectTimer) return
|
||||
|
||||
clearTimeout(this.reconnectTimer)
|
||||
this.reconnectTimer = null
|
||||
}
|
||||
|
||||
private clearBackgroundCloseTimer() {
|
||||
if (!this.backgroundCloseTimer) return
|
||||
|
||||
clearTimeout(this.backgroundCloseTimer)
|
||||
this.backgroundCloseTimer = null
|
||||
}
|
||||
|
||||
private setConnectionStatus(status: SSEConnectionStatus) {
|
||||
if (this.connectionStatus === status) return
|
||||
|
||||
this.connectionStatus = status
|
||||
this.statusListeners.forEach((listener, listenerId) => {
|
||||
try {
|
||||
listener(status)
|
||||
} catch (error) {
|
||||
console.error(`SSE: 状态监听器错误 [${listenerId}]`, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加消息监听器
|
||||
*/
|
||||
addMessageListener(id: string, listener: (event: MessageEvent) => void) {
|
||||
addMessageListener(id: string, listener: SSEMessageListener) {
|
||||
if (this.isDestroyed) return
|
||||
|
||||
this.listeners.set(id, listener)
|
||||
|
||||
// 如果还没有连接且不在后台,现在建立连接
|
||||
if (!this.eventSource && !this.isBackground && !this.isConnecting) {
|
||||
this.reconnectSSE()
|
||||
if (
|
||||
!this.isBackground &&
|
||||
!this.isConnecting &&
|
||||
(!this.eventSource || this.eventSource.readyState === EventSource.CLOSED)
|
||||
) {
|
||||
this.reconnectSSE(0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +249,8 @@ export class SSEManager {
|
||||
* 移除消息监听器
|
||||
*/
|
||||
removeMessageListener(id: string) {
|
||||
if (this.isDestroyed) return
|
||||
|
||||
this.listeners.delete(id)
|
||||
|
||||
// 如果没有监听器了,关闭连接
|
||||
@@ -184,25 +270,17 @@ export class SSEManager {
|
||||
* 销毁管理器并清理所有引用
|
||||
*/
|
||||
destroy() {
|
||||
if (this.isDestroyed) return
|
||||
|
||||
this.isDestroyed = true
|
||||
this.resetConnectionState(true)
|
||||
this.removeVisibilityListener()
|
||||
}
|
||||
|
||||
private resetConnectionState(clearListeners = false) {
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close()
|
||||
this.eventSource = null
|
||||
}
|
||||
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer)
|
||||
this.reconnectTimer = null
|
||||
}
|
||||
|
||||
if (this.backgroundCloseTimer) {
|
||||
clearTimeout(this.backgroundCloseTimer)
|
||||
this.backgroundCloseTimer = null
|
||||
}
|
||||
this.closeCurrentEventSource()
|
||||
this.clearReconnectTimer()
|
||||
this.clearBackgroundCloseTimer()
|
||||
|
||||
if (clearListeners) {
|
||||
this.listeners.clear()
|
||||
@@ -210,6 +288,31 @@ export class SSEManager {
|
||||
|
||||
this.isConnecting = false
|
||||
this.reconnectAttempts = 0
|
||||
this.setConnectionStatus(this.listeners.size > 0 ? 'closed' : 'idle')
|
||||
|
||||
if (clearListeners) {
|
||||
this.statusListeners.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加连接状态监听器
|
||||
*/
|
||||
addStatusListener(id: string, listener: SSEStatusListener, emitCurrent = true) {
|
||||
if (this.isDestroyed) return
|
||||
|
||||
this.statusListeners.set(id, listener)
|
||||
|
||||
if (emitCurrent) {
|
||||
listener(this.connectionStatus)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除连接状态监听器
|
||||
*/
|
||||
removeStatusListener(id: string) {
|
||||
this.statusListeners.delete(id)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,6 +322,13 @@ export class SSEManager {
|
||||
return this.eventSource?.readyState ?? EventSource.CLOSED
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内部连接状态
|
||||
*/
|
||||
get status(): SSEConnectionStatus {
|
||||
return this.connectionStatus
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接URL
|
||||
*/
|
||||
@@ -230,10 +340,12 @@ export class SSEManager {
|
||||
* 强制重新连接
|
||||
*/
|
||||
forceReconnect() {
|
||||
if (this.isDestroyed) return
|
||||
|
||||
const hasActiveListeners = this.listeners.size > 0
|
||||
this.close()
|
||||
if (!this.isBackground && hasActiveListeners) {
|
||||
this.reconnectSSE()
|
||||
this.reconnectSSE(0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ import { useTheme } from 'vuetify'
|
||||
import { hexToRgb } from '@layouts/utils'
|
||||
import api from '@/api'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
import { useBackground } from '@/composables/useBackground'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
const { useDataRefresh } = useBackgroundOptimization()
|
||||
const { useDataRefresh } = useBackground()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -122,7 +122,7 @@ async function loadCpuData() {
|
||||
}
|
||||
}
|
||||
|
||||
// 使用优化的数据刷新定时器
|
||||
// 使用数据刷新定时器
|
||||
const { loading } = useDataRefresh(
|
||||
'analytics-cpu',
|
||||
loadCpuData,
|
||||
|
||||
@@ -4,11 +4,11 @@ import { hexToRgb } from '@layouts/utils'
|
||||
import api from '@/api'
|
||||
import { formatBytes } from '@/@core/utils/formatters'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
import { useBackground } from '@/composables/useBackground'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
const { useDataRefresh } = useBackgroundOptimization()
|
||||
const { useDataRefresh } = useBackground()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -127,7 +127,7 @@ async function loadMemoryData() {
|
||||
}
|
||||
}
|
||||
|
||||
// 使用优化的数据刷新定时器
|
||||
// 使用数据刷新定时器
|
||||
const { loading } = useDataRefresh(
|
||||
'analytics-memory',
|
||||
loadMemoryData,
|
||||
|
||||
@@ -3,11 +3,11 @@ import { useTheme } from 'vuetify'
|
||||
import { hexToRgb } from '@layouts/utils'
|
||||
import api from '@/api'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
import { useBackground } from '@/composables/useBackground'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
const { useDataRefresh } = useBackgroundOptimization()
|
||||
const { useDataRefresh } = useBackground()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -160,7 +160,7 @@ async function getNetworkUsage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 使用优化的数据刷新定时器
|
||||
// 使用数据刷新定时器
|
||||
useDataRefresh(
|
||||
'dashboard-network',
|
||||
getNetworkUsage,
|
||||
|
||||
@@ -3,11 +3,11 @@ import { formatSeconds } from '@/@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import type { Process } from '@/api/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
import { useBackground } from '@/composables/useBackground'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
const { useDataRefresh } = useBackgroundOptimization()
|
||||
const { useDataRefresh } = useBackground()
|
||||
|
||||
// 表头
|
||||
const headers = [
|
||||
@@ -31,7 +31,7 @@ async function loadProcessList() {
|
||||
}
|
||||
}
|
||||
|
||||
// 使用优化的数据刷新定时器
|
||||
// 使用数据刷新定时器
|
||||
useDataRefresh(
|
||||
'dashboard-processes',
|
||||
loadProcessList,
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
import api from '@/api'
|
||||
import type { ScheduleInfo } from '@/api/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
import { useBackground } from '@/composables/useBackground'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
const { useDataRefresh } = useBackgroundOptimization()
|
||||
const { useDataRefresh } = useBackground()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -34,7 +34,7 @@ async function loadSchedulerList() {
|
||||
}
|
||||
}
|
||||
|
||||
// 使用优化的数据刷新定时器
|
||||
// 使用数据刷新定时器
|
||||
useDataRefresh(
|
||||
'dashboard-scheduler',
|
||||
loadSchedulerList,
|
||||
|
||||
@@ -3,11 +3,11 @@ import { formatFileSize } from '@/@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import type { DownloaderInfo } from '@/api/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
import { useBackground } from '@/composables/useBackground'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
const { useDataRefresh } = useBackgroundOptimization()
|
||||
const { useDataRefresh } = useBackground()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -77,7 +77,7 @@ async function loadDownloaderInfo() {
|
||||
}
|
||||
}
|
||||
|
||||
// 使用优化的数据刷新定时器
|
||||
// 使用数据刷新定时器
|
||||
const { loading } = useDataRefresh(
|
||||
'analytics-speed',
|
||||
loadDownloaderInfo,
|
||||
|
||||
@@ -7,11 +7,11 @@ import DownloadingCard from '@/components/cards/DownloadingCard.vue'
|
||||
import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
import { useBackground } from '@/composables/useBackground'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
const { useDataRefresh } = useBackgroundOptimization()
|
||||
const { useDataRefresh } = useBackground()
|
||||
|
||||
// 定义输入参数
|
||||
const props = defineProps<{
|
||||
@@ -56,7 +56,7 @@ const filteredDataList = computed(() => {
|
||||
else return dataList.value.filter(data => data.userid === userName || data.username === userName)
|
||||
})
|
||||
|
||||
// 使用优化的数据刷新定时器
|
||||
// 使用数据刷新定时器
|
||||
const { loading: dataLoading } = useDataRefresh(
|
||||
'downloading-list',
|
||||
fetchData,
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
import { useDynamicButton } from '@/composables/useDynamicButton'
|
||||
import { useAvailableHeight } from '@/composables/useAvailableHeight'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
import { useBackground } from '@/composables/useBackground'
|
||||
import { useGlobalSettingsStore } from '@/stores'
|
||||
|
||||
// i18n
|
||||
@@ -27,7 +27,7 @@ const globalSettingsStore = useGlobalSettingsStore()
|
||||
const display = useDisplay()
|
||||
// PWA模式检测
|
||||
const { appMode } = usePWA()
|
||||
const { useProgressSSE } = useBackgroundOptimization()
|
||||
const { useProgressSSE } = useBackground()
|
||||
|
||||
// 计算列表可用高度
|
||||
// componentOffset = VCardItem搜索栏(68) + VDivider(1) + 分页栏(40) + VCard边距(2) = 111
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useTheme } from 'vuetify'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
import { useBackground } from '@/composables/useBackground'
|
||||
import { useAvailableHeight } from '@/composables/useAvailableHeight'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
@@ -46,7 +46,7 @@ const props = defineProps<{
|
||||
const { t } = useI18n()
|
||||
const theme = useTheme()
|
||||
const display = useDisplay()
|
||||
const { useSSE } = useBackgroundOptimization()
|
||||
const { useSSE } = useBackground()
|
||||
|
||||
const DEFAULT_LEVELS = ['TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
|
||||
const MAX_LOG_LINES = 600
|
||||
|
||||
@@ -3,11 +3,11 @@ import type { Message } from '@/api/types'
|
||||
import MessageCard from '@/components/cards/MessageCard.vue'
|
||||
import api from '@/api'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
import { useBackground } from '@/composables/useBackground'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
const { useSSE } = useBackgroundOptimization()
|
||||
const { useSSE } = useBackground()
|
||||
|
||||
// 消息列表
|
||||
const messages = ref<Message[]>([])
|
||||
@@ -201,7 +201,7 @@ function handleSSEMessage(event: MessageEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
// 使用优化的SSE连接
|
||||
// 使用SSE连接
|
||||
const { manager, isConnected } = useSSE(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/message?role=user`,
|
||||
handleSSEMessage,
|
||||
|
||||
@@ -3,11 +3,11 @@ import { useToast } from 'vue-toastification'
|
||||
import api from '@/api'
|
||||
import type { ScheduleInfo } from '@/api/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
import { useBackground } from '@/composables/useBackground'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
const { useDataRefresh } = useBackgroundOptimization()
|
||||
const { useDataRefresh } = useBackground()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
@@ -59,7 +59,7 @@ function runCommand(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 使用优化的数据刷新定时器
|
||||
// 使用数据刷新定时器
|
||||
useDataRefresh(
|
||||
'scheduler-list',
|
||||
loadSchedulerList,
|
||||
|
||||
Reference in New Issue
Block a user