improve(web):优化前端定时任务执行策略,仅在页面激活时发送请求
This commit is contained in:
1 parent
a266d3cd83
commit
3d4e4d125a
13 files changed
+290
-95
No files matched your search
@@ -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 }
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user