backup: snapshot before optimization
This commit is contained in:
@@ -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 () {}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationBarTitleText": "平板支撑训练"
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
var storage = require('../../utils/storage')
|
||||
var util = require('../../utils/util')
|
||||
var themeMod = require('../../utils/theme')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
theme: { primary: '#FF6B35', primaryLight: '#FF8C5A', primaryBg: '#FFF3ED', primaryRgb: '255,107,53' },
|
||||
themeStyle: '',
|
||||
totalDuration: 0,
|
||||
totalDurationText: '',
|
||||
totalSessions: 0,
|
||||
maxDuration: 0,
|
||||
maxDurationText: '',
|
||||
currentYear: new Date().getFullYear(),
|
||||
currentMonth: new Date().getMonth() + 1,
|
||||
monthRecords: [],
|
||||
historyList: [],
|
||||
showDayDetail: false,
|
||||
dayDetail: {}
|
||||
},
|
||||
|
||||
onLoad: function () {
|
||||
this.refresh()
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
themeMod.applyThemeToPage(this)
|
||||
try {
|
||||
var tb = this.getTabBar()
|
||||
if (tb) tb.setData({ selected: 1 })
|
||||
} catch (e) {}
|
||||
this.refresh()
|
||||
},
|
||||
|
||||
onPullDownRefresh: function () {
|
||||
this.refresh()
|
||||
wx.stopPullDownRefresh()
|
||||
},
|
||||
|
||||
refresh: function () {
|
||||
var stats = storage.getTotalStats()
|
||||
var monthKey = this.data.currentYear + '-' +
|
||||
(this.data.currentMonth < 10 ? '0' : '') + this.data.currentMonth
|
||||
var records = storage.getRecordsByMonth(monthKey)
|
||||
|
||||
var allRecords = []
|
||||
var all = storage.getRecords()
|
||||
Object.keys(all).forEach(function (key) {
|
||||
allRecords = allRecords.concat(all[key])
|
||||
})
|
||||
allRecords.sort(function (a, b) { return b.date.localeCompare(a.date) })
|
||||
|
||||
this.setData({
|
||||
totalDuration: stats.totalDuration,
|
||||
totalDurationText: util.formatDuration(stats.totalDuration),
|
||||
totalSessions: stats.totalSessions,
|
||||
maxDuration: stats.maxDuration,
|
||||
maxDurationText: util.formatDuration(stats.maxDuration),
|
||||
monthRecords: records,
|
||||
historyList: allRecords.slice(0, 30)
|
||||
})
|
||||
},
|
||||
|
||||
onPrevMonth: function () {
|
||||
var year = this.data.currentYear
|
||||
var month = this.data.currentMonth
|
||||
if (month === 1) {
|
||||
year--
|
||||
month = 12
|
||||
} else {
|
||||
month--
|
||||
}
|
||||
this.setData({ currentYear: year, currentMonth: month })
|
||||
this.refresh()
|
||||
},
|
||||
|
||||
onNextMonth: function () {
|
||||
var year = this.data.currentYear
|
||||
var month = this.data.currentMonth
|
||||
if (month === 12) {
|
||||
year++
|
||||
month = 1
|
||||
} else {
|
||||
month++
|
||||
}
|
||||
this.setData({ currentYear: year, currentMonth: month })
|
||||
this.refresh()
|
||||
},
|
||||
|
||||
onDayTap: function (e) {
|
||||
var detail = e.detail
|
||||
var dateStr = detail.year + '-' +
|
||||
(detail.month < 10 ? '0' : '') + detail.month + '-' +
|
||||
(detail.day < 10 ? '0' : '') + detail.day
|
||||
|
||||
var allRecords = []
|
||||
var all = storage.getRecords()
|
||||
Object.keys(all).forEach(function (key) {
|
||||
allRecords = allRecords.concat(all[key])
|
||||
})
|
||||
|
||||
var sessions = allRecords.filter(function (r) { return r.date === dateStr })
|
||||
var totalDur = 0
|
||||
sessions.forEach(function (r) { totalDur += r.duration })
|
||||
var calories = Math.round(totalDur * 0.068)
|
||||
|
||||
this.setData({
|
||||
showDayDetail: true,
|
||||
dayDetail: {
|
||||
date: dateStr,
|
||||
sessions: sessions.length,
|
||||
duration: totalDur,
|
||||
durationText: util.formatDuration(totalDur),
|
||||
calories: calories
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
onCloseDayDetail: function () {
|
||||
this.setData({ showDayDetail: false })
|
||||
},
|
||||
|
||||
noop: function () {},
|
||||
|
||||
onDeleteRecord: function (e) {
|
||||
var id = e.currentTarget.dataset.id
|
||||
var self = this
|
||||
wx.showModal({
|
||||
title: '删除记录',
|
||||
content: '确定要删除这条训练记录吗?',
|
||||
success: function (res) {
|
||||
if (res.confirm) {
|
||||
storage.deleteRecord(id)
|
||||
self.refresh()
|
||||
wx.showToast({ title: '已删除', icon: 'none', duration: 1200 })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"calendar-heatmap": "../../components/calendar-heatmap/calendar-heatmap"
|
||||
},
|
||||
"navigationBarTitleText": "训练记录"
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<view class="container" style="{{themeStyle}}">
|
||||
<!-- 统计卡片 -->
|
||||
<view class="stats card">
|
||||
<view class="stat-item">
|
||||
<text class="stat-emoji">📊</text>
|
||||
<text class="stat-num">{{totalSessions}}次</text>
|
||||
<text class="stat-label">训练次数</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-emoji">⏱</text>
|
||||
<text class="stat-num">{{totalDurationText}}</text>
|
||||
<text class="stat-label">累计时长</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-emoji">🏆</text>
|
||||
<text class="stat-num">{{maxDurationText}}</text>
|
||||
<text class="stat-label">最长记录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 日历热力图 -->
|
||||
<view class="calendar-section card">
|
||||
<view class="month-picker">
|
||||
<view class="month-arrow" bindtap="onPrevMonth">‹</view>
|
||||
<view class="month-title-wrap">
|
||||
<text class="month-emoji">🗓</text>
|
||||
<text class="month-title">{{currentYear}}年 {{currentMonth}}月</text>
|
||||
</view>
|
||||
<view class="month-arrow" bindtap="onNextMonth">›</view>
|
||||
</view>
|
||||
<calendar-heatmap
|
||||
year="{{currentYear}}"
|
||||
month="{{currentMonth}}"
|
||||
records="{{monthRecords}}"
|
||||
binddaytap="onDayTap"
|
||||
></calendar-heatmap>
|
||||
</view>
|
||||
|
||||
<!-- 历史记录 -->
|
||||
<view class="history-section card">
|
||||
<view class="history-header">
|
||||
<text class="section-title">📋 最近记录</text>
|
||||
</view>
|
||||
<view wx:if="{{historyList.length === 0}}" class="empty-state">
|
||||
<text class="empty-emoji">🏃</text>
|
||||
<text class="empty-text">还没有训练记录</text>
|
||||
<text class="empty-sub">开始你的第一次平板支撑吧</text>
|
||||
</view>
|
||||
<view class="history-list">
|
||||
<view class="history-item" wx:for="{{historyList}}" wx:key="id" data-id="{{item.id}}" bindlongpress="onDeleteRecord">
|
||||
<text class="history-icon">
|
||||
{{item.duration >= 120 ? '🔥' : item.duration >= 60 ? '💪' : '✅'}}
|
||||
</text>
|
||||
<view class="history-left">
|
||||
<text class="history-date">{{item.date}}</text>
|
||||
<text class="history-plan">第 {{item.day}} 天</text>
|
||||
</view>
|
||||
<view class="history-right">
|
||||
<text class="history-duration">{{item.duration}}s</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 日期详情弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showDayDetail}}" bindtap="onCloseDayDetail">
|
||||
<view class="day-detail-modal" catchtap="noop">
|
||||
<view class="detail-date-row">
|
||||
<text class="detail-emoji">📅</text>
|
||||
<text class="detail-date">{{dayDetail.date}}</text>
|
||||
</view>
|
||||
<view class="detail-grid">
|
||||
<view class="detail-item">
|
||||
<text class="detail-num">{{dayDetail.sessions}}次</text>
|
||||
<text class="detail-label">训练次数</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-num">{{dayDetail.durationText}}</text>
|
||||
<text class="detail-label">总时长</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-num">{{dayDetail.calories}}</text>
|
||||
<text class="detail-label">约消耗 (千卡)</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="detail-close" bindtap="onCloseDayDetail">
|
||||
<text>关闭</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,230 @@
|
||||
.stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
text-align: center;
|
||||
padding: 32rpx 16rpx;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-emoji {
|
||||
font-size: 28rpx;
|
||||
width: 56rpx;
|
||||
height: 40rpx;
|
||||
line-height: 40rpx;
|
||||
text-align: center;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.stat-num {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
animation: popIn 0.5s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 4rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ---- calendar ---- */
|
||||
.calendar-section {
|
||||
padding: 24rpx 24rpx 28rpx;
|
||||
}
|
||||
|
||||
.month-picker {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.month-title-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.month-emoji {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.month-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.month-arrow {
|
||||
font-size: 44rpx;
|
||||
color: var(--primary);
|
||||
padding: 0 20rpx;
|
||||
font-weight: 300;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.month-arrow:active {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* ---- history ---- */
|
||||
.history-header {
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 60rpx 0;
|
||||
}
|
||||
|
||||
.empty-emoji {
|
||||
font-size: 64rpx;
|
||||
margin-bottom: 16rpx;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: #CCC;
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.empty-sub {
|
||||
font-size: 24rpx;
|
||||
color: #E0E0E0;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid var(--border);
|
||||
animation: cardIn 0.35s ease both;
|
||||
}
|
||||
|
||||
.history-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.history-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.history-date {
|
||||
font-size: 28rpx;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.history-plan {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.history-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.history-duration {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* ---- day detail popup ---- */
|
||||
.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: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.day-detail-modal {
|
||||
width: 560rpx;
|
||||
background: #fff;
|
||||
border-radius: 28rpx;
|
||||
padding: 48rpx 40rpx 36rpx;
|
||||
animation: popIn 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.detail-date-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 36rpx;
|
||||
}
|
||||
|
||||
.detail-emoji {
|
||||
font-size: 32rpx;
|
||||
margin-right: 10rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.detail-date {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-bottom: 36rpx;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.detail-num {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.detail-close {
|
||||
padding: 16rpx 0;
|
||||
font-size: 28rpx;
|
||||
color: var(--text-secondary);
|
||||
border-top: 1rpx solid var(--border);
|
||||
}
|
||||
|
||||
.history-icon {
|
||||
font-size: 28rpx;
|
||||
margin-right: 16rpx;
|
||||
flex-shrink: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
var storage = require('../../utils/storage')
|
||||
var themeMod = require('../../utils/theme')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
theme: { primary: '#FF6B35', primaryLight: '#FF8C5A', primaryBg: '#FFF3ED', primaryRgb: '255,107,53' },
|
||||
themeStyle: '',
|
||||
themes: themeMod.THEMES,
|
||||
currentThemeId: 'orange',
|
||||
plans: [
|
||||
{ id: 'beginner', name: '初级', desc: '7天计划 · 30秒起步', days: 7 },
|
||||
{ id: 'intermediate', name: '中级', desc: '14天计划 · 60秒起步', days: 14 },
|
||||
{ id: 'advanced', name: '高级', desc: '30天计划 · 90秒起步', days: 30 }
|
||||
],
|
||||
currentPlanId: 'beginner',
|
||||
dailyReminder: true,
|
||||
reminderTime: '08:00',
|
||||
voiceGuide: true,
|
||||
vibrate: true
|
||||
},
|
||||
|
||||
onLoad: function () {
|
||||
var s = storage.getSettings()
|
||||
var theme = themeMod.getCurrentTheme()
|
||||
themeMod.applyThemeToPage(this)
|
||||
this.setData({
|
||||
currentPlanId: s.planId,
|
||||
dailyReminder: s.dailyReminder,
|
||||
reminderTime: s.reminderTime,
|
||||
voiceGuide: s.voiceGuide,
|
||||
vibrate: s.vibrate,
|
||||
currentThemeId: theme.id
|
||||
})
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
try {
|
||||
var tb = this.getTabBar()
|
||||
if (tb) tb.setData({ selected: 2 })
|
||||
} catch (e) {}
|
||||
themeMod.applyThemeToPage(this)
|
||||
},
|
||||
|
||||
onSelectTheme: function (e) {
|
||||
var id = e.currentTarget.dataset.id
|
||||
var theme = themeMod.setTheme(id)
|
||||
this.setData({ currentThemeId: id })
|
||||
|
||||
try {
|
||||
var tb = this.getTabBar()
|
||||
if (tb) {
|
||||
tb.setData({ themeStyle: themeMod.getThemeStyle(theme) })
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
themeMod.applyThemeToPage(this)
|
||||
},
|
||||
|
||||
onSelectPlan: function (e) {
|
||||
var planId = e.currentTarget.dataset.id
|
||||
var s = storage.getSettings()
|
||||
s.planId = planId
|
||||
storage.saveSettings(s)
|
||||
this.setData({ currentPlanId: planId })
|
||||
wx.showToast({ title: '计划已切换', icon: 'success', duration: 1200 })
|
||||
},
|
||||
|
||||
onToggleReminder: function (e) {
|
||||
var s = storage.getSettings()
|
||||
s.dailyReminder = e.detail.value
|
||||
storage.saveSettings(s)
|
||||
this.setData({ dailyReminder: e.detail.value })
|
||||
},
|
||||
|
||||
onReminderTimeChange: function (e) {
|
||||
var s = storage.getSettings()
|
||||
s.reminderTime = e.detail.value
|
||||
storage.saveSettings(s)
|
||||
this.setData({ reminderTime: e.detail.value })
|
||||
},
|
||||
|
||||
onToggleVoice: function (e) {
|
||||
var s = storage.getSettings()
|
||||
s.voiceGuide = e.detail.value
|
||||
storage.saveSettings(s)
|
||||
this.setData({ voiceGuide: e.detail.value })
|
||||
},
|
||||
|
||||
onToggleVibrate: function (e) {
|
||||
var s = storage.getSettings()
|
||||
s.vibrate = e.detail.value
|
||||
storage.saveSettings(s)
|
||||
this.setData({ vibrate: e.detail.value })
|
||||
},
|
||||
|
||||
onCopyWechat: function () {
|
||||
wx.setClipboardData({
|
||||
data: '刘承',
|
||||
success: function () {
|
||||
wx.showToast({ title: '已复制', icon: 'success', duration: 1500 })
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationBarTitleText": "设置"
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<view class="container" style="{{themeStyle}}">
|
||||
<!-- 配色方案 -->
|
||||
<view class="card">
|
||||
<view class="section-header">
|
||||
<text class="section-emoji">🎨</text>
|
||||
<text class="section-title">配色方案</text>
|
||||
</view>
|
||||
<view class="theme-list">
|
||||
<view
|
||||
class="theme-item {{currentThemeId === item.id ? 'active' : ''}}"
|
||||
wx:for="{{themes}}"
|
||||
wx:key="id"
|
||||
data-id="{{item.id}}"
|
||||
bindtap="onSelectTheme"
|
||||
>
|
||||
<view class="theme-color-dot" style="background: {{item.primary}};"></view>
|
||||
<text class="theme-name">{{item.name}}</text>
|
||||
<text class="plan-check" wx:if="{{currentThemeId === item.id}}">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 训练计划选择 -->
|
||||
<view class="card">
|
||||
<view class="section-header">
|
||||
<text class="section-emoji">📋</text>
|
||||
<text class="section-title">训练计划</text>
|
||||
</view>
|
||||
<view class="plan-list">
|
||||
<view
|
||||
class="plan-item {{currentPlanId === item.id ? 'active' : ''}}"
|
||||
wx:for="{{plans}}"
|
||||
wx:key="id"
|
||||
data-id="{{item.id}}"
|
||||
bindtap="onSelectPlan"
|
||||
>
|
||||
<view class="plan-radio">
|
||||
<view class="radio-dot" wx:if="{{currentPlanId === item.id}}"></view>
|
||||
</view>
|
||||
<view class="plan-content">
|
||||
<text class="plan-item-name">{{item.name}}</text>
|
||||
<text class="plan-item-desc">{{item.desc}}</text>
|
||||
</view>
|
||||
<text class="plan-check" wx:if="{{currentPlanId === item.id}}">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提醒设置 -->
|
||||
<view class="card">
|
||||
<view class="section-header">
|
||||
<text class="section-emoji">🔔</text>
|
||||
<text class="section-title">提醒</text>
|
||||
</view>
|
||||
<view class="setting-row">
|
||||
<view class="setting-left">
|
||||
<text class="setting-icon">📅</text>
|
||||
<text class="setting-label">每日提醒</text>
|
||||
</view>
|
||||
<switch checked="{{dailyReminder}}" bindchange="onToggleReminder" color="{{theme.primary}}"/>
|
||||
</view>
|
||||
<view class="setting-row" wx:if="{{dailyReminder}}">
|
||||
<view class="setting-left">
|
||||
<text class="setting-icon">🕐</text>
|
||||
<text class="setting-label">提醒时间</text>
|
||||
</view>
|
||||
<picker mode="time" value="{{reminderTime}}" bindchange="onReminderTimeChange">
|
||||
<text class="setting-value">{{reminderTime}}</text>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 训练偏好 -->
|
||||
<view class="card">
|
||||
<view class="section-header">
|
||||
<text class="section-emoji">⚙</text>
|
||||
<text class="section-title">训练偏好</text>
|
||||
</view>
|
||||
<view class="setting-row">
|
||||
<view class="setting-left">
|
||||
<text class="setting-icon">🔊</text>
|
||||
<text class="setting-label">语音引导</text>
|
||||
</view>
|
||||
<switch checked="{{voiceGuide}}" bindchange="onToggleVoice" color="{{theme.primary}}"/>
|
||||
</view>
|
||||
<view class="setting-row">
|
||||
<view class="setting-left">
|
||||
<text class="setting-icon">📳</text>
|
||||
<text class="setting-label">振动反馈</text>
|
||||
</view>
|
||||
<switch checked="{{vibrate}}" bindchange="onToggleVibrate" color="{{theme.primary}}"/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 关于 -->
|
||||
<view class="card about-card">
|
||||
<view class="section-header">
|
||||
<text class="section-emoji">ℹ</text>
|
||||
<text class="section-title">关于</text>
|
||||
</view>
|
||||
<view class="about-list">
|
||||
<view class="about-row">
|
||||
<text class="about-label">版本</text>
|
||||
<text class="about-value">v1.0.0</text>
|
||||
</view>
|
||||
<view class="about-row" bindtap="onCopyWechat">
|
||||
<text class="about-label">开发者</text>
|
||||
<text class="about-value about-link">刘承</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="about-footer">
|
||||
<text class="about-copy">Made with ❤️ for better fitness</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,220 @@
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.section-emoji {
|
||||
font-size: 28rpx;
|
||||
margin-right: 10rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.section-header .section-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* ---- theme picker ---- */
|
||||
.theme-list {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.theme-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 22rpx 0;
|
||||
border-bottom: 1rpx solid var(--border);
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.theme-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.theme-item.active {
|
||||
background: var(--primary-bg);
|
||||
margin: 0 -32rpx;
|
||||
padding-left: 32rpx;
|
||||
padding-right: 32rpx;
|
||||
border-radius: 16rpx;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.theme-item.active + .theme-item {
|
||||
border-top: 1rpx solid transparent;
|
||||
}
|
||||
|
||||
.theme-color-dot {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 50%;
|
||||
margin-right: 20rpx;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.15);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.theme-item:active .theme-color-dot {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
.theme-name {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ---- plan list ---- */
|
||||
.plan-list {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.plan-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid var(--border);
|
||||
}
|
||||
|
||||
.plan-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.plan-item.active {
|
||||
background: var(--primary-bg);
|
||||
margin: 0 -32rpx;
|
||||
padding-left: 32rpx;
|
||||
padding-right: 32rpx;
|
||||
border-radius: 16rpx;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.plan-radio {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border: 3rpx solid #DDD;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
flex-shrink: 0;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.plan-item.active .plan-radio {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.radio-dot {
|
||||
width: 22rpx;
|
||||
height: 22rpx;
|
||||
background: var(--primary);
|
||||
border-radius: 50%;
|
||||
animation: popIn 0.3s ease;
|
||||
}
|
||||
|
||||
.plan-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.plan-item-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.plan-item-desc {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.plan-check {
|
||||
color: var(--primary);
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ---- setting rows ---- */
|
||||
.setting-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid var(--border);
|
||||
}
|
||||
|
||||
.setting-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.setting-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.setting-icon {
|
||||
font-size: 28rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
font-size: 28rpx;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.setting-value {
|
||||
font-size: 28rpx;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* ---- about ---- */
|
||||
.about-card {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.about-list {
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.about-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 18rpx 0;
|
||||
border-bottom: 1rpx solid var(--border);
|
||||
}
|
||||
|
||||
.about-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.about-label {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.about-value {
|
||||
font-size: 26rpx;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.about-link {
|
||||
color: var(--primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.about-footer {
|
||||
margin-top: 24rpx;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx solid var(--border);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.about-copy {
|
||||
font-size: 22rpx;
|
||||
color: #CCCCCC;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
var Timer = require('../../utils/timer')
|
||||
var storage = require('../../utils/storage')
|
||||
var planMod = require('../../utils/plan')
|
||||
var themeMod = require('../../utils/theme')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
theme: { primary: '#FF6B35', primaryLight: '#FF8C5A', primaryBg: '#FFF3ED', primaryRgb: '255,107,53' },
|
||||
themeStyle: '',
|
||||
duration: 60,
|
||||
remaining: 60,
|
||||
status: 'idle',
|
||||
isRunning: false,
|
||||
isPaused: false,
|
||||
isCompleted: false,
|
||||
overtime: 0,
|
||||
todayTarget: 0,
|
||||
planDay: 1,
|
||||
isFreeMode: false
|
||||
},
|
||||
|
||||
onLoad: function (options) {
|
||||
themeMod.applyThemeToPage(this)
|
||||
|
||||
var target
|
||||
var planDay = 1
|
||||
var isFreeMode = false
|
||||
|
||||
if (options && options.free) {
|
||||
target = parseInt(options.free) || 60
|
||||
isFreeMode = true
|
||||
} else {
|
||||
var settings = storage.getSettings()
|
||||
var records = []
|
||||
var allRecords = storage.getRecords()
|
||||
Object.keys(allRecords).forEach(function (key) {
|
||||
records = records.concat(allRecords[key])
|
||||
})
|
||||
|
||||
var plan = planMod.getPlan(settings.planId)
|
||||
planDay = planMod.getPlanDay(settings.planId, records)
|
||||
target = planMod.getTodayTarget(settings.planId, Math.min(planDay, plan.totalDays))
|
||||
}
|
||||
|
||||
this.setData({
|
||||
duration: target,
|
||||
remaining: target,
|
||||
todayTarget: target,
|
||||
planDay: isFreeMode ? 0 : Math.min(planDay, 99),
|
||||
isFreeMode: isFreeMode
|
||||
})
|
||||
|
||||
var self = this
|
||||
this._timer = new Timer({
|
||||
onTick: function (tick) {
|
||||
self.setData({
|
||||
remaining: tick.remaining > 0 ? tick.remaining : 0,
|
||||
overtime: tick.remaining <= 0 ? tick.elapsed - self.data.duration : 0
|
||||
})
|
||||
},
|
||||
onComplete: function () {
|
||||
self.setData({ status: 'completed', isCompleted: true })
|
||||
if (storage.getSettings().vibrate) {
|
||||
try { wx.vibrateLong() } catch (e) {}
|
||||
}
|
||||
wx.showToast({ title: '目标完成! 太棒了!', icon: 'success', duration: 2000 })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
onUnload: function () {
|
||||
if (this._timer) this._timer.stop()
|
||||
},
|
||||
|
||||
onStart: function () {
|
||||
if (this.data.isPaused) {
|
||||
this._timer.resume()
|
||||
this.setData({ isRunning: true, isPaused: false, status: 'running' })
|
||||
return
|
||||
}
|
||||
if (this.data.isRunning) return
|
||||
this._timer.start(this.data.duration)
|
||||
this.setData({ isRunning: true, status: 'running' })
|
||||
},
|
||||
|
||||
onPause: function () {
|
||||
if (!this.data.isRunning || this.data.isPaused) return
|
||||
this._timer.pause()
|
||||
this.setData({ isPaused: true, status: 'paused' })
|
||||
},
|
||||
|
||||
onStop: function () {
|
||||
var self = this
|
||||
wx.showModal({
|
||||
title: '结束训练',
|
||||
content: '确定要结束本次训练吗?',
|
||||
success: function (res) {
|
||||
if (res.confirm) self.finishTraining()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
finishTraining: function () {
|
||||
var elapsed = this._timer.stop()
|
||||
var today = storage.getToday()
|
||||
storage.saveRecord({
|
||||
date: today,
|
||||
duration: elapsed,
|
||||
planId: storage.getSettings().planId,
|
||||
day: this.data.planDay
|
||||
})
|
||||
storage.updateStreak(today)
|
||||
|
||||
wx.showToast({ title: '已记录 ' + elapsed + '秒', icon: 'none', duration: 1500 })
|
||||
setTimeout(function () { wx.navigateBack() }, 1500)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"progress-ring": "../../components/progress-ring/progress-ring"
|
||||
},
|
||||
"navigationBarTitleText": "训练中"
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<view class="container timer-page" style="{{themeStyle}}">
|
||||
|
||||
<!-- 呼吸引导环 + 进度条 -->
|
||||
<view class="ring-wrapper">
|
||||
<view class="breathe-ring breathe-ring-1 {{status === 'running' ? 'fast' : ''}}"
|
||||
wx:if="{{status !== 'completed'}}"></view>
|
||||
<view class="breathe-ring breathe-ring-2 {{status === 'running' ? 'fast' : ''}}"
|
||||
wx:if="{{status !== 'completed'}}"></view>
|
||||
<view class="breathe-ring breathe-ring-3 {{status === 'running' ? 'fast' : ''}}"
|
||||
wx:if="{{status !== 'completed'}}"></view>
|
||||
|
||||
<progress-ring
|
||||
duration="{{duration}}"
|
||||
remaining="{{isCompleted ? 0 : remaining}}"
|
||||
size="{{360}}"
|
||||
ringWidth="{{16}}"
|
||||
status="{{status}}"
|
||||
primaryColor="{{theme.primary}}"
|
||||
primaryLightColor="{{theme.primaryLight}}"
|
||||
></progress-ring>
|
||||
|
||||
<!-- 完成庆祝粒子 -->
|
||||
<view class="celebrate-burst" wx:if="{{isCompleted}}">
|
||||
<view class="spark s1">✨</view>
|
||||
<view class="spark s2">⭐</view>
|
||||
<view class="spark s3">💫</view>
|
||||
<view class="spark s4">🌟</view>
|
||||
<view class="spark s5">✨</view>
|
||||
<view class="spark s6">💪</view>
|
||||
</view>
|
||||
|
||||
<view class="overtime-badge" wx:if="{{isCompleted && overtime > 0}}">
|
||||
<text class="overtime-icon">🔥</text>
|
||||
<text>+{{overtime}}s</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 状态提示 -->
|
||||
<view class="hint-section">
|
||||
<view class="hint-row" wx:if="{{status === 'idle'}}">
|
||||
<text class="hint-emoji">🧘</text>
|
||||
<text class="hint-text">
|
||||
<block wx:if="{{isFreeMode}}">自由训练 · 目标 {{duration}} 秒</block>
|
||||
<block wx:else>今日目标 {{duration}} 秒 · 第 {{planDay}} 天</block>
|
||||
</text>
|
||||
</view>
|
||||
<view class="hint-row" wx:elif="{{status === 'running'}}">
|
||||
<text class="hint-emoji running-pulse">💪</text>
|
||||
<text class="hint-text running">保持姿势,核心收紧!</text>
|
||||
</view>
|
||||
<view class="hint-row" wx:elif="{{status === 'paused'}}">
|
||||
<text class="hint-emoji">⏸</text>
|
||||
<text class="hint-text paused">已暂停</text>
|
||||
</view>
|
||||
<view class="hint-row completed-hint" wx:elif="{{status === 'completed'}}">
|
||||
<text class="hint-emoji completed-bounce">🎉</text>
|
||||
<text class="hint-text completed">目标完成! 继续坚持!</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 控制按钮 -->
|
||||
<view class="controls">
|
||||
<button
|
||||
wx:if="{{!isRunning && !isPaused}}"
|
||||
class="ctrl-btn start-btn"
|
||||
bindtap="onStart"
|
||||
>
|
||||
<text class="ctrl-icon">▶</text>
|
||||
<text>开始</text>
|
||||
</button>
|
||||
|
||||
<view class="ctrl-row" wx:if="{{isRunning || isPaused}}">
|
||||
<button
|
||||
wx:if="{{!isPaused}}"
|
||||
class="ctrl-btn pause-btn"
|
||||
bindtap="onPause"
|
||||
>
|
||||
<text class="ctrl-icon">⏸</text>
|
||||
<text>暂停</text>
|
||||
</button>
|
||||
<button
|
||||
wx:if="{{isPaused}}"
|
||||
class="ctrl-btn resume-btn"
|
||||
bindtap="onStart"
|
||||
>
|
||||
<text class="ctrl-icon">▶</text>
|
||||
<text>继续</text>
|
||||
</button>
|
||||
<button class="ctrl-btn stop-btn" bindtap="onStop">
|
||||
<text class="ctrl-icon">⏹</text>
|
||||
<text>结束</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,245 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
/* ---- ring wrapper ---- */
|
||||
.ring-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* breathing guide rings — three concentric layers */
|
||||
.breathe-ring {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
border: 3rpx solid rgba(var(--primary-rgb), 0.15);
|
||||
animation: breathePulse 3.5s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.breathe-ring-1 {
|
||||
width: 480rpx;
|
||||
height: 480rpx;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.breathe-ring-2 {
|
||||
width: 400rpx;
|
||||
height: 400rpx;
|
||||
animation-delay: 0.5s;
|
||||
border-color: rgba(var(--primary-rgb), 0.25);
|
||||
border-width: 4rpx;
|
||||
}
|
||||
|
||||
.breathe-ring-3 {
|
||||
width: 320rpx;
|
||||
height: 320rpx;
|
||||
animation-delay: 1s;
|
||||
border-color: rgba(var(--primary-rgb), 0.35);
|
||||
border-width: 5rpx;
|
||||
}
|
||||
|
||||
.breathe-ring.fast {
|
||||
animation: breathePulseFast 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.breathe-ring.fast.breathe-ring-1 {
|
||||
animation-delay: 0s;
|
||||
border-color: rgba(var(--primary-rgb), 0.3);
|
||||
}
|
||||
|
||||
.breathe-ring.fast.breathe-ring-2 {
|
||||
animation-delay: 0.25s;
|
||||
border-color: rgba(var(--primary-rgb), 0.45);
|
||||
}
|
||||
|
||||
.breathe-ring.fast.breathe-ring-3 {
|
||||
animation-delay: 0.5s;
|
||||
border-color: rgba(var(--primary-rgb), 0.6);
|
||||
}
|
||||
|
||||
/* celebration burst */
|
||||
.celebrate-burst {
|
||||
position: absolute;
|
||||
width: 520rpx;
|
||||
height: 520rpx;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.spark {
|
||||
position: absolute;
|
||||
font-size: 36rpx;
|
||||
animation: sparkBurst 1s ease-out forwards;
|
||||
}
|
||||
|
||||
.s1 { top: -10rpx; left: 50%; margin-left: -18rpx; 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);
|
||||
}
|
||||
}
|
||||
|
||||
/* overtime badge */
|
||||
.overtime-badge {
|
||||
position: absolute;
|
||||
bottom: 10rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(76, 175, 80, 0.12);
|
||||
border: 2rpx solid rgba(76, 175, 80, 0.3);
|
||||
color: var(--success);
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
animation: popIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.overtime-icon {
|
||||
font-size: 28rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* ---- hint section ---- */
|
||||
.hint-section {
|
||||
margin-top: 36rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hint-row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.hint-emoji {
|
||||
font-size: 28rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.running-pulse {
|
||||
animation: float 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.completed-bounce {
|
||||
animation: bounceSoft 0.6s ease infinite;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
font-size: 28rpx;
|
||||
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-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
width: 300rpx;
|
||||
height: 108rpx;
|
||||
border-radius: 54rpx;
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ctrl-btn:active {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
|
||||
.ctrl-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.ctrl-icon {
|
||||
font-size: 32rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.start-btn {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-light));
|
||||
color: #fff;
|
||||
box-shadow: 0 8rpx 28rpx rgba(var(--primary-rgb), 0.35);
|
||||
}
|
||||
|
||||
.pause-btn {
|
||||
background: var(--primary-bg);
|
||||
color: var(--primary);
|
||||
margin-right: 28rpx;
|
||||
}
|
||||
|
||||
.resume-btn {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-light));
|
||||
color: #fff;
|
||||
margin-right: 28rpx;
|
||||
box-shadow: 0 8rpx 28rpx rgba(var(--primary-rgb), 0.35);
|
||||
}
|
||||
|
||||
.stop-btn {
|
||||
background: #F5F5F5;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.ctrl-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
Reference in New Issue
Block a user