Files
wx_pbzc/pages/index/index.wxss
T
lc 3532d0111b feat: 循环训练模式 + 语音补全 / 屏幕常亮 / 卡片渲染修复
循环训练(circuit)
- 新增 utils/circuitTimer.js 状态机: 工作->休息->...->完成, stop() 返回有效撑总秒(不含休息)
- 首页新增「循环训练·分组练习」入口 + stepper 配置弹窗
- 配置本地持久化 circuit_config(每组时长/组数/休息/每周目标), 下次自动预填
- 记录以 planId:'circuit' + mode:'circuit' 落库, duration 语义不变 -> 排行榜/统计/每日计划进度零改动
- 记录页展示「循环 N组xMs·休Rs」

计时器顶部循环进度面板
- 组进度点阵按组数等分铺满(flex:1 1 0, 去掉 max-width 封顶), 当前组以增高而非加宽强调
- 大号「X/Y 组」+ 四态阶段徽章(撑住/休息中/准备开始/已暂停) + 累计秒数副行
- 状态灯用纯 CSS 圆点(SVG 图标颜色烤死在 data URI 里, CSS 改不动)
- 超过 15 组自动降级为线性进度条; 面板不设固定高度, 规避真机大字体档裁字

修复: 循环训练全程无语音
- 根因: circuit 的 onTick 走 _circuitCue(只有震动), 从未调用 _remind
- 接入整场进度播报, 基准为 sets x holdPerSet(而非每组), 避免 4 组播 4 次「已完成一半」
- 新增 restStart/nextSet/lastSet 三条不含数字的词条, 客户端与 tts 云函数词表同步
- 修复必然撞车的时序: 偶数组时 halfway 与休息切换同一 tick, 加 2.6s 优先窗口让位
- 防吵闸门: 每组 <10s 不播过场语音, 休息 <5s 不播; 预载按模式取词条

训练期间屏幕常亮
- wx.setKeepScreenOn: onStart 开 / onUnload 关 / onShow 在 isRunning 时重新武装
- 切后台再回来该标志会被系统清掉, 故必须重新武装, 否则后半程丢语音与震动
- 低基础库与 Android 省电模式静默降级, 不弹 toast

修复: 首页打卡卡片左侧两个竖条
- 根因: ui-card 宿主节点无 display 声明为 inline, 页面侧加的 border 被打断成两个零宽行盒碎片
- ui-card 加 :host{display:block}, margin-bottom 从内部 view 上提到宿主
- 高亮由 border 改为 box-shadow spread 描边(不占布局, 无跳动) + 补 border-radius
- 连带修复失效的交错入场: nth-child 跨组件边界恒匹配 1, 改为 index 属性驱动内联 animation-delay, 14 处调用点补序号

注意: tts 云函数需重新部署, 否则新增的 3 条词条返回 Unknown prompt key(静默降级不报错)
2026-08-03 16:34:22 +08:00

481 lines
9.2 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ---- greeting ---- */
.greeting-row {
padding: 16rpx 8rpx 24rpx;
animation: cardIn 0.4s ease both;
}
.greeting-text {
display: flex;
align-items: center;
margin-bottom: 8rpx;
}
.greeting-icon {
width: 44rpx;
height: 44rpx;
margin-right: 12rpx;
}
.greeting-title {
font-size: 40rpx;
font-weight: 700;
color: var(--text);
line-height: 1.2;
}
.greeting-sub {
font-size: 26rpx;
color: var(--text-secondary);
margin-left: 60rpx;
line-height: 1;
}
/* ---- header card ---- */
.header-card {
display: flex;
flex-direction: column;
gap: 28rpx;
padding: 4rpx 0;
}
/* 上区:左连胜视觉重心,右计划徽章 */
.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;
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);
font-variant-numeric: tabular-nums;
}
.streak-label {
font-size: 22rpx;
color: var(--text-secondary);
margin-top: 4rpx;
line-height: 1;
}
@keyframes streakHalo {
0%, 100% { transform: scale(0.85); opacity: 0.7; }
50% { transform: scale(1.1); opacity: 1; }
}
/* 计划徽章:移到上区右侧,半透明白底药丸,与火焰渐变卡片区分 */
.plan-badge {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.18);
padding: 8rpx 18rpx;
border-radius: 999rpx;
flex-shrink: 0;
}
.plan-badge-icon {
width: 28rpx;
height: 28rpx;
margin-right: 8rpx;
}
.plan-badge-text {
font-size: 26rpx;
font-weight: 600;
color: var(--text);
line-height: 1;
}
/* 下区:分段进度 */
.segment-progress {
display: flex;
flex-direction: column;
gap: 10rpx;
}
/* 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 {
position: absolute;
top: 50%;
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background: var(--card-bg);
transform: translate(-50%, -50%);
border: 2rpx solid var(--border);
}
.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 {
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;
}
.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;
color: var(--text-secondary);
line-height: 1;
}
/* ---- action buttons ---- */
.action-buttons {
display: flex;
flex-direction: column;
gap: 24rpx;
margin: 8rpx 0 8rpx;
}
/* ---- picker ---- */
.picker-title {
font-size: 34rpx;
font-weight: 600;
color: var(--text);
display: block;
text-align: center;
margin-bottom: 32rpx;
}
.preset-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-bottom: 28rpx;
}
.preset-item {
flex: 0 0 calc(33.33% - 12rpx);
box-sizing: border-box;
background: var(--bg-soft);
border: 2rpx solid transparent;
border-radius: 16rpx;
padding: 20rpx 0;
text-align: center;
display: flex;
flex-direction: column;
transition: all 0.2s ease;
}
.preset-item:active {
transform: scale(0.94);
}
.preset-item.selected {
background: var(--primary-bg);
border: 2rpx solid var(--primary);
}
.preset-num {
font-size: 36rpx;
font-weight: 700;
color: var(--text);
line-height: 1.1;
}
.preset-item.selected .preset-num {
color: var(--primary);
}
.preset-unit {
font-size: 20rpx;
color: var(--text-secondary);
margin-top: 2rpx;
line-height: 1;
}
.custom-input-row {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 28rpx;
}
.custom-label {
font-size: 28rpx;
color: var(--text-secondary);
line-height: 1;
}
.custom-input {
width: 160rpx;
height: 64rpx;
background: var(--bg-soft);
border-radius: 12rpx;
text-align: center;
font-size: 32rpx;
font-weight: 600;
color: var(--primary);
margin: 0 16rpx;
}
.picker-cancel {
display: block;
text-align: center;
font-size: 28rpx;
color: var(--text-secondary);
padding: 12rpx 0;
margin-top: 8rpx;
}
/* ---- circuit (循环训练) config panel ---- */
.picker-sub {
display: block;
text-align: center;
font-size: 24rpx;
color: var(--text-secondary);
margin: -18rpx 0 24rpx;
}
.circuit-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 8rpx;
border-bottom: 1rpx solid var(--border);
}
.circuit-row-label {
font-size: 30rpx;
color: var(--text);
font-weight: 500;
}
.stepper {
display: flex;
align-items: center;
gap: 20rpx;
}
.stepper-btn {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: var(--bg-soft);
color: var(--primary);
font-size: 40rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
transition: transform 0.12s ease, opacity 0.12s ease;
}
.stepper-btn:active {
transform: scale(0.9);
opacity: 0.8;
}
.stepper-val {
min-width: 124rpx;
text-align: center;
font-size: 32rpx;
font-weight: 700;
color: var(--text);
font-variant-numeric: tabular-nums;
}
/* ---- 训练完成高亮 (3 秒后淡出) ---- */
/* 这个 class 落在 <ui-card> 宿主节点上(ui-card.wxss 已把宿主设为 display:block
否则 border/box-shadow 只会碎成两根竖条、transform 完全不生效)。
描边用 box-shadow 的 spread 而非 borderborder 会撑大宿主 8rpx,高亮出现/消失
时整页布局跳动;box-shadow 不占布局空间。宿主自身无圆角,必须补上与内部卡片
一致的 24rpx,否则描边会是直角框套在圆角卡片外面。 */
.just-completed {
animation: justCompletedPulse 1s ease-in-out 2;
border-radius: 24rpx;
box-shadow: 0 0 0 4rpx var(--primary), 0 4rpx 24rpx rgba(var(--primary-rgb), 0.3);
}
@keyframes justCompletedPulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.02); }
}