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:
parent
7fc76df829
commit
e651086e6d
@ -1,7 +1,10 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(./gradlew:*)"
|
||||
"Bash(./gradlew:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(git branch:*)",
|
||||
"Bash(git add:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
14
README.md
14
README.md
@ -69,14 +69,14 @@
|
||||
- [x] 定期自动备份
|
||||
- [x] 备份加密功能
|
||||
|
||||
### 5. 预算管理 (进行中 🚀)
|
||||
### 5. 预算管理 (基本完成 ✨)
|
||||
- [x] 预算数据模型设计
|
||||
- [x] 数据库架构实现
|
||||
- [ ] 预算管理界面
|
||||
- [ ] 月度预算设置
|
||||
- [x] 预算管理界面
|
||||
- [x] 月度预算设置
|
||||
- [ ] 预算超支提醒
|
||||
- [ ] 分类预算管理
|
||||
- [ ] 成员预算管理
|
||||
- [x] 分类预算管理
|
||||
- [x] 成员预算管理
|
||||
- [ ] 预算分析报告
|
||||
|
||||
### 6. 体验优化 (持续进行 🔄)
|
||||
@ -127,6 +127,10 @@
|
||||
- 预算数据模型设计
|
||||
- 支持总预算、分类预算、成员预算
|
||||
- 数据库架构实现(升级到版本6)
|
||||
- 预算管理界面设计
|
||||
- 预算编辑对话框
|
||||
- 预算状态可视化(进度条、超支提醒)
|
||||
- 预算导航集成
|
||||
|
||||
### v1.4
|
||||
- 数据安全功能
|
||||
|
@ -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("取消")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
@ -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()
|
||||
|
||||
// 主题设置项
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user