topfans/frontend/pages/support-activity/components/ContributionList.vue
zheng020 a2052b673c feat: add ContributionList component for realtime display
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 11:26:46 +08:00

189 lines
3.7 KiB
Vue

<template>
<view class="contribution-list" v-if="visible">
<view class="list-header">
<text class="header-title">实时贡献</text>
</view>
<scroll-view class="list-content" scroll-y>
<view
v-for="(record, index) in records"
:key="record.id"
class="contribution-item"
:class="{ 'new-item': index === 0 && isNewRecord(record.id) }"
>
<image class="user-avatar" :src="record.user_avatar" mode="aspectFill" />
<text class="user-nickname">{{ record.user_nickname }}</text>
<text class="contribute-text">贡献了</text>
<image class="item-icon" :src="record.item_icon" mode="aspectFill" />
<text class="item-name">{{ record.item_name }}</text>
<text class="item-quantity">x{{ record.quantity }}</text>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useContributionPolling } from '../composables/useContributionPolling.js'
const props = defineProps({
activityId: {
type: String,
required: true
}
})
const visible = ref(true)
const isPageActive = ref(true)
const newRecordIds = ref(new Set())
// 使用轮询 composable
const { records, loading, error, start, stop, refresh } = useContributionPolling(
computed(() => props.activityId),
isPageActive
)
// 判断记录是否为新记录(刚加入的)
function isNewRecord(id) {
return newRecordIds.value.has(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.add(id)
// 2秒后移除新记录标记
setTimeout(() => {
newRecordIds.value.delete(id)
}, 2000)
}
}
}
}, { deep: true })
onMounted(() => {
isPageActive.value = true
})
// 页面生命周期集成
onUnmounted(() => {
stop()
})
// 暴露给父组件使用
defineExpose({
refresh
})
</script>
<style scoped>
.contribution-list {
width: 100%;
padding: 0 24rpx;
margin-top: 20rpx;
}
.list-header {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
}
.header-title {
font-size: 28rpx;
font-weight: bold;
color: #fff;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.5);
}
.list-content {
height: 200rpx;
background: rgba(0, 0, 0, 0.3);
border-radius: 16rpx;
padding: 16rpx;
}
.contribution-item {
display: flex;
align-items: center;
padding: 12rpx 0;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);
animation: fadeIn 0.3s ease-out;
}
.contribution-item:last-child {
border-bottom: none;
}
.new-item {
animation: slideIn 0.3s ease-out;
}
.user-avatar {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
margin-right: 12rpx;
}
.user-nickname {
font-size: 24rpx;
color: #fff;
max-width: 120rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.contribute-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.7);
margin: 0 12rpx;
}
.item-icon {
width: 36rpx;
height: 36rpx;
margin-right: 8rpx;
}
.item-name {
font-size: 24rpx;
color: #fff;
margin-right: 8rpx;
}
.item-quantity {
font-size: 24rpx;
color: #ffcc00;
font-weight: bold;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20rpx);
}
to {
opacity: 1;
transform: translateX(0);
}
}
</style>