feat(ui): 记录页月份联动与多项 UI 美化/修复

- records: 底部列表与月份选择器联动,新增 [本月|最近] 分段控件;标题/空态随模式切换
- leaderboard: 领奖台布局(冠军居中+皇冠)、次数标注于底座且三档水平对齐;头像 cloud:// 批量预解析提速(云函数需重新部署)
- index/timer: 主色底图标改白色变体修复隐形;目标数字按微信字体缩放补偿真机遮挡
- settings: 配色方案横滑渐变色卡;头像上传前压缩至 200px
- theme: 新增 teal 主题;暗黑背景统一对齐
- 圆形头像双保险(image 自身 border-radius)修复月/年榜方图

注: cloudfunctions/leaderboard 改动须在开发者工具重新部署才生效。
This commit is contained in:
2026-07-22 09:36:22 +08:00
parent 70b87d424d
commit 6176f2c341
32 changed files with 924 additions and 197 deletions
+54 -3
View File
@@ -14,6 +14,13 @@ Page({
todayDone: false,
todayDuration: 0,
todayTargetText: '',
// Ring number font size in rpx. WeChat auto-scales text by the user's
// font-size setting (fontSizeScaleFactor) but does NOT scale rpx box
// dimensions — so on a real device with enlarged system font the 84rpx
// number grows past the fixed-size circular ring and gets clipped. We
// cancel that scale here so the number renders at a constant visual size
// (matching the devtools/simulator) on every device. See _readFontScale().
targetTimeFontSize: 84,
streakCount: 0,
planName: '',
planDay: 1,
@@ -28,9 +35,50 @@ Page({
onLoad() {
themeMod.applyThemeToPage(this)
this._readFontScale()
this._firstShow = true
},
/**
* Read the device's WeChat font-size scale and compensate the ring number
* so it never overflows the fixed circular clip on real devices.
*
* WeChat auto-applies `fontSizeScaleFactor` to ALL text (including rpx font
* sizes) but leaves rpx box dimensions untouched. A user who bumps up the
* system font therefore sees enlarged text inside a non-enlarged ring.
*
* CRITICAL asymmetry: the DevTools simulator reports the SAME
* `fontSizeScaleFactor` as a real device (the user's actual WeChat setting)
* but does NOT actually scale rendered text. So if we compensate there, the
* number ends up too small in the simulator. We therefore SKIP the
* compensation under DevTools (host.env === 'devtools') and keep the design
* size — matching what the simulator renders literally.
*
* On real devices, dividing our design size by the factor cancels the
* auto-scale, keeping the number a constant 84rpx-equivalent everywhere.
* Bounded so a bad reading can only ever make the number smaller (safe),
* never larger (overflow).
*/
_readFontScale() {
let factor = 1
try {
const info = wx.getAppBaseInfo ? wx.getAppBaseInfo() : wx.getSystemInfoSync()
const isDevtools = info.host && info.host.env === 'devtools'
if (!isDevtools) {
if (typeof info.fontSizeScaleFactor === 'number' && info.fontSizeScaleFactor > 0) {
factor = info.fontSizeScaleFactor
} else if (typeof info.fontSizeSetting === 'number' && info.fontSizeSetting > 0) {
// fontSizeScaleFactor == currentFontSize / standardFontSize(17px)
factor = info.fontSizeSetting / 17
}
}
} catch (e) {}
const DESIGN = 84
let size = Math.round(DESIGN / factor)
size = Math.max(48, Math.min(96, size))
this.setData({ targetTimeFontSize: size })
},
onShow() {
themeMod.applyThemeToPage(this)
try {
@@ -86,9 +134,12 @@ Page({
const customPlans = storage.getCustomPlans()
const plan = planMod.getPlan(settings.planId, customPlans)
const planDay = planMod.getPlanDay(settings.planId, allRecords, settings.planStartDate, customPlans)
const target = planMod.getTodayTarget(settings.planId, Math.min(planDay, plan.totalDays))
const clampedDay = Math.min(planDay, plan.totalDays)
const target = planMod.getTodayTarget(settings.planId, clampedDay)
const theme = themeMod.getCurrentTheme()
// 防御:totalDays 为 0(极端数据损坏)时避免除以 0 得到 NaN
const planProgress = plan.totalDays > 0 ? Math.round((clampedDay / plan.totalDays) * 100) : 0
this.setData({
theme,
icons: iconsMod.build(theme),
@@ -98,9 +149,9 @@ Page({
todayTargetText: util.formatTime(target),
streakCount: streak.count,
planName: plan.name,
planDay: Math.min(planDay, plan.totalDays),
planDay: clampedDay,
planTotal: plan.totalDays,
planProgress: Math.round((Math.min(planDay, plan.totalDays) / plan.totalDays) * 100)
planProgress
})
},
+5 -5
View File
@@ -10,17 +10,17 @@
</view>
<!-- 连续打卡卡片 -->
<ui-card variant="in" class="{{justCompleted ? 'just-completed' : ''}}">
<ui-card variant="gradient in" class="{{justCompleted ? 'just-completed' : ''}}">
<view class="header-card">
<view class="streak-section">
<image class="streak-icon" src="{{icons.hotFill}}" mode="aspectFit"></image>
<image class="streak-icon" src="{{icons.hotWhite}}" mode="aspectFit"></image>
<text class="streak-num">{{streakCount}}</text>
<text class="streak-label">连续打卡</text>
</view>
<view class="divider"></view>
<view class="plan-info">
<view class="plan-badge">
<image class="plan-badge-icon" src="{{icons.formFill}}" mode="aspectFit"></image>
<image class="plan-badge-icon" src="{{icons.formWhite}}" mode="aspectFit"></image>
<text class="plan-badge-text">{{planName}}</text>
</view>
<view class="progress-bar-wrap">
@@ -44,7 +44,7 @@
</view>
<view class="target-display">
<view class="target-ring">
<text class="target-time">{{todayTargetText}}</text>
<text class="target-time" style="font-size: {{targetTimeFontSize}}rpx;">{{todayTargetText}}</text>
</view>
<text class="target-unit">平板支撑</text>
</view>
@@ -70,7 +70,7 @@
size="xl"
block
text="{{todayDone ? '再次训练' : '开始训练'}}"
icon-src="{{icons.playFill}}"
icon-src="{{icons.playWhite}}"
bindtap="onStartTrain"
></ui-btn>
+24 -12
View File
@@ -54,8 +54,11 @@
.streak-num {
font-size: 56rpx;
font-weight: 700;
color: var(--primary);
line-height: 1.1;
/* On the gradient hero card --text cascades to white; on any plain card it
stays dark, so var(--text) reads correctly in both contexts. We must NOT
use var(--primary) here — that would be orange-on-orange and invisible. */
color: var(--text);
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.12);
}
.streak-label {
@@ -114,7 +117,9 @@
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary), var(--primary-light));
/* On the gradient hero card --on-primary (white) overrides the fallback,
so the fill stays visible instead of blending into the orange card. */
background: var(--on-primary, linear-gradient(90deg, var(--primary), var(--primary-light)));
border-radius: 4rpx;
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
@@ -126,9 +131,9 @@
top: -4rpx;
width: 16rpx;
height: 16rpx;
background: var(--primary);
background: var(--on-primary, var(--primary));
border-radius: 50%;
box-shadow: 0 0 10rpx var(--primary);
box-shadow: 0 0 10rpx var(--on-primary, var(--primary));
}
.progress-label {
@@ -167,20 +172,27 @@
display: inline-flex;
align-items: center;
justify-content: center;
width: 200rpx;
height: 200rpx;
width: 280rpx;
height: 280rpx;
border-radius: 50%;
background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.08), rgba(var(--primary-rgb), 0.02));
border: 4rpx solid rgba(var(--primary-rgb), 0.15);
margin-bottom: 16rpx;
background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.10), rgba(var(--primary-rgb), 0.03));
border: 6rpx solid rgba(var(--primary-rgb), 0.18);
box-shadow: 0 0 0 14rpx rgba(var(--primary-rgb), 0.05);
margin-bottom: 20rpx;
overflow: hidden;
}
.target-time {
font-size: 64rpx;
font-weight: 700;
font-size: 84rpx;
font-weight: 800;
color: var(--primary);
font-variant-numeric: tabular-nums;
letter-spacing: -1rpx;
line-height: 1;
max-width: 240rpx;
text-align: center;
white-space: nowrap;
overflow: hidden;
}
.target-unit {