feat(ui): 记录页月份联动与多项 UI 美化/修复
- records: 底部列表与月份选择器联动,新增 [本月|最近] 分段控件;标题/空态随模式切换 - leaderboard: 领奖台布局(冠军居中+皇冠)、次数标注于底座且三档水平对齐;头像 cloud:// 批量预解析提速(云函数需重新部署) - index/timer: 主色底图标改白色变体修复隐形;目标数字按微信字体缩放补偿真机遮挡 - settings: 配色方案横滑渐变色卡;头像上传前压缩至 200px - theme: 新增 teal 主题;暗黑背景统一对齐 - 圆形头像双保险(image 自身 border-radius)修复月/年榜方图 注: cloudfunctions/leaderboard 改动须在开发者工具重新部署才生效。
This commit is contained in:
@@ -194,12 +194,37 @@ Page({
|
||||
*/
|
||||
async _uploadAvatar(filePath) {
|
||||
if (!cloud.enabled) throw new Error('cloud not enabled')
|
||||
// Shrink the chosen avatar (chooseAvatar returns a full-res PNG) before
|
||||
// upload — a 200px-wide file is tens of KB vs the original's MB, which
|
||||
// is what lets remote avatars load in well under a second on the
|
||||
// leaderboard. Falls back to the original file if compression is
|
||||
// unavailable (e.g. old base lib / devtools) so upload never breaks.
|
||||
const compressed = await this._compressAvatar(filePath)
|
||||
const cloudPath = `avatar/${Date.now()}-${Math.floor(Math.random() * 1e6)}.png`
|
||||
const res = await wx.cloud.uploadFile({ cloudPath, filePath })
|
||||
const res = await wx.cloud.uploadFile({ cloudPath, filePath: compressed })
|
||||
if (!res || !res.fileID) throw new Error('uploadFile returned no fileID')
|
||||
return res.fileID
|
||||
},
|
||||
|
||||
/**
|
||||
* Compress an avatar temp file to ~200px wide before upload. Uses
|
||||
* wx.compressImage (base lib 2.21.0+ honours `compressedWidth`; older
|
||||
* libs ignore it and just apply `quality`, still a win). Resolves to the
|
||||
* original path on any failure so the caller degrades gracefully.
|
||||
*/
|
||||
_compressAvatar(filePath) {
|
||||
return new Promise((resolve) => {
|
||||
if (!wx.compressImage || !filePath) { resolve(filePath); return }
|
||||
wx.compressImage({
|
||||
src: filePath,
|
||||
quality: 80,
|
||||
compressedWidth: 200,
|
||||
success: (res) => resolve((res && res.tempFilePath) || filePath),
|
||||
fail: () => resolve(filePath)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
onNicknameBlur(e) {
|
||||
const value = (e.detail.value || '').trim()
|
||||
if (!value) return
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</view>
|
||||
<view class="profile-row">
|
||||
<button class="avatar-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
|
||||
<image class="avatar-img" src="{{avatarUrl || icons.peopleFill}}" mode="aspectFit"></image>
|
||||
<image class="avatar-img {{avatarUrl ? 'has-avatar' : ''}}" src="{{avatarUrl || icons.peopleFill}}" mode="{{avatarUrl ? 'aspectFill' : 'aspectFit'}}"></image>
|
||||
<view class="avatar-overlay">
|
||||
<text>点击设置</text>
|
||||
</view>
|
||||
@@ -39,17 +39,18 @@
|
||||
<image class="section-icon" src="{{icons.skinFill}}" mode="aspectFit"></image>
|
||||
<text class="section-title">配色方案</text>
|
||||
</view>
|
||||
<view class="theme-list">
|
||||
<view class="theme-cards">
|
||||
<view
|
||||
class="theme-item {{currentThemeId === item.id ? 'active' : ''}}"
|
||||
class="theme-card {{currentThemeId === item.id ? 'active' : ''}}"
|
||||
wx:for="{{themes}}"
|
||||
wx:key="id"
|
||||
data-id="{{item.id}}"
|
||||
bindtap="onSelectTheme"
|
||||
>
|
||||
<view class="theme-color-dot" style="background: {{item.primary}};"></view>
|
||||
<text class="theme-name">{{item.name}}</text>
|
||||
<image class="theme-check-img" wx:if="{{currentThemeId === item.id}}" src="{{icons.check}}" mode="aspectFit"></image>
|
||||
<view class="theme-card-swatch" style="background: linear-gradient(135deg, {{item.primary}}, {{item.primaryLight}});">
|
||||
<image class="theme-card-check" wx:if="{{currentThemeId === item.id}}" src="{{icons.checkWhite}}" mode="aspectFit"></image>
|
||||
</view>
|
||||
<text class="theme-card-name {{currentThemeId === item.id ? 'active' : ''}}">{{item.name}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</ui-card>
|
||||
|
||||
@@ -14,7 +14,64 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* ---- theme picker ---- */
|
||||
/* ---- theme picker: horizontal gradient swatch cards ---- */
|
||||
.theme-cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
padding-top: 8rpx;
|
||||
}
|
||||
|
||||
.theme-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: calc((100% - 40rpx) / 3);
|
||||
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.theme-card:active {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
|
||||
.theme-card-swatch {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.10);
|
||||
border: 4rpx solid transparent;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.theme-card.active .theme-card-swatch {
|
||||
border-color: var(--card-bg);
|
||||
box-shadow: 0 0 0 4rpx var(--primary), 0 6rpx 16rpx rgba(var(--primary-rgb), 0.35);
|
||||
transform: scale(1.04);
|
||||
}
|
||||
|
||||
.theme-card-check {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.25));
|
||||
}
|
||||
|
||||
.theme-card-name {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 12rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.theme-card-name.active {
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ---- dark-mode & plan list (row-style pickers, e.g. 深色模式) ---- */
|
||||
.theme-list { margin-top: 0; }
|
||||
|
||||
.theme-item {
|
||||
@@ -40,20 +97,6 @@
|
||||
border-top: 1rpx solid transparent;
|
||||
}
|
||||
|
||||
.theme-color-dot {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 50%;
|
||||
margin-right: 20rpx;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.15);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.theme-item:active .theme-color-dot {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
.theme-name {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
@@ -78,6 +121,10 @@
|
||||
|
||||
.plan-item:last-child { border-bottom: none; }
|
||||
|
||||
.plan-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.plan-item.active {
|
||||
background: rgba(var(--primary-rgb), 0.12);
|
||||
margin: 0 -32rpx;
|
||||
@@ -87,6 +134,19 @@
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* left accent bar on the active plan, making the selection scan-able */
|
||||
.plan-item.active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 6rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 3rpx;
|
||||
background: linear-gradient(180deg, var(--primary), var(--primary-light));
|
||||
}
|
||||
|
||||
.plan-radio {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
@@ -283,6 +343,11 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 真实头像(正方形)用 aspectFill 填满圆形容器,去掉占位图的内缩 padding */
|
||||
.avatar-img.has-avatar {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.avatar-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
||||
Reference in New Issue
Block a user