feat(ui): 记录页月份联动与多项 UI 美化/修复
- records: 底部列表与月份选择器联动,新增 [本月|最近] 分段控件;标题/空态随模式切换 - leaderboard: 领奖台布局(冠军居中+皇冠)、次数标注于底座且三档水平对齐;头像 cloud:// 批量预解析提速(云函数需重新部署) - index/timer: 主色底图标改白色变体修复隐形;目标数字按微信字体缩放补偿真机遮挡 - settings: 配色方案横滑渐变色卡;头像上传前压缩至 200px - theme: 新增 teal 主题;暗黑背景统一对齐 - 圆形头像双保险(image 自身 border-radius)修复月/年榜方图 注: cloudfunctions/leaderboard 改动须在开发者工具重新部署才生效。
This commit is contained in:
+2
-1
@@ -7,7 +7,8 @@ const STORAGE_KEY = 'dark_mode_pref'
|
||||
|
||||
function _readSystemTheme() {
|
||||
try {
|
||||
const info = wx.getSystemInfoSync()
|
||||
// wx.getSystemInfoSync 已废弃,改用 wx.getWindowInfo(同样返回 theme 字段)
|
||||
const info = wx.getWindowInfo()
|
||||
return info.theme === 'dark'
|
||||
} catch (e) {
|
||||
return false
|
||||
|
||||
@@ -53,6 +53,15 @@ const PATHS = {
|
||||
rankFill: '<path d="M3.5 18.49l6-6.01 4 4L22 6.92l-1.41-1.41-7.09 7.97-4-4L2 16.99z"/>',
|
||||
crown: '<path d="M5 16L3 5l5.5 5L12 4l3.5 6L21 5l-2 11H5zm0 2h14v2H5z" fill-rule="evenodd"/>',
|
||||
crownFill: '<path d="M5 16L3 5l5.5 5L12 4l3.5 6L21 5l-2 11H5zm0 2h14v2H5z"/>',
|
||||
// Trophy (record celebrations) — Material emoji-events
|
||||
trophy: '<path d="M19 5h-2V3H7v2H5c-1.1 0-2 .9-2 2v1c0 2.55 1.92 4.63 4.39 4.94.63 1.5 1.98 2.63 3.61 2.96V19H7v2h10v-2h-4v-3.1c1.63-.33 2.98-1.46 3.61-2.96C19.08 12.63 21 10.55 21 8V7c0-1.1-.9-2-2-2zM5 8V7h2v3.82C5.84 10.4 5 9.3 5 8zm14 0c0 1.3-.84 2.4-2 2.82V7h2v1z" fill-rule="evenodd"/>',
|
||||
trophyFill: '<path d="M19 5h-2V3H7v2H5c-1.1 0-2 .9-2 2v1c0 2.55 1.92 4.63 4.39 4.94.63 1.5 1.98 2.63 3.61 2.96V19H7v2h10v-2h-4v-3.1c1.63-.33 2.98-1.46 3.61-2.96C19.08 12.63 21 10.55 21 8V7c0-1.1-.9-2-2-2zM5 8V7h2v3.82C5.84 10.4 5 9.3 5 8zm14 0c0 1.3-.84 2.4-2 2.82V7h2v1z"/>',
|
||||
// Party popper (first-time celebrations) — Material celebration
|
||||
party: '<path d="M2 22l14-5-9-9-5 14zm12.53-9.47l1.41-1.41 2.83 2.83-1.41 1.41-2.83-2.83zm.53-6.53L16.47 4.59l1.41-1.41 1.41 1.41-1.41 1.42zM21 11h-3v-2h3v2zm-5.65-4.35L14.24 5.54 15.65 4.1l1.11 1.11-1.41 1.44zM13 3V0h2v3h-2zM8.35 5.65l-1.41-1.44L8.06 3.1l1.41 1.41 1.12 1.14z" fill-rule="evenodd"/>',
|
||||
partyFill: '<path d="M2 22l14-5-9-9-5 14zm12.53-9.47l1.41-1.41 2.83 2.83-1.41 1.41-2.83-2.83zm.53-6.53L16.47 4.59l1.41-1.41 1.41 1.41-1.41 1.42zM21 11h-3v-2h3v2zm-5.65-4.35L14.24 5.54 15.65 4.1l1.11 1.11-1.41 1.44zM13 3V0h2v3h-2zM8.35 5.65l-1.41-1.44L8.06 3.1l1.41 1.41 1.12 1.14z"/>',
|
||||
// Sparkle (generic celebrations + confetti particles)
|
||||
sparkle: '<path d="M12 2l1.8 5.7L19.5 9l-5.7 1.3L12 16l-1.8-5.7L4.5 9l5.7-1.3L12 2zm7 11l.9 2.8 2.8.9-2.8.9-.9 2.8-.9-2.8-2.8-.9 2.8-.9.9-2.8zM5 14l.7 2.1 2.1.7-2.1.7L5 19.5l-.7-2.1-2.1-.7 2.1-.7L5 14z" fill-rule="evenodd"/>',
|
||||
sparkleFill: '<path d="M12 2l1.8 5.7L19.5 9l-5.7 1.3L12 16l-1.8-5.7L4.5 9l5.7-1.3L12 2zm7 11l.9 2.8 2.8.9-2.8.9-.9 2.8-.9-2.8-2.8-.9 2.8-.9.9-2.8zM5 14l.7 2.1 2.1.7-2.1.7L5 19.5l-.7-2.1-2.1-.7 2.1-.7L5 14z"/>',
|
||||
form: '<path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z" fill-rule="evenodd"/>',
|
||||
formFill: '<path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/>',
|
||||
// Notification
|
||||
@@ -132,6 +141,19 @@ const build = (theme) => {
|
||||
out.deleteInactive = _svg(PATHS.delete, INACTIVE)
|
||||
out.successFill = _svg(PATHS.roundCheckFill, s)
|
||||
out.successLine = _svg(PATHS.roundCheck, INACTIVE)
|
||||
// White variants for icons sitting on the theme-gradient card / disc
|
||||
// (completion modal, podium accents, the index hero card, etc.) AND for
|
||||
// icons rendered on the primary (filled) buttons, whose background is the
|
||||
// same theme color as a *Fill variant — orange-on-orange would vanish.
|
||||
// Baked-in white is REQUIRED: these are data-URI SVG <image> tags, so CSS
|
||||
// color / currentColor cannot recolor them; only a white SVG works.
|
||||
out.trophyWhite = _svg(PATHS.trophyFill, '#FFFFFF')
|
||||
out.partyWhite = _svg(PATHS.partyFill, '#FFFFFF')
|
||||
out.hotWhite = _svg(PATHS.hotFill, '#FFFFFF')
|
||||
out.formWhite = _svg(PATHS.formFill, '#FFFFFF')
|
||||
out.playWhite = _svg(PATHS.playFill, '#FFFFFF')
|
||||
out.sparkleWhite = _svg(PATHS.sparkleFill, '#FFFFFF')
|
||||
out.checkWhite = _svg(PATHS.check, '#FFFFFF')
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -75,6 +75,9 @@ const getPlan = (planId, customPlans) => {
|
||||
|
||||
const getTodayTarget = (planId, currentDay) => {
|
||||
const plan = getPlan(planId)
|
||||
// 防御:计划数据损坏(planId 无效 / days 为空)时直接返回 0,
|
||||
// 避免 plan.days[0] 为 undefined 后访问 .target 抛出崩溃。
|
||||
if (!plan.days || plan.days.length === 0) return 0
|
||||
const day = plan.days.find(d => d.day === currentDay)
|
||||
return day ? day.target : plan.days[0].target
|
||||
}
|
||||
@@ -95,7 +98,9 @@ const getPlanDay = (planId, records, planStartDate, customPlans) => {
|
||||
return true
|
||||
})
|
||||
|
||||
const trainedDays = new Set(eligibleRecords.map(r => r.date)).size
|
||||
// 用 dateOnly 去重:r.date 带时分秒,同一天多次训练必须算作"1 天",
|
||||
// 否则会被当成分属两天而错误跳过计划进度。
|
||||
const trainedDays = new Set(eligibleRecords.map(r => dateOnly(r.date))).size
|
||||
return Math.min(trainedDays + 1, plan.totalDays)
|
||||
}
|
||||
|
||||
|
||||
+15
-4
@@ -38,6 +38,14 @@ const THEMES = [
|
||||
primaryLight: '#F06A9A',
|
||||
primaryBg: '#FDEEF3',
|
||||
primaryRgb: '236,64,122'
|
||||
},
|
||||
{
|
||||
id: 'teal',
|
||||
name: '湖青',
|
||||
primary: '#00BCD4',
|
||||
primaryLight: '#4DD0E1',
|
||||
primaryBg: '#E0F7FA',
|
||||
primaryRgb: '0,188,212'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -95,9 +103,12 @@ const setTheme = (id) => {
|
||||
|
||||
const getThemeStyle = (theme, isDark = false) => {
|
||||
const t = theme || getCurrentTheme()
|
||||
// Dark palette is unified with theme.json (#131316 family) so the chrome
|
||||
// background and the page background stop disagreeing. Card is brightened
|
||||
// to keep elevation readable on the darker backdrop.
|
||||
const baseVars = isDark
|
||||
? '--text:#E5E5E7;--text-secondary:#98989F;--bg:#2C2C2E;--bg-gradient-start:#2C2C2E;--bg-gradient-end:#38383A;--card-bg:#3A3A3C;--bg-soft:#48484A;--border:#48484A;--success:#30D158;--success-rgb:48,209,88;--danger:#FF6B6B;--danger-rgb:255,107,107;'
|
||||
: BASE_VARS
|
||||
? '--text:#E5E5E7;--text-secondary:#98989F;--bg:#131316;--bg-gradient-start:#131316;--bg-gradient-end:#1C1C20;--card-bg:#26262B;--bg-soft:#343439;--border:#343439;--success:#30D158;--success-rgb:48,209,88;--danger:#FF6B6B;--danger-rgb:255,107,107;'
|
||||
: `--text:#333333;--text-secondary:#999999;--bg:${t.primaryBg};--bg-gradient-start:${t.primaryBg};--bg-gradient-end:#FAFAFA;--card-bg:#FFFFFF;--bg-soft:#F0F0F0;--border:#EEEEEE;--success:#4CAF50;--success-rgb:76,175,80;--danger:#E53935;--danger-rgb:229,57,53;`
|
||||
return `${baseVars}--primary:${t.primary};--primary-light:${t.primaryLight};--primary-bg:${t.primaryBg};--primary-rgb:${t.primaryRgb};`
|
||||
}
|
||||
|
||||
@@ -116,7 +127,7 @@ const applyThemeToPage = (self) => {
|
||||
// theme.json 的 dark section 只响应系统暗黑模式。用户手动选择暗黑
|
||||
// 但系统是明亮时,窗口背景仍为浅色,切 tab 会闪白。这里用 API 强制设置
|
||||
// 窗口背景,让 tab 切换时的瞬间底色与当前模式一致。
|
||||
const bg = isDark ? '#0F0F12' : '#F5F5F5'
|
||||
const bg = isDark ? '#131316' : theme.primaryBg
|
||||
try { wx.setBackgroundColor({ backgroundColor: bg, backgroundColorTop: bg, backgroundColorBottom: bg }) } catch (e) {}
|
||||
}
|
||||
|
||||
@@ -125,7 +136,7 @@ const applyWindowBg = () => {
|
||||
try {
|
||||
const darkMod = require('./darkMode')
|
||||
const isDark = darkMod.getInitialDarkMode()
|
||||
const bg = isDark ? '#0F0F12' : '#F5F5F5'
|
||||
const bg = isDark ? '#131316' : getCurrentTheme().primaryBg
|
||||
wx.setBackgroundColor({ backgroundColor: bg, backgroundColorTop: bg, backgroundColorBottom: bg })
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
+19
-7
@@ -36,14 +36,25 @@ class Timer {
|
||||
this._tick()
|
||||
this._intervalId = setInterval(() => { this._tick() }, 1000)
|
||||
|
||||
// Register foreground listener (idempotent — we keep a flag so we don't
|
||||
// Register foreground listener (idempotent — keep a flag so we don't
|
||||
// double-register if start() is called multiple times in one session)
|
||||
if (!this._appShowBound && typeof wx.onAppShow === 'function') {
|
||||
try {
|
||||
wx.onAppShow(this._onAppShow)
|
||||
this._appShowBound = true
|
||||
} catch (e) {}
|
||||
}
|
||||
this._bindAppShow()
|
||||
}
|
||||
|
||||
_bindAppShow() {
|
||||
if (this._appShowBound || typeof wx.onAppShow !== 'function') return
|
||||
try {
|
||||
wx.onAppShow(this._onAppShow)
|
||||
this._appShowBound = true
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
_unbindAppShow() {
|
||||
if (!this._appShowBound || typeof wx.offAppShow !== 'function') return
|
||||
try {
|
||||
wx.offAppShow(this._onAppShow)
|
||||
} catch (e) {}
|
||||
this._appShowBound = false
|
||||
}
|
||||
|
||||
pause() {
|
||||
@@ -67,6 +78,7 @@ class Timer {
|
||||
this._paused = false
|
||||
clearInterval(this._intervalId)
|
||||
this._intervalId = null
|
||||
this._unbindAppShow()
|
||||
return this._elapsed
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user