feat(积分收集): 重构积分收集组件以支持后端数据
- 移除模拟数据,使用接口返回的待收集积分数据 - 添加积分来源类型映射和显示 - 修改总步数计算逻辑以匹配新数据结构 - 添加对积分和待收集积分的监听
Showing
2 changed files
with
78 additions
and
98 deletions
| ... | @@ -16,13 +16,9 @@ | ... | @@ -16,13 +16,9 @@ |
| 16 | :style="getItemStyle(item)" | 16 | :style="getItemStyle(item)" |
| 17 | @tap="collectItem(item)" | 17 | @tap="collectItem(item)" |
| 18 | > | 18 | > |
| 19 | - <view v-if="item.type === 'steps'" class="item-content"> | 19 | + <view class="item-content"> |
| 20 | - <text class="item-value">{{ item.steps }}步</text> | 20 | + <text class="item-value">{{ item.sourceLabel }}</text> |
| 21 | - <text class="item-type">{{ item.value }}分</text> | 21 | + <text class="item-type">{{ item.points }}分</text> |
| 22 | - </view> | ||
| 23 | - <view v-else class="item-content"> | ||
| 24 | - <text class="item-value">奖励</text> | ||
| 25 | - <text class="item-type">{{ item.value }}分</text> | ||
| 26 | </view> | 22 | </view> |
| 27 | </view> | 23 | </view> |
| 28 | 24 | ||
| ... | @@ -38,7 +34,7 @@ | ... | @@ -38,7 +34,7 @@ |
| 38 | </template> | 34 | </template> |
| 39 | 35 | ||
| 40 | <script setup> | 36 | <script setup> |
| 41 | -import { ref, onMounted, defineProps, defineExpose, defineEmits } from 'vue' | 37 | +import { ref, onMounted, defineProps, defineExpose, defineEmits, watch } from 'vue' |
| 42 | import Taro, { useDidShow } from '@tarojs/taro' | 38 | import Taro, { useDidShow } from '@tarojs/taro' |
| 43 | 39 | ||
| 44 | const emit = defineEmits(['collection-complete']) | 40 | const emit = defineEmits(['collection-complete']) |
| ... | @@ -46,46 +42,64 @@ const props = defineProps({ | ... | @@ -46,46 +42,64 @@ const props = defineProps({ |
| 46 | height: { | 42 | height: { |
| 47 | type: String, | 43 | type: String, |
| 48 | default: '30vh' | 44 | default: '30vh' |
| 45 | + }, | ||
| 46 | + pendingPoints: { | ||
| 47 | + type: Array, | ||
| 48 | + default: () => [] | ||
| 49 | + }, | ||
| 50 | + totalPoints: { | ||
| 51 | + type: Number, | ||
| 52 | + default: 0 | ||
| 49 | } | 53 | } |
| 50 | }) | 54 | }) |
| 51 | 55 | ||
| 52 | // 响应式数据 | 56 | // 响应式数据 |
| 53 | -const totalPoints = ref(12500) // 总积分 | 57 | +const animatedTotalPoints = ref(props.totalPoints) // 动画中的总积分 |
| 54 | -const animatedTotalPoints = ref(12500) // 动画中的总积分 | ||
| 55 | const floatingItems = ref([]) // 漂浮的积分项 | 58 | const floatingItems = ref([]) // 漂浮的积分项 |
| 56 | const isCollecting = ref(false) // 是否正在收集,防止重复触发 | 59 | const isCollecting = ref(false) // 是否正在收集,防止重复触发 |
| 57 | 60 | ||
| 61 | +// source_type 中英文映射 | ||
| 62 | +const sourceTypeMap = { | ||
| 63 | + 'WALKING': '步数积分', | ||
| 64 | + 'CHECK_IN': '活动打卡', | ||
| 65 | + 'CHECK_IN_COUNT': '完成活动', | ||
| 66 | + 'FAMILY_SIZE': '家庭成员', | ||
| 67 | + 'COMPANION_PHOTO': '陪伴拍照', | ||
| 68 | + 'WHEELCHAIR_COMPANION': '陪伴轮椅' | ||
| 69 | +} | ||
| 70 | + | ||
| 58 | /** | 71 | /** |
| 59 | - * 生成模拟数据并分配随机位置 | 72 | + * 根据pending_points数据生成漂浮积分项并分配随机位置 |
| 60 | */ | 73 | */ |
| 61 | -const generateMockData = () => { | 74 | +const generatePointsData = () => { |
| 62 | - const mockItems = [ | 75 | + if (!props.pendingPoints || props.pendingPoints.length === 0) { |
| 63 | - { id: 1, type: 'steps', value: 5000, steps: 5000 }, | 76 | + return []; |
| 64 | - { id: 2, type: 'points', value: 500 }, | 77 | + } |
| 65 | - { id: 3, type: 'steps', value: 8000, steps: 5000 }, | 78 | + |
| 66 | - { id: 4, type: 'points', value: 30 }, | 79 | + const pointsItems = props.pendingPoints.map(item => ({ |
| 67 | - { id: 5, type: 'steps', value: 12000, steps: 12000 }, | 80 | + id: item.id, |
| 68 | - { id: 6, type: 'points', value: 800 }, | 81 | + points: parseInt(item.points), |
| 69 | - { id: 7, type: 'points', value: 250 }, | 82 | + sourceType: item.source_type, |
| 70 | - { id: 8, type: 'steps', value: 6000, steps: 6000 }, | 83 | + sourceLabel: sourceTypeMap[item.source_type] || item.source_type, |
| 71 | - { id: 9, type: 'points', value: 1000 }, | 84 | + title: item.title, |
| 72 | - { id: 10, type: 'steps', value: 10000, steps: 10000 }, | 85 | + note: item.note, |
| 73 | - ].map(item => ({ ...item, collecting: false })); // 初始化collecting状态 | 86 | + collecting: false |
| 74 | - | 87 | + })); |
| 75 | - const maxValue = Math.max(...mockItems.map(i => i.value)); | 88 | + |
| 89 | + const maxValue = Math.max(...pointsItems.map(i => i.points), 1); | ||
| 76 | const baseSize = 80; | 90 | const baseSize = 80; |
| 77 | const { windowWidth, windowHeight } = Taro.getWindowInfo(); | 91 | const { windowWidth, windowHeight } = Taro.getWindowInfo(); |
| 78 | const positionedItems = []; | 92 | const positionedItems = []; |
| 79 | 93 | ||
| 80 | // 为每个项目分配一个不重叠的随机位置 | 94 | // 为每个项目分配一个不重叠的随机位置 |
| 81 | - return mockItems.map(item => { | 95 | + return pointsItems.map(item => { |
| 82 | let x, y, hasCollision; | 96 | let x, y, hasCollision; |
| 83 | const maxAttempts = 100; // 限制尝试次数以避免无限循环 | 97 | const maxAttempts = 100; // 限制尝试次数以避免无限循环 |
| 84 | let attempts = 0; | 98 | let attempts = 0; |
| 85 | const centerNoFlyZone = 25; // 中心圆形25%的禁飞区半径 | 99 | const centerNoFlyZone = 25; // 中心圆形25%的禁飞区半径 |
| 86 | 100 | ||
| 87 | // 计算项目大小和半径 | 101 | // 计算项目大小和半径 |
| 88 | - const sizeRatio = item.value / maxValue; | 102 | + const sizeRatio = item.points / maxValue; |
| 89 | const size = baseSize + (sizeRatio * 40); // rpx | 103 | const size = baseSize + (sizeRatio * 40); // rpx |
| 90 | const radiusXPercent = (size / 750) * 100 / 2 * (windowWidth / windowHeight) ; // 归一化为高度的百分比 | 104 | const radiusXPercent = (size / 750) * 100 / 2 * (windowWidth / windowHeight) ; // 归一化为高度的百分比 |
| 91 | 105 | ||
| ... | @@ -140,8 +154,8 @@ const generateMockData = () => { | ... | @@ -140,8 +154,8 @@ const generateMockData = () => { |
| 140 | */ | 154 | */ |
| 141 | const getItemStyle = (item) => { | 155 | const getItemStyle = (item) => { |
| 142 | const baseSize = 80; | 156 | const baseSize = 80; |
| 143 | - const maxValue = Math.max(...(floatingItems.value.map(i => i.value).length > 0 ? floatingItems.value.map(i => i.value) : [1])); | 157 | + const maxValue = Math.max(...(floatingItems.value.map(i => i.points).length > 0 ? floatingItems.value.map(i => i.points) : [1])); |
| 144 | - const sizeRatio = item.value / maxValue; | 158 | + const sizeRatio = item.points / maxValue; |
| 145 | const size = baseSize + (sizeRatio * 40); | 159 | const size = baseSize + (sizeRatio * 40); |
| 146 | 160 | ||
| 147 | const style = { | 161 | const style = { |
| ... | @@ -174,13 +188,13 @@ const collectItem = (item) => { | ... | @@ -174,13 +188,13 @@ const collectItem = (item) => { |
| 174 | item.collecting = true; | 188 | item.collecting = true; |
| 175 | 189 | ||
| 176 | setTimeout(() => { | 190 | setTimeout(() => { |
| 177 | - const totalToAdd = item.type === 'steps' ? Math.floor(item.value / 10) : item.value; | 191 | + const totalToAdd = item.points; |
| 178 | - animateNumber(totalPoints.value, totalPoints.value + totalToAdd); | 192 | + const newTotal = props.totalPoints + totalToAdd; |
| 179 | - totalPoints.value += totalToAdd; | 193 | + animateNumber(animatedTotalPoints.value, newTotal); |
| 180 | 194 | ||
| 181 | floatingItems.value = floatingItems.value.filter(i => i.id !== item.id); | 195 | floatingItems.value = floatingItems.value.filter(i => i.id !== item.id); |
| 182 | if (floatingItems.value.length === 0) { | 196 | if (floatingItems.value.length === 0) { |
| 183 | - emit('collection-complete', totalPoints.value); | 197 | + emit('collection-complete', newTotal); |
| 184 | } | 198 | } |
| 185 | }, 800); // 动画时长 | 199 | }, 800); // 动画时长 |
| 186 | } | 200 | } |
| ... | @@ -199,17 +213,17 @@ const collectAll = async () => { | ... | @@ -199,17 +213,17 @@ const collectAll = async () => { |
| 199 | setTimeout(() => { | 213 | setTimeout(() => { |
| 200 | item.collecting = true; | 214 | item.collecting = true; |
| 201 | }, index * 80); // 依次触发动画 | 215 | }, index * 80); // 依次触发动画 |
| 202 | - totalToAdd += item.value; | 216 | + totalToAdd += item.points; |
| 203 | }); | 217 | }); |
| 204 | 218 | ||
| 205 | const totalAnimationTime = itemsToCollect.length * 80 + 800; | 219 | const totalAnimationTime = itemsToCollect.length * 80 + 800; |
| 206 | setTimeout(() => { | 220 | setTimeout(() => { |
| 207 | - animateNumber(totalPoints.value, totalPoints.value + totalToAdd); | 221 | + const newTotal = props.totalPoints + totalToAdd; |
| 208 | - totalPoints.value += totalToAdd; | 222 | + animateNumber(animatedTotalPoints.value, newTotal); |
| 209 | 223 | ||
| 210 | floatingItems.value = []; | 224 | floatingItems.value = []; |
| 211 | isCollecting.value = false; | 225 | isCollecting.value = false; |
| 212 | - emit('collection-complete', totalPoints.value); | 226 | + emit('collection-complete', newTotal); |
| 213 | }, totalAnimationTime); | 227 | }, totalAnimationTime); |
| 214 | } | 228 | } |
| 215 | 229 | ||
| ... | @@ -243,10 +257,16 @@ defineExpose({ | ... | @@ -243,10 +257,16 @@ defineExpose({ |
| 243 | * 初始化数据 | 257 | * 初始化数据 |
| 244 | */ | 258 | */ |
| 245 | const initData = async () => { | 259 | const initData = async () => { |
| 246 | - floatingItems.value = generateMockData(); | 260 | + floatingItems.value = generatePointsData(); |
| 247 | - console.warn('初始化数据') | 261 | + animatedTotalPoints.value = props.totalPoints; |
| 262 | + console.warn('初始化积分数据', props.pendingPoints) | ||
| 248 | } | 263 | } |
| 249 | 264 | ||
| 265 | +// 监听props变化 | ||
| 266 | +watch(() => [props.pendingPoints, props.totalPoints], () => { | ||
| 267 | + initData(); | ||
| 268 | +}, { deep: true, immediate: true }) | ||
| 269 | + | ||
| 250 | // 组件挂载时初始化数据 | 270 | // 组件挂载时初始化数据 |
| 251 | onMounted(() => { | 271 | onMounted(() => { |
| 252 | initData(); | 272 | initData(); | ... | ... |
| ... | @@ -29,7 +29,7 @@ | ... | @@ -29,7 +29,7 @@ |
| 29 | <view class="flex justify-between items-center"> | 29 | <view class="flex justify-between items-center"> |
| 30 | <view class="flex items-baseline"> | 30 | <view class="flex items-baseline"> |
| 31 | <span class="text-4xl font-bold"> | 31 | <span class="text-4xl font-bold"> |
| 32 | - {{ todaySteps }} | 32 | + {{ todaySteps?.toLocaleString() }} |
| 33 | </span> | 33 | </span> |
| 34 | <span class="ml-1 text-gray-500">步</span> | 34 | <span class="ml-1 text-gray-500">步</span> |
| 35 | </view> | 35 | </view> |
| ... | @@ -51,6 +51,7 @@ | ... | @@ -51,6 +51,7 @@ |
| 51 | ref="pointsCollectorRef" | 51 | ref="pointsCollectorRef" |
| 52 | height="30vh" | 52 | height="30vh" |
| 53 | :total-points="finalTotalPoints" | 53 | :total-points="finalTotalPoints" |
| 54 | + :pending-points="pendingPoints" | ||
| 54 | @collection-complete="handleCollectionComplete" | 55 | @collection-complete="handleCollectionComplete" |
| 55 | /> | 56 | /> |
| 56 | </template> | 57 | </template> |
| ... | @@ -77,14 +78,14 @@ | ... | @@ -77,14 +78,14 @@ |
| 77 | <view class="flex justify-between items-center mb-4"> | 78 | <view class="flex justify-between items-center mb-4"> |
| 78 | <h2 class="font-medium text-lg">今日家庭步数排行</h2> | 79 | <h2 class="font-medium text-lg">今日家庭步数排行</h2> |
| 79 | <span class="text-sm text-gray-500"> | 80 | <span class="text-sm text-gray-500"> |
| 80 | - 总计 {{ getTotalSteps(totalFamilySteps).toLocaleString() }} 步 | 81 | + 总计 {{ getTotalSteps() }} 步 |
| 81 | </span> | 82 | </span> |
| 82 | </view> | 83 | </view> |
| 83 | <view class="grid grid-cols-4 gap-2"> | 84 | <view class="grid grid-cols-4 gap-2"> |
| 84 | - <view v-for="member in familyMembers" :key="member.id" class="flex flex-col items-center"> | 85 | + <view v-for="member in familyMembers" :key="member.user_id" class="flex flex-col items-center"> |
| 85 | - <image :src="member.avatar" :alt="member.name" class="w-16 h-16 rounded-full mb-1" /> | 86 | + <image :src="member.avatar_url || defaultAvatar" :alt="member.role" class="w-16 h-16 rounded-full mb-1" /> |
| 86 | <span class="text-sm text-gray-700"> | 87 | <span class="text-sm text-gray-700"> |
| 87 | - {{ member.steps.toLocaleString() }}步 | 88 | + {{ member?.today_step?.toLocaleString() }}步 |
| 88 | </span> | 89 | </span> |
| 89 | </view> | 90 | </view> |
| 90 | </view> | 91 | </view> |
| ... | @@ -205,6 +206,7 @@ const pointsCollectorRef = ref(null) | ... | @@ -205,6 +206,7 @@ const pointsCollectorRef = ref(null) |
| 205 | const weRunAuthRef = ref(null) | 206 | const weRunAuthRef = ref(null) |
| 206 | const showTotalPointsOnly = ref(false) | 207 | const showTotalPointsOnly = ref(false) |
| 207 | const finalTotalPoints = ref(0) | 208 | const finalTotalPoints = ref(0) |
| 209 | +const pendingPoints = ref([]) // 待收集的积分数据 | ||
| 208 | 210 | ||
| 209 | const familyName = ref('') | 211 | const familyName = ref('') |
| 210 | const familySlogn = ref('') | 212 | const familySlogn = ref('') |
| ... | @@ -291,60 +293,14 @@ const handleStepsSynced = async (data) => { | ... | @@ -291,60 +293,14 @@ const handleStepsSynced = async (data) => { |
| 291 | 293 | ||
| 292 | /** | 294 | /** |
| 293 | * 计算总步数(包含用户步数和家庭成员步数) | 295 | * 计算总步数(包含用户步数和家庭成员步数) |
| 294 | - * @param {number} userSteps - 用户步数 | 296 | + * @returns {string} 格式化后的总步数 |
| 295 | - * @returns {number} 总步数 | ||
| 296 | */ | 297 | */ |
| 297 | -const getTotalSteps = (userSteps) => { | 298 | +const getTotalSteps = () => { |
| 298 | - return familyMembers.value.reduce((sum, member) => sum + member.steps, 0) + userSteps | 299 | + return familyMembers.value.reduce((sum, member) => sum + member.today_step, 0).toLocaleString() |
| 299 | } | 300 | } |
| 300 | 301 | ||
| 301 | // Mock data for family members | 302 | // Mock data for family members |
| 302 | -const familyMembers = ref([ | 303 | +const familyMembers = ref([]); |
| 303 | - { | ||
| 304 | - id: 1, | ||
| 305 | - name: '妈妈', | ||
| 306 | - steps: 7000, | ||
| 307 | - avatar: 'https://randomuser.me/api/portraits/women/44.jpg' | ||
| 308 | - }, | ||
| 309 | - { | ||
| 310 | - id: 2, | ||
| 311 | - name: '爸爸', | ||
| 312 | - steps: 6000, | ||
| 313 | - avatar: 'https://randomuser.me/api/portraits/men/32.jpg' | ||
| 314 | - }, | ||
| 315 | - { | ||
| 316 | - id: 3, | ||
| 317 | - name: '儿子', | ||
| 318 | - steps: 5000, | ||
| 319 | - avatar: 'https://randomuser.me/api/portraits/men/22.jpg' | ||
| 320 | - }, | ||
| 321 | - { | ||
| 322 | - id: 4, | ||
| 323 | - name: '女儿', | ||
| 324 | - steps: 4000, | ||
| 325 | - avatar: 'https://randomuser.me/api/portraits/women/29.jpg' | ||
| 326 | - }, | ||
| 327 | - { | ||
| 328 | - id: 5, | ||
| 329 | - name: '孙子', | ||
| 330 | - steps: 3000, | ||
| 331 | - avatar: 'https://randomuser.me/api/portraits/men/25.jpg' | ||
| 332 | - }, | ||
| 333 | - { | ||
| 334 | - id: 6, | ||
| 335 | - name: '孙女', | ||
| 336 | - steps: 20000, | ||
| 337 | - avatar: 'https://randomuser.me/api/portraits/women/27.jpg' | ||
| 338 | - }, | ||
| 339 | -]); | ||
| 340 | - | ||
| 341 | -// 注意:totalSteps 计算逻辑已移至 getTotalSteps 方法中 | ||
| 342 | - | ||
| 343 | -const handleSyncSteps = () => { | ||
| 344 | - // In a real app, this would sync with a health API | ||
| 345 | - // For demo purposes, we'll just log and do nothing | ||
| 346 | - console.log('Syncing steps...'); | ||
| 347 | -}; | ||
| 348 | 304 | ||
| 349 | const goToProfile = () => { | 305 | const goToProfile = () => { |
| 350 | Taro.navigateTo({ url: '/pages/EditFamily/index' }); | 306 | Taro.navigateTo({ url: '/pages/EditFamily/index' }); |
| ... | @@ -376,10 +332,14 @@ const initPageData = async () => { | ... | @@ -376,10 +332,14 @@ const initPageData = async () => { |
| 376 | if (code) { | 332 | if (code) { |
| 377 | // 获取用户信息 | 333 | // 获取用户信息 |
| 378 | console.warn(data); | 334 | console.warn(data); |
| 379 | - // 真实情况下 需要重新获取积分列表, 测试随机积分获取状态, 接口有积分列表就显示出来, 没有就不显示 | 335 | + // 设置待收集的积分数据 |
| 336 | + pendingPoints.value = data.pending_points || []; | ||
| 337 | + // 获取今日家庭总步数 | ||
| 338 | + familyMembers.value = data.step_ranking || []; | ||
| 339 | + // 根据是否有待收集积分决定显示模式 | ||
| 380 | showTotalPointsOnly.value = !data.pending_points.length; | 340 | showTotalPointsOnly.value = !data.pending_points.length; |
| 381 | - // 总积分数量也要从接口获取传进去 | 341 | + // 总积分数量从接口获取 |
| 382 | - finalTotalPoints.value = 10086; | 342 | + finalTotalPoints.value = data.family.total_points || 0; |
| 383 | // 获取用户信息 | 343 | // 获取用户信息 |
| 384 | familyName.value = data.family.name; | 344 | familyName.value = data.family.name; |
| 385 | familySlogn.value = data.family.note; | 345 | familySlogn.value = data.family.note; | ... | ... |
-
Please register or login to post a comment