import { ref, watch, onUnmounted } from 'vue' import { getActivityContributionsLatestApi } from '@/utils/api.js' /** * 贡献轮询逻辑 composable * @param {Ref} activityId - 活动ID * @param {boolean} isPageActive - 页面是否可见 * @param {Object} [options] - 配置项 * @param {boolean} [options.enableTTL=false] - 是否开启"每条记录 5 秒后自动消失"的开关(默认关闭,记录一直保留直到被新数据挤掉) * @param {number} [options.ttlMs=5000] - 自动消失的延时(毫秒),仅在 enableTTL=true 时生效 * @returns {Object} records, visible, loading, error, start, stop, reset */ export function useContributionPolling(activityId, isPageActive, options = {}) { const { enableTTL = false, ttlMs = 5000 } = options const MAX_RECORDS = 5 const POLL_INTERVAL = 1000 // 每秒拉取 const records = ref([]) const visible = ref(true) const loading = ref(false) const error = ref(null) let latestTimestamp = 0 // 当前列表中最新记录的时间戳 let latestId = 0 // 当前列表中最新记录的 ID(同时间戳内二次筛选用) let pollingTimer = null let isPolling = false const recordTimers = new Map() // 重置记录计时器 function resetRecordTimer(record) { // TTL 功能未开启时:不做任何定时器与淡出处理,记录会一直保留 if (!enableTTL) return // 清除已有定时器 if (recordTimers.has(record.id)) { clearTimeout(recordTimers.get(record.id)) recordTimers.delete(record.id) } // 清除淡出状态(新数据到来时重置) const target = records.value.find(r => r.id === record.id) if (target) { target.fading = false } // 设置新的消失定时器 const timer = setTimeout(() => { // 标记为淡出状态 const target = records.value.find(r => r.id === record.id) if (target) { target.fading = true // 等待动画完成后移除 setTimeout(() => { records.value = records.value.filter(r => r.id !== record.id) recordTimers.delete(record.id) }, 500) } else { recordTimers.delete(record.id) } }, ttlMs) recordTimers.set(record.id, timer) } // 获取最新贡献记录 async function fetchLatest() { if (!activityId.value) return try { const res = await getActivityContributionsLatestApi(activityId.value, latestTimestamp, latestId, 3) // 处理 code 不为 200 的情况(静默忽略) if (res.code !== 0) return const newRecords = res.data?.records || [] if (newRecords.length === 0) return // API 返回的 records 按 created_at DESC, id DESC 排序。 // 只需比对第一条;后面的记录时间戳/ID 必然不更小。 const firstRecord = newRecords[0] const isNew = firstRecord.created_at > latestTimestamp || (firstRecord.created_at === latestTimestamp && firstRecord.id > latestId) if (isNew) { // 重置所有现有记录的计时器(新数据到来,刷新列表) records.value.forEach(resetRecordTimer) // 一次性插入本轮拉取到的全部新记录(API 已按"新→旧"排序,新的在前) records.value = [...newRecords, ...records.value].slice(0, MAX_RECORDS) // 为每条新记录启动消失计时器 newRecords.forEach(resetRecordTimer) // 更新游标到最新一条 latestTimestamp = firstRecord.created_at latestId = firstRecord.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, 0, MAX_RECORDS) if (res.code !== 0) { 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)) // 更新 latestTimestamp 和 latestId if (newRecords.length > 0) { latestTimestamp = newRecords[0].created_at latestId = newRecords[0].id } } 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 // 如果 latestTimestamp 为 0,说明是首次或切换活动,清空列表 if (latestTimestamp === 0) { records.value = [] } fetchLatest() pollingTimer = setInterval(fetchLatest, POLL_INTERVAL) } // 停止轮询(保留状态) function stop() { if (pollingTimer) { clearInterval(pollingTimer) pollingTimer = null } // 停止所有记录的计时器 recordTimers.forEach(timer => clearTimeout(timer)) recordTimers.clear() isPolling = false // 不清空 records、latestTimestamp、latestId,保留状态以便恢复 } // 重置所有状态(切换活动时用) function reset() { stop() latestTimestamp = 0 latestId = 0 records.value = [] } // 监听页面可见性 watch(isPageActive, (active) => { if (active) { visible.value = true start() } else { visible.value = false stop() } }, { immediate: true }) // 监听 activityId 变化 watch(activityId, (newId, oldId) => { if (newId !== oldId) { reset() if (isPageActive.value && newId) { start() } } }) // 组件卸载时清理 onUnmounted(() => { reset() }) return { records, visible, loading, error, start, stop, reset } }