Files
wx_pbzc/utils/plan.js
T
lc 6176f2c341 feat(ui): 记录页月份联动与多项 UI 美化/修复
- records: 底部列表与月份选择器联动,新增 [本月|最近] 分段控件;标题/空态随模式切换
- leaderboard: 领奖台布局(冠军居中+皇冠)、次数标注于底座且三档水平对齐;头像 cloud:// 批量预解析提速(云函数需重新部署)
- index/timer: 主色底图标改白色变体修复隐形;目标数字按微信字体缩放补偿真机遮挡
- settings: 配色方案横滑渐变色卡;头像上传前压缩至 200px
- theme: 新增 teal 主题;暗黑背景统一对齐
- 圆形头像双保险(image 自身 border-radius)修复月/年榜方图

注: cloudfunctions/leaderboard 改动须在开发者工具重新部署才生效。
2026-07-22 09:36:22 +08:00

108 lines
3.5 KiB
JavaScript

const { formatDate, dateOnly } = 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)
// 防御:计划数据损坏(planId 无效 / days 为空)时直接返回 0,
// 避免 plan.days[0] 为 undefined 后访问 .target 抛出崩溃。
if (!plan.days || plan.days.length === 0) return 0
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, customPlans) => {
const plan = getPlan(planId, customPlans)
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 (dateOnly(r.date) === dateOnly(today)) return false
if (planStartDate && r.date < planStartDate) return false
return true
})
// 用 dateOnly 去重:r.date 带时分秒,同一天多次训练必须算作"1 天",
// 否则会被当成分属两天而错误跳过计划进度。
const trainedDays = new Set(eligibleRecords.map(r => dateOnly(r.date))).size
return Math.min(trainedDays + 1, plan.totalDays)
}
module.exports = { plans, generatePlanDays, getPlan, getTodayTarget, getPlanDay }