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