backup: snapshot before optimization
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
# CodeGraph data files
|
||||
# These are local to each machine and should not be committed
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.db-wal
|
||||
*.db-shm
|
||||
|
||||
# Cache
|
||||
cache/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Hook markers
|
||||
.dirty
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"pid": 75366,
|
||||
"version": "0.9.7",
|
||||
"socketPath": "/Users/liubleed/Documents/wx_pbzc/.codegraph/daemon.sock",
|
||||
"startedAt": 1780561314477
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
var themeMod = require('./utils/theme')
|
||||
|
||||
App({
|
||||
onLaunch: function () {
|
||||
var settings = wx.getStorageSync('user_settings')
|
||||
if (!settings) {
|
||||
wx.setStorageSync('user_settings', {
|
||||
planId: 'beginner',
|
||||
dailyReminder: true,
|
||||
reminderTime: '08:00',
|
||||
voiceGuide: true,
|
||||
vibrate: true
|
||||
})
|
||||
}
|
||||
var streak = wx.getStorageSync('current_streak')
|
||||
if (!streak) {
|
||||
wx.setStorageSync('current_streak', { count: 0, lastDate: '' })
|
||||
}
|
||||
this.globalData.theme = themeMod.getCurrentTheme()
|
||||
},
|
||||
|
||||
globalData: {
|
||||
planId: 'beginner',
|
||||
theme: null
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"pages": [
|
||||
"pages/index/index",
|
||||
"pages/timer/timer",
|
||||
"pages/records/records",
|
||||
"pages/settings/settings"
|
||||
],
|
||||
"window": {
|
||||
"navigationBarBackgroundColor": "#FF6B35",
|
||||
"navigationBarTitleText": "平板支撑训练",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#F5F5F5"
|
||||
},
|
||||
"tabBar": {
|
||||
"custom": true,
|
||||
"color": "#999999",
|
||||
"selectedColor": "#FF6B35",
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"borderStyle": "black",
|
||||
"list": [
|
||||
{ "pagePath": "pages/index/index", "text": "训练" },
|
||||
{ "pagePath": "pages/records/records", "text": "记录" },
|
||||
{ "pagePath": "pages/settings/settings", "text": "设置" }
|
||||
]
|
||||
},
|
||||
"sitemapLocation": "sitemap.json"
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
page {
|
||||
--primary: #FF6B35;
|
||||
--primary-light: #FF8C5A;
|
||||
--primary-bg: #FFF3ED;
|
||||
--primary-rgb: 255,107,53;
|
||||
--text: #333333;
|
||||
--text-secondary: #999999;
|
||||
--bg: #F5F5F5;
|
||||
--card-bg: #FFFFFF;
|
||||
--success: #4CAF50;
|
||||
--border: #EEEEEE;
|
||||
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC',
|
||||
'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
font-size: 28rpx;
|
||||
color: var(--text);
|
||||
background-color: var(--bg);
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 24rpx;
|
||||
padding-bottom: calc(134rpx + env(safe-area-inset-bottom));
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg);
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||
animation: cardIn 0.45s ease both;
|
||||
}
|
||||
|
||||
.card:nth-child(1) { animation-delay: 0.05s; }
|
||||
.card:nth-child(2) { animation-delay: 0.15s; }
|
||||
.card:nth-child(3) { animation-delay: 0.25s; }
|
||||
.card:nth-child(4) { animation-delay: 0.35s; }
|
||||
|
||||
@keyframes cardIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(40rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-light));
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 48rpx;
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
padding: 24rpx 0;
|
||||
text-align: center;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
box-shadow: 0 8rpx 24rpx rgba(var(--primary-rgb), 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: scale(0.96);
|
||||
box-shadow: 0 4rpx 12rpx rgba(var(--primary-rgb), 0.2);
|
||||
}
|
||||
|
||||
/* ---- pulse ring (used on timer page) ---- */
|
||||
@keyframes breathePulse {
|
||||
0%, 100% {
|
||||
transform: scale(0.92);
|
||||
opacity: 0.6;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.08);
|
||||
opacity: 0.15;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes breathePulseFast {
|
||||
0%, 100% {
|
||||
transform: scale(0.94);
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.06);
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- float ---- */
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-12rpx); }
|
||||
}
|
||||
|
||||
/* ---- pop / bounce ---- */
|
||||
@keyframes popIn {
|
||||
0% { transform: scale(0); opacity: 0; }
|
||||
60% { transform: scale(1.15); }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes bounceSoft {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
30% { transform: translateY(-16rpx); }
|
||||
50% { transform: translateY(0); }
|
||||
70% { transform: translateY(-6rpx); }
|
||||
}
|
||||
|
||||
/* ---- shimmer ---- */
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
/* ---- spin ---- */
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
/* ---- icon sizing ---- */
|
||||
.icon {
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.icon-lg { font-size: 40rpx; }
|
||||
.icon-md { font-size: 28rpx; }
|
||||
.icon-sm { font-size: 22rpx; }
|
||||
|
||||
.icon-inline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
var util = require('../../utils/util')
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
year: { type: Number, value: new Date().getFullYear() },
|
||||
month: { type: Number, value: new Date().getMonth() + 1 },
|
||||
records: { type: Array, value: [] }
|
||||
},
|
||||
|
||||
data: {
|
||||
weeks: []
|
||||
},
|
||||
|
||||
observers: {
|
||||
'year, month, records': function (year, month, records) {
|
||||
this._compute(year, month, records)
|
||||
}
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
ready: function () {
|
||||
this._compute(this.properties.year, this.properties.month, this.properties.records)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
_compute: function (year, month, records) {
|
||||
var weeks = util.getMonthCalendar(year, month)
|
||||
var recordMap = {}
|
||||
if (records && records.length) {
|
||||
records.forEach(function (r) {
|
||||
var d = parseInt(r.date.split('-')[2])
|
||||
recordMap[d] = (recordMap[d] || 0) + r.duration
|
||||
})
|
||||
}
|
||||
|
||||
var data = weeks.map(function (week) {
|
||||
return week.map(function (day) {
|
||||
if (!day) return null
|
||||
var duration = recordMap[day] || 0
|
||||
var color = 'transparent'
|
||||
if (duration > 0) {
|
||||
if (duration < 30) color = '#FFE0D0'
|
||||
else if (duration < 60) color = '#FFB088'
|
||||
else if (duration < 120) color = '#FF6B35'
|
||||
else color = '#E05520'
|
||||
}
|
||||
return { day: day, duration: duration, color: color }
|
||||
})
|
||||
})
|
||||
|
||||
this.setData({ weeks: data })
|
||||
},
|
||||
|
||||
onDayTap: function (e) {
|
||||
var day = e.currentTarget.dataset.day
|
||||
if (!day) return
|
||||
var cell = null
|
||||
var weeks = this.data.weeks
|
||||
for (var i = 0; i < weeks.length; i++) {
|
||||
for (var j = 0; j < weeks[i].length; j++) {
|
||||
var c = weeks[i][j]
|
||||
if (c && c.day === day) { cell = c; break }
|
||||
}
|
||||
if (cell) break
|
||||
}
|
||||
if (!cell || cell.duration <= 0) return
|
||||
this.triggerEvent('daytap', {
|
||||
year: this.properties.year,
|
||||
month: this.properties.month,
|
||||
day: day,
|
||||
duration: cell.duration
|
||||
})
|
||||
},
|
||||
|
||||
getHeatColor: function (cell) {
|
||||
if (!cell || cell.duration <= 0) return 'transparent'
|
||||
var d = cell.duration
|
||||
if (d < 30) return '#FFE0D0'
|
||||
if (d < 60) return '#FFB088'
|
||||
if (d < 120) return '#FF6B35'
|
||||
return '#E05520'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<view class="heatmap-container">
|
||||
<view class="calendar-header">
|
||||
<text class="month-label">{{year}}年{{month}}月</text>
|
||||
<view class="weekday-labels">
|
||||
<text class="weekday" wx:for="{{['日','一','二','三','四','五','六']}}" wx:key="*this">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="calendar-grid">
|
||||
<view class="calendar-week" wx:for="{{weeks}}" wx:key="index">
|
||||
<view
|
||||
class="calendar-day {{cell && cell.duration > 0 ? 'has-data' : 'empty'}}"
|
||||
wx:for="{{item}}"
|
||||
wx:for-item="cell"
|
||||
wx:key="index"
|
||||
style="background-color: {{cell ? cell.color : 'transparent'}};"
|
||||
data-day="{{cell ? cell.day : ''}}"
|
||||
bindtap="onDayTap"
|
||||
>
|
||||
<text class="day-num" wx:if="{{cell}}">{{cell.day}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="heat-legend">
|
||||
<text class="legend-label">少</text>
|
||||
<view class="legend-block" style="background:#FFE0D0;"></view>
|
||||
<view class="legend-block" style="background:#FFB088;"></view>
|
||||
<view class="legend-block" style="background:#FF6B35;"></view>
|
||||
<view class="legend-block" style="background:#E05520;"></view>
|
||||
<text class="legend-label">多</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,77 @@
|
||||
.heatmap-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.month-label {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12rpx;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.weekday-labels {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.weekday {
|
||||
width: 56rpx;
|
||||
text-align: center;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.calendar-week {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.calendar-day {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 10rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.calendar-day.empty {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.day-num {
|
||||
font-size: 22rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.heat-legend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 16rpx;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.legend-label {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.legend-block {
|
||||
width: 32rpx;
|
||||
height: 24rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
var util = require('../../utils/util')
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
duration: { type: Number, value: 60 },
|
||||
remaining: { type: Number, value: 60 },
|
||||
size: { type: Number, value: 280 },
|
||||
ringWidth: { type: Number, value: 14 },
|
||||
status: { type: String, value: 'idle' },
|
||||
primaryColor: { type: String, value: '#FF6B35' },
|
||||
primaryLightColor: { type: String, value: '#FF8C5A' }
|
||||
},
|
||||
|
||||
data: {
|
||||
displayTime: '00:00',
|
||||
statusText: '准备开始'
|
||||
},
|
||||
|
||||
observers: {
|
||||
'remaining, status, duration, size, ringWidth, primaryColor': function () {
|
||||
var texts = { idle: '准备开始', running: '坚持住', paused: '已暂停', completed: '完成!' }
|
||||
this.setData({
|
||||
displayTime: util.formatTime(this.data.remaining),
|
||||
statusText: texts[this.data.status] || ''
|
||||
})
|
||||
this._draw()
|
||||
}
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
ready: function () {
|
||||
var self = this
|
||||
var query = this.createSelectorQuery()
|
||||
query.select('#ringCanvas')
|
||||
.fields({ node: true, size: true })
|
||||
.exec(function (res) {
|
||||
if (!res || !res[0] || !res[0].node) return
|
||||
var canvas = res[0].node
|
||||
var dpr = wx.getSystemInfoSync().pixelRatio || 2
|
||||
var displaySize = self.data.size
|
||||
canvas.width = displaySize * dpr
|
||||
canvas.height = displaySize * dpr
|
||||
self._ctx = canvas.getContext('2d')
|
||||
self._dpr = dpr
|
||||
self._draw()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
_draw: function () {
|
||||
var ctx = this._ctx
|
||||
if (!ctx) return
|
||||
|
||||
var size = this.data.size
|
||||
var dpr = this._dpr
|
||||
var ringWidth = this.data.ringWidth
|
||||
var duration = this.data.duration
|
||||
var remaining = this.data.remaining
|
||||
var primaryColor = this.data.primaryColor
|
||||
|
||||
var w = size * dpr
|
||||
var h = size * dpr
|
||||
ctx.clearRect(0, 0, w, h)
|
||||
|
||||
var cx = w / 2
|
||||
var cy = h / 2
|
||||
var radius = (size - ringWidth) / 2 * dpr
|
||||
var lw = ringWidth * dpr
|
||||
|
||||
// track ring
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, radius, 0, Math.PI * 2)
|
||||
ctx.strokeStyle = '#EEEEEE'
|
||||
ctx.lineWidth = lw
|
||||
ctx.lineCap = 'round'
|
||||
ctx.stroke()
|
||||
|
||||
// progress arc (clockwise from 12 o'clock, represents remaining time)
|
||||
var ratio = duration > 0 ? Math.max(0, Math.min(1, remaining / duration)) : 0
|
||||
if (ratio > 0.001) {
|
||||
var startAngle = -Math.PI / 2
|
||||
var endAngle = startAngle + ratio * Math.PI * 2
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, radius, startAngle, endAngle)
|
||||
ctx.strokeStyle = primaryColor
|
||||
ctx.lineWidth = lw
|
||||
ctx.lineCap = 'round'
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<view class="ring-wrap" style="width: {{size}}rpx; height: {{size}}rpx;">
|
||||
<canvas type="2d" id="ringCanvas" class="ring-canvas" style="width: {{size}}rpx; height: {{size}}rpx;"></canvas>
|
||||
<view class="ring-center">
|
||||
<text class="time-text">{{displayTime}}</text>
|
||||
<text class="status-text">{{statusText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,37 @@
|
||||
.ring-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ring-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.ring-center {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.time-text {
|
||||
font-size: 80rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: 2rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 10rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
var themeMod = require('../utils/theme')
|
||||
|
||||
Component({
|
||||
data: {
|
||||
selected: 0,
|
||||
themeStyle: ''
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached: function () {
|
||||
var theme = themeMod.getCurrentTheme()
|
||||
this.setData({ themeStyle: themeMod.getThemeStyle(theme) })
|
||||
}
|
||||
},
|
||||
|
||||
pageLifetimes: {
|
||||
show: function () {
|
||||
var theme = themeMod.getCurrentTheme()
|
||||
this.setData({ themeStyle: themeMod.getThemeStyle(theme) })
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
switchTab: function (e) {
|
||||
var index = e.currentTarget.dataset.index
|
||||
var pages = [
|
||||
'/pages/index/index',
|
||||
'/pages/records/records',
|
||||
'/pages/settings/settings'
|
||||
]
|
||||
|
||||
if (index === this.data.selected) return
|
||||
|
||||
wx.switchTab({ url: pages[index] })
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<view class="tab-bar" style="{{themeStyle}}">
|
||||
<view
|
||||
class="tab-item {{selected === 0 ? 'active' : ''}}"
|
||||
data-index="0"
|
||||
bindtap="switchTab"
|
||||
>
|
||||
<view class="tab-icon">
|
||||
<view class="icon-home {{selected === 0 ? 'active' : ''}}">
|
||||
<view class="home-roof"></view>
|
||||
<view class="home-body"></view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="tab-text">训练</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="tab-item {{selected === 1 ? 'active' : ''}}"
|
||||
data-index="1"
|
||||
bindtap="switchTab"
|
||||
>
|
||||
<view class="tab-icon">
|
||||
<view class="icon-records {{selected === 1 ? 'active' : ''}}">
|
||||
<view class="bar bar1"></view>
|
||||
<view class="bar bar2"></view>
|
||||
<view class="bar bar3"></view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="tab-text">记录</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="tab-item {{selected === 2 ? 'active' : ''}}"
|
||||
data-index="2"
|
||||
bindtap="switchTab"
|
||||
>
|
||||
<view class="tab-icon">
|
||||
<view class="icon-settings {{selected === 2 ? 'active' : ''}}">
|
||||
<view class="gear-outer"></view>
|
||||
<view class="gear-center"></view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="tab-text">设置</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,146 @@
|
||||
.tab-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 110rpx;
|
||||
background: #FFFFFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
border-top: 1rpx solid rgba(0, 0, 0, 0.06);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6rpx 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tab-item:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-top: 8rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tab-item.active .tab-text {
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ---- icon: home ---- */
|
||||
.icon-home {
|
||||
position: relative;
|
||||
width: 44rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
.tab-item.active .icon-home {
|
||||
animation: bounceSoft 0.5s ease;
|
||||
}
|
||||
|
||||
.home-roof {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 22rpx solid transparent;
|
||||
border-right: 22rpx solid transparent;
|
||||
border-bottom: 18rpx solid #888;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.tab-item.active .home-roof {
|
||||
border-bottom-color: var(--primary);
|
||||
}
|
||||
|
||||
.home-body {
|
||||
width: 22rpx;
|
||||
height: 20rpx;
|
||||
background: #888;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 11rpx;
|
||||
border-radius: 2rpx 2rpx 0 0;
|
||||
}
|
||||
|
||||
.tab-item.active .home-body {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
/* ---- icon: records (3 bars) ---- */
|
||||
.icon-records {
|
||||
width: 40rpx;
|
||||
height: 34rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.tab-item.active .icon-records {
|
||||
animation: bounceSoft 0.5s ease;
|
||||
}
|
||||
|
||||
.bar {
|
||||
height: 6rpx;
|
||||
background: #888;
|
||||
border-radius: 3rpx;
|
||||
}
|
||||
|
||||
.bar1 { width: 100%; }
|
||||
.bar2 { width: 65%; }
|
||||
.bar3 { width: 80%; }
|
||||
|
||||
.tab-item.active .bar {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
/* ---- icon: settings (gear) ---- */
|
||||
.icon-settings {
|
||||
position: relative;
|
||||
width: 42rpx;
|
||||
height: 42rpx;
|
||||
}
|
||||
|
||||
.tab-item.active .icon-settings {
|
||||
animation: bounceSoft 0.5s ease;
|
||||
}
|
||||
|
||||
.gear-outer {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border: 5rpx solid #888;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 3rpx;
|
||||
left: 3rpx;
|
||||
}
|
||||
|
||||
.tab-item.active .gear-outer {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.gear-center {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
background: #888;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 15rpx;
|
||||
left: 15rpx;
|
||||
}
|
||||
|
||||
.tab-item.active .gear-center {
|
||||
background: var(--primary);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"description": "平板支撑训练小程序",
|
||||
"packOptions": {
|
||||
"ignore": [],
|
||||
"include": []
|
||||
},
|
||||
"setting": {
|
||||
"bundle": false,
|
||||
"userConfirmedBundleSwitch": false,
|
||||
"urlCheck": true,
|
||||
"scopeDataCheck": false,
|
||||
"coverView": true,
|
||||
"es6": true,
|
||||
"postcss": true,
|
||||
"compileHotReLoad": false,
|
||||
"lazyloadPlaceholderEnable": false,
|
||||
"preloadBackgroundData": false,
|
||||
"minified": true,
|
||||
"autoAudits": false,
|
||||
"newFeature": false,
|
||||
"uglifyFileName": false,
|
||||
"uploadWithSourceMap": true,
|
||||
"useIsolateContext": true,
|
||||
"nodeModules": false,
|
||||
"enhance": true,
|
||||
"useMultiFrameRuntime": true,
|
||||
"useApiHook": true,
|
||||
"useApiHostProcess": true,
|
||||
"showShadowRootInWxmlPanel": true,
|
||||
"packNpmManually": false,
|
||||
"enableEngineNative": false,
|
||||
"packNpmRelationList": [],
|
||||
"minifyWXSS": true,
|
||||
"showES6CompileOption": false,
|
||||
"minifyWXML": true,
|
||||
"babelSetting": {
|
||||
"ignore": [],
|
||||
"disablePlugins": [],
|
||||
"outputPath": ""
|
||||
},
|
||||
"condition": false,
|
||||
"compileWorklet": false,
|
||||
"localPlugins": false,
|
||||
"disableUseStrict": false,
|
||||
"useCompilerPlugins": false,
|
||||
"swc": false,
|
||||
"disableSWC": true
|
||||
},
|
||||
"compileType": "miniprogram",
|
||||
"libVersion": "3.3.4",
|
||||
"appid": "wxb7f1bc86924869bc",
|
||||
"projectname": "pbzc",
|
||||
"condition": {},
|
||||
"editorSetting": {
|
||||
"tabIndent": "insertSpaces",
|
||||
"tabSize": 2
|
||||
},
|
||||
"simulatorPluginLibVersion": {}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"libVersion": "3.16.0",
|
||||
"projectname": "wx_pbzc",
|
||||
"condition": {},
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"coverView": true,
|
||||
"lazyloadPlaceholderEnable": false,
|
||||
"skylineRenderEnable": false,
|
||||
"preloadBackgroundData": false,
|
||||
"autoAudits": false,
|
||||
"useApiHook": true,
|
||||
"showShadowRootInWxmlPanel": true,
|
||||
"useStaticServer": false,
|
||||
"useLanDebug": false,
|
||||
"showES6CompileOption": false,
|
||||
"compileHotReLoad": true,
|
||||
"checkInvalidKey": true,
|
||||
"ignoreDevUnusedFiles": true,
|
||||
"bigPackageSizeSupport": false,
|
||||
"useIsolateContext": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"action": "allow",
|
||||
"page": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 70 B |
Binary file not shown.
|
After Width: | Height: | Size: 70 B |
Binary file not shown.
|
After Width: | Height: | Size: 70 B |
Binary file not shown.
|
After Width: | Height: | Size: 70 B |
Binary file not shown.
|
After Width: | Height: | Size: 70 B |
Binary file not shown.
|
After Width: | Height: | Size: 70 B |
@@ -0,0 +1,73 @@
|
||||
var plans = {
|
||||
beginner: {
|
||||
id: 'beginner',
|
||||
name: '初级 (7天)',
|
||||
totalDays: 7,
|
||||
description: '适合初学者,从30秒起步',
|
||||
days: [
|
||||
{ day: 1, target: 30 },
|
||||
{ day: 2, target: 40 },
|
||||
{ day: 3, target: 50 },
|
||||
{ day: 4, target: 60 },
|
||||
{ day: 5, target: 70 },
|
||||
{ day: 6, target: 80 },
|
||||
{ day: 7, target: 90 }
|
||||
]
|
||||
},
|
||||
intermediate: {
|
||||
id: 'intermediate',
|
||||
name: '中级 (14天)',
|
||||
totalDays: 14,
|
||||
description: '有一定基础,从60秒起步',
|
||||
days: [
|
||||
{ day: 1, target: 60 }, { day: 2, target: 60 },
|
||||
{ day: 3, target: 75 }, { day: 4, target: 75 },
|
||||
{ day: 5, target: 90 }, { day: 6, target: 90 },
|
||||
{ day: 7, target: 105 }, { day: 8, target: 105 },
|
||||
{ day: 9, target: 120 }, { day: 10, target: 120 },
|
||||
{ day: 11, target: 135 }, { day: 12, target: 135 },
|
||||
{ day: 13, target: 150 }, { day: 14, target: 150 }
|
||||
]
|
||||
},
|
||||
advanced: {
|
||||
id: 'advanced',
|
||||
name: '高级 (30天)',
|
||||
totalDays: 30,
|
||||
description: '挑战自我,从90秒起步',
|
||||
days: (function () {
|
||||
var arr = []
|
||||
for (var i = 1; i <= 30; i++) {
|
||||
arr.push({ day: i, target: 90 + Math.floor((i - 1) / 3) * 15 })
|
||||
}
|
||||
return arr
|
||||
})()
|
||||
}
|
||||
}
|
||||
|
||||
function getPlan(planId) {
|
||||
return plans[planId] || plans.beginner
|
||||
}
|
||||
|
||||
function getTodayTarget(planId, currentDay) {
|
||||
var plan = getPlan(planId)
|
||||
for (var i = 0; i < plan.days.length; i++) {
|
||||
if (plan.days[i].day === currentDay) return plan.days[i].target
|
||||
}
|
||||
return plan.days[0].target
|
||||
}
|
||||
|
||||
function getPlanDay(planId, records) {
|
||||
var plan = getPlan(planId)
|
||||
var now = new Date()
|
||||
var today = now.getFullYear() + '-' +
|
||||
('0' + (now.getMonth() + 1)).slice(-2) + '-' +
|
||||
('0' + now.getDate()).slice(-2)
|
||||
var dates = {}
|
||||
records.forEach(function (r) {
|
||||
if (r.date !== today) dates[r.date] = true
|
||||
})
|
||||
var trainedDays = Object.keys(dates).length
|
||||
return Math.min(trainedDays + 1, plan.totalDays)
|
||||
}
|
||||
|
||||
module.exports = { plans: plans, getPlan: getPlan, getTodayTarget: getTodayTarget, getPlanDay: getPlanDay }
|
||||
@@ -0,0 +1,132 @@
|
||||
var RECORDS_KEY = 'training_records'
|
||||
var SETTINGS_KEY = 'user_settings'
|
||||
var STREAK_KEY = 'current_streak'
|
||||
|
||||
function getRecords() {
|
||||
return wx.getStorageSync(RECORDS_KEY) || {}
|
||||
}
|
||||
|
||||
function saveRecord(record) {
|
||||
record.id = Date.now()
|
||||
var records = getRecords()
|
||||
var month = record.date.substring(0, 7)
|
||||
if (!records[month]) records[month] = []
|
||||
records[month].push(record)
|
||||
records[month].sort(function (a, b) { return b.date.localeCompare(a.date) })
|
||||
wx.setStorageSync(RECORDS_KEY, records)
|
||||
}
|
||||
|
||||
function deleteRecord(recordId) {
|
||||
var records = getRecords()
|
||||
Object.keys(records).forEach(function (month) {
|
||||
records[month] = records[month].filter(function (r) { return r.id !== recordId })
|
||||
if (records[month].length === 0) delete records[month]
|
||||
})
|
||||
wx.setStorageSync(RECORDS_KEY, records)
|
||||
}
|
||||
|
||||
function getRecordsByMonth(month) {
|
||||
var records = getRecords()
|
||||
return records[month] || []
|
||||
}
|
||||
|
||||
function getTotalStats() {
|
||||
var records = getRecords()
|
||||
var totalDuration = 0
|
||||
var totalSessions = 0
|
||||
var maxDuration = 0
|
||||
Object.keys(records).forEach(function (key) {
|
||||
var list = records[key]
|
||||
list.forEach(function (r) {
|
||||
totalDuration += r.duration
|
||||
totalSessions++
|
||||
if (r.duration > maxDuration) maxDuration = r.duration
|
||||
})
|
||||
})
|
||||
return { totalDuration: totalDuration, totalSessions: totalSessions, maxDuration: maxDuration }
|
||||
}
|
||||
|
||||
function getSettings() {
|
||||
return wx.getStorageSync(SETTINGS_KEY) || {
|
||||
planId: 'beginner',
|
||||
dailyReminder: true,
|
||||
reminderTime: '08:00',
|
||||
voiceGuide: true,
|
||||
vibrate: true
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings(settings) {
|
||||
wx.setStorageSync(SETTINGS_KEY, settings)
|
||||
}
|
||||
|
||||
function getStreak() {
|
||||
return wx.getStorageSync(STREAK_KEY) || { count: 0, lastDate: '' }
|
||||
}
|
||||
|
||||
function updateStreak(date) {
|
||||
var streak = getStreak()
|
||||
var today = date || getToday()
|
||||
var yesterday = getDateOffset(today, -1)
|
||||
|
||||
if (streak.lastDate === today) return streak
|
||||
|
||||
if (streak.lastDate === yesterday) {
|
||||
streak.count += 1
|
||||
} else {
|
||||
streak.count = 1
|
||||
}
|
||||
streak.lastDate = today
|
||||
wx.setStorageSync(STREAK_KEY, streak)
|
||||
return streak
|
||||
}
|
||||
|
||||
function getToday() {
|
||||
return formatDate(new Date())
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
var y = date.getFullYear()
|
||||
var m = String(date.getMonth() + 1)
|
||||
if (m.length < 2) m = '0' + m
|
||||
var d = String(date.getDate())
|
||||
if (d.length < 2) d = '0' + d
|
||||
return y + '-' + m + '-' + d
|
||||
}
|
||||
|
||||
function getDateOffset(dateStr, offset) {
|
||||
var d = new Date(dateStr)
|
||||
d.setDate(d.getDate() + offset)
|
||||
return formatDate(d)
|
||||
}
|
||||
|
||||
function getTodayRecord() {
|
||||
var today = getToday()
|
||||
var month = today.substring(0, 7)
|
||||
var records = getRecordsByMonth(month)
|
||||
var totalDuration = 0
|
||||
var hasRecord = false
|
||||
for (var i = 0; i < records.length; i++) {
|
||||
if (records[i].date === today) {
|
||||
totalDuration += records[i].duration
|
||||
hasRecord = true
|
||||
}
|
||||
}
|
||||
return hasRecord ? { date: today, duration: totalDuration } : null
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getRecords: getRecords,
|
||||
saveRecord: saveRecord,
|
||||
deleteRecord: deleteRecord,
|
||||
getRecordsByMonth: getRecordsByMonth,
|
||||
getTotalStats: getTotalStats,
|
||||
getSettings: getSettings,
|
||||
saveSettings: saveSettings,
|
||||
getStreak: getStreak,
|
||||
updateStreak: updateStreak,
|
||||
getToday: getToday,
|
||||
formatDate: formatDate,
|
||||
getDateOffset: getDateOffset,
|
||||
getTodayRecord: getTodayRecord
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
var THEMES = [
|
||||
{
|
||||
id: 'orange',
|
||||
name: '活力橙',
|
||||
primary: '#FF6B35',
|
||||
primaryLight: '#FF8C5A',
|
||||
primaryBg: '#FFF3ED',
|
||||
primaryRgb: '255,107,53'
|
||||
},
|
||||
{
|
||||
id: 'blue',
|
||||
name: '深海蓝',
|
||||
primary: '#2979FF',
|
||||
primaryLight: '#5C9CFF',
|
||||
primaryBg: '#EDF4FF',
|
||||
primaryRgb: '41,121,255'
|
||||
},
|
||||
{
|
||||
id: 'green',
|
||||
name: '森林绿',
|
||||
primary: '#43A047',
|
||||
primaryLight: '#6EC072',
|
||||
primaryBg: '#EDF7EE',
|
||||
primaryRgb: '67,160,71'
|
||||
},
|
||||
{
|
||||
id: 'purple',
|
||||
name: '优雅紫',
|
||||
primary: '#7E57C2',
|
||||
primaryLight: '#A08AD6',
|
||||
primaryBg: '#F5F0FA',
|
||||
primaryRgb: '126,87,194'
|
||||
},
|
||||
{
|
||||
id: 'pink',
|
||||
name: '樱花粉',
|
||||
primary: '#EC407A',
|
||||
primaryLight: '#F06A9A',
|
||||
primaryBg: '#FDEEF3',
|
||||
primaryRgb: '236,64,122'
|
||||
}
|
||||
]
|
||||
|
||||
var DEFAULT_THEME_ID = 'orange'
|
||||
var THEME_KEY = 'app_theme'
|
||||
var _lastNavColor = ''
|
||||
var _navColorTimer = null
|
||||
|
||||
function getThemeById(id) {
|
||||
return THEMES.find(function (t) { return t.id === id }) || THEMES[0]
|
||||
}
|
||||
|
||||
function getCurrentTheme() {
|
||||
var id = wx.getStorageSync(THEME_KEY) || DEFAULT_THEME_ID
|
||||
return getThemeById(id)
|
||||
}
|
||||
|
||||
function _setNavBarColor(primary) {
|
||||
if (_lastNavColor === primary) return
|
||||
_lastNavColor = primary
|
||||
if (_navColorTimer) clearTimeout(_navColorTimer)
|
||||
_navColorTimer = setTimeout(function () {
|
||||
try {
|
||||
wx.setNavigationBarColor({
|
||||
frontColor: '#ffffff',
|
||||
backgroundColor: primary
|
||||
})
|
||||
} catch (e) {}
|
||||
}, 50)
|
||||
}
|
||||
|
||||
function setTheme(id) {
|
||||
wx.setStorageSync(THEME_KEY, id)
|
||||
var theme = getThemeById(id)
|
||||
_setNavBarColor(theme.primary)
|
||||
var app = getApp()
|
||||
if (app && app.globalData) app.globalData.theme = theme
|
||||
return theme
|
||||
}
|
||||
|
||||
function applyThemeToPage(self) {
|
||||
var theme = getCurrentTheme()
|
||||
self.setData({
|
||||
theme: theme,
|
||||
themeStyle: '--primary:' + theme.primary + ';--primary-light:' + theme.primaryLight + ';--primary-bg:' + theme.primaryBg + ';--primary-rgb:' + theme.primaryRgb + ';'
|
||||
})
|
||||
_setNavBarColor(theme.primary)
|
||||
}
|
||||
|
||||
function getThemeStyle(theme) {
|
||||
var t = theme || getCurrentTheme()
|
||||
return '--primary:' + t.primary + ';--primary-light:' + t.primaryLight + ';--primary-bg:' + t.primaryBg + ';--primary-rgb:' + t.primaryRgb + ';'
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
THEMES: THEMES,
|
||||
DEFAULT_THEME_ID: DEFAULT_THEME_ID,
|
||||
getThemeById: getThemeById,
|
||||
getCurrentTheme: getCurrentTheme,
|
||||
setTheme: setTheme,
|
||||
applyThemeToPage: applyThemeToPage,
|
||||
getThemeStyle: getThemeStyle
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
function Timer(options) {
|
||||
options = options || {}
|
||||
this.onTick = options.onTick || function () {}
|
||||
this.onComplete = options.onComplete || function () {}
|
||||
|
||||
this._duration = 0
|
||||
this._remaining = 0
|
||||
this._elapsed = 0
|
||||
this._startTime = 0
|
||||
this._pausedTime = 0
|
||||
this._intervalId = null
|
||||
this._running = false
|
||||
this._paused = false
|
||||
this._completed = false
|
||||
}
|
||||
|
||||
Timer.prototype.start = function (duration) {
|
||||
this._duration = duration
|
||||
this._remaining = duration
|
||||
this._elapsed = 0
|
||||
this._completed = false
|
||||
this._running = true
|
||||
this._paused = false
|
||||
this._startTime = Date.now()
|
||||
this._tick()
|
||||
var self = this
|
||||
this._intervalId = setInterval(function () { self._tick() }, 1000)
|
||||
}
|
||||
|
||||
Timer.prototype.pause = function () {
|
||||
if (!this._running || this._paused) return
|
||||
this._paused = true
|
||||
this._pausedTime = Date.now()
|
||||
clearInterval(this._intervalId)
|
||||
this._intervalId = null
|
||||
}
|
||||
|
||||
Timer.prototype.resume = function () {
|
||||
if (!this._running || !this._paused) return
|
||||
this._startTime += Date.now() - this._pausedTime
|
||||
this._paused = false
|
||||
this._tick()
|
||||
var self = this
|
||||
this._intervalId = setInterval(function () { self._tick() }, 1000)
|
||||
}
|
||||
|
||||
Timer.prototype.stop = function () {
|
||||
this._running = false
|
||||
this._paused = false
|
||||
clearInterval(this._intervalId)
|
||||
this._intervalId = null
|
||||
return this._elapsed
|
||||
}
|
||||
|
||||
Timer.prototype._tick = function () {
|
||||
if (!this._running || this._paused) return
|
||||
this._elapsed = Math.floor((Date.now() - this._startTime) / 1000)
|
||||
this._remaining = Math.max(0, this._duration - this._elapsed)
|
||||
|
||||
this.onTick({
|
||||
remaining: this._remaining,
|
||||
elapsed: this._elapsed,
|
||||
duration: this._duration
|
||||
})
|
||||
|
||||
if (this._remaining <= 0 && !this._completed) {
|
||||
this._completed = true
|
||||
this.onComplete({ elapsed: this._elapsed, duration: this._duration })
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(Timer.prototype, 'isRunning', {
|
||||
get: function () { return this._running && !this._paused }
|
||||
})
|
||||
|
||||
Object.defineProperty(Timer.prototype, 'isPaused', {
|
||||
get: function () { return this._paused }
|
||||
})
|
||||
|
||||
Object.defineProperty(Timer.prototype, 'isCompleted', {
|
||||
get: function () { return this._completed }
|
||||
})
|
||||
|
||||
Object.defineProperty(Timer.prototype, 'remaining', {
|
||||
get: function () { return this._remaining }
|
||||
})
|
||||
|
||||
Object.defineProperty(Timer.prototype, 'elapsed', {
|
||||
get: function () { return this._elapsed }
|
||||
})
|
||||
|
||||
module.exports = Timer
|
||||
@@ -0,0 +1,44 @@
|
||||
function formatTime(seconds) {
|
||||
var m = Math.floor(seconds / 60)
|
||||
var s = seconds % 60
|
||||
return (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s
|
||||
}
|
||||
|
||||
function formatDuration(seconds) {
|
||||
if (seconds < 60) return seconds + '秒'
|
||||
var m = Math.floor(seconds / 60)
|
||||
var s = seconds % 60
|
||||
return s > 0 ? m + '分' + s + '秒' : m + '分钟'
|
||||
}
|
||||
|
||||
function getDaysInMonth(year, month) {
|
||||
return new Date(year, month, 0).getDate()
|
||||
}
|
||||
|
||||
function getMonthCalendar(year, month) {
|
||||
var firstDay = new Date(year, month - 1, 1)
|
||||
var lastDay = new Date(year, month, 0)
|
||||
var daysInMonth = lastDay.getDate()
|
||||
var startDayOfWeek = firstDay.getDay()
|
||||
|
||||
var weeks = []
|
||||
var week = [null, null, null, null, null, null, null]
|
||||
|
||||
for (var d = 1; d <= daysInMonth; d++) {
|
||||
var dow = (startDayOfWeek + d - 1) % 7
|
||||
week[dow] = d
|
||||
if (dow === 6 || d === daysInMonth) {
|
||||
weeks.push(week.slice())
|
||||
week = [null, null, null, null, null, null, null]
|
||||
}
|
||||
}
|
||||
|
||||
return weeks
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
formatTime: formatTime,
|
||||
formatDuration: formatDuration,
|
||||
getDaysInMonth: getDaysInMonth,
|
||||
getMonthCalendar: getMonthCalendar
|
||||
}
|
||||
Reference in New Issue
Block a user