储蓄游戏模块
🎮 概述
储蓄游戏模块是 Penny Lens 应用的创新功能模块,通过游戏化的方式激励用户控制消费、养成储蓄习惯。基于周消费目标设定,通过实时计算和可视化展示帮助用户达成储蓄目标。
🏗️ 模块架构
核心组件
src/├── pages/savings/ # 储蓄页面│ ├── index.vue # 储蓄主页│ ├── archive.vue # 储蓄历史│ ├── virtual-transactions.vue # 虚拟交易明细│ └── components/ # 储蓄页面组件│ ├── WeeklyProgress.vue # 周进度组件│ ├── PiggyBankCard.vue # 存钱罐卡片│ ├── ExpenseDetail.vue # 消费明细│ └── ActionBar.vue # 操作栏├── store/savings.ts # 储蓄状态管理├── services/api/savings.ts # 储蓄API服务├── utils/savingCalculator.ts # 储蓄计算器├── types/savings.ts # 储蓄类型定义└── enums/savingsEnum.ts # 储蓄枚举🎯 核心概念
储蓄目标系统
// 周目标接口
interface WeeklyGoal {
id: string;
userId: string;
weekStartTimestamp: number; // 周开始时间戳
weekEndTimestamp: number; // 周结束时间戳
goalCents: number; // 目标金额(分)
}
// 周归档记录
interface WeekArchive {
weekId: string; // 周ID (如: 2025-W34)
userId: string;
startDateTimestamp: number;
endDateTimestamp: number;
goalCents: number;
actualSpentCents: number;
savedCents: number;
status: "done" | "partial" | "failed";
}虚拟交易系统
// 虚拟交易记录
interface VirtualTransaction {
id: string;
userId: string;
weekId: string;
type: "deposit" | "withdraw";
amountCents: number;
description: string;
createdAt: number;
}
// 虚拟余额
interface VirtualSaving {
userId: string;
balanceCents: number;
locked?: boolean;
}🔧 核心功能
1. 储蓄概览
功能描述: 显示当前用户的储蓄概览信息
实现流程:
- 获取当前周的时间范围
- 查询用户当前周的储蓄目标
- 从记账记录聚合计算实际消费金额
- 计算节省金额(目标 - 实际消费)
- 获取虚拟余额总额
- 计算宠物等级和心情状态
代码示例:
// 储蓄概览响应
interface SavingsOverviewResponse {
weekGoal: WeeklyGoal | null;
actualSpentCents: number;
savedCents: number;
virtualBalanceCents: number;
weekStartTimestamp: number;
weekEndTimestamp: number;
weekId: string;
petLevel: number;
petMood: string;
progressPercentage: number;
}
// 获取储蓄概览
async function getSavingsOverview(): Promise<SavingsOverviewResponse> {
const response = await savingsApi.getOverview()
return response.data
}
// 计算宠物等级
function calculatePetLevel(totalSavedCents: number): number {
const LEVEL_THRESHOLDS = {
LEVEL_2: 5000, // 50元
LEVEL_3: 20000, // 200元
LEVEL_4: 50000, // 500元
LEVEL_5: 100000 // 1000元
}
if (totalSavedCents < LEVEL_THRESHOLDS.LEVEL_2) return 1
if (totalSavedCents < LEVEL_THRESHOLDS.LEVEL_3) return 2
if (totalSavedCents < LEVEL_THRESHOLDS.LEVEL_4) return 3
if (totalSavedCents < LEVEL_THRESHOLDS.LEVEL_5) return 4
return 5
}
// 计算宠物心情
function calculatePetMood(progressPercentage: number): string {
if (progressPercentage >= 100) return 'excited'
if (progressPercentage >= 80) return 'happy'
if (progressPercentage >= 60) return 'normal'
if (progressPercentage >= 40) return 'worried'
return 'sad'
}2. 设置储蓄目标
功能描述: 设置或更新当前周的储蓄目标
实现流程:
- 用户输入目标金额
- 验证目标金额有效性
- 保存到后端
- 更新本地状态
代码示例:
// 设置储蓄目标
async function setWeeklyGoal(goalData: WeeklyGoalRequest): Promise<WeeklyGoal> {
// 参数验证
if (goalData.goalCents <= 0) {
throw new Error('目标金额必须大于0')
}
if (goalData.weekStartTimestamp >= goalData.weekEndTimestamp) {
throw new Error('周开始时间必须早于结束时间')
}
const response = await savingsApi.setGoal(goalData)
return response.data
}
// 获取当前周范围
function getCurrentWeekRange(): { start: Date; end: Date } {
const now = new Date()
const startDate = new Date(now)
const dayOfWeek = now.getDay()
// 调整为周一为周开始
const diff = dayOfWeek === 0 ? -6 : 1 - dayOfWeek
startDate.setDate(now.getDate() + diff)
startDate.setHours(0, 0, 0, 0)
const endDate = new Date(startDate)
endDate.setDate(startDate.getDate() + 6)
endDate.setHours(23, 59, 59, 999)
return { start: startDate, end: endDate }
}3. 虚拟交易系统
功能描述: 管理虚拟余额的存取操作
实现流程:
- 用户选择交易类型(存入/提取)
- 输入交易金额和原因
- 验证余额是否充足
- 创建虚拟交易记录
- 更新虚拟余额
代码示例:
// 虚拟余额提取
async function withdrawFromVirtualBalance(amountCents: number, reason: string): Promise<WithdrawResponse> {
// 检查当前虚拟余额
const overview = await getSavingsOverview()
if (overview.virtualBalanceCents < amountCents) {
return {
success: false,
newBalanceCents: overview.virtualBalanceCents,
message: '虚拟余额不足'
}
}
const response = await savingsApi.withdraw({
amountCents,
reason
})
return response.data
}
// 虚拟余额存入(自动)
async function depositToVirtualBalance(weekId: string, savedCents: number): Promise<void> {
if (savedCents > 0) {
await savingsApi.deposit({
weekId,
amountCents: savedCents,
description: `第${weekId}周储蓄奖励`
})
}
}4. 周归档功能
功能描述: 归档指定周的储蓄记录
实现流程:
- 检查该周是否已归档
- 获取周目标和实际消费
- 计算节省金额和达成状态
- 如有节省金额,自动添加到虚拟余额
- 创建归档记录
代码示例:
// 归档周记录
async function archiveWeek(weekStartTimestamp: number, weekEndTimestamp: number): Promise<WeekArchive> {
const response = await savingsApi.archiveWeek({
weekStartTimestamp,
weekEndTimestamp
})
return response.data
}
// 计算达成状态
function calculateAchievementStatus(goalCents: number, actualSpentCents: number): string {
if (actualSpentCents <= goalCents) return "done"
if (actualSpentCents <= goalCents * 1.2) return "partial" // 超支20%内
return "failed"
}🎮 游戏化设计
1. 存钱罐等级系统
功能描述: 基于虚拟余额的等级成长系统
代码示例:
// 宠物等级配置
const PET_LEVELS = [
{
level: 1,
name: '小萌新',
description: '刚开始存钱的小萌新',
requiredSavings: 0,
emoji: '🐣',
color: '#ffd700'
},
{
level: 2,
name: '存钱新手',
description: '已经存了50元的新手',
requiredSavings: 5000,
emoji: '🐥',
color: '#ffb347'
},
{
level: 3,
name: '存钱达人',
description: '存了200元的达人',
requiredSavings: 20000,
emoji: '🐔',
color: '#ff8c00'
},
{
level: 4,
name: '存钱专家',
description: '存了500元的专家',
requiredSavings: 50000,
emoji: '🦅',
color: '#ff6347'
},
{
level: 5,
name: '存钱大师',
description: '存了1000元的大师',
requiredSavings: 100000,
emoji: '🦅',
color: '#ff4500'
}
]
// 获取宠物等级信息
function getPetLevelInfo(level: number): PetLevel {
return PET_LEVELS.find(pet => pet.level === level) || PET_LEVELS[0]
}
// 计算下一等级进度
function calculateNextLevelProgress(currentLevel: number, totalSaved: number): {
nextLevel: PetLevel;
progress: number;
remaining: number;
} {
const nextLevel = PET_LEVELS.find(pet => pet.level === currentLevel + 1)
if (!nextLevel) {
return {
nextLevel: PET_LEVELS[PET_LEVELS.length - 1],
progress: 100,
remaining: 0
}
}
const currentLevelInfo = getPetLevelInfo(currentLevel)
const progress = Math.min(100, ((totalSaved - currentLevelInfo.requiredSavings) / (nextLevel.requiredSavings - currentLevelInfo.requiredSavings)) * 100)
const remaining = Math.max(0, nextLevel.requiredSavings - totalSaved)
return {
nextLevel,
progress,
remaining
}
}2. 节省力评分系统
功能描述: 基于实际消费行为的综合评分体系
代码示例:
// 节省力统计
interface SavingPowerStats {
savedDays: number; // 节省天数
totalSavingAmount: number; // 总节省金额
maxDailySaving: number; // 最大单日节省
savingStreak: number; // 节省连续天数
savingPower: number; // 节省力评分 0-100
savingRate: number; // 节省率 0-100%
totalOverspent: number; // 总超支金额
healthStatus: 'healthy' | 'warning' | 'danger' | 'critical';
overBudgetRatio: number; // 超支比例
}
// 计算节省力评分
function calculateSavingPower(
recordedDays: CalculatedDayData[],
weekGoal: number
): SavingPowerStats {
const savedDays = recordedDays.filter(day => day.dailySaving > 0).length
const totalSavingAmount = recordedDays.reduce((sum, day) => sum + day.dailySaving, 0)
const maxDailySaving = Math.max(...recordedDays.map(day => day.dailySaving), 0)
const savingStreak = calculateSavingStreak(recordedDays)
const totalOverspent = recordedDays.reduce((sum, day) => sum + Math.max(0, day.dailySpent - day.dailyBudget), 0)
// 计算节省力评分 (0-100分)
const savedDaysRatio = recordedDays.length > 0 ? (savedDays / recordedDays.length) * 40 : 0
const averageSavingRate = recordedDays.length > 0 ? (totalSavingAmount / (recordedDays.length * weekGoal / 7)) * 30 : 0
const maxSavingBonus = Math.min(15, (maxDailySaving / (weekGoal / 7)) * 15)
const streakBonus = Math.min(15, (savingStreak / 7) * 15)
const savingPower = Math.min(100, savedDaysRatio + averageSavingRate + maxSavingBonus + streakBonus)
const savingRate = weekGoal > 0 ? Math.min(100, (totalSavingAmount / weekGoal) * 100) : 0
const overBudgetRatio = weekGoal > 0 ? (totalOverspent / weekGoal) * 100 : 0
// 健康状态
let healthStatus: 'healthy' | 'warning' | 'danger' | 'critical'
if (savingPower >= 80) healthStatus = 'healthy'
else if (savingPower >= 60) healthStatus = 'warning'
else if (savingPower >= 40) healthStatus = 'danger'
else healthStatus = 'critical'
return {
savedDays,
totalSavingAmount,
maxDailySaving,
savingStreak,
savingPower: Math.round(savingPower * 10) / 10,
savingRate: Math.round(savingRate * 10) / 10,
totalOverspent,
healthStatus,
overBudgetRatio: Math.round(overBudgetRatio * 10) / 10
}
}
// 计算节省连续天数
function calculateSavingStreak(recordedDays: CalculatedDayData[]): number {
let streak = 0
let maxStreak = 0
for (let i = recordedDays.length - 1; i >= 0; i--) {
if (recordedDays[i].dailySaving > 0) {
streak++
maxStreak = Math.max(maxStreak, streak)
} else {
streak = 0
}
}
return maxStreak
}3. 成就系统
功能描述: 基于储蓄行为的成就奖励系统
代码示例:
// 成就类型
interface Achievement {
id: string;
name: string;
description: string;
icon: string;
condition: AchievementCondition;
reward: AchievementReward;
unlocked: boolean;
unlockedAt?: number;
}
// 成就条件
interface AchievementCondition {
type: 'saving_streak' | 'total_saved' | 'weekly_goal' | 'pet_level';
value: number;
period?: 'daily' | 'weekly' | 'monthly' | 'all_time';
}
// 成就奖励
interface AchievementReward {
type: 'virtual_balance' | 'pet_exp' | 'badge';
amount: number;
description: string;
}
// 成就配置
const ACHIEVEMENTS: Achievement[] = [
{
id: 'first_save',
name: '初次储蓄',
description: '完成第一次储蓄',
icon: 'i-mdi-trophy',
condition: { type: 'total_saved', value: 1 },
reward: { type: 'virtual_balance', amount: 1000, description: '获得10元虚拟余额' },
unlocked: false
},
{
id: 'saving_streak_7',
name: '连续储蓄达人',
description: '连续7天完成储蓄目标',
icon: 'i-mdi-fire',
condition: { type: 'saving_streak', value: 7 },
reward: { type: 'virtual_balance', amount: 5000, description: '获得50元虚拟余额' },
unlocked: false
},
{
id: 'pet_level_5',
name: '存钱大师',
description: '宠物等级达到5级',
icon: 'i-mdi-crown',
condition: { type: 'pet_level', value: 5 },
reward: { type: 'badge', amount: 1, description: '获得大师徽章' },
unlocked: false
}
]
// 检查成就解锁
function checkAchievements(userStats: UserStats): Achievement[] {
const unlockedAchievements: Achievement[] = []
ACHIEVEMENTS.forEach(achievement => {
if (achievement.unlocked) return
let conditionMet = false
switch (achievement.condition.type) {
case 'saving_streak':
conditionMet = userStats.savingStreak >= achievement.condition.value
break
case 'total_saved':
conditionMet = userStats.totalSaved >= achievement.condition.value
break
case 'weekly_goal':
conditionMet = userStats.weeklyGoalsMet >= achievement.condition.value
break
case 'pet_level':
conditionMet = userStats.petLevel >= achievement.condition.value
break
}
if (conditionMet) {
achievement.unlocked = true
achievement.unlockedAt = Date.now()
unlockedAchievements.push(achievement)
}
})
return unlockedAchievements
}📊 储蓄计算器
功能描述: 核心的储蓄计算逻辑
代码示例:
// 储蓄计算器
export class SavingCalculator {
// 计算周预算
static calculateWeeklyBudget(
weekGoalYuan: number,
dailyExpenseData: Array<{ date: string, amount: number }>,
currentWeekStart: Date
): WeeklyCalculationResult {
const weekDays = this.generateWeekDays(currentWeekStart)
const dailyBudget = weekGoalYuan / 7
// 计算每日数据
const calculatedDays = weekDays.map(day => {
const dayData = dailyExpenseData.find(d => d.date === this.formatDate(day.date))
const dailySpent = dayData ? dayData.amount : 0
const dailySaving = Math.max(0, dailyBudget - dailySpent)
return {
date: day.date,
isToday: day.isToday,
hasRecord: !!dayData,
dailySpent,
dailyBudget,
dailySaving,
status: this.calculateDailyStatus(dailySpent, dailyBudget)
} as CalculatedDayData
})
// 计算总节省金额
const actualSavedYuan = calculatedDays.reduce((sum, day) => sum + day.dailySaving, 0)
// 计算节省力统计
const savingPowerStats = this.calculateSavingPowerStats(calculatedDays, weekGoalYuan)
return {
weekDays: calculatedDays,
actualSavedYuan,
savingPowerStats
}
}
// 生成一周的天数
private static generateWeekDays(weekStart: Date): Array<{ date: Date; isToday: boolean }> {
const days = []
const today = new Date()
for (let i = 0; i < 7; i++) {
const date = new Date(weekStart)
date.setDate(weekStart.getDate() + i)
days.push({
date,
isToday: this.isSameDay(date, today)
})
}
return days
}
// 计算每日状态
private static calculateDailyStatus(dailySpent: number, dailyBudget: number): DailyStatusType {
if (dailySpent === 0) return DAILY_STATUS.NORMAL
const ratio = dailySpent / dailyBudget
if (ratio <= 0.8) return DAILY_STATUS.SAVED
if (ratio <= 1.0) return DAILY_STATUS.NORMAL
if (ratio <= 1.2) return DAILY_STATUS.OVERSPENT
return DAILY_STATUS.EXTREME_OVERSPENT
}
// 格式化日期
private static formatDate(date: Date): string {
return date.toISOString().split('T')[0]
}
// 判断是否为同一天
private static isSameDay(date1: Date, date2: Date): boolean {
return date1.toDateString() === date2.toDateString()
}
}🔄 状态管理
Pinia Store
功能描述: 使用Pinia管理储蓄游戏状态
代码示例:
// 储蓄状态管理
export const useSavingsStore = defineStore('savings', () => {
// 状态
const weekGoalYuan = ref(0)
const actualSpentYuan = ref(0)
const savedYuan = ref(0)
const virtualBalanceYuan = ref(0)
const weeklyCalculationResult = ref<WeeklyCalculationResult | null>(null)
const petLevel = ref(1)
const petMood = ref('normal')
const isLoading = ref(false)
// 计算属性
const progressPercentage = computed(() => {
if (weekGoalYuan.value === 0) return 0
return Math.min(100, (savedYuan.value / weekGoalYuan.value) * 100)
})
const isGoalMet = computed(() => actualSpentYuan.value <= weekGoalYuan.value)
const savingPowerStats = computed(() =>
weeklyCalculationResult.value?.savingPowerStats || {
savedDays: 0,
totalSavingAmount: 0,
maxDailySaving: 0,
savingStreak: 0,
savingPower: 0,
savingRate: 0,
totalOverspent: 0,
healthStatus: 'healthy',
overBudgetRatio: 0
}
)
// 操作
const fetchSavingsOverview = async (): Promise<void> => {
isLoading.value = true
try {
const overview = await savingsService.getOverview()
weekGoalYuan.value = overview.weekGoal ? overview.weekGoal.goalCents / 100 : 0
actualSpentYuan.value = overview.actualSpentCents / 100
savedYuan.value = overview.savedCents / 100
virtualBalanceYuan.value = overview.virtualBalanceCents / 100
petLevel.value = overview.petLevel
petMood.value = overview.petMood
// 计算周数据
if (weekGoalYuan.value > 0) {
const weekRange = getCurrentWeekRange()
const expenseData = await getWeeklyExpenseData(weekRange.start, weekRange.end)
weeklyCalculationResult.value = SavingCalculator.calculateWeeklyBudget(
weekGoalYuan.value,
expenseData,
weekRange.start
)
}
} catch (error) {
console.error('获取储蓄概览失败:', error)
throw error
} finally {
isLoading.value = false
}
}
const setWeeklyGoal = async (goalYuan: number): Promise<void> => {
try {
const weekRange = getCurrentWeekRange()
await savingsService.setGoal({
weekStartTimestamp: weekRange.start.getTime(),
weekEndTimestamp: weekRange.end.getTime(),
goalCents: goalYuan * 100
})
weekGoalYuan.value = goalYuan
// 重新计算周数据
const expenseData = await getWeeklyExpenseData(weekRange.start, weekRange.end)
weeklyCalculationResult.value = SavingCalculator.calculateWeeklyBudget(
goalYuan,
expenseData,
weekRange.start
)
} catch (error) {
console.error('设置周目标失败:', error)
throw error
}
}
const withdrawFromVirtualBalance = async (amountYuan: number, reason: string): Promise<boolean> => {
try {
const result = await savingsService.withdraw({
amountCents: amountYuan * 100,
reason
})
if (result.success) {
virtualBalanceYuan.value = result.newBalanceCents / 100
return true
}
return false
} catch (error) {
console.error('提取虚拟余额失败:', error)
throw error
}
}
return {
weekGoalYuan,
actualSpentYuan,
savedYuan,
virtualBalanceYuan,
weeklyCalculationResult,
petLevel,
petMood,
isLoading,
progressPercentage,
isGoalMet,
savingPowerStats,
fetchSavingsOverview,
setWeeklyGoal,
withdrawFromVirtualBalance
}
})📚 最佳实践
1. 游戏化设计
- 清晰的进度反馈
- 有意义的奖励机制
- 适度的挑战难度
- 社交分享功能
2. 数据一致性
- 实时更新储蓄状态
- 同步虚拟交易记录
- 处理并发操作
- 数据验证和错误处理
3. 性能优化
- 缓存计算结果
- 懒加载历史数据
- 优化动画效果
- 减少不必要的计算
4. 用户体验
- 直观的视觉反馈
- 流畅的交互动画
- 清晰的成就提示
- 便捷的操作流程
Penny Lens 储蓄游戏模块 - 让储蓄变得更有趣、更有动力 🎮
