feat(ranking): 添加数字滚动动画组件并集成到排行榜卡片
在排行榜卡片中添加数字滚动动画效果,当用户滚动到排行榜区域时触发动画 新增 NumberRoll 组件实现数字滚动效果 在 Dashboard 页面添加滚动监听以触发动画 调整排行榜卡片样式和间距
Showing
4 changed files
with
405 additions
and
7 deletions
| ... | @@ -14,6 +14,7 @@ declare module 'vue' { | ... | @@ -14,6 +14,7 @@ declare module 'vue' { |
| 14 | FamilyAlbum: typeof import('./src/components/FamilyAlbum.vue')['default'] | 14 | FamilyAlbum: typeof import('./src/components/FamilyAlbum.vue')['default'] |
| 15 | GlassCard: typeof import('./src/components/GlassCard.vue')['default'] | 15 | GlassCard: typeof import('./src/components/GlassCard.vue')['default'] |
| 16 | NavBar: typeof import('./src/components/navBar.vue')['default'] | 16 | NavBar: typeof import('./src/components/navBar.vue')['default'] |
| 17 | + NumberRoll: typeof import('./src/components/NumberRoll.vue')['default'] | ||
| 17 | NutActionSheet: typeof import('@nutui/nutui-taro')['ActionSheet'] | 18 | NutActionSheet: typeof import('@nutui/nutui-taro')['ActionSheet'] |
| 18 | NutButton: typeof import('@nutui/nutui-taro')['Button'] | 19 | NutButton: typeof import('@nutui/nutui-taro')['Button'] |
| 19 | NutCol: typeof import('@nutui/nutui-taro')['Col'] | 20 | NutCol: typeof import('@nutui/nutui-taro')['Col'] | ... | ... |
src/components/NumberRoll.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <view class="number-roll-container"> | ||
| 3 | + <view | ||
| 4 | + v-for="(digit, index) in digits" | ||
| 5 | + :key="index" | ||
| 6 | + class="digit-container" | ||
| 7 | + > | ||
| 8 | + <view class="digit-wrapper"> | ||
| 9 | + <view | ||
| 10 | + class="digit-column" | ||
| 11 | + :style="{ transform: `translateY(-${digit.currentValue * 10}%)` }" | ||
| 12 | + > | ||
| 13 | + <view | ||
| 14 | + v-for="num in 10" | ||
| 15 | + :key="num - 1" | ||
| 16 | + class="digit-item" | ||
| 17 | + > | ||
| 18 | + {{ num - 1 }} | ||
| 19 | + </view> | ||
| 20 | + </view> | ||
| 21 | + </view> | ||
| 22 | + </view> | ||
| 23 | + </view> | ||
| 24 | +</template> | ||
| 25 | + | ||
| 26 | +<script setup> | ||
| 27 | +import { ref, computed, watch, defineExpose } from 'vue' | ||
| 28 | + | ||
| 29 | +// Props | ||
| 30 | +const props = defineProps({ | ||
| 31 | + // 目标数值 | ||
| 32 | + targetValue: { | ||
| 33 | + type: Number, | ||
| 34 | + default: 0 | ||
| 35 | + }, | ||
| 36 | + // 数字位数 | ||
| 37 | + digitCount: { | ||
| 38 | + type: Number, | ||
| 39 | + default: 4 | ||
| 40 | + }, | ||
| 41 | + // 滚动速度(毫秒) | ||
| 42 | + duration: { | ||
| 43 | + type: Number, | ||
| 44 | + default: 2000 | ||
| 45 | + }, | ||
| 46 | + // 每位数字之间的延迟(毫秒) | ||
| 47 | + digitDelay: { | ||
| 48 | + type: Number, | ||
| 49 | + default: 200 | ||
| 50 | + } | ||
| 51 | +}) | ||
| 52 | + | ||
| 53 | +// 响应式数据 | ||
| 54 | +const isAnimating = ref(false) | ||
| 55 | +const isPaused = ref(false) | ||
| 56 | +const animationTimers = ref([]) | ||
| 57 | + | ||
| 58 | +// 初始化数字数组 | ||
| 59 | +const digits = ref([]) | ||
| 60 | + | ||
| 61 | +// 初始化数字位 | ||
| 62 | +const initDigits = () => { | ||
| 63 | + digits.value = Array.from({ length: props.digitCount }, (_, index) => ({ | ||
| 64 | + currentValue: 0, | ||
| 65 | + targetValue: 0, | ||
| 66 | + animationId: null, | ||
| 67 | + startTime: 0, | ||
| 68 | + pausedTime: 0 | ||
| 69 | + })) | ||
| 70 | +} | ||
| 71 | + | ||
| 72 | +// 将数字转换为数组 | ||
| 73 | +const parseTargetValue = (value) => { | ||
| 74 | + const str = value.toString().padStart(props.digitCount, '0') | ||
| 75 | + return str.split('').map(Number) | ||
| 76 | +} | ||
| 77 | + | ||
| 78 | +// 开始滚动动画 | ||
| 79 | +const startAnimation = () => { | ||
| 80 | + if (isAnimating.value && !isPaused.value) return | ||
| 81 | + | ||
| 82 | + isAnimating.value = true | ||
| 83 | + isPaused.value = false | ||
| 84 | + | ||
| 85 | + const targetDigits = parseTargetValue(props.targetValue) | ||
| 86 | + | ||
| 87 | + // 为每个数字位设置目标值 | ||
| 88 | + digits.value.forEach((digit, index) => { | ||
| 89 | + digit.targetValue = targetDigits[index] | ||
| 90 | + }) | ||
| 91 | + | ||
| 92 | + // 依次启动每个数字位的动画 | ||
| 93 | + digits.value.forEach((digit, index) => { | ||
| 94 | + const delay = index * props.digitDelay | ||
| 95 | + | ||
| 96 | + setTimeout(() => { | ||
| 97 | + if (!isAnimating.value || isPaused.value) return | ||
| 98 | + animateDigit(digit, index) | ||
| 99 | + }, delay) | ||
| 100 | + }) | ||
| 101 | +} | ||
| 102 | + | ||
| 103 | +// 单个数字位的动画 | ||
| 104 | +const animateDigit = (digit, index) => { | ||
| 105 | + const startValue = digit.currentValue | ||
| 106 | + const endValue = digit.targetValue | ||
| 107 | + const startTime = Date.now() | ||
| 108 | + | ||
| 109 | + digit.startTime = startTime | ||
| 110 | + | ||
| 111 | + const animate = () => { | ||
| 112 | + if (!isAnimating.value || isPaused.value) return | ||
| 113 | + | ||
| 114 | + const currentTime = Date.now() | ||
| 115 | + const elapsed = currentTime - startTime | ||
| 116 | + const progress = Math.min(elapsed / props.duration, 1) | ||
| 117 | + | ||
| 118 | + // 使用缓动函数 | ||
| 119 | + const easeProgress = easeOutCubic(progress) | ||
| 120 | + | ||
| 121 | + // 计算当前值 | ||
| 122 | + const currentValue = startValue + (endValue - startValue) * easeProgress | ||
| 123 | + digit.currentValue = currentValue | ||
| 124 | + | ||
| 125 | + if (progress < 1) { | ||
| 126 | + digit.animationId = requestAnimationFrame(animate) | ||
| 127 | + } else { | ||
| 128 | + digit.currentValue = endValue | ||
| 129 | + checkAnimationComplete() | ||
| 130 | + } | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + digit.animationId = requestAnimationFrame(animate) | ||
| 134 | +} | ||
| 135 | + | ||
| 136 | +// 缓动函数 | ||
| 137 | +const easeOutCubic = (t) => { | ||
| 138 | + return 1 - Math.pow(1 - t, 3) | ||
| 139 | +} | ||
| 140 | + | ||
| 141 | +// 检查动画是否完成 | ||
| 142 | +const checkAnimationComplete = () => { | ||
| 143 | + const allComplete = digits.value.every(digit => | ||
| 144 | + Math.abs(digit.currentValue - digit.targetValue) < 0.01 | ||
| 145 | + ) | ||
| 146 | + | ||
| 147 | + if (allComplete) { | ||
| 148 | + isAnimating.value = false | ||
| 149 | + // 确保所有数字都是整数 | ||
| 150 | + digits.value.forEach(digit => { | ||
| 151 | + digit.currentValue = digit.targetValue | ||
| 152 | + }) | ||
| 153 | + } | ||
| 154 | +} | ||
| 155 | + | ||
| 156 | +// 暂停动画 | ||
| 157 | +const pauseAnimation = () => { | ||
| 158 | + if (!isAnimating.value || isPaused.value) return | ||
| 159 | + | ||
| 160 | + isPaused.value = true | ||
| 161 | + | ||
| 162 | + digits.value.forEach(digit => { | ||
| 163 | + if (digit.animationId) { | ||
| 164 | + cancelAnimationFrame(digit.animationId) | ||
| 165 | + digit.animationId = null | ||
| 166 | + digit.pausedTime = Date.now() | ||
| 167 | + } | ||
| 168 | + }) | ||
| 169 | +} | ||
| 170 | + | ||
| 171 | +// 恢复动画 | ||
| 172 | +const resumeAnimation = () => { | ||
| 173 | + if (!isAnimating.value || !isPaused.value) return | ||
| 174 | + | ||
| 175 | + isPaused.value = false | ||
| 176 | + | ||
| 177 | + digits.value.forEach((digit, index) => { | ||
| 178 | + if (digit.currentValue !== digit.targetValue) { | ||
| 179 | + // 调整开始时间以补偿暂停的时间 | ||
| 180 | + const pausedDuration = digit.pausedTime - digit.startTime | ||
| 181 | + digit.startTime = Date.now() - pausedDuration | ||
| 182 | + animateDigit(digit, index) | ||
| 183 | + } | ||
| 184 | + }) | ||
| 185 | +} | ||
| 186 | + | ||
| 187 | +// 重置动画 | ||
| 188 | +const resetAnimation = () => { | ||
| 189 | + isAnimating.value = false | ||
| 190 | + isPaused.value = false | ||
| 191 | + | ||
| 192 | + // 取消所有动画 | ||
| 193 | + digits.value.forEach(digit => { | ||
| 194 | + if (digit.animationId) { | ||
| 195 | + cancelAnimationFrame(digit.animationId) | ||
| 196 | + digit.animationId = null | ||
| 197 | + } | ||
| 198 | + digit.currentValue = 0 | ||
| 199 | + digit.targetValue = 0 | ||
| 200 | + digit.startTime = 0 | ||
| 201 | + digit.pausedTime = 0 | ||
| 202 | + }) | ||
| 203 | +} | ||
| 204 | + | ||
| 205 | +// 监听目标值变化 | ||
| 206 | +watch(() => props.targetValue, () => { | ||
| 207 | + if (isAnimating.value) { | ||
| 208 | + resetAnimation() | ||
| 209 | + } | ||
| 210 | +}) | ||
| 211 | + | ||
| 212 | +// 初始化 | ||
| 213 | +initDigits() | ||
| 214 | + | ||
| 215 | +// 暴露方法 | ||
| 216 | +defineExpose({ | ||
| 217 | + start: startAnimation, | ||
| 218 | + pause: pauseAnimation, | ||
| 219 | + resume: resumeAnimation, | ||
| 220 | + reset: resetAnimation, | ||
| 221 | + isAnimating: computed(() => isAnimating.value), | ||
| 222 | + isPaused: computed(() => isPaused.value) | ||
| 223 | +}) | ||
| 224 | +</script> | ||
| 225 | + | ||
| 226 | +<style lang="less"> | ||
| 227 | +.number-roll-container { | ||
| 228 | + display: flex; | ||
| 229 | + align-items: center; | ||
| 230 | + justify-content: center; | ||
| 231 | + gap: 4rpx; | ||
| 232 | +} | ||
| 233 | + | ||
| 234 | +.digit-container { | ||
| 235 | + position: relative; | ||
| 236 | + width: 40rpx; | ||
| 237 | + height: 60rpx; | ||
| 238 | + overflow: hidden; | ||
| 239 | + background: rgba(255, 255, 255, 0.1); | ||
| 240 | + border-radius: 8rpx; | ||
| 241 | + border: 1rpx solid rgba(255, 255, 255, 0.2); | ||
| 242 | +} | ||
| 243 | + | ||
| 244 | +.digit-wrapper { | ||
| 245 | + position: relative; | ||
| 246 | + width: 100%; | ||
| 247 | + height: 100%; | ||
| 248 | + overflow: hidden; | ||
| 249 | +} | ||
| 250 | + | ||
| 251 | +.digit-column { | ||
| 252 | + position: absolute; | ||
| 253 | + top: 0; | ||
| 254 | + left: 0; | ||
| 255 | + width: 100%; | ||
| 256 | + transition: transform 0.1s ease-out; | ||
| 257 | +} | ||
| 258 | + | ||
| 259 | +.digit-item { | ||
| 260 | + width: 100%; | ||
| 261 | + height: 60rpx; | ||
| 262 | + display: flex; | ||
| 263 | + align-items: center; | ||
| 264 | + justify-content: center; | ||
| 265 | + font-size: 32rpx; | ||
| 266 | + font-weight: bold; | ||
| 267 | + color: #fff; | ||
| 268 | + font-family: 'Courier New', monospace; | ||
| 269 | +} | ||
| 270 | +</style> |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-01-09 00:00:00 | 2 | * @Date: 2025-01-09 00:00:00 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-10-11 13:50:59 | 4 | + * @LastEditTime: 2025-10-25 18:26:37 |
| 5 | * @FilePath: /lls_program/src/components/RankingCard.vue | 5 | * @FilePath: /lls_program/src/components/RankingCard.vue |
| 6 | * @Description: 排行榜卡片组件 | 6 | * @Description: 排行榜卡片组件 |
| 7 | --> | 7 | --> |
| ... | @@ -46,6 +46,20 @@ | ... | @@ -46,6 +46,20 @@ |
| 46 | <view v-if="activeTab === 'support'" class="absolute font-bold text-white bg-orange-500 top-0 rounded-full px-4 py-1" style="right: 30rpx; font-size: 23rpx;" @tap="joinOrganization">助力码</view> | 46 | <view v-if="activeTab === 'support'" class="absolute font-bold text-white bg-orange-500 top-0 rounded-full px-4 py-1" style="right: 30rpx; font-size: 23rpx;" @tap="joinOrganization">助力码</view> |
| 47 | </view> | 47 | </view> |
| 48 | 48 | ||
| 49 | + <view class="relative mb-2 text-white"> | ||
| 50 | + <view class="flex items-center justify-center"> | ||
| 51 | + <text class="mr-1" style="font-size: 28rpx;">总数: </text> | ||
| 52 | + <NumberRoll | ||
| 53 | + ref="numberRollRef" | ||
| 54 | + :target-value="currentTotalFamilySum" | ||
| 55 | + :digit-count="currentTotalFamilySum.length" | ||
| 56 | + :duration="1500" | ||
| 57 | + :digit-delay="150" | ||
| 58 | + /> | ||
| 59 | + <text class="mr-1" style="font-size: 28rpx;">个家庭</text> | ||
| 60 | + </view> | ||
| 61 | + </view> | ||
| 62 | + | ||
| 49 | <!-- 助力榜空状态提示 --> | 63 | <!-- 助力榜空状态提示 --> |
| 50 | <view v-if="activeTab === 'support' && (!supportData || !supportData.families || supportData.families.length === 0)" class="support-empty-state"> | 64 | <view v-if="activeTab === 'support' && (!supportData || !supportData.families || supportData.families.length === 0)" class="support-empty-state"> |
| 51 | <view class="empty-image"> | 65 | <view class="empty-image"> |
| ... | @@ -174,9 +188,10 @@ | ... | @@ -174,9 +188,10 @@ |
| 174 | </template> | 188 | </template> |
| 175 | 189 | ||
| 176 | <script setup> | 190 | <script setup> |
| 177 | -import { ref, computed, onMounted } from 'vue' | 191 | +import { ref, computed, onMounted, watch, nextTick } from 'vue' |
| 178 | import Taro from '@tarojs/taro' | 192 | import Taro from '@tarojs/taro' |
| 179 | import { IconFont } from '@nutui/icons-vue-taro'; | 193 | import { IconFont } from '@nutui/icons-vue-taro'; |
| 194 | +import NumberRoll from './NumberRoll.vue' | ||
| 180 | // 默认头像 | 195 | // 默认头像 |
| 181 | const defaultAvatar = 'https://cdn.ipadbiz.cn/lls_prog/images/%E5%85%A8%E5%AE%B6%E7%A6%8F3_%E5%89%AF%E6%9C%AC.jpg?imageMogr2/strip/quality/60' | 196 | const defaultAvatar = 'https://cdn.ipadbiz.cn/lls_prog/images/%E5%85%A8%E5%AE%B6%E7%A6%8F3_%E5%89%AF%E6%9C%AC.jpg?imageMogr2/strip/quality/60' |
| 182 | // 助力榜图片 | 197 | // 助力榜图片 |
| ... | @@ -228,7 +243,11 @@ const loading = ref(false) | ... | @@ -228,7 +243,11 @@ const loading = ref(false) |
| 228 | // 排行榜日期 | 243 | // 排行榜日期 |
| 229 | const currentDate = ref('') | 244 | const currentDate = ref('') |
| 230 | 245 | ||
| 246 | +// 当前总家庭数 | ||
| 247 | +const currentTotalFamilySum = ref(0); | ||
| 231 | 248 | ||
| 249 | +// 数字滚动组件引用 | ||
| 250 | +const numberRollRef = ref(null) | ||
| 232 | 251 | ||
| 233 | /** | 252 | /** |
| 234 | * 切换tab | 253 | * 切换tab |
| ... | @@ -281,6 +300,9 @@ const loadLeaderboardData = async (isInitialLoad = false) => { | ... | @@ -281,6 +300,9 @@ const loadLeaderboardData = async (isInitialLoad = false) => { |
| 281 | } | 300 | } |
| 282 | // 设置当前日期 | 301 | // 设置当前日期 |
| 283 | currentDate.value = response.data.yesterday | 302 | currentDate.value = response.data.yesterday |
| 303 | + // TODO: 设置总家庭数 | ||
| 304 | + const newTotalFamilySum = 2318; | ||
| 305 | + currentTotalFamilySum.value = newTotalFamilySum; | ||
| 284 | } | 306 | } |
| 285 | } catch (error) { | 307 | } catch (error) { |
| 286 | console.error('获取排行榜数据失败:', error) | 308 | console.error('获取排行榜数据失败:', error) |
| ... | @@ -465,7 +487,17 @@ onMounted(async () => { | ... | @@ -465,7 +487,17 @@ onMounted(async () => { |
| 465 | 487 | ||
| 466 | // 暴露刷新方法给父组件 | 488 | // 暴露刷新方法给父组件 |
| 467 | defineExpose({ | 489 | defineExpose({ |
| 468 | - refreshData | 490 | + refreshData, |
| 491 | + startNumberRoll: () => { | ||
| 492 | + if (numberRollRef.value) { | ||
| 493 | + numberRollRef.value.start() | ||
| 494 | + } | ||
| 495 | + }, | ||
| 496 | + resetNumberRoll: () => { | ||
| 497 | + if (numberRollRef.value) { | ||
| 498 | + numberRollRef.value.reset() | ||
| 499 | + } | ||
| 500 | + } | ||
| 469 | }) | 501 | }) |
| 470 | </script> | 502 | </script> |
| 471 | 503 | ||
| ... | @@ -582,7 +614,7 @@ defineExpose({ | ... | @@ -582,7 +614,7 @@ defineExpose({ |
| 582 | color: rgba(255, 255, 255, 0.8); | 614 | color: rgba(255, 255, 255, 0.8); |
| 583 | text-align: center; | 615 | text-align: center; |
| 584 | margin-top: 40rpx; | 616 | margin-top: 40rpx; |
| 585 | - margin-bottom: 40rpx; | 617 | + margin-bottom: 10rpx; |
| 586 | } | 618 | } |
| 587 | } | 619 | } |
| 588 | 620 | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-08-27 17:43:45 | 2 | * @Date: 2025-08-27 17:43:45 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-10-01 11:30:56 | 4 | + * @LastEditTime: 2025-10-25 18:22:46 |
| 5 | * @FilePath: /lls_program/src/pages/Dashboard/index.vue | 5 | * @FilePath: /lls_program/src/pages/Dashboard/index.vue |
| 6 | * @Description: 首页 | 6 | * @Description: 首页 |
| 7 | --> | 7 | --> |
| ... | @@ -149,7 +149,9 @@ | ... | @@ -149,7 +149,9 @@ |
| 149 | </WeRunAuth> | 149 | </WeRunAuth> |
| 150 | 150 | ||
| 151 | <!-- 排行榜卡片 --> | 151 | <!-- 排行榜卡片 --> |
| 152 | - <RankingCard ref="rankingCardRef" :onViewMore="openFamilyRank" /> | 152 | + <view id="ranking-card-container" ref="rankingCardContainer"> |
| 153 | + <RankingCard ref="rankingCardRef" :onViewMore="openFamilyRank" /> | ||
| 154 | + </view> | ||
| 153 | 155 | ||
| 154 | <!-- Family album --> | 156 | <!-- Family album --> |
| 155 | <FamilyAlbum | 157 | <FamilyAlbum |
| ... | @@ -172,7 +174,7 @@ | ... | @@ -172,7 +174,7 @@ |
| 172 | <script setup> | 174 | <script setup> |
| 173 | import "./index.less"; | 175 | import "./index.less"; |
| 174 | import { ref, computed, onMounted, onUnmounted } from 'vue'; | 176 | import { ref, computed, onMounted, onUnmounted } from 'vue'; |
| 175 | -import Taro, { useDidShow, useReady, useLoad } from '@tarojs/taro'; | 177 | +import Taro, { useDidShow, useReady, useLoad, usePageScroll } from '@tarojs/taro'; |
| 176 | import { handleSharePageAuth, addShareFlag } from '@/utils/authRedirect'; | 178 | import { handleSharePageAuth, addShareFlag } from '@/utils/authRedirect'; |
| 177 | import { Setting, Photograph, IconFont } from '@nutui/icons-vue-taro'; | 179 | import { Setting, Photograph, IconFont } from '@nutui/icons-vue-taro'; |
| 178 | import BottomNav from '@/components/BottomNav.vue'; | 180 | import BottomNav from '@/components/BottomNav.vue'; |
| ... | @@ -195,6 +197,7 @@ const isWeRunAuthorized = ref(false); | ... | @@ -195,6 +197,7 @@ const isWeRunAuthorized = ref(false); |
| 195 | const pointsCollectorRef = ref(null) | 197 | const pointsCollectorRef = ref(null) |
| 196 | const weRunAuthRef = ref(null) | 198 | const weRunAuthRef = ref(null) |
| 197 | const rankingCardRef = ref(null) | 199 | const rankingCardRef = ref(null) |
| 200 | +const rankingCardContainer = ref(null) | ||
| 198 | const showTotalPointsOnly = ref(false) | 201 | const showTotalPointsOnly = ref(false) |
| 199 | const finalTotalPoints = ref(0) | 202 | const finalTotalPoints = ref(0) |
| 200 | const pendingPoints = ref([]) // 待收集的积分数据 | 203 | const pendingPoints = ref([]) // 待收集的积分数据 |
| ... | @@ -302,19 +305,33 @@ const handleSyncFailed = (data) => { | ... | @@ -302,19 +305,33 @@ const handleSyncFailed = (data) => { |
| 302 | console.log('微信步数同步失败:', data) | 305 | console.log('微信步数同步失败:', data) |
| 303 | } | 306 | } |
| 304 | 307 | ||
| 308 | +// 滚动监听相关 | ||
| 309 | +const hasTriggeredNumberRoll = ref(false) | ||
| 310 | + | ||
| 305 | // 监听全局的微信步数同步成功事件(用于处理401重试后的成功情况) | 311 | // 监听全局的微信步数同步成功事件(用于处理401重试后的成功情况) |
| 306 | onMounted(() => { | 312 | onMounted(() => { |
| 307 | if (Taro.eventCenter) { | 313 | if (Taro.eventCenter) { |
| 308 | Taro.eventCenter.on('wx-steps-sync-success', handleStepsSynced); | 314 | Taro.eventCenter.on('wx-steps-sync-success', handleStepsSynced); |
| 309 | } | 315 | } |
| 316 | + | ||
| 317 | + // 设置滚动监听(实际通过 usePageScroll 实现) | ||
| 318 | + setupScrollListener() | ||
| 310 | }); | 319 | }); |
| 311 | 320 | ||
| 312 | onUnmounted(() => { | 321 | onUnmounted(() => { |
| 313 | if (Taro.eventCenter) { | 322 | if (Taro.eventCenter) { |
| 314 | Taro.eventCenter.off('wx-steps-sync-success', handleStepsSynced); | 323 | Taro.eventCenter.off('wx-steps-sync-success', handleStepsSynced); |
| 315 | } | 324 | } |
| 325 | + | ||
| 326 | + // 清理滚动监听 | ||
| 327 | + cleanupScrollListener() | ||
| 316 | }); | 328 | }); |
| 317 | 329 | ||
| 330 | +// 使用 usePageScroll 监听页面滚动 | ||
| 331 | +usePageScroll((res) => { | ||
| 332 | + handlePageScroll(res.scrollTop) | ||
| 333 | +}) | ||
| 334 | + | ||
| 318 | /** | 335 | /** |
| 319 | * 计算总步数(包含用户步数和家庭成员步数) | 336 | * 计算总步数(包含用户步数和家庭成员步数) |
| 320 | * @returns {string} 格式化后的总步数 | 337 | * @returns {string} 格式化后的总步数 |
| ... | @@ -588,4 +605,82 @@ const isTabletDevice = computed(() => { | ... | @@ -588,4 +605,82 @@ const isTabletDevice = computed(() => { |
| 588 | // 普通手机设备比例通常在 0.4-0.6 之间 | 605 | // 普通手机设备比例通常在 0.4-0.6 之间 |
| 589 | return screenRatio > 0.65; | 606 | return screenRatio > 0.65; |
| 590 | }); | 607 | }); |
| 608 | + | ||
| 609 | +/** | ||
| 610 | + * 设置滚动监听 | ||
| 611 | + */ | ||
| 612 | +const setupScrollListener = () => { | ||
| 613 | + // 使用 usePageScroll 监听页面滚动 | ||
| 614 | + // 注意:usePageScroll 需要在组件顶层调用 | ||
| 615 | +} | ||
| 616 | + | ||
| 617 | +/** | ||
| 618 | + * 清理滚动监听 | ||
| 619 | + */ | ||
| 620 | +const cleanupScrollListener = () => { | ||
| 621 | + // usePageScroll 会自动清理,无需手动清理 | ||
| 622 | +} | ||
| 623 | + | ||
| 624 | +/** | ||
| 625 | + * 处理页面滚动事件 | ||
| 626 | + * @param {number} scrollTop - 滚动距离 | ||
| 627 | + */ | ||
| 628 | +const handlePageScroll = (scrollTop) => { | ||
| 629 | + // 如果已经触发过数字滚动,则不再处理 | ||
| 630 | + if (hasTriggeredNumberRoll.value) { | ||
| 631 | + return | ||
| 632 | + } | ||
| 633 | + | ||
| 634 | + // console.log('页面滚动:', scrollTop) | ||
| 635 | + | ||
| 636 | + // 检查 RankingCard 组件是否进入视窗 | ||
| 637 | + if (rankingCardContainer.value) { | ||
| 638 | + Taro.createSelectorQuery() | ||
| 639 | + .select('#ranking-card-container') | ||
| 640 | + .boundingClientRect((rect) => { | ||
| 641 | + if (rect) { | ||
| 642 | + // console.log('RankingCard 位置信息:', rect) | ||
| 643 | + // 获取系统信息 | ||
| 644 | + Taro.getSystemInfo({ | ||
| 645 | + success: (res) => { | ||
| 646 | + const windowHeight = res.windowHeight | ||
| 647 | + // 当组件顶部进入视窗下方80%位置时触发 | ||
| 648 | + const triggerPoint = windowHeight * 0.8 | ||
| 649 | + | ||
| 650 | + // console.log('窗口高度:', windowHeight, '触发点:', triggerPoint, 'rect.top:', rect.top) | ||
| 651 | + | ||
| 652 | + if (rect.top <= triggerPoint && rect.bottom >= 0) { | ||
| 653 | + // console.log('触发数字滚动动画!') | ||
| 654 | + // 触发数字滚动动画 | ||
| 655 | + triggerNumberRoll() | ||
| 656 | + } | ||
| 657 | + } | ||
| 658 | + }) | ||
| 659 | + } else { | ||
| 660 | + console.log('未找到 RankingCard 容器元素') | ||
| 661 | + } | ||
| 662 | + }) | ||
| 663 | + .exec() | ||
| 664 | + } else { | ||
| 665 | + console.log('rankingCardContainer.value 为空') | ||
| 666 | + } | ||
| 667 | +} | ||
| 668 | + | ||
| 669 | +/** | ||
| 670 | + * 触发数字滚动动画 | ||
| 671 | + */ | ||
| 672 | +const triggerNumberRoll = () => { | ||
| 673 | + if (hasTriggeredNumberRoll.value) { | ||
| 674 | + return | ||
| 675 | + } | ||
| 676 | + | ||
| 677 | + hasTriggeredNumberRoll.value = true | ||
| 678 | + | ||
| 679 | + // 延迟一点时间再触发,让用户看到滚动效果 | ||
| 680 | + setTimeout(() => { | ||
| 681 | + if (rankingCardRef.value && rankingCardRef.value.startNumberRoll) { | ||
| 682 | + rankingCardRef.value.startNumberRoll() | ||
| 683 | + } | ||
| 684 | + }, 300) | ||
| 685 | +} | ||
| 591 | </script> | 686 | </script> | ... | ... |
-
Please register or login to post a comment