diff --git a/frontend/pages/support-activity/composables/useContributionPolling.js b/frontend/pages/support-activity/composables/useContributionPolling.js new file mode 100644 index 0000000..8ddba82 --- /dev/null +++ b/frontend/pages/support-activity/composables/useContributionPolling.js @@ -0,0 +1,176 @@ +import { ref, watch, onUnmounted } from 'vue' +import { getActivityContributionsLatestApi } from '@/utils/api.js' + +/** + * 贡献轮询逻辑 composable + * @param {Ref} 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 + } +} \ No newline at end of file