diff --git a/.claude/settings.local.json b/.claude/settings.local.json index f50d242..82d5634 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,10 @@ { "permissions": { "allow": [ - "Bash(./gradlew:*)" + "Bash(./gradlew:*)", + "Bash(git push:*)", + "Bash(git branch:*)", + "Bash(git add:*)" ], "deny": [] } diff --git a/README.md b/README.md index 25f8be3..2d193bd 100644 --- a/README.md +++ b/README.md @@ -69,14 +69,14 @@ - [x] 定期自动备份 - [x] 备份加密功能 -### 5. 预算管理 (进行中 🚀) +### 5. 预算管理 (基本完成 ✨) - [x] 预算数据模型设计 - [x] 数据库架构实现 -- [ ] 预算管理界面 -- [ ] 月度预算设置 +- [x] 预算管理界面 +- [x] 月度预算设置 - [ ] 预算超支提醒 -- [ ] 分类预算管理 -- [ ] 成员预算管理 +- [x] 分类预算管理 +- [x] 成员预算管理 - [ ] 预算分析报告 ### 6. 体验优化 (持续进行 🔄) @@ -127,6 +127,10 @@ - 预算数据模型设计 - 支持总预算、分类预算、成员预算 - 数据库架构实现(升级到版本6) + - 预算管理界面设计 + - 预算编辑对话框 + - 预算状态可视化(进度条、超支提醒) + - 预算导航集成 ### v1.4 - 数据安全功能 diff --git a/app/src/main/java/com/yovinchen/bookkeeping/ui/dialog/BudgetEditDialog.kt b/app/src/main/java/com/yovinchen/bookkeeping/ui/dialog/BudgetEditDialog.kt new file mode 100644 index 0000000..cb02f0d --- /dev/null +++ b/app/src/main/java/com/yovinchen/bookkeeping/ui/dialog/BudgetEditDialog.kt @@ -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, + members: List, + 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("取消") + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/yovinchen/bookkeeping/ui/navigation/MainNavigation.kt b/app/src/main/java/com/yovinchen/bookkeeping/ui/navigation/MainNavigation.kt index fa746af..4c7fd9f 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/ui/navigation/MainNavigation.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/ui/navigation/MainNavigation.kt @@ -47,6 +47,10 @@ sealed class Screen( "设置", iconResId = R.drawable.setting ) + object Budget : Screen( + "budget", + "预算管理" + ) object CategoryDetail : Screen( "category_detail/{category}/{startMonth}/{endMonth}", "分类详情" @@ -148,9 +152,16 @@ fun MainNavigation( composable(Screen.Settings.route) { SettingsScreen( currentTheme = currentTheme, - onThemeChange = onThemeChange + onThemeChange = onThemeChange, + onNavigateToBudget = { + navController.navigate(Screen.Budget.route) + } ) } + + composable(Screen.Budget.route) { + BudgetScreen() + } composable( route = Screen.CategoryDetail.route, diff --git a/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/BudgetScreen.kt b/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/BudgetScreen.kt new file mode 100644 index 0000000..44fe5a0 --- /dev/null +++ b/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/BudgetScreen.kt @@ -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, + 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, + 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, + 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) +} \ No newline at end of file diff --git a/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/SettingsScreen.kt b/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/SettingsScreen.kt index ea59a9f..b5e8805 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/SettingsScreen.kt @@ -31,6 +31,7 @@ import com.yovinchen.bookkeeping.viewmodel.* fun SettingsScreen( currentTheme: ThemeMode, onThemeChange: (ThemeMode) -> Unit, + onNavigateToBudget: () -> Unit = {}, viewModel: SettingsViewModel = viewModel(), memberViewModel: MemberViewModel = viewModel() ) { @@ -75,6 +76,17 @@ fun SettingsScreen( modifier = Modifier.clickable { showBackupDialog = true } ) + HorizontalDivider() + + // 预算管理设置项 + ListItem( + headlineContent = { Text("预算管理") }, + supportingContent = { Text("设置和管理预算") }, + modifier = Modifier.clickable { + onNavigateToBudget() + } + ) + HorizontalDivider() // 主题设置项 diff --git a/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/BudgetViewModel.kt b/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/BudgetViewModel.kt new file mode 100644 index 0000000..63a71a5 --- /dev/null +++ b/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/BudgetViewModel.kt @@ -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(null) + val totalBudgetStatus: StateFlow = _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(null) + val editingBudget: StateFlow = _editingBudget.asStateFlow() + + // 对话框显示状态 + private val _showBudgetDialog = MutableStateFlow(false) + val showBudgetDialog: StateFlow = _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() + } + } +} \ No newline at end of file