topfans/frontend/pages/support-activity/composables/useContributionPolling.js
zheng020 cb96326b3d feat: add useContributionPolling composable for realtime contributions
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 11:15:20 +08:00

176 lines
4.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ref, watch, onUnmounted } from 'vue'
import { getActivityContributionsLatestApi } from '@/utils/api.js'
/**
* 贡献轮询逻辑 composable
* @param {Ref<string>} activityId - 活动ID
* @param {boolean} isPageActive - 页面是否可见
* @returns {Object} records, loading, error, start, stop, refresh
*/
export function useContributionPolling(activityId, isPageActive) {
const MAX_RECORDS = 5
const POLL_INTERVAL = 1000 // 每秒拉取
const RECORD_TTL = 5000 // 每条记录 5 秒后消失
const records = ref([])
const loading = ref(false)
const error = ref(null)
let latestId = 0
let pollingTimer = null
let isPolling = false
const recordTimers = new Map()
// 重置记录计时器
function resetRecordTimer(record) {
if (recordTimers.has(record.id)) {
clearTimeout(recordTimers.get(record.id))
}
const timer = setTimeout(() => {
records.value = records.value.filter(r => r.id !== record.id)
recordTimers.delete(record.id)
}, RECORD_TTL)
recordTimers.set(record.id, timer)
}
// 获取最新贡献记录
async function fetchLatest() {
if (!activityId.value) return
try {
const res = await getActivityContributionsLatestApi(activityId.value, latestId, 1)
// 处理 code 不为 200 的情况(静默忽略)
if (res.code !== 200) return
const newRecords = res.data?.records || []
if (newRecords.length === 0) return
const newRecord = newRecords[0]
// 检测到新记录id > latestId
if (newRecord.id > latestId) {
// 重置所有现有记录的计时器
records.value.forEach(resetRecordTimer)
// 新记录插入到列表头部
records.value = [newRecord, ...records.value].slice(0, MAX_RECORDS)
// 为新记录启动消失计时器
resetRecordTimer(newRecord)
// 更新 latestId
latestId = newRecord.id
}
} catch (e) {
// 网络错误等,静默忽略,继续等待下一次轮询
console.error('[useContributionPolling] fetchLatest error:', e)
}
}
// 全量拉取(首次或重新开始)
async function fetchAll() {
if (!activityId.value) return
loading.value = true
error.value = null
try {
const res = await getActivityContributionsLatestApi(activityId.value, 0, MAX_RECORDS)
if (res.code !== 200) {
throw new Error(res.message || '获取贡献记录失败')
}
const newRecords = res.data?.records || []
// 清除所有计时器
recordTimers.forEach(timer => clearTimeout(timer))
recordTimers.clear()
// 重置列表
records.value = newRecords
// 为每条记录启动计时器
newRecords.forEach(record => resetRecordTimer(record))
// 更新 latestId
latestId = newRecords.length > 0 ? newRecords[newRecords.length - 1].id : 0
} catch (e) {
error.value = e.message || '获取贡献记录失败'
console.error('[useContributionPolling] fetchAll error:', e)
} finally {
loading.value = false
}
}
// 开始轮询
function start() {
if (pollingTimer) return
if (!activityId.value) return
isPolling = true
latestId = 0
// 全量拉取首次
fetchAll()
// 启动定时轮询
pollingTimer = setInterval(fetchLatest, POLL_INTERVAL)
}
// 停止轮询
function stop() {
if (pollingTimer) {
clearInterval(pollingTimer)
pollingTimer = null
}
// 清除所有计时器
recordTimers.forEach(timer => clearTimeout(timer))
recordTimers.clear()
isPolling = false
records.value = []
latestId = 0
}
// 强制刷新(全量拉取)
function refresh() {
stop()
start()
}
// 监听页面可见性
watch(isPageActive, (active) => {
if (active) {
start()
} else {
stop()
}
}, { immediate: true })
// 监听 activityId 变化
watch(activityId, (newId, oldId) => {
if (newId !== oldId) {
stop()
if (isPageActive.value && newId) {
start()
}
}
})
// 组件卸载时清理
onUnmounted(() => {
stop()
})
return {
records,
loading,
error,
start,
stop,
refresh
}
}