mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-06 16:16:42 +08:00
Merge pull request #360 from jxxghp/cursor/analyze-factors-causing-ios-to-kill-pwa-ac82
Analyze factors causing iOS to kill PWA
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "moviepilot",
|
"name": "moviepilot",
|
||||||
"version": "2.6.2",
|
"version": "2.6.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": "dist/service.js",
|
"bin": "dist/service.js",
|
||||||
|
|||||||
+31
-20
@@ -9,6 +9,7 @@ import { SupportedLocale } from '@/types/i18n'
|
|||||||
import { checkAndEmitUnreadMessages } from '@/utils/badge'
|
import { checkAndEmitUnreadMessages } from '@/utils/badge'
|
||||||
import { preloadImage } from './@core/utils/image'
|
import { preloadImage } from './@core/utils/image'
|
||||||
import { globalLoadingStateManager } from '@/utils/loadingStateManager'
|
import { globalLoadingStateManager } from '@/utils/loadingStateManager'
|
||||||
|
import { addBackgroundTimer, removeBackgroundTimer } from '@/utils/backgroundManager'
|
||||||
|
|
||||||
// 生效主题
|
// 生效主题
|
||||||
const { global: globalTheme } = useTheme()
|
const { global: globalTheme } = useTheme()
|
||||||
@@ -34,7 +35,6 @@ const loginStateKey = computed(() => (isLogin.value ? 'logged-in' : 'logged-out'
|
|||||||
const backgroundImages = ref<string[]>([])
|
const backgroundImages = ref<string[]>([])
|
||||||
const activeImageIndex = ref(0)
|
const activeImageIndex = ref(0)
|
||||||
const isTransparentTheme = computed(() => globalTheme.name.value === 'transparent')
|
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() {
|
function startBackgroundRotation() {
|
||||||
// 清除轮换定时器
|
// 清除现有定时器
|
||||||
if (backgroundRotationTimer) clearInterval(backgroundRotationTimer)
|
removeBackgroundTimer('background-rotation')
|
||||||
|
|
||||||
if (backgroundImages.value.length > 1) {
|
if (backgroundImages.value.length > 1) {
|
||||||
// 每10秒切换一次
|
// 使用优化的定时器管理器,后台时自动暂停
|
||||||
backgroundRotationTimer = setInterval(() => {
|
addBackgroundTimer(
|
||||||
// 计算下一个图片索引
|
'background-rotation',
|
||||||
const nextIndex = (activeImageIndex.value + 1) % backgroundImages.value.length
|
rotateBackgroundImage,
|
||||||
// 预加载下一张图片
|
10000, // 每10秒切换一次
|
||||||
preloadImage(backgroundImages.value[nextIndex]).then(success => {
|
{
|
||||||
// 只有图片成功加载才切换
|
runInBackground: false, // 后台时不运行
|
||||||
if (success) {
|
skipInitialRun: true // 不需要立即执行
|
||||||
activeImageIndex.value = nextIndex
|
}
|
||||||
}
|
)
|
||||||
})
|
|
||||||
}, 10000)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,11 +234,8 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
// 清除轮换定时器
|
// 清除背景轮换定时器
|
||||||
if (backgroundRotationTimer) {
|
removeBackgroundTimer('background-rotation')
|
||||||
clearInterval(backgroundRotationTimer)
|
|
||||||
backgroundRotationTimer = null
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ import { cloneDeep } from 'lodash-es'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { downloaderDict } from '@/api/constants'
|
import { downloaderDict } from '@/api/constants'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
|
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||||
|
|
||||||
// 显示器宽度
|
// 显示器宽度
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
|
|
||||||
// 获取i18n实例
|
// 获取i18n实例
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { useConditionalDataRefresh } = useBackgroundOptimization()
|
||||||
|
|
||||||
// 定义输入
|
// 定义输入
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -43,9 +45,6 @@ const emit = defineEmits(['close', 'done', 'change'])
|
|||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|
||||||
// timeout定时器
|
|
||||||
let timeoutTimer: NodeJS.Timeout | undefined = undefined
|
|
||||||
|
|
||||||
// 上传速率
|
// 上传速率
|
||||||
const upload_rate = ref(0)
|
const upload_rate = ref(0)
|
||||||
|
|
||||||
@@ -64,9 +63,15 @@ const downloaderInfo = ref<DownloaderConf>({
|
|||||||
config: {},
|
config: {},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 下载器是否应该刷新数据的计算属性
|
||||||
|
const shouldRefresh = computed(() => props.allowRefresh && props.downloader.enabled)
|
||||||
|
|
||||||
// 调用API查询下载器数据
|
// 调用API查询下载器数据
|
||||||
async function loadDownloaderInfo() {
|
async function loadDownloaderInfo() {
|
||||||
if (!props.allowRefresh) {
|
if (!shouldRefresh.value) {
|
||||||
|
// 当下载器被禁用时,重置速率数据
|
||||||
|
upload_rate.value = 0
|
||||||
|
download_rate.value = 0
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -79,11 +84,6 @@ async function loadDownloaderInfo() {
|
|||||||
if (res) {
|
if (res) {
|
||||||
upload_rate.value = res.upload_speed
|
upload_rate.value = res.upload_speed
|
||||||
download_rate.value = res.download_speed
|
download_rate.value = res.download_speed
|
||||||
// 定时查询
|
|
||||||
clearTimeout(timeoutTimer)
|
|
||||||
if (props.downloader.enabled) {
|
|
||||||
timeoutTimer = setTimeout(loadDownloaderInfo, 3000)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
@@ -141,14 +141,17 @@ function onClose() {
|
|||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
// 使用条件性数据刷新定时器(只在下载器启用时运行)
|
||||||
if (props.downloader.enabled) {
|
const { stop: stopRefresh } = useConditionalDataRefresh(
|
||||||
await loadDownloaderInfo()
|
`downloader-${props.downloader.name}`,
|
||||||
}
|
loadDownloaderInfo,
|
||||||
})
|
shouldRefresh, // 响应式条件:只有当allowRefresh为true且downloader启用时才运行
|
||||||
|
3000, // 3秒间隔
|
||||||
|
true // 立即执行一次
|
||||||
|
)
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (timeoutTimer) clearTimeout(timeoutTimer)
|
stopRefresh()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import ProgressDialog from './ProgressDialog.vue'
|
|||||||
import { FileItem, StorageConf, TransferDirectoryConf, TransferForm } from '@/api/types'
|
import { FileItem, StorageConf, TransferDirectoryConf, TransferForm } from '@/api/types'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useGlobalSettingsStore } from '@/stores'
|
import { useGlobalSettingsStore } from '@/stores'
|
||||||
|
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { useProgressSSE } = useBackgroundOptimization()
|
||||||
|
|
||||||
// 显示器宽度
|
// 显示器宽度
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
@@ -49,8 +51,8 @@ const $toast = useToast()
|
|||||||
// TMDB选择对话框
|
// TMDB选择对话框
|
||||||
const mediaSelectorDialog = ref(false)
|
const mediaSelectorDialog = ref(false)
|
||||||
|
|
||||||
// 加载进度SSE
|
// 进度是否激活
|
||||||
const progressEventSource = ref<EventSource>()
|
const progressActive = ref(false)
|
||||||
|
|
||||||
// 整理进度条
|
// 整理进度条
|
||||||
const progressDialog = ref(false)
|
const progressDialog = ref(false)
|
||||||
@@ -189,34 +191,34 @@ async function handleTransferLog(logid: number, background: boolean = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 进度SSE消息处理函数
|
||||||
|
function handleProgressMessage(event: MessageEvent) {
|
||||||
|
const progress = JSON.parse(event.data)
|
||||||
|
if (progress) {
|
||||||
|
progressText.value = progress.text
|
||||||
|
progressValue.value = progress.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用优化的进度SSE连接
|
||||||
|
const progressSSE = useProgressSSE(
|
||||||
|
`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer`,
|
||||||
|
handleProgressMessage,
|
||||||
|
'reorganize-progress',
|
||||||
|
progressActive
|
||||||
|
)
|
||||||
|
|
||||||
// 使用SSE监听加载进度
|
// 使用SSE监听加载进度
|
||||||
function startLoadingProgress() {
|
function startLoadingProgress() {
|
||||||
// 在创建新连接之前,先确保任何可能存在的旧连接都被关闭了,防止因快速重复点击而产生孤儿连接。
|
|
||||||
if (progressEventSource.value) {
|
|
||||||
progressEventSource.value.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
progressText.value = t('dialog.reorganize.processing')
|
progressText.value = t('dialog.reorganize.processing')
|
||||||
progressEventSource.value = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer`)
|
progressActive.value = true
|
||||||
progressEventSource.value.onmessage = event => {
|
progressSSE.start()
|
||||||
const progress = JSON.parse(event.data)
|
|
||||||
if (progress) {
|
|
||||||
progressText.value = progress.text
|
|
||||||
progressValue.value = progress.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发生错误时,也确保连接被关闭,避免重试等意外行为
|
|
||||||
progressEventSource.value.onerror = () => {
|
|
||||||
if (progressEventSource.value) {
|
|
||||||
progressEventSource.value.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止监听加载进度
|
// 停止监听加载进度
|
||||||
function stopLoadingProgress() {
|
function stopLoadingProgress() {
|
||||||
progressEventSource.value?.close()
|
progressActive.value = false
|
||||||
|
progressSSE.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 整理文件
|
// 整理文件
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import api from '@/api'
|
|||||||
import { FileItem, TransferQueue } from '@/api/types'
|
import { FileItem, TransferQueue } from '@/api/types'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||||
|
|
||||||
// 多语言支持
|
// 多语言支持
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { useProgressSSE } = useBackgroundOptimization()
|
||||||
|
|
||||||
// 显示器宽度
|
// 显示器宽度
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
@@ -16,9 +18,6 @@ const emit = defineEmits(['close'])
|
|||||||
// 数据列表
|
// 数据列表
|
||||||
const dataList = ref<TransferQueue[]>([])
|
const dataList = ref<TransferQueue[]>([])
|
||||||
|
|
||||||
// 加载进度SSE
|
|
||||||
const progressEventSource = ref<EventSource>()
|
|
||||||
|
|
||||||
// 整理进度文本
|
// 整理进度文本
|
||||||
const progressText = ref(t('dialog.transferQueue.processing'))
|
const progressText = ref(t('dialog.transferQueue.processing'))
|
||||||
|
|
||||||
@@ -28,6 +27,9 @@ const progressValue = ref(0)
|
|||||||
// 数据可刷新标志
|
// 数据可刷新标志
|
||||||
const refreshFlag = ref(false)
|
const refreshFlag = ref(false)
|
||||||
|
|
||||||
|
// 进度是否激活
|
||||||
|
const progressActive = ref(false)
|
||||||
|
|
||||||
// 活动标签
|
// 活动标签
|
||||||
const activeTab = ref('')
|
const activeTab = ref('')
|
||||||
|
|
||||||
@@ -91,42 +93,54 @@ async function remove_queue_task(fileitem: FileItem) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用SSE监听加载进度
|
// 进度SSE消息处理函数
|
||||||
function startLoadingProgress() {
|
function handleProgressMessage(event: MessageEvent) {
|
||||||
progressText.value = t('dialog.transferQueue.processing')
|
const progress = JSON.parse(event.data)
|
||||||
progressEventSource.value = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer`)
|
if (progress) {
|
||||||
progressEventSource.value.onmessage = event => {
|
if (!progress.enable) {
|
||||||
const progress = JSON.parse(event.data)
|
progressText.value = t('dialog.transferQueue.processing')
|
||||||
if (progress) {
|
progressValue.value = 0
|
||||||
if (!progress.enable) {
|
if (refreshFlag.value) {
|
||||||
progressText.value = t('dialog.transferQueue.processing')
|
refreshFlag.value = false
|
||||||
progressValue.value = 0
|
get_transfer_queue()
|
||||||
if (refreshFlag.value) {
|
|
||||||
refreshFlag.value = false
|
|
||||||
get_transfer_queue()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
progressText.value = progress.text
|
return
|
||||||
progressValue.value = progress.value
|
}
|
||||||
if (progress.value >= 100 && refreshFlag.value) {
|
progressText.value = progress.text
|
||||||
|
progressValue.value = progress.value
|
||||||
|
if (progress.value >= 100 && refreshFlag.value) {
|
||||||
|
refreshFlag.value = false
|
||||||
|
get_transfer_queue()
|
||||||
|
} else {
|
||||||
|
if (progress.value > 0 && refreshFlag.value && progress.text?.includes('整理完成')) {
|
||||||
refreshFlag.value = false
|
refreshFlag.value = false
|
||||||
get_transfer_queue()
|
get_transfer_queue()
|
||||||
} else {
|
} else {
|
||||||
if (progress.value > 0 && refreshFlag.value && progress.text?.includes('整理完成')) {
|
refreshFlag.value = true
|
||||||
refreshFlag.value = false
|
|
||||||
get_transfer_queue()
|
|
||||||
} else {
|
|
||||||
refreshFlag.value = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用优化的进度SSE连接
|
||||||
|
const progressSSE = useProgressSSE(
|
||||||
|
`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer`,
|
||||||
|
handleProgressMessage,
|
||||||
|
'transfer-queue-progress',
|
||||||
|
progressActive
|
||||||
|
)
|
||||||
|
|
||||||
|
// 使用SSE监听加载进度
|
||||||
|
function startLoadingProgress() {
|
||||||
|
progressText.value = t('dialog.transferQueue.processing')
|
||||||
|
progressActive.value = true
|
||||||
|
progressSSE.start()
|
||||||
|
}
|
||||||
|
|
||||||
// 停止监听加载进度
|
// 停止监听加载进度
|
||||||
function stopLoadingProgress() {
|
function stopLoadingProgress() {
|
||||||
progressEventSource.value?.close()
|
progressActive.value = false
|
||||||
|
progressSSE.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import ProgressDialog from '../dialog/ProgressDialog.vue'
|
|||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import MediaInfoDialog from '../dialog/MediaInfoDialog.vue'
|
import MediaInfoDialog from '../dialog/MediaInfoDialog.vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { useProgressSSE } = useBackgroundOptimization()
|
||||||
|
|
||||||
// 显示器宽度
|
// 显示器宽度
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
@@ -105,8 +107,8 @@ const nameTestDialog = ref(false)
|
|||||||
// 弹出菜单
|
// 弹出菜单
|
||||||
const dropdownItems = ref<{ [key: string]: any }[]>([])
|
const dropdownItems = ref<{ [key: string]: any }[]>([])
|
||||||
|
|
||||||
// 加载进度SSE
|
// 进度是否激活
|
||||||
const progressEventSource = ref<EventSource>()
|
const progressActive = ref(false)
|
||||||
|
|
||||||
// 目录过滤
|
// 目录过滤
|
||||||
const dirs = computed(() => items.value.filter(item => item.type === 'dir' && item.name.includes(filter.value)))
|
const dirs = computed(() => items.value.filter(item => item.type === 'dir' && item.name.includes(filter.value)))
|
||||||
@@ -530,22 +532,34 @@ async function batchScrape() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 进度SSE消息处理函数
|
||||||
|
function handleProgressMessage(event: MessageEvent) {
|
||||||
|
const progress = JSON.parse(event.data)
|
||||||
|
if (progress) {
|
||||||
|
progressText.value = progress.text
|
||||||
|
progressValue.value = progress.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用优化的进度SSE连接
|
||||||
|
const progressSSE = useProgressSSE(
|
||||||
|
`${import.meta.env.VITE_API_BASE_URL}system/progress/batchrename`,
|
||||||
|
handleProgressMessage,
|
||||||
|
'file-batch-rename-progress',
|
||||||
|
progressActive
|
||||||
|
)
|
||||||
|
|
||||||
// 使用SSE监听加载进度
|
// 使用SSE监听加载进度
|
||||||
function startLoadingProgress() {
|
function startLoadingProgress() {
|
||||||
progressText.value = t('common.pleaseWait')
|
progressText.value = t('common.pleaseWait')
|
||||||
progressEventSource.value = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/progress/batchrename`)
|
progressActive.value = true
|
||||||
progressEventSource.value.onmessage = event => {
|
progressSSE.start()
|
||||||
const progress = JSON.parse(event.data)
|
|
||||||
if (progress) {
|
|
||||||
progressText.value = progress.text
|
|
||||||
progressValue.value = progress.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止监听加载进度
|
// 停止监听加载进度
|
||||||
function stopLoadingProgress() {
|
function stopLoadingProgress() {
|
||||||
progressEventSource.value?.close()
|
progressActive.value = false
|
||||||
|
progressSSE.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -0,0 +1,292 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用条件性数据刷新定时器(用于需要动态启停的场景)
|
||||||
|
* @param id 定时器ID
|
||||||
|
* @param loadDataFunc 加载数据函数
|
||||||
|
* @param condition 条件响应式引用,为true时启动定时器
|
||||||
|
* @param interval 刷新间隔(毫秒)
|
||||||
|
* @param immediate 是否立即执行
|
||||||
|
*/
|
||||||
|
const useConditionalDataRefresh = (
|
||||||
|
id: string,
|
||||||
|
loadDataFunc: () => Promise<void> | void,
|
||||||
|
condition: Ref<boolean>,
|
||||||
|
interval: number = 3000,
|
||||||
|
immediate: boolean = true
|
||||||
|
) => {
|
||||||
|
const loading = ref(false)
|
||||||
|
const isTimerActive = ref(false)
|
||||||
|
|
||||||
|
const wrappedLoadData = async () => {
|
||||||
|
if (loading.value || !condition.value) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await loadDataFunc()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`条件数据刷新失败 [${id}]:`, error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTimer = () => {
|
||||||
|
if (!isTimerActive.value && condition.value) {
|
||||||
|
addBackgroundTimer(
|
||||||
|
id,
|
||||||
|
wrappedLoadData,
|
||||||
|
interval,
|
||||||
|
{
|
||||||
|
runInBackground: false,
|
||||||
|
skipInitialRun: !immediate
|
||||||
|
}
|
||||||
|
)
|
||||||
|
isTimerActive.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopTimer = () => {
|
||||||
|
if (isTimerActive.value) {
|
||||||
|
removeBackgroundTimer(id)
|
||||||
|
isTimerActive.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (condition.value) {
|
||||||
|
startTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听条件变化
|
||||||
|
watch(condition, (newValue: boolean) => {
|
||||||
|
if (newValue) {
|
||||||
|
startTimer()
|
||||||
|
} else {
|
||||||
|
stopTimer()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopTimer()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
refresh: wrappedLoadData,
|
||||||
|
stop: stopTimer,
|
||||||
|
start: startTimer,
|
||||||
|
isActive: isTimerActive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
useSSE,
|
||||||
|
useTimer,
|
||||||
|
useDelayedSSE,
|
||||||
|
useProgressSSE,
|
||||||
|
useDataRefresh,
|
||||||
|
useConditionalDataRefresh
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,10 @@
|
|||||||
import { formatDateDifference } from '@core/utils/formatters'
|
import { formatDateDifference } from '@core/utils/formatters'
|
||||||
import { SystemNotification } from '@/api/types'
|
import { SystemNotification } from '@/api/types'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { useDelayedSSE } = useBackgroundOptimization()
|
||||||
|
|
||||||
// 是否有新消息
|
// 是否有新消息
|
||||||
const hasNewMessage = ref(false)
|
const hasNewMessage = ref(false)
|
||||||
@@ -11,9 +13,6 @@ const hasNewMessage = ref(false)
|
|||||||
// 通知列表
|
// 通知列表
|
||||||
const notificationList = ref<SystemNotification[]>([])
|
const notificationList = ref<SystemNotification[]>([])
|
||||||
|
|
||||||
// 事件源
|
|
||||||
let eventSource: EventSource | null = null
|
|
||||||
|
|
||||||
// 弹窗
|
// 弹窗
|
||||||
const appsMenu = ref(false)
|
const appsMenu = ref(false)
|
||||||
|
|
||||||
@@ -27,30 +26,27 @@ function markAllAsRead() {
|
|||||||
appsMenu.value = false
|
appsMenu.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSE持续接收消息
|
// 消息处理函数
|
||||||
function startSSEMessager() {
|
function handleMessage(event: MessageEvent) {
|
||||||
// 延迟 3 秒启动 SSE,避免相关认证信息尚未写入 Cookie 导致 403
|
if (event.data) {
|
||||||
setTimeout(() => {
|
const noti: SystemNotification = JSON.parse(event.data)
|
||||||
eventSource = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/message`)
|
notificationList.value.unshift(noti)
|
||||||
eventSource.addEventListener('message', event => {
|
hasNewMessage.value = true
|
||||||
if (event.data) {
|
}
|
||||||
const noti: SystemNotification = JSON.parse(event.data)
|
|
||||||
notificationList.value.unshift(noti)
|
|
||||||
hasNewMessage.value = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, 3000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 页面加载时,加载当前用户数据
|
// 使用优化的SSE连接,延迟3秒启动,避免认证问题
|
||||||
onBeforeMount(async () => {
|
useDelayedSSE(
|
||||||
startSSEMessager()
|
`${import.meta.env.VITE_API_BASE_URL}system/message`,
|
||||||
})
|
handleMessage,
|
||||||
|
'user-notification',
|
||||||
// 页面卸载时,关闭事件源
|
3000,
|
||||||
onBeforeUnmount(() => {
|
{
|
||||||
if (eventSource) eventSource.close()
|
backgroundCloseDelay: 5000,
|
||||||
})
|
reconnectDelay: 3000,
|
||||||
|
maxReconnectAttempts: 3
|
||||||
|
}
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
+25
-1
@@ -43,8 +43,10 @@ import HeaderTab from './layouts/components/HeaderTab.vue'
|
|||||||
// 7. 样式文件 - 合并为单一导入
|
// 7. 样式文件 - 合并为单一导入
|
||||||
import '@/styles/main.scss'
|
import '@/styles/main.scss'
|
||||||
|
|
||||||
// 8. PWA状态管理
|
// 8. PWA状态管理和后台优化
|
||||||
import { PWAStateController } from '@/utils/pwaStateManager'
|
import { PWAStateController } from '@/utils/pwaStateManager'
|
||||||
|
import { backgroundManager } from '@/utils/backgroundManager'
|
||||||
|
import { sseManagerSingleton } from '@/utils/sseManager'
|
||||||
import { checkPWAStatus } from '@/@core/utils/navigator'
|
import { checkPWAStatus } from '@/@core/utils/navigator'
|
||||||
|
|
||||||
// PWA状态管理器初始化函数
|
// PWA状态管理器初始化函数
|
||||||
@@ -136,5 +138,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 }
|
export { pwaStateController }
|
||||||
|
|||||||
+33
-29
@@ -6,9 +6,11 @@ import type { Context } from '@/api/types'
|
|||||||
import TorrentCardListView from '@/views/torrent/TorrentCardListView.vue'
|
import TorrentCardListView from '@/views/torrent/TorrentCardListView.vue'
|
||||||
import TorrentRowListView from '@/views/torrent/TorrentRowListView.vue'
|
import TorrentRowListView from '@/views/torrent/TorrentRowListView.vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { useProgressSSE } = useBackgroundOptimization()
|
||||||
|
|
||||||
// 路由参数
|
// 路由参数
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -55,8 +57,8 @@ const progressValue = ref(0)
|
|||||||
// 进度是否有效
|
// 进度是否有效
|
||||||
const progressEnabled = ref(false)
|
const progressEnabled = ref(false)
|
||||||
|
|
||||||
// 加载进度SSE
|
// 进度是否激活
|
||||||
const progressEventSource = ref<EventSource>()
|
const progressActive = ref(false)
|
||||||
|
|
||||||
// 错误标题
|
// 错误标题
|
||||||
const errorTitle = ref(t('resource.noData'))
|
const errorTitle = ref(t('resource.noData'))
|
||||||
@@ -68,51 +70,53 @@ const errorDescription = ref(t('resource.noResourceFound'))
|
|||||||
const watchProgressValue = watch(
|
const watchProgressValue = watch(
|
||||||
progressValue,
|
progressValue,
|
||||||
debounce(async () => {
|
debounce(async () => {
|
||||||
if (progressEventSource.value && progressValue.value < 100) {
|
if (progressActive.value && progressValue.value < 100) {
|
||||||
console.warn('卡进度超时 关闭进度条')
|
console.warn('卡进度超时 关闭进度条')
|
||||||
stopLoadingProgress()
|
stopLoadingProgress()
|
||||||
}
|
}
|
||||||
}, 60_000),
|
}, 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监听加载进度
|
// 使用SSE监听加载进度
|
||||||
function startLoadingProgress() {
|
function startLoadingProgress() {
|
||||||
watchProgressValue.resume()
|
watchProgressValue.resume()
|
||||||
progressText.value = t('resource.searching')
|
progressText.value = t('resource.searching')
|
||||||
progressValue.value = 0
|
progressValue.value = 0
|
||||||
progressEnabled.value = false
|
progressEnabled.value = false
|
||||||
progressEventSource.value = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/progress/search`)
|
progressActive.value = true
|
||||||
progressEventSource.value.onmessage = event => {
|
progressSSE.start()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止监听加载进度
|
// 停止监听加载进度
|
||||||
function stopLoadingProgress() {
|
function stopLoadingProgress() {
|
||||||
watchProgressValue.pause()
|
watchProgressValue.pause()
|
||||||
if (progressEventSource.value) {
|
progressActive.value = false
|
||||||
progressEventSource.value.close()
|
progressSSE.stop()
|
||||||
progressEventSource.value = undefined
|
|
||||||
|
|
||||||
// 确保进度显示100%,然后再渐进清零
|
// 确保进度显示100%,然后再渐进清零
|
||||||
progressValue.value = 100
|
progressValue.value = 100
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
progressValue.value = 0
|
progressValue.value = 0
|
||||||
progressEnabled.value = false
|
progressEnabled.value = false
|
||||||
}, 1500) // 延长到1.5秒,让用户有足够时间看到完成状态
|
}, 1500) // 延长到1.5秒,让用户有足够时间看到完成状态
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置视图类型
|
// 设置视图类型
|
||||||
|
|||||||
@@ -0,0 +1,276 @@
|
|||||||
|
/**
|
||||||
|
* 后台管理器
|
||||||
|
* 统一管理定时器和后台活动,减少iOS系统杀掉应用的概率
|
||||||
|
*/
|
||||||
|
export class BackgroundManager {
|
||||||
|
private timers: Map<string, {
|
||||||
|
callback: () => void
|
||||||
|
interval: number
|
||||||
|
timer: ReturnType<typeof setInterval> | null
|
||||||
|
pausedAt?: number
|
||||||
|
runInBackground?: boolean
|
||||||
|
}> = new Map()
|
||||||
|
|
||||||
|
private isBackground = false
|
||||||
|
private isDestroyed = false
|
||||||
|
private lastActivityTime = Date.now()
|
||||||
|
private activityTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.setupVisibilityListener()
|
||||||
|
this.setupActivityTracking()
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupVisibilityListener() {
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
const wasBackground = this.isBackground
|
||||||
|
this.isBackground = document.hidden
|
||||||
|
|
||||||
|
if (this.isBackground && !wasBackground) {
|
||||||
|
console.log('Background: 进入后台,暂停定时器')
|
||||||
|
this.pauseAllTimers()
|
||||||
|
} else if (!this.isBackground && wasBackground) {
|
||||||
|
console.log('Background: 回到前台,恢复定时器')
|
||||||
|
this.resumeAllTimers()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 页面卸载时清理
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
this.destroy()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupActivityTracking() {
|
||||||
|
// 跟踪用户活动
|
||||||
|
const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click']
|
||||||
|
|
||||||
|
const updateActivity = () => {
|
||||||
|
this.lastActivityTime = Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
events.forEach(event => {
|
||||||
|
document.addEventListener(event, updateActivity, { passive: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
// 定期更新活动状态
|
||||||
|
this.activityTimer = setInterval(() => {
|
||||||
|
// 如果超过5分钟没有活动,可以考虑减少后台活动
|
||||||
|
const inactiveTime = Date.now() - this.lastActivityTime
|
||||||
|
if (inactiveTime > 5 * 60 * 1000) {
|
||||||
|
console.log('Background: 用户长时间不活跃')
|
||||||
|
}
|
||||||
|
}, 60000) // 每分钟检查一次
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加定时器
|
||||||
|
*/
|
||||||
|
addTimer(
|
||||||
|
id: string,
|
||||||
|
callback: () => void,
|
||||||
|
interval: number,
|
||||||
|
options: {
|
||||||
|
runInBackground?: boolean
|
||||||
|
skipInitialRun?: boolean
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
const { runInBackground = false, skipInitialRun = false } = options
|
||||||
|
|
||||||
|
this.removeTimer(id)
|
||||||
|
|
||||||
|
const timerConfig = {
|
||||||
|
callback,
|
||||||
|
interval,
|
||||||
|
timer: null as ReturnType<typeof setInterval> | null,
|
||||||
|
runInBackground
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建定时器
|
||||||
|
const wrappedCallback = () => {
|
||||||
|
if (this.isDestroyed) return
|
||||||
|
|
||||||
|
// 只有在前台运行,或者明确允许后台运行时才执行
|
||||||
|
if (!this.isBackground || runInBackground) {
|
||||||
|
try {
|
||||||
|
callback()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Background: 定时器 ${id} 执行错误:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timerConfig.timer = setInterval(wrappedCallback, interval)
|
||||||
|
this.timers.set(id, timerConfig)
|
||||||
|
|
||||||
|
// 如果不跳过初始运行,立即执行一次
|
||||||
|
if (!skipInitialRun) {
|
||||||
|
wrappedCallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Background: 添加定时器 ${id}, 间隔 ${interval}ms`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除定时器
|
||||||
|
*/
|
||||||
|
removeTimer(id: string) {
|
||||||
|
const timerConfig = this.timers.get(id)
|
||||||
|
if (timerConfig) {
|
||||||
|
if (timerConfig.timer) {
|
||||||
|
clearInterval(timerConfig.timer)
|
||||||
|
}
|
||||||
|
this.timers.delete(id)
|
||||||
|
console.log(`Background: 移除定时器 ${id}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 暂停所有定时器
|
||||||
|
*/
|
||||||
|
private pauseAllTimers() {
|
||||||
|
this.timers.forEach((timerConfig, id) => {
|
||||||
|
if (timerConfig.timer && !timerConfig.runInBackground) {
|
||||||
|
clearInterval(timerConfig.timer)
|
||||||
|
timerConfig.timer = null
|
||||||
|
timerConfig.pausedAt = Date.now()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复所有定时器
|
||||||
|
*/
|
||||||
|
private resumeAllTimers() {
|
||||||
|
this.timers.forEach((timerConfig, id) => {
|
||||||
|
if (!timerConfig.timer) {
|
||||||
|
const wrappedCallback = () => {
|
||||||
|
if (this.isDestroyed) return
|
||||||
|
|
||||||
|
if (!this.isBackground || timerConfig.runInBackground) {
|
||||||
|
try {
|
||||||
|
timerConfig.callback()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Background: 定时器 ${id} 执行错误:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timerConfig.timer = setInterval(wrappedCallback, timerConfig.interval)
|
||||||
|
delete timerConfig.pausedAt
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取定时器状态
|
||||||
|
*/
|
||||||
|
getTimerStatus(id: string): 'running' | 'paused' | 'not-found' {
|
||||||
|
const timerConfig = this.timers.get(id)
|
||||||
|
if (!timerConfig) return 'not-found'
|
||||||
|
return timerConfig.timer ? 'running' : 'paused'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有定时器信息
|
||||||
|
*/
|
||||||
|
getTimersInfo(): Array<{
|
||||||
|
id: string
|
||||||
|
interval: number
|
||||||
|
status: 'running' | 'paused'
|
||||||
|
runInBackground: boolean
|
||||||
|
pausedAt?: number
|
||||||
|
}> {
|
||||||
|
return Array.from(this.timers.entries()).map(([id, config]) => ({
|
||||||
|
id,
|
||||||
|
interval: config.interval,
|
||||||
|
status: config.timer ? 'running' : 'paused',
|
||||||
|
runInBackground: config.runInBackground || false,
|
||||||
|
pausedAt: config.pausedAt
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否活跃
|
||||||
|
*/
|
||||||
|
isUserActive(maxInactiveTime = 5 * 60 * 1000): boolean {
|
||||||
|
return Date.now() - this.lastActivityTime < maxInactiveTime
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最后活动时间
|
||||||
|
*/
|
||||||
|
getLastActivityTime(): number {
|
||||||
|
return this.lastActivityTime
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前状态
|
||||||
|
*/
|
||||||
|
getStatus(): {
|
||||||
|
isBackground: boolean
|
||||||
|
isDestroyed: boolean
|
||||||
|
timerCount: number
|
||||||
|
lastActivityTime: number
|
||||||
|
isUserActive: boolean
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
isBackground: this.isBackground,
|
||||||
|
isDestroyed: this.isDestroyed,
|
||||||
|
timerCount: this.timers.size,
|
||||||
|
lastActivityTime: this.lastActivityTime,
|
||||||
|
isUserActive: this.isUserActive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁管理器
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
this.isDestroyed = true
|
||||||
|
|
||||||
|
// 清理所有定时器
|
||||||
|
this.timers.forEach((timerConfig, id) => {
|
||||||
|
if (timerConfig.timer) {
|
||||||
|
clearInterval(timerConfig.timer)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.timers.clear()
|
||||||
|
|
||||||
|
// 清理活动跟踪定时器
|
||||||
|
if (this.activityTimer) {
|
||||||
|
clearInterval(this.activityTimer)
|
||||||
|
this.activityTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Background: 管理器已销毁')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局后台管理器实例
|
||||||
|
*/
|
||||||
|
export const backgroundManager = new BackgroundManager()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 便捷的定时器管理函数
|
||||||
|
*/
|
||||||
|
export function addBackgroundTimer(
|
||||||
|
id: string,
|
||||||
|
callback: () => void,
|
||||||
|
interval: number,
|
||||||
|
options?: {
|
||||||
|
runInBackground?: boolean
|
||||||
|
skipInitialRun?: boolean
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
backgroundManager.addTimer(id, callback, interval, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeBackgroundTimer(id: string) {
|
||||||
|
backgroundManager.removeTimer(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBackgroundTimerStatus(id: string) {
|
||||||
|
return backgroundManager.getTimerStatus(id)
|
||||||
|
}
|
||||||
@@ -594,12 +594,22 @@ export class PWAStateController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setupPeriodicSave(): void {
|
private setupPeriodicSave(): void {
|
||||||
// 每30秒保存一次状态
|
// 导入后台管理器
|
||||||
setInterval(() => {
|
import('@/utils/backgroundManager').then(({ addBackgroundTimer }) => {
|
||||||
if (!document.hidden) {
|
// 使用后台管理器,延长间隔
|
||||||
this.saveCurrentState()
|
addBackgroundTimer(
|
||||||
}
|
'pwa-state-save',
|
||||||
}, 30000)
|
() => {
|
||||||
|
// 只在前台时保存状态(由后台管理器自动处理)
|
||||||
|
this.saveCurrentState()
|
||||||
|
},
|
||||||
|
60000, // 改为60秒,减少频率
|
||||||
|
{
|
||||||
|
runInBackground: false, // 后台时不保存
|
||||||
|
skipInitialRun: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAppSpecificState(): any {
|
private getAppSpecificState(): any {
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
/**
|
||||||
|
* SSE连接管理器
|
||||||
|
* 优化后台SSE连接,减少iOS系统杀掉应用的概率
|
||||||
|
*/
|
||||||
|
export class SSEManager {
|
||||||
|
private eventSource: EventSource | null = null
|
||||||
|
private url: string
|
||||||
|
private isBackground = false
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(url: string, options: Partial<typeof SSEManager.prototype.options> = {}) {
|
||||||
|
this.url = url
|
||||||
|
this.options = {
|
||||||
|
backgroundCloseDelay: 5000, // 5秒后关闭后台连接
|
||||||
|
reconnectDelay: 3000, // 3秒后重连
|
||||||
|
maxReconnectAttempts: 3,
|
||||||
|
...options
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setupVisibilityListener()
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupVisibilityListener() {
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.hidden) {
|
||||||
|
this.handleBackground()
|
||||||
|
} else {
|
||||||
|
this.handleForeground()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 页面卸载时关闭连接
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
this.close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleBackground() {
|
||||||
|
this.isBackground = true
|
||||||
|
|
||||||
|
// 延迟关闭SSE连接,避免频繁切换
|
||||||
|
if (this.backgroundCloseTimer) {
|
||||||
|
clearTimeout(this.backgroundCloseTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.backgroundCloseTimer = window.setTimeout(() => {
|
||||||
|
if (this.isBackground && this.eventSource) {
|
||||||
|
console.log('SSE: 后台关闭连接')
|
||||||
|
this.eventSource.close()
|
||||||
|
this.eventSource = null
|
||||||
|
}
|
||||||
|
}, this.options.backgroundCloseDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleForeground() {
|
||||||
|
this.isBackground = false
|
||||||
|
|
||||||
|
// 清除后台关闭定时器
|
||||||
|
if (this.backgroundCloseTimer) {
|
||||||
|
clearTimeout(this.backgroundCloseTimer)
|
||||||
|
this.backgroundCloseTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 立即重新建立连接
|
||||||
|
if (!this.eventSource || this.eventSource.readyState === EventSource.CLOSED) {
|
||||||
|
console.log('SSE: 前台恢复连接')
|
||||||
|
this.reconnectSSE()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private reconnectSSE(attemptCount = 0) {
|
||||||
|
if (attemptCount >= this.options.maxReconnectAttempts) {
|
||||||
|
console.warn('SSE: 达到最大重连次数')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.eventSource = new EventSource(this.url)
|
||||||
|
|
||||||
|
this.eventSource.onopen = () => {
|
||||||
|
console.log('SSE: 连接已建立')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eventSource.onerror = (error) => {
|
||||||
|
console.error('SSE: 连接错误', error)
|
||||||
|
|
||||||
|
if (this.eventSource?.readyState === EventSource.CLOSED) {
|
||||||
|
// 连接已关闭,尝试重连
|
||||||
|
if (this.reconnectTimer) {
|
||||||
|
clearTimeout(this.reconnectTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reconnectTimer = window.setTimeout(() => {
|
||||||
|
if (!this.isBackground) {
|
||||||
|
this.reconnectSSE(attemptCount + 1)
|
||||||
|
}
|
||||||
|
}, this.options.reconnectDelay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eventSource.onmessage = (event) => {
|
||||||
|
// 分发消息给所有监听器
|
||||||
|
this.listeners.forEach(listener => {
|
||||||
|
try {
|
||||||
|
listener(event)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('SSE: 监听器错误', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('SSE: 创建连接失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加消息监听器
|
||||||
|
*/
|
||||||
|
addMessageListener(id: string, listener: (event: MessageEvent) => void) {
|
||||||
|
this.listeners.set(id, listener)
|
||||||
|
|
||||||
|
// 如果还没有连接,现在建立连接
|
||||||
|
if (!this.eventSource && !this.isBackground) {
|
||||||
|
this.reconnectSSE()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除消息监听器
|
||||||
|
*/
|
||||||
|
removeMessageListener(id: string) {
|
||||||
|
this.listeners.delete(id)
|
||||||
|
|
||||||
|
// 如果没有监听器了,关闭连接
|
||||||
|
if (this.listeners.size === 0) {
|
||||||
|
this.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭连接
|
||||||
|
*/
|
||||||
|
close() {
|
||||||
|
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.listeners.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取连接状态
|
||||||
|
*/
|
||||||
|
get readyState(): number {
|
||||||
|
return this.eventSource?.readyState ?? EventSource.CLOSED
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取连接URL
|
||||||
|
*/
|
||||||
|
get connectionUrl(): string {
|
||||||
|
return this.url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSE管理器单例
|
||||||
|
*/
|
||||||
|
class SSEManagerSingleton {
|
||||||
|
private managers: Map<string, SSEManager> = new Map()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取或创建SSE管理器
|
||||||
|
*/
|
||||||
|
getManager(url: string, options?: ConstructorParameters<typeof SSEManager>[1]): SSEManager {
|
||||||
|
if (!this.managers.has(url)) {
|
||||||
|
this.managers.set(url, new SSEManager(url, options))
|
||||||
|
}
|
||||||
|
return this.managers.get(url)!
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭指定URL的管理器
|
||||||
|
*/
|
||||||
|
closeManager(url: string) {
|
||||||
|
const manager = this.managers.get(url)
|
||||||
|
if (manager) {
|
||||||
|
manager.close()
|
||||||
|
this.managers.delete(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭所有管理器
|
||||||
|
*/
|
||||||
|
closeAllManagers() {
|
||||||
|
this.managers.forEach(manager => manager.close())
|
||||||
|
this.managers.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sseManagerSingleton = new SSEManagerSingleton()
|
||||||
@@ -3,9 +3,11 @@ import { useTheme } from 'vuetify'
|
|||||||
import { hexToRgb } from '@layouts/utils'
|
import { hexToRgb } from '@layouts/utils'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { useDataRefresh } = useBackgroundOptimization()
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -29,9 +31,6 @@ const variableTheme = controlledComputed(
|
|||||||
|
|
||||||
const chartKey = ref(0)
|
const chartKey = ref(0)
|
||||||
|
|
||||||
// 定时器
|
|
||||||
let refreshTimer: NodeJS.Timeout | null = null
|
|
||||||
|
|
||||||
// 时间序列
|
// 时间序列
|
||||||
const series = ref([
|
const series = ref([
|
||||||
{
|
{
|
||||||
@@ -107,7 +106,7 @@ const chartOptions = controlledComputed(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 调用API接口获取最新CPU使用率
|
// 调用API接口获取最新CPU使用率
|
||||||
async function getCpuUsage() {
|
async function loadCpuData() {
|
||||||
if (!props.allowRefresh) return
|
if (!props.allowRefresh) return
|
||||||
try {
|
try {
|
||||||
// 请求数据
|
// 请求数据
|
||||||
@@ -123,23 +122,13 @@ async function getCpuUsage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
// 使用优化的数据刷新定时器
|
||||||
// 延迟启动,确保组件完全挂载
|
const { loading } = useDataRefresh(
|
||||||
nextTick(() => {
|
'analytics-cpu',
|
||||||
getCpuUsage()
|
loadCpuData,
|
||||||
refreshTimer = setInterval(() => {
|
2000, // 2秒间隔
|
||||||
getCpuUsage()
|
true // 立即执行
|
||||||
}, 2000)
|
)
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 组件卸载时停止定时器
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (refreshTimer) {
|
|
||||||
clearInterval(refreshTimer)
|
|
||||||
refreshTimer = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import { hexToRgb } from '@layouts/utils'
|
|||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { formatBytes } from '@/@core/utils/formatters'
|
import { formatBytes } from '@/@core/utils/formatters'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { useDataRefresh } = useBackgroundOptimization()
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -30,9 +32,6 @@ const variableTheme = controlledComputed(
|
|||||||
|
|
||||||
const chartKey = ref(0)
|
const chartKey = ref(0)
|
||||||
|
|
||||||
// 定时器
|
|
||||||
let refreshTimer: NodeJS.Timeout | null = null
|
|
||||||
|
|
||||||
// 时间序列
|
// 时间序列
|
||||||
const series = ref([
|
const series = ref([
|
||||||
{
|
{
|
||||||
@@ -113,7 +112,7 @@ const chartOptions = controlledComputed(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 调用API接口获取最新内存使用量
|
// 调用API接口获取最新内存使用量
|
||||||
async function getMemorgUsage() {
|
async function loadMemoryData() {
|
||||||
if (!props.allowRefresh) return
|
if (!props.allowRefresh) return
|
||||||
try {
|
try {
|
||||||
// 请求数据
|
// 请求数据
|
||||||
@@ -128,24 +127,13 @@ async function getMemorgUsage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
// 使用优化的数据刷新定时器
|
||||||
// 延迟启动,确保组件完全挂载
|
const { loading } = useDataRefresh(
|
||||||
nextTick(() => {
|
'analytics-memory',
|
||||||
getMemorgUsage()
|
loadMemoryData,
|
||||||
// 启动定时器
|
3000, // 3秒间隔
|
||||||
refreshTimer = setInterval(() => {
|
true // 立即执行
|
||||||
getMemorgUsage()
|
)
|
||||||
}, 3000)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 组件卸载时停止定时器
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (refreshTimer) {
|
|
||||||
clearInterval(refreshTimer)
|
|
||||||
refreshTimer = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
// 使用nextTick确保DOM准备完成后再更新chartKey
|
// 使用nextTick确保DOM准备完成后再更新chartKey
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { useTheme } from 'vuetify'
|
|||||||
import { hexToRgb } from '@layouts/utils'
|
import { hexToRgb } from '@layouts/utils'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { useDataRefresh } = useBackgroundOptimization()
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -29,9 +31,6 @@ const variableTheme = controlledComputed(
|
|||||||
|
|
||||||
const chartKey = ref(0)
|
const chartKey = ref(0)
|
||||||
|
|
||||||
// 定时器
|
|
||||||
let refreshTimer: NodeJS.Timeout | null = null
|
|
||||||
|
|
||||||
// 时间序列 - 上行和下行流量
|
// 时间序列 - 上行和下行流量
|
||||||
const series = ref([
|
const series = ref([
|
||||||
{
|
{
|
||||||
@@ -161,23 +160,13 @@ async function getNetworkUsage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
// 使用优化的数据刷新定时器
|
||||||
// 延迟启动,确保组件完全挂载
|
useDataRefresh(
|
||||||
nextTick(() => {
|
'dashboard-network',
|
||||||
getNetworkUsage()
|
getNetworkUsage,
|
||||||
refreshTimer = setInterval(() => {
|
2000, // 2秒间隔
|
||||||
getNetworkUsage()
|
true // 立即执行
|
||||||
}, 2000)
|
)
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 组件卸载时停止定时器
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (refreshTimer) {
|
|
||||||
clearInterval(refreshTimer)
|
|
||||||
refreshTimer = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { formatSeconds } from '@/@core/utils/formatters'
|
|||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { Process } from '@/api/types'
|
import type { Process } from '@/api/types'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { useDataRefresh } = useBackgroundOptimization()
|
||||||
|
|
||||||
// 表头
|
// 表头
|
||||||
const headers = [
|
const headers = [
|
||||||
@@ -18,9 +20,6 @@ const headers = [
|
|||||||
// 数据列表
|
// 数据列表
|
||||||
const processList = ref<Process[]>([])
|
const processList = ref<Process[]>([])
|
||||||
|
|
||||||
// 定时器
|
|
||||||
let refreshTimer: NodeJS.Timeout | null = null
|
|
||||||
|
|
||||||
// 调用API加载数据
|
// 调用API加载数据
|
||||||
async function loadProcessList() {
|
async function loadProcessList() {
|
||||||
try {
|
try {
|
||||||
@@ -32,22 +31,13 @@ async function loadProcessList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
// 使用优化的数据刷新定时器
|
||||||
loadProcessList()
|
useDataRefresh(
|
||||||
|
'dashboard-processes',
|
||||||
// 启动定时器
|
loadProcessList,
|
||||||
refreshTimer = setInterval(() => {
|
5000, // 5秒间隔
|
||||||
loadProcessList()
|
true // 立即执行
|
||||||
}, 5000)
|
)
|
||||||
})
|
|
||||||
|
|
||||||
// 组件卸载时停止定时器
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (refreshTimer) {
|
|
||||||
clearInterval(refreshTimer)
|
|
||||||
refreshTimer = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { ScheduleInfo } from '@/api/types'
|
import type { ScheduleInfo } from '@/api/types'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { useDataRefresh } = useBackgroundOptimization()
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -18,9 +20,6 @@ const props = defineProps({
|
|||||||
// 定时服务列表
|
// 定时服务列表
|
||||||
const schedulerList = ref<ScheduleInfo[]>([])
|
const schedulerList = ref<ScheduleInfo[]>([])
|
||||||
|
|
||||||
// 定时器
|
|
||||||
let refreshTimer: NodeJS.Timeout | null = null
|
|
||||||
|
|
||||||
// 调用API加载定时服务列表
|
// 调用API加载定时服务列表
|
||||||
async function loadSchedulerList() {
|
async function loadSchedulerList() {
|
||||||
if (!props.allowRefresh) {
|
if (!props.allowRefresh) {
|
||||||
@@ -35,22 +34,13 @@ async function loadSchedulerList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
// 使用优化的数据刷新定时器
|
||||||
loadSchedulerList()
|
useDataRefresh(
|
||||||
|
'dashboard-scheduler',
|
||||||
// 启动定时器
|
loadSchedulerList,
|
||||||
refreshTimer = setInterval(() => {
|
60000, // 60秒间隔
|
||||||
loadSchedulerList()
|
true // 立即执行
|
||||||
}, 60000)
|
)
|
||||||
})
|
|
||||||
|
|
||||||
// 组件卸载时停止定时器
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (refreshTimer) {
|
|
||||||
clearInterval(refreshTimer)
|
|
||||||
refreshTimer = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { formatFileSize } from '@/@core/utils/formatters'
|
|||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { DownloaderInfo } from '@/api/types'
|
import type { DownloaderInfo } from '@/api/types'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { useDataRefresh } = useBackgroundOptimization()
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -16,9 +18,6 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// 定时器
|
|
||||||
let refreshTimer: NodeJS.Timeout | null = null
|
|
||||||
|
|
||||||
// 下载器信息
|
// 下载器信息
|
||||||
const downloadInfo = ref<DownloaderInfo>({
|
const downloadInfo = ref<DownloaderInfo>({
|
||||||
// 下载速度
|
// 下载速度
|
||||||
@@ -78,22 +77,13 @@ async function loadDownloaderInfo() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
// 使用优化的数据刷新定时器
|
||||||
loadDownloaderInfo()
|
const { loading } = useDataRefresh(
|
||||||
|
'analytics-speed',
|
||||||
// 启动定时器
|
loadDownloaderInfo,
|
||||||
refreshTimer = setInterval(() => {
|
3000, // 3秒间隔
|
||||||
loadDownloaderInfo()
|
true // 立即执行
|
||||||
}, 3000)
|
)
|
||||||
})
|
|
||||||
|
|
||||||
// 组件卸载时停止定时器
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (refreshTimer) {
|
|
||||||
clearInterval(refreshTimer)
|
|
||||||
refreshTimer = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import NoDataFound from '@/components/NoDataFound.vue'
|
|||||||
import DownloadingCard from '@/components/cards/DownloadingCard.vue'
|
import DownloadingCard from '@/components/cards/DownloadingCard.vue'
|
||||||
import { useUserStore } from '@/stores'
|
import { useUserStore } from '@/stores'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { useDataRefresh } = useBackgroundOptimization()
|
||||||
|
|
||||||
// 定义输入参数
|
// 定义输入参数
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -18,9 +20,6 @@ const props = defineProps<{
|
|||||||
// 用户 Store
|
// 用户 Store
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
// 定时器
|
|
||||||
let refreshTimer: NodeJS.Timeout | null = null
|
|
||||||
|
|
||||||
// 数据列表
|
// 数据列表
|
||||||
const dataList = ref<DownloadingInfo[]>([])
|
const dataList = ref<DownloadingInfo[]>([])
|
||||||
|
|
||||||
@@ -56,23 +55,13 @@ const filteredDataList = computed(() => {
|
|||||||
else return dataList.value.filter(data => data.userid === userName || data.username === userName)
|
else return dataList.value.filter(data => data.userid === userName || data.username === userName)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 加载时获取数据
|
// 使用优化的数据刷新定时器
|
||||||
onBeforeMount(() => {
|
const { loading: dataLoading } = useDataRefresh(
|
||||||
fetchData()
|
'downloading-list',
|
||||||
|
fetchData,
|
||||||
// 启动定时器
|
3000, // 3秒间隔
|
||||||
refreshTimer = setInterval(() => {
|
true // 立即执行
|
||||||
fetchData()
|
)
|
||||||
}, 3000)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 组件卸载时停止定时器
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (refreshTimer) {
|
|
||||||
clearInterval(refreshTimer)
|
|
||||||
refreshTimer = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { useToast } from 'vue-toastification'
|
|||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { ScheduleInfo } from '@/api/types'
|
import type { ScheduleInfo } from '@/api/types'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { useDataRefresh } = useBackgroundOptimization()
|
||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
@@ -13,9 +15,6 @@ const $toast = useToast()
|
|||||||
// 定时服务列表
|
// 定时服务列表
|
||||||
const schedulerList = ref<ScheduleInfo[]>([])
|
const schedulerList = ref<ScheduleInfo[]>([])
|
||||||
|
|
||||||
// 定时器
|
|
||||||
let refreshTimer: NodeJS.Timeout | null = null
|
|
||||||
|
|
||||||
// 调用API加载定时服务列表
|
// 调用API加载定时服务列表
|
||||||
async function loadSchedulerList() {
|
async function loadSchedulerList() {
|
||||||
try {
|
try {
|
||||||
@@ -60,22 +59,13 @@ function runCommand(id: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
// 使用优化的数据刷新定时器
|
||||||
loadSchedulerList()
|
useDataRefresh(
|
||||||
|
'scheduler-list',
|
||||||
// 启动定时器
|
loadSchedulerList,
|
||||||
refreshTimer = setInterval(() => {
|
5000, // 5秒间隔
|
||||||
loadSchedulerList()
|
true // 立即执行
|
||||||
}, 5000)
|
)
|
||||||
})
|
|
||||||
|
|
||||||
// 组件卸载时停止定时器
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (refreshTimer) {
|
|
||||||
clearInterval(refreshTimer)
|
|
||||||
refreshTimer = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
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'
|
||||||
|
|
||||||
// 定义输入变量
|
// 定义输入变量
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -10,6 +11,7 @@ const props = defineProps<{
|
|||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
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 }[]>([])
|
||||||
@@ -22,9 +24,6 @@ const headers = [
|
|||||||
{ title: t('logging.content'), value: 'content' },
|
{ title: t('logging.content'), value: 'content' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// SSE消息对象
|
|
||||||
let eventSource: EventSource | null = null
|
|
||||||
|
|
||||||
// 日志颜色映射表
|
// 日志颜色映射表
|
||||||
const logColorMap: Record<string, string> = {
|
const logColorMap: Record<string, string> = {
|
||||||
DEBUG: 'secondary',
|
DEBUG: 'secondary',
|
||||||
@@ -38,55 +37,54 @@ function getLogColor(level: string): string {
|
|||||||
return logColorMap[level] || 'secondary'
|
return logColorMap[level] || 'secondary'
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSE持续获取日志
|
// 日志缓冲区和超时处理
|
||||||
function startSSELogging() {
|
const buffer: string[] = []
|
||||||
console.log(props.logfile)
|
let timeoutId: number | null = null
|
||||||
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
|
|
||||||
|
|
||||||
eventSource.addEventListener('message', event => {
|
// SSE消息处理函数
|
||||||
const message = event.data
|
function handleSSEMessage(event: MessageEvent) {
|
||||||
if (message) {
|
const message = event.data
|
||||||
buffer.push(message)
|
if (message) {
|
||||||
if (!timeoutId) {
|
buffer.push(message)
|
||||||
timeoutId = window.setTimeout(() => {
|
if (!timeoutId) {
|
||||||
// 解析新日志
|
timeoutId = window.setTimeout(() => {
|
||||||
const newParsedLogs = buffer
|
// 解析新日志
|
||||||
.map(log => {
|
const newParsedLogs = buffer
|
||||||
const logPattern = /^【(.*?)】\s*([\d]{4}-\d{2}-\d{2}(?:\s+\d{2}:\d{2})?)\s+(.*?)\s*-\s*(.*?)\s*-\s*(.*)$/
|
.map(log => {
|
||||||
const matches = log.match(logPattern)
|
const logPattern = /^【(.*?)】\s*([\d]{4}-\d{2}-\d{2}(?:\s+\d{2}:\d{2})?)\s+(.*?)\s*-\s*(.*?)\s*-\s*(.*)$/
|
||||||
if (matches) {
|
const matches = log.match(logPattern)
|
||||||
const [, level, date, time, program, content] = matches
|
if (matches) {
|
||||||
return { level, date, time, program, content }
|
const [, level, date, time, program, content] = matches
|
||||||
}
|
return { level, date, time, program, content }
|
||||||
return null
|
}
|
||||||
})
|
return null
|
||||||
.filter(Boolean)
|
})
|
||||||
// 倒序后插入parsedLogs顶部
|
.filter(Boolean)
|
||||||
parsedLogs.value.unshift(...(newParsedLogs.reverse() as any[]))
|
// 倒序后插入parsedLogs顶部
|
||||||
// 保留最新的200条日志
|
parsedLogs.value.unshift(...(newParsedLogs.reverse() as any[]))
|
||||||
parsedLogs.value = parsedLogs.value.slice(0, 200)
|
// 保留最新的200条日志
|
||||||
// 重置buffer
|
parsedLogs.value = parsedLogs.value.slice(0, 200)
|
||||||
buffer.length = 0
|
// 重置buffer
|
||||||
timeoutId = null
|
buffer.length = 0
|
||||||
}, 100)
|
timeoutId = null
|
||||||
}
|
}, 100)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
// 使用优化的SSE连接
|
||||||
startSSELogging()
|
useSSE(
|
||||||
})
|
`${import.meta.env.VITE_API_BASE_URL}system/logging?logfile=${
|
||||||
|
encodeURIComponent(props.logfile) ?? 'moviepilot.log'
|
||||||
onBeforeUnmount(() => {
|
}`,
|
||||||
if (eventSource) eventSource.close()
|
handleSSEMessage,
|
||||||
})
|
`logging-${props.logfile}`,
|
||||||
|
{
|
||||||
|
backgroundCloseDelay: 5000,
|
||||||
|
reconnectDelay: 3000,
|
||||||
|
maxReconnectAttempts: 3
|
||||||
|
}
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import type { Message } from '@/api/types'
|
|||||||
import MessageCard from '@/components/cards/MessageCard.vue'
|
import MessageCard from '@/components/cards/MessageCard.vue'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { useSSE } = useBackgroundOptimization()
|
||||||
|
|
||||||
// 定义事件
|
// 定义事件
|
||||||
const emit = defineEmits(['scroll'])
|
const emit = defineEmits(['scroll'])
|
||||||
@@ -27,25 +29,31 @@ const page = ref(1)
|
|||||||
// 存量消息最新时间
|
// 存量消息最新时间
|
||||||
const lastTime = ref('')
|
const lastTime = ref('')
|
||||||
|
|
||||||
// SSE消息对象
|
// SSE消息处理函数
|
||||||
let eventSource: EventSource | null = null
|
function handleSSEMessage(event: MessageEvent) {
|
||||||
|
const message = event.data
|
||||||
// SSE持续获取消息
|
if (message) {
|
||||||
function startSSEMessager() {
|
const object = JSON.parse(message)
|
||||||
eventSource = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/message?role=user`)
|
if (compareTime(object.date, lastTime.value) <= 0) return
|
||||||
eventSource.addEventListener('message', event => {
|
messages.value.push(object)
|
||||||
const message = event.data
|
nextTick(() => {
|
||||||
if (message) {
|
emit('scroll') // 新消息到达时触发智能滚动
|
||||||
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加载存量消息
|
// 调用API加载存量消息
|
||||||
async function loadMessages({ done }: { done: any }) {
|
async function loadMessages({ done }: { done: any }) {
|
||||||
// 如果正在加载中,直接返回
|
// 如果正在加载中,直接返回
|
||||||
@@ -85,8 +93,6 @@ async function loadMessages({ done }: { done: any }) {
|
|||||||
}
|
}
|
||||||
// 取消加载中
|
// 取消加载中
|
||||||
loading.value = false
|
loading.value = false
|
||||||
// 监听SSE消息
|
|
||||||
startSSEMessager()
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
@@ -110,10 +116,6 @@ onMounted(() => {
|
|||||||
emit('scroll')
|
emit('scroll')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (eventSource) eventSource.close()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
Reference in New Issue
Block a user