From 8339d3d5da0871c373e174d14b3e593cbee4efc7 Mon Sep 17 00:00:00 2001 From: yovinchen Date: Thu, 28 Nov 2024 14:21:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=B1=BB=E5=88=AB?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增类别详情相关组件和视图模型 2. 优化饼图显示效果 3. 完善导航系统 4. 改进数据查询接口 --- .../bookkeeping/data/BookkeepingDao.kt | 11 + .../bookkeeping/model/AnalysisType.kt | 7 + .../bookkeeping/model/CategoryStat.kt | 8 + .../ui/components/CategoryPieChart.kt | 14 +- .../ui/components/CategoryStatItem.kt | 71 +++++ .../bookkeeping/ui/components/RecordItem.kt | 9 +- .../ui/navigation/MainNavigation.kt | 45 ++- .../bookkeeping/ui/screen/AnalysisScreen.kt | 265 +++++------------- .../ui/screen/CategoryDetailScreen.kt | 147 ++++++++++ .../viewmodel/AnalysisViewModel.kt | 13 +- .../viewmodel/CategoryDetailViewModel.kt | 52 ++++ .../CategoryDetailViewModelFactory.kt | 20 ++ gradle.properties | 4 +- 13 files changed, 450 insertions(+), 216 deletions(-) create mode 100644 app/src/main/java/com/yovinchen/bookkeeping/model/AnalysisType.kt create mode 100644 app/src/main/java/com/yovinchen/bookkeeping/model/CategoryStat.kt create mode 100644 app/src/main/java/com/yovinchen/bookkeeping/ui/components/CategoryStatItem.kt create mode 100644 app/src/main/java/com/yovinchen/bookkeeping/ui/screen/CategoryDetailScreen.kt create mode 100644 app/src/main/java/com/yovinchen/bookkeeping/viewmodel/CategoryDetailViewModel.kt create mode 100644 app/src/main/java/com/yovinchen/bookkeeping/viewmodel/CategoryDetailViewModelFactory.kt diff --git a/app/src/main/java/com/yovinchen/bookkeeping/data/BookkeepingDao.kt b/app/src/main/java/com/yovinchen/bookkeeping/data/BookkeepingDao.kt index 78aea1a..9d5bba7 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/data/BookkeepingDao.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/data/BookkeepingDao.kt @@ -27,6 +27,17 @@ interface BookkeepingDao { @Query("SELECT SUM(amount) FROM bookkeeping_records WHERE type = :type AND (memberId = :memberId OR memberId IS NULL)") fun getTotalAmountByType(type: TransactionType, memberId: Int? = null): Flow + @Query(""" + SELECT * FROM bookkeeping_records + WHERE category = :category + AND strftime('%Y-%m', datetime(date/1000, 'unixepoch')) = :yearMonth + ORDER BY date DESC + """) + fun getRecordsByCategoryAndMonth( + category: String, + yearMonth: String + ): Flow> + @Insert suspend fun insertRecord(record: BookkeepingRecord): Long diff --git a/app/src/main/java/com/yovinchen/bookkeeping/model/AnalysisType.kt b/app/src/main/java/com/yovinchen/bookkeeping/model/AnalysisType.kt new file mode 100644 index 0000000..1220cd3 --- /dev/null +++ b/app/src/main/java/com/yovinchen/bookkeeping/model/AnalysisType.kt @@ -0,0 +1,7 @@ +package com.yovinchen.bookkeeping.model + +enum class AnalysisType { + EXPENSE, + INCOME, + TREND +} diff --git a/app/src/main/java/com/yovinchen/bookkeeping/model/CategoryStat.kt b/app/src/main/java/com/yovinchen/bookkeeping/model/CategoryStat.kt new file mode 100644 index 0000000..0411e50 --- /dev/null +++ b/app/src/main/java/com/yovinchen/bookkeeping/model/CategoryStat.kt @@ -0,0 +1,8 @@ +package com.yovinchen.bookkeeping.model + +data class CategoryStat( + val category: String, + val amount: Double, + val count: Int = 0, + val percentage: Double = 0.0 +) diff --git a/app/src/main/java/com/yovinchen/bookkeeping/ui/components/CategoryPieChart.kt b/app/src/main/java/com/yovinchen/bookkeeping/ui/components/CategoryPieChart.kt index caac939..e0853bc 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/ui/components/CategoryPieChart.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/ui/components/CategoryPieChart.kt @@ -35,19 +35,19 @@ fun CategoryPieChart( description.isEnabled = false setUsePercentValues(true) setDrawEntryLabels(true) - + // 禁用图例显示 legend.isEnabled = false - + isDrawHoleEnabled = true holeRadius = 40f setHoleColor(AndroidColor.TRANSPARENT) setTransparentCircleRadius(45f) - - // 设置标签文字颜色为白色(因为标签在彩色扇形上) - setEntryLabelColor(AndroidColor.WHITE) + + // 设置标签文字颜色 + setEntryLabelColor(textColor) setEntryLabelTextSize(12f) - + // 设置中心文字颜色跟随主题 setCenterTextColor(textColor) } @@ -61,7 +61,7 @@ fun CategoryPieChart( colors = ColorTemplate.MATERIAL_COLORS.toList() valueTextSize = 14f valueFormatter = PercentFormatter(chart) - valueTextColor = AndroidColor.WHITE // 扇形上的数值文字保持白色 + valueTextColor = textColor setDrawValues(true) } diff --git a/app/src/main/java/com/yovinchen/bookkeeping/ui/components/CategoryStatItem.kt b/app/src/main/java/com/yovinchen/bookkeeping/ui/components/CategoryStatItem.kt new file mode 100644 index 0000000..a783421 --- /dev/null +++ b/app/src/main/java/com/yovinchen/bookkeeping/ui/components/CategoryStatItem.kt @@ -0,0 +1,71 @@ +package com.yovinchen.bookkeeping.ui.components + +import android.annotation.SuppressLint +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.yovinchen.bookkeeping.model.CategoryStat + +@SuppressLint("DefaultLocale") +@Composable +fun CategoryStatItem( + stat: CategoryStat, + onClick: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(vertical = 8.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stat.category, + style = MaterialTheme.typography.bodyLarge + ) + Text( + text = String.format("%.2f", stat.amount), + style = MaterialTheme.typography.bodyLarge + ) + } + + Spacer(modifier = Modifier.height(4.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + LinearProgressIndicator( + progress = { stat.percentage.toFloat() / 100f }, + modifier = Modifier + .weight(1f) + .height(8.dp) + .background( + MaterialTheme.colorScheme.surfaceVariant, + RoundedCornerShape(4.dp) + ), + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = String.format("%.1f%%", stat.percentage), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} diff --git a/app/src/main/java/com/yovinchen/bookkeeping/ui/components/RecordItem.kt b/app/src/main/java/com/yovinchen/bookkeeping/ui/components/RecordItem.kt index cad2220..0b41365 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/ui/components/RecordItem.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/ui/components/RecordItem.kt @@ -22,6 +22,7 @@ fun RecordItem( members: List = emptyList() ) { var showDeleteDialog by remember { mutableStateOf(false) } +// val dateFormat = remember { SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) } val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) } val member = members.find { it.id == record.memberId } @@ -48,14 +49,18 @@ fun RecordItem( style = MaterialTheme.typography.bodyLarge ) - // 第二行:时间 | 成员 | 详情 + // 第二行:日期和时间 | 成员 | 详情 Text( text = buildString { +// append(dateFormat.format(record.date)) +// append(" ") append(timeFormat.format(record.date)) - if (member != null && member.name != "自己") { +// if (member != null && member.name != "自己") { append(" | ") + if (member != null) { append(member.name) } +// } if (record.description.isNotEmpty()) { append(" | ") append(record.description) 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 71f714e..ee73056 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 @@ -3,6 +3,7 @@ package com.yovinchen.bookkeeping.ui.navigation import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.List import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.outlined.Analytics import androidx.compose.material3.ExperimentalMaterial3Api @@ -16,14 +17,19 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument import com.yovinchen.bookkeeping.model.ThemeMode import com.yovinchen.bookkeeping.ui.screen.AnalysisScreen +import com.yovinchen.bookkeeping.ui.screen.CategoryDetailScreen import com.yovinchen.bookkeeping.ui.screen.HomeScreen import com.yovinchen.bookkeeping.ui.screen.SettingsScreen +import java.time.YearMonth +import java.time.format.DateTimeFormatter sealed class Screen( val route: String, @@ -33,6 +39,14 @@ sealed class Screen( data object Home : Screen("home", Icons.Default.Home, "主页") data object Analysis : Screen("analysis", Icons.Outlined.Analytics, "分析") data object Settings : Screen("settings", Icons.Default.Settings, "设置") + data object CategoryDetail : Screen( + "category_detail/{category}/{yearMonth}", + Icons.Default.List, + "分类详情" + ) { + fun createRoute(category: String, yearMonth: String) = + "category_detail/$category/$yearMonth" + } } @OptIn(ExperimentalMaterial3Api::class) @@ -78,12 +92,37 @@ fun MainNavigation( modifier = Modifier.padding(innerPadding) ) { composable(Screen.Home.route) { HomeScreen() } - composable(Screen.Analysis.route) { AnalysisScreen() } - composable(Screen.Settings.route) { + composable(Screen.Analysis.route) { + AnalysisScreen( + onNavigateToCategoryDetail = { category, month -> + val monthStr = month.format(DateTimeFormatter.ofPattern("yyyy-MM")) + navController.navigate(Screen.CategoryDetail.createRoute(category, monthStr)) + } + ) + } + composable(Screen.Settings.route) { SettingsScreen( currentTheme = currentTheme, onThemeChange = onThemeChange - ) + ) + } + + composable( + route = Screen.CategoryDetail.route, + arguments = listOf( + navArgument("category") { type = NavType.StringType }, + navArgument("yearMonth") { type = NavType.StringType } + ) + ) { backStackEntry -> + val category = backStackEntry.arguments?.getString("category") ?: return@composable + val yearMonth = YearMonth.parse( + backStackEntry.arguments?.getString("yearMonth") ?: return@composable + ) + CategoryDetailScreen( + category = category, + month = yearMonth, + onBack = { navController.popBackStack() } + ) } } } diff --git a/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/AnalysisScreen.kt b/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/AnalysisScreen.kt index 0d0dbe9..dc7708b 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/AnalysisScreen.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/AnalysisScreen.kt @@ -1,228 +1,109 @@ package com.yovinchen.bookkeeping.ui.screen -import android.annotation.SuppressLint -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width +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.automirrored.filled.KeyboardArrowLeft -import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight -import androidx.compose.material3.FilterChip -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.yovinchen.bookkeeping.model.AnalysisType import com.yovinchen.bookkeeping.ui.components.CategoryPieChart +import com.yovinchen.bookkeeping.ui.components.CategoryStatItem import com.yovinchen.bookkeeping.ui.components.MonthYearPicker -import com.yovinchen.bookkeeping.viewmodel.AnalysisType import com.yovinchen.bookkeeping.viewmodel.AnalysisViewModel -import com.yovinchen.bookkeeping.viewmodel.CategoryStat +import java.time.YearMonth +import java.time.format.DateTimeFormatter +@OptIn(ExperimentalMaterial3Api::class) @Composable fun AnalysisScreen( - modifier: Modifier = Modifier, - viewModel: AnalysisViewModel = viewModel() + onNavigateToCategoryDetail: (String, YearMonth) -> Unit ) { + val viewModel: AnalysisViewModel = viewModel() val selectedMonth by viewModel.selectedMonth.collectAsState() - val selectedType by viewModel.selectedAnalysisType.collectAsState() + val selectedAnalysisType by viewModel.selectedAnalysisType.collectAsState() val categoryStats by viewModel.categoryStats.collectAsState() + var showMonthPicker by remember { mutableStateOf(false) } - LazyColumn( - modifier = modifier - .fillMaxSize() - .padding(16.dp) - ) { - // 月份选择器 - item { + Scaffold { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + // 月份选择器和类型切换 Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - IconButton(onClick = { - viewModel.setSelectedMonth(selectedMonth.minusMonths(1)) - }) { - Icon(Icons.AutoMirrored.Filled.KeyboardArrowLeft, "上个月") + // 月份选择按钮 + Button(onClick = { showMonthPicker = true }) { + Text(selectedMonth.format(DateTimeFormatter.ofPattern("yyyy年MM月"))) } - Text( - text = "${selectedMonth.year}年${selectedMonth.monthValue}月", - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.clickable { showMonthPicker = true } - ) - - IconButton(onClick = { - viewModel.setSelectedMonth(selectedMonth.plusMonths(1)) - }) { - Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, "下个月") - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - // 分析类型选择 - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly - ) { - AnalysisType.entries.forEach { type -> - FilterChip( - selected = selectedType == type, - onClick = { viewModel.setAnalysisType(type) }, - label = { - Text( - when (type) { - AnalysisType.EXPENSE -> "支出分析" - AnalysisType.INCOME -> "收入分析" - AnalysisType.TREND -> "收支趋势" - } - ) - } - ) - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - // 统计内容 - when (selectedType) { - AnalysisType.EXPENSE, AnalysisType.INCOME -> { - Text( - text = if (selectedType == AnalysisType.EXPENSE) "" else "", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(vertical = 8.dp) - ) - - if (categoryStats.isNotEmpty()) { - val pieChartData = categoryStats.map { stat -> - stat.category to stat.percentage.toFloat() - } - CategoryPieChart( - categoryData = pieChartData, - modifier = Modifier - .fillMaxWidth() - .height(300.dp) - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = "分类明细", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(vertical = 8.dp) - ) - } else { - Text( - text = "暂无数据", - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .padding(32.dp) + // 类型切换 + Row { + AnalysisType.values().forEach { type -> + FilterChip( + selected = selectedAnalysisType == type, + onClick = { viewModel.setAnalysisType(type) }, + label = { + Text( + when (type) { + AnalysisType.EXPENSE -> "支出" + AnalysisType.INCOME -> "收入" + AnalysisType.TREND -> "趋势" + } + ) + }, + modifier = Modifier.padding(horizontal = 4.dp) ) } } - AnalysisType.TREND -> { - Text( - text = "收支趋势分析(开发中)", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(vertical = 8.dp) + } + + // 饼图 + if (selectedAnalysisType != AnalysisType.TREND) { + CategoryPieChart( + categoryData = categoryStats.map { Pair(it.category, it.percentage.toFloat()) }, + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + .padding(16.dp) + ) + } + + // 分类统计列表 + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(16.dp) + ) { + items(categoryStats) { stat -> + CategoryStatItem( + stat = stat, + onClick = { onNavigateToCategoryDetail(stat.category, selectedMonth) } ) } } } - // 分类统计列表 - if (selectedType != AnalysisType.TREND && categoryStats.isNotEmpty()) { - items(categoryStats) { stat -> - CategoryStatItem(stat) - } - } - } - - if (showMonthPicker) { - MonthYearPicker( - selectedMonth = selectedMonth, - onMonthSelected = { month -> - viewModel.setSelectedMonth(month) - showMonthPicker = false - }, - onDismiss = { showMonthPicker = false } - ) - } -} - -@SuppressLint("DefaultLocale") -@Composable -fun CategoryStatItem(stat: CategoryStat) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stat.category, - style = MaterialTheme.typography.bodyLarge - ) - Text( - text = String.format("%.2f", stat.amount), - style = MaterialTheme.typography.bodyLarge - ) - } - - Spacer(modifier = Modifier.height(4.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - LinearProgressIndicator( - progress = { stat.percentage.toFloat() / 100f }, - modifier = Modifier - .weight(1f) - .height(8.dp) - .background( - MaterialTheme.colorScheme.surfaceVariant, - RoundedCornerShape(4.dp) - ), - ) - - Spacer(modifier = Modifier.width(8.dp)) - - Text( - text = String.format("%.1f%%", stat.percentage), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + // 月份选择器对话框 + if (showMonthPicker) { + MonthYearPicker( + selectedMonth = selectedMonth, + onMonthSelected = { + viewModel.setSelectedMonth(it) + showMonthPicker = false + }, + onDismiss = { showMonthPicker = false } ) } } diff --git a/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/CategoryDetailScreen.kt b/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/CategoryDetailScreen.kt new file mode 100644 index 0000000..5432d81 --- /dev/null +++ b/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/CategoryDetailScreen.kt @@ -0,0 +1,147 @@ +package com.yovinchen.bookkeeping.ui.screen + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.yovinchen.bookkeeping.data.BookkeepingDatabase +import com.yovinchen.bookkeeping.model.BookkeepingRecord +import com.yovinchen.bookkeeping.ui.components.RecordItem +import com.yovinchen.bookkeeping.viewmodel.CategoryDetailViewModel +import com.yovinchen.bookkeeping.viewmodel.CategoryDetailViewModelFactory +import java.text.SimpleDateFormat +import java.time.YearMonth +import java.time.format.DateTimeFormatter +import java.util.Locale + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CategoryDetailScreen( + category: String, + month: YearMonth, + onBack: () -> Unit +) { + val context = LocalContext.current + val database = remember { BookkeepingDatabase.getDatabase(context) } + val viewModel: CategoryDetailViewModel = viewModel( + factory = CategoryDetailViewModelFactory(database, category, month) + ) + + val records by viewModel.records.collectAsState() + val total by viewModel.total.collectAsState() + val members by viewModel.members.collectAsState() + val groupedRecords = remember(records) { + records.groupBy { record -> + SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(record.date) + } + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("$category - ${month.format(DateTimeFormatter.ofPattern("yyyy年MM月"))}") }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.Default.ArrowBack, contentDescription = "返回") + } + } + ) + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + // 总金额显示 + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "总金额", + style = MaterialTheme.typography.titleMedium + ) + Text( + text = String.format("%.2f", total), + style = MaterialTheme.typography.titleLarge + ) + } + } + + // 记录列表 + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + groupedRecords.forEach { (date, dayRecords) -> + item { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + // 日期标签 + Text( + text = SimpleDateFormat( + "yyyy年MM月dd日 E", + Locale.CHINESE + ).format(dayRecords.first().date), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // 当天的记录 + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + dayRecords.forEachIndexed { index, record -> + RecordItem( + record = record, + onClick = {}, + members = members + ) + + if (index < dayRecords.size - 1) { + HorizontalDivider( + modifier = Modifier.padding(vertical = 4.dp), + color = MaterialTheme.colorScheme.surfaceVariant, + thickness = 0.5.dp + ) + } + } + } + } + } + } + } + } + } + } +} diff --git a/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/AnalysisViewModel.kt b/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/AnalysisViewModel.kt index 5288525..3d07c3b 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/AnalysisViewModel.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/AnalysisViewModel.kt @@ -4,6 +4,8 @@ import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.yovinchen.bookkeeping.data.BookkeepingDatabase +import com.yovinchen.bookkeeping.model.AnalysisType +import com.yovinchen.bookkeeping.model.CategoryStat import com.yovinchen.bookkeeping.model.TransactionType import kotlinx.coroutines.flow.* import java.time.LocalDateTime @@ -59,14 +61,3 @@ class AnalysisViewModel(application: Application) : AndroidViewModel(application _selectedAnalysisType.value = type } } - -enum class AnalysisType { - EXPENSE, INCOME, TREND -} - -data class CategoryStat( - val category: String, - val amount: Double, - val count: Int, - val percentage: Double = 0.0 -) diff --git a/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/CategoryDetailViewModel.kt b/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/CategoryDetailViewModel.kt new file mode 100644 index 0000000..1379818 --- /dev/null +++ b/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/CategoryDetailViewModel.kt @@ -0,0 +1,52 @@ +package com.yovinchen.bookkeeping.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.yovinchen.bookkeeping.data.BookkeepingDatabase +import com.yovinchen.bookkeeping.model.BookkeepingRecord +import com.yovinchen.bookkeeping.model.Member +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import java.time.YearMonth +import java.time.format.DateTimeFormatter + +class CategoryDetailViewModel( + private val database: BookkeepingDatabase, + private val category: String, + private val month: YearMonth +) : ViewModel() { + private val _records = MutableStateFlow>(emptyList()) + val records: StateFlow> = _records + + private val _total = MutableStateFlow(0.0) + val total: StateFlow = _total + + private val _members = MutableStateFlow>(emptyList()) + val members: StateFlow> = _members + + init { + loadRecords() + loadMembers() + } + + private fun loadRecords() { + viewModelScope.launch { + val monthStr = month.format(DateTimeFormatter.ofPattern("yyyy-MM")) + database.bookkeepingDao().getRecordsByCategoryAndMonth(category, monthStr) + .collect { records -> + _records.value = records + _total.value = records.sumOf { it.amount } + } + } + } + + private fun loadMembers() { + viewModelScope.launch { + database.memberDao().getAllMembers().collect { members -> + _members.value = members + } + } + } +} diff --git a/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/CategoryDetailViewModelFactory.kt b/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/CategoryDetailViewModelFactory.kt new file mode 100644 index 0000000..1a4e83d --- /dev/null +++ b/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/CategoryDetailViewModelFactory.kt @@ -0,0 +1,20 @@ +package com.yovinchen.bookkeeping.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.yovinchen.bookkeeping.data.BookkeepingDatabase +import java.time.YearMonth + +class CategoryDetailViewModelFactory( + private val database: BookkeepingDatabase, + private val category: String, + private val month: YearMonth +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(CategoryDetailViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return CategoryDetailViewModel(database, category, month) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} diff --git a/gradle.properties b/gradle.properties index 20e2a01..1ea80a2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,6 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true +# Kotlin +org.gradle.java.home=/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home \ No newline at end of file