feat: update contribution polling per latest spec - timestamp+id polling, combo_count, fading animation
This commit is contained in:
parent
ba075fffd3
commit
7e1da61397
@ -8,21 +8,21 @@
|
|||||||
v-for="(record, index) in records"
|
v-for="(record, index) in records"
|
||||||
:key="record.id"
|
:key="record.id"
|
||||||
class="contribution-item"
|
class="contribution-item"
|
||||||
:class="{ 'new-item': index === 0 && isNewRecord(record.id) }"
|
:class="{ 'new-item': index === 0, 'fading-out': record.fading }"
|
||||||
>
|
>
|
||||||
<image class="user-avatar" :src="record.user_avatar" mode="aspectFill" />
|
<image class="user-avatar" :src="record.user_avatar" mode="aspectFill" />
|
||||||
<text class="user-nickname">{{ record.user_nickname }}</text>
|
<text class="user-nickname">{{ record.user_nickname }}</text>
|
||||||
<text class="contribute-text">贡献了</text>
|
<text class="contribute-text">贡献了</text>
|
||||||
<image class="item-icon" :src="record.item_icon" mode="aspectFill" />
|
<image class="item-icon" :src="record.item_icon" mode="aspectFill" />
|
||||||
<text class="item-name">{{ record.item_name }}</text>
|
<text class="item-name">{{ record.item_name }}</text>
|
||||||
<text class="item-quantity">x{{ record.quantity }}</text>
|
<text class="item-quantity">x{{ record.combo_count > 1 ? record.combo_count : record.quantity }}</text>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { useContributionPolling } from '../composables/useContributionPolling.js'
|
import { useContributionPolling } from '../composables/useContributionPolling.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -32,40 +32,14 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const visible = ref(true)
|
|
||||||
const isPageActive = ref(true)
|
const isPageActive = ref(true)
|
||||||
const newRecordIds = ref([])
|
|
||||||
|
|
||||||
// 使用轮询 composable
|
// 使用轮询 composable
|
||||||
const { records, loading, error, start, stop, refresh } = useContributionPolling(
|
const { records, visible, loading, error, start, stop, reset } = useContributionPolling(
|
||||||
computed(() => props.activityId),
|
computed(() => props.activityId),
|
||||||
isPageActive
|
isPageActive
|
||||||
)
|
)
|
||||||
|
|
||||||
// 判断记录是否为新记录(刚加入的)
|
|
||||||
function isNewRecord(id) {
|
|
||||||
return newRecordIds.value.includes(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听 records 变化,标记新记录
|
|
||||||
watch(records, (newRecords, oldRecords) => {
|
|
||||||
if (newRecords.length > 0 && oldRecords) {
|
|
||||||
const newIds = newRecords.map(r => r.id)
|
|
||||||
const oldIds = oldRecords.map(r => r.id)
|
|
||||||
|
|
||||||
// 找出新增的记录ID
|
|
||||||
for (const id of newIds) {
|
|
||||||
if (!oldIds.includes(id)) {
|
|
||||||
newRecordIds.value.push(id)
|
|
||||||
// 2秒后移除新记录标记
|
|
||||||
setTimeout(() => {
|
|
||||||
newRecordIds.value = newRecordIds.value.filter(i => i !== id)
|
|
||||||
}, 2000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, { deep: true })
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
isPageActive.value = true
|
isPageActive.value = true
|
||||||
})
|
})
|
||||||
@ -77,7 +51,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
// 暴露给父组件使用
|
// 暴露给父组件使用
|
||||||
defineExpose({
|
defineExpose({
|
||||||
refresh
|
reset
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -125,6 +99,10 @@ defineExpose({
|
|||||||
animation: slideIn 0.3s ease-out;
|
animation: slideIn 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fading-out {
|
||||||
|
animation: fadeOut 0.5s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
.user-avatar {
|
.user-avatar {
|
||||||
width: 48rpx;
|
width: 48rpx;
|
||||||
height: 48rpx;
|
height: 48rpx;
|
||||||
@ -186,4 +164,9 @@ defineExpose({
|
|||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
from { opacity: 1; }
|
||||||
|
to { opacity: 0; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -5,7 +5,7 @@ import { getActivityContributionsLatestApi } from '@/utils/api.js'
|
|||||||
* 贡献轮询逻辑 composable
|
* 贡献轮询逻辑 composable
|
||||||
* @param {Ref<string>} activityId - 活动ID
|
* @param {Ref<string>} activityId - 活动ID
|
||||||
* @param {boolean} isPageActive - 页面是否可见
|
* @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) {
|
export function useContributionPolling(activityId, isPageActive) {
|
||||||
const MAX_RECORDS = 5
|
const MAX_RECORDS = 5
|
||||||
@ -13,22 +13,42 @@ export function useContributionPolling(activityId, isPageActive) {
|
|||||||
const RECORD_TTL = 5000 // 每条记录 5 秒后消失
|
const RECORD_TTL = 5000 // 每条记录 5 秒后消失
|
||||||
|
|
||||||
const records = ref([])
|
const records = ref([])
|
||||||
|
const visible = ref(true)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
|
|
||||||
let latestId = 0
|
let latestTimestamp = 0 // 当前列表中最新记录的时间戳
|
||||||
|
let latestId = 0 // 当前列表中最新记录的 ID(同时间戳内二次筛选用)
|
||||||
let pollingTimer = null
|
let pollingTimer = null
|
||||||
let isPolling = false
|
let isPolling = false
|
||||||
const recordTimers = new Map()
|
const recordTimers = new Map()
|
||||||
|
|
||||||
// 重置记录计时器
|
// 重置记录计时器
|
||||||
function resetRecordTimer(record) {
|
function resetRecordTimer(record) {
|
||||||
|
// 清除已有定时器
|
||||||
if (recordTimers.has(record.id)) {
|
if (recordTimers.has(record.id)) {
|
||||||
clearTimeout(recordTimers.get(record.id))
|
clearTimeout(recordTimers.get(record.id))
|
||||||
}
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
records.value = records.value.filter(r => r.id !== record.id)
|
|
||||||
recordTimers.delete(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)
|
}, RECORD_TTL)
|
||||||
recordTimers.set(record.id, timer)
|
recordTimers.set(record.id, timer)
|
||||||
}
|
}
|
||||||
@ -38,7 +58,7 @@ export function useContributionPolling(activityId, isPageActive) {
|
|||||||
if (!activityId.value) return
|
if (!activityId.value) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await getActivityContributionsLatestApi(activityId.value, latestId, 1)
|
const res = await getActivityContributionsLatestApi(activityId.value, latestTimestamp, latestId, 1)
|
||||||
|
|
||||||
// 处理 code 不为 200 的情况(静默忽略)
|
// 处理 code 不为 200 的情况(静默忽略)
|
||||||
if (res.code !== 200) return
|
if (res.code !== 200) return
|
||||||
@ -48,18 +68,19 @@ export function useContributionPolling(activityId, isPageActive) {
|
|||||||
|
|
||||||
const newRecord = newRecords[0]
|
const newRecord = newRecords[0]
|
||||||
|
|
||||||
// 检测到新记录(id > latestId)
|
// 检测到新记录(时间戳更新,或时间戳相同但 ID 更新)
|
||||||
if (newRecord.id > latestId) {
|
const isNew = newRecord.created_at > latestTimestamp ||
|
||||||
// 重置所有现有记录的计时器
|
(newRecord.created_at === latestTimestamp && newRecord.id > latestId)
|
||||||
records.value.forEach(resetRecordTimer)
|
|
||||||
|
|
||||||
|
if (isNew) {
|
||||||
|
// 重置所有现有记录的计时器(新数据到来,刷新列表)
|
||||||
|
records.value.forEach(resetRecordTimer)
|
||||||
// 新记录插入到列表头部
|
// 新记录插入到列表头部
|
||||||
records.value = [newRecord, ...records.value].slice(0, MAX_RECORDS)
|
records.value = [newRecord, ...records.value].slice(0, MAX_RECORDS)
|
||||||
|
|
||||||
// 为新记录启动消失计时器
|
// 为新记录启动消失计时器
|
||||||
resetRecordTimer(newRecord)
|
resetRecordTimer(newRecord)
|
||||||
|
// 更新时间戳和 ID
|
||||||
// 更新 latestId
|
latestTimestamp = newRecord.created_at
|
||||||
latestId = newRecord.id
|
latestId = newRecord.id
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -76,7 +97,7 @@ export function useContributionPolling(activityId, isPageActive) {
|
|||||||
error.value = null
|
error.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await getActivityContributionsLatestApi(activityId.value, 0, MAX_RECORDS)
|
const res = await getActivityContributionsLatestApi(activityId.value, 0, 0, MAX_RECORDS)
|
||||||
|
|
||||||
if (res.code !== 200) {
|
if (res.code !== 200) {
|
||||||
throw new Error(res.message || '获取贡献记录失败')
|
throw new Error(res.message || '获取贡献记录失败')
|
||||||
@ -94,8 +115,11 @@ export function useContributionPolling(activityId, isPageActive) {
|
|||||||
// 为每条记录启动计时器
|
// 为每条记录启动计时器
|
||||||
newRecords.forEach(record => resetRecordTimer(record))
|
newRecords.forEach(record => resetRecordTimer(record))
|
||||||
|
|
||||||
// 更新 latestId
|
// 更新 latestTimestamp 和 latestId
|
||||||
latestId = newRecords.length > 0 ? newRecords[newRecords.length - 1].id : 0
|
if (newRecords.length > 0) {
|
||||||
|
latestTimestamp = newRecords[0].created_at
|
||||||
|
latestId = newRecords[0].id
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e.message || '获取贡献记录失败'
|
error.value = e.message || '获取贡献记录失败'
|
||||||
console.error('[useContributionPolling] fetchAll error:', e)
|
console.error('[useContributionPolling] fetchAll error:', e)
|
||||||
@ -110,42 +134,42 @@ export function useContributionPolling(activityId, isPageActive) {
|
|||||||
if (!activityId.value) return
|
if (!activityId.value) return
|
||||||
|
|
||||||
isPolling = true
|
isPolling = true
|
||||||
latestId = 0
|
// 如果 latestTimestamp 为 0,说明是首次或切换活动,清空列表
|
||||||
|
if (latestTimestamp === 0) {
|
||||||
// 全量拉取首次
|
records.value = []
|
||||||
fetchAll()
|
}
|
||||||
|
fetchLatest()
|
||||||
// 启动定时轮询
|
|
||||||
pollingTimer = setInterval(fetchLatest, POLL_INTERVAL)
|
pollingTimer = setInterval(fetchLatest, POLL_INTERVAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止轮询
|
// 停止轮询(保留状态)
|
||||||
function stop() {
|
function stop() {
|
||||||
if (pollingTimer) {
|
if (pollingTimer) {
|
||||||
clearInterval(pollingTimer)
|
clearInterval(pollingTimer)
|
||||||
pollingTimer = null
|
pollingTimer = null
|
||||||
}
|
}
|
||||||
|
// 停止所有记录的计时器
|
||||||
// 清除所有计时器
|
|
||||||
recordTimers.forEach(timer => clearTimeout(timer))
|
recordTimers.forEach(timer => clearTimeout(timer))
|
||||||
recordTimers.clear()
|
recordTimers.clear()
|
||||||
|
|
||||||
isPolling = false
|
isPolling = false
|
||||||
records.value = []
|
// 不清空 records、latestTimestamp、latestId,保留状态以便恢复
|
||||||
latestId = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 强制刷新(全量拉取)
|
// 重置所有状态(切换活动时用)
|
||||||
function refresh() {
|
function reset() {
|
||||||
stop()
|
stop()
|
||||||
start()
|
latestTimestamp = 0
|
||||||
|
latestId = 0
|
||||||
|
records.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听页面可见性
|
// 监听页面可见性
|
||||||
watch(isPageActive, (active) => {
|
watch(isPageActive, (active) => {
|
||||||
if (active) {
|
if (active) {
|
||||||
|
visible.value = true
|
||||||
start()
|
start()
|
||||||
} else {
|
} else {
|
||||||
|
visible.value = false
|
||||||
stop()
|
stop()
|
||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
@ -153,7 +177,7 @@ export function useContributionPolling(activityId, isPageActive) {
|
|||||||
// 监听 activityId 变化
|
// 监听 activityId 变化
|
||||||
watch(activityId, (newId, oldId) => {
|
watch(activityId, (newId, oldId) => {
|
||||||
if (newId !== oldId) {
|
if (newId !== oldId) {
|
||||||
stop()
|
reset()
|
||||||
if (isPageActive.value && newId) {
|
if (isPageActive.value && newId) {
|
||||||
start()
|
start()
|
||||||
}
|
}
|
||||||
@ -162,15 +186,16 @@ export function useContributionPolling(activityId, isPageActive) {
|
|||||||
|
|
||||||
// 组件卸载时清理
|
// 组件卸载时清理
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stop()
|
reset()
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
records,
|
records,
|
||||||
|
visible,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
start,
|
start,
|
||||||
stop,
|
stop,
|
||||||
refresh
|
reset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user