feat: 实现预算管理功能界面

1. 预算管理界面
   - 创建 BudgetScreen 预算管理主界面
   - 支持总览、分类预算、成员预算三个标签页
   - 实现预算状态可视化(进度条、超支提醒)
   - 预算项目的启用/禁用切换

2. 预算编辑功能
   - 创建 BudgetEditDialog 预算编辑对话框
   - 支持设置预算类型、金额、预警阈值
   - 分类预算和成员预算的选择器
   - 自动设置月度周期

3. 业务逻辑
   - 创建 BudgetViewModel 管理预算状态
   - 实现预算的创建、更新、删除功能
   - 预算状态的实时计算和更新

4. 导航集成
   - 在设置页面添加预算管理入口
   - 更新导航系统支持预算管理界面
   - 添加预算管理路由

5. 文档更新
   - 更新 README 版本历史
   - 标记预算管理功能为基本完成
   - 更新功能进度状态

注:界面已完成,待实现预算超支提醒和分析报告功能

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
yovinchen 2025-07-19 23:09:38 +08:00
parent 7fc76df829
commit e651086e6d
7 changed files with 856 additions and 7 deletions

View File

@ -1,7 +1,10 @@
{ {
"permissions": { "permissions": {
"allow": [ "allow": [
"Bash(./gradlew:*)" "Bash(./gradlew:*)",
"Bash(git push:*)",
"Bash(git branch:*)",
"Bash(git add:*)"
], ],
"deny": [] "deny": []
} }

View File

@ -69,14 +69,14 @@
- [x] 定期自动备份 - [x] 定期自动备份
- [x] 备份加密功能 - [x] 备份加密功能
### 5. 预算管理 (进行中 🚀) ### 5. 预算管理 (基本完成 ✨)
- [x] 预算数据模型设计 - [x] 预算数据模型设计
- [x] 数据库架构实现 - [x] 数据库架构实现
- [ ] 预算管理界面 - [x] 预算管理界面
- [ ] 月度预算设置 - [x] 月度预算设置
- [ ] 预算超支提醒 - [ ] 预算超支提醒
- [ ] 分类预算管理 - [x] 分类预算管理
- [ ] 成员预算管理 - [x] 成员预算管理
- [ ] 预算分析报告 - [ ] 预算分析报告
### 6. 体验优化 (持续进行 🔄) ### 6. 体验优化 (持续进行 🔄)
@ -127,6 +127,10 @@
- 预算数据模型设计 - 预算数据模型设计
- 支持总预算、分类预算、成员预算 - 支持总预算、分类预算、成员预算
- 数据库架构实现升级到版本6 - 数据库架构实现升级到版本6
- 预算管理界面设计
- 预算编辑对话框
- 预算状态可视化(进度条、超支提醒)
- 预算导航集成
### v1.4 ### v1.4
- 数据安全功能 - 数据安全功能

View File

@ -0,0 +1,223 @@
package com.yovinchen.bookkeeping.ui.dialog
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.yovinchen.bookkeeping.model.*
import java.util.*
/**
* 预算编辑对话框
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BudgetEditDialog(
budget: Budget? = null,
categories: List<Category>,
members: List<Member>,
onDismiss: () -> Unit,
onConfirm: (Budget) -> Unit
) {
var selectedType by remember { mutableStateOf(budget?.type ?: BudgetType.TOTAL) }
var amount by remember { mutableStateOf(budget?.amount?.toString() ?: "") }
var selectedCategory by remember { mutableStateOf(budget?.categoryName) }
var selectedMemberId by remember { mutableStateOf(budget?.memberId) }
var alertThreshold by remember { mutableStateOf((budget?.alertThreshold ?: 0.8) * 100) }
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(if (budget == null) "添加预算" else "编辑预算")
},
text = {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// 预算类型选择
Text(
text = "预算类型",
style = MaterialTheme.typography.labelMedium
)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
FilterChip(
selected = selectedType == BudgetType.TOTAL,
onClick = { selectedType = BudgetType.TOTAL },
label = { Text("总预算") }
)
FilterChip(
selected = selectedType == BudgetType.CATEGORY,
onClick = { selectedType = BudgetType.CATEGORY },
label = { Text("分类预算") }
)
FilterChip(
selected = selectedType == BudgetType.MEMBER,
onClick = { selectedType = BudgetType.MEMBER },
label = { Text("成员预算") }
)
}
// 金额输入
OutlinedTextField(
value = amount,
onValueChange = { amount = it.filter { char -> char.isDigit() || char == '.' } },
label = { Text("预算金额") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
// 分类选择(仅在分类预算时显示)
if (selectedType == BudgetType.CATEGORY) {
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = !expanded }
) {
OutlinedTextField(
value = selectedCategory ?: "",
onValueChange = {},
label = { Text("选择分类") },
readOnly = true,
modifier = Modifier
.fillMaxWidth()
.menuAnchor(),
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
}
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
categories.forEach { category ->
DropdownMenuItem(
text = { Text(category.name) },
onClick = {
selectedCategory = category.name
expanded = false
}
)
}
}
}
}
// 成员选择(仅在成员预算时显示)
if (selectedType == BudgetType.MEMBER) {
var expanded by remember { mutableStateOf(false) }
val selectedMember = members.find { it.id == selectedMemberId }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = !expanded }
) {
OutlinedTextField(
value = selectedMember?.name ?: "",
onValueChange = {},
label = { Text("选择成员") },
readOnly = true,
modifier = Modifier
.fillMaxWidth()
.menuAnchor(),
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
}
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
members.forEach { member ->
DropdownMenuItem(
text = { Text(member.name) },
onClick = {
selectedMemberId = member.id
expanded = false
}
)
}
}
}
}
// 预警阈值
Column {
Text(
text = "预警阈值: ${alertThreshold.toInt()}%",
style = MaterialTheme.typography.labelMedium
)
Slider(
value = alertThreshold.toFloat(),
onValueChange = { alertThreshold = it.toDouble() },
valueRange = 50f..95f,
steps = 8,
modifier = Modifier.fillMaxWidth()
)
Text(
text = "当使用金额达到预算的 ${alertThreshold.toInt()}% 时提醒",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
},
confirmButton = {
TextButton(
onClick = {
val amountValue = amount.toDoubleOrNull()
if (amountValue != null && amountValue > 0) {
val calendar = Calendar.getInstance()
val startDate = budget?.startDate ?: calendar.time
// 设置结束日期为当月最后一天
calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMaximum(Calendar.DAY_OF_MONTH))
calendar.set(Calendar.HOUR_OF_DAY, 23)
calendar.set(Calendar.MINUTE, 59)
calendar.set(Calendar.SECOND, 59)
val endDate = calendar.time
val newBudget = Budget(
id = budget?.id ?: 0,
type = selectedType,
amount = amountValue,
categoryName = if (selectedType == BudgetType.CATEGORY) selectedCategory else null,
memberId = if (selectedType == BudgetType.MEMBER) selectedMemberId else null,
startDate = startDate,
endDate = endDate,
alertThreshold = alertThreshold / 100,
isEnabled = budget?.isEnabled ?: true,
createdAt = budget?.createdAt ?: Date(),
updatedAt = Date()
)
onConfirm(newBudget)
}
},
enabled = amount.toDoubleOrNull() != null && amount.toDouble() > 0 &&
(selectedType != BudgetType.CATEGORY || selectedCategory != null) &&
(selectedType != BudgetType.MEMBER || selectedMemberId != null)
) {
Text("确定")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("取消")
}
}
)
}

View File

@ -47,6 +47,10 @@ sealed class Screen(
"设置", "设置",
iconResId = R.drawable.setting iconResId = R.drawable.setting
) )
object Budget : Screen(
"budget",
"预算管理"
)
object CategoryDetail : Screen( object CategoryDetail : Screen(
"category_detail/{category}/{startMonth}/{endMonth}", "category_detail/{category}/{startMonth}/{endMonth}",
"分类详情" "分类详情"
@ -148,10 +152,17 @@ fun MainNavigation(
composable(Screen.Settings.route) { composable(Screen.Settings.route) {
SettingsScreen( SettingsScreen(
currentTheme = currentTheme, currentTheme = currentTheme,
onThemeChange = onThemeChange onThemeChange = onThemeChange,
onNavigateToBudget = {
navController.navigate(Screen.Budget.route)
}
) )
} }
composable(Screen.Budget.route) {
BudgetScreen()
}
composable( composable(
route = Screen.CategoryDetail.route, route = Screen.CategoryDetail.route,
arguments = listOf( arguments = listOf(

View File

@ -0,0 +1,411 @@
package com.yovinchen.bookkeeping.ui.screen
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.yovinchen.bookkeeping.model.*
import com.yovinchen.bookkeeping.ui.dialog.BudgetEditDialog
import com.yovinchen.bookkeeping.viewmodel.BudgetViewModel
import com.yovinchen.bookkeeping.viewmodel.HomeViewModel
import com.yovinchen.bookkeeping.viewmodel.MemberViewModel
import java.text.NumberFormat
import java.util.Locale
/**
* 预算管理界面
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BudgetScreen(
viewModel: BudgetViewModel = viewModel(),
homeViewModel: HomeViewModel = viewModel(),
memberViewModel: MemberViewModel = viewModel()
) {
val budgetStatuses by viewModel.activeBudgetStatuses.collectAsState()
val totalBudgetStatus by viewModel.totalBudgetStatus.collectAsState()
val categoryBudgetStatuses by viewModel.categoryBudgetStatuses.collectAsState()
val memberBudgetStatuses by viewModel.memberBudgetStatuses.collectAsState()
val showBudgetDialog by viewModel.showBudgetDialog.collectAsState()
val editingBudget by viewModel.editingBudget.collectAsState()
val categories by homeViewModel.categories.collectAsState()
val members by memberViewModel.allMembers.collectAsState(initial = emptyList())
var selectedTab by remember { mutableStateOf(0) }
val tabs = listOf("总览", "分类预算", "成员预算")
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
// 页面标题
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "预算管理",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
// 添加预算按钮
IconButton(
onClick = { viewModel.showEditBudgetDialog() }
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "添加预算",
tint = MaterialTheme.colorScheme.primary
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// 总预算概览卡片
totalBudgetStatus?.let { status ->
BudgetOverviewCard(status)
Spacer(modifier = Modifier.height(16.dp))
}
// Tab 选择器
TabRow(selectedTabIndex = selectedTab) {
tabs.forEachIndexed { index, title ->
Tab(
selected = selectedTab == index,
onClick = { selectedTab = index },
text = { Text(title) }
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// Tab 内容
when (selectedTab) {
0 -> BudgetOverviewTab(budgetStatuses, viewModel)
1 -> CategoryBudgetTab(categoryBudgetStatuses, viewModel)
2 -> MemberBudgetTab(memberBudgetStatuses, viewModel)
}
}
// 预算编辑对话框
if (showBudgetDialog) {
BudgetEditDialog(
budget = editingBudget,
categories = categories.filter { it.type == TransactionType.EXPENSE },
members = members,
onDismiss = { viewModel.hideBudgetDialog() },
onConfirm = { budget ->
if (editingBudget == null) {
viewModel.createBudget(
type = budget.type,
amount = budget.amount,
categoryName = budget.categoryName,
memberId = budget.memberId,
alertThreshold = budget.alertThreshold
)
} else {
viewModel.updateBudget(budget)
}
viewModel.hideBudgetDialog()
}
)
}
}
/**
* 预算概览卡片
*/
@Composable
private fun BudgetOverviewCard(budgetStatus: BudgetStatus) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "本月总预算",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Text(
text = formatCurrency(budgetStatus.budget.amount),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
}
Spacer(modifier = Modifier.height(12.dp))
// 进度条
LinearProgressIndicator(
progress = budgetStatus.percentage.toFloat().coerceIn(0f, 1f),
modifier = Modifier
.fillMaxWidth()
.height(8.dp)
.clip(RoundedCornerShape(4.dp)),
color = when {
budgetStatus.isOverBudget -> MaterialTheme.colorScheme.error
budgetStatus.isNearLimit -> MaterialTheme.colorScheme.tertiary
else -> MaterialTheme.colorScheme.primary
}
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Column {
Text(
text = "已使用",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = formatCurrency(budgetStatus.spent),
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium
)
}
Column(horizontalAlignment = Alignment.End) {
Text(
text = "剩余",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = formatCurrency(budgetStatus.remaining),
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium,
color = when {
budgetStatus.isOverBudget -> MaterialTheme.colorScheme.error
budgetStatus.isNearLimit -> MaterialTheme.colorScheme.tertiary
else -> MaterialTheme.colorScheme.primary
}
)
}
}
if (budgetStatus.isOverBudget) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "⚠️ 已超出预算 ${formatCurrency(kotlin.math.abs(budgetStatus.remaining))}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
} else if (budgetStatus.isNearLimit) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "⚠️ 接近预算限制",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.tertiary,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
}
}
}
}
/**
* 预算总览标签页
*/
@Composable
private fun BudgetOverviewTab(
budgetStatuses: List<BudgetStatus>,
viewModel: BudgetViewModel
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(budgetStatuses) { status ->
BudgetItem(
budgetStatus = status,
onClick = { viewModel.showEditBudgetDialog(status.budget) },
onToggleEnabled = { viewModel.toggleBudgetEnabled(status.budget) }
)
}
}
}
/**
* 分类预算标签页
*/
@Composable
private fun CategoryBudgetTab(
categoryBudgetStatuses: List<BudgetStatus>,
viewModel: BudgetViewModel
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(categoryBudgetStatuses) { status ->
BudgetItem(
budgetStatus = status,
onClick = { viewModel.showEditBudgetDialog(status.budget) },
onToggleEnabled = { viewModel.toggleBudgetEnabled(status.budget) }
)
}
}
}
/**
* 成员预算标签页
*/
@Composable
private fun MemberBudgetTab(
memberBudgetStatuses: List<BudgetStatus>,
viewModel: BudgetViewModel
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(memberBudgetStatuses) { status ->
BudgetItem(
budgetStatus = status,
onClick = { viewModel.showEditBudgetDialog(status.budget) },
onToggleEnabled = { viewModel.toggleBudgetEnabled(status.budget) }
)
}
}
}
/**
* 预算项目组件
*/
@Composable
private fun BudgetItem(
budgetStatus: BudgetStatus,
onClick: () -> Unit,
onToggleEnabled: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() },
colors = CardDefaults.cardColors(
containerColor = if (budgetStatus.budget.isEnabled) {
MaterialTheme.colorScheme.surface
} else {
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
}
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = when (budgetStatus.budget.type) {
BudgetType.TOTAL -> "总预算"
BudgetType.CATEGORY -> budgetStatus.budget.categoryName ?: "未知分类"
BudgetType.MEMBER -> "成员预算"
},
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(4.dp))
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "预算: ${formatCurrency(budgetStatus.budget.amount)}",
style = MaterialTheme.typography.bodyMedium
)
Text(
text = "已用: ${formatCurrency(budgetStatus.spent)}",
style = MaterialTheme.typography.bodyMedium,
color = when {
budgetStatus.isOverBudget -> MaterialTheme.colorScheme.error
budgetStatus.isNearLimit -> MaterialTheme.colorScheme.tertiary
else -> MaterialTheme.colorScheme.onSurfaceVariant
}
)
}
Spacer(modifier = Modifier.height(8.dp))
LinearProgressIndicator(
progress = budgetStatus.percentage.toFloat().coerceIn(0f, 1f),
modifier = Modifier
.fillMaxWidth()
.height(4.dp)
.clip(RoundedCornerShape(2.dp)),
color = when {
budgetStatus.isOverBudget -> MaterialTheme.colorScheme.error
budgetStatus.isNearLimit -> MaterialTheme.colorScheme.tertiary
else -> MaterialTheme.colorScheme.primary
}
)
}
IconButton(onClick = onToggleEnabled) {
Icon(
imageVector = if (budgetStatus.budget.isEnabled) {
Icons.Default.CheckCircle
} else {
Icons.Default.Cancel
},
contentDescription = if (budgetStatus.budget.isEnabled) "禁用" else "启用",
tint = if (budgetStatus.budget.isEnabled) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
)
}
}
}
}
/**
* 格式化货币
*/
private fun formatCurrency(amount: Double): String {
val format = NumberFormat.getCurrencyInstance(Locale.CHINA)
return format.format(amount)
}

View File

@ -31,6 +31,7 @@ import com.yovinchen.bookkeeping.viewmodel.*
fun SettingsScreen( fun SettingsScreen(
currentTheme: ThemeMode, currentTheme: ThemeMode,
onThemeChange: (ThemeMode) -> Unit, onThemeChange: (ThemeMode) -> Unit,
onNavigateToBudget: () -> Unit = {},
viewModel: SettingsViewModel = viewModel(), viewModel: SettingsViewModel = viewModel(),
memberViewModel: MemberViewModel = viewModel() memberViewModel: MemberViewModel = viewModel()
) { ) {
@ -77,6 +78,17 @@ fun SettingsScreen(
HorizontalDivider() HorizontalDivider()
// 预算管理设置项
ListItem(
headlineContent = { Text("预算管理") },
supportingContent = { Text("设置和管理预算") },
modifier = Modifier.clickable {
onNavigateToBudget()
}
)
HorizontalDivider()
// 主题设置项 // 主题设置项
ListItem( ListItem(
headlineContent = { Text("主题设置") }, headlineContent = { Text("主题设置") },

View File

@ -0,0 +1,185 @@
package com.yovinchen.bookkeeping.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.yovinchen.bookkeeping.data.BookkeepingDatabase
import com.yovinchen.bookkeeping.data.BudgetRepository
import com.yovinchen.bookkeeping.model.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import java.util.*
import java.util.Calendar
/**
* 预算管理 ViewModel
* 负责预算相关的业务逻辑和状态管理
*/
class BudgetViewModel(application: Application) : AndroidViewModel(application) {
private val database = BookkeepingDatabase.getDatabase(application)
private val budgetRepository = BudgetRepository(
database.budgetDao(),
database.bookkeepingDao(),
database.memberDao()
)
// 所有预算列表
val allBudgets = budgetRepository.getAllBudgets()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
// 当前活跃的预算状态
val activeBudgetStatuses = budgetRepository.getActiveBudgetStatuses()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
// 总预算状态
private val _totalBudgetStatus = MutableStateFlow<BudgetStatus?>(null)
val totalBudgetStatus: StateFlow<BudgetStatus?> = _totalBudgetStatus.asStateFlow()
// 分类预算状态
val categoryBudgetStatuses = budgetRepository.getCategoryBudgetStatuses()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
// 成员预算状态
val memberBudgetStatuses = budgetRepository.getMemberBudgetStatuses()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
// 编辑中的预算
private val _editingBudget = MutableStateFlow<Budget?>(null)
val editingBudget: StateFlow<Budget?> = _editingBudget.asStateFlow()
// 对话框显示状态
private val _showBudgetDialog = MutableStateFlow(false)
val showBudgetDialog: StateFlow<Boolean> = _showBudgetDialog.asStateFlow()
init {
// 初始化时加载总预算状态
loadTotalBudgetStatus()
}
/**
* 加载总预算状态
*/
private fun loadTotalBudgetStatus() {
viewModelScope.launch {
_totalBudgetStatus.value = budgetRepository.getTotalBudgetStatus()
}
}
/**
* 创建新预算
*/
fun createBudget(
type: BudgetType,
amount: Double,
categoryName: String? = null,
memberId: Int? = null,
alertThreshold: Double = 0.8
) {
viewModelScope.launch {
val calendar = Calendar.getInstance()
val startDate = calendar.time
// 设置结束日期为当月最后一天
calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMaximum(Calendar.DAY_OF_MONTH))
calendar.set(Calendar.HOUR_OF_DAY, 23)
calendar.set(Calendar.MINUTE, 59)
calendar.set(Calendar.SECOND, 59)
val endDate = calendar.time
val budget = Budget(
type = type,
amount = amount,
categoryName = categoryName,
memberId = memberId,
startDate = startDate,
endDate = endDate,
alertThreshold = alertThreshold
)
budgetRepository.createBudget(budget)
loadTotalBudgetStatus()
}
}
/**
* 更新预算
*/
fun updateBudget(budget: Budget) {
viewModelScope.launch {
budgetRepository.updateBudget(budget)
loadTotalBudgetStatus()
}
}
/**
* 删除预算
*/
fun deleteBudget(budget: Budget) {
viewModelScope.launch {
budgetRepository.deleteBudget(budget)
loadTotalBudgetStatus()
}
}
/**
* 切换预算启用状态
*/
fun toggleBudgetEnabled(budget: Budget) {
viewModelScope.launch {
budgetRepository.updateBudgetEnabled(budget.id, !budget.isEnabled)
loadTotalBudgetStatus()
}
}
/**
* 显示预算编辑对话框
*/
fun showEditBudgetDialog(budget: Budget? = null) {
_editingBudget.value = budget
_showBudgetDialog.value = true
}
/**
* 隐藏预算编辑对话框
*/
fun hideBudgetDialog() {
_showBudgetDialog.value = false
_editingBudget.value = null
}
/**
* 检查预算警报
*/
fun checkBudgetAlerts() {
viewModelScope.launch {
val alerts = budgetRepository.checkBudgetAlerts()
// 这里可以触发通知或其他警报机制
// 暂时先不实现,等完成通知功能后再补充
}
}
/**
* 清理过期预算
*/
fun cleanupExpiredBudgets() {
viewModelScope.launch {
budgetRepository.cleanupExpiredBudgets()
}
}
}