2026-04-20 15:53:49 +08:00

425 lines
9.1 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>
<!-- Tab 切换 -->
<view class="tab-bar glass-card">
<view
class="tab-item"
:class="{ active: gameType === 'minesweeper' }"
@tap="switchTab('minesweeper')"
>
<text class="tab-text">付费模式</text>
</view>
<view
class="tab-item"
:class="{ active: gameType === 'minesweeper_free' }"
@tap="switchTab('minesweeper_free')"
>
<text class="tab-text">免费模式</text>
</view>
</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: {
switchTab(type) {
if (this.gameType === type) return
this.gameType = type
this.fetchList(true)
},
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;
}
/* Tab */
.tab-bar {
position: relative;
z-index: 10;
display: flex;
margin: 24rpx 32rpx 20rpx;
padding: 8rpx;
border-radius: $radius-round;
}
.tab-item {
flex: 1;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: $radius-round;
transition: all $transition-normal $ease-out;
&.active {
background: $gradient-brand;
box-shadow: $shadow-warm;
.tab-text { color: #fff; font-weight: 700; }
}
}
.tab-text {
font-size: $font-md;
color: $text-sub;
font-weight: 600;
}
/* 我的排名 */
.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;
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>