207 lines
6.1 KiB
JavaScript
207 lines
6.1 KiB
JavaScript
import { ref, watch, onUnmounted } from 'vue'
|
||
import { getActivityContributionsLatestApi } from '@/utils/api.js'
|
||
|
||
/**
|
||
* 贡献轮询逻辑 composable
|
||
* @param {Ref<string>} 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
|
||
}
|
||
} |