const formatTime = (seconds) => { const m = Math.floor(seconds / 60) const s = seconds % 60 return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` } const formatDuration = (seconds) => { if (seconds < 60) return `${seconds}秒` const m = Math.floor(seconds / 60) const s = seconds % 60 return s > 0 ? `${m}分${s}秒` : `${m}分钟` } const getDaysInMonth = (year, month) => new Date(year, month, 0).getDate() const getMonthCalendar = (year, month) => { const firstDay = new Date(year, month - 1, 1) const daysInMonth = new Date(year, month, 0).getDate() const startDayOfWeek = firstDay.getDay() const weeks = [] let week = [null, null, null, null, null, null, null] for (let d = 1; d <= daysInMonth; d++) { const 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 } /** * Format a Date as `YYYY-MM-DD HH:MM:SS` in local time. * * Used as the canonical `record.date` string so each record has a * precise timestamp (e.g. "2026-06-11 14:30:45"). Use `_dateOnly()` * to strip back to YYYY-MM-DD for date-level comparisons (streak, * leaderboard day filter, etc.). */ const formatDate = (date) => { const y = date.getFullYear() const mo = String(date.getMonth() + 1).padStart(2, '0') const d = String(date.getDate()).padStart(2, '0') const h = String(date.getHours()).padStart(2, '0') const mi = String(date.getMinutes()).padStart(2, '0') const s = String(date.getSeconds()).padStart(2, '0') return `${y}-${mo}-${d} ${h}:${mi}:${s}` } /** Strip the time portion of a formatDate string → "YYYY-MM-DD". */ const _dateOnly = (s) => (typeof s === 'string' && s.length >= 10) ? s.substring(0, 10) : '' /** * Convert a local file path (typically `wxfile://...` from chooseAvatar) to a * base64 data URI. Used to make temporary file URLs survive the WeChat * expiration window (a few days) by embedding the bytes inline. * * Rejects if the file exceeds `maxBytes` (default 512KB) so a stray huge * image doesn't blow the 10MB total wx.storage budget. Caller should * fall back to the raw URL on rejection. */ const fileToDataURI = (filePath, maxBytes = 512 * 1024) => { return new Promise((resolve, reject) => { if (!filePath || typeof wx.getFileSystemManager !== 'function') { reject(new Error('FileSystemManager unavailable')) return } const fs = wx.getFileSystemManager() fs.getFileInfo({ filePath, success: (info) => { if (info.size > maxBytes) { reject(new Error(`File too large: ${info.size} > ${maxBytes}`)) return } fs.readFile({ filePath, encoding: 'base64', success: (res) => { // chooseAvatar returns PNG; allow JPG/GIF/WEBP fallbacks. const m = (filePath.match(/\.(\w+)(?:\?|$)/) || [, 'png'])[1].toLowerCase() const mime = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp' }[m] || 'image/png' resolve(`data:${mime};base64,${res.data}`) }, fail: reject }) }, fail: reject }) }) } module.exports = { formatTime, formatDuration, getDaysInMonth, getMonthCalendar, formatDate, dateOnly: _dateOnly, fileToDataURI }