Files
wx_pbzc/pages/leaderboard/leaderboard.wxml
T
lc b52db87ddf feat: 排行榜虚拟领奖台 + 勋章对齐与尺寸统一
- 领奖台数据不足 3 名时显示虚拟空位(虚线轮廓+虚位以待)
- 领奖台勋章与昵称对齐改为等高盒结构性修复(移除 margin 盲推)
- 全 app 成就勋章图标尺寸统一为 30rpx(前三名/列表/设置页/记录页一致)
- 勋章 SVG 按包围盒精确居中(utils/icons.js)
- 设置页/记录页勋章展示配套调整
2026-08-04 11:23:48 +08:00

177 lines
10 KiB
Plaintext

<view class="container page-enter" style="{{themeStyle}}">
<!-- Period tabs: pill-style segmented control, consistent with settings -->
<view class="period-seg-wrap">
<view class="period-seg">
<view
wx:for="{{periods}}"
wx:key="key"
class="period-seg-item {{activePeriod === item.key ? 'active' : ''}}"
data-period="{{item.key}}"
bindtap="switchPeriod"
>{{item.label}}</view>
</view>
</view>
<!-- 刷新中指示器(下拉刷新) -->
<view class="refresh-indicator" wx:if="{{pullRefreshing}}">
<view class="refresh-spinner"></view>
<text class="refresh-text">刷新中…</text>
</view>
<!-- Loading: 旋转加载图标 + 骨架屏 -->
<view class="leaderboard-skeleton" wx:if="{{loading}}">
<view class="lb-loading">
<view class="lb-loading__spinner"></view>
<text class="lb-loading__text">加载中…</text>
</view>
<ui-skeleton variant="list-row" count="{{5}}" />
</view>
<!-- Empty: 奖杯光环插画 -->
<view class="empty-state" wx:elif="{{empty}}">
<view class="empty-illus">
<view class="empty-halo"></view>
<image class="empty-trophy" src="{{icons.trophyFill}}" mode="aspectFit"></image>
<image class="empty-spark empty-spark--1" src="{{icons.sparkleFill}}" mode="aspectFit"></image>
<image class="empty-spark empty-spark--2" src="{{icons.sparkleFill}}" mode="aspectFit"></image>
<image class="empty-spark empty-spark--3" src="{{icons.sparkleFill}}" mode="aspectFit"></image>
</view>
<text class="empty-text">暂无排行数据</text>
<text class="empty-sub">快去训练,争当第一名!</text>
</view>
<block wx:else>
<!-- 耐力榜领奖台顶部文案:哲理注脚,点出"撑得越久不一定越好" -->
<view class="podium-slogan" wx:if="{{activePeriod === 'endurance'}}">{{podiumSlogan}}</view>
<!-- Podium: top 3 (shown whenever there is at least 1 entry; empty slots render as virtual placeholders) -->
<view class="podium" wx:if="{{rankedList.length >= 1}}">
<!-- 2nd -->
<view class="podium-slot podium-slot--second rank-item--enter" style="animation-delay: 60ms;">
<block wx:if="{{rankedList[1]}}">
<view class="podium-avatar-wrap">
<view class="avatar avatar--md {{rankedList[1].avatarUrl ? 'has-avatar' : ''}}"><image class="avatar__img" src="{{rankedList[1].avatarUrl || icons.peopleFill}}" mode="{{rankedList[1].avatarUrl ? 'aspectFill' : 'aspectFit'}}"></image></view>
<view class="podium-medal podium-medal--silver">2</view>
</view>
<view class="podium-name-row">
<text class="podium-name">{{rankedList[1].name}}</text>
<image wx:if="{{rankedList[1].badgeIcon}}" class="podium-badge" src="{{rankedList[1].badgeIcon}}" mode="aspectFit" data-name="{{rankedList[1].badgeName}}" data-days="{{rankedList[1].badgeDays}}" bindtap="onBadgeTap"></image>
</view>
<text class="podium-dur">{{rankedList[1].durationText}}</text>
<view class="podium-base podium-base--second"><view class="podium-base__label {{activePeriod === 'endurance' ? 'is-two' : ''}}"><block wx:if="{{activePeriod === 'endurance'}}"><text class="podium-base__date">{{rankedList[1].subDate}}</text><text class="podium-base__time">{{rankedList[1].subTime}}</text></block><text wx:else>{{rankedList[1].subText}}</text></view></view>
</block>
<block wx:else>
<view class="podium-avatar-wrap">
<view class="avatar avatar--md podium-avatar--empty"><image class="avatar__img podium-avatar__img--empty" src="{{icons.peopleFill}}" mode="aspectFit"></image></view>
<view class="podium-medal podium-medal--silver">2</view>
</view>
<view class="podium-base podium-base--second"><view class="podium-base__label"><text class="podium-empty-label">虚位以待</text></view></view>
</block>
</view>
<!-- 1st -->
<view class="podium-slot podium-slot--first rank-item--enter" style="animation-delay: 0ms;">
<block wx:if="{{rankedList[0]}}">
<image class="podium-crown" src="{{icons.crownFill}}" mode="aspectFit"></image>
<view class="podium-avatar-wrap">
<view class="avatar avatar--md avatar--lg {{rankedList[0].avatarUrl ? 'has-avatar' : ''}}"><image class="avatar__img" src="{{rankedList[0].avatarUrl || icons.peopleFill}}" mode="{{rankedList[0].avatarUrl ? 'aspectFill' : 'aspectFit'}}"></image></view>
<view class="podium-medal podium-medal--gold">1</view>
</view>
<view class="podium-name-row">
<text class="podium-name podium-name--first">{{rankedList[0].name}}</text>
<image wx:if="{{rankedList[0].badgeIcon}}" class="podium-badge podium-badge--first" src="{{rankedList[0].badgeIcon}}" mode="aspectFit" data-name="{{rankedList[0].badgeName}}" data-days="{{rankedList[0].badgeDays}}" bindtap="onBadgeTap"></image>
</view>
<text class="podium-dur podium-dur--first">{{rankedList[0].durationText}}</text>
<view class="podium-base podium-base--first"><view class="podium-base__label {{activePeriod === 'endurance' ? 'is-two' : ''}}"><block wx:if="{{activePeriod === 'endurance'}}"><text class="podium-base__date">{{rankedList[0].subDate}}</text><text class="podium-base__time">{{rankedList[0].subTime}}</text></block><text wx:else>{{rankedList[0].subText}}</text></view></view>
</block>
<block wx:else>
<image class="podium-crown podium-crown--empty" src="{{icons.crownFill}}" mode="aspectFit"></image>
<view class="podium-avatar-wrap">
<view class="avatar avatar--md avatar--lg podium-avatar--empty"><image class="avatar__img podium-avatar__img--empty" src="{{icons.peopleFill}}" mode="aspectFit"></image></view>
<view class="podium-medal podium-medal--gold">1</view>
</view>
<view class="podium-base podium-base--first"><view class="podium-base__label"><text class="podium-empty-label">虚位以待</text></view></view>
</block>
</view>
<!-- 3rd -->
<view class="podium-slot podium-slot--third rank-item--enter" style="animation-delay: 120ms;">
<block wx:if="{{rankedList[2]}}">
<view class="podium-avatar-wrap">
<view class="avatar avatar--md {{rankedList[2].avatarUrl ? 'has-avatar' : ''}}"><image class="avatar__img" src="{{rankedList[2].avatarUrl || icons.peopleFill}}" mode="{{rankedList[2].avatarUrl ? 'aspectFill' : 'aspectFit'}}"></image></view>
<view class="podium-medal podium-medal--bronze">3</view>
</view>
<view class="podium-name-row">
<text class="podium-name">{{rankedList[2].name}}</text>
<image wx:if="{{rankedList[2].badgeIcon}}" class="podium-badge" src="{{rankedList[2].badgeIcon}}" mode="aspectFit" data-name="{{rankedList[2].badgeName}}" data-days="{{rankedList[2].badgeDays}}" bindtap="onBadgeTap"></image>
</view>
<text class="podium-dur">{{rankedList[2].durationText}}</text>
<view class="podium-base podium-base--third"><view class="podium-base__label {{activePeriod === 'endurance' ? 'is-two' : ''}}"><block wx:if="{{activePeriod === 'endurance'}}"><text class="podium-base__date">{{rankedList[2].subDate}}</text><text class="podium-base__time">{{rankedList[2].subTime}}</text></block><text wx:else>{{rankedList[2].subText}}</text></view></view>
</block>
<block wx:else>
<view class="podium-avatar-wrap">
<view class="avatar avatar--md podium-avatar--empty"><image class="avatar__img podium-avatar__img--empty" src="{{icons.peopleFill}}" mode="aspectFit"></image></view>
<view class="podium-medal podium-medal--bronze">3</view>
</view>
<view class="podium-base podium-base--third"><view class="podium-base__label"><text class="podium-empty-label">虚位以待</text></view></view>
</block>
</view>
</view>
<!-- Rank list: top-3 shown on the podium are skipped when podium exists -->
<view class="rank-list">
<view
wx:for="{{rankedList}}"
wx:key="rank"
wx:if="{{index >= 3}}"
class="rank-item {{item.isMe ? 'is-me' : ''}} rank-item--enter"
style="animation-delay: {{index < 10 ? index * 60 : 0}}ms;"
>
<view class="rank-num {{item.rank === 1 ? 'gold' : item.rank === 2 ? 'silver' : item.rank === 3 ? 'bronze' : ''}}">
<text>{{item.rank}}</text>
</view>
<view class="avatar avatar--sm {{item.avatarUrl ? 'has-avatar' : ''}}"><image class="avatar__img" src="{{item.avatarUrl || icons.peopleFill}}" mode="{{item.avatarUrl ? 'aspectFill' : 'aspectFit'}}"></image></view>
<view class="rank-info">
<view class="rank-name-row">
<text class="rank-name">{{item.name}}</text>
<!-- 每个用户的勋章:云函数下发 totalDays,浅底行用主题色版。
无勋章(未达3天)或旧快照无 totalDays 时不渲染 -->
<image
wx:if="{{item.badgeIcon}}"
class="rank-badge"
src="{{item.badgeIcon}}"
mode="aspectFit"
data-name="{{item.badgeName}}"
data-days="{{item.badgeDays}}"
bindtap="onBadgeTap"
></image>
</view>
<text class="rank-sessions">{{item.subText}}</text>
</view>
<text class="rank-dur">{{item.durationText}}</text>
</view>
</view>
</block>
<!-- My entry pinned at bottom (if not already in list) -->
<view class="my-bar" wx:if="{{myEntry && myEntry.rank > rankedList.length}}">
<view class="rank-num my-bar__num">{{myEntry.rank}}</view>
<view class="avatar avatar--sm my-bar__avatar {{myEntry.avatarUrl ? 'has-avatar' : ''}}"><image class="avatar__img" src="{{myEntry.avatarUrl || icons.peopleFill}}" mode="{{myEntry.avatarUrl ? 'aspectFill' : 'aspectFit'}}"></image></view>
<view class="rank-info">
<view class="rank-name-row">
<text class="rank-name">我 ({{myEntry.name}})</text>
<!-- 我的勋章:主题渐变底用白版 -->
<image
wx:if="{{myEntry.badgeIconWhite}}"
class="rank-badge"
src="{{myEntry.badgeIconWhite}}"
mode="aspectFit"
data-name="{{myEntry.badgeName}}"
data-days="{{myEntry.badgeDays}}"
bindtap="onBadgeTap"
></image>
</view>
<text class="rank-sessions">{{myEntry.subText}}</text>
</view>
<text class="rank-dur">{{myEntry.durationText}}</text>
</view>
</view>