hookehuyr

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

- 重构排行榜组件,从API动态加载数据
- 添加排行榜日期显示功能
- 优化区域切换逻辑,支持动态区域显示
- 调整返回顶部按钮位置
- 添加默认头像处理逻辑
<!--
* @Date: 2025-09-02 13:06:39
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-02 13:12:20
* @LastEditTime: 2025-09-09 14:17:13
* @FilePath: /lls_program/src/components/BackToTop.vue
* @Description: 文件描述
-->
......@@ -56,7 +56,7 @@ const scrollToTop = () => {
.back-to-top {
position: fixed;
right: 30rpx;
bottom: 180rpx;
bottom: 250rpx;
width: 80rpx;
height: 80rpx;
background: rgba(64, 158, 255, 0.8);
......
<!--
* @Date: 2025-01-09 00:00:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-08 20:22:19
* @LastEditTime: 2025-09-09 14:18:28
* @FilePath: /lls_program/src/components/RankingCard.vue
* @Description: 排行榜卡片组件
-->
......@@ -21,84 +21,85 @@
:class="{ 'indicator-shanghai': activeTab === 'shanghai' }"
></view>
<view
v-for="region in availableRegions.slice(0, 2)"
:key="region.value"
class="tab-item"
:class="{ active: activeTab === 'huangpu' }"
@click="switchTab('huangpu')"
:class="{ active: activeTab === region.value }"
@click="switchTab(region.value)"
>
黄埔榜
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'shanghai' }"
@click="switchTab('shanghai')"
>
上海榜
{{ region.text === '上海市' ? '上海榜' : region.text.replace('区', '榜') }}
</view>
</view>
<!-- 排行榜内容 -->
<view class="rank-content" :class="{ 'content-switching': isContentSwitching }">
<!-- 排行榜日期 -->
<view class="rank-date">
{{ currentDate }}
</view>
<!-- 前三名展示 -->
<view class="top-three">
<!-- 第二名 -->
<view class="rank-item second">
<view v-if="topRanks[1]" class="rank-item second">
<view class="crown crown-silver">👑</view>
<view class="avatar">
<image :src="topRanks[1]?.avatar" class="avatar-img" mode="aspectFill" />
<image :src="topRanks[1]?.avatar_url || defaultAvatar" class="avatar-img" mode="aspectFill" />
</view>
<view class="family-name">{{ topRanks[1]?.familyName }}</view>
<view class="leader-name">大家长:{{ topRanks[1]?.leaderName }}</view>
<view class="family-name">{{ topRanks[1]?.name }}</view>
<view class="leader-name">大家长:{{ topRanks[1]?.created_by_nickname }}</view>
<view class="rank-number">
<view class="rank-num">2</view>
<view class="steps-in-rank">{{ formatSteps(topRanks[1]?.steps) }}</view>
<view class="steps-in-rank">{{ formatSteps(topRanks[1]?.step) }}</view>
</view>
</view>
<!-- 第一名 -->
<view class="rank-item first">
<view v-if="topRanks[0]" class="rank-item first">
<view class="crown crown-gold">👑</view>
<view class="avatar">
<image :src="topRanks[0]?.avatar" class="avatar-img" mode="aspectFill" />
<image :src="topRanks[0]?.avatar_url || defaultAvatar" class="avatar-img" mode="aspectFill" />
</view>
<view class="family-name">{{ topRanks[0]?.familyName }}</view>
<view class="leader-name">大家长:{{ topRanks[0]?.leaderName }}</view>
<view class="family-name">{{ topRanks[0]?.name }}</view>
<view class="leader-name">大家长:{{ topRanks[0]?.created_by_nickname }}</view>
<view class="rank-number">
<view class="rank-num">1</view>
<view class="steps-in-rank">{{ formatSteps(topRanks[0]?.steps) }}</view>
<view class="steps-in-rank">{{ formatSteps(topRanks[0]?.step) }}</view>
</view>
</view>
<!-- 第三名 -->
<view class="rank-item third">
<view v-if="topRanks[2]" class="rank-item third">
<view class="crown crown-bronze">👑</view>
<view class="avatar">
<image :src="topRanks[2]?.avatar" class="avatar-img" mode="aspectFill" />
<image :src="topRanks[2]?.avatar_url || defaultAvatar" class="avatar-img" mode="aspectFill" />
</view>
<view class="family-name">{{ topRanks[2]?.familyName }}</view>
<view class="leader-name">大家长:{{ topRanks[2]?.leaderName }}</view>
<view class="family-name">{{ topRanks[2]?.name }}</view>
<view class="leader-name">大家长:{{ topRanks[2]?.created_by_nickname }}</view>
<view class="rank-number">
<view class="rank-num">3</view>
<view class="steps-in-rank">{{ formatSteps(topRanks[2]?.steps) }}</view>
<view class="steps-in-rank">{{ formatSteps(topRanks[2]?.step) }}</view>
</view>
</view>
</view>
</view>
<!-- 我的排名卡片 -->
<view class="my-rank-section">
<view v-if="myRank" class="my-rank-section">
<view class="my-rank-content">
<view class="my-rank-left">
<view class="my-rank-number">{{ myRank.rank }}+</view>
<view class="my-rank-number">
{{ myRank.isNotRanked ? '未上榜' : (myRank.rank > 99 ? myRank.rank + '+' : myRank.rank) }}
</view>
<view class="my-avatar">
<image :src="myRank.avatar" class="my-avatar-img" mode="aspectFill" />
<image :src="myRank.avatar_url || defaultAvatar" class="my-avatar-img" mode="aspectFill" />
</view>
<view class="my-family-info">
<view class="my-family-name">{{ myRank.familyName }}</view>
<view class="my-leader-name">大家长:{{ myRank.leaderName }}</view>
<view class="my-family-name">{{ myRank.family_name }}</view>
<view class="my-leader-name">大家长:{{ myRank.created_by_nickname }}</view>
</view>
</view>
<view class="my-rank-right">
<view class="my-steps">{{ formatStepsForList(myRank.steps) }}</view>
<view class="my-steps">{{ formatStepsForList(myRank.step) }}</view>
<view class="rank-status">{{ myRank.status }}</view>
</view>
</view>
......@@ -107,7 +108,21 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed, onMounted } from 'vue'
// 默认头像
const defaultAvatar = 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
// 导入接口
import { getStepLeaderboardAPI } from '@/api/points'
// 区域信息
import { SHANGHAI_REGION as shanghaiRegion } from '@/utils/config'
const shanghaiRegionOptions = computed(() => {
return shanghaiRegion.map(item => ({
text: item.text,
value: String(item.value)
}))
})
// 定义props
const props = defineProps({
......@@ -118,25 +133,37 @@ const props = defineProps({
})
// 当前激活的tab
const activeTab = ref('huangpu')
const activeTab = ref('310101')
// 内容切换状态
const isContentSwitching = ref(false)
// 排行榜数据
const leaderboardData = ref(null)
// 加载状态
const loading = ref(false)
// 排行榜日期
const currentDate = ref('')
/**
* 切换tab
* @param {string} tab - tab名称
*/
const switchTab = (tab) => {
const switchTab = async (tab) => {
if (activeTab.value === tab) return
// 开始切换动画
isContentSwitching.value = true
// 延迟切换内容,让淡出动画先执行
setTimeout(() => {
setTimeout(async () => {
activeTab.value = tab
// 重新加载排行榜数据
await loadLeaderboardData(false)
// 内容切换后,结束切换状态,开始淡入动画
setTimeout(() => {
isContentSwitching.value = false
......@@ -145,6 +172,79 @@ const switchTab = (tab) => {
}
/**
* 加载排行榜数据
* @param {boolean} isInitialLoad - 是否为初始加载,避免无限递归
*/
const loadLeaderboardData = async (isInitialLoad = false) => {
try {
loading.value = true
const params = {}
// 添加current_country参数:1=是,0=否,默认为否
// 根据activeTab动态设置:上海榜时为0,区域榜时为1
params.current_country = activeTab.value === 'shanghai' ? '0' : '1'
const response = await getStepLeaderboardAPI(params)
if (response.code) {
leaderboardData.value = response.data
// 设置当前日期
currentDate.value = response.data.yesterday
// 只在初始加载时从current_family.county获取区县信息,设置默认tab
if (isInitialLoad && response.data.current_family) {
const currentFamilyCounty = response.data.current_family.county;
if (currentFamilyCounty && String(currentFamilyCounty) !== activeTab.value) {
// 只在county与当前activeTab不同时才设置,确保county字段为字符串格式
activeTab.value = String(currentFamilyCounty)
// 设置activeTab后需要重新加载数据以获取正确的区县排行榜
await loadLeaderboardData(false)
return
}
}
}
} catch (error) {
console.error('获取排行榜数据失败:', error)
} finally {
loading.value = false
}
}
/**
* 计算当前区域的中文名称
*/
const currentRegionName = computed(() => {
if (activeTab.value === 'shanghai') {
return '上海市'
}
const region = shanghaiRegionOptions.value.find(item => item.value === activeTab.value)
return region ? region.text : '黄浦区'
})
/**
* 计算可用的区域选项
*/
const availableRegions = computed(() => {
// 从current_family.county获取区县信息,优先显示用户区域,然后是上海市
const currentFamilyCounty = leaderboardData.value?.current_family?.county
if (currentFamilyCounty) {
// 确保county字段为字符串格式进行比较
const userCounty = String(currentFamilyCounty)
// value值需要转成字符串进行比较
const userRegion = shanghaiRegionOptions.value.find(item => item.value === userCounty)
if (userRegion) {
// 用户区域在第一位,上海市在第二位
return [userRegion, { text: '上海市', value: 'shanghai' }]
}
}
// 默认显示黄浦区和上海市
return [
{ text: '黄浦区', value: '310101' },
{ text: '上海市', value: 'shanghai' }
]
})
/**
* 格式化步数显示
* @param {number} steps - 步数
* @returns {string} 格式化后的步数
......@@ -171,39 +271,47 @@ const handleViewMore = () => {
}
}
// 前三名数据
const topRanks = ref([
{
rank: 1,
familyName: '明媚的晴',
leaderName: '张明',
avatar: 'https://cdn.ipadbiz.cn/hager/0513-1_FsxMk28AGz6N_D1zZFFOl_EaRdss.png',
steps: 45670
},
{
rank: 2,
familyName: '甜心小桃',
leaderName: '李桃',
avatar: 'https://cdn.ipadbiz.cn/hager/0513-1_FsxMk28AGz6N_D1zZFFOl_EaRdss.png',
steps: 42350
},
{
rank: 3,
familyName: '真心找爱',
leaderName: '王真',
avatar: 'https://cdn.ipadbiz.cn/hager/0513-1_FsxMk28AGz6N_D1zZFFOl_EaRdss.png',
steps: 38920
// 计算前三名数据
const topRanks = computed(() => {
if (!leaderboardData.value || !leaderboardData.value.families) {
return []
}
])
// 我的排名信息
const myRank = ref({
rank: 99,
familyName: '和谐之家',
leaderName: '陈家明',
avatar: 'https://cdn.ipadbiz.cn/hager/0513-1_FsxMk28AGz6N_D1zZFFOl_EaRdss.png',
steps: 8920,
status: '未上榜'
return leaderboardData.value.families.slice(0, 3)
})
// 计算我的排名信息
const myRank = computed(() => {
if (!leaderboardData.value || !leaderboardData.value.current_family) {
return null
}
const currentFamily = leaderboardData.value.current_family
// 如果没有排名信息,返回未上榜状态
if (!currentFamily.rank || currentFamily.rank === 0) {
return {
...currentFamily,
rank: 0,
status: '未上榜',
isNotRanked: true
}
}
return {
...currentFamily,
status: currentFamily.rank > 100 ? '未上榜' : '已上榜',
isNotRanked: false
}
})
/**
* 页面初始化
*/
onMounted(async () => {
// 直接加载排行榜数据,使用current_country参数获取当前家庭所在区县信息
await loadLeaderboardData(true)
})
</script>
......@@ -305,6 +413,14 @@ const myRank = ref({
opacity: 0.3;
transform: translateY(-20rpx);
}
.rank-date {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
text-align: center;
margin-top: 40rpx;
margin-bottom: 40rpx;
}
}
.top-three {
......
<!--
* @Date: 2025-09-01 13:07:52
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-09 14:07:23
* @LastEditTime: 2025-09-09 14:16:08
* @FilePath: /lls_program/src/pages/FamilyRank/index.vue
* @Description: 文件描述
-->
......@@ -34,6 +34,11 @@
<view class="loading-text">加载中...</view>
</view>
<!-- 排行榜日期 -->
<view class="rank-date">
{{ currentDate }}
</view>
<!-- 前三名展示 -->
<view v-else-if="topThreeData.length > 0" class="top-three">
<!-- 第二名 -->
......@@ -131,7 +136,7 @@
</view>
<!-- 返回顶部组件 -->
<!-- <BackToTop :distance="200" /> -->
<BackToTop :distance="200" />
</view>
</template>
......@@ -165,7 +170,8 @@ const leaderboardData = ref(null)
// 加载状态
const loading = ref(false)
// 当前日期
const currentDate = ref('')
/**
* 切换tab
......@@ -230,6 +236,7 @@ const loadLeaderboardData = async (isInitialLoad = false) => {
const response = await getStepLeaderboardAPI(params)
if (response.code) {
leaderboardData.value = response.data
currentDate.value = response.data.yesterday
// 只在初始加载时从current_family.county获取区县信息,设置默认tab
if (isInitialLoad && response.data.current_family) {
......@@ -411,6 +418,14 @@ onMounted(async () => {
opacity: 0.3;
transform: translateY(-20rpx);
}
.rank-date {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
text-align: center;
margin-top: 40rpx;
margin-bottom: 40rpx;
}
}
.top-three {
......