From 7e1da6139745732310d45d5989ac2fcc04e808b1 Mon Sep 17 00:00:00 2001 From: zheng020 Date: Thu, 14 May 2026 12:16:35 +0800 Subject: [PATCH] feat: update contribution polling per latest spec - timestamp+id polling, combo_count, fading animation --- .../components/ContributionList.vue | 45 +++------ .../composables/useContributionPolling.js | 93 ++++++++++++------- 2 files changed, 73 insertions(+), 65 deletions(-) diff --git a/frontend/pages/support-activity/components/ContributionList.vue b/frontend/pages/support-activity/components/ContributionList.vue index 6151923..6418ad5 100644 --- a/frontend/pages/support-activity/components/ContributionList.vue +++ b/frontend/pages/support-activity/components/ContributionList.vue @@ -8,21 +8,21 @@ v-for="(record, index) in records" :key="record.id" class="contribution-item" - :class="{ 'new-item': index === 0 && isNewRecord(record.id) }" + :class="{ 'new-item': index === 0, 'fading-out': record.fading }" > {{ record.user_nickname }} 贡献了 {{ record.item_name }} - x{{ record.quantity }} + x{{ record.combo_count > 1 ? record.combo_count : record.quantity }} @@ -125,6 +99,10 @@ defineExpose({ animation: slideIn 0.3s ease-out; } +.fading-out { + animation: fadeOut 0.5s forwards; +} + .user-avatar { width: 48rpx; height: 48rpx; @@ -186,4 +164,9 @@ defineExpose({ transform: translateX(0); } } + +@keyframes fadeOut { + from { opacity: 1; } + to { opacity: 0; } +} \ No newline at end of file diff --git a/frontend/pages/support-activity/composables/useContributionPolling.js b/frontend/pages/support-activity/composables/useContributionPolling.js index 8ddba82..df18828 100644 --- a/frontend/pages/support-activity/composables/useContributionPolling.js +++ b/frontend/pages/support-activity/composables/useContributionPolling.js @@ -5,7 +5,7 @@ import { getActivityContributionsLatestApi } from '@/utils/api.js' * 贡献轮询逻辑 composable * @param {Ref} activityId - 活动ID * @param {boolean} isPageActive - 页面是否可见 - * @returns {Object} records, loading, error, start, stop, refresh + * @returns {Object} records, visible, loading, error, start, stop, reset */ export function useContributionPolling(activityId, isPageActive) { const MAX_RECORDS = 5 @@ -13,22 +13,42 @@ export function useContributionPolling(activityId, isPageActive) { const RECORD_TTL = 5000 // 每条记录 5 秒后消失 const records = ref([]) + const visible = ref(true) const loading = ref(false) const error = ref(null) - let latestId = 0 + let latestTimestamp = 0 // 当前列表中最新记录的时间戳 + let latestId = 0 // 当前列表中最新记录的 ID(同时间戳内二次筛选用) 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) + } + // 清除淡出状态(新数据到来时重置) + 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) + } }, RECORD_TTL) recordTimers.set(record.id, timer) } @@ -38,7 +58,7 @@ export function useContributionPolling(activityId, isPageActive) { if (!activityId.value) return try { - const res = await getActivityContributionsLatestApi(activityId.value, latestId, 1) + const res = await getActivityContributionsLatestApi(activityId.value, latestTimestamp, latestId, 1) // 处理 code 不为 200 的情况(静默忽略) if (res.code !== 200) return @@ -48,18 +68,19 @@ export function useContributionPolling(activityId, isPageActive) { const newRecord = newRecords[0] - // 检测到新记录(id > latestId) - if (newRecord.id > latestId) { - // 重置所有现有记录的计时器 - records.value.forEach(resetRecordTimer) + // 检测到新记录(时间戳更新,或时间戳相同但 ID 更新) + const isNew = newRecord.created_at > latestTimestamp || + (newRecord.created_at === latestTimestamp && newRecord.id > latestId) + if (isNew) { + // 重置所有现有记录的计时器(新数据到来,刷新列表) + records.value.forEach(resetRecordTimer) // 新记录插入到列表头部 records.value = [newRecord, ...records.value].slice(0, MAX_RECORDS) - // 为新记录启动消失计时器 resetRecordTimer(newRecord) - - // 更新 latestId + // 更新时间戳和 ID + latestTimestamp = newRecord.created_at latestId = newRecord.id } } catch (e) { @@ -76,7 +97,7 @@ export function useContributionPolling(activityId, isPageActive) { error.value = null try { - const res = await getActivityContributionsLatestApi(activityId.value, 0, MAX_RECORDS) + const res = await getActivityContributionsLatestApi(activityId.value, 0, 0, MAX_RECORDS) if (res.code !== 200) { throw new Error(res.message || '获取贡献记录失败') @@ -94,8 +115,11 @@ export function useContributionPolling(activityId, isPageActive) { // 为每条记录启动计时器 newRecords.forEach(record => resetRecordTimer(record)) - // 更新 latestId - latestId = newRecords.length > 0 ? newRecords[newRecords.length - 1].id : 0 + // 更新 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) @@ -110,42 +134,42 @@ export function useContributionPolling(activityId, isPageActive) { if (!activityId.value) return isPolling = true - latestId = 0 - - // 全量拉取首次 - fetchAll() - - // 启动定时轮询 + // 如果 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.value = [] - latestId = 0 + // 不清空 records、latestTimestamp、latestId,保留状态以便恢复 } - // 强制刷新(全量拉取) - function refresh() { + // 重置所有状态(切换活动时用) + function reset() { stop() - start() + latestTimestamp = 0 + latestId = 0 + records.value = [] } // 监听页面可见性 watch(isPageActive, (active) => { if (active) { + visible.value = true start() } else { + visible.value = false stop() } }, { immediate: true }) @@ -153,7 +177,7 @@ export function useContributionPolling(activityId, isPageActive) { // 监听 activityId 变化 watch(activityId, (newId, oldId) => { if (newId !== oldId) { - stop() + reset() if (isPageActive.value && newId) { start() } @@ -162,15 +186,16 @@ export function useContributionPolling(activityId, isPageActive) { // 组件卸载时清理 onUnmounted(() => { - stop() + reset() }) return { records, + visible, loading, error, start, stop, - refresh + reset } } \ No newline at end of file