From 6f89494c0bcfab7bb72a61e71b1627475391816c Mon Sep 17 00:00:00 2001 From: cnliucheng Date: Tue, 11 Aug 2026 14:32:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(index):=20=E9=A6=96=E9=A1=B5=E6=94=B9?= =?UTF-8?q?=E7=89=88=20hero=20+=20=E8=BF=9B=E5=BA=A6=E7=82=B9=E5=9C=B0?= =?UTF-8?q?=E5=9B=BE=20+=20=E8=AE=A1=E6=97=B6=E7=8E=AF=20dial=20+=20CTA=20?= =?UTF-8?q?=E8=84=89=E5=86=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 全宽橙渐变 hero:头像+昵称+按小时问候+「时长」敢不敢来挑战+连胜(昵称同行) - 进度点地图 quest-card 上浮叠 hero,圆点+连线 done/now/final 三态,与 CTA 等宽 - 计时环 dial 虚线刻度环+呼吸动画,整体放大(320rpx/数字96rpx) - CTA「立即挑战+时长」带脉冲,保留自由/循环入口 - 千卡按 4 千卡/分钟估算(激励参考);删除「今天还没开始」整段与连胜前图标 - 循环训练秒数步进 ±10;取消每日语录模块 --- pages/index/index.js | 84 +++++-- pages/index/index.wxml | 120 ++++------ pages/index/index.wxss | 488 ++++++++++++++++++----------------------- 3 files changed, 326 insertions(+), 366 deletions(-) diff --git a/pages/index/index.js b/pages/index/index.js index 8904c39..bbc1d75 100644 --- a/pages/index/index.js +++ b/pages/index/index.js @@ -25,14 +25,17 @@ Page({ // 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, + targetTimeFontSize: 96, streakCount: 0, planName: '', planDay: 1, planTotal: 7, - planProgress: 0, - useSegments: false, - planDays: [], + // hero / 进度地图 / dial 相关展示字段(由 refresh 生成) + greetText: '你好', + streakText: '今天开启坚持之旅', + questNodes: [], + questTip: '', + estKcal: 0, showPicker: false, pickerInputFocus: false, presets: [30, 60, 90, 120, 180, 300], @@ -68,8 +71,8 @@ Page({ * 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. + * On real devices, dividing our design size by the factor cancels the + * auto-scale, keeping the number a constant 96rpx-equivalent everywhere. * Bounded so a bad reading can only ever make the number smaller (safe), * never larger (overflow). */ @@ -87,7 +90,7 @@ Page({ } } } catch (e) {} - const DESIGN = 84 + const DESIGN = 96 let size = Math.round(DESIGN / factor) size = Math.max(48, Math.min(96, size)) this.setData({ targetTimeFontSize: size }) @@ -152,12 +155,17 @@ Page({ const target = planMod.getTodayTarget(settings.planId, clampedDay, customPlans) const theme = themeMod.getCurrentTheme() - // 防御:totalDays 为 0(极端数据损坏)时避免除以 0 得到 NaN - const planProgress = plan.totalDays > 0 ? Math.round((clampedDay / plan.totalDays) * 100) : 0 - // 分段格子(每天一格)只在计划天数 <= 10 时用;更长则退化为里程碑连续条,避免格子过密 - const useSegments = plan.totalDays > 0 && plan.totalDays <= 10 - const planDays = useSegments ? Array.from({ length: plan.totalDays }, (_, i) => i) : [] const profile = storage.getProfile() + const greetText = this._greetByHour() + const streakText = streak.count > 0 + ? `已连续 ${streak.count} 天打卡` + : '今天开启坚持之旅' + const questNodes = this._buildQuestMap(plan.totalDays, clampedDay) + const questTip = clampedDay >= plan.totalDays + ? '🎉 本期计划已完成,去记录页看看战绩' + : `⚡ 再坚持 ${plan.totalDays - clampedDay} 天,完成本期计划` + // 千卡粗算:按约 4 千卡/分钟估算(1分30秒≈6千卡)。仅作激励性参考,非精确值。 + const estKcal = Math.max(1, Math.round((target / 60) * 4)) this.setData({ theme, icons: iconsMod.build(theme), @@ -169,14 +177,60 @@ Page({ planName: plan.name, planDay: clampedDay, planTotal: plan.totalDays, - planProgress, - useSegments, - planDays, + greetText, + streakText, + questNodes, + questTip, + estKcal, nickName: profile.nickname || '', avatarUrl: profile.avatarUrl || '' }) }, + _greetByHour() { + const h = new Date().getHours() + if (h < 6) return '夜深了' + if (h < 12) return '早上好' + if (h < 14) return '中午好' + if (h < 18) return '下午好' + if (h < 22) return '晚上好' + return '夜深了' + }, + + /** + * 生成闯关进度地图节点。计划天数 <= 8 时画每天节点;> 8 时退化为 + * 5 个里程碑节点(0/25/50/75/100%),避免节点过密。state: done|now|todo。 + */ + _buildQuestMap(total, day) { + const nodes = [] + if (total > 0 && total <= 8) { + for (let i = 1; i <= total; i++) { + nodes.push({ + label: i === total ? '🏆' : String(i), + state: i < day ? 'done' : (i === day ? 'now' : 'todo'), + final: i === total + }) + } + return nodes + } + const marks = [0, 0.25, 0.5, 0.75, 1] + let nowAssigned = false + marks.forEach((m, idx) => { + const dayAt = Math.round(m * total) + const isFinal = idx === marks.length - 1 + let state + if (dayAt <= day) state = 'done' + else if (!nowAssigned) { state = 'now'; nowAssigned = true } + else state = 'todo' + nodes.push({ + label: isFinal ? '🏆' : (m === 0 ? '起' : String(dayAt)), + state, + final: isFinal + }) + }) + return nodes + }, + onStartTrain() { wx.navigateTo({ url: '/pages/timer/timer' }) }, diff --git a/pages/index/index.wxml b/pages/index/index.wxml index 06fedfe..f182e85 100644 --- a/pages/index/index.wxml +++ b/pages/index/index.wxml @@ -1,91 +1,51 @@ - - - - - + + + + + + + + + {{greetText}},{{nickName || '运动达人'}} + {{streakText}} + - - {{nickName || '运动达人'}},{{todayDone ? '太棒了!' : '准备好了吗?'}} - {{todayDone ? '继续保持这个势头' : '今天的平板支撑等着你'}} + + 「{{todayTargetText}}」 + 敢不敢来挑战? - - - - - - - - - - - - {{streakCount}} - 连续打卡 - - - - - {{planName}} - - - - - - - - - - - - - - - - - - 第 {{planDay}} / {{planTotal}} 天 - + + + + {{planName}}第 {{planDay}} / {{planTotal}} 天 + 🏆 + + + {{item.label}} + + + + {{questTip}} - - - - - - 今日目标 - - - - {{todayTargetText}} - - - - - - - 今日已完成 {{todayDuration}} 秒 - - - - - 还未开始训练 - - + + + + {{todayTargetText}} + 今日目标时长 - + ⚡ 预计消耗 {{estKcal}} 千卡 + @@ -93,10 +53,10 @@ variant="primary" size="xl" block - text="{{todayDone ? '再次训练' : '开始训练'}}" + text="{{todayDone ? '再战一次 ' : '立即挑战 '}}{{todayTargetText}}" icon-src="{{icons.playWhite}}" bindtap="onStartTrain" - custom-style="border-radius:16rpx;" + custom-style="border-radius:16rpx;box-shadow:0 8rpx 20rpx rgba(var(--primary-rgb),0.35);animation:ctaPulse 2s ease-in-out infinite;" > diff --git a/pages/index/index.wxss b/pages/index/index.wxss index 285cb7e..d656731 100644 --- a/pages/index/index.wxss +++ b/pages/index/index.wxss @@ -1,335 +1,275 @@ -/* ---- greeting ---- */ -.greeting-row { - display: flex; - align-items: center; - gap: 20rpx; - padding: 16rpx 8rpx 24rpx; - animation: cardIn 0.4s ease both; +/* ===== 全宽渐变头图 hero ===== */ +/* 负 margin 抵消 container 的 24rpx 内边距,让头图左右贯穿并贴到导航栏底边; + 底部留大内边距给下方 quest-card 负 margin 上浮叠住。文字统一白色,图标用 + *White 变体(若有)。hero 用 opacity-only 动画,避免 transform 把 .container + 变成 containing block 而破坏弹窗定位(同 app.wxss pageIn 思路)。 */ +.hero { + margin: -24rpx -24rpx 0; + padding: 24rpx 24rpx 72rpx; + background: linear-gradient(160deg, var(--primary) 0%, var(--primary-light) 100%); + color: #fff; + position: relative; + overflow: hidden; + animation: heroFade 0.4s ease both; } -/* 圆形头像:外层 overflow:hidden + 内层 image border-radius:50% 双保险, - 避免远程(cloud://)头像裁不出圆角而露直角。无头像时显示人形图标占位, - 加浅色底让占位更有质感;有头像时去掉底,避免深色头像边缘露白缝。 */ -.greeting-avatar { - width: 88rpx; - height: 88rpx; +.hero::after { + content: ''; + position: absolute; + right: -60rpx; + top: -60rpx; + width: 240rpx; + height: 240rpx; + border-radius: 50%; + background: radial-gradient(circle, rgba(255, 255, 255, 0.18), transparent 70%); + pointer-events: none; +} + +@keyframes heroFade { + from { opacity: 0; } + to { opacity: 1; } +} + +/* 顶部行:圆形头像 + 时间问候 */ +.hero-top { + display: flex; + align-items: center; + gap: 18rpx; +} + +.hero-avatar { + width: 84rpx; + height: 84rpx; flex-shrink: 0; border-radius: 50%; overflow: hidden; - background: var(--bg-soft); + background: #fff; display: flex; align-items: center; justify-content: center; + border: 3rpx solid rgba(255, 255, 255, 0.6); } -.greeting-avatar-img { +.hero-avatar-img { width: 100%; height: 100%; border-radius: 50%; display: block; } -.greeting-avatar.has-avatar { +/* 有头像时去掉白底,只留浅描边,避免深色头像边缘露白缝 */ +.hero-avatar.has-avatar { background: transparent; + border-color: rgba(255, 255, 255, 0.85); } -.greeting-main { - flex: 1; +.hero-user { + display: flex; + flex-direction: row; + align-items: center; + gap: 14rpx; min-width: 0; } -.greeting-title { - display: block; - font-size: 40rpx; - font-weight: 700; - color: var(--text); - line-height: 1.3; +.hero-greet { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.92); + font-weight: 500; } -.greeting-sub { - display: block; - font-size: 26rpx; - color: var(--text-secondary); - margin-top: 4rpx; - line-height: 1.2; -} - -/* ---- header card ---- */ -.header-card { +/* 挑战主句:时长强调色 + 「敢不敢」白字 */ +.hero-challenge { + margin-top: 28rpx; display: flex; - flex-direction: column; - gap: 28rpx; - padding: 4rpx 0; + align-items: baseline; + flex-wrap: wrap; } -/* 上区:左连胜视觉重心,右计划徽章 */ -.streak-row { - display: flex; - align-items: center; - justify-content: space-between; -} - -.streak-section { - display: flex; - align-items: center; -} - -/* 火焰图标 + 光晕:光晕脉动让火焰"活"起来 */ -.streak-icon-wrap { - position: relative; - width: 80rpx; - height: 80rpx; - display: flex; - align-items: center; - justify-content: center; - margin-right: 16rpx; - flex-shrink: 0; -} - -.streak-halo { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: 50%; - background: radial-gradient(circle at center, - rgba(var(--primary-rgb), 0.45) 0%, - rgba(var(--primary-rgb), 0.18) 50%, - rgba(var(--primary-rgb), 0) 75%); - animation: streakHalo 2s ease-in-out infinite; -} - -.streak-icon { - position: relative; - width: 64rpx; - height: 64rpx; - z-index: 1; - animation: float 2.5s ease-in-out infinite; -} - -.streak-text { - display: flex; - flex-direction: column; -} - -.streak-num { - font-size: 72rpx; +.hero-time { + font-size: 48rpx; font-weight: 800; - line-height: 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 12rpx rgba(0, 0, 0, 0.15); + color: #FFE3C2; + letter-spacing: 1rpx; + margin-right: 12rpx; font-variant-numeric: tabular-nums; } -.streak-label { - font-size: 22rpx; - color: var(--text-secondary); - margin-top: 4rpx; - line-height: 1; +.hero-cta-text { + font-size: 44rpx; + font-weight: 800; + color: #fff; + text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.15); + line-height: 1.2; } -@keyframes streakHalo { - 0%, 100% { transform: scale(0.85); opacity: 0.7; } - 50% { transform: scale(1.1); opacity: 1; } +.hero-social { + margin-top: 0; + font-size: 28rpx; + color: rgba(255, 255, 255, 0.9); } -/* 计划徽章:移到上区右侧,半透明白底药丸,与火焰渐变卡片区分 */ -.plan-badge { +/* ===== 闯关进度地图(上浮卡片) ===== */ +.quest-card { + margin-top: -56rpx; + margin-left: 0; + margin-right: 0; + position: relative; + z-index: 2; +} + +.quest-head { display: flex; align-items: center; - background: rgba(255, 255, 255, 0.18); - padding: 8rpx 18rpx; - border-radius: 999rpx; - flex-shrink: 0; + justify-content: space-between; + margin-bottom: 24rpx; } -.plan-badge-icon { - width: 28rpx; - height: 28rpx; - margin-right: 8rpx; -} - -.plan-badge-text { - font-size: 26rpx; - font-weight: 600; +.quest-title { + font-size: 32rpx; + font-weight: 700; color: var(--text); - line-height: 1; } -/* 下区:分段进度 */ -.segment-progress { +.quest-sub { + font-size: 24rpx; + font-weight: 400; + color: var(--text-secondary); + margin-left: 12rpx; +} + +.quest-trophy { + font-size: 40rpx; +} + +.quest-map { + display: flex; + align-items: center; + padding: 8rpx 4rpx; +} + +.q-node { + width: 48rpx; + height: 48rpx; + border-radius: 50%; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 24rpx; + font-weight: 700; + background: var(--bg-soft); + color: var(--text-secondary); +} + +.q-node.done { + background: var(--primary); + color: #fff; +} + +.q-node.now { + background: linear-gradient(135deg, var(--primary), var(--primary-light)); + color: #fff; + box-shadow: 0 0 0 6rpx rgba(var(--primary-rgb), 0.18); + animation: qPulse 1.6s ease-in-out infinite; +} + +.q-node.final { + background: #FFF3E0; + font-size: 30rpx; +} + +.q-link { + flex: 1; + height: 4rpx; + background: var(--bg-soft); + margin: 0 2rpx; +} + +.q-link.done { + background: var(--primary); +} + +.quest-tip { + display: block; + margin-top: 20rpx; + font-size: 24rpx; + font-weight: 600; + color: var(--primary); +} + +@keyframes qPulse { + 0% { box-shadow: 0 0 0 4rpx rgba(var(--primary-rgb), 0.28); } + 70% { box-shadow: 0 0 0 12rpx rgba(var(--primary-rgb), 0); } + 100% { box-shadow: 0 0 0 4rpx rgba(var(--primary-rgb), 0); } +} + +/* ===== 计时环 dial ===== */ +.timer-wrap { + text-align: center; + margin: 32rpx 0 8rpx; +} + +.dial { + width: 320rpx; + height: 320rpx; + border-radius: 50%; + margin: 0 auto; + position: relative; + background: radial-gradient(circle at 35% 30%, #ffffff, #f4f6f2 70%); + box-shadow: inset 0 4rpx 14rpx rgba(var(--primary-rgb), 0.12), 0 10rpx 24rpx rgba(var(--primary-rgb), 0.12); display: flex; flex-direction: column; - gap: 10rpx; + align-items: center; + justify-content: center; + animation: ringBreathe 3s ease-in-out infinite; } -/* planTotal <= 10:按天分段格子 */ -.segment-track { - display: flex; - gap: 8rpx; -} - -.segment-cell { - flex: 1; - height: 12rpx; - border-radius: 6rpx; - background: transparent; - border: 2rpx solid rgba(255, 255, 255, 0.30); - box-sizing: border-box; - transition: background 0.3s ease, border-color 0.3s ease; -} - -.segment-cell.done { - background: var(--on-primary, #FFFFFF); - border-color: var(--on-primary, #FFFFFF); -} - -.segment-cell.current { - background: var(--on-primary, #FFFFFF); - border-color: var(--on-primary, #FFFFFF); - animation: segmentPulse 1.2s ease-in-out infinite; -} - -/* planTotal > 10:连续条 + 里程碑节点,避免格子过密 */ -.milestone-track { - width: 100%; -} - -.milestone-bar { - position: relative; - width: 100%; - height: 12rpx; - background: rgba(0, 0, 0, 0.12); - border-radius: 6rpx; - overflow: visible; -} - -.milestone-fill { - height: 100%; - background: var(--on-primary, linear-gradient(90deg, var(--primary), var(--primary-light))); - border-radius: 6rpx; - transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1); -} - -.milestone-node { +/* 虚线刻度环 */ +.dial::before { + content: ''; position: absolute; - top: 50%; - width: 10rpx; - height: 10rpx; + inset: 14rpx; border-radius: 50%; - background: var(--card-bg); - transform: translate(-50%, -50%); - border: 2rpx solid var(--border); + border: 2rpx dashed rgba(var(--primary-rgb), 0.28); } -.progress-label { - font-size: 22rpx; - color: var(--text-secondary); - line-height: 1; -} - -@keyframes segmentPulse { - 0%, 100% { box-shadow: 0 0 4rpx rgba(var(--primary-rgb), 0.4); } - 50% { box-shadow: 0 0 14rpx rgba(var(--primary-rgb), 0.9); } -} - -/* ---- target card ---- */ -.target-section { - text-align: center; -} - -.target-header { - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 8rpx; -} - -.target-header .section-title { - margin-bottom: 0; -} - -.section-icon { - width: 32rpx; - height: 32rpx; - margin-right: 8rpx; -} - -.target-display { - margin: 20rpx 0; -} - -.target-ring { - display: inline-flex; - align-items: center; - justify-content: center; - width: 280rpx; - height: 280rpx; - border-radius: 50%; - 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); - overflow: hidden; -} - -.target-time { +.dial-time { font-size: 84rpx; font-weight: 800; color: var(--primary); font-variant-numeric: tabular-nums; letter-spacing: -1rpx; line-height: 1; - max-width: 240rpx; + max-width: 280rpx; text-align: center; white-space: nowrap; overflow: hidden; } -.today-status { - margin-top: 16rpx; - display: flex; - align-items: center; - justify-content: center; - gap: 8rpx; -} - -.done-badge { - display: inline-flex; - align-items: center; - background: rgba(var(--success-rgb), 0.08); - padding: 10rpx 24rpx; - border-radius: 24rpx; -} - -.done-icon { - width: 28rpx; - height: 28rpx; - margin-right: 6rpx; -} - -.done-text { - font-size: 26rpx; - color: var(--success); - line-height: 1; -} - -.pending-icon { - width: 28rpx; - height: 28rpx; - margin-right: 6rpx; -} - -.pending-text { - font-size: 26rpx; +.dial-label { + font-size: 24rpx; color: var(--text-secondary); - line-height: 1; + margin-top: 8rpx; } -/* ---- action buttons ---- */ +.benefit { + margin-top: 20rpx; + font-size: 28rpx; + color: var(--text-secondary); +} + +.benefit-hl { + color: var(--primary); + font-weight: 700; + font-size: 30rpx; +} + +@keyframes ringBreathe { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.02); } +} + +/* ===== action buttons ===== */ .action-buttons { display: flex; flex-direction: column; @@ -348,7 +288,13 @@ min-width: 0; /* 防止图标+文字在窄屏撑破 flex 项 */ } -/* ---- picker ---- */ +/* 主按钮脉冲发光(加在 ui-btn 宿主上,__usePrivacyCheck__ 不影响) */ +@keyframes ctaPulse { + 0%, 100% { box-shadow: 0 8rpx 20rpx rgba(var(--primary-rgb), 0.35); } + 50% { box-shadow: 0 8rpx 30rpx rgba(var(--primary-rgb), 0.6); } +} + +/* ===== picker ===== */ .picker-title { font-size: 34rpx; font-weight: 600; @@ -439,7 +385,7 @@ margin-top: 8rpx; } -/* ---- circuit (循环训练) config panel ---- */ +/* ===== circuit (循环训练) config panel ===== */ .picker-sub { display: block; text-align: center; @@ -497,7 +443,7 @@ font-variant-numeric: tabular-nums; } -/* ---- 训练完成高亮 (3 秒后淡出) ---- */ +/* ===== 训练完成高亮 (3 秒后淡出) ===== */ /* 这个 class 落在 宿主节点上(ui-card.wxss 已把宿主设为 display:block, 否则 border/box-shadow 只会碎成两根竖条、transform 完全不生效)。 描边用 box-shadow 的 spread 而非 border:border 会撑大宿主 8rpx,高亮出现/消失