fix(ui): 多项 UI 修复 — 弹窗遮挡/按钮可见性/暗黑闪烁/云函数版本
- 计划编辑弹窗移出 .container 避免 fixed 降级 + 修复滚动 - dock bottom 40rpx→8rpx 紧贴底部, sheet padding 同步调整 - 计划编辑器取消/恢复默认按钮可见性修复(bg 匹配容器) - ui-btn--danger 按钮可见性修复 - 清除确认弹窗跟随暗黑模式切换 - icons.js build() 中 Fill 路径覆盖 outlined 路径修复 - plan.js getPlanDay 忽略 customPlans 修复 - darkMode.js 冗余三元简化 - util.js fileToDataURI fallback 可读性提升 - theme.js: BASE_VARS 与 getThemeStyle 统一来源, 去 50ms navbar 延迟 - app.wxss: 移除 page transition 和 background-color 减少闪白 - 云函数 wx-server-sdk 版本统一 ~2.6.3, tts cloud.init 一致化 - 闪白问题: wx.setBackgroundColor 三层防护(onLaunch/applyThemeToPage/switchTab) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,9 @@ App({
|
||||
|
||||
this.globalData.theme = themeMod.getCurrentTheme()
|
||||
|
||||
// 窗口背景色 — 冷启动就设好,避免暗黑模式下切 tab 闪白
|
||||
themeMod.applyWindowBg()
|
||||
|
||||
// Sync with cloud: always locate our doc first (pullAll caches
|
||||
// _openid / _docId), then either restore or push. The previous flow
|
||||
// only pulled when local was empty, so on a cold start with non-empty
|
||||
|
||||
@@ -12,8 +12,6 @@ page {
|
||||
'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
font-size: 28rpx;
|
||||
color: var(--text);
|
||||
background-color: var(--bg);
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
/* dark mode overrides (only for system dark mode; user choice goes through
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"wx-server-sdk": "latest"
|
||||
"wx-server-sdk": "~2.6.3"
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,6 @@
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"wx-server-sdk": "latest"
|
||||
"wx-server-sdk": "~2.6.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
// those prefixes are reserved by Tencent Cloud SCF. Use the TTS_ prefix.
|
||||
|
||||
const cloud = require('wx-server-sdk')
|
||||
cloud.init()
|
||||
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
|
||||
|
||||
// Polished, fixed prompts.
|
||||
const PROMPTS = {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"wx-server-sdk": "latest",
|
||||
"wx-server-sdk": "~2.6.3",
|
||||
"tencentcloud-sdk-nodejs": "^4.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
}
|
||||
|
||||
.ui-btn--danger {
|
||||
background: var(--card-bg);
|
||||
background: rgba(var(--danger-rgb), 0.06);
|
||||
color: var(--danger);
|
||||
border: 2rpx solid rgba(var(--danger-rgb), 0.3);
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
width: 100%;
|
||||
border-radius: 32rpx 32rpx 0 0;
|
||||
padding: 28rpx 32rpx 40rpx;
|
||||
/* bottom sheet 需要避开 Dock TabBar(高度 ~110rpx + 40rpx 间隙),
|
||||
否则最底下的按钮会被 toolbar 遮挡。150rpx = dock 高度 + margin。 */
|
||||
padding-bottom: calc(150rpx + env(safe-area-inset-bottom));
|
||||
/* bottom sheet 需要避开 Dock TabBar (占视口底部 ~158rpx)。
|
||||
168rpx 留出可见间距。 */
|
||||
padding-bottom: calc(168rpx + env(safe-area-inset-bottom));
|
||||
animation: uiModalSlideUp 0.3s cubic-bezier(0.32, 0.72, 0, 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ module.exports = {
|
||||
version: 'v2.7',
|
||||
|
||||
/** 最后更新日期,外显在设置页「关于」 */
|
||||
updatedAt: '2026-07-09',
|
||||
updatedAt: '2026-07-10',
|
||||
|
||||
/** 开发者名称 / 微信号,设置页点击可复制 */
|
||||
developer: '刘承',
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
const themeMod = require('../utils/theme')
|
||||
const darkMod = require('../utils/darkMode')
|
||||
|
||||
// Resolve the active theme synchronously at module load so the first paint
|
||||
// already has the correct CSS variables — avoids a flash of the default theme.
|
||||
const _initialTheme = themeMod.getCurrentTheme()
|
||||
const _initialThemeStyle = themeMod.getThemeStyle(_initialTheme)
|
||||
const _initialThemeStyle = themeMod.getThemeStyle(_initialTheme, darkMod.getInitialDarkMode())
|
||||
|
||||
// Inactive icon color follows the dark mode: matches --text-secondary in dark
|
||||
// mode and a softer gray in light mode. Kept in sync with utils/theme.js so
|
||||
@@ -11,10 +12,8 @@ const _initialThemeStyle = themeMod.getThemeStyle(_initialTheme)
|
||||
const LIGHT_INACTIVE = '#999999'
|
||||
const DARK_INACTIVE = '#98989F'
|
||||
|
||||
const _getInactiveColor = () => {
|
||||
const darkMod = require('../utils/darkMode')
|
||||
return darkMod.getInitialDarkMode() ? DARK_INACTIVE : LIGHT_INACTIVE
|
||||
}
|
||||
const _getInactiveColor = () =>
|
||||
darkMod.getInitialDarkMode() ? DARK_INACTIVE : LIGHT_INACTIVE
|
||||
|
||||
const _initialInactiveColor = _getInactiveColor()
|
||||
|
||||
@@ -97,6 +96,8 @@ Component({
|
||||
'/pages/settings/settings'
|
||||
]
|
||||
if (index === this.data.selected) return
|
||||
// 切 tab 前设窗口背景色,避免真机过渡动画露出白色底色
|
||||
try { themeMod.applyWindowBg() } catch (e) {}
|
||||
wx.switchTab({ url: pages[index] })
|
||||
},
|
||||
// Called by settings.onSelectDarkMode - the dock isn't in
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.dock {
|
||||
position: fixed;
|
||||
bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||||
bottom: calc(8rpx + env(safe-area-inset-bottom));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: calc(100% - 80rpx);
|
||||
|
||||
@@ -85,7 +85,7 @@ Page({
|
||||
const allRecords = Object.values(storage.getRecords()).flat()
|
||||
const customPlans = storage.getCustomPlans()
|
||||
const plan = planMod.getPlan(settings.planId, customPlans)
|
||||
const planDay = planMod.getPlanDay(settings.planId, allRecords, settings.planStartDate)
|
||||
const planDay = planMod.getPlanDay(settings.planId, allRecords, settings.planStartDate, customPlans)
|
||||
const target = planMod.getTodayTarget(settings.planId, Math.min(planDay, plan.totalDays))
|
||||
|
||||
const theme = themeMod.getCurrentTheme()
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
const storage = require('../../utils/storage')
|
||||
const themeMod = require('../../utils/theme')
|
||||
const darkMod = require('../../utils/darkMode')
|
||||
const planMod = require('../../utils/plan')
|
||||
const iconsMod = require('../../utils/icons')
|
||||
const cloud = require('../../utils/cloud')
|
||||
const config = require('../../config')
|
||||
const darkMod = require('../../utils/darkMode')
|
||||
const util = require('../../utils/util')
|
||||
|
||||
/**
|
||||
@@ -217,7 +217,7 @@ Page({
|
||||
this.setData({ currentThemeId: id })
|
||||
try {
|
||||
const tb = this.getTabBar()
|
||||
if (tb) tb.setData({ themeStyle: themeMod.getThemeStyle(theme) })
|
||||
if (tb) tb.setData({ themeStyle: themeMod.getThemeStyle(theme, darkMod.getInitialDarkMode()) })
|
||||
} catch (e) {}
|
||||
themeMod.applyThemeToPage(this)
|
||||
},
|
||||
|
||||
@@ -197,12 +197,14 @@
|
||||
<text class="about-copy">Made with ❤️ for better fitness</text>
|
||||
</view>
|
||||
</ui-card>
|
||||
</view>
|
||||
<!-- /container — 关闭在这里,让下面的弹窗 fixed 相对视口而非 .container -->
|
||||
|
||||
<!-- 训练计划编辑器:用普通 view 不用 ui-modal,
|
||||
因为 WeChat 自定义组件根元素上 position: fixed 会被吃,导致 body 被推到容器末尾。
|
||||
普通 view 的 position: fixed 在小程序里 100% 生效。 -->
|
||||
<view wx:if="{{showPlanEditor}}" class="plan-editor-mask" catchtouchmove="onNoop">
|
||||
<view class="plan-editor-mask__bg" bindtap="onCloseEditor"></view>
|
||||
<view wx:if="{{showPlanEditor}}" class="plan-editor-mask" style="{{themeStyle}}">
|
||||
<view class="plan-editor-mask__bg" bindtap="onCloseEditor" catchtouchmove="onNoop"></view>
|
||||
<view class="plan-editor-sheet" catchtap="onNoop">
|
||||
<view class="plan-editor-handle"></view>
|
||||
<view class="editor-title">编辑训练计划</view>
|
||||
@@ -284,33 +286,37 @@
|
||||
<view class="editor-action-reset" wx:if="{{editingIsCustom}}" bindtap="onResetPlan">
|
||||
<text>恢复默认</text>
|
||||
</view>
|
||||
<view class="editor-action-cancel" bindtap="onCloseEditor">
|
||||
<text>取消</text>
|
||||
</view>
|
||||
<view class="editor-action-save" bindtap="onSavePlan">
|
||||
<text>保存</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="editor-cancel" bindtap="onCloseEditor">取消</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 清除数据确认弹窗:用自定义 ui-modal 替代 wx.showModal
|
||||
避免开发者工具中 wx.showModal 首次点击不关闭的问题 -->
|
||||
<ui-modal
|
||||
visible="{{showClearConfirm}}"
|
||||
position="center"
|
||||
showHandle="{{false}}"
|
||||
bind:close="onCancelClear"
|
||||
>
|
||||
<view class="confirm-dialog" catchtap="onNoop">
|
||||
<text class="confirm-title">清除数据</text>
|
||||
<text class="confirm-desc">将删除所有训练记录和连续打卡数据,此操作不可恢复。确定继续吗?</text>
|
||||
<view class="confirm-actions">
|
||||
<view class="confirm-btn confirm-btn--cancel" bindtap="onCancelClear">
|
||||
<text>取消</text>
|
||||
</view>
|
||||
<view class="confirm-btn confirm-btn--danger" bindtap="onConfirmClear">
|
||||
<text>确定清除</text>
|
||||
避免开发者工具中 wx.showModal 首次点击不关闭的问题。
|
||||
外层 view 注入 themeStyle,确保弹窗跟随暗黑模式切换。 -->
|
||||
<view style="{{themeStyle}}">
|
||||
<ui-modal
|
||||
visible="{{showClearConfirm}}"
|
||||
position="center"
|
||||
showHandle="{{false}}"
|
||||
bind:close="onCancelClear"
|
||||
>
|
||||
<view class="confirm-dialog" catchtap="onNoop">
|
||||
<text class="confirm-title">清除数据</text>
|
||||
<text class="confirm-desc">将删除所有训练记录和连续打卡数据,此操作不可恢复。确定继续吗?</text>
|
||||
<view class="confirm-actions">
|
||||
<view class="confirm-btn confirm-btn--cancel" bindtap="onCancelClear">
|
||||
<text>取消</text>
|
||||
</view>
|
||||
<view class="confirm-btn confirm-btn--danger" bindtap="onConfirmClear">
|
||||
<text>确定清除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</ui-modal>
|
||||
</view>
|
||||
</ui-modal>
|
||||
</view>
|
||||
|
||||
@@ -370,12 +370,11 @@
|
||||
box-sizing: border-box;
|
||||
background: var(--card-bg);
|
||||
border-radius: 32rpx 32rpx 0 0;
|
||||
padding: 12rpx 32rpx 40rpx;
|
||||
/* Sheet is z-index 1000 above the tab bar (z-index 999), but the tab bar
|
||||
still visually covers the bottom of the sheet. Add the tab bar's
|
||||
height (110rpx + safe area) to padding-bottom so "保存/取消" sit
|
||||
clearly above the tab bar. */
|
||||
padding-bottom: calc(150rpx + env(safe-area-inset-bottom));
|
||||
padding: 12rpx 32rpx 0;
|
||||
/* Dock 原生 cover-view 层永远在 webview 上方。
|
||||
dock 占视口底部: 8rpx(bottom) + 118rpx(高度) + 32rpx(shadow) ≈ 158rpx。
|
||||
168rpx 给「保存/取消」留出 10rpx 可见间距。 */
|
||||
padding-bottom: calc(168rpx + env(safe-area-inset-bottom));
|
||||
animation: planEditorSlideUp 0.3s cubic-bezier(0.32, 0.72, 0, 1);
|
||||
}
|
||||
|
||||
@@ -506,6 +505,7 @@
|
||||
}
|
||||
|
||||
.editor-action-reset,
|
||||
.editor-action-cancel,
|
||||
.editor-action-save {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
@@ -519,7 +519,7 @@
|
||||
}
|
||||
|
||||
.editor-action-reset {
|
||||
background: var(--card-bg);
|
||||
background: var(--bg-soft);
|
||||
color: var(--text-secondary);
|
||||
border: 2rpx solid var(--border);
|
||||
}
|
||||
@@ -528,6 +528,16 @@
|
||||
background: var(--bg-soft);
|
||||
}
|
||||
|
||||
.editor-action-cancel {
|
||||
background: var(--bg-soft);
|
||||
color: var(--text-secondary);
|
||||
border: 2rpx solid var(--border);
|
||||
}
|
||||
|
||||
.editor-action-cancel:active {
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.editor-action-save {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-light));
|
||||
color: #FFFFFF;
|
||||
@@ -538,14 +548,6 @@
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.editor-cancel {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: var(--text-secondary);
|
||||
padding: 12rpx 0 0;
|
||||
}
|
||||
|
||||
/* ---- 清除数据确认弹窗 ---- */
|
||||
|
||||
.confirm-dialog {
|
||||
|
||||
@@ -72,7 +72,7 @@ Page({
|
||||
const allRecords = Object.values(storage.getRecords()).flat()
|
||||
const customPlans = storage.getCustomPlans()
|
||||
const plan = planMod.getPlan(settings.planId, customPlans)
|
||||
planDay = planMod.getPlanDay(settings.planId, allRecords, settings.planStartDate)
|
||||
planDay = planMod.getPlanDay(settings.planId, allRecords, settings.planStartDate, customPlans)
|
||||
target = planMod.getTodayTarget(settings.planId, Math.min(planDay, plan.totalDays))
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ const STORAGE_KEY = 'dark_mode_pref'
|
||||
function _readSystemTheme() {
|
||||
try {
|
||||
const info = wx.getSystemInfoSync()
|
||||
return info.theme === 'dark' ? true : false
|
||||
return info.theme === 'dark'
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
|
||||
+4
-2
@@ -118,10 +118,12 @@ const build = (theme) => {
|
||||
const out = {}
|
||||
Object.keys(PATHS).forEach((name) => {
|
||||
if (name.endsWith('Fill')) {
|
||||
const base = name.slice(0, -4)
|
||||
out[base] = _svg(PATHS[name], INACTIVE)
|
||||
// Fill variant: filled path + primary color.
|
||||
// Do NOT write out[base] here — that would overwrite the outlined
|
||||
// variant set by the base-name entry below with the wrong path.
|
||||
out[name] = _svg(PATHS[name], p)
|
||||
} else {
|
||||
// Outlined variant: outlined path + inactive gray.
|
||||
out[name] = _svg(PATHS[name], INACTIVE)
|
||||
}
|
||||
})
|
||||
|
||||
+2
-2
@@ -83,8 +83,8 @@ const getTodayTarget = (planId, currentDay) => {
|
||||
* Calculate the current plan day based on training records since plan start.
|
||||
* Only counts records from the current plan's start date onward.
|
||||
*/
|
||||
const getPlanDay = (planId, records, planStartDate) => {
|
||||
const plan = getPlan(planId)
|
||||
const getPlanDay = (planId, records, planStartDate, customPlans) => {
|
||||
const plan = getPlan(planId, customPlans)
|
||||
const today = formatDate(new Date())
|
||||
|
||||
// Filter records: same plan, after plan start date, and not today
|
||||
|
||||
+28
-12
@@ -41,10 +41,12 @@ const THEMES = [
|
||||
}
|
||||
]
|
||||
|
||||
const BASE_VARS = '--text:#333;--text-secondary:#999;--bg:#F5F5F5;--card-bg:#FFFFFF;--border:#EEE;--success:#4CAF50;--success-rgb:76,175,80;--danger:#E53935;--danger-rgb:229,57,53;--primary:#FF6B35;--primary-light:#FF8C5A;--primary-bg:#FFF3ED;--primary-rgb:255,107,53;'
|
||||
// Light-mode base CSS custom properties shared between the cold-start export
|
||||
// (used by every page as initial data) and getThemeStyle(isDark=false).
|
||||
// Update here ONLY — getThemeStyle picks it up for the light branch.
|
||||
const BASE_VARS = '--text:#333333;--text-secondary:#999999;--bg:#F5F5F5;--bg-gradient-start:#F5F5F5;--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;'
|
||||
const DEFAULT_THEME_ID = 'orange'
|
||||
const THEME_KEY = 'app_theme'
|
||||
let _navColorTimer = null
|
||||
|
||||
const getThemeById = (id) => THEMES.find(t => t.id === id) || THEMES[0]
|
||||
|
||||
@@ -61,15 +63,12 @@ const getCurrentTheme = () => {
|
||||
}
|
||||
|
||||
const _setNavBarColor = (primary) => {
|
||||
if (_navColorTimer) clearTimeout(_navColorTimer)
|
||||
_navColorTimer = setTimeout(() => {
|
||||
try {
|
||||
wx.setNavigationBarColor({
|
||||
frontColor: '#ffffff',
|
||||
backgroundColor: primary
|
||||
})
|
||||
} catch (e) {}
|
||||
}, 50)
|
||||
try {
|
||||
wx.setNavigationBarColor({
|
||||
frontColor: '#ffffff',
|
||||
backgroundColor: primary
|
||||
})
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const setTheme = (id) => {
|
||||
@@ -98,7 +97,7 @@ const getThemeStyle = (theme, isDark = false) => {
|
||||
const t = theme || getCurrentTheme()
|
||||
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;'
|
||||
: '--text:#333333;--text-secondary:#999999;--bg:#F5F5F5;--bg-gradient-start:#F5F5F5;--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;'
|
||||
: BASE_VARS
|
||||
return `${baseVars}--primary:${t.primary};--primary-light:${t.primaryLight};--primary-bg:${t.primaryBg};--primary-rgb:${t.primaryRgb};`
|
||||
}
|
||||
|
||||
@@ -113,12 +112,29 @@ const applyThemeToPage = (self) => {
|
||||
}
|
||||
self.setData({ theme, themeStyle: nextStyle, isDark })
|
||||
_setNavBarColor(theme.primary)
|
||||
|
||||
// theme.json 的 dark section 只响应系统暗黑模式。用户手动选择暗黑
|
||||
// 但系统是明亮时,窗口背景仍为浅色,切 tab 会闪白。这里用 API 强制设置
|
||||
// 窗口背景,让 tab 切换时的瞬间底色与当前模式一致。
|
||||
const bg = isDark ? '#0F0F12' : '#F5F5F5'
|
||||
try { wx.setBackgroundColor({ backgroundColor: bg, backgroundColorTop: bg, backgroundColorBottom: bg }) } catch (e) {}
|
||||
}
|
||||
|
||||
/** 仅设置窗口背景色,不触发页面 setData — 供 app.js 冷启动使用 */
|
||||
const applyWindowBg = () => {
|
||||
try {
|
||||
const darkMod = require('./darkMode')
|
||||
const isDark = darkMod.getInitialDarkMode()
|
||||
const bg = isDark ? '#0F0F12' : '#F5F5F5'
|
||||
wx.setBackgroundColor({ backgroundColor: bg, backgroundColorTop: bg, backgroundColorBottom: bg })
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
THEMES,
|
||||
DEFAULT_THEME_ID,
|
||||
BASE_VARS,
|
||||
applyWindowBg,
|
||||
getThemeById,
|
||||
getCurrentTheme,
|
||||
setTheme,
|
||||
|
||||
+1
-1
@@ -82,7 +82,7 @@ const fileToDataURI = (filePath, maxBytes = 512 * 1024) => {
|
||||
encoding: 'base64',
|
||||
success: (res) => {
|
||||
// chooseAvatar returns PNG; allow JPG/GIF/WEBP fallbacks.
|
||||
const m = (filePath.match(/\.(\w+)(?:\?|$)/) || [, 'png'])[1].toLowerCase()
|
||||
const m = ((filePath.match(/\.(\w+)(?:\?|$)/) || [])[1] || 'png').toLowerCase()
|
||||
const mime = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp' }[m] || 'image/png'
|
||||
resolve(`data:${mime};base64,${res.data}`)
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user