feat: add useContributionPolling composable for realtime contributions
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
f3b11c5ceb
commit
cb96326b3d
@ -0,0 +1,176 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user