From af880c23eb4fee0b466fed8f9f48cbaba36f7067 Mon Sep 17 00:00:00 2001 From: yovinchen Date: Wed, 27 Nov 2024 17:49:47 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=88=86=E6=9E=90=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=EF=BC=8C=E5=AE=8C=E5=96=84=E5=A4=A7=E4=BD=93=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=E5=86=85=E5=AE=B9=20-=20=E9=A1=B6=E9=83=A8=E6=9C=88?= =?UTF-8?q?=E4=BB=BD=E9=80=89=E6=8B=A9=E5=99=A8=EF=BC=9A=E5=8F=AF=E4=BB=A5?= =?UTF-8?q?=E5=89=8D=E5=90=8E=E5=88=87=E6=8D=A2=E6=9C=88=E4=BB=BD=E6=88=96?= =?UTF-8?q?=E7=9B=B4=E6=8E=A5=E9=80=89=E6=8B=A9=E5=85=B7=E4=BD=93=E6=9C=88?= =?UTF-8?q?=E4=BB=BD=20-=20=E5=88=86=E6=9E=90=E7=B1=BB=E5=9E=8B=E5=88=87?= =?UTF-8?q?=E6=8D=A2=EF=BC=9A=E6=94=AF=E5=87=BA=E5=88=86=E6=9E=90/?= =?UTF-8?q?=E6=94=B6=E5=85=A5=E5=88=86=E6=9E=90/=E6=94=B6=E6=94=AF?= =?UTF-8?q?=E8=B6=8B=E5=8A=BF=20-=20=E6=95=B0=E6=8D=AE=E5=8F=AF=E8=A7=86?= =?UTF-8?q?=E5=8C=96=EF=BC=9A=20-=20=E4=BD=BF=E7=94=A8=E9=A5=BC=E5=9B=BE?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=E5=90=84=E5=88=86=E7=B1=BB=E5=8D=A0=E6=AF=94?= =?UTF-8?q?=20-=20=E4=BD=BF=E7=94=A8=E5=88=97=E8=A1=A8=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E8=AF=A6=E7=BB=86=E6=95=B0=E6=8D=AE=EF=BC=8C=E5=8C=85=E6=8B=AC?= =?UTF-8?q?=E9=87=91=E9=A2=9D=E3=80=81=E7=99=BE=E5=88=86=E6=AF=94=E5=92=8C?= =?UTF-8?q?=E8=BF=9B=E5=BA=A6=E6=9D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 15 +- app/build.gradle.kts | 4 + .../bookkeeping/model/BookkeepingRecord.kt | 4 + .../ui/components/CategoryPieChart.kt | 56 +++++ .../ui/components/MonthYearPicker.kt | 88 +++++++ .../ui/navigation/MainNavigation.kt | 65 +++--- .../bookkeeping/ui/screen/AnalysisScreen.kt | 214 ++++++++++++++++++ .../viewmodel/AnalysisViewModel.kt | 72 ++++++ gradle/libs.versions.toml | 2 + settings.gradle.kts | 2 + 10 files changed, 476 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/com/yovinchen/bookkeeping/ui/components/CategoryPieChart.kt create mode 100644 app/src/main/java/com/yovinchen/bookkeeping/ui/components/MonthYearPicker.kt create mode 100644 app/src/main/java/com/yovinchen/bookkeeping/ui/screen/AnalysisScreen.kt create mode 100644 app/src/main/java/com/yovinchen/bookkeeping/viewmodel/AnalysisViewModel.kt diff --git a/README.md b/README.md index f8ad555..1e03c8d 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ ## 🗺 开发路线图 -### 1. 基础记账 (已完成 ✨) +### 0. 基础记账 (已完成 ✨) - [x] 收入/支出记录管理 - [x] 分类管理系统 - [x] 自定义日期选择器 @@ -33,41 +33,40 @@ - [x] 深色/浅色主题切换 - [x] 主题色自定义 -### 2. 成员系统 (已完成 🎉) +### 1. 成员系统 (已完成 🎉) - [x] 成员添加/编辑/删除 - [x] 记账时选择相关成员 - [x] 主页账单修改相关成员 - [x] 成员消费统计 -### 3. 数据分析 (进行中 🚀) +### 2. 图表分析 (进行中 🚀) - [ ] 支出/收入趋势图表 - [ ] 分类占比饼图 - [ ] 月度/年度报表 - [ ] 成员消费分析 - [ ] 自定义统计周期 -### 4. 数据管理 (计划中 📝) +### 3. 数据管理 (计划中 📝) - [ ] 导出 CSV/Excel 功能 -- [ ] 云端备份支持 - [ ] 数据迁移工具 - [ ] 定期自动备份 - [ ] 备份加密功能 -### 5. 预算管理 (计划中 💡) +### 4. 预算管理 (计划中 💡) - [ ] 月度预算设置 - [ ] 预算超支提醒 - [ ] 分类预算管理 - [ ] 成员预算管理 - [ ] 预算分析报告 -### 6. 体验优化 (持续进行 🔄) +### 5. 体验优化 (持续进行 🔄) - [x] 深色模式支持 - [ ] 手势操作优化 - [ ] 快速记账小组件 - [ ] 多语言支持 - [ ] 自定义主题 -### 7. 性能提升 (持续进行 ⚡️) +### 6. 性能提升 (持续进行 ⚡️) - [ ] 大数据量处理优化 - [ ] 启动速度优化 - [ ] 内存使用优化 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e3d89d3..2920943 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -89,6 +89,7 @@ dependencies { implementation(libs.androidx.room.common) implementation(libs.androidx.navigation.common.ktx) implementation(libs.androidx.navigation.compose) + implementation(libs.vision.internal.vkp) // Room val roomVersion = "2.6.1" @@ -96,6 +97,9 @@ dependencies { implementation("androidx.room:room-ktx:$roomVersion") ksp("androidx.room:room-compiler:$roomVersion") + // 图表库 + implementation("com.github.PhilJay:MPAndroidChart:v3.1.0") + implementation("androidx.compose.material:material-icons-extended:1.4.3") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") diff --git a/app/src/main/java/com/yovinchen/bookkeeping/model/BookkeepingRecord.kt b/app/src/main/java/com/yovinchen/bookkeeping/model/BookkeepingRecord.kt index 07d90f8..5ec3bd4 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/model/BookkeepingRecord.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/model/BookkeepingRecord.kt @@ -2,6 +2,7 @@ package com.yovinchen.bookkeeping.model import androidx.room.Entity import androidx.room.ForeignKey +import androidx.room.Index import androidx.room.PrimaryKey import androidx.room.TypeConverter import androidx.room.TypeConverters @@ -43,6 +44,9 @@ class Converters { childColumns = ["memberId"], onDelete = ForeignKey.SET_NULL ) + ], + indices = [ + Index(value = ["memberId"]) ] ) @TypeConverters(Converters::class) 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 new file mode 100644 index 0000000..0dc7325 --- /dev/null +++ b/app/src/main/java/com/yovinchen/bookkeeping/ui/components/CategoryPieChart.kt @@ -0,0 +1,56 @@ +package com.yovinchen.bookkeeping.ui.components + +import android.graphics.Color +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.github.mikephil.charting.charts.PieChart +import com.github.mikephil.charting.data.PieData +import com.github.mikephil.charting.data.PieDataSet +import com.github.mikephil.charting.data.PieEntry +import com.github.mikephil.charting.formatter.PercentFormatter +import com.github.mikephil.charting.utils.ColorTemplate + +@Composable +fun CategoryPieChart( + categoryData: List>, + modifier: Modifier = Modifier +) { + AndroidView( + modifier = modifier + .fillMaxWidth() + .height(300.dp), + factory = { context -> + PieChart(context).apply { + description.isEnabled = false + setUsePercentValues(true) + setDrawEntryLabels(true) + legend.isEnabled = true + isDrawHoleEnabled = true + holeRadius = 40f + setHoleColor(Color.TRANSPARENT) + setTransparentCircleRadius(45f) + } + }, + update = { chart -> + val entries = categoryData.map { (category, amount) -> + PieEntry(amount, category) + } + + val dataSet = PieDataSet(entries, "分类占比").apply { + colors = ColorTemplate.MATERIAL_COLORS.toList() + valueTextSize = 14f + valueFormatter = PercentFormatter(chart) + valueTextColor = Color.WHITE + setDrawValues(true) + } + + val pieData = PieData(dataSet) + chart.data = pieData + chart.invalidate() + } + ) +} diff --git a/app/src/main/java/com/yovinchen/bookkeeping/ui/components/MonthYearPicker.kt b/app/src/main/java/com/yovinchen/bookkeeping/ui/components/MonthYearPicker.kt new file mode 100644 index 0000000..45da91c --- /dev/null +++ b/app/src/main/java/com/yovinchen/bookkeeping/ui/components/MonthYearPicker.kt @@ -0,0 +1,88 @@ +package com.yovinchen.bookkeeping.ui.components + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import java.time.YearMonth + +@Composable +fun MonthYearPicker( + selectedMonth: YearMonth, + onMonthSelected: (YearMonth) -> Unit, + onDismiss: () -> Unit +) { + var year by remember { mutableStateOf(selectedMonth.year) } + var month by remember { mutableStateOf(selectedMonth.monthValue) } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("选择月份") }, + text = { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // 年份选择 + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text("年份:") + OutlinedButton( + onClick = { year-- } + ) { + Text("-") + } + Text(year.toString()) + OutlinedButton( + onClick = { year++ } + ) { + Text("+") + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // 月份选择 + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text("月份:") + OutlinedButton( + onClick = { + if (month > 1) month-- + } + ) { + Text("-") + } + Text(month.toString()) + OutlinedButton( + onClick = { + if (month < 12) month++ + } + ) { + Text("+") + } + } + } + }, + confirmButton = { + TextButton( + onClick = { + onMonthSelected(YearMonth.of(year, month)) + } + ) { + Text("确定") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("取消") + } + } + ) +} 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 a8d05d3..71f714e 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 @@ -4,38 +4,35 @@ 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.Settings +import androidx.compose.material.icons.outlined.Analytics import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.compose.ui.graphics.vector.ImageVector import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.yovinchen.bookkeeping.model.ThemeMode +import com.yovinchen.bookkeeping.ui.screen.AnalysisScreen import com.yovinchen.bookkeeping.ui.screen.HomeScreen import com.yovinchen.bookkeeping.ui.screen.SettingsScreen -sealed class Screen(val route: String, val icon: @Composable () -> Unit, val label: String) { - object Home : Screen( - route = "home", - icon = { Icon(Icons.Default.Home, contentDescription = "主页") }, - label = "主页" - ) - object Settings : Screen( - route = "settings", - icon = { Icon(Icons.Default.Settings, contentDescription = "设置") }, - label = "设置" - ) +sealed class Screen( + val route: String, + val icon: ImageVector, + val label: String +) { + data object Home : Screen("home", Icons.Default.Home, "主页") + data object Analysis : Screen("analysis", Icons.Outlined.Analytics, "分析") + data object Settings : Screen("settings", Icons.Default.Settings, "设置") } @OptIn(ExperimentalMaterial3Api::class) @@ -45,22 +42,22 @@ fun MainNavigation( onThemeChange: (ThemeMode) -> Unit ) { val navController = rememberNavController() - val items = listOf(Screen.Home, Screen.Settings) Scaffold( bottomBar = { - NavigationBar( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface, - ) { + NavigationBar { val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentDestination = navBackStackEntry?.destination + val currentRoute = navBackStackEntry?.destination?.route - items.forEach { screen -> + listOf( + Screen.Home, + Screen.Analysis, + Screen.Settings + ).forEach { screen -> NavigationBarItem( - icon = screen.icon, + icon = { Icon(screen.icon, contentDescription = screen.label) }, label = { Text(screen.label) }, - selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true, + selected = currentRoute == screen.route, onClick = { navController.navigate(screen.route) { popUpTo(navController.graph.findStartDestination().id) { @@ -69,32 +66,24 @@ fun MainNavigation( launchSingleTop = true restoreState = true } - }, - colors = NavigationBarItemDefaults.colors( - selectedIconColor = MaterialTheme.colorScheme.primary, - selectedTextColor = MaterialTheme.colorScheme.primary, - unselectedIconColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), - unselectedTextColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), - indicatorColor = MaterialTheme.colorScheme.surfaceVariant - ) + } ) } } } - ) { paddingValues -> + ) { innerPadding -> NavHost( navController = navController, startDestination = Screen.Home.route, - modifier = Modifier.padding(paddingValues) + modifier = Modifier.padding(innerPadding) ) { - composable(Screen.Home.route) { - HomeScreen() - } - composable(Screen.Settings.route) { + composable(Screen.Home.route) { HomeScreen() } + composable(Screen.Analysis.route) { AnalysisScreen() } + composable(Screen.Settings.route) { SettingsScreen( currentTheme = currentTheme, onThemeChange = onThemeChange - ) + ) } } } 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 new file mode 100644 index 0000000..8e35a7e --- /dev/null +++ b/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/AnalysisScreen.kt @@ -0,0 +1,214 @@ +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.automirrored.filled.KeyboardArrowLeft +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.yovinchen.bookkeeping.ui.components.CategoryPieChart +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 + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AnalysisScreen( + modifier: Modifier = Modifier, + viewModel: AnalysisViewModel = viewModel() +) { + val selectedMonth by viewModel.selectedMonth.collectAsState() + val selectedType by viewModel.selectedAnalysisType.collectAsState() + val categoryStats by viewModel.categoryStats.collectAsState() + var showMonthPicker by remember { mutableStateOf(false) } + + Column( + modifier = modifier + .fillMaxSize() + .padding(16.dp) + ) { + // 月份选择器 + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + IconButton(onClick = { + viewModel.setSelectedMonth(selectedMonth.minusMonths(1)) + }) { + Icon(Icons.AutoMirrored.Filled.KeyboardArrowLeft, "上个月") + } + + 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.values().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.isEmpty()) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(300.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = "暂无数据", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } else { + // 添加饼图 + CategoryPieChart( + categoryData = categoryStats.map { + it.category to it.amount.toFloat() + } + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // 分类列表 + LazyColumn { + items(categoryStats) { stat -> + CategoryStatItem(stat) + } + } + } + AnalysisType.TREND -> { + // TODO: 实现收支趋势图表 + Text( + text = "收支趋势", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(vertical = 8.dp) + ) + } + } + } + + if (showMonthPicker) { + MonthYearPicker( + selectedMonth = selectedMonth, + onMonthSelected = { yearMonth -> + viewModel.setSelectedMonth(yearMonth) + showMonthPicker = false + }, + onDismiss = { showMonthPicker = false } + ) + } +} + +@Composable +fun CategoryStatItem(stat: CategoryStat) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stat.category, + style = MaterialTheme.typography.titleMedium + ) + Text( + text = String.format("%.1f%%", stat.percentage), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + // 进度条 + LinearProgressIndicator( + progress = { (stat.percentage / 100).toFloat() }, + modifier = Modifier + .fillMaxWidth() + .height(8.dp), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "¥${String.format("%.2f", stat.amount)}", + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = "${stat.count}笔", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline + ) + } + } + } +} diff --git a/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/AnalysisViewModel.kt b/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/AnalysisViewModel.kt new file mode 100644 index 0000000..5288525 --- /dev/null +++ b/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/AnalysisViewModel.kt @@ -0,0 +1,72 @@ +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.model.TransactionType +import kotlinx.coroutines.flow.* +import java.time.LocalDateTime +import java.time.YearMonth +import java.time.ZoneId + +class AnalysisViewModel(application: Application) : AndroidViewModel(application) { + private val recordDao = BookkeepingDatabase.getDatabase(application).bookkeepingDao() + + private val _selectedMonth = MutableStateFlow(YearMonth.now()) + val selectedMonth = _selectedMonth.asStateFlow() + + private val _selectedAnalysisType = MutableStateFlow(AnalysisType.EXPENSE) + val selectedAnalysisType = _selectedAnalysisType.asStateFlow() + + val categoryStats = combine(selectedMonth, selectedAnalysisType) { month, type -> + val records = recordDao.getAllRecords().first() + val monthRecords = records.filter { + val recordDate = LocalDateTime.ofInstant(it.date.toInstant(), ZoneId.systemDefault()) + YearMonth.from(recordDate) == month && it.type == when(type) { + AnalysisType.EXPENSE -> TransactionType.EXPENSE + AnalysisType.INCOME -> TransactionType.INCOME + else -> null + } + } + + // 按分类统计 + val categoryMap = monthRecords.groupBy { it.category } + val stats = categoryMap.map { (category, records) -> + CategoryStat( + category = category, + amount = records.sumOf { it.amount }, + count = records.size + ) + }.sortedByDescending { it.amount } + + // 计算总额 + val total = stats.sumOf { it.amount } + + // 计算百分比 + stats.map { it.copy(percentage = if (total > 0) it.amount / total * 100 else 0.0) } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = emptyList() + ) + + fun setSelectedMonth(month: YearMonth) { + _selectedMonth.value = month + } + + fun setAnalysisType(type: AnalysisType) { + _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/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3efafc9..3ec9b45 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ composeBom = "2024.04.01" roomCommon = "2.6.1" navigationCommonKtx = "2.8.4" navigationCompose = "2.8.4" +visionInternalVkp = "18.2.3" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -30,6 +31,7 @@ androidx-material3 = { group = "androidx.compose.material3", name = "material3" androidx-room-common = { group = "androidx.room", name = "room-common", version.ref = "roomCommon" } androidx-navigation-common-ktx = { group = "androidx.navigation", name = "navigation-common-ktx", version.ref = "navigationCommonKtx" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } +vision-internal-vkp = { group = "com.google.mlkit", name = "vision-internal-vkp", version.ref = "visionInternalVkp" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/settings.gradle.kts b/settings.gradle.kts index c595d41..711d76a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,6 +9,7 @@ pluginManagement { } mavenCentral() gradlePluginPortal() + maven { url = uri("https://jitpack.io") } } } dependencyResolutionManagement { @@ -16,6 +17,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven { url = uri("https://jitpack.io") } } }