Files
hmc_gbhd/frontend/src/pages/activity/rank/index.vue
T
2025-05-06 19:40:35 +08:00

536 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page-container">
<t-dropdown-menu class="top-dropdown">
<t-dropdown-item :options="actOptions" :value="activityId"/>
</t-dropdown-menu>
<view class="rank-header">
<view class="title-container">
<view class="title">活动排行榜</view>
<view class="title-underline"></view>
</view>
<view class="trophy-icon">🏆</view>
</view>
<view class="rank-table-header">
<view class="rank-header-item rank-position">名次</view>
<view class="rank-header-item rank-team">队伍名称</view>
<view class="rank-header-item rank-score">得分</view>
</view>
<scroll-view scroll-y refresher-enabled refresher-background="#f5f5f5" :refresher-triggered="refresherTriggered"
@refresherrefresh="handleRefresh"
class="rank-scroll-view">
<view class="rank-container">
<view
v-for="(team, index) in rankList"
:key="team.id"
:class="['rank-item', team.isMyTeam ? 'my-team' : '']"
:style="{animationDelay: `${index * 0.1}s`}"
>
<!-- 名次 -->
<view class="rank-position">
<text v-if="index === 0" class="medal-icon">🥇</text>
<text v-else-if="index === 1" class="medal-icon">🥈</text>
<text v-else-if="index === 2" class="medal-icon">🥉</text>
<view v-else :class="['rank-number', team.isMyTeam ? 'my-rank-number' : '']">
{{ index + 1 }}
</view>
</view>
<!-- 队伍信息 -->
<view class="rank-team">
<view class="team-info">
<!-- <image class="team-avatar" :src="team.avatar || '/static/team-avatar.png'" mode="aspectFill"></image>-->
<view class="team-detail">
<view class="team-name-row">
<text class="team-name">{{ team.groupName }}</text>
<t-tag v-if="team.isMyTeam" theme="primary" variant="light" size="small" class="my-team-tag">我的队伍</t-tag>
</view>
<view class="progress-bar">
<view class="progress-bar-bg"></view>
<view
class="progress-bar-fill"
:style="{width: `${(team.totalScore / 100) * 100}%`}"
></view>
</view>
</view>
</view>
</view>
<!-- 得分 -->
<view class="rank-score">
<view class="score-value">{{ team.totalScore }}</view>
</view>
</view>
<view class="empty-container" v-if="!loading && rankList.length === 0">
<view class="empty-illustration">
<t-icon name="time" size="72rpx" class="empty-icon" />
</view>
<view class="empty-text">成绩暂未公布</view>
<view class="empty-subtext">排行榜将在赛事结束后更新,请稍后查看</view>
</view>
</view>
</scroll-view>
<view class="refresh-btn" @click="handleRefresh">
<t-icon name="refresh" size="40rpx" />
</view>
</view>
</template>
<script setup>
import {ref, onMounted, computed} from 'vue';
import { Toast } from 'tdesign-mobile-vue';
import { useRequest } from "@/utils/request";
import {onLoad} from "@dcloudio/uni-app";
// 排行榜数据
const actList = ref([])
const rankList = ref([]);
const activityId = ref(null);
const loading = ref(false);
const refresherTriggered = ref(false);
const actOptions = computed(() => {
return actList.value.map(item => {
return {
value: item.id,
label: item.title
}
})
})
// 获取排行榜数据
const getRankData = async () => {
loading.value = true;
try {
const {code,data, msg} = await useRequest().post('/client/group/list-group-rank/' + activityId.value);
if (code === 0) {
rankList.value = data;
} else {
Toast({message: msg, theme: 'error'});
}
} catch (error) {
console.error('获取排行榜数据失败', error);
Toast({ message: '获取排行榜数据失败', theme: 'error' });
} finally {
loading.value = false;
// 重置刷新状态
if (refresherTriggered.value) {
refresherTriggered.value = false;
}
}
};
async function listMineAct() {
try {
const {data, code, msg} = await useRequest().get('/client/activity/list-mine-act')
if (code === 0) {
actList.value = data
if (actList.value.length && activityId.value == null) {
activityId.value = actList.value[0].id
}
} else {
Toast.error(msg)
}
} catch (e) {
Toast.error(e.message)
}
}
// 刷新
const handleRefresh = () => {
if (loading.value) {
refresherTriggered.value = false;
return;
}
refresherTriggered.value = true;
getRankData();
};
onLoad((option) => {
if (option.id) {
activityId.value = Number(option.id)
}
});
// 页面加载时获取数据
onMounted(async () => {
await listMineAct()
await getRankData()
});
</script>
<style scoped lang="less">
.page-container{
position: relative;
}
.bg-circles {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
}
.bg-circle {
position: absolute;
border-radius: 50%;
background: linear-gradient(135deg, #f45a5a, #f78484);
opacity: 0.06;
}
.circle-1 {
width: 600rpx;
height: 600rpx;
top: -200rpx;
right: -200rpx;
animation: float1 18s infinite ease-in-out;
}
.circle-2 {
width: 500rpx;
height: 500rpx;
bottom: -150rpx;
left: -150rpx;
animation: float2 20s infinite ease-in-out;
}
.circle-3 {
width: 300rpx;
height: 300rpx;
top: 40%;
right: -100rpx;
opacity: 0.04;
animation: float3 15s infinite ease-in-out;
}
@keyframes float1 {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(30rpx, 30rpx); }
}
@keyframes float2 {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(50rpx, -20rpx); }
}
@keyframes float3 {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(-30rpx, 40rpx); }
}
.rank-header {
flex-shrink: 0;
background: linear-gradient(135deg, #f45a5a, #f78484);
height: 200rpx;
padding: 20rpx 32rpx;
color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
box-shadow: 0 4rpx 20rpx rgba(244, 90, 90, 0.3);
border-bottom-left-radius: 30rpx;
border-bottom-right-radius: 30rpx;
z-index: 1;
}
.title-container {
position: relative;
}
.title {
font-size: 40rpx;
font-weight: bold;
text-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.title-underline {
position: absolute;
bottom: -10rpx;
left: 0;
width: 60rpx;
height: 6rpx;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 6rpx;
}
.trophy-icon {
font-size: 70rpx;
filter: drop-shadow(0 3px 6px rgba(0, 0, 0, 0.2));
animation: bounce 2s infinite ease-in-out;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10rpx); }
}
.rank-scroll-view {
flex: 1;
position: relative;
z-index: 1;
overflow-y: auto;
padding: 20rpx 0;
}
.rank-container {
//padding: 20rpx 24rpx 120rpx;
padding: 20rpx 24rpx 0 24rpx;
}
.rank-table-header {
flex-shrink: 0;
display: flex;
align-items: center;
padding: 24rpx 16rpx;
font-weight: bold;
color: #666;
border-bottom: 1px solid rgba(244, 90, 90, 0.1);
background-color: rgba(255, 255, 255, 0.8);
margin: 16rpx;
border-radius: 16rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
position: sticky;
top: 0;
z-index: 10;
}
.rank-header-item, .rank-position, .rank-team, .rank-score {
padding: 0 10rpx;
}
.rank-position {
width: 15%;
display: flex;
justify-content: center;
align-items: center;
}
.rank-team {
width: 65%;
overflow: hidden;
}
.rank-score {
width: 20%;
display: flex;
justify-content: center;
align-items: center;
}
.rank-item {
display: flex;
align-items: center;
padding: 24rpx 16rpx;
margin-bottom: 16rpx;
background-color: #fff;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
position: relative;
transition: all 0.3s;
animation: slideIn 0.5s forwards;
opacity: 0;
transform: translateY(20rpx);
&:active {
transform: scale(0.98);
background-color: #fafafa;
}
}
@keyframes slideIn {
to {
opacity: 1;
transform: translateY(0);
}
}
.my-team {
background-color: rgba(244, 90, 90, 0.05);
border-left: 8rpx solid #f45a5a;
position: relative;
overflow: hidden;
}
.my-team::after {
content: '';
position: absolute;
top: -20rpx;
right: -20rpx;
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background-color: rgba(244, 90, 90, 0.1);
z-index: 0;
}
.medal-icon {
font-size: 46rpx;
}
.rank-number {
width: 46rpx;
height: 46rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-weight: bold;
background-color: #f4f4f4;
color: #666;
font-size: 24rpx;
}
.my-rank-number {
background-color: rgba(244, 90, 90, 0.1);
color: #f45a5a;
}
.team-info {
display: flex;
align-items: center;
width: 100%;
}
.team-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-right: 16rpx;
background-color: #f0f0f0;
border: 2px solid #fff;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.team-detail {
flex: 1;
overflow: hidden;
}
.team-name-row {
display: flex;
align-items: center;
flex-wrap: wrap;
margin-bottom: 10rpx;
}
.team-name {
font-weight: 500;
font-size: 28rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #333;
}
.my-team-tag {
margin-left: 10rpx;
background-color: rgba(244, 90, 90, 0.1);
color: #f45a5a;
border: 1px solid rgba(244, 90, 90, 0.2);
}
.progress-bar {
height: 6rpx;
width: 100%;
position: relative;
}
.progress-bar-bg {
position: absolute;
width: 100%;
height: 100%;
background-color: #f0f0f0;
border-radius: 3rpx;
}
.progress-bar-fill {
position: absolute;
height: 100%;
background: linear-gradient(to right, #f45a5a, #f78484);
border-radius: 3rpx;
transition: width 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}
.score-value {
font-weight: bold;
color: #f45a5a;
font-size: 32rpx;
}
.loading-container {
padding: 40rpx 0;
text-align: center;
}
.refresh-btn {
position: fixed;
right: 30rpx;
bottom: 140rpx;
width: 90rpx;
height: 90rpx;
border-radius: 50%;
background: linear-gradient(135deg, #f45a5a, #f78484);
box-shadow: 0 4rpx 20rpx rgba(244, 90, 90, 0.4);
display: flex;
align-items: center;
justify-content: center;
color: white;
z-index: 100;
}
@keyframes pulse {
0% {
transform: scale(1);
box-shadow: 0 4rpx 20rpx rgba(244, 90, 90, 0.4);
}
50% {
transform: scale(1.05);
box-shadow: 0 4rpx 25rpx rgba(244, 90, 90, 0.6);
}
100% {
transform: scale(1);
box-shadow: 0 4rpx 20rpx rgba(244, 90, 90, 0.4);
}
}
/* 空状态样式 */
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 40rpx;
text-align: center;
}
.empty-illustration {
margin-bottom: 30rpx;
}
.empty-icon {
color: rgba(244, 90, 90, 0.5);
}
.empty-text {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 16rpx;
}
.empty-subtext {
font-size: 28rpx;
color: #999;
max-width: 500rpx;
line-height: 1.5;
}
</style>