- 移除动态主题切换功能,改为全局统一深色主题 - 实现自定义TabBar组件替换原生tabBar,解决iOS闪白问题 - 创建main容器页面统一管理三个tab页面的生命周期和状态 - 将所有页面的onShow/onHide等生命周期方法迁移到子组件内部 - 更新页面路径从index改为main,并调整路由跳转逻辑 - 移除theme-mixin和相关主题工具函数 - 统一页面背景色设置,优化用户体验一致性
311 lines
9.1 KiB
JavaScript
311 lines
9.1 KiB
JavaScript
/* 碰盏日记 - 工具函数 */
|
||
|
||
import { DRINK_CATEGORIES } from './constants'
|
||
|
||
/**
|
||
* 获取酒类图标:优先返回自定义图片路径,否则返回 emoji
|
||
*/
|
||
export function getCatIcon(catId) {
|
||
const cat = DRINK_CATEGORIES.find(c => c.id === catId)
|
||
if (!cat) return '🥃'
|
||
return cat.icon || cat.emoji
|
||
}
|
||
|
||
export function isIconPath(val) {
|
||
return typeof val === 'string' && val.charAt(0) === '/'
|
||
}
|
||
|
||
/**
|
||
* 判断头像地址是否可用
|
||
* wxfile://tmp_ 等本地临时路径仅本机当次有效,不能用于展示他人头像,
|
||
* 展示层对此类地址应回退为占位头像
|
||
*/
|
||
export function isValidAvatar(url) {
|
||
return typeof url === 'string' && /^https?:\/\//.test(url)
|
||
}
|
||
|
||
/**
|
||
* 标准杯换算引擎
|
||
* 核心公式: 标准杯 = (饮用量ml × 酒精度数% × 0.8) / 10
|
||
* 1标准杯 = 10g纯酒精
|
||
*/
|
||
export function calcStandardCups(amountMl, degreePercent) {
|
||
if (!amountMl || !degreePercent) return 0
|
||
return Math.round((amountMl * (degreePercent / 100) * 0.8 / 10) * 100) / 100
|
||
}
|
||
|
||
/**
|
||
* 单位换算为ml
|
||
*/
|
||
const UNIT_TO_ML = {
|
||
ml: 1,
|
||
liang: 50, // 1两 = 50ml
|
||
bottle: 500, // 1瓶 = 500ml (啤酒默认)
|
||
cup: 150, // 1杯 = 150ml
|
||
can: 330, // 1听 = 330ml
|
||
shot: 30 // 1shot = 30ml
|
||
}
|
||
|
||
export function unitToMl(amount, unit) {
|
||
const factor = UNIT_TO_ML[unit] || 1
|
||
return amount * factor
|
||
}
|
||
|
||
/**
|
||
* 计算一组饮品的总标准杯
|
||
*/
|
||
export function calcTotalStandardCups(drinks) {
|
||
return drinks.reduce((sum, d) => {
|
||
const ml = unitToMl(d.amount, d.unit)
|
||
return sum + calcStandardCups(ml, d.degree)
|
||
}, 0)
|
||
}
|
||
|
||
/**
|
||
* 日期格式化
|
||
*/
|
||
export function formatDate(date, format = 'YYYY-MM-DD') {
|
||
const d = new Date(date)
|
||
const year = d.getFullYear()
|
||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||
const day = String(d.getDate()).padStart(2, '0')
|
||
const hours = String(d.getHours()).padStart(2, '0')
|
||
const minutes = String(d.getMinutes()).padStart(2, '0')
|
||
|
||
return format
|
||
.replace('YYYY', year)
|
||
.replace('MM', month)
|
||
.replace('DD', day)
|
||
.replace('HH', hours)
|
||
.replace('mm', minutes)
|
||
}
|
||
|
||
/**
|
||
* 获取指定月份的日历数据
|
||
* @param {number} year
|
||
* @param {number} month 1-12
|
||
* @returns {Array} 日历数组,包含前置空白、日期、后置空白
|
||
*/
|
||
export function getCalendarDays(year, month) {
|
||
const firstDay = new Date(year, month - 1, 1)
|
||
const lastDay = new Date(year, month, 0)
|
||
const daysInMonth = lastDay.getDate()
|
||
// 周一为一周起始 (0=周一, 6=周日)
|
||
let startWeekday = firstDay.getDay() - 1
|
||
if (startWeekday < 0) startWeekday = 6
|
||
|
||
const days = []
|
||
// 前置空白
|
||
for (let i = 0; i < startWeekday; i++) {
|
||
days.push({ day: null, date: null })
|
||
}
|
||
// 日期
|
||
for (let i = 1; i <= daysInMonth; i++) {
|
||
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(i).padStart(2, '0')}`
|
||
days.push({
|
||
day: i,
|
||
date: dateStr,
|
||
isToday: dateStr === formatDate(new Date(), 'YYYY-MM-DD')
|
||
})
|
||
}
|
||
return days
|
||
}
|
||
|
||
/**
|
||
* 获取本周一的日期
|
||
*/
|
||
export function getWeekStart(date = new Date()) {
|
||
const d = new Date(date)
|
||
const day = d.getDay()
|
||
const diff = d.getDate() - day + (day === 0 ? -6 : 1)
|
||
return new Date(d.setDate(diff))
|
||
}
|
||
|
||
/**
|
||
* 获取时间问候语
|
||
*/
|
||
export function getGreeting() {
|
||
const hour = new Date().getHours()
|
||
if (hour < 6) return '夜深了'
|
||
if (hour < 12) return '早上好'
|
||
if (hour < 14) return '中午好'
|
||
if (hour < 18) return '下午好'
|
||
if (hour < 22) return '晚上好'
|
||
return '夜深了'
|
||
}
|
||
|
||
/**
|
||
* 随机获取一条酒言酒语
|
||
*/
|
||
export function getRandomQuote(quotes) {
|
||
return quotes[Math.floor(Math.random() * quotes.length)]
|
||
}
|
||
|
||
/**
|
||
* 统计数据计算
|
||
*/
|
||
export function calcStats(records) {
|
||
const now = new Date()
|
||
const today = formatDate(now, 'YYYY-MM-DD')
|
||
|
||
// 本周统计
|
||
const weekStart = getWeekStart(now)
|
||
const weekRecords = records.filter(r => new Date(r.date) >= weekStart)
|
||
const weekDrinkCount = weekRecords.filter(r => r.mode === 'drank').length
|
||
const weekCups = weekRecords.reduce((sum, r) => sum + (r.standardCupsTotal || 0), 0)
|
||
|
||
// 本月统计
|
||
const monthStr = formatDate(now, 'YYYY-MM')
|
||
const monthRecords = records.filter(r => r.date && r.date.startsWith(monthStr))
|
||
const monthDrinkCount = monthRecords.filter(r => r.mode === 'drank').length
|
||
const monthCups = monthRecords.reduce((sum, r) => sum + (r.standardCupsTotal || 0), 0)
|
||
|
||
// 连续天数
|
||
let streak = 0
|
||
let streakType = 'drank'
|
||
const d = new Date()
|
||
// 检查今天是否有记录
|
||
const todayRecord = records.find(r => r.date === today)
|
||
if (todayRecord) {
|
||
streakType = todayRecord.mode
|
||
streak = 1
|
||
// 向前追溯
|
||
for (let i = 1; i < 365; i++) {
|
||
const prevDate = new Date(d)
|
||
prevDate.setDate(prevDate.getDate() - i)
|
||
const prevStr = formatDate(prevDate, 'YYYY-MM-DD')
|
||
const prev = records.find(r => r.date === prevStr)
|
||
if (prev && prev.mode === streakType) {
|
||
streak++
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
// 总打卡天数
|
||
const totalDays = new Set(records.filter(r => r.mode === 'drank').map(r => r.date)).size
|
||
const totalRecords = records.filter(r => r.mode === 'drank').length
|
||
const totalCups = records.reduce((sum, r) => sum + (r.standardCupsTotal || 0), 0)
|
||
|
||
// 最爱酒类
|
||
const categoryCount = {}
|
||
records.forEach(r => {
|
||
if (r.drinks) {
|
||
r.drinks.forEach(d => {
|
||
categoryCount[d.category] = (categoryCount[d.category] || 0) + 1
|
||
})
|
||
}
|
||
})
|
||
const favCategory = Object.entries(categoryCount).sort((a, b) => b[1] - a[1])[0]
|
||
|
||
return {
|
||
weekDrinkCount,
|
||
weekCups: Math.round(weekCups * 10) / 10,
|
||
monthDrinkCount,
|
||
monthCups: Math.round(monthCups * 10) / 10,
|
||
streak,
|
||
streakType,
|
||
totalDays,
|
||
totalRecords,
|
||
totalCups: Math.round(totalCups * 10) / 10,
|
||
favCategory: favCategory ? favCategory[0] : null,
|
||
categoryVariety: Object.keys(categoryCount).length
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 本地计算连续打卡天数(修正后端 streak 不区分饮酒/戒酒模式的问题)
|
||
* 双轨计算:
|
||
* - 连续饮酒:从今天的饮酒记录向前追溯;今天未饮酒(戒酒/未打卡)则从昨天算起
|
||
* - 连续戒酒:从今天的戒酒记录向前追溯;今天饮酒/未打卡则从昨天算起
|
||
* @param {Array} records 记录列表,每项需含 date 和 mode 字段
|
||
* @returns {{ streak, streakType, drankStreak, abstainStreak }}
|
||
* streak/streakType:主展示值(今天有记录取今天模式,否则取最近记录模式)
|
||
*/
|
||
export function calcLocalStreak(records) {
|
||
if (!records || !records.length) {
|
||
return { streak: 0, streakType: 'drank', drankStreak: 0, abstainStreak: 0 }
|
||
}
|
||
const today = formatDate(new Date(), 'YYYY-MM-DD')
|
||
// 按日期去重(同一天多条取第一条)
|
||
const byDate = {}
|
||
records.forEach(r => {
|
||
if (r && r.date && byDate[r.date] === undefined) byDate[r.date] = r
|
||
})
|
||
const dates = Object.keys(byDate).sort()
|
||
// 单模式连续链:优先从今天起算,今天非该模式则宽限到昨天
|
||
const chainOf = (mode) => {
|
||
const todayMode = byDate[today] ? byDate[today].mode : null
|
||
let base = null
|
||
if (todayMode === mode) {
|
||
base = today
|
||
} else {
|
||
const y = new Date()
|
||
y.setDate(y.getDate() - 1)
|
||
const yStr = formatDate(y, 'YYYY-MM-DD')
|
||
if (byDate[yStr] && byDate[yStr].mode === mode) base = yStr
|
||
}
|
||
if (!base) return 0
|
||
let count = 1
|
||
const d = new Date(base)
|
||
for (let i = 1; i < 366; i++) {
|
||
const prev = new Date(d)
|
||
prev.setDate(prev.getDate() - i)
|
||
const r = byDate[formatDate(prev, 'YYYY-MM-DD')]
|
||
if (r && r.mode === mode) {
|
||
count++
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
return count
|
||
}
|
||
const drankStreak = chainOf('drank')
|
||
const abstainStreak = chainOf('abstain')
|
||
// 主展示值:今天有记录取今天模式,否则取最近一次记录的模式
|
||
let streak
|
||
let streakType
|
||
if (byDate[today]) {
|
||
streakType = byDate[today].mode || 'drank'
|
||
streak = streakType === 'drank' ? drankStreak : abstainStreak
|
||
} else {
|
||
const lastDate = dates[dates.length - 1]
|
||
streakType = byDate[lastDate].mode || 'drank'
|
||
streak = streakType === 'drank' ? drankStreak : abstainStreak
|
||
}
|
||
return { streak, streakType, drankStreak, abstainStreak }
|
||
}
|
||
|
||
/**
|
||
* 检查成就解锁
|
||
*/
|
||
export function checkAchievements(stats, achievements) {
|
||
return achievements.map(a => {
|
||
let unlocked = false
|
||
const { type, value, category } = a.condition
|
||
|
||
switch (type) {
|
||
case 'total_records':
|
||
unlocked = stats.totalRecords >= value
|
||
break
|
||
case 'total_standard_cups':
|
||
unlocked = stats.totalCups >= value
|
||
break
|
||
case 'streak_days':
|
||
unlocked = stats.streak >= value
|
||
break
|
||
case 'category_variety':
|
||
unlocked = stats.categoryVariety >= value
|
||
break
|
||
case 'category_records':
|
||
// 依赖 stats.categoryRecords({ baijiu: n, beer: n, ... });
|
||
// 统计中无该字段时保守判为未解锁
|
||
unlocked = ((stats.categoryRecords && stats.categoryRecords[category]) || 0) >= value
|
||
break
|
||
}
|
||
|
||
return { ...a, unlocked }
|
||
})
|
||
}
|