backup: snapshot before optimization

This commit is contained in:
2026-06-04 16:25:43 +08:00
commit ff2700c067
47 changed files with 3332 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
var storage = require('../../utils/storage')
var planMod = require('../../utils/plan')
var util = require('../../utils/util')
var themeMod = require('../../utils/theme')
Page({
data: {
theme: { primary: '#FF6B35', primaryLight: '#FF8C5A', primaryBg: '#FFF3ED', primaryRgb: '255,107,53' },
themeStyle: '',
todayTarget: 0,
todayDone: false,
todayDuration: 0,
todayTargetText: '',
streakCount: 0,
planName: '',
planDay: 1,
planTotal: 7,
planProgress: 0,
showPicker: false,
presets: [30, 60, 90, 120, 180, 300],
customDuration: 60,
customInput: ''
},
onLoad: function () {
this.refresh()
},
onShow: function () {
themeMod.applyThemeToPage(this)
try {
var tb = this.getTabBar()
if (tb) tb.setData({ selected: 0 })
} catch (e) {}
this.refresh()
},
onPullDownRefresh: function () {
this.refresh()
wx.stopPullDownRefresh()
},
refresh: function () {
var settings = storage.getSettings()
var streak = storage.getStreak()
var todayRecord = storage.getTodayRecord()
var records = []
var allRecords = storage.getRecords()
Object.keys(allRecords).forEach(function (key) {
records = records.concat(allRecords[key])
})
var plan = planMod.getPlan(settings.planId)
var planDay = planMod.getPlanDay(settings.planId, records)
var target = planMod.getTodayTarget(settings.planId, Math.min(planDay, plan.totalDays))
this.setData({
todayTarget: target,
todayDone: todayRecord && todayRecord.duration >= target,
todayDuration: todayRecord ? todayRecord.duration : 0,
todayTargetText: util.formatTime(target),
streakCount: streak.count,
planName: plan.name,
planDay: Math.min(planDay, plan.totalDays),
planTotal: plan.totalDays,
planProgress: Math.round((Math.min(planDay, plan.totalDays) / plan.totalDays) * 100)
})
},
onStartTrain: function () {
wx.navigateTo({ url: '/pages/timer/timer' })
},
onFreeTrain: function () {
this.setData({ showPicker: true, customInput: '', customDuration: 60 })
},
onClosePicker: function () {
this.setData({ showPicker: false })
},
onSelectPreset: function (e) {
var val = e.currentTarget.dataset.value
this.setData({ customDuration: val, customInput: '' })
},
onCustomInput: function (e) {
var val = parseInt(e.detail.value) || 0
this.setData({ customInput: e.detail.value, customDuration: val })
},
onConfirmFree: function () {
var dur = this.data.customDuration
if (!dur || dur <= 0) {
wx.showToast({ title: '请输入有效时长', icon: 'none' })
return
}
this.setData({ showPicker: false })
wx.navigateTo({ url: '/pages/timer/timer?free=' + dur })
},
noop: function () {}
})
+4
View File
@@ -0,0 +1,4 @@
{
"usingComponents": {},
"navigationBarTitleText": "平板支撑训练"
}
+113
View File
@@ -0,0 +1,113 @@
<view class="container" style="{{themeStyle}}">
<!-- 顶部问候 -->
<view class="greeting-row">
<view class="greeting-text">
<text class="greeting-emoji">{{todayDone ? '💪' : '👋'}}</text>
<text class="greeting-title">{{todayDone ? '太棒了!' : '准备好了吗?'}}</text>
</view>
<text class="greeting-sub">{{todayDone ? '继续保持这个势头' : '今天的平板支撑等着你'}}</text>
</view>
<!-- 连续打卡卡片 -->
<view class="header-card card">
<view class="streak-section">
<text class="streak-icon">🔥</text>
<text class="streak-num">{{streakCount}}</text>
<text class="streak-label">连续打卡</text>
</view>
<view class="divider"></view>
<view class="plan-info">
<view class="plan-badge">
<text class="plan-badge-icon">📋</text>
<text class="plan-badge-text">{{planName}}</text>
</view>
<view class="progress-bar-wrap">
<view class="progress-bar">
<view class="progress-fill" style="width: {{planProgress}}%;">
<view class="progress-glow"></view>
</view>
</view>
<text class="progress-label">第 {{planDay}} / {{planTotal}} 天</text>
</view>
</view>
</view>
<!-- 今日目标卡片 -->
<view class="target-section card">
<view class="target-header">
<text class="target-icon">🎯</text>
<text class="section-title">今日目标</text>
</view>
<view class="target-display">
<view class="target-ring">
<text class="target-time">{{todayTargetText}}</text>
</view>
<text class="target-unit">平板支撑</text>
</view>
<view class="today-status">
<block wx:if="{{todayDone}}">
<view class="done-badge">
<text class="done-emoji">✨</text>
<text class="done-text">今日已完成 {{todayDuration}} 秒</text>
</view>
</block>
<block wx:else>
<text class="pending-text">⏳ 还未开始训练</text>
</block>
</view>
</view>
<!-- 操作按钮区 -->
<view class="action-buttons">
<button class="start-btn btn-primary" bindtap="onStartTrain">
<text class="btn-icon">{{todayDone ? '🔄' : '▶'}}</text>
<text>{{todayDone ? '再次训练' : '开始训练'}}</text>
</button>
<button class="free-btn" bindtap="onFreeTrain">
<text class="free-icon">⏱</text>
<text>自由训练 · 自定义时长</text>
</button>
</view>
<!-- 小提示 -->
<view class="tip-card">
<text class="tip-icon">💡</text>
<text class="tip-text">保持身体成一条直线,核心收紧,均匀呼吸</text>
</view>
</view>
<!-- 时间选择弹窗 -->
<view class="modal-overlay" wx:if="{{showPicker}}" bindtap="onClosePicker">
<view class="picker-modal" catchtap="noop">
<text class="picker-title">⏱ 选择训练时长</text>
<view class="preset-grid">
<view
class="preset-item {{customDuration === item ? 'selected' : ''}}"
wx:for="{{presets}}"
wx:key="*this"
data-value="{{item}}"
bindtap="onSelectPreset"
>
<text class="preset-num">{{item}}</text>
<text class="preset-unit">秒</text>
</view>
</view>
<view class="custom-input-row">
<text class="custom-label">自定义:</text>
<input
class="custom-input"
type="number"
placeholder="输入秒数"
value="{{customInput}}"
bindinput="onCustomInput"
/>
<text class="custom-label">秒</text>
</view>
<button class="picker-confirm btn-primary" bindtap="onConfirmFree">
开始自由训练
</button>
<text class="picker-cancel" bindtap="onClosePicker">取消</text>
</view>
</view>
+414
View File
@@ -0,0 +1,414 @@
/* ---- greeting ---- */
.greeting-row {
padding: 16rpx 8rpx 24rpx;
animation: cardIn 0.4s ease both;
}
.greeting-text {
display: flex;
align-items: center;
margin-bottom: 8rpx;
}
.greeting-emoji {
font-size: 40rpx;
margin-right: 12rpx;
line-height: 1;
}
.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: 56rpx;
line-height: 1;
}
/* ---- header card ---- */
.header-card {
display: flex;
align-items: center;
padding: 36rpx 32rpx;
}
.streak-section {
display: flex;
flex-direction: column;
align-items: center;
min-width: 140rpx;
}
.streak-icon {
font-size: 40rpx;
margin-bottom: 4rpx;
line-height: 1;
animation: float 2.5s ease-in-out infinite;
}
.streak-num {
font-size: 56rpx;
font-weight: 700;
color: var(--primary);
line-height: 1.1;
}
.streak-label {
font-size: 22rpx;
color: var(--text-secondary);
margin-top: 4rpx;
line-height: 1;
}
.divider {
width: 2rpx;
height: 80rpx;
background: var(--border);
margin: 0 28rpx;
}
.plan-info {
flex: 1;
display: flex;
flex-direction: column;
}
.plan-badge {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.plan-badge-icon {
font-size: 28rpx;
margin-right: 8rpx;
line-height: 1;
}
.plan-badge-text {
font-size: 26rpx;
font-weight: 600;
color: var(--text);
line-height: 1;
}
.progress-bar-wrap {
display: flex;
flex-direction: column;
}
.progress-bar {
width: 100%;
height: 8rpx;
background: var(--border);
border-radius: 4rpx;
overflow: visible;
margin-bottom: 8rpx;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary), var(--primary-light));
border-radius: 4rpx;
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.progress-glow {
position: absolute;
right: -4rpx;
top: -4rpx;
width: 16rpx;
height: 16rpx;
background: var(--primary);
border-radius: 50%;
box-shadow: 0 0 10rpx var(--primary);
}
.progress-label {
font-size: 22rpx;
color: var(--text-secondary);
line-height: 1;
}
/* ---- target card ---- */
.target-section {
text-align: center;
padding: 36rpx 32rpx;
}
.target-header {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8rpx;
}
.target-header .section-title {
margin-bottom: 0;
}
.target-icon {
font-size: 28rpx;
margin-right: 8rpx;
line-height: 1;
}
.target-display {
margin: 20rpx 0;
}
.target-ring {
display: inline-flex;
align-items: center;
justify-content: center;
width: 200rpx;
height: 200rpx;
border-radius: 50%;
background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.08), rgba(var(--primary-rgb), 0.02));
border: 4rpx solid rgba(var(--primary-rgb), 0.15);
margin-bottom: 16rpx;
}
.target-time {
font-size: 64rpx;
font-weight: 700;
color: var(--primary);
font-variant-numeric: tabular-nums;
line-height: 1;
}
.target-unit {
display: block;
font-size: 26rpx;
color: var(--text-secondary);
margin-top: 4rpx;
line-height: 1;
}
.today-status {
margin-top: 16rpx;
}
.done-badge {
display: inline-flex;
align-items: center;
background: rgba(76, 175, 80, 0.08);
padding: 10rpx 24rpx;
border-radius: 24rpx;
}
.done-emoji {
font-size: 28rpx;
margin-right: 6rpx;
line-height: 1;
}
.done-text {
font-size: 26rpx;
color: var(--success);
line-height: 1;
}
.pending-text {
font-size: 26rpx;
color: #CCC;
line-height: 1;
}
/* ---- action buttons ---- */
.action-buttons {
margin-top: 16rpx;
}
.start-btn {
width: 100%;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10rpx;
padding: 24rpx 0;
}
.btn-icon {
font-size: 28rpx;
line-height: 1;
}
.free-btn {
width: 100%;
margin-top: 20rpx;
background: transparent;
border: 2rpx solid rgba(var(--primary-rgb), 0.3);
color: var(--text-secondary);
border-radius: 48rpx;
font-size: 28rpx;
padding: 20rpx 0;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8rpx;
}
.free-btn::after {
border: none;
}
.free-btn:active {
background: var(--primary-bg);
border-color: var(--primary);
color: var(--primary);
}
.free-icon {
font-size: 28rpx;
line-height: 1;
}
/* ---- tip card ---- */
.tip-card {
display: flex;
align-items: flex-start;
padding: 24rpx 32rpx;
margin-top: 32rpx;
background: rgba(var(--primary-rgb), 0.04);
border-radius: 20rpx;
animation: cardIn 0.45s 0.4s ease both;
}
.tip-icon {
font-size: 28rpx;
margin-right: 10rpx;
flex-shrink: 0;
line-height: 1.6;
}
.tip-text {
font-size: 28rpx;
color: var(--text-secondary);
line-height: 1.6;
}
/* ---- picker modal ---- */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 1000;
display: flex;
align-items: flex-end;
justify-content: center;
}
.picker-modal {
width: 100%;
background: #fff;
border-radius: 32rpx 32rpx 0 0;
padding: 40rpx 32rpx;
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
animation: slideUp 0.3s cubic-bezier(0.32, 0.72, 0, 1);
}
@keyframes slideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
.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);
background: #F5F5F5;
border-radius: 16rpx;
padding: 20rpx 0;
text-align: center;
display: flex;
flex-direction: column;
}
.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: #666;
line-height: 1;
}
.custom-input {
width: 160rpx;
height: 64rpx;
background: #F5F5F5;
border-radius: 12rpx;
text-align: center;
font-size: 32rpx;
font-weight: 600;
color: var(--primary);
margin: 0 16rpx;
}
.picker-confirm {
width: 100%;
margin-bottom: 16rpx;
}
.picker-cancel {
display: block;
text-align: center;
font-size: 28rpx;
color: var(--text-secondary);
padding: 12rpx 0;
}