# 实时贡献显示前端实现计划 > **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:** 在活动页面实现实时贡献列表展示,从下往上堆叠,最新贡献显示在顶部 **Architecture:** 轮询模式:页面可见时每秒拉取一次最新贡献记录,增量更新列表(最多5条),页面不可见时停止并清空列表 **Tech Stack:** Vue 3 Composition API, uni-app, 原生小程序轮询 --- ## 文件结构 ``` frontend/ ├── utils/ │ └── api.js # 新增 getActivityContributionsLatestApi ├── pages/ │ └── support-activity/ │ ├── composables/ │ │ └── useContributionPolling.js # 新增:贡献轮询逻辑 │ ├── components/ │ │ └── ContributionList.vue # 新增:贡献列表组件 │ └── index.vue # 修改:引入 ContributionList ``` --- ## Task 1: 添加 API 方法到 api.js **Files:** - Modify: `frontend/utils/api.js:550` (在 purchaseActivityItemApi 之后) - [ ] **Step 1: 添加获取活动贡献记录 API** 在 `purchaseActivityItemApi` 函数后面(第551行位置)添加: ```javascript // 获取活动最新贡献记录(实时轮询用) export function getActivityContributionsLatestApi(activityId, sinceId = 0, limit = 5) { return request({ url: `/api/v1/activity/${activityId}/contributions/latest?since_id=${sinceId}&limit=${limit}`, method: 'GET' }) } ``` - [ ] **Step 2: 提交** ```bash git add frontend/utils/api.js git commit -m "feat: add getActivityContributionsLatestApi for realtime contributions" ``` --- ## Task 2: 创建 useContributionPolling.js Composable **Files:** - Create: `frontend/pages/support-activity/composables/useContributionPolling.js` - [ ] **Step 1: 创建 composable 目录** ```bash mkdir -p frontend/pages/support-activity/composables ``` - [ ] **Step 2: 编写 useContributionPolling.js** ```javascript import { ref, watch, onUnmounted } from 'vue' import { getActivityContributionsLatestApi } from '@/utils/api.js' /** * 贡献轮询逻辑 composable * @param {Ref} activityId - 活动ID * @param {boolean} isPageActive - 页面是否可见 * @returns {Object} records, loading, error, start, stop, refresh */ export function useContributionPolling(activityId, isPageActive) { const MAX_RECORDS = 5 const POLL_INTERVAL = 1000 // 每秒拉取 const RECORD_TTL = 5000 // 每条记录 5 秒后消失 const records = ref([]) const loading = ref(false) const error = ref(null) let latestId = 0 let pollingTimer = null let isPolling = false const recordTimers = new Map() // 重置记录计时器 function resetRecordTimer(record) { if (recordTimers.has(record.id)) { clearTimeout(recordTimers.get(record.id)) } const timer = setTimeout(() => { records.value = records.value.filter(r => r.id !== record.id) recordTimers.delete(record.id) }, RECORD_TTL) recordTimers.set(record.id, timer) } // 获取最新贡献记录 async function fetchLatest() { if (!activityId.value) return try { const res = await getActivityContributionsLatestApi(activityId.value, latestId, 1) // 处理 code 不为 200 的情况(静默忽略) if (res.code !== 200) return const newRecords = res.data?.records || [] if (newRecords.length === 0) return const newRecord = newRecords[0] // 检测到新记录(id > latestId) if (newRecord.id > latestId) { // 重置所有现有记录的计时器 records.value.forEach(resetRecordTimer) // 新记录插入到列表头部 records.value = [newRecord, ...records.value].slice(0, MAX_RECORDS) // 为新记录启动消失计时器 resetRecordTimer(newRecord) // 更新 latestId latestId = newRecord.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, MAX_RECORDS) if (res.code !== 200) { 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)) // 更新 latestId latestId = newRecords.length > 0 ? newRecords[newRecords.length - 1].id : 0 } 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 latestId = 0 // 全量拉取首次 fetchAll() // 启动定时轮询 pollingTimer = setInterval(fetchLatest, POLL_INTERVAL) } // 停止轮询 function stop() { if (pollingTimer) { clearInterval(pollingTimer) pollingTimer = null } // 清除所有计时器 recordTimers.forEach(timer => clearTimeout(timer)) recordTimers.clear() isPolling = false records.value = [] latestId = 0 } // 强制刷新(全量拉取) function refresh() { stop() start() } // 监听页面可见性 watch(isPageActive, (active) => { if (active) { start() } else { stop() } }, { immediate: true }) // 监听 activityId 变化 watch(activityId, (newId, oldId) => { if (newId !== oldId) { stop() if (isPageActive.value && newId) { start() } } }) // 组件卸载时清理 onUnmounted(() => { stop() }) return { records, loading, error, start, stop, refresh } } ``` - [ ] **Step 3: 提交** ```bash git add frontend/pages/support-activity/composables/useContributionPolling.js git commit -m "feat: add useContributionPolling composable for realtime contributions" ``` --- ## Task 3: 创建 ContributionList.vue 组件 **Files:** - Create: `frontend/pages/support-activity/components/ContributionList.vue` - [ ] **Step 1: 编写 ContributionList.vue** ```vue ``` - [ ] **Step 2: 提交** ```bash git add frontend/pages/support-activity/components/ContributionList.vue git commit -m "feat: add ContributionList component for realtime display" ``` --- ## Task 4: 在 index.vue 中集成 ContributionList **Files:** - Modify: `frontend/pages/support-activity/index.vue:103-113` (imports) - Modify: `frontend/pages/support-activity/index.vue:14-21` (ThemeBanner 下方插入 ContributionList) - Modify: `frontend/pages/support-activity/index.vue:476-482` (onHide 逻辑,停止贡献轮询) - [ ] **Step 1: 修改 imports,添加 ContributionList 引入** 在 import StageArea 那行(第111行)后面添加: ```javascript import ContributionList from './components/ContributionList.vue' ``` - [ ] **Step 2: 在 ThemeBanner 下方、StageArea 上方插入 ContributionList** 在 index.vue 第21行(ThemeBanner 结束标签后)插入: ```vue { isPageActive.value = true if (!isLoading.value && !errorMessage.value && progressManager) { progressManager.resumePolling() } }) ``` - [ ] **Step 4: 在 onHide 中停止贡献轮询(页面不可见时)** 在 onHide 回调中(大约第479行): ```javascript onHide(() => { isPageActive.value = false if (progressManager) { progressManager.pausePolling() } }) ``` 注意:ContributionList 内部已经通过 isPageActive ref 监听页面可见性,当 onHide 时会自动停止轮询。 - [ ] **Step 5: 添加 contribution-list-wrapper 样式** 在 index.vue 的 style 末尾(大约第721行之后)添加: ```css /* 实时贡献列表样式 */ .contribution-list-wrapper { width: 100%; padding: 0 24rpx; } ``` - [ ] **Step 6: 提交** ```bash git add frontend/pages/support-activity/index.vue git commit -m "feat: integrate ContributionList in support-activity page" ``` --- ## 验证步骤 1. **编译检查**:确保 `npm run dev` 无报错 2. **功能检查**: - 页面可见时,贡献列表每1秒轮询一次 - 页面不可见时(切换到后台),贡献列表停止轮询并清空 - 新贡献出现时,列表头部插入,最旧记录被移除 - 每条记录5秒无新数据后淡出消失 3. **页面集成检查**: - ThemeBanner 下方显示"实时贡献"标题 - 列表最多显示5条记录 - 贡献项格式:头像 + 昵称 + "贡献了" + 道具图标 + 名称 + 数量