2026-04-21 12:53:34 +08:00

400 lines
8.6 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">
<view class="bg-decoration"></view>
<!-- 对战分说明 -->
<view class="score-tip glass-card">
<text class="score-tip-icon">💡</text>
<text class="score-tip-text">对战分 = 游戏内排名积分赢局 +1000 基础分得分/伤害/宝箱均有加成与平台充值积分无关</text>
</view>
<!-- 我的排名 -->
<view class="my-card glass-card" v-if="myRank">
<view class="my-left">
<text class="my-label">我的排名</text>
<text class="my-rank">{{ myRank.rank ? `#${myRank.rank}` : '未上榜' }}</text>
</view>
<view class="my-divider"></view>
<view class="my-stats">
<view class="stat-col">
<text class="stat-val">{{ myRank.wins || 0 }}</text>
<text class="stat-key">胜场</text>
</view>
<view class="stat-col">
<text class="stat-val">{{ myRank.matches_played || 0 }}</text>
<text class="stat-key">总场次</text>
</view>
<view class="stat-col">
<text class="stat-val">{{ formatWinRate(myRank.win_rate) }}</text>
<text class="stat-key">胜率</text>
</view>
</view>
<view class="my-divider"></view>
<view class="my-right">
<text class="my-pts">{{ myRank.total_rank_points || 0 }}</text>
<text class="my-pts-label">对战分</text>
</view>
</view>
<!-- 榜单 -->
<scroll-view scroll-y class="list-wrap" @scrolltolower="loadMore">
<view v-if="loading && list.length === 0" class="state-box">
<text class="state-icon"></text>
<text class="state-text">加载中...</text>
</view>
<view v-else-if="!loading && list.length === 0" class="state-box">
<text class="state-icon">🏆</text>
<text class="state-text">暂无数据快来挑战吧</text>
</view>
<view v-else class="list">
<view
v-for="item in list"
:key="item.user_id"
class="rank-row glass-card"
:class="{ 'is-me': item.user_id === myUserId }"
>
<view class="rank-badge">
<text v-if="item.rank === 1" class="medal">🥇</text>
<text v-else-if="item.rank === 2" class="medal">🥈</text>
<text v-else-if="item.rank === 3" class="medal">🥉</text>
<text v-else class="rank-no">{{ item.rank }}</text>
</view>
<image class="avatar" :src="item.avatar || fallback" mode="aspectFill" />
<view class="item-info">
<view class="name-row">
<text class="item-name">{{ item.nickname || '匿名玩家' }}</text>
<text v-if="item.user_id === myUserId" class="me-tag"></text>
</view>
<text class="item-sub">{{ item.wins }} · {{ item.matches_played }} · {{ formatWinRate(item.win_rate) }}胜率</text>
</view>
<view class="pts-box">
<text class="pts-val">{{ item.total_rank_points }}</text>
<text class="pts-unit"></text>
</view>
</view>
<view v-if="loadingMore" class="footer-tip"><text class="footer-txt">加载中...</text></view>
<view v-if="!hasMore && list.length > 0" class="footer-tip"><text class="footer-txt"> 已显示全部 </text></view>
</view>
</scroll-view>
</view>
</template>
<script>
import { authRequest } from '../../../utils/request.js'
export default {
data() {
return {
gameType: 'minesweeper',
list: [],
myRank: null,
myUserId: null,
total: 0,
page: 1,
pageSize: 20,
loading: false,
loadingMore: false,
hasMore: true,
fallback: 'https://via.placeholder.com/80/FF6B00/FFFFFF?text=U',
}
},
onLoad() {
const info = uni.getStorageSync('user_info') || {}
this.myUserId = info.id || info.user_id || null
this.fetchList(true)
},
methods: {
async fetchList(reset = false) {
if (reset) {
this.list = []
this.page = 1
this.hasMore = true
this.myRank = null
this.loading = true
} else {
if (!this.hasMore || this.loadingMore) return
this.loadingMore = true
}
try {
const res = await authRequest({
url: '/api/app/games/leaderboard',
method: 'GET',
data: { game_type: this.gameType, page: this.page, page_size: this.pageSize },
})
const items = res.list || []
this.list = reset ? items : [...this.list, ...items]
this.total = res.total || 0
this.myRank = res.me || null
this.hasMore = this.list.length < this.total
if (this.hasMore) this.page++
} catch {
uni.showToast({ title: '加载失败,请重试', icon: 'none' })
} finally {
this.loading = false
this.loadingMore = false
}
},
loadMore() { this.fetchList(false) },
formatWinRate(rate) {
if (rate == null) return '0%'
return `${(rate * 100).toFixed(1)}%`
},
},
}
</script>
<style lang="scss" scoped>
@import '@/uni.scss';
.page {
min-height: 100vh;
background: $bg-page;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
/* 对战分说明 */
.score-tip {
position: relative;
z-index: 5;
margin: 0 32rpx 20rpx;
padding: 20rpx 24rpx;
display: flex;
align-items: flex-start;
gap: 12rpx;
}
.score-tip-icon {
font-size: 28rpx;
flex-shrink: 0;
line-height: 1.6;
}
.score-tip-text {
font-size: 22rpx;
color: $text-sub;
line-height: 1.6;
}
/* 我的排名 */
.my-card {
position: relative;
z-index: 5;
margin: 0 32rpx 24rpx;
padding: 28rpx 24rpx;
display: flex;
align-items: center;
gap: 0;
}
.my-left {
display: flex;
flex-direction: column;
align-items: center;
min-width: 100rpx;
}
.my-label {
font-size: $font-xs;
color: $text-sub;
margin-bottom: 8rpx;
}
.my-rank {
font-size: 38rpx;
font-weight: 900;
color: $brand-primary;
}
.my-divider {
width: 1px;
height: 60rpx;
background: $border-color-light;
margin: 0 20rpx;
}
.my-stats {
flex: 1;
display: flex;
align-items: center;
justify-content: space-around;
}
.stat-col {
display: flex;
flex-direction: column;
align-items: center;
gap: 4rpx;
}
.stat-val {
font-size: 30rpx;
font-weight: 800;
color: $text-main;
}
.stat-key {
font-size: $font-xs;
color: $text-sub;
}
.my-right {
display: flex;
flex-direction: column;
align-items: center;
min-width: 80rpx;
}
.my-pts {
font-size: 38rpx;
font-weight: 900;
color: $brand-primary;
line-height: 1;
}
.my-pts-label {
font-size: $font-xs;
color: $text-sub;
margin-top: 6rpx;
}
/* 列表 */
.list-wrap {
flex: 1;
height: 0;
padding: 0 32rpx;
}
.state-box {
display: flex;
flex-direction: column;
align-items: center;
padding: 120rpx 0;
gap: 20rpx;
}
.state-icon { font-size: 80rpx; }
.state-text { font-size: $font-md; color: $text-sub; }
.list {
display: flex;
flex-direction: column;
gap: 16rpx;
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
}
.rank-row {
display: flex;
align-items: center;
padding: 20rpx 24rpx;
gap: 20rpx;
&.is-me {
border: 2rpx solid rgba($brand-primary, 0.4);
background: linear-gradient(135deg, rgba($brand-primary, 0.06) 0%, $bg-glass 100%);
}
}
.rank-badge {
width: 52rpx;
height: 52rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.medal { font-size: 44rpx; }
.rank-no {
font-size: $font-md;
font-weight: 800;
color: $text-sub;
}
.avatar {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
background: $bg-secondary;
flex-shrink: 0;
border: 2rpx solid $border-color-light;
}
.item-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.name-row {
display: flex;
align-items: center;
gap: 12rpx;
}
.item-name {
font-size: $font-lg;
font-weight: 700;
color: $text-main;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 220rpx;
}
.me-tag {
font-size: $font-xs;
font-weight: 700;
color: #fff;
background: $gradient-brand;
padding: 2rpx 14rpx;
border-radius: $radius-round;
flex-shrink: 0;
}
.item-sub {
font-size: $font-sm;
color: $text-sub;
}
.pts-box {
display: flex;
align-items: baseline;
gap: 4rpx;
flex-shrink: 0;
}
.pts-val {
font-size: 36rpx;
font-weight: 900;
color: $brand-primary;
}
.pts-unit {
font-size: $font-sm;
color: $text-sub;
}
.footer-tip {
padding: 32rpx 0;
text-align: center;
}
.footer-txt {
font-size: $font-sm;
color: $text-tertiary;
}
</style>