b52db87ddf
- 领奖台数据不足 3 名时显示虚拟空位(虚线轮廓+虚位以待) - 领奖台勋章与昵称对齐改为等高盒结构性修复(移除 margin 盲推) - 全 app 成就勋章图标尺寸统一为 30rpx(前三名/列表/设置页/记录页一致) - 勋章 SVG 按包围盒精确居中(utils/icons.js) - 设置页/记录页勋章展示配套调整
177 lines
10 KiB
Plaintext
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>
|