hookehuyr

feat(排行榜): 实现动态数据加载并添加日期显示

- 重构排行榜组件,从API动态加载数据
- 添加排行榜日期显示功能
- 优化区域切换逻辑,支持动态区域显示
- 调整返回顶部按钮位置
- 添加默认头像处理逻辑
1 <!-- 1 <!--
2 * @Date: 2025-09-02 13:06:39 2 * @Date: 2025-09-02 13:06:39
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-09-02 13:12:20 4 + * @LastEditTime: 2025-09-09 14:17:13
5 * @FilePath: /lls_program/src/components/BackToTop.vue 5 * @FilePath: /lls_program/src/components/BackToTop.vue
6 * @Description: 文件描述 6 * @Description: 文件描述
7 --> 7 -->
...@@ -56,7 +56,7 @@ const scrollToTop = () => { ...@@ -56,7 +56,7 @@ const scrollToTop = () => {
56 .back-to-top { 56 .back-to-top {
57 position: fixed; 57 position: fixed;
58 right: 30rpx; 58 right: 30rpx;
59 - bottom: 180rpx; 59 + bottom: 250rpx;
60 width: 80rpx; 60 width: 80rpx;
61 height: 80rpx; 61 height: 80rpx;
62 background: rgba(64, 158, 255, 0.8); 62 background: rgba(64, 158, 255, 0.8);
......
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-09-08 20:22:19 4 + * @LastEditTime: 2025-09-09 14:18:28
5 * @FilePath: /lls_program/src/components/RankingCard.vue 5 * @FilePath: /lls_program/src/components/RankingCard.vue
6 * @Description: 排行榜卡片组件 6 * @Description: 排行榜卡片组件
7 --> 7 -->
...@@ -21,84 +21,85 @@ ...@@ -21,84 +21,85 @@
21 :class="{ 'indicator-shanghai': activeTab === 'shanghai' }" 21 :class="{ 'indicator-shanghai': activeTab === 'shanghai' }"
22 ></view> 22 ></view>
23 <view 23 <view
24 + v-for="region in availableRegions.slice(0, 2)"
25 + :key="region.value"
24 class="tab-item" 26 class="tab-item"
25 - :class="{ active: activeTab === 'huangpu' }" 27 + :class="{ active: activeTab === region.value }"
26 - @click="switchTab('huangpu')" 28 + @click="switchTab(region.value)"
27 > 29 >
28 - 黄埔榜 30 + {{ region.text === '上海市' ? '上海榜' : region.text.replace('区', '榜') }}
29 - </view>
30 - <view
31 - class="tab-item"
32 - :class="{ active: activeTab === 'shanghai' }"
33 - @click="switchTab('shanghai')"
34 - >
35 - 上海榜
36 </view> 31 </view>
37 </view> 32 </view>
38 33
39 <!-- 排行榜内容 --> 34 <!-- 排行榜内容 -->
40 <view class="rank-content" :class="{ 'content-switching': isContentSwitching }"> 35 <view class="rank-content" :class="{ 'content-switching': isContentSwitching }">
36 + <!-- 排行榜日期 -->
37 + <view class="rank-date">
38 + {{ currentDate }}
39 + </view>
41 <!-- 前三名展示 --> 40 <!-- 前三名展示 -->
42 <view class="top-three"> 41 <view class="top-three">
43 <!-- 第二名 --> 42 <!-- 第二名 -->
44 - <view class="rank-item second"> 43 + <view v-if="topRanks[1]" class="rank-item second">
45 <view class="crown crown-silver">👑</view> 44 <view class="crown crown-silver">👑</view>
46 <view class="avatar"> 45 <view class="avatar">
47 - <image :src="topRanks[1]?.avatar" class="avatar-img" mode="aspectFill" /> 46 + <image :src="topRanks[1]?.avatar_url || defaultAvatar" class="avatar-img" mode="aspectFill" />
48 </view> 47 </view>
49 - <view class="family-name">{{ topRanks[1]?.familyName }}</view> 48 + <view class="family-name">{{ topRanks[1]?.name }}</view>
50 - <view class="leader-name">大家长:{{ topRanks[1]?.leaderName }}</view> 49 + <view class="leader-name">大家长:{{ topRanks[1]?.created_by_nickname }}</view>
51 <view class="rank-number"> 50 <view class="rank-number">
52 <view class="rank-num">2</view> 51 <view class="rank-num">2</view>
53 - <view class="steps-in-rank">{{ formatSteps(topRanks[1]?.steps) }}</view> 52 + <view class="steps-in-rank">{{ formatSteps(topRanks[1]?.step) }}</view>
54 </view> 53 </view>
55 </view> 54 </view>
56 55
57 <!-- 第一名 --> 56 <!-- 第一名 -->
58 - <view class="rank-item first"> 57 + <view v-if="topRanks[0]" class="rank-item first">
59 <view class="crown crown-gold">👑</view> 58 <view class="crown crown-gold">👑</view>
60 <view class="avatar"> 59 <view class="avatar">
61 - <image :src="topRanks[0]?.avatar" class="avatar-img" mode="aspectFill" /> 60 + <image :src="topRanks[0]?.avatar_url || defaultAvatar" class="avatar-img" mode="aspectFill" />
62 </view> 61 </view>
63 - <view class="family-name">{{ topRanks[0]?.familyName }}</view> 62 + <view class="family-name">{{ topRanks[0]?.name }}</view>
64 - <view class="leader-name">大家长:{{ topRanks[0]?.leaderName }}</view> 63 + <view class="leader-name">大家长:{{ topRanks[0]?.created_by_nickname }}</view>
65 <view class="rank-number"> 64 <view class="rank-number">
66 <view class="rank-num">1</view> 65 <view class="rank-num">1</view>
67 - <view class="steps-in-rank">{{ formatSteps(topRanks[0]?.steps) }}</view> 66 + <view class="steps-in-rank">{{ formatSteps(topRanks[0]?.step) }}</view>
68 </view> 67 </view>
69 </view> 68 </view>
70 69
71 <!-- 第三名 --> 70 <!-- 第三名 -->
72 - <view class="rank-item third"> 71 + <view v-if="topRanks[2]" class="rank-item third">
73 <view class="crown crown-bronze">👑</view> 72 <view class="crown crown-bronze">👑</view>
74 <view class="avatar"> 73 <view class="avatar">
75 - <image :src="topRanks[2]?.avatar" class="avatar-img" mode="aspectFill" /> 74 + <image :src="topRanks[2]?.avatar_url || defaultAvatar" class="avatar-img" mode="aspectFill" />
76 </view> 75 </view>
77 - <view class="family-name">{{ topRanks[2]?.familyName }}</view> 76 + <view class="family-name">{{ topRanks[2]?.name }}</view>
78 - <view class="leader-name">大家长:{{ topRanks[2]?.leaderName }}</view> 77 + <view class="leader-name">大家长:{{ topRanks[2]?.created_by_nickname }}</view>
79 <view class="rank-number"> 78 <view class="rank-number">
80 <view class="rank-num">3</view> 79 <view class="rank-num">3</view>
81 - <view class="steps-in-rank">{{ formatSteps(topRanks[2]?.steps) }}</view> 80 + <view class="steps-in-rank">{{ formatSteps(topRanks[2]?.step) }}</view>
82 </view> 81 </view>
83 </view> 82 </view>
84 </view> 83 </view>
85 </view> 84 </view>
86 85
87 <!-- 我的排名卡片 --> 86 <!-- 我的排名卡片 -->
88 - <view class="my-rank-section"> 87 + <view v-if="myRank" class="my-rank-section">
89 <view class="my-rank-content"> 88 <view class="my-rank-content">
90 <view class="my-rank-left"> 89 <view class="my-rank-left">
91 - <view class="my-rank-number">{{ myRank.rank }}+</view> 90 + <view class="my-rank-number">
91 + {{ myRank.isNotRanked ? '未上榜' : (myRank.rank > 99 ? myRank.rank + '+' : myRank.rank) }}
92 + </view>
92 <view class="my-avatar"> 93 <view class="my-avatar">
93 - <image :src="myRank.avatar" class="my-avatar-img" mode="aspectFill" /> 94 + <image :src="myRank.avatar_url || defaultAvatar" class="my-avatar-img" mode="aspectFill" />
94 </view> 95 </view>
95 <view class="my-family-info"> 96 <view class="my-family-info">
96 - <view class="my-family-name">{{ myRank.familyName }}</view> 97 + <view class="my-family-name">{{ myRank.family_name }}</view>
97 - <view class="my-leader-name">大家长:{{ myRank.leaderName }}</view> 98 + <view class="my-leader-name">大家长:{{ myRank.created_by_nickname }}</view>
98 </view> 99 </view>
99 </view> 100 </view>
100 <view class="my-rank-right"> 101 <view class="my-rank-right">
101 - <view class="my-steps">{{ formatStepsForList(myRank.steps) }}</view> 102 + <view class="my-steps">{{ formatStepsForList(myRank.step) }}</view>
102 <view class="rank-status">{{ myRank.status }}</view> 103 <view class="rank-status">{{ myRank.status }}</view>
103 </view> 104 </view>
104 </view> 105 </view>
...@@ -107,7 +108,21 @@ ...@@ -107,7 +108,21 @@
107 </template> 108 </template>
108 109
109 <script setup> 110 <script setup>
110 -import { ref } from 'vue' 111 +import { ref, computed, onMounted } from 'vue'
112 +// 默认头像
113 +const defaultAvatar = 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
114 +// 导入接口
115 +import { getStepLeaderboardAPI } from '@/api/points'
116 +
117 +// 区域信息
118 +import { SHANGHAI_REGION as shanghaiRegion } from '@/utils/config'
119 +
120 +const shanghaiRegionOptions = computed(() => {
121 + return shanghaiRegion.map(item => ({
122 + text: item.text,
123 + value: String(item.value)
124 + }))
125 +})
111 126
112 // 定义props 127 // 定义props
113 const props = defineProps({ 128 const props = defineProps({
...@@ -118,25 +133,37 @@ const props = defineProps({ ...@@ -118,25 +133,37 @@ const props = defineProps({
118 }) 133 })
119 134
120 // 当前激活的tab 135 // 当前激活的tab
121 -const activeTab = ref('huangpu') 136 +const activeTab = ref('310101')
122 137
123 // 内容切换状态 138 // 内容切换状态
124 const isContentSwitching = ref(false) 139 const isContentSwitching = ref(false)
125 140
141 +// 排行榜数据
142 +const leaderboardData = ref(null)
143 +
144 +// 加载状态
145 +const loading = ref(false)
146 +
147 +// 排行榜日期
148 +const currentDate = ref('')
149 +
126 /** 150 /**
127 * 切换tab 151 * 切换tab
128 * @param {string} tab - tab名称 152 * @param {string} tab - tab名称
129 */ 153 */
130 -const switchTab = (tab) => { 154 +const switchTab = async (tab) => {
131 if (activeTab.value === tab) return 155 if (activeTab.value === tab) return
132 156
133 // 开始切换动画 157 // 开始切换动画
134 isContentSwitching.value = true 158 isContentSwitching.value = true
135 159
136 // 延迟切换内容,让淡出动画先执行 160 // 延迟切换内容,让淡出动画先执行
137 - setTimeout(() => { 161 + setTimeout(async () => {
138 activeTab.value = tab 162 activeTab.value = tab
139 163
164 + // 重新加载排行榜数据
165 + await loadLeaderboardData(false)
166 +
140 // 内容切换后,结束切换状态,开始淡入动画 167 // 内容切换后,结束切换状态,开始淡入动画
141 setTimeout(() => { 168 setTimeout(() => {
142 isContentSwitching.value = false 169 isContentSwitching.value = false
...@@ -145,6 +172,79 @@ const switchTab = (tab) => { ...@@ -145,6 +172,79 @@ const switchTab = (tab) => {
145 } 172 }
146 173
147 /** 174 /**
175 + * 加载排行榜数据
176 + * @param {boolean} isInitialLoad - 是否为初始加载,避免无限递归
177 + */
178 +const loadLeaderboardData = async (isInitialLoad = false) => {
179 + try {
180 + loading.value = true
181 + const params = {}
182 +
183 + // 添加current_country参数:1=是,0=否,默认为否
184 + // 根据activeTab动态设置:上海榜时为0,区域榜时为1
185 + params.current_country = activeTab.value === 'shanghai' ? '0' : '1'
186 +
187 + const response = await getStepLeaderboardAPI(params)
188 + if (response.code) {
189 + leaderboardData.value = response.data
190 + // 设置当前日期
191 + currentDate.value = response.data.yesterday
192 +
193 + // 只在初始加载时从current_family.county获取区县信息,设置默认tab
194 + if (isInitialLoad && response.data.current_family) {
195 + const currentFamilyCounty = response.data.current_family.county;
196 + if (currentFamilyCounty && String(currentFamilyCounty) !== activeTab.value) {
197 + // 只在county与当前activeTab不同时才设置,确保county字段为字符串格式
198 + activeTab.value = String(currentFamilyCounty)
199 + // 设置activeTab后需要重新加载数据以获取正确的区县排行榜
200 + await loadLeaderboardData(false)
201 + return
202 + }
203 + }
204 + }
205 + } catch (error) {
206 + console.error('获取排行榜数据失败:', error)
207 + } finally {
208 + loading.value = false
209 + }
210 +}
211 +
212 +/**
213 + * 计算当前区域的中文名称
214 + */
215 +const currentRegionName = computed(() => {
216 + if (activeTab.value === 'shanghai') {
217 + return '上海市'
218 + }
219 + const region = shanghaiRegionOptions.value.find(item => item.value === activeTab.value)
220 + return region ? region.text : '黄浦区'
221 +})
222 +
223 +/**
224 + * 计算可用的区域选项
225 + */
226 +const availableRegions = computed(() => {
227 + // 从current_family.county获取区县信息,优先显示用户区域,然后是上海市
228 + const currentFamilyCounty = leaderboardData.value?.current_family?.county
229 + if (currentFamilyCounty) {
230 + // 确保county字段为字符串格式进行比较
231 + const userCounty = String(currentFamilyCounty)
232 + // value值需要转成字符串进行比较
233 + const userRegion = shanghaiRegionOptions.value.find(item => item.value === userCounty)
234 + if (userRegion) {
235 + // 用户区域在第一位,上海市在第二位
236 + return [userRegion, { text: '上海市', value: 'shanghai' }]
237 + }
238 + }
239 +
240 + // 默认显示黄浦区和上海市
241 + return [
242 + { text: '黄浦区', value: '310101' },
243 + { text: '上海市', value: 'shanghai' }
244 + ]
245 +})
246 +
247 +/**
148 * 格式化步数显示 248 * 格式化步数显示
149 * @param {number} steps - 步数 249 * @param {number} steps - 步数
150 * @returns {string} 格式化后的步数 250 * @returns {string} 格式化后的步数
...@@ -171,39 +271,47 @@ const handleViewMore = () => { ...@@ -171,39 +271,47 @@ const handleViewMore = () => {
171 } 271 }
172 } 272 }
173 273
174 -// 前三名数据 274 +
175 -const topRanks = ref([ 275 +
176 - { 276 +// 计算前三名数据
177 - rank: 1, 277 +const topRanks = computed(() => {
178 - familyName: '明媚的晴', 278 + if (!leaderboardData.value || !leaderboardData.value.families) {
179 - leaderName: '张明', 279 + return []
180 - avatar: 'https://cdn.ipadbiz.cn/hager/0513-1_FsxMk28AGz6N_D1zZFFOl_EaRdss.png', 280 + }
181 - steps: 45670 281 + return leaderboardData.value.families.slice(0, 3)
182 - }, 282 +})
183 - { 283 +
184 - rank: 2, 284 +// 计算我的排名信息
185 - familyName: '甜心小桃', 285 +const myRank = computed(() => {
186 - leaderName: '李桃', 286 + if (!leaderboardData.value || !leaderboardData.value.current_family) {
187 - avatar: 'https://cdn.ipadbiz.cn/hager/0513-1_FsxMk28AGz6N_D1zZFFOl_EaRdss.png', 287 + return null
188 - steps: 42350 288 + }
189 - }, 289 +
190 - { 290 + const currentFamily = leaderboardData.value.current_family
191 - rank: 3, 291 +
192 - familyName: '真心找爱', 292 + // 如果没有排名信息,返回未上榜状态
193 - leaderName: '王真', 293 + if (!currentFamily.rank || currentFamily.rank === 0) {
194 - avatar: 'https://cdn.ipadbiz.cn/hager/0513-1_FsxMk28AGz6N_D1zZFFOl_EaRdss.png', 294 + return {
195 - steps: 38920 295 + ...currentFamily,
196 - } 296 + rank: 0,
197 -]) 297 + status: '未上榜',
198 - 298 + isNotRanked: true
199 -// 我的排名信息 299 + }
200 -const myRank = ref({ 300 + }
201 - rank: 99, 301 +
202 - familyName: '和谐之家', 302 + return {
203 - leaderName: '陈家明', 303 + ...currentFamily,
204 - avatar: 'https://cdn.ipadbiz.cn/hager/0513-1_FsxMk28AGz6N_D1zZFFOl_EaRdss.png', 304 + status: currentFamily.rank > 100 ? '未上榜' : '已上榜',
205 - steps: 8920, 305 + isNotRanked: false
206 - status: '未上榜' 306 + }
307 +})
308 +
309 +/**
310 + * 页面初始化
311 + */
312 +onMounted(async () => {
313 + // 直接加载排行榜数据,使用current_country参数获取当前家庭所在区县信息
314 + await loadLeaderboardData(true)
207 }) 315 })
208 </script> 316 </script>
209 317
...@@ -305,6 +413,14 @@ const myRank = ref({ ...@@ -305,6 +413,14 @@ const myRank = ref({
305 opacity: 0.3; 413 opacity: 0.3;
306 transform: translateY(-20rpx); 414 transform: translateY(-20rpx);
307 } 415 }
416 +
417 + .rank-date {
418 + font-size: 28rpx;
419 + color: rgba(255, 255, 255, 0.8);
420 + text-align: center;
421 + margin-top: 40rpx;
422 + margin-bottom: 40rpx;
423 + }
308 } 424 }
309 425
310 .top-three { 426 .top-three {
......
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-09-09 14:07:23 4 + * @LastEditTime: 2025-09-09 14:16:08
5 * @FilePath: /lls_program/src/pages/FamilyRank/index.vue 5 * @FilePath: /lls_program/src/pages/FamilyRank/index.vue
6 * @Description: 文件描述 6 * @Description: 文件描述
7 --> 7 -->
...@@ -34,6 +34,11 @@ ...@@ -34,6 +34,11 @@
34 <view class="loading-text">加载中...</view> 34 <view class="loading-text">加载中...</view>
35 </view> 35 </view>
36 36
37 + <!-- 排行榜日期 -->
38 + <view class="rank-date">
39 + {{ currentDate }}
40 + </view>
41 +
37 <!-- 前三名展示 --> 42 <!-- 前三名展示 -->
38 <view v-else-if="topThreeData.length > 0" class="top-three"> 43 <view v-else-if="topThreeData.length > 0" class="top-three">
39 <!-- 第二名 --> 44 <!-- 第二名 -->
...@@ -131,7 +136,7 @@ ...@@ -131,7 +136,7 @@
131 </view> 136 </view>
132 137
133 <!-- 返回顶部组件 --> 138 <!-- 返回顶部组件 -->
134 - <!-- <BackToTop :distance="200" /> --> 139 + <BackToTop :distance="200" />
135 </view> 140 </view>
136 </template> 141 </template>
137 142
...@@ -165,7 +170,8 @@ const leaderboardData = ref(null) ...@@ -165,7 +170,8 @@ const leaderboardData = ref(null)
165 // 加载状态 170 // 加载状态
166 const loading = ref(false) 171 const loading = ref(false)
167 172
168 - 173 +// 当前日期
174 +const currentDate = ref('')
169 175
170 /** 176 /**
171 * 切换tab 177 * 切换tab
...@@ -230,6 +236,7 @@ const loadLeaderboardData = async (isInitialLoad = false) => { ...@@ -230,6 +236,7 @@ const loadLeaderboardData = async (isInitialLoad = false) => {
230 const response = await getStepLeaderboardAPI(params) 236 const response = await getStepLeaderboardAPI(params)
231 if (response.code) { 237 if (response.code) {
232 leaderboardData.value = response.data 238 leaderboardData.value = response.data
239 + currentDate.value = response.data.yesterday
233 240
234 // 只在初始加载时从current_family.county获取区县信息,设置默认tab 241 // 只在初始加载时从current_family.county获取区县信息,设置默认tab
235 if (isInitialLoad && response.data.current_family) { 242 if (isInitialLoad && response.data.current_family) {
...@@ -411,6 +418,14 @@ onMounted(async () => { ...@@ -411,6 +418,14 @@ onMounted(async () => {
411 opacity: 0.3; 418 opacity: 0.3;
412 transform: translateY(-20rpx); 419 transform: translateY(-20rpx);
413 } 420 }
421 +
422 + .rank-date {
423 + font-size: 28rpx;
424 + color: rgba(255, 255, 255, 0.8);
425 + text-align: center;
426 + margin-top: 40rpx;
427 + margin-bottom: 40rpx;
428 + }
414 } 429 }
415 430
416 .top-three { 431 .top-three {
......