feat: add ContributionList component for realtime display
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
cb96326b3d
commit
a2052b673c
189
frontend/pages/support-activity/components/ContributionList.vue
Normal file
189
frontend/pages/support-activity/components/ContributionList.vue
Normal file
@ -0,0 +1,189 @@
|
||||
<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>
|
||||
Loading…
Reference in New Issue
Block a user