1d55df72b3
Major features: - Training plan editor: edit preset days/duration per plan (in-place override via custom_plans storage; preset ids preserved so existing records stay valid) - Voice prompts at 4 fixed points during training (halfway / 30s / 10s / done) via Tencent Cloud TTS (cloudfunctions/tts + utils/voice.js) - Free-training button: switched from outline (transparent) to ghost variant (theme-tinted) for visual weight Bug fixes: - 4 functional pages: SVG data URIs failed to render in WeChat because '\#' in colors was parsed as a data-URI fragment delimiter. encode the whole SVG via encodeURIComponent in utils/icons.js build(). - Old records (saved before id field was added) could not be deleted (data-id was empty, triggered defensive guard). Backfill stable legacy-<month>-<index>-<duration> ids in getRecords() and persist. - settings.js plan-row editor button event was bubbling up to the row's bindtap (which also fired onSelectPlan). Wrapped in catch:tap. UI: - Settings page: 3 hardcoded plans replaced with dynamic buildPlansList that overlays custom_plans on top of presets - Plan editor: bottom-sheet modal in settings page (regular view, not ui-modal — WeChat custom component root element drops position:fixed in this runtime) - Free-training button: rounded pill style, full-width, 24rpx gap from primary action; bottom sheet uses max-height: 88vh + internal scroll - Version bumped to v1.5, last updated 2026-06-10 Removed: - Daily reminder section (dailyReminder / reminderTime) — replaced by the 4 voice prompts which cover the same user need without requiring long-term scheduling that WeChat mini-programs can't actually do Misc: - utils/plan.js refactored to formula-driven: presets declare totalDays/startTarget/increment/cycleDays, days[] is generated. Same formula applies to custom plans. - Timer _remind() guards each prompt on minimum duration so short free-mode sessions don't fire 'last30' at the start - Cloud storage cloud_plans field added to data push payload; restored on first install via _restoreFromCloud - .gitignore added for local AI tool caches (.reasonix/, reasonix.toml, .codegraph/daemon.pid)
103 lines
3.1 KiB
JavaScript
103 lines
3.1 KiB
JavaScript
const { formatDate } = require('./util')
|
|
|
|
/**
|
|
* Build the per-day target array for a plan from its formula fields.
|
|
*
|
|
* target(day) = startTarget + floor((day - 1) / cycleDays) * increment
|
|
*
|
|
* Examples:
|
|
* { totalDays: 7, startTarget: 30, increment: 10, cycleDays: 1 }
|
|
* → 30, 40, 50, 60, 70, 80, 90 (preset: 初级)
|
|
* { totalDays: 14, startTarget: 60, increment: 15, cycleDays: 2 }
|
|
* → 60, 60, 75, 75, 90, 90, ... (preset: 中级)
|
|
* { totalDays: 30, startTarget: 90, increment: 15, cycleDays: 3 }
|
|
* → 90, 90, 90, 105, 105, 105, ... (preset: 高级)
|
|
*/
|
|
const generatePlanDays = ({ totalDays, startTarget, increment, cycleDays }) => {
|
|
const n = Math.max(0, Math.floor(totalDays) || 0)
|
|
const start = Math.max(0, Math.floor(startTarget) || 0)
|
|
const inc = Math.max(0, Math.floor(increment) || 0)
|
|
const cycle = Math.max(1, Math.floor(cycleDays) || 1)
|
|
const days = []
|
|
for (let i = 0; i < n; i++) {
|
|
const d = i + 1
|
|
days.push({ day: d, target: start + Math.floor((d - 1) / cycle) * inc })
|
|
}
|
|
return days
|
|
}
|
|
|
|
const _describe = (plan) =>
|
|
`${plan.totalDays}天计划 · ${plan.startTarget}秒起步`
|
|
|
|
const _buildPlan = (base) => {
|
|
const plan = { ...base }
|
|
plan.days = generatePlanDays(plan)
|
|
plan.description = _describe(plan)
|
|
return plan
|
|
}
|
|
|
|
const plans = {
|
|
beginner: _buildPlan({
|
|
id: 'beginner',
|
|
name: '初级 (7天)',
|
|
totalDays: 7,
|
|
startTarget: 30,
|
|
increment: 10,
|
|
cycleDays: 1
|
|
}),
|
|
intermediate: _buildPlan({
|
|
id: 'intermediate',
|
|
name: '中级 (14天)',
|
|
totalDays: 14,
|
|
startTarget: 60,
|
|
increment: 15,
|
|
cycleDays: 2
|
|
}),
|
|
advanced: _buildPlan({
|
|
id: 'advanced',
|
|
name: '高级 (30天)',
|
|
totalDays: 30,
|
|
startTarget: 90,
|
|
increment: 15,
|
|
cycleDays: 3
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Look up a plan by id. User-customized plans (passed in `customPlans`)
|
|
* shadow the preset with the same id, so e.g. editing "beginner" replaces
|
|
* the preset beginner for the user without changing the planId.
|
|
*/
|
|
const getPlan = (planId, customPlans) => {
|
|
if (customPlans && customPlans[planId]) return customPlans[planId]
|
|
return plans[planId] || plans.beginner
|
|
}
|
|
|
|
const getTodayTarget = (planId, currentDay) => {
|
|
const plan = getPlan(planId)
|
|
const day = plan.days.find(d => d.day === currentDay)
|
|
return day ? day.target : plan.days[0].target
|
|
}
|
|
|
|
/**
|
|
* Calculate the current plan day based on training records since plan start.
|
|
* Only counts records from the current plan's start date onward.
|
|
*/
|
|
const getPlanDay = (planId, records, planStartDate) => {
|
|
const plan = getPlan(planId)
|
|
const today = formatDate(new Date())
|
|
|
|
// Filter records: same plan, after plan start date, and not today
|
|
const eligibleRecords = records.filter(r => {
|
|
if (r.planId !== planId) return false
|
|
if (r.date === today) return false
|
|
if (planStartDate && r.date < planStartDate) return false
|
|
return true
|
|
})
|
|
|
|
const trainedDays = new Set(eligibleRecords.map(r => r.date)).size
|
|
return Math.min(trainedDays + 1, plan.totalDays)
|
|
}
|
|
|
|
module.exports = { plans, generatePlanDays, getPlan, getTodayTarget, getPlanDay }
|