improve(web):优化前端定时任务执行策略,仅在页面激活时发送请求

This commit is contained in:
qian committed 2026-09-10 23:13:22 +08:00
1 parent a266d3cd83
commit 3d4e4d125a
13 files changed
+290 -95

No files matched your search

+105
View File
@@ -0,0 +1,105 @@
/**
* 页面可见性感知的轮询工具
*
* 后台标签页的轮询请求既浪费带宽也无展示意义,浏览器自身的定时器节流
* 又不可靠(部分平台放宽到每分钟一次,仍会产生请求)。统一改由
* visibilitychange 显式控制:页面隐藏时停止排队(隐藏期间零请求),
* 恢复可见时若已超过轮询间隔立即补拉一次,未到则按剩余时间续排。
*
* 内部用递归 setTimeout 而非 setInterval:上一次回调完成后才排下一次,
* 慢响应下不会堆叠请求。每个调用点创建独立实例,与组件生命周期无关,
* 组件外(模块级单例)也可使用。
*/
export interface PollerOptions {
/** start() 时是否立即执行一次回调。默认 false,与 setInterval「先等一个完整间隔」的语义一致 */
immediate?: boolean
}
export interface Poller {
/** 启动轮询。幂等:已运行时调用无效果 */
start(): void
/** 停止轮询并移除可见性监听。幂等,可再次 start() 重新开始完整间隔 */
stop(): void
}
export function createPoller(
callback: () => void | Promise<unknown>,
intervalMs: number,
options?: PollerOptions,
): Poller {
/** 是否处于运行中(start 后 stop 前) */
let active = false
/** 回调是否执行中,防重入 */
let inFlight = false
/** 上次回调的执行起点,兼作调度锚点(计算剩余等待时间) */
let lastRunAt = 0
let timer: ReturnType<typeof setTimeout> | null = null
function clearTimer(): void {
if (timer !== null) {
clearTimeout(timer)
timer = null
}
}
function run(): void {
// 防重入:可见性事件与到期定时器可能同时触发
if (inFlight) return
// 隐藏态兜底:定时器到期与 visibilitychange 存在事件顺序竞态,执行前再判一次
if (document.hidden) return
inFlight = true
lastRunAt = Date.now()
// 回调异常吞掉,保证后续调度不中断(调用点自身已有 try/catch,这里是双保险)
Promise.resolve(callback())
.catch(() => {})
.finally(() => {
inFlight = false
if (active) schedule()
})
}
/** 唯一的排队入口:所有续排路径都经过这里,保证同一时刻至多一个待触发定时器 */
function schedule(): void {
if (!active || document.hidden) return
clearTimer()
const remaining = intervalMs - (Date.now() - lastRunAt)
if (remaining <= 0) {
run()
return
}
timer = setTimeout(run, remaining)
}
function onVisibilityChange(): void {
if (!active) return
if (document.hidden) {
// 隐藏:停止排队。在途请求自然完成,完成后发现隐藏态不会再续排
clearTimer()
} else {
// 恢复可见:已超间隔则立即补拉(remaining <= 0 走 run),否则按剩余时间续排
schedule()
}
}
function start(): void {
if (active) return
active = true
// 锚点重置:stop 后再 start 视为全新周期,首个回调在完整间隔之后
lastRunAt = Date.now()
document.addEventListener('visibilitychange', onVisibilityChange)
if (options?.immediate) {
run()
} else {
schedule()
}
}
function stop(): void {
active = false
clearTimer()
document.removeEventListener('visibilitychange', onVisibilityChange)
}
return { start, stop }
}
+13 -17
View File
@@ -8,6 +8,7 @@ import request from '@/utils/request'
import RouteErrorBoundary from '@/components/RouteErrorBoundary.vue'
import { useWatermark } from '@/composables/useWatermark'
import { useSiteName } from '@/composables/useSiteName'
import { createPoller } from '@/composables/usePolling'
import {
IconDesktop,
IconDashboard,
@@ -65,7 +66,8 @@ const checking = ref(false)
const updateStatus = ref<any>(null)
const releaseNotes = ref('')
const releaseUrl = ref('')
const updatePollTimer = ref<ReturnType<typeof setInterval> | null>(null)
// 更新进度轮询:页面隐藏时暂停,恢复可见时立即补拉进度
const updatePoller = createPoller(fetchUpdateStatus, 2000)
const releaseNotesHtml = computed(() => {
if (!releaseNotes.value) return ''
@@ -152,14 +154,11 @@ function closeUpdateModal() {
function startPolling() {
stopPolling()
updatePollTimer.value = setInterval(fetchUpdateStatus, 2000)
updatePoller.start()
}
function stopPolling() {
if (updatePollTimer.value) {
clearInterval(updatePollTimer.value)
updatePollTimer.value = null
}
updatePoller.stop()
}
async function executeUpdate() {
@@ -504,7 +503,8 @@ const bellData = ref<{
feedbacks_wait: string
total: number
} | null>(null)
let bellTimer: ReturnType<typeof setInterval> | null = null
// 待办铃铛轮询:页面隐藏时暂停,恢复可见时按剩余时间续排
const bellPoller = createPoller(fetchBellSummary, 60000)
async function fetchBellSummary() {
try {
@@ -519,16 +519,11 @@ async function fetchBellSummary() {
function startBellPolling() {
fetchBellSummary()
if (!bellTimer) {
bellTimer = setInterval(fetchBellSummary, 60000)
}
bellPoller.start()
}
function stopBellPolling() {
if (bellTimer) {
clearInterval(bellTimer)
bellTimer = null
}
bellPoller.stop()
}
function toggleBell() {
@@ -553,7 +548,8 @@ watch(canSeeBell, (visible) => {
// 60s 轮询不会额外压库。
const workbenchUrgent = ref(0)
const workbenchTotal = ref(0)
let badgeTimer: ReturnType<typeof setInterval> | null = null
// 工作台红点轮询:页面隐藏时暂停,恢复可见时按剩余时间续排
const badgePoller = createPoller(fetchWorkbenchBadges, 60000)
// 角标只挂工作台菜单:待办的排查线索只在工作台的描述文案里,
// 业务菜单里没有对应的定位入口,挂数字只会带来「进去了却找不到问题」的困惑
@@ -587,7 +583,7 @@ onMounted(() => {
}
fetchWorkbenchBadges()
badgeTimer = setInterval(fetchWorkbenchBadges, 60000)
badgePoller.start()
axios.get('/api/settings/public').then((res) => {
const settings = res.data?.data?.settings
@@ -605,7 +601,7 @@ onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
window.removeEventListener('resize', updateMobile)
clearTimeout(popupHideTimer!)
if (badgeTimer) clearInterval(badgeTimer)
badgePoller.stop()
stopBellPolling()
stopPolling()
unmountWatermark()
+5 -3
View File
@@ -27,6 +27,7 @@ import {
} from '@arco-design/web-vue/es/icon'
import request from '@/utils/request'
import { formatBilling } from '@/composables/useCurrency'
import { createPoller } from '@/composables/usePolling'
const router = useRouter()
@@ -68,7 +69,8 @@ const onlyUrgent = ref(false)
const activeDomain = ref<DomainKey | 'all'>('all')
const lastRefresh = ref<Date | null>(null)
let pollTimer: ReturnType<typeof setInterval> | null = null
// 静默轮询:页面隐藏时暂停,恢复可见时按剩余时间续排
const summaryPoller = createPoller(() => fetchSummary(true), 60000)
// ===== 派生 =====
const items = computed<any[]>(() => summary.value?.items || [])
@@ -184,11 +186,11 @@ function availClass(m: any) {
onMounted(() => {
fetchSummary()
// 后端整份结果有 30s Redis 缓存,前端 60s 轮询不会额外压库
pollTimer = setInterval(() => fetchSummary(true), 60000)
summaryPoller.start()
})
onBeforeUnmount(() => {
if (pollTimer) clearInterval(pollTimer)
summaryPoller.stop()
})
</script>
@@ -7,6 +7,7 @@ import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/compon
import VChart from 'vue-echarts'
import PageHeader from '@/components/PageHeader.vue'
import request from '@/utils/request'
import { createPoller } from '@/composables/usePolling'
import { IconThunderbolt, IconSwap, IconClockCircle, IconCode, IconBarChart, IconLayers, IconApps, IconUserGroup, IconFire, IconStorage } from '@arco-design/web-vue/es/icon'
use([CanvasRenderer, LineChart, BarChart, GridComponent, TooltipComponent, LegendComponent])
@@ -188,8 +189,9 @@ const poolChannelInflight = computed(() => {
return entries.map(([k, v]) => ({ channel: k, count: v as number, pct: ((v as number) / max) * 100 }))
})
let pollTimer: ReturnType<typeof setInterval> | null = null
let resizeTimer: ReturnType<typeof setTimeout> | null = null
// 实时指标轮询:页面隐藏时暂停,恢复可见时立即补拉(隐藏期间的监控数据无展示意义)
const realtimePoller = createPoller(fetchRealtime, 10000)
function onWindowResize() {
if (resizeTimer) clearTimeout(resizeTimer)
@@ -202,16 +204,13 @@ function onWindowResize() {
onMounted(() => {
fetchRealtime()
pollTimer = setInterval(fetchRealtime, 10000)
realtimePoller.start()
elapsedTimer = setInterval(() => { now.value = Date.now() }, 1000)
window.addEventListener('resize', onWindowResize)
})
onUnmounted(() => {
if (pollTimer) {
clearInterval(pollTimer)
pollTimer = null
}
realtimePoller.stop()
if (elapsedTimer) {
clearInterval(elapsedTimer)
elapsedTimer = null
@@ -1,20 +1,21 @@
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { usePublicSettings } from '@/composables/usePublicSettings'
import { createPoller } from '@/composables/usePolling'
import Icon from '@/components/common/Icon.vue'
const { settings, fetchSettings } = usePublicSettings()
const dismissed = ref(false)
let timer: ReturnType<typeof setInterval> | null = null
// 每 60 秒强制刷新一次维护状态(绕过缓存);页面隐藏时暂停,恢复可见时按剩余时间续排
const maintenancePoller = createPoller(() => fetchSettings(true), 60 * 1000)
onMounted(async () => {
await fetchSettings()
// 每 60 秒强制刷新一次维护状态(绕过缓存)
timer = setInterval(() => fetchSettings(true), 60 * 1000)
maintenancePoller.start()
})
onBeforeUnmount(() => {
if (timer) clearInterval(timer)
maintenancePoller.stop()
})
function dismiss() {
@@ -49,11 +49,13 @@ import { computed, ref, onMounted, onBeforeUnmount } from 'vue'
import AnnouncementBanner from '../common/AnnouncementBanner.vue'
import request from '@/utils/request'
import { usePublicSettings } from '@/composables/usePublicSettings'
import { createPoller } from '@/composables/usePolling'
const { settings: publicSettings } = usePublicSettings()
const siteName = computed(() => publicSettings.value.site_name || 'Team-API')
const announcements = ref<any[]>([])
let timer: ReturnType<typeof setInterval> | null = null
// 登录页公告轮询:页面隐藏时暂停,恢复可见时按剩余时间续排
const announcementPoller = createPoller(fetchAnnouncements, 30 * 60 * 1000)
async function fetchAnnouncements() {
try {
@@ -66,11 +68,11 @@ async function fetchAnnouncements() {
onMounted(() => {
fetchAnnouncements()
timer = setInterval(fetchAnnouncements, 30 * 60 * 1000)
announcementPoller.start()
})
onBeforeUnmount(() => {
if (timer) clearInterval(timer)
announcementPoller.stop()
})
</script>
@@ -1,8 +1,10 @@
import { ref } from 'vue'
import request from '@/utils/request'
import { createPoller } from '@/composables/usePolling'
const unreadCount = ref(0)
let pollTimer: ReturnType<typeof setInterval> | null = null
// 未读数轮询:页面隐藏时暂停,恢复可见时按剩余时间续排
const poller = createPoller(fetchCount, 10 * 60 * 1000)
let onNewCallback: ((count: number) => void) | null = null
async function fetchCount() {
@@ -22,16 +24,11 @@ async function fetchCount() {
function startPolling() {
fetchCount()
if (!pollTimer) {
pollTimer = setInterval(fetchCount, 10 * 60 * 1000)
}
poller.start()
}
function stopPolling() {
if (pollTimer) {
clearInterval(pollTimer)
pollTimer = null
}
poller.stop()
}
function decrement() {
+105
View File
@@ -0,0 +1,105 @@
/**
* 页面可见性感知的轮询工具
*
* 后台标签页的轮询请求既浪费带宽也无展示意义,浏览器自身的定时器节流
* 又不可靠(部分平台放宽到每分钟一次,仍会产生请求)。统一改由
* visibilitychange 显式控制:页面隐藏时停止排队(隐藏期间零请求),
* 恢复可见时若已超过轮询间隔立即补拉一次,未到则按剩余时间续排。
*
* 内部用递归 setTimeout 而非 setInterval:上一次回调完成后才排下一次,
* 慢响应下不会堆叠请求。每个调用点创建独立实例,与组件生命周期无关,
* 组件外(模块级单例)也可使用。
*/
export interface PollerOptions {
/** start() 时是否立即执行一次回调。默认 false,与 setInterval「先等一个完整间隔」的语义一致 */
immediate?: boolean
}
export interface Poller {
/** 启动轮询。幂等:已运行时调用无效果 */
start(): void
/** 停止轮询并移除可见性监听。幂等,可再次 start() 重新开始完整间隔 */
stop(): void
}
export function createPoller(
callback: () => void | Promise<unknown>,
intervalMs: number,
options?: PollerOptions,
): Poller {
/** 是否处于运行中(start 后 stop 前) */
let active = false
/** 回调是否执行中,防重入 */
let inFlight = false
/** 上次回调的执行起点,兼作调度锚点(计算剩余等待时间) */
let lastRunAt = 0
let timer: ReturnType<typeof setTimeout> | null = null
function clearTimer(): void {
if (timer !== null) {
clearTimeout(timer)
timer = null
}
}
function run(): void {
// 防重入:可见性事件与到期定时器可能同时触发
if (inFlight) return
// 隐藏态兜底:定时器到期与 visibilitychange 存在事件顺序竞态,执行前再判一次
if (document.hidden) return
inFlight = true
lastRunAt = Date.now()
// 回调异常吞掉,保证后续调度不中断(调用点自身已有 try/catch,这里是双保险)
Promise.resolve(callback())
.catch(() => {})
.finally(() => {
inFlight = false
if (active) schedule()
})
}
/** 唯一的排队入口:所有续排路径都经过这里,保证同一时刻至多一个待触发定时器 */
function schedule(): void {
if (!active || document.hidden) return
clearTimer()
const remaining = intervalMs - (Date.now() - lastRunAt)
if (remaining <= 0) {
run()
return
}
timer = setTimeout(run, remaining)
}
function onVisibilityChange(): void {
if (!active) return
if (document.hidden) {
// 隐藏:停止排队。在途请求自然完成,完成后发现隐藏态不会再续排
clearTimer()
} else {
// 恢复可见:已超间隔则立即补拉(remaining <= 0 走 run),否则按剩余时间续排
schedule()
}
}
function start(): void {
if (active) return
active = true
// 锚点重置:stop 后再 start 视为全新周期,首个回调在完整间隔之后
lastRunAt = Date.now()
document.addEventListener('visibilitychange', onVisibilityChange)
if (options?.immediate) {
run()
} else {
schedule()
}
}
function stop(): void {
active = false
clearTimer()
document.removeEventListener('visibilitychange', onVisibilityChange)
}
return { start, stop }
}
+10 -16
View File
@@ -6,6 +6,7 @@ import { useNotificationCount } from '@/composables/useNotificationCount'
import { useAnnouncementRead } from '@/composables/useAnnouncementRead'
import { usePublicSettings } from '@/composables/usePublicSettings'
import { useWatermark } from '@/composables/useWatermark'
import { createPoller } from '@/composables/usePolling'
import { formatBilling } from '@/composables/useCurrency'
import { toast } from '@/utils/toast'
import Icon from '@/components/common/Icon.vue'
@@ -26,9 +27,11 @@ const announcePanelOpen = ref(false)
const announceDetailItem = ref<any>(null)
const consoleAnnouncements = ref<any[]>([])
const { unreadCount: announceUnreadCount, markAsRead: markAnnouncementRead, markAllRead: markAllAnnouncementsRead, isRead: isAnnouncementRead } = useAnnouncementRead(consoleAnnouncements)
let announcementTimer: ReturnType<typeof setInterval> | null = null
// 公告轮询:页面隐藏时暂停,恢复可见时按剩余时间续排
const announcementPoller = createPoller(fetchAnnouncements, 30 * 60 * 1000)
const walletBalance = ref<string>('')
let walletTimer: ReturnType<typeof setInterval> | null = null
// 钱包余额轮询:页面隐藏时暂停,恢复可见时按剩余时间续排
const walletPoller = createPoller(fetchWalletBalance, 5 * 60 * 1000)
const memberQuota = ref<{ used: number; limit: number } | null>(null)
const { unreadCount, startPolling: startNotificationPolling, stopPolling: stopNotificationPolling, setOnNewNotification } = useNotificationCount()
setOnNewNotification((newCount: number) => {
@@ -166,10 +169,7 @@ function renderMarkdown(text: string): string {
async function handleLogout() {
stopNotificationPolling()
if (announcementTimer) {
clearInterval(announcementTimer)
announcementTimer = null
}
announcementPoller.stop()
await authStore.logout()
router.push('/tenant/login')
}
@@ -232,11 +232,11 @@ onMounted(async () => {
authStore.loadFromStorage()
document.addEventListener('click', handleClickOutside)
fetchAnnouncements()
announcementTimer = setInterval(fetchAnnouncements, 30 * 60 * 1000)
announcementPoller.start()
startNotificationPolling()
if (canViewWallet.value) {
fetchWalletBalance()
walletTimer = setInterval(fetchWalletBalance, 5 * 60 * 1000)
walletPoller.start()
} else {
fetchMemberQuota()
}
@@ -254,14 +254,8 @@ onMounted(async () => {
onBeforeUnmount(() => {
document.removeEventListener('click', handleClickOutside)
stopNotificationPolling()
if (announcementTimer) {
clearInterval(announcementTimer)
announcementTimer = null
}
if (walletTimer) {
clearInterval(walletTimer)
walletTimer = null
}
announcementPoller.stop()
walletPoller.stop()
unmountWatermark()
})
</script>
@@ -7,6 +7,7 @@ import ModelDistChart from '@/components/charts/ModelDistChart.vue'
import TokenTrendChart from '@/components/charts/TokenTrendChart.vue'
import { useTenantAuthStore } from '@/stores/tenant-auth'
import request from '@/utils/request'
import { createPoller } from '@/composables/usePolling'
interface DayStats {
requests: number
@@ -391,7 +392,8 @@ watch(selectedDays, () => {
})
// 实时 RPM/TPM 依赖 60 秒滑动窗口,仅静默轮询 /tenant/dashboard 保持指标新鲜(30s 一次)
let dashboardTimer: ReturnType<typeof setInterval> | null = null
// 页面隐藏时暂停轮询,恢复可见时已超间隔则立即补拉
const dashboardPoller = createPoller(refreshDashboardSilently, 30000)
async function refreshDashboardSilently() {
try {
@@ -404,11 +406,11 @@ async function refreshDashboardSilently() {
onMounted(() => {
refreshAll()
dashboardTimer = setInterval(refreshDashboardSilently, 30000)
dashboardPoller.start()
})
onBeforeUnmount(() => {
if (dashboardTimer) clearInterval(dashboardTimer)
dashboardPoller.stop()
})
</script>
+6 -9
View File
@@ -9,6 +9,7 @@ import { renderBadge, tableScrollX } from '@/utils/renderUtils'
import request from '@/utils/request'
import { dispatchPayment } from '@/utils/payment'
import { formatBilling, formatOrder, displayCurrency, currencySymbol, displayToCny, cnyToDisplay } from '@/composables/useCurrency'
import { createPoller } from '@/composables/usePolling'
const route = useRoute()
const wallet = ref<any>(null)
@@ -33,7 +34,8 @@ const payResult = ref<'success' | 'fail' | 'processing' | ''>('')
const showFrozenModal = ref(false)
const frozenItems = ref<any[]>([])
const frozenLoading = ref(false)
let frozenTimer: ReturnType<typeof setInterval> | null = null
// 冻结明细轮询:仅弹窗打开期间运行;页面隐藏时暂停,恢复可见时按剩余时间续排
const frozenPoller = createPoller(fetchFrozenItems, 10000)
// Redeem
const showRedeemModal = ref(false)
@@ -210,15 +212,12 @@ async function fetchFrozenItems() {
function openFrozenModal() {
showFrozenModal.value = true
fetchFrozenItems()
frozenTimer = setInterval(fetchFrozenItems, 10000)
frozenPoller.start()
}
function closeFrozenModal() {
showFrozenModal.value = false
if (frozenTimer) {
clearInterval(frozenTimer)
frozenTimer = null
}
frozenPoller.stop()
}
async function openRedeemModal() {
@@ -357,9 +356,7 @@ onMounted(() => {
})
onBeforeUnmount(() => {
if (frozenTimer) {
clearInterval(frozenTimer)
}
frozenPoller.stop()
})
</script>
@@ -2,6 +2,7 @@
import { ref, reactive, computed, watch, onUnmounted } from 'vue'
import { NInput } from 'naive-ui'
import { createPlaygroundApi } from '@/utils/playgroundApi'
import { createPoller } from '@/composables/usePolling'
import { calculateCost } from './calculateCost'
import ParamRefModal from './ParamRefModal.vue'
import Icon from '@/components/common/Icon.vue'
@@ -188,8 +189,9 @@ interface AsyncTask {
}
const asyncTask = ref<AsyncTask | null>(null)
const polling = ref(false)
let pollTimer: ReturnType<typeof setTimeout> | null = null
// 轮询上限:100 次 × 3s ≈ 5 分钟,超过判定超时,防止无限轮询。
// 任务状态轮询:页面隐藏时暂停(任务在服务端继续执行),恢复可见时立即补拉
const taskPoller = createPoller(pollLoop, 3000, { immediate: true })
// 轮询上限:100 次实际轮询 ≈ 5 分钟(页面隐藏期间不计数),超过判定超时,防止无限轮询。
const MAX_POLL_ATTEMPTS = 100
let pollAttempts = 0
@@ -312,15 +314,12 @@ async function generateSync(api: ReturnType<typeof createPlaygroundApi>, body: R
function startPolling() {
polling.value = true
pollAttempts = 0
pollLoop()
taskPoller.start()
}
function stopPolling() {
polling.value = false
if (pollTimer) {
clearTimeout(pollTimer)
pollTimer = null
}
taskPoller.stop()
}
async function pollLoop() {
@@ -329,7 +328,7 @@ async function pollLoop() {
// 轮询次数上限保护:超过上限(约 5 分钟)判定为超时,停止轮询并提示,
// 避免上游卡死 / 后端漏推终态时前端无限轮询。
if (pollAttempts >= MAX_POLL_ATTEMPTS) {
polling.value = false
stopPolling()
asyncTask.value = {
...asyncTask.value,
status: 'FAILURE',
@@ -352,7 +351,7 @@ async function pollLoop() {
}
if (data.status === 'SUCCESS') {
polling.value = false
stopPolling()
// 多图:优先用 data 数组渲染全部图片;回退到单 url(向后兼容旧后端)。
if (Array.isArray(data.data) && data.data.length > 0) {
images.value = data.data
@@ -364,16 +363,12 @@ async function pollLoop() {
return
}
if (data.status === 'FAILURE') {
polling.value = false
stopPolling()
return
}
} catch {
// 轮询失败不中断,继续尝试
}
if (polling.value) {
pollTimer = setTimeout(pollLoop, 3000)
}
}
function imageSrc(img: ImageResult): string {
@@ -1,7 +1,8 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, onUnmounted } from 'vue'
import { NInput, NInputNumber } from 'naive-ui'
import { createPlaygroundApi } from '@/utils/playgroundApi'
import { createPoller } from '@/composables/usePolling'
import Icon from '@/components/common/Icon.vue'
import BaseSelect from '../../../components/common/BaseSelect.vue'
@@ -51,7 +52,8 @@ interface TaskInfo {
}
const currentTask = ref<TaskInfo | null>(null)
const polling = ref(false)
let pollTimer: ReturnType<typeof setTimeout> | null = null
// 任务状态轮询:页面隐藏时暂停(任务在服务端继续执行),恢复可见时立即补拉
const taskPoller = createPoller(pollLoop, 3000, { immediate: true })
const statusLabel: Record<string, string> = {
SUBMITTED: '已提交',
@@ -129,15 +131,12 @@ async function submitTask() {
function startPolling() {
polling.value = true
pollLoop()
taskPoller.start()
}
function stopPolling() {
polling.value = false
if (pollTimer) {
clearTimeout(pollTimer)
pollTimer = null
}
taskPoller.stop()
}
async function pollLoop() {
@@ -158,18 +157,19 @@ async function pollLoop() {
}
if (data.status === 'SUCCESS' || data.status === 'FAILURE') {
polling.value = false
stopPolling()
return
}
} catch {
// 轮询失败不中断,继续尝试
}
if (polling.value) {
pollTimer = setTimeout(pollLoop, 3000)
}
}
// 组件卸载时停止轮询,避免路由离开后仍持续请求任务状态
onUnmounted(() => {
stopPolling()
})
function resetTask() {
stopPolling()
currentTask.value = null