mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-30 21:00:43 +08:00
Optimize PWA background performance with SSE and timer management
Co-authored-by: jxxghp <jxxghp@163.com>
This commit is contained in:
51
src/App.vue
51
src/App.vue
@@ -9,6 +9,7 @@ import { SupportedLocale } from '@/types/i18n'
|
||||
import { checkAndEmitUnreadMessages } from '@/utils/badge'
|
||||
import { preloadImage } from './@core/utils/image'
|
||||
import { globalLoadingStateManager } from '@/utils/loadingStateManager'
|
||||
import { addBackgroundTimer, removeBackgroundTimer } from '@/utils/backgroundManager'
|
||||
|
||||
// 生效主题
|
||||
const { global: globalTheme } = useTheme()
|
||||
@@ -34,7 +35,6 @@ const loginStateKey = computed(() => (isLogin.value ? 'logged-in' : 'logged-out'
|
||||
const backgroundImages = ref<string[]>([])
|
||||
const activeImageIndex = ref(0)
|
||||
const isTransparentTheme = computed(() => globalTheme.name.value === 'transparent')
|
||||
let backgroundRotationTimer: NodeJS.Timeout | null = null
|
||||
|
||||
|
||||
|
||||
@@ -102,23 +102,37 @@ async function fetchBackgroundImages() {
|
||||
}
|
||||
}
|
||||
|
||||
// 背景图片轮换函数
|
||||
function rotateBackgroundImage() {
|
||||
if (backgroundImages.value.length > 1) {
|
||||
// 计算下一个图片索引
|
||||
const nextIndex = (activeImageIndex.value + 1) % backgroundImages.value.length
|
||||
// 预加载下一张图片
|
||||
preloadImage(backgroundImages.value[nextIndex]).then(success => {
|
||||
// 只有图片成功加载才切换
|
||||
if (success) {
|
||||
activeImageIndex.value = nextIndex
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 开始背景图片轮换
|
||||
function startBackgroundRotation() {
|
||||
// 清除轮换定时器
|
||||
if (backgroundRotationTimer) clearInterval(backgroundRotationTimer)
|
||||
// 清除现有定时器
|
||||
removeBackgroundTimer('background-rotation')
|
||||
|
||||
if (backgroundImages.value.length > 1) {
|
||||
// 每10秒切换一次
|
||||
backgroundRotationTimer = setInterval(() => {
|
||||
// 计算下一个图片索引
|
||||
const nextIndex = (activeImageIndex.value + 1) % backgroundImages.value.length
|
||||
// 预加载下一张图片
|
||||
preloadImage(backgroundImages.value[nextIndex]).then(success => {
|
||||
// 只有图片成功加载才切换
|
||||
if (success) {
|
||||
activeImageIndex.value = nextIndex
|
||||
}
|
||||
})
|
||||
}, 10000)
|
||||
// 使用优化的定时器管理器,后台时自动暂停
|
||||
addBackgroundTimer(
|
||||
'background-rotation',
|
||||
rotateBackgroundImage,
|
||||
10000, // 每10秒切换一次
|
||||
{
|
||||
runInBackground: false, // 后台时不运行
|
||||
skipInitialRun: true // 不需要立即执行
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,11 +234,8 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清除轮换定时器
|
||||
if (backgroundRotationTimer) {
|
||||
clearInterval(backgroundRotationTimer)
|
||||
backgroundRotationTimer = null
|
||||
}
|
||||
// 清除背景轮换定时器
|
||||
removeBackgroundTimer('background-rotation')
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
210
src/composables/useBackgroundOptimization.ts
Normal file
210
src/composables/useBackgroundOptimization.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { onMounted, onUnmounted, ref, type Ref } from 'vue'
|
||||
import { sseManagerSingleton } from '@/utils/sseManager'
|
||||
import { addBackgroundTimer, removeBackgroundTimer } from '@/utils/backgroundManager'
|
||||
|
||||
/**
|
||||
* 后台优化组合函数
|
||||
* 统一管理SSE连接和定时器,优化iOS后台性能
|
||||
*/
|
||||
export function useBackgroundOptimization() {
|
||||
|
||||
/**
|
||||
* 使用优化的SSE连接
|
||||
* @param url SSE连接地址
|
||||
* @param messageHandler 消息处理函数
|
||||
* @param listenerId 监听器ID(用于区分不同的监听器)
|
||||
* @param options 选项
|
||||
*/
|
||||
const useSSE = (
|
||||
url: string,
|
||||
messageHandler: (event: MessageEvent) => void,
|
||||
listenerId: string,
|
||||
options?: {
|
||||
backgroundCloseDelay?: number
|
||||
reconnectDelay?: number
|
||||
maxReconnectAttempts?: number
|
||||
}
|
||||
) => {
|
||||
const manager = sseManagerSingleton.getManager(url, options)
|
||||
|
||||
onMounted(() => {
|
||||
manager.addMessageListener(listenerId, messageHandler)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
manager.removeMessageListener(listenerId)
|
||||
})
|
||||
|
||||
return {
|
||||
manager,
|
||||
readyState: () => manager.readyState,
|
||||
close: () => manager.removeMessageListener(listenerId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用优化的定时器
|
||||
* @param id 定时器ID
|
||||
* @param callback 回调函数
|
||||
* @param interval 间隔时间(毫秒)
|
||||
* @param options 选项
|
||||
*/
|
||||
const useTimer = (
|
||||
id: string,
|
||||
callback: () => void,
|
||||
interval: number,
|
||||
options?: {
|
||||
runInBackground?: boolean
|
||||
skipInitialRun?: boolean
|
||||
}
|
||||
) => {
|
||||
onMounted(() => {
|
||||
addBackgroundTimer(id, callback, interval, options)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
removeBackgroundTimer(id)
|
||||
})
|
||||
|
||||
return {
|
||||
remove: () => removeBackgroundTimer(id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用延迟SSE连接(类似原来的setTimeout延迟)
|
||||
* @param url SSE连接地址
|
||||
* @param messageHandler 消息处理函数
|
||||
* @param listenerId 监听器ID
|
||||
* @param delay 延迟时间(毫秒)
|
||||
* @param options SSE选项
|
||||
*/
|
||||
const useDelayedSSE = (
|
||||
url: string,
|
||||
messageHandler: (event: MessageEvent) => void,
|
||||
listenerId: string,
|
||||
delay: number = 3000,
|
||||
options?: Parameters<typeof useSSE>[3]
|
||||
) => {
|
||||
const manager = sseManagerSingleton.getManager(url, options)
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
manager.addMessageListener(listenerId, messageHandler)
|
||||
}, delay)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
manager.removeMessageListener(listenerId)
|
||||
})
|
||||
|
||||
return {
|
||||
manager,
|
||||
readyState: () => manager.readyState,
|
||||
close: () => manager.removeMessageListener(listenerId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用进度SSE连接(用于进度监听)
|
||||
* @param url SSE连接地址
|
||||
* @param messageHandler 消息处理函数
|
||||
* @param listenerId 监听器ID
|
||||
* @param isActive 是否激活的响应式变量
|
||||
*/
|
||||
const useProgressSSE = (
|
||||
url: string,
|
||||
messageHandler: (event: MessageEvent) => void,
|
||||
listenerId: string,
|
||||
isActive: Ref<boolean>
|
||||
) => {
|
||||
const manager = sseManagerSingleton.getManager(url, {
|
||||
backgroundCloseDelay: 1000, // 进度SSE更快关闭
|
||||
reconnectDelay: 1000,
|
||||
maxReconnectAttempts: 5
|
||||
})
|
||||
|
||||
const startProgress = () => {
|
||||
if (isActive.value) {
|
||||
manager.addMessageListener(listenerId, messageHandler)
|
||||
}
|
||||
}
|
||||
|
||||
const stopProgress = () => {
|
||||
manager.removeMessageListener(listenerId)
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
stopProgress()
|
||||
})
|
||||
|
||||
return {
|
||||
start: startProgress,
|
||||
stop: stopProgress,
|
||||
manager
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用数据刷新定时器(用于仪表盘等数据刷新)
|
||||
* @param id 定时器ID
|
||||
* @param loadDataFunc 加载数据函数
|
||||
* @param interval 刷新间隔(毫秒)
|
||||
* @param immediate 是否立即执行
|
||||
*/
|
||||
const useDataRefresh = (
|
||||
id: string,
|
||||
loadDataFunc: () => Promise<void> | void,
|
||||
interval: number = 3000,
|
||||
immediate: boolean = true
|
||||
) => {
|
||||
const loading = ref(false)
|
||||
|
||||
const wrappedLoadData = async () => {
|
||||
if (loading.value) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
await loadDataFunc()
|
||||
} catch (error) {
|
||||
console.error(`数据刷新失败 [${id}]:`, error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (immediate) {
|
||||
await wrappedLoadData()
|
||||
}
|
||||
|
||||
addBackgroundTimer(
|
||||
id,
|
||||
wrappedLoadData,
|
||||
interval,
|
||||
{
|
||||
runInBackground: false, // 后台不刷新数据
|
||||
skipInitialRun: true // 已经手动执行过了
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
removeBackgroundTimer(id)
|
||||
})
|
||||
|
||||
return {
|
||||
loading,
|
||||
refresh: wrappedLoadData,
|
||||
stop: () => removeBackgroundTimer(id)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
useSSE,
|
||||
useTimer,
|
||||
useDelayedSSE,
|
||||
useProgressSSE,
|
||||
useDataRefresh
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@
|
||||
import { formatDateDifference } from '@core/utils/formatters'
|
||||
import { SystemNotification } from '@/api/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { useDelayedSSE } = useBackgroundOptimization()
|
||||
|
||||
// 是否有新消息
|
||||
const hasNewMessage = ref(false)
|
||||
@@ -11,9 +13,6 @@ const hasNewMessage = ref(false)
|
||||
// 通知列表
|
||||
const notificationList = ref<SystemNotification[]>([])
|
||||
|
||||
// 事件源
|
||||
let eventSource: EventSource | null = null
|
||||
|
||||
// 弹窗
|
||||
const appsMenu = ref(false)
|
||||
|
||||
@@ -27,30 +26,27 @@ function markAllAsRead() {
|
||||
appsMenu.value = false
|
||||
}
|
||||
|
||||
// SSE持续接收消息
|
||||
function startSSEMessager() {
|
||||
// 延迟 3 秒启动 SSE,避免相关认证信息尚未写入 Cookie 导致 403
|
||||
setTimeout(() => {
|
||||
eventSource = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/message`)
|
||||
eventSource.addEventListener('message', event => {
|
||||
if (event.data) {
|
||||
const noti: SystemNotification = JSON.parse(event.data)
|
||||
notificationList.value.unshift(noti)
|
||||
hasNewMessage.value = true
|
||||
}
|
||||
})
|
||||
}, 3000)
|
||||
// 消息处理函数
|
||||
function handleMessage(event: MessageEvent) {
|
||||
if (event.data) {
|
||||
const noti: SystemNotification = JSON.parse(event.data)
|
||||
notificationList.value.unshift(noti)
|
||||
hasNewMessage.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时,加载当前用户数据
|
||||
onBeforeMount(async () => {
|
||||
startSSEMessager()
|
||||
})
|
||||
|
||||
// 页面卸载时,关闭事件源
|
||||
onBeforeUnmount(() => {
|
||||
if (eventSource) eventSource.close()
|
||||
})
|
||||
// 使用优化的SSE连接,延迟3秒启动,避免认证问题
|
||||
useDelayedSSE(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/message`,
|
||||
handleMessage,
|
||||
'user-notification',
|
||||
3000,
|
||||
{
|
||||
backgroundCloseDelay: 5000,
|
||||
reconnectDelay: 3000,
|
||||
maxReconnectAttempts: 3
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
26
src/main.ts
26
src/main.ts
@@ -43,8 +43,10 @@ import HeaderTab from './layouts/components/HeaderTab.vue'
|
||||
// 7. 样式文件 - 合并为单一导入
|
||||
import '@/styles/main.scss'
|
||||
|
||||
// 8. PWA状态管理
|
||||
// 8. PWA状态管理和后台优化
|
||||
import { PWAStateController } from '@/utils/pwaStateManager'
|
||||
import { backgroundManager } from '@/utils/backgroundManager'
|
||||
import { sseManagerSingleton } from '@/utils/sseManager'
|
||||
|
||||
// PWA状态管理器初始化函数
|
||||
const initializePWABeforeMount = async () => {
|
||||
@@ -137,5 +139,27 @@ if (pwaStateController) {
|
||||
})
|
||||
}
|
||||
|
||||
// 6. 初始化后台优化工具
|
||||
console.log('初始化后台优化工具...')
|
||||
|
||||
// 将后台管理器绑定到全局对象(便于调试)
|
||||
if (import.meta.env.MODE === 'development') {
|
||||
;(window as any).backgroundManager = backgroundManager
|
||||
;(window as any).sseManagerSingleton = sseManagerSingleton
|
||||
|
||||
// 添加全局调试函数
|
||||
;(window as any).debugBackground = () => {
|
||||
console.table(backgroundManager.getTimersInfo())
|
||||
console.log('Background Status:', backgroundManager.getStatus())
|
||||
}
|
||||
}
|
||||
|
||||
// 页面卸载时清理后台管理器
|
||||
window.addEventListener('beforeunload', () => {
|
||||
console.log('应用卸载,清理后台资源...')
|
||||
backgroundManager.destroy()
|
||||
sseManagerSingleton.closeAllManagers()
|
||||
})
|
||||
|
||||
// 导出状态管理器供其他模块使用
|
||||
export { pwaStateController }
|
||||
|
||||
@@ -6,9 +6,11 @@ import type { Context } from '@/api/types'
|
||||
import TorrentCardListView from '@/views/torrent/TorrentCardListView.vue'
|
||||
import TorrentRowListView from '@/views/torrent/TorrentRowListView.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
const { useProgressSSE } = useBackgroundOptimization()
|
||||
|
||||
// 路由参数
|
||||
const route = useRoute()
|
||||
@@ -55,8 +57,8 @@ const progressValue = ref(0)
|
||||
// 进度是否有效
|
||||
const progressEnabled = ref(false)
|
||||
|
||||
// 加载进度SSE
|
||||
const progressEventSource = ref<EventSource>()
|
||||
// 进度是否激活
|
||||
const progressActive = ref(false)
|
||||
|
||||
// 错误标题
|
||||
const errorTitle = ref(t('resource.noData'))
|
||||
@@ -68,51 +70,53 @@ const errorDescription = ref(t('resource.noResourceFound'))
|
||||
const watchProgressValue = watch(
|
||||
progressValue,
|
||||
debounce(async () => {
|
||||
if (progressEventSource.value && progressValue.value < 100) {
|
||||
if (progressActive.value && progressValue.value < 100) {
|
||||
console.warn('卡进度超时 关闭进度条')
|
||||
stopLoadingProgress()
|
||||
}
|
||||
}, 60_000),
|
||||
)
|
||||
|
||||
// 进度SSE消息处理函数
|
||||
function handleProgressMessage(event: MessageEvent) {
|
||||
const progress = JSON.parse(event.data)
|
||||
if (progress) {
|
||||
progressText.value = progress.text
|
||||
progressValue.value = progress.value
|
||||
progressEnabled.value = progress.enable
|
||||
}
|
||||
}
|
||||
|
||||
// 使用优化的进度SSE连接
|
||||
const progressSSE = useProgressSSE(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/progress/search`,
|
||||
handleProgressMessage,
|
||||
'resource-search-progress',
|
||||
progressActive
|
||||
)
|
||||
|
||||
// 使用SSE监听加载进度
|
||||
function startLoadingProgress() {
|
||||
watchProgressValue.resume()
|
||||
progressText.value = t('resource.searching')
|
||||
progressValue.value = 0
|
||||
progressEnabled.value = false
|
||||
progressEventSource.value = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/progress/search`)
|
||||
progressEventSource.value.onmessage = event => {
|
||||
const progress = JSON.parse(event.data)
|
||||
if (progress) {
|
||||
progressText.value = progress.text
|
||||
progressValue.value = progress.value
|
||||
progressEnabled.value = progress.enable
|
||||
}
|
||||
}
|
||||
|
||||
// 添加错误处理
|
||||
progressEventSource.value.onerror = () => {
|
||||
setTimeout(() => {
|
||||
stopLoadingProgress()
|
||||
}, 1000)
|
||||
}
|
||||
progressActive.value = true
|
||||
progressSSE.start()
|
||||
}
|
||||
|
||||
// 停止监听加载进度
|
||||
function stopLoadingProgress() {
|
||||
watchProgressValue.pause()
|
||||
if (progressEventSource.value) {
|
||||
progressEventSource.value.close()
|
||||
progressEventSource.value = undefined
|
||||
progressActive.value = false
|
||||
progressSSE.stop()
|
||||
|
||||
// 确保进度显示100%,然后再渐进清零
|
||||
progressValue.value = 100
|
||||
setTimeout(() => {
|
||||
progressValue.value = 0
|
||||
progressEnabled.value = false
|
||||
}, 1500) // 延长到1.5秒,让用户有足够时间看到完成状态
|
||||
}
|
||||
// 确保进度显示100%,然后再渐进清零
|
||||
progressValue.value = 100
|
||||
setTimeout(() => {
|
||||
progressValue.value = 0
|
||||
progressEnabled.value = false
|
||||
}, 1500) // 延长到1.5秒,让用户有足够时间看到完成状态
|
||||
}
|
||||
|
||||
// 设置视图类型
|
||||
|
||||
@@ -594,12 +594,22 @@ export class PWAStateController {
|
||||
}
|
||||
|
||||
private setupPeriodicSave(): void {
|
||||
// 每30秒保存一次状态
|
||||
setInterval(() => {
|
||||
if (!document.hidden) {
|
||||
this.saveCurrentState()
|
||||
}
|
||||
}, 30000)
|
||||
// 导入后台管理器
|
||||
import('@/utils/backgroundManager').then(({ addBackgroundTimer }) => {
|
||||
// 使用后台管理器,延长间隔
|
||||
addBackgroundTimer(
|
||||
'pwa-state-save',
|
||||
() => {
|
||||
// 只在前台时保存状态(由后台管理器自动处理)
|
||||
this.saveCurrentState()
|
||||
},
|
||||
60000, // 改为60秒,减少频率
|
||||
{
|
||||
runInBackground: false, // 后台时不保存
|
||||
skipInitialRun: true
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private getAppSpecificState(): any {
|
||||
|
||||
@@ -3,9 +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'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
const { useDataRefresh } = useBackgroundOptimization()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -29,9 +31,6 @@ const variableTheme = controlledComputed(
|
||||
|
||||
const chartKey = ref(0)
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 时间序列
|
||||
const series = ref([
|
||||
{
|
||||
@@ -107,7 +106,7 @@ const chartOptions = controlledComputed(
|
||||
)
|
||||
|
||||
// 调用API接口获取最新CPU使用率
|
||||
async function getCpuUsage() {
|
||||
async function loadCpuData() {
|
||||
if (!props.allowRefresh) return
|
||||
try {
|
||||
// 请求数据
|
||||
@@ -123,23 +122,13 @@ async function getCpuUsage() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 延迟启动,确保组件完全挂载
|
||||
nextTick(() => {
|
||||
getCpuUsage()
|
||||
refreshTimer = setInterval(() => {
|
||||
getCpuUsage()
|
||||
}, 2000)
|
||||
})
|
||||
})
|
||||
|
||||
// 组件卸载时停止定时器
|
||||
onUnmounted(() => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
refreshTimer = null
|
||||
}
|
||||
})
|
||||
// 使用优化的数据刷新定时器
|
||||
const { loading } = useDataRefresh(
|
||||
'analytics-cpu',
|
||||
loadCpuData,
|
||||
2000, // 2秒间隔
|
||||
true // 立即执行
|
||||
)
|
||||
|
||||
onActivated(() => {
|
||||
nextTick(() => {
|
||||
|
||||
@@ -4,9 +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'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
const { useDataRefresh } = useBackgroundOptimization()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -30,9 +32,6 @@ const variableTheme = controlledComputed(
|
||||
|
||||
const chartKey = ref(0)
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 时间序列
|
||||
const series = ref([
|
||||
{
|
||||
@@ -113,7 +112,7 @@ const chartOptions = controlledComputed(
|
||||
)
|
||||
|
||||
// 调用API接口获取最新内存使用量
|
||||
async function getMemorgUsage() {
|
||||
async function loadMemoryData() {
|
||||
if (!props.allowRefresh) return
|
||||
try {
|
||||
// 请求数据
|
||||
@@ -128,24 +127,13 @@ async function getMemorgUsage() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 延迟启动,确保组件完全挂载
|
||||
nextTick(() => {
|
||||
getMemorgUsage()
|
||||
// 启动定时器
|
||||
refreshTimer = setInterval(() => {
|
||||
getMemorgUsage()
|
||||
}, 3000)
|
||||
})
|
||||
})
|
||||
|
||||
// 组件卸载时停止定时器
|
||||
onUnmounted(() => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
refreshTimer = null
|
||||
}
|
||||
})
|
||||
// 使用优化的数据刷新定时器
|
||||
const { loading } = useDataRefresh(
|
||||
'analytics-memory',
|
||||
loadMemoryData,
|
||||
3000, // 3秒间隔
|
||||
true // 立即执行
|
||||
)
|
||||
|
||||
onActivated(() => {
|
||||
// 使用nextTick确保DOM准备完成后再更新chartKey
|
||||
|
||||
@@ -3,9 +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'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
const { useDataRefresh } = useBackgroundOptimization()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -16,9 +18,6 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 下载器信息
|
||||
const downloadInfo = ref<DownloaderInfo>({
|
||||
// 下载速度
|
||||
@@ -78,22 +77,13 @@ async function loadDownloaderInfo() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDownloaderInfo()
|
||||
|
||||
// 启动定时器
|
||||
refreshTimer = setInterval(() => {
|
||||
loadDownloaderInfo()
|
||||
}, 3000)
|
||||
})
|
||||
|
||||
// 组件卸载时停止定时器
|
||||
onUnmounted(() => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
refreshTimer = null
|
||||
}
|
||||
})
|
||||
// 使用优化的数据刷新定时器
|
||||
const { loading } = useDataRefresh(
|
||||
'analytics-speed',
|
||||
loadDownloaderInfo,
|
||||
3000, // 3秒间隔
|
||||
true // 立即执行
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { isToday } from '@/@core/utils/index'
|
||||
import dayjs from 'dayjs';
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
|
||||
// 定义输入变量
|
||||
const props = defineProps<{
|
||||
@@ -10,6 +11,7 @@ const props = defineProps<{
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
const { useSSE } = useBackgroundOptimization()
|
||||
|
||||
// 已解析的日志列表
|
||||
const parsedLogs = ref<{ level: string; date: string; time: string; program: string; content: string }[]>([])
|
||||
@@ -22,9 +24,6 @@ const headers = [
|
||||
{ title: t('logging.content'), value: 'content' },
|
||||
]
|
||||
|
||||
// SSE消息对象
|
||||
let eventSource: EventSource | null = null
|
||||
|
||||
// 日志颜色映射表
|
||||
const logColorMap: Record<string, string> = {
|
||||
DEBUG: 'secondary',
|
||||
@@ -38,55 +37,54 @@ function getLogColor(level: string): string {
|
||||
return logColorMap[level] || 'secondary'
|
||||
}
|
||||
|
||||
// SSE持续获取日志
|
||||
function startSSELogging() {
|
||||
console.log(props.logfile)
|
||||
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
|
||||
// 日志缓冲区和超时处理
|
||||
const buffer: string[] = []
|
||||
let timeoutId: number | null = null
|
||||
|
||||
eventSource.addEventListener('message', event => {
|
||||
const message = event.data
|
||||
if (message) {
|
||||
buffer.push(message)
|
||||
if (!timeoutId) {
|
||||
timeoutId = window.setTimeout(() => {
|
||||
// 解析新日志
|
||||
const newParsedLogs = buffer
|
||||
.map(log => {
|
||||
const logPattern = /^【(.*?)】\s*([\d]{4}-\d{2}-\d{2}(?:\s+\d{2}:\d{2})?)\s+(.*?)\s*-\s*(.*?)\s*-\s*(.*)$/
|
||||
const matches = log.match(logPattern)
|
||||
if (matches) {
|
||||
const [, level, date, time, program, content] = matches
|
||||
return { level, date, time, program, content }
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter(Boolean)
|
||||
// 倒序后插入parsedLogs顶部
|
||||
parsedLogs.value.unshift(...(newParsedLogs.reverse() as any[]))
|
||||
// 保留最新的200条日志
|
||||
parsedLogs.value = parsedLogs.value.slice(0, 200)
|
||||
// 重置buffer
|
||||
buffer.length = 0
|
||||
timeoutId = null
|
||||
}, 100)
|
||||
}
|
||||
// SSE消息处理函数
|
||||
function handleSSEMessage(event: MessageEvent) {
|
||||
const message = event.data
|
||||
if (message) {
|
||||
buffer.push(message)
|
||||
if (!timeoutId) {
|
||||
timeoutId = window.setTimeout(() => {
|
||||
// 解析新日志
|
||||
const newParsedLogs = buffer
|
||||
.map(log => {
|
||||
const logPattern = /^【(.*?)】\s*([\d]{4}-\d{2}-\d{2}(?:\s+\d{2}:\d{2})?)\s+(.*?)\s*-\s*(.*?)\s*-\s*(.*)$/
|
||||
const matches = log.match(logPattern)
|
||||
if (matches) {
|
||||
const [, level, date, time, program, content] = matches
|
||||
return { level, date, time, program, content }
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter(Boolean)
|
||||
// 倒序后插入parsedLogs顶部
|
||||
parsedLogs.value.unshift(...(newParsedLogs.reverse() as any[]))
|
||||
// 保留最新的200条日志
|
||||
parsedLogs.value = parsedLogs.value.slice(0, 200)
|
||||
// 重置buffer
|
||||
buffer.length = 0
|
||||
timeoutId = null
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
startSSELogging()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (eventSource) eventSource.close()
|
||||
})
|
||||
// 使用优化的SSE连接
|
||||
useSSE(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/logging?logfile=${
|
||||
encodeURIComponent(props.logfile) ?? 'moviepilot.log'
|
||||
}`,
|
||||
handleSSEMessage,
|
||||
`logging-${props.logfile}`,
|
||||
{
|
||||
backgroundCloseDelay: 5000,
|
||||
reconnectDelay: 3000,
|
||||
maxReconnectAttempts: 3
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -3,9 +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'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
const { useSSE } = useBackgroundOptimization()
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['scroll'])
|
||||
@@ -27,25 +29,31 @@ const page = ref(1)
|
||||
// 存量消息最新时间
|
||||
const lastTime = ref('')
|
||||
|
||||
// SSE消息对象
|
||||
let eventSource: EventSource | null = null
|
||||
|
||||
// SSE持续获取消息
|
||||
function startSSEMessager() {
|
||||
eventSource = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/message?role=user`)
|
||||
eventSource.addEventListener('message', event => {
|
||||
const message = event.data
|
||||
if (message) {
|
||||
const object = JSON.parse(message)
|
||||
if (compareTime(object.date, lastTime.value) <= 0) return
|
||||
messages.value.push(object)
|
||||
nextTick(() => {
|
||||
emit('scroll') // 新消息到达时触发智能滚动
|
||||
})
|
||||
}
|
||||
})
|
||||
// SSE消息处理函数
|
||||
function handleSSEMessage(event: MessageEvent) {
|
||||
const message = event.data
|
||||
if (message) {
|
||||
const object = JSON.parse(message)
|
||||
if (compareTime(object.date, lastTime.value) <= 0) return
|
||||
messages.value.push(object)
|
||||
nextTick(() => {
|
||||
emit('scroll') // 新消息到达时触发智能滚动
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 使用优化的SSE连接
|
||||
const sseConnection = useSSE(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/message?role=user`,
|
||||
handleSSEMessage,
|
||||
'message-view',
|
||||
{
|
||||
backgroundCloseDelay: 5000,
|
||||
reconnectDelay: 3000,
|
||||
maxReconnectAttempts: 3
|
||||
}
|
||||
)
|
||||
|
||||
// 调用API加载存量消息
|
||||
async function loadMessages({ done }: { done: any }) {
|
||||
// 如果正在加载中,直接返回
|
||||
@@ -85,8 +93,6 @@ async function loadMessages({ done }: { done: any }) {
|
||||
}
|
||||
// 取消加载中
|
||||
loading.value = false
|
||||
// 监听SSE消息
|
||||
startSSEMessager()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
@@ -110,10 +116,6 @@ onMounted(() => {
|
||||
emit('scroll')
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (eventSource) eventSource.close()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
Reference in New Issue
Block a user