feat(排行榜): 添加可滚动家庭列表组件替换弹幕功能
- 新增 ScrollableFamilyList 组件用于展示家庭信息 - 替换原有 NativeDanmuComponent 实现 - 优化支持页面的布局和样式 - 添加家庭数据获取和处理逻辑
Showing
4 changed files
with
496 additions
and
25 deletions
| ... | @@ -37,6 +37,7 @@ declare module 'vue' { | ... | @@ -37,6 +37,7 @@ declare module 'vue' { |
| 37 | RankingCard: typeof import('./src/components/RankingCard.vue')['default'] | 37 | RankingCard: typeof import('./src/components/RankingCard.vue')['default'] |
| 38 | RouterLink: typeof import('vue-router')['RouterLink'] | 38 | RouterLink: typeof import('vue-router')['RouterLink'] |
| 39 | RouterView: typeof import('vue-router')['RouterView'] | 39 | RouterView: typeof import('vue-router')['RouterView'] |
| 40 | + ScrollableFamilyList: typeof import('./src/components/ScrollableFamilyList.vue')['default'] | ||
| 40 | ShareButton: typeof import('./src/components/ShareButton/index.vue')['default'] | 41 | ShareButton: typeof import('./src/components/ShareButton/index.vue')['default'] |
| 41 | TabBar: typeof import('./src/components/TabBar.vue')['default'] | 42 | TabBar: typeof import('./src/components/TabBar.vue')['default'] |
| 42 | TotalPointsDisplay: typeof import('./src/components/TotalPointsDisplay.vue')['default'] | 43 | TotalPointsDisplay: typeof import('./src/components/TotalPointsDisplay.vue')['default'] | ... | ... |
| 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-29 10:17:37 | 4 | + * @LastEditTime: 2025-10-29 13:07:23 |
| 5 | * @FilePath: /lls_program/src/components/RankingCard.vue | 5 | * @FilePath: /lls_program/src/components/RankingCard.vue |
| 6 | * @Description: 排行榜卡片组件 | 6 | * @Description: 排行榜卡片组件 |
| 7 | --> | 7 | --> |
| ... | @@ -42,9 +42,11 @@ | ... | @@ -42,9 +42,11 @@ |
| 42 | <view class="rank-content" :class="{ 'content-switching': isContentSwitching }" style="margin: 40rpx;"> | 42 | <view class="rank-content" :class="{ 'content-switching': isContentSwitching }" style="margin: 40rpx;"> |
| 43 | <!-- 排行榜日期 --> | 43 | <!-- 排行榜日期 --> |
| 44 | <view class="rank-date relative"> | 44 | <view class="rank-date relative"> |
| 45 | - <!-- <view class="flex items-center justify-center"><text class="mr-2">截止昨日</text>{{ currentDate }}<text v-if="activeTab !== 'shanghai'" class="ml-2">排名</text><IconFont name="ask" size="16" class="ml-2" @click="handleRankAskClick"></IconFont></view> --> | 45 | + <view v-if="activeTab !== 'support'" class="flex items-center justify-center"><text class="mr-2">截止昨日</text>{{ currentDate }}<text class="ml-2">排名</text><IconFont name="ask" size="14" class="ml-2" @click="handleRankAskClick"></IconFont></view> |
| 46 | - <view class="flex items-center justify-center"><text class="mr-2">截止昨日</text>{{ currentDate }}<text class="ml-2">排名</text><IconFont v-if="activeTab !== 'support'" name="ask" size="16" class="ml-2" @click="handleRankAskClick"></IconFont></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: 45rpx; top: -5rpx; font-size: 23rpx;" @tap="joinOrganization">助力码</view> --> |
| 47 | - <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; top: -5rpx; font-size: 23rpx; z-index: 999;" @tap="joinOrganization">助力码</view> | 47 | + <view v-if="activeTab === 'support'" class="flex items-center justify-center"> |
| 48 | + <text class="font-bold text-white bg-orange-500 top-0 rounded-full px-4 py-1" style="font-size: 23rpx;" @tap="joinOrganization">助力码</text> | ||
| 49 | + </view> | ||
| 48 | </view> | 50 | </view> |
| 49 | 51 | ||
| 50 | <!-- <view v-if="activeTab === 'shanghai'" class="relative mb-2 text-white"> | 52 | <!-- <view v-if="activeTab === 'shanghai'" class="relative mb-2 text-white"> |
| ... | @@ -125,7 +127,7 @@ | ... | @@ -125,7 +127,7 @@ |
| 125 | </view> | 127 | </view> |
| 126 | 128 | ||
| 127 | <!-- 隐藏的弹幕组件,在页面加载时就开始滚动 --> | 129 | <!-- 隐藏的弹幕组件,在页面加载时就开始滚动 --> |
| 128 | - <view class="danmu-section" :style="{ visibility: activeTab === 'support' ? 'visible' : 'hidden', position: activeTab === 'support' ? 'static' : 'absolute', top: activeTab === 'support' ? 'auto' : '-9999px' }"> | 130 | + <!-- <view class="danmu-section" :style="{ visibility: activeTab === 'support' ? 'visible' : 'hidden', position: activeTab === 'support' ? 'static' : 'absolute', top: activeTab === 'support' ? 'auto' : '-9999px' }"> |
| 129 | <NativeDanmuComponent | 131 | <NativeDanmuComponent |
| 130 | :container-height="700" | 132 | :container-height="700" |
| 131 | :show-controls="true" | 133 | :show-controls="true" |
| ... | @@ -134,6 +136,14 @@ | ... | @@ -134,6 +136,14 @@ |
| 134 | @danmu-hover="handleDanmuHover" | 136 | @danmu-hover="handleDanmuHover" |
| 135 | ref="danmuRef" | 137 | ref="danmuRef" |
| 136 | /> | 138 | /> |
| 139 | + </view> --> | ||
| 140 | + | ||
| 141 | + <!-- 可滚动家庭简介列表组件 --> | ||
| 142 | + <view v-if="activeTab === 'support'" class="scrollable-family-section mt-1" style="padding: 0 24rpx 34rpx;"> | ||
| 143 | + <ScrollableFamilyList | ||
| 144 | + :family-data="familyDanmus" | ||
| 145 | + height="560rpx" | ||
| 146 | + /> | ||
| 137 | </view> | 147 | </view> |
| 138 | 148 | ||
| 139 | <!-- 我的排名卡片 --> | 149 | <!-- 我的排名卡片 --> |
| ... | @@ -205,7 +215,8 @@ import { ref, computed, onMounted, watch, nextTick } from 'vue' | ... | @@ -205,7 +215,8 @@ import { ref, computed, onMounted, watch, nextTick } from 'vue' |
| 205 | import Taro from '@tarojs/taro' | 215 | import Taro from '@tarojs/taro' |
| 206 | import { IconFont } from '@nutui/icons-vue-taro'; | 216 | import { IconFont } from '@nutui/icons-vue-taro'; |
| 207 | // import NumberRoll from './NumberRoll.vue' | 217 | // import NumberRoll from './NumberRoll.vue' |
| 208 | -import NativeDanmuComponent from '@/components/NativeDanmuComponent.vue' | 218 | +// import NativeDanmuComponent from '@/components/NativeDanmuComponent.vue' |
| 219 | +import ScrollableFamilyList from '@/components/ScrollableFamilyList.vue' | ||
| 209 | // 默认头像 | 220 | // 默认头像 |
| 210 | 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' | 221 | 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' |
| 211 | // 助力榜图片 | 222 | // 助力榜图片 |
| ... | @@ -324,6 +335,9 @@ const switchTab = async (tab) => { | ... | @@ -324,6 +335,9 @@ const switchTab = async (tab) => { |
| 324 | }, 200) | 335 | }, 200) |
| 325 | } | 336 | } |
| 326 | 337 | ||
| 338 | +// 家庭弹幕数据 | ||
| 339 | +const familyDanmus = ref([]) | ||
| 340 | + | ||
| 327 | /** | 341 | /** |
| 328 | * 加载排行榜数据 | 342 | * 加载排行榜数据 |
| 329 | * @param {boolean} isInitialLoad - 是否为初始加载,避免无限递归 | 343 | * @param {boolean} isInitialLoad - 是否为初始加载,避免无限递归 |
| ... | @@ -353,6 +367,15 @@ const loadLeaderboardData = async (isInitialLoad = false) => { | ... | @@ -353,6 +367,15 @@ const loadLeaderboardData = async (isInitialLoad = false) => { |
| 353 | currentDate.value = response.data.yesterday | 367 | currentDate.value = response.data.yesterday |
| 354 | // 设置总家庭数 | 368 | // 设置总家庭数 |
| 355 | currentTotalFamilySum.value = response.data.family_count || 0; | 369 | currentTotalFamilySum.value = response.data.family_count || 0; |
| 370 | + // 家庭弹幕数据处理 | ||
| 371 | + if (response.data?.bullet_families) { | ||
| 372 | + familyDanmus.value = response.data.bullet_families.map(family => ({ | ||
| 373 | + id: family.id, | ||
| 374 | + familyName: family.name, | ||
| 375 | + familyIntro: family.note, | ||
| 376 | + avatar: family.avatar_url || defaultAvatar | ||
| 377 | + })) | ||
| 378 | + } | ||
| 356 | } | 379 | } |
| 357 | } catch (error) { | 380 | } catch (error) { |
| 358 | console.error('获取排行榜数据失败:', error) | 381 | console.error('获取排行榜数据失败:', error) | ... | ... |
src/components/ScrollableFamilyList.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <view class="scrollable-family-list" :style="{ height: containerHeight }"> | ||
| 3 | + <scroll-view | ||
| 4 | + class="scroll-container" | ||
| 5 | + :scroll-y="true" | ||
| 6 | + :scroll-top="scrollTop" | ||
| 7 | + :enable-back-to-top="false" | ||
| 8 | + :scroll-with-animation="true" | ||
| 9 | + :enhanced="true" | ||
| 10 | + :bounces="true" | ||
| 11 | + :show-scrollbar="false" | ||
| 12 | + @scroll="onScroll" | ||
| 13 | + @scrolltoupper="onScrollToUpper" | ||
| 14 | + @scrolltolower="onScrollToLower" | ||
| 15 | + > | ||
| 16 | + <view class="content-wrapper" :style="{ opacity: contentOpacity }"> | ||
| 17 | + <view | ||
| 18 | + v-for="(item, index) in currentPageData" | ||
| 19 | + :key="`${currentPage}-${index}`" | ||
| 20 | + class="family-item" | ||
| 21 | + :style="getItemStyle(index)" | ||
| 22 | + > | ||
| 23 | + <view class="family-content"> | ||
| 24 | + <!-- 头像 --> | ||
| 25 | + <view class="family-avatar-container"> | ||
| 26 | + <image | ||
| 27 | + :src="item.avatar" | ||
| 28 | + class="family-avatar" | ||
| 29 | + mode="aspectFill" | ||
| 30 | + /> | ||
| 31 | + </view> | ||
| 32 | + | ||
| 33 | + <!-- 信息区域 --> | ||
| 34 | + <view class="family-info"> | ||
| 35 | + <!-- 标题行 --> | ||
| 36 | + <view class="family-title-row"> | ||
| 37 | + <text class="family-name">{{ item.familyName }}</text> | ||
| 38 | + </view> | ||
| 39 | + | ||
| 40 | + <!-- 介绍 --> | ||
| 41 | + <text class="family-intro">{{ item.familyIntro }}</text> | ||
| 42 | + </view> | ||
| 43 | + </view> | ||
| 44 | + </view> | ||
| 45 | + </view> | ||
| 46 | + </scroll-view> | ||
| 47 | + </view> | ||
| 48 | +</template> | ||
| 49 | + | ||
| 50 | +<script setup> | ||
| 51 | +import { ref, computed, watch, onMounted, nextTick } from 'vue' | ||
| 52 | +import Taro, { usePageScroll } from '@tarojs/taro' | ||
| 53 | + | ||
| 54 | +// Props | ||
| 55 | +const props = defineProps({ | ||
| 56 | + // 家庭数据列表 | ||
| 57 | + familyData: { | ||
| 58 | + type: Array, | ||
| 59 | + default: () => [] | ||
| 60 | + }, | ||
| 61 | + // 容器高度 | ||
| 62 | + height: { | ||
| 63 | + type: String, | ||
| 64 | + default: '600rpx' | ||
| 65 | + }, | ||
| 66 | + // 每页显示的行数 | ||
| 67 | + itemsPerPage: { | ||
| 68 | + type: Number, | ||
| 69 | + default: 5 | ||
| 70 | + }, | ||
| 71 | + // 是否监听外部容器滚动 | ||
| 72 | + listenExternalScroll: { | ||
| 73 | + type: Boolean, | ||
| 74 | + default: false | ||
| 75 | + } | ||
| 76 | +}) | ||
| 77 | + | ||
| 78 | +// 响应式数据 | ||
| 79 | +const scrollTop = ref(0) | ||
| 80 | +const currentPage = ref(0) | ||
| 81 | +const contentOpacity = ref(1) | ||
| 82 | +const isTransitioning = ref(false) | ||
| 83 | +const lastScrollTop = ref(0) | ||
| 84 | +const scrollDirection = ref('') // 'up' | 'down' | ||
| 85 | + | ||
| 86 | +// 计算属性 | ||
| 87 | +const containerHeight = computed(() => props.height) | ||
| 88 | + | ||
| 89 | +const totalPages = computed(() => { | ||
| 90 | + return Math.ceil(props.familyData.length / props.itemsPerPage) | ||
| 91 | +}) | ||
| 92 | + | ||
| 93 | +const currentPageData = computed(() => { | ||
| 94 | + const start = currentPage.value * props.itemsPerPage | ||
| 95 | + // 每页多显示一个item,有利于滚动触发 | ||
| 96 | + const end = start + props.itemsPerPage + 2 | ||
| 97 | + return props.familyData.slice(start, end) | ||
| 98 | +}) | ||
| 99 | + | ||
| 100 | +// 随机水平位置数组 | ||
| 101 | +const randomOffsets = ref([]) | ||
| 102 | + | ||
| 103 | +// 生成随机水平偏移 | ||
| 104 | +const generateRandomOffsets = () => { | ||
| 105 | + // 为实际显示的item数量生成偏移(包括多出的一个) | ||
| 106 | + const actualItemCount = Math.min(props.itemsPerPage + 2, props.familyData.length - currentPage.value * props.itemsPerPage) | ||
| 107 | + | ||
| 108 | + randomOffsets.value = Array.from({ length: actualItemCount }, (_, index) => { | ||
| 109 | + // 确保一屏内有靠最左和最右的简介,但不能被裁切 | ||
| 110 | + if (index === 0) { | ||
| 111 | + // 第一个简介靠左,但留出足够边距避免裁切 | ||
| 112 | + return 0 // 不偏移,保持在安全区域 | ||
| 113 | + } else if (index === actualItemCount - 1) { | ||
| 114 | + // 最后一个简介靠最右,使用特殊标记 | ||
| 115 | + return 100 // 使用特殊值标记需要贴右边 | ||
| 116 | + } else { | ||
| 117 | + // 其他简介在中间随机分布,避免负偏移 | ||
| 118 | + return Math.floor(Math.random() * 81) // 0rpx 到 80rpx | ||
| 119 | + } | ||
| 120 | + }) | ||
| 121 | +} | ||
| 122 | + | ||
| 123 | +// 获取每个项目的样式 | ||
| 124 | +const getItemStyle = (index) => { | ||
| 125 | + const offset = randomOffsets.value[index] || 0 | ||
| 126 | + const isLastItem = index === randomOffsets.value.length - 1 | ||
| 127 | + | ||
| 128 | + // 最后一个item贴右边的特殊处理 | ||
| 129 | + if (isLastItem && offset === 100) { | ||
| 130 | + return { | ||
| 131 | + marginBottom: '24rpx', | ||
| 132 | + maxWidth: '100%', | ||
| 133 | + paddingLeft: '0', | ||
| 134 | + paddingRight: '0', | ||
| 135 | + display: 'flex', | ||
| 136 | + justifyContent: 'flex-end', // 让内容靠右对齐 | ||
| 137 | + // 不使用transform,完全依靠justify-content来贴右边 | ||
| 138 | + } | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | + // 其他item的正常处理 | ||
| 142 | + return { | ||
| 143 | + transform: `translateX(${offset}rpx)`, | ||
| 144 | + marginBottom: '24rpx', | ||
| 145 | + // 确保内容不被裁切,所有偏移都是正值或0 | ||
| 146 | + paddingRight: offset > 50 ? '24rpx' : '0', | ||
| 147 | + maxWidth: '100%', | ||
| 148 | + // 确保左侧有足够的边距 | ||
| 149 | + paddingLeft: '0' | ||
| 150 | + } | ||
| 151 | +} | ||
| 152 | + | ||
| 153 | +// 滚动事件处理 | ||
| 154 | +const onScroll = (e) => { | ||
| 155 | + if (isTransitioning.value) return | ||
| 156 | + | ||
| 157 | + const currentScrollTop = e.detail.scrollTop | ||
| 158 | + const delta = currentScrollTop - lastScrollTop.value | ||
| 159 | + | ||
| 160 | + // 提高阈值,减少误触发 | ||
| 161 | + if (Math.abs(delta) > 10) { | ||
| 162 | + scrollDirection.value = delta > 0 ? 'down' : 'up' | ||
| 163 | + } | ||
| 164 | + | ||
| 165 | + lastScrollTop.value = currentScrollTop | ||
| 166 | +} | ||
| 167 | + | ||
| 168 | +// 滚动到顶部 - 优化触发逻辑 | ||
| 169 | +const onScrollToUpper = () => { | ||
| 170 | + if (isTransitioning.value) return | ||
| 171 | + | ||
| 172 | + // 添加延迟防抖,避免快速触发 | ||
| 173 | + setTimeout(() => { | ||
| 174 | + if (scrollDirection.value === 'up' && !isTransitioning.value) { | ||
| 175 | + changePage('prev') | ||
| 176 | + } | ||
| 177 | + }, 100) | ||
| 178 | +} | ||
| 179 | + | ||
| 180 | +// 滚动到底部 - 优化触发逻辑 | ||
| 181 | +const onScrollToLower = () => { | ||
| 182 | + if (isTransitioning.value) return | ||
| 183 | + | ||
| 184 | + // 添加延迟防抖,避免快速触发 | ||
| 185 | + setTimeout(() => { | ||
| 186 | + if (scrollDirection.value === 'down' && !isTransitioning.value) { | ||
| 187 | + changePage('next') | ||
| 188 | + } | ||
| 189 | + }, 100) | ||
| 190 | +} | ||
| 191 | + | ||
| 192 | +// 切换页面 - 优化过渡效果 | ||
| 193 | +const changePage = async (direction) => { | ||
| 194 | + if (isTransitioning.value) return | ||
| 195 | + | ||
| 196 | + isTransitioning.value = true | ||
| 197 | + | ||
| 198 | + // 重置滚动方向,避免连续触发 | ||
| 199 | + scrollDirection.value = '' | ||
| 200 | + | ||
| 201 | + // 淡出效果 | ||
| 202 | + contentOpacity.value = 0 | ||
| 203 | + | ||
| 204 | + await new Promise(resolve => setTimeout(resolve, 250)) | ||
| 205 | + | ||
| 206 | + // 更新页面 | ||
| 207 | + if (direction === 'next') { | ||
| 208 | + currentPage.value = (currentPage.value + 1) % totalPages.value | ||
| 209 | + } else { | ||
| 210 | + currentPage.value = currentPage.value === 0 | ||
| 211 | + ? totalPages.value - 1 | ||
| 212 | + : currentPage.value - 1 | ||
| 213 | + } | ||
| 214 | + | ||
| 215 | + generateRandomOffsets() | ||
| 216 | + | ||
| 217 | + // 重置滚动位置到中间位置,确保有足够的滚动空间 | ||
| 218 | + // 设置一个中间值,让用户可以向上或向下滚动 | ||
| 219 | + scrollTop.value = 100 | ||
| 220 | + | ||
| 221 | + await nextTick() | ||
| 222 | + | ||
| 223 | + // 再次确保滚动位置重置,给足够的滚动触发空间 | ||
| 224 | + setTimeout(() => { | ||
| 225 | + scrollTop.value = 50 | ||
| 226 | + }, 50) | ||
| 227 | + | ||
| 228 | + // 淡入效果 | ||
| 229 | + contentOpacity.value = 1 | ||
| 230 | + | ||
| 231 | + // 延长过渡锁定时间,确保动画完成 | ||
| 232 | + setTimeout(() => { | ||
| 233 | + isTransitioning.value = false | ||
| 234 | + // 最终重置到一个稳定的中间位置 | ||
| 235 | + scrollTop.value = 80 | ||
| 236 | + }, 400) | ||
| 237 | +} | ||
| 238 | + | ||
| 239 | +// 外部滚动监听 | ||
| 240 | +const handleExternalScroll = (res) => { | ||
| 241 | + if (!props.listenExternalScroll || isTransitioning.value) return | ||
| 242 | + | ||
| 243 | + // 获取页面滚动信息 | ||
| 244 | + const { scrollTop } = res | ||
| 245 | + const windowHeight = Taro.getSystemInfoSync().windowHeight | ||
| 246 | + | ||
| 247 | + // 使用createSelectorQuery获取页面高度 | ||
| 248 | + const query = Taro.createSelectorQuery() | ||
| 249 | + query.selectViewport().scrollOffset() | ||
| 250 | + query.exec((queryRes) => { | ||
| 251 | + if (queryRes && queryRes[0]) { | ||
| 252 | + const { scrollHeight } = queryRes[0] | ||
| 253 | + | ||
| 254 | + // 判断是否滚动到底部(留一些余量) | ||
| 255 | + if (scrollTop + windowHeight >= scrollHeight - 50) { | ||
| 256 | + // 触发翻页到下一页 | ||
| 257 | + changePage('next') | ||
| 258 | + } | ||
| 259 | + } | ||
| 260 | + }) | ||
| 261 | +} | ||
| 262 | + | ||
| 263 | +// 使用usePageScroll监听页面滚动 | ||
| 264 | +if (props.listenExternalScroll) { | ||
| 265 | + usePageScroll(handleExternalScroll) | ||
| 266 | +} | ||
| 267 | + | ||
| 268 | +// 监听数据变化 | ||
| 269 | +watch(() => props.familyData, () => { | ||
| 270 | + currentPage.value = 0 | ||
| 271 | + generateRandomOffsets() | ||
| 272 | +}, { immediate: true }) | ||
| 273 | + | ||
| 274 | +// 组件挂载 | ||
| 275 | +onMounted(() => { | ||
| 276 | + generateRandomOffsets() | ||
| 277 | +}) | ||
| 278 | +</script> | ||
| 279 | + | ||
| 280 | +<style> | ||
| 281 | +.scrollable-family-list { | ||
| 282 | + position: relative; | ||
| 283 | + width: 100%; | ||
| 284 | + /* border-radius: 20rpx; */ | ||
| 285 | + overflow: hidden; | ||
| 286 | + /* background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); */ | ||
| 287 | + | ||
| 288 | +} | ||
| 289 | + | ||
| 290 | +.scroll-container { | ||
| 291 | + width: 100%; | ||
| 292 | + height: 100%; | ||
| 293 | +} | ||
| 294 | + | ||
| 295 | +.content-wrapper { | ||
| 296 | + /* padding: 32rpx 0; */ | ||
| 297 | + padding-top: 32rpx; | ||
| 298 | + transition: opacity 0.3s ease; | ||
| 299 | + min-height: 100%; | ||
| 300 | +} | ||
| 301 | + | ||
| 302 | +.family-item { | ||
| 303 | + width: 100%; | ||
| 304 | + transition: transform 0.3s ease; | ||
| 305 | +} | ||
| 306 | + | ||
| 307 | +.family-content { | ||
| 308 | + display: flex; | ||
| 309 | + align-items: center; | ||
| 310 | + background: rgba(255, 255, 255, 0.75); | ||
| 311 | + border-radius: 30rpx; | ||
| 312 | + padding: 20rpx 24rpx; | ||
| 313 | + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08); | ||
| 314 | + backdrop-filter: blur(12rpx); | ||
| 315 | + border: 2rpx solid rgba(255, 255, 255, 0.5); | ||
| 316 | + max-width: 500rpx; | ||
| 317 | + min-width: 350rpx; | ||
| 318 | + transition: all 0.3s ease; | ||
| 319 | +} | ||
| 320 | + | ||
| 321 | +.family-content:hover { | ||
| 322 | + transform: translateY(-4rpx); | ||
| 323 | + box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.12); | ||
| 324 | +} | ||
| 325 | + | ||
| 326 | +.family-avatar-container { | ||
| 327 | + flex-shrink: 0; | ||
| 328 | + margin-right: 20rpx; | ||
| 329 | + position: relative; | ||
| 330 | +} | ||
| 331 | + | ||
| 332 | +.family-avatar { | ||
| 333 | + width: 64rpx; | ||
| 334 | + height: 64rpx; | ||
| 335 | + border-radius: 50%; | ||
| 336 | + border: 3rpx solid #fff; | ||
| 337 | + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15); | ||
| 338 | +} | ||
| 339 | + | ||
| 340 | +.family-avatar-container::after { | ||
| 341 | + content: ''; | ||
| 342 | + position: absolute; | ||
| 343 | + top: -3rpx; | ||
| 344 | + left: -3rpx; | ||
| 345 | + right: -3rpx; | ||
| 346 | + bottom: -3rpx; | ||
| 347 | + border-radius: 50%; | ||
| 348 | + background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4); | ||
| 349 | + z-index: -1; | ||
| 350 | + animation: rotate 4s linear infinite; | ||
| 351 | +} | ||
| 352 | + | ||
| 353 | +@keyframes rotate { | ||
| 354 | + from { transform: rotate(0deg); } | ||
| 355 | + to { transform: rotate(360deg); } | ||
| 356 | +} | ||
| 357 | + | ||
| 358 | +.family-info { | ||
| 359 | + flex: 1; | ||
| 360 | + min-width: 0; | ||
| 361 | +} | ||
| 362 | + | ||
| 363 | +.family-title-row { | ||
| 364 | + display: flex; | ||
| 365 | + align-items: center; | ||
| 366 | + margin-bottom: 8rpx; | ||
| 367 | +} | ||
| 368 | + | ||
| 369 | +.family-name { | ||
| 370 | + font-weight: 700; | ||
| 371 | + color: #1a202c; | ||
| 372 | + font-size: 32rpx; | ||
| 373 | + margin-right: 12rpx; | ||
| 374 | + flex-shrink: 0; | ||
| 375 | + text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.1); | ||
| 376 | + overflow: hidden; | ||
| 377 | + text-overflow: ellipsis; | ||
| 378 | + white-space: nowrap; | ||
| 379 | +} | ||
| 380 | + | ||
| 381 | +.family-intro { | ||
| 382 | + color: #4a5568; | ||
| 383 | + font-size: 26rpx; | ||
| 384 | + display: -webkit-box; | ||
| 385 | + overflow: hidden; | ||
| 386 | + text-overflow: ellipsis; | ||
| 387 | + -webkit-line-clamp: 2; | ||
| 388 | + -webkit-box-orient: vertical; | ||
| 389 | + line-height: 1.5; | ||
| 390 | + font-weight: 500; | ||
| 391 | +} | ||
| 392 | + | ||
| 393 | +.page-indicator { | ||
| 394 | + position: absolute; | ||
| 395 | + bottom: 16rpx; | ||
| 396 | + left: 50%; | ||
| 397 | + transform: translateX(-50%); | ||
| 398 | + display: flex; | ||
| 399 | + gap: 12rpx; | ||
| 400 | + z-index: 10; | ||
| 401 | +} | ||
| 402 | + | ||
| 403 | +.indicator-dot { | ||
| 404 | + width: 12rpx; | ||
| 405 | + height: 12rpx; | ||
| 406 | + border-radius: 50%; | ||
| 407 | + background: rgba(255, 255, 255, 0.4); | ||
| 408 | + transition: all 0.3s ease; | ||
| 409 | +} | ||
| 410 | + | ||
| 411 | +.indicator-dot.active { | ||
| 412 | + background: rgba(255, 255, 255, 0.9); | ||
| 413 | + transform: scale(1.2); | ||
| 414 | +} | ||
| 415 | +</style> |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-09-01 13:07:52 | 2 | * @Date: 2025-09-01 13:07:52 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-10-29 10:18:35 | 4 | + * @LastEditTime: 2025-10-29 12:49:27 |
| 5 | * @FilePath: /lls_program/src/pages/FamilyRank/index.vue | 5 | * @FilePath: /lls_program/src/pages/FamilyRank/index.vue |
| 6 | * @Description: 文件描述 | 6 | * @Description: 文件描述 |
| 7 | --> | 7 | --> |
| 8 | <template> | 8 | <template> |
| 9 | - <view class="family-rank-page"> | 9 | + <view class="family-rank-page" :style="{ paddingBottom: activeTab === 'support' ? '0' : '34rpx' }"> |
| 10 | <!-- 顶部导航 --> | 10 | <!-- 顶部导航 --> |
| 11 | <view class="header"> | 11 | <view class="header"> |
| 12 | <view class="nav-tabs"> | 12 | <view class="nav-tabs"> |
| ... | @@ -40,9 +40,11 @@ | ... | @@ -40,9 +40,11 @@ |
| 40 | 40 | ||
| 41 | <!-- 排行榜日期 --> | 41 | <!-- 排行榜日期 --> |
| 42 | <view v-if="!loading" class="rank-date relative"> | 42 | <view v-if="!loading" class="rank-date relative"> |
| 43 | - <!-- <view class="flex items-center justify-center"><text class="mr-2">截止昨日</text>{{ currentDate }}<text v-if="activeTab !== 'shanghai'" class="ml-2">排名</text><IconFont name="ask" size="14" class="ml-2" @click="handleRankAskClick"></IconFont></view> --> | 43 | + <view v-if="activeTab !== 'support'" class="flex items-center justify-center"><text class="mr-2">截止昨日</text>{{ currentDate }}<text class="ml-2">排名</text><IconFont name="ask" size="14" class="ml-2" @click="handleRankAskClick"></IconFont></view> |
| 44 | - <view class="flex items-center justify-center"><text class="mr-2">截止昨日</text>{{ currentDate }}<text class="ml-2">排名</text><IconFont name="ask" size="14" class="ml-2" @click="handleRankAskClick"></IconFont></view> | 44 | + <!-- <view v-if="activeTab === 'support'" class="absolute font-bold text-white bg-orange-500 top-0 rounded-full px-4 py-1" style="right: 45rpx; top: -5rpx; font-size: 23rpx;" @tap="joinOrganization">助力码</view> --> |
| 45 | - <view v-if="activeTab === 'support'" class="absolute font-bold text-white bg-orange-500 top-0 rounded-full px-4 py-1" style="right: 45rpx; top: -5rpx; font-size: 23rpx;" @tap="joinOrganization">助力码</view> | 45 | + <view v-if="activeTab === 'support'" class="flex items-center justify-center"> |
| 46 | + <text class="font-bold text-white bg-orange-500 top-0 rounded-full px-4 py-1" style="font-size: 23rpx;" @tap="joinOrganization">助力码</text> | ||
| 47 | + </view> | ||
| 46 | </view> | 48 | </view> |
| 47 | 49 | ||
| 48 | <!-- 参与家庭数量显示 --> | 50 | <!-- 参与家庭数量显示 --> |
| ... | @@ -157,7 +159,7 @@ | ... | @@ -157,7 +159,7 @@ |
| 157 | </view> | 159 | </view> |
| 158 | 160 | ||
| 159 | <!-- 隐藏的弹幕组件,在页面加载时就开始滚动 --> | 161 | <!-- 隐藏的弹幕组件,在页面加载时就开始滚动 --> |
| 160 | - <view class="danmu-section mt-8" :style="{ visibility: activeTab === 'support' ? 'visible' : 'hidden', position: activeTab === 'support' ? 'static' : 'absolute', top: activeTab === 'support' ? 'auto' : '-9999px' }"> | 162 | + <!-- <view class="danmu-section mt-8" :style="{ visibility: activeTab === 'support' ? 'visible' : 'hidden', position: activeTab === 'support' ? 'static' : 'absolute', top: activeTab === 'support' ? 'auto' : '-9999px' }"> |
| 161 | <NativeDanmuComponent | 163 | <NativeDanmuComponent |
| 162 | :container-height="1200" | 164 | :container-height="1200" |
| 163 | :show-controls="true" | 165 | :show-controls="true" |
| ... | @@ -166,6 +168,15 @@ | ... | @@ -166,6 +168,15 @@ |
| 166 | @danmu-hover="handleDanmuHover" | 168 | @danmu-hover="handleDanmuHover" |
| 167 | ref="danmuRef" | 169 | ref="danmuRef" |
| 168 | /> | 170 | /> |
| 171 | + </view> --> | ||
| 172 | + | ||
| 173 | + <!-- 可滚动家庭简介列表组件 --> | ||
| 174 | + <view v-if="activeTab === 'support'" class="scrollable-family-section mt-1" style="padding: 0 24rpx;"> | ||
| 175 | + <ScrollableFamilyList | ||
| 176 | + :family-data="familyDanmus" | ||
| 177 | + height="1200rpx" | ||
| 178 | + :listen-external-scroll="true" | ||
| 179 | + /> | ||
| 169 | </view> | 180 | </view> |
| 170 | 181 | ||
| 171 | <!-- 我的排名悬浮卡片 --> | 182 | <!-- 我的排名悬浮卡片 --> |
| ... | @@ -220,7 +231,8 @@ import { ref, computed, onMounted } from 'vue' | ... | @@ -220,7 +231,8 @@ import { ref, computed, onMounted } from 'vue' |
| 220 | import Taro from '@tarojs/taro' | 231 | import Taro from '@tarojs/taro' |
| 221 | import { IconFont } from '@nutui/icons-vue-taro'; | 232 | import { IconFont } from '@nutui/icons-vue-taro'; |
| 222 | import BackToTop from '@/components/BackToTop.vue' | 233 | import BackToTop from '@/components/BackToTop.vue' |
| 223 | -import NativeDanmuComponent from '@/components/NativeDanmuComponent.vue' | 234 | +import ScrollableFamilyList from '@/components/ScrollableFamilyList.vue' |
| 235 | +// import NativeDanmuComponent from '@/components/NativeDanmuComponent.vue' | ||
| 224 | // import NumberRoll from '@/components/NumberRoll.vue' | 236 | // import NumberRoll from '@/components/NumberRoll.vue' |
| 225 | // 默认头像 | 237 | // 默认头像 |
| 226 | 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' | 238 | 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' |
| ... | @@ -274,6 +286,9 @@ const loading = ref(false) | ... | @@ -274,6 +286,9 @@ const loading = ref(false) |
| 274 | // 当前日期 | 286 | // 当前日期 |
| 275 | const currentDate = ref('') | 287 | const currentDate = ref('') |
| 276 | 288 | ||
| 289 | +// 家庭弹幕数据 | ||
| 290 | +const familyDanmus = ref([]) | ||
| 291 | + | ||
| 277 | /** | 292 | /** |
| 278 | * 处理排行榜须知点击事件 | 293 | * 处理排行榜须知点击事件 |
| 279 | */ | 294 | */ |
| ... | @@ -285,22 +300,22 @@ const handleRankAskClick = () => { | ... | @@ -285,22 +300,22 @@ const handleRankAskClick = () => { |
| 285 | /** | 300 | /** |
| 286 | * 弹幕组件相关 | 301 | * 弹幕组件相关 |
| 287 | */ | 302 | */ |
| 288 | -const danmuRef = ref(null) | 303 | +// const danmuRef = ref(null) |
| 289 | 304 | ||
| 290 | // 处理弹幕点击事件 | 305 | // 处理弹幕点击事件 |
| 291 | -const handleDanmuClick = (familyData) => { | 306 | +// const handleDanmuClick = (familyData) => { |
| 292 | - console.log('弹幕点击:', familyData) | 307 | +// console.log('弹幕点击:', familyData) |
| 293 | - // Taro.showToast({ | 308 | +// // Taro.showToast({ |
| 294 | - // title: `点击了${familyData.familyName}`, | 309 | +// // title: `点击了${familyData.familyName}`, |
| 295 | - // icon: 'none', | 310 | +// // icon: 'none', |
| 296 | - // duration: 2000 | 311 | +// // duration: 2000 |
| 297 | - // }) | 312 | +// // }) |
| 298 | -} | 313 | +// } |
| 299 | 314 | ||
| 300 | // 处理弹幕悬停事件 | 315 | // 处理弹幕悬停事件 |
| 301 | -const handleDanmuHover = (familyData) => { | 316 | +// const handleDanmuHover = (familyData) => { |
| 302 | - console.log('弹幕悬停:', familyData) | 317 | +// console.log('弹幕悬停:', familyData) |
| 303 | -} | 318 | +// } |
| 304 | 319 | ||
| 305 | /** | 320 | /** |
| 306 | * 触发数字滚动动画 | 321 | * 触发数字滚动动画 |
| ... | @@ -411,6 +426,15 @@ const loadLeaderboardData = async (isInitialLoad = false) => { | ... | @@ -411,6 +426,15 @@ const loadLeaderboardData = async (isInitialLoad = false) => { |
| 411 | currentDate.value = response.data.yesterday | 426 | currentDate.value = response.data.yesterday |
| 412 | // 设置总家庭数 | 427 | // 设置总家庭数 |
| 413 | currentTotalFamilySum.value = response.data.family_count || 0; | 428 | currentTotalFamilySum.value = response.data.family_count || 0; |
| 429 | + // 家庭弹幕数据处理 | ||
| 430 | + if (response.data?.bullet_families) { | ||
| 431 | + familyDanmus.value = response.data.bullet_families.map(family => ({ | ||
| 432 | + id: family.id, | ||
| 433 | + familyName: family.name, | ||
| 434 | + familyIntro: family.note, | ||
| 435 | + avatar: family.avatar_url || defaultAvatar | ||
| 436 | + })) | ||
| 437 | + } | ||
| 414 | } | 438 | } |
| 415 | } catch (error) { | 439 | } catch (error) { |
| 416 | console.error('获取排行榜数据失败:', error) | 440 | console.error('获取排行榜数据失败:', error) |
| ... | @@ -948,6 +972,14 @@ onMounted(async () => { | ... | @@ -948,6 +972,14 @@ onMounted(async () => { |
| 948 | } | 972 | } |
| 949 | } | 973 | } |
| 950 | 974 | ||
| 975 | + /* 可滚动家庭简介列表样式 */ | ||
| 976 | + .scrollable-family-section { | ||
| 977 | + // margin: 32rpx 24rpx; | ||
| 978 | + // border-radius: 20rpx; | ||
| 979 | + // overflow: hidden; | ||
| 980 | + // box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1); | ||
| 981 | + } | ||
| 982 | + | ||
| 951 | /* 加载状态 */ | 983 | /* 加载状态 */ |
| 952 | .loading-container { | 984 | .loading-container { |
| 953 | display: flex; | 985 | display: flex; | ... | ... |
-
Please register or login to post a comment