.timer-page { display: flex; flex-direction: column; align-items: center; justify-content: space-between; padding-top: 80rpx; padding-bottom: calc(170rpx + env(safe-area-inset-bottom)); min-height: 100vh; box-sizing: border-box; } /* ---- 循环训练进度面板 ---- 注意:整块不设固定 height,靠 padding 撑开。真机大字体档下文字会被微信 放大,固定高度 + overflow 会把数字裁掉(项目里踩过这个坑)。 */ .circuit-bar { /* 抬一层:呼吸光晕(.breathe-ring, z-index:0)在 DOM 里排在本面板之后, 不抬层会把橙色晕染盖到卡片上。 */ position: relative; z-index: 1; width: 620rpx; max-width: 86%; box-sizing: border-box; padding: 26rpx 32rpx 22rpx; margin-bottom: 12rpx; background: var(--card-bg); border: 1rpx solid rgba(var(--primary-rgb), 0.14); border-radius: 28rpx; box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.05); transition: border-color 0.35s ease, box-shadow 0.35s ease; animation: circuitBarIn 0.45s cubic-bezier(0.34, 1.4, 0.64, 1) both; } /* 休息态整块降温:边框转成 success 绿,和"撑住"的主色态形成对比 */ .circuit-bar.is-rest { border-color: rgba(var(--success-rgb), 0.32); box-shadow: 0 6rpx 20rpx rgba(var(--success-rgb), 0.10); } @keyframes circuitBarIn { 0% { opacity: 0; transform: translateY(-16rpx); } 100% { opacity: 1; transform: translateY(0); } } /* --- 组进度点阵 --- 整行铺满面板内宽,每段按组数严格等分(分段进度条式)。 */ .circuit-dots { display: flex; align-items: center; width: 100%; /* 锁住行高:当前组比其它段高 6rpx,不锁行高会在 idle→running 时整块跳一下 */ min-height: 18rpx; gap: 8rpx; margin-bottom: 22rpx; } /* flex:1 1 0 + min-width:0 → 每段等分剩余宽度,与内容无关。 不设 max-width:一旦封顶,组数少时点阵就只占左边一截(此前的问题)。 当前组不再靠"加宽"强调(那会破坏等分),改为增高+发光,宽度恒等分。 */ .circuit-dot { flex: 1 1 0; min-width: 0; height: 12rpx; border-radius: 6rpx; background: var(--bg-soft); transition: height 0.3s ease, background 0.3s ease, box-shadow 0.3s ease; } .circuit-dot.is-done { background: var(--primary); opacity: 0.85; } /* 当前组:增高 + 渐变 + 发光呼吸,一眼锁定进度位置(宽度仍等分) */ .circuit-dot.is-active { height: 18rpx; border-radius: 9rpx; opacity: 1; background: linear-gradient(90deg, var(--primary), var(--primary-light)); box-shadow: 0 0 14rpx rgba(var(--primary-rgb), 0.55); animation: circuitDotPulse 1.5s ease-in-out infinite; } /* 休息时高亮"下一组",给用户一个即将开始的预告焦点 */ .circuit-dot.is-next { height: 18rpx; border-radius: 9rpx; background: rgba(var(--success-rgb), 0.45); animation: circuitDotPulse 1.8s ease-in-out infinite; } @keyframes circuitDotPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } } /* 暂停时冻结所有呼吸动画,静止本身就是"已暂停"的信号 */ .circuit-bar.is-paused .circuit-dot { animation: none; } /* --- 组数过多时的线性降级条 --- */ .circuit-track { height: 12rpx; border-radius: 6rpx; background: var(--bg-soft); overflow: hidden; margin-bottom: 22rpx; } .circuit-track-fill { height: 100%; border-radius: 6rpx; background: linear-gradient(90deg, var(--primary), var(--primary-light)); transition: width 0.4s ease; } /* --- 主信息行:大数字 + 阶段徽章 --- */ .circuit-meta { display: flex; align-items: center; justify-content: space-between; } .circuit-count { display: flex; align-items: baseline; } .circuit-count-num { font-size: 56rpx; font-weight: 800; color: var(--text); line-height: 1; font-variant-numeric: tabular-nums; letter-spacing: -1rpx; } .circuit-count-total { font-size: 32rpx; font-weight: 600; color: var(--text-secondary); line-height: 1; margin-left: 2rpx; } .circuit-count-unit { font-size: 26rpx; color: var(--text-secondary); margin-left: 8rpx; } /* --- 阶段徽章 --- */ .circuit-phase { display: flex; align-items: center; gap: 10rpx; padding: 12rpx 24rpx; border-radius: 30rpx; transition: background 0.3s ease; } /* 状态指示灯用 CSS 画而非 icon:项目里的 SVG 图标颜色烤死在 data URI 里, CSS 改不动;这里需要跟随状态换色,纯 CSS 圆点是唯一干净的做法。 */ .circuit-phase-dot { width: 14rpx; height: 14rpx; border-radius: 50%; flex-shrink: 0; } .circuit-phase-text { font-size: 26rpx; font-weight: 600; line-height: 1; white-space: nowrap; } /* 工作态:主色渐变实心 + 白灯白字 */ .circuit-phase--work { background: linear-gradient(135deg, var(--primary), var(--primary-light)); box-shadow: 0 4rpx 14rpx rgba(var(--primary-rgb), 0.32); } .circuit-phase--work .circuit-phase-dot { background: #FFFFFF; animation: phaseDotBlink 1.2s ease-in-out infinite; } .circuit-phase--work .circuit-phase-text { color: #FFFFFF; } /* 休息态:绿色淡底,视觉降温 */ .circuit-phase--rest { background: rgba(var(--success-rgb), 0.14); } .circuit-phase--rest .circuit-phase-dot { background: var(--success); animation: phaseDotBlink 1.8s ease-in-out infinite; } .circuit-phase--rest .circuit-phase-text { color: var(--success); } /* 待机态 / 暂停态:中性灰,不抢视觉 */ .circuit-phase--idle, .circuit-phase--pause { background: var(--bg-soft); } .circuit-phase--idle .circuit-phase-dot, .circuit-phase--pause .circuit-phase-dot { background: var(--text-secondary); } .circuit-phase--idle .circuit-phase-text, .circuit-phase--pause .circuit-phase-text { color: var(--text-secondary); } @keyframes phaseDotBlink { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.4; transform: scale(0.8); } } /* --- 副信息行 --- */ .circuit-sub { display: block; margin-top: 14rpx; font-size: 24rpx; color: var(--text-secondary); line-height: 1.3; } /* ---- ring wrapper ---- */ .ring-wrapper { position: relative; display: flex; align-items: center; justify-content: center; } /* breathing guide rings — soft radial glows instead of hard outline circles, so they read as ambient halo rather than a bullseye */ .breathe-ring { position: absolute; border-radius: 50%; background: radial-gradient(circle, rgba(var(--primary-rgb), 0.10) 0%, rgba(var(--primary-rgb), 0) 65%); animation: breathePulse 3.5s ease-in-out infinite; pointer-events: none; z-index: 0; } .breathe-ring-1 { width: 520rpx; height: 520rpx; animation-delay: 0s; } .breathe-ring-2 { width: 440rpx; height: 440rpx; animation-delay: 0.5s; background: radial-gradient(circle, rgba(var(--primary-rgb), 0.14) 0%, rgba(var(--primary-rgb), 0) 65%); } .breathe-ring-3 { width: 380rpx; height: 380rpx; animation-delay: 1s; background: radial-gradient(circle, rgba(var(--primary-rgb), 0.18) 0%, rgba(var(--primary-rgb), 0) 65%); } .breathe-ring.fast { animation: breathePulseFast 1.4s ease-in-out infinite; } .breathe-ring.fast.breathe-ring-1 { animation-delay: 0s; } .breathe-ring.fast.breathe-ring-2 { animation-delay: 0.25s; } .breathe-ring.fast.breathe-ring-3 { animation-delay: 0.5s; } /* celebration burst */ .celebrate-burst { position: absolute; width: 520rpx; height: 520rpx; pointer-events: none; z-index: 2; } .spark { position: absolute; width: 44rpx; height: 44rpx; animation: sparkBurst 1s ease-out forwards; } .s1 { top: -10rpx; left: 50%; margin-left: -22rpx; animation-delay: 0s; } .s2 { top: 30rpx; right: -10rpx; animation-delay: 0.1s; } .s3 { bottom: -10rpx; right: 50rpx; animation-delay: 0.2s; } .s4 { bottom: 20rpx; left: 30rpx; animation-delay: 0.15s; } .s5 { top: 50%; right: -20rpx; animation-delay: 0.25s; } .s6 { top: -20rpx; left: 40rpx; animation-delay: 0.05s; } @keyframes sparkBurst { 0% { opacity: 0; transform: translate(0, 0) scale(0.3); } 40% { opacity: 1; transform: translate(0, -30rpx) scale(1.3); } 100% { opacity: 0; transform: translate(0, -80rpx) scale(0.4); } } /* ---- hint section ---- */ .hint-section { margin-top: 36rpx; text-align: center; } .hint-row { display: inline-flex; align-items: center; justify-content: center; gap: 10rpx; } .hint-icon { width: 36rpx; height: 36rpx; } .running-pulse { animation: float 1.5s ease-in-out infinite; } .completed-bounce { animation: bounceSoft 0.6s ease infinite; } .hint-text { font-size: 34rpx; color: var(--text-secondary); line-height: 1.3; } .hint-text.running { color: var(--primary); font-weight: 600; } .hint-text.paused { color: var(--primary-light); } .hint-text.completed { color: var(--success); font-weight: 600; } .completed-hint { animation: popIn 0.5s ease; } /* ---- controls ---- */ .controls { margin-top: 60rpx; width: 100%; display: flex; justify-content: center; } .ctrl-row { display: flex; justify-content: center; align-items: center; } /* ---- full-screen confetti ---- */ .confetti { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; pointer-events: none; z-index: 9999; overflow: hidden; } .confetti-piece { position: absolute; top: -20rpx; border-radius: 4rpx; animation: confettiFall 2.4s ease-out forwards; } /* Higher celebration levels: faster / longer fall */ .confetti-piece--milestone, .confetti-piece--first, .confetti-piece--record { animation-duration: 3.2s; } .confetti-piece--first { animation-duration: 4s; } @keyframes confettiFall { 0% { opacity: 1; transform: translateY(0) rotate(0deg); } 100% { opacity: 0.6; transform: translateY(100vh) rotate(720deg); } } /* ---- achievement banner ---- */ .achievement-banner { position: fixed; top: 160rpx; left: 50%; transform: translateX(-50%); background: linear-gradient(135deg, var(--primary), var(--primary-light)); color: #FFFFFF; font-size: 32rpx; font-weight: 700; padding: 24rpx 48rpx; border-radius: 48rpx; box-shadow: 0 8rpx 24rpx rgba(var(--primary-rgb), 0.4); z-index: 10000; animation: achievementIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both, achievementOut 0.4s ease-in 3.5s both; white-space: nowrap; } .achievement-text { display: block; line-height: 1; } @keyframes achievementIn { 0% { opacity: 0; transform: translateX(-50%) scale(0.5); } 100% { opacity: 1; transform: translateX(-50%) scale(1); } } @keyframes achievementOut { 0% { opacity: 1; transform: translateX(-50%) scale(1); } 100% { opacity: 0; transform: translateX(-50%) scale(0.8); } } /* ---- completion modal (full-screen summary after training ends) ---- */ .completion-mask { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.55); z-index: 10000; display: flex; align-items: center; justify-content: center; padding: 40rpx; box-sizing: border-box; animation: completionMaskIn 0.3s ease; } @keyframes completionMaskIn { from { opacity: 0; } to { opacity: 1; } } .completion-card { width: 100%; max-width: 640rpx; background: var(--card-bg); border-radius: 32rpx; padding: 56rpx 40rpx 40rpx; display: flex; flex-direction: column; align-items: center; box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.25); animation: completionCardIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); } @keyframes completionCardIn { 0% { transform: scale(0.6) translateY(40rpx); opacity: 0; } 100% { transform: scale(1) translateY(0); opacity: 1; } } .completion-emoji-wrap { width: 140rpx; height: 140rpx; border-radius: 50%; background: linear-gradient(135deg, var(--primary), var(--primary-light)); display: flex; align-items: center; justify-content: center; margin-bottom: 24rpx; box-shadow: 0 8rpx 24rpx rgba(var(--primary-rgb), 0.4); animation: completionEmojiPulse 1.6s ease-in-out infinite; } @keyframes completionEmojiPulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.08); } } .completion-icon { width: 80rpx; height: 80rpx; } .completion-title { font-size: 32rpx; font-weight: 600; color: var(--text); margin-bottom: 32rpx; text-align: center; } .completion-number-wrap { display: flex; align-items: baseline; justify-content: center; margin-bottom: 8rpx; } .completion-number { font-size: 180rpx; font-weight: 800; color: var(--primary); line-height: 1; font-variant-numeric: tabular-nums; letter-spacing: -4rpx; text-shadow: 0 4rpx 12rpx rgba(var(--primary-rgb), 0.2); } /* >= 60 秒时数字部分变成「X分Y」,字符变多,缩小字号避免撑破卡片 */ .completion-number--long { font-size: 140rpx; } .completion-unit { font-size: 36rpx; font-weight: 600; color: var(--text-secondary); margin-left: 8rpx; } .completion-label { font-size: 26rpx; color: var(--text-secondary); margin-bottom: 24rpx; } .completion-overtime { display: flex; align-items: baseline; gap: 6rpx; background: rgba(var(--primary-rgb), 0.12); padding: 12rpx 24rpx; border-radius: 32rpx; margin-bottom: 16rpx; animation: overtimePop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) 1.1s both; } @keyframes overtimePop { 0% { transform: scale(0); opacity: 0; } 100% { transform: scale(1); opacity: 1; } } .overtime-plus { font-size: 32rpx; font-weight: 700; color: var(--primary); line-height: 1; } .overtime-label { font-size: 24rpx; color: var(--primary); font-weight: 500; } .completion-streak { display: flex; align-items: center; gap: 8rpx; margin-bottom: 36rpx; font-size: 26rpx; color: var(--text); } .streak-flame { width: 32rpx; height: 32rpx; } .streak-text { font-weight: 500; } .completion-actions { display: flex; gap: 20rpx; width: 100%; margin-bottom: 12rpx; } .completion-btn { flex: 1; height: 88rpx; display: flex; align-items: center; justify-content: center; border-radius: 44rpx; font-size: 30rpx; font-weight: 600; transition: transform 0.15s ease, opacity 0.15s ease, background 0.2s ease; background: var(--bg-soft); color: var(--text); } .completion-btn:active { transform: scale(0.96); opacity: 0.85; } .completion-btn--primary { background: linear-gradient(135deg, var(--primary), var(--primary-light)); color: #FFFFFF; box-shadow: 0 8rpx 20rpx rgba(var(--primary-rgb), 0.3); } .completion-btn--text { background: transparent; color: var(--text-secondary); font-weight: 500; height: 64rpx; font-size: 26rpx; margin-top: 4rpx; } /* ---- stop confirm modal (替代 wx.showModal,自定义实现) ---- */ .stop-confirm-mask { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.55); z-index: 9999; display: flex; align-items: center; justify-content: center; padding: 40rpx; box-sizing: border-box; animation: completionMaskIn 0.25s ease; } .stop-confirm-card { width: 100%; max-width: 560rpx; background: var(--card-bg); border-radius: 24rpx; padding: 48rpx 40rpx 24rpx; display: flex; flex-direction: column; align-items: center; box-shadow: 0 12rpx 36rpx rgba(0, 0, 0, 0.22); animation: stopConfirmPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); } @keyframes stopConfirmPop { 0% { transform: scale(0.85); opacity: 0; } 100% { transform: scale(1); opacity: 1; } } .stop-confirm-title { font-size: 32rpx; font-weight: 700; color: var(--text); margin-bottom: 20rpx; line-height: 1.3; } .stop-confirm-content { font-size: 28rpx; color: var(--text-secondary); line-height: 1.5; text-align: center; margin-bottom: 40rpx; } .stop-confirm-actions { display: flex; width: 100%; gap: 16rpx; border-top: 1rpx solid var(--border); padding-top: 24rpx; } .stop-confirm-btn { flex: 1; height: 80rpx; display: flex; align-items: center; justify-content: center; font-size: 30rpx; font-weight: 600; border-radius: 12rpx; background: var(--bg-soft); color: var(--text); transition: transform 0.15s ease, opacity 0.15s ease, background 0.2s ease; } .stop-confirm-btn:active { transform: scale(0.96); opacity: 0.85; } .stop-confirm-btn--primary { background: linear-gradient(135deg, var(--primary), var(--primary-light)); color: #FFFFFF; box-shadow: 0 6rpx 16rpx rgba(var(--primary-rgb), 0.3); }