716 lines
18 KiB
Markdown
716 lines
18 KiB
Markdown
# ActivityRankingModal 实现计划
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 创建 `ActivityRankingModal.vue` 弹窗组件,从 `ThemeBanner` 点击触发,显示单活动的排名数据
|
||
|
||
**Architecture:** 复用 `RankingModal.vue` 的现有样式主题和数据转换逻辑,创建专用于活动排名的简化弹窗组件
|
||
|
||
**Tech Stack:** Vue 3 Composition API, uni-app, 复用现有 RankingModal/TOP3Card/RankingListItem 组件
|
||
|
||
---
|
||
|
||
## 文件结构
|
||
|
||
```
|
||
frontend/pages/support-activity/
|
||
├── components/
|
||
│ ├── ActivityRankingModal.vue ← 新建
|
||
│ ├── ThemeBanner.vue ← 修改:添加点击事件触发弹窗
|
||
│ ├── TOP3Card.vue ← 复用(来自 pages/components/)
|
||
│ └── RankingListItem.vue ← 复用(来自 pages/components/)
|
||
└── index.vue ← 修改:引入并使用 ActivityRankingModal
|
||
|
||
frontend/utils/api.js ← 无需修改(getActivityRankingApi 已存在)
|
||
docs/superpowers/specs/2026-05-14-activity-ranking-modal-design.md ← 已存在
|
||
```
|
||
|
||
---
|
||
|
||
## 任务分解
|
||
|
||
### Task 1: 创建 ActivityRankingModal.vue 组件
|
||
|
||
**文件:**
|
||
- 创建: `frontend/pages/support-activity/components/ActivityRankingModal.vue`
|
||
|
||
- [ ] **Step 1: 创建基础模板和样式结构**
|
||
|
||
```vue
|
||
<template>
|
||
<view v-if="visible" class="modal-wrapper">
|
||
<transition name="fade">
|
||
<view v-if="visible" class="modal-mask" @tap="handleClose"></view>
|
||
</transition>
|
||
|
||
<transition name="slide-up">
|
||
<view v-if="visible" class="modal-container" @tap.stop>
|
||
<!-- 背景 -->
|
||
<view class="modal-background"></view>
|
||
|
||
<!-- 内容区域 -->
|
||
<view class="modal-content">
|
||
<!-- 头部 -->
|
||
<view class="header-section">
|
||
<text class="activity-title">{{ activityTitle }}</text>
|
||
<view class="close-btn" @tap="handleClose">
|
||
<text class="close-icon">×</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 滚动内容区 -->
|
||
<scroll-view
|
||
class="scrollable-content"
|
||
scroll-y="true"
|
||
:show-scrollbar="false"
|
||
:refresher-enabled="true"
|
||
:refresher-triggered="isRefreshing"
|
||
@refresherrefresh="handleRefresh"
|
||
@scrolltolower="handleScrollToLower"
|
||
:lower-threshold="100"
|
||
>
|
||
<!-- 加载中 -->
|
||
<view v-if="isLoading && !isRefreshing" class="loading-container">
|
||
<view class="loading-spinner"></view>
|
||
<text class="loading-text">加载中...</text>
|
||
</view>
|
||
|
||
<!-- 错误状态 -->
|
||
<view v-else-if="errorMessage" class="error-container">
|
||
<text class="error-text">{{ errorMessage }}</text>
|
||
<button class="retry-button" @tap="loadRankingData">重试</button>
|
||
</view>
|
||
|
||
<!-- TOP3 区域 -->
|
||
<view v-else-if="top3Users.length > 0" class="top3-section">
|
||
<TOP3Card
|
||
v-for="user in top3Users"
|
||
:key="user.userId"
|
||
:rank="user.rank"
|
||
:avatar="user.avatar"
|
||
:nickname="user.nickname"
|
||
:popularityScore="user.popularityScore"
|
||
:artworkImage="user.artworkImage"
|
||
:userId="user.userId"
|
||
:isCurrentUser="isCurrentUser(user.userId)"
|
||
@visit="handleVisit"
|
||
@view-profile="handleViewProfile"
|
||
/>
|
||
</view>
|
||
|
||
<!-- 空状态 -->
|
||
<view v-if="!isLoading && !errorMessage && rankingData.length === 0" class="empty-data">
|
||
<text class="empty-text">暂无排名数据</text>
|
||
</view>
|
||
|
||
<!-- 排名列表 -->
|
||
<view v-if="listUsers.length > 0" class="ranking-list-section">
|
||
<RankingListItem
|
||
v-for="item in listUsers"
|
||
:key="item.userId"
|
||
:rank="item.rank"
|
||
:userId="item.userId"
|
||
:avatar="item.avatar"
|
||
:nickname="item.nickname"
|
||
:popularityScore="item.popularityScore"
|
||
:artworkImage="item.artworkImage"
|
||
:artworkId="item.artworkId"
|
||
:showVisitButton="!isCurrentUser(item.userId)"
|
||
:isCurrentUser="isCurrentUser(item.userId)"
|
||
@visit="handleVisit"
|
||
@view-profile="handleViewProfile"
|
||
@artwork-click="handleArtworkClick"
|
||
/>
|
||
</view>
|
||
|
||
<!-- 加载更多 -->
|
||
<view v-if="isLoadingMore" class="loading-more-container">
|
||
<view class="loading-spinner-small"></view>
|
||
<text class="loading-more-text">加载中...</text>
|
||
</view>
|
||
|
||
<!-- 底部占位 -->
|
||
<view class="bottom-spacer"></view>
|
||
</scroll-view>
|
||
|
||
<!-- 当前用户栏 -->
|
||
<view v-if="!isLoading" class="current-user-bar">
|
||
<view class="current-user-content">
|
||
<image
|
||
class="current-user-avatar"
|
||
:src="currentUserInfo.avatar"
|
||
mode="aspectFill"
|
||
@error="handleCurrentUserAvatarError"
|
||
/>
|
||
<view class="current-user-info">
|
||
<view class="current-user-score">
|
||
<image class="flame-icon" src="/static/rank/spark.png" mode="aspectFit"></image>
|
||
<text class="score-text">{{ formatPopularityScore(currentUserInfo.popularityScore) }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="current-user-rank">
|
||
<text class="rank-text">{{ formatCurrentUserRank(currentUserInfo.rank) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</transition>
|
||
</view>
|
||
</template>
|
||
```
|
||
|
||
- [ ] **Step 2: 实现 script 部分**
|
||
|
||
```vue
|
||
<script setup>
|
||
import { ref, computed, watch } from 'vue'
|
||
import { getActivityRankingApi } from '@/utils/api.js'
|
||
import TOP3Card from '../../components/TOP3Card.vue'
|
||
import RankingListItem from '../../components/RankingListItem.vue'
|
||
|
||
const props = defineProps({
|
||
visible: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
activityId: {
|
||
type: [String, Number],
|
||
required: true
|
||
},
|
||
starId: {
|
||
type: [String, Number],
|
||
default: null
|
||
},
|
||
activityTitle: {
|
||
type: String,
|
||
default: '活动排名'
|
||
},
|
||
currentUser: {
|
||
type: Object,
|
||
default: null
|
||
}
|
||
})
|
||
|
||
const emit = defineEmits(['update:visible', 'visit', 'view-profile', 'view-artwork'])
|
||
|
||
// 状态管理
|
||
const rankingData = ref([])
|
||
const isLoading = ref(false)
|
||
const isRefreshing = ref(false)
|
||
const isLoadingMore = ref(false)
|
||
const hasNoMoreData = ref(false)
|
||
const errorMessage = ref(null)
|
||
const currentUserInfo = ref({
|
||
userId: 'currentUser',
|
||
avatar: '/static/avatar/1.jpeg',
|
||
nickname: '我',
|
||
popularityScore: 0,
|
||
rank: null
|
||
})
|
||
|
||
// 分页
|
||
const pageInfo = ref({ currentPage: 1, hasMore: true })
|
||
const PAGE_SIZE = 10
|
||
|
||
// 计算属性:TOP3 用户
|
||
const top3Users = computed(() => {
|
||
return rankingData.value
|
||
.filter(user => user.rank >= 1 && user.rank <= 3)
|
||
.sort((a, b) => a.rank - b.rank)
|
||
})
|
||
|
||
// 计算属性:列表用户(第4名及以后)
|
||
const listUsers = computed(() => {
|
||
return rankingData.value
|
||
.filter(user => user.rank >= 4)
|
||
.sort((a, b) => a.rank - b.rank)
|
||
})
|
||
|
||
// 数据转换函数(复用 RankingModal 逻辑)
|
||
const transformActivityRankingData = async (apiResponse) => {
|
||
if (!apiResponse || !apiResponse.data) return []
|
||
|
||
const { items } = apiResponse.data
|
||
if (!Array.isArray(items)) return []
|
||
|
||
return items.map(item => ({
|
||
rank: item.rank,
|
||
userId: String(item.user_id),
|
||
avatar: item.avatar_url || '/static/avatar/1.jpeg',
|
||
nickname: item.nickname || '未知用户',
|
||
popularityScore: item.total_contribution || 0,
|
||
artworkImage: '',
|
||
artworkId: null
|
||
}))
|
||
}
|
||
|
||
// 转换当前用户数据
|
||
const transformMyActivityContribution = async (myContribution) => {
|
||
return {
|
||
userId: 'currentUser',
|
||
avatar: myContribution?.avatar_url || '/static/avatar/1.jpeg',
|
||
popularityScore: myContribution?.total_contribution || 0,
|
||
rank: myContribution?.rank > 0 ? myContribution.rank : null
|
||
}
|
||
}
|
||
|
||
// 加载排名数据
|
||
const loadRankingData = async (page = 1, isRefreshAction = false) => {
|
||
try {
|
||
if (page === 1 && !isRefreshAction) {
|
||
isLoading.value = true
|
||
} else if (page > 1) {
|
||
isLoadingMore.value = true
|
||
}
|
||
errorMessage.value = null
|
||
|
||
const starId = props.starId || uni.getStorageSync('star_id')
|
||
const apiResponse = await getActivityRankingApi(props.activityId, starId, page, PAGE_SIZE)
|
||
|
||
if (apiResponse && apiResponse.code === 200 && apiResponse.data) {
|
||
const transformedData = await transformActivityRankingData(apiResponse)
|
||
|
||
if (page === 1) {
|
||
rankingData.value = transformedData
|
||
pageInfo.value = { currentPage: 1, hasMore: transformedData.length >= PAGE_SIZE }
|
||
} else {
|
||
rankingData.value.push(...transformedData)
|
||
pageInfo.value.currentPage = page
|
||
pageInfo.value.hasMore = transformedData.length >= PAGE_SIZE
|
||
}
|
||
|
||
hasNoMoreData.value = !pageInfo.value.hasMore
|
||
|
||
// 处理当前用户数据
|
||
if (page === 1 && apiResponse.data.my_contribution) {
|
||
currentUserInfo.value = await transformMyActivityContribution(apiResponse.data.my_contribution)
|
||
}
|
||
|
||
return true
|
||
} else {
|
||
throw new Error(apiResponse?.message || '获取排名失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to load ranking:', error)
|
||
errorMessage.value = error.message || '加载失败'
|
||
return false
|
||
} finally {
|
||
if (page === 1 && !isRefreshAction) {
|
||
isLoading.value = false
|
||
} else if (page > 1) {
|
||
isLoadingMore.value = false
|
||
}
|
||
}
|
||
}
|
||
|
||
// 刷新
|
||
const handleRefresh = async () => {
|
||
if (isRefreshing.value) return
|
||
|
||
isRefreshing.value = true
|
||
const success = await loadRankingData(1, true)
|
||
|
||
if (success) {
|
||
uni.showToast({ title: '刷新成功', icon: 'success', duration: 1500 })
|
||
} else {
|
||
uni.showToast({ title: '刷新失败', icon: 'none', duration: 2000 })
|
||
}
|
||
|
||
setTimeout(() => { isRefreshing.value = false }, 500)
|
||
}
|
||
|
||
// 滚动加载更多
|
||
const handleScrollToLower = async () => {
|
||
if (isLoadingMore.value || hasNoMoreData.value) return
|
||
|
||
isLoadingMore.value = true
|
||
await loadRankingData(pageInfo.value.currentPage + 1)
|
||
isLoadingMore.value = false
|
||
}
|
||
|
||
// 工具函数
|
||
const formatPopularityScore = (score) => {
|
||
if (typeof score !== 'number' || isNaN(score) || score < 0) return '0'
|
||
if (score >= 1000000) return (score / 1000000).toFixed(1) + 'M'
|
||
if (score >= 1000) return (score / 1000).toFixed(1) + 'K'
|
||
return score.toString()
|
||
}
|
||
|
||
const formatCurrentUserRank = (rank) => {
|
||
if (!rank || rank <= 0) return '未上榜'
|
||
return `第${rank}名`
|
||
}
|
||
|
||
const isCurrentUser = (userId) => userId === currentUserInfo.value.userId
|
||
|
||
const handleCurrentUserAvatarError = (e) => {
|
||
e.target.src = '/static/avatar/1.jpeg'
|
||
}
|
||
|
||
// 事件处理
|
||
const handleClose = () => emit('update:visible', false)
|
||
|
||
const handleVisit = (userId, nickname) => emit('visit', { userId, nickname })
|
||
|
||
const handleViewProfile = (userId) => emit('view-profile', userId)
|
||
|
||
const handleArtworkClick = (data) => emit('view-artwork', data)
|
||
|
||
// 监听 visible 变化
|
||
watch(() => props.visible, async (newVisible) => {
|
||
if (newVisible && props.activityId) {
|
||
rankingData.value = []
|
||
pageInfo.value = { currentPage: 1, hasMore: true }
|
||
hasNoMoreData.value = false
|
||
await loadRankingData(1)
|
||
}
|
||
})
|
||
</script>
|
||
```
|
||
|
||
- [ ] **Step 3: 添加样式**
|
||
|
||
```vue
|
||
<style scoped>
|
||
/* 动画 */
|
||
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; }
|
||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||
|
||
.slide-up-enter-active, .slide-up-leave-active { transition: transform 0.3s ease; }
|
||
.slide-up-enter-from, .slide-up-leave-to { transform: translateY(100%); }
|
||
|
||
.modal-wrapper {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 9999;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: center;
|
||
}
|
||
|
||
.modal-mask {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.6);
|
||
}
|
||
|
||
.modal-container {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 80vh;
|
||
border-radius: 48rpx 48rpx 0 0;
|
||
overflow: hidden;
|
||
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
|
||
}
|
||
|
||
.modal-background {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-image: url('/static/rank/paihangbang.png');
|
||
background-size: 100% 100%;
|
||
background-repeat: no-repeat;
|
||
background-position: center;
|
||
}
|
||
|
||
.modal-content {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
padding: 40rpx 20rpx 20rpx;
|
||
}
|
||
|
||
.header-section {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
padding: 0 20rpx 30rpx;
|
||
}
|
||
|
||
.activity-title {
|
||
font-size: 36rpx;
|
||
font-weight: bold;
|
||
color: #fff;
|
||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.5);
|
||
}
|
||
|
||
.close-btn {
|
||
position: absolute;
|
||
right: 20rpx;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.close-icon {
|
||
font-size: 40rpx;
|
||
color: #fff;
|
||
}
|
||
|
||
.scrollable-content {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding-bottom: 200rpx;
|
||
}
|
||
|
||
/* 复用 RankingModal 样式 */
|
||
.loading-container, .error-container, .empty-data {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 400rpx;
|
||
padding: 40rpx;
|
||
}
|
||
|
||
.loading-spinner {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
border: 4rpx solid rgba(255, 255, 255, 0.3);
|
||
border-top-color: #fff;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
.loading-text, .error-text, .empty-text {
|
||
margin-top: 20rpx;
|
||
font-size: 28rpx;
|
||
color: rgba(255, 255, 255, 0.8);
|
||
}
|
||
|
||
.retry-button {
|
||
margin-top: 30rpx;
|
||
padding: 16rpx 40rpx;
|
||
background: linear-gradient(135deg, #ff6b9d, #ffa06b);
|
||
border-radius: 40rpx;
|
||
color: #fff;
|
||
font-size: 28rpx;
|
||
border: none;
|
||
}
|
||
|
||
.top3-section {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 40rpx;
|
||
padding: 0 48rpx;
|
||
margin-bottom: 60rpx;
|
||
}
|
||
|
||
.ranking-list-section {
|
||
padding: 0 48rpx;
|
||
}
|
||
|
||
.loading-more-container {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 30rpx;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.loading-spinner-small {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
border: 3rpx solid rgba(255, 255, 255, 0.3);
|
||
border-top-color: #fff;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
.loading-more-text {
|
||
font-size: 24rpx;
|
||
color: rgba(255, 255, 255, 0.7);
|
||
}
|
||
|
||
.bottom-spacer {
|
||
height: 20rpx;
|
||
}
|
||
|
||
.current-user-bar {
|
||
position: absolute;
|
||
bottom: 120rpx;
|
||
left: 40rpx;
|
||
right: 40rpx;
|
||
padding: 24rpx;
|
||
background: linear-gradient(135deg, rgba(255, 107, 157, 0.9), rgba(255, 177, 153, 0.9));
|
||
border-radius: 32rpx;
|
||
box-shadow: 0 -4rpx 16rpx rgba(255, 107, 157, 0.35);
|
||
border: 2rpx solid rgba(255, 255, 255, 0.3);
|
||
}
|
||
|
||
.current-user-content {
|
||
display: flex;
|
||
align-items: center;
|
||
z-index: 1;
|
||
}
|
||
|
||
.current-user-avatar {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
border-radius: 50%;
|
||
border: 4rpx solid rgba(255, 255, 255, 0.9);
|
||
}
|
||
|
||
.current-user-info {
|
||
flex: 1;
|
||
margin-left: 34rpx;
|
||
}
|
||
|
||
.current-user-score {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.flame-icon {
|
||
width: 44rpx;
|
||
height: 44rpx;
|
||
}
|
||
|
||
.score-text {
|
||
font-size: 26rpx;
|
||
margin-left: 8rpx;
|
||
color: #fff;
|
||
}
|
||
|
||
.current-user-rank {
|
||
padding: 16rpx 28rpx;
|
||
}
|
||
|
||
.rank-text {
|
||
font-size: 30rpx;
|
||
color: #fff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
</style>
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: 修改 ThemeBanner.vue 添加点击事件
|
||
|
||
**文件:**
|
||
- 修改: `frontend/pages/support-activity/components/ThemeBanner.vue:1-36`(template 部分)
|
||
|
||
- [ ] **Step 1: 在 banner-content 上添加点击事件**
|
||
|
||
在 `<view class="banner-content">` 上添加 `@tap="handleBannerClick"`
|
||
|
||
- [ ] **Step 2: 添加 emit 定义**
|
||
|
||
在 script 部分添加:
|
||
```javascript
|
||
const emit = defineEmits(['tap'])
|
||
|
||
// 添加点击处理函数
|
||
const handleBannerClick = () => {
|
||
emit('tap')
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: 修改 support-activity/index.vue 引入和使用组件
|
||
|
||
**文件:**
|
||
- 修改: `frontend/pages/support-activity/index.vue:14-21`(template)
|
||
- 修改: `frontend/pages/support-activity/index.vue:105-122`(script import)
|
||
|
||
- [ ] **Step 1: 引入 ActivityRankingModal**
|
||
|
||
```javascript
|
||
import ActivityRankingModal from './components/ActivityRankingModal.vue'
|
||
```
|
||
|
||
- [ ] **Step 2: 添加弹窗状态和引用**
|
||
|
||
```javascript
|
||
const rankingModalVisible = ref(false)
|
||
const currentActivityTitle = ref('')
|
||
```
|
||
|
||
- [ ] **Step 3: 在 template 中添加组件**
|
||
|
||
```vue
|
||
<!-- ThemeBanner 添加 @tap 打开弹窗 -->
|
||
<ThemeBanner
|
||
v-if="config"
|
||
:title="config.title"
|
||
:banner-image="config.bannerImage"
|
||
:current="progressData.current"
|
||
:target="progressData.target"
|
||
:is-stale-data="isStaleData"
|
||
@tap="openRankingModal"
|
||
/>
|
||
|
||
<!-- 添加 ActivityRankingModal -->
|
||
<ActivityRankingModal
|
||
v-model:visible="rankingModalVisible"
|
||
:activity-id="activityId"
|
||
:activity-title="currentActivityTitle"
|
||
@visit="handleVisitUser"
|
||
@view-profile="handleViewUserProfile"
|
||
@view-artwork="handleViewArtwork"
|
||
/>
|
||
```
|
||
|
||
- [ ] **Step 4: 添加打开弹窗方法**
|
||
|
||
```javascript
|
||
const openRankingModal = () => {
|
||
currentActivityTitle.value = config.value?.title || '活动排名'
|
||
rankingModalVisible.value = true
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: 提交代码
|
||
|
||
- [ ] **Step 1: 提交所有更改**
|
||
|
||
```bash
|
||
git add frontend/pages/support-activity/components/ActivityRankingModal.vue \
|
||
frontend/pages/support-activity/components/ThemeBanner.vue \
|
||
frontend/pages/support-activity/index.vue
|
||
git commit -m "feat: 添加 ActivityRankingModal 活动榜单弹窗组件"
|
||
```
|
||
|
||
---
|
||
|
||
## 验证步骤
|
||
|
||
1. 启动开发服务器:`cd frontend && npm run dev`
|
||
2. 进入活动页面,点击 ThemeBanner 区域
|
||
3. 验证弹窗正常显示TOP3和排名列表
|
||
4. 验证下拉刷新和滚动加载更多功能
|
||
5. 验证当前用户栏正确显示
|
||
|
||
---
|
||
|
||
## 依赖项
|
||
|
||
- `TOP3Card.vue` - 来自 `frontend/pages/components/TOP3Card.vue`
|
||
- `RankingListItem.vue` - 来自 `frontend/pages/components/RankingListItem.vue`
|
||
- `getActivityRankingApi` - 来自 `frontend/utils/api.js` |