feat: 添加类别详情页面

1. 新增类别详情相关组件和视图模型
2. 优化饼图显示效果
3. 完善导航系统
4. 改进数据查询接口
This commit is contained in:
yovinchen 2024-11-28 14:21:32 +08:00
parent c3f108ab57
commit 8339d3d5da
13 changed files with 450 additions and 216 deletions

View File

@ -27,6 +27,17 @@ interface BookkeepingDao {
@Query("SELECT SUM(amount) FROM bookkeeping_records WHERE type = :type AND (memberId = :memberId OR memberId IS NULL)") @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<Double?> fun getTotalAmountByType(type: TransactionType, memberId: Int? = null): Flow<Double?>
@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<List<BookkeepingRecord>>
@Insert @Insert
suspend fun insertRecord(record: BookkeepingRecord): Long suspend fun insertRecord(record: BookkeepingRecord): Long

View File

@ -0,0 +1,7 @@
package com.yovinchen.bookkeeping.model
enum class AnalysisType {
EXPENSE,
INCOME,
TREND
}

View File

@ -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
)

View File

@ -35,19 +35,19 @@ fun CategoryPieChart(
description.isEnabled = false description.isEnabled = false
setUsePercentValues(true) setUsePercentValues(true)
setDrawEntryLabels(true) setDrawEntryLabels(true)
// 禁用图例显示 // 禁用图例显示
legend.isEnabled = false legend.isEnabled = false
isDrawHoleEnabled = true isDrawHoleEnabled = true
holeRadius = 40f holeRadius = 40f
setHoleColor(AndroidColor.TRANSPARENT) setHoleColor(AndroidColor.TRANSPARENT)
setTransparentCircleRadius(45f) setTransparentCircleRadius(45f)
// 设置标签文字颜色为白色(因为标签在彩色扇形上) // 设置标签文字颜色
setEntryLabelColor(AndroidColor.WHITE) setEntryLabelColor(textColor)
setEntryLabelTextSize(12f) setEntryLabelTextSize(12f)
// 设置中心文字颜色跟随主题 // 设置中心文字颜色跟随主题
setCenterTextColor(textColor) setCenterTextColor(textColor)
} }
@ -61,7 +61,7 @@ fun CategoryPieChart(
colors = ColorTemplate.MATERIAL_COLORS.toList() colors = ColorTemplate.MATERIAL_COLORS.toList()
valueTextSize = 14f valueTextSize = 14f
valueFormatter = PercentFormatter(chart) valueFormatter = PercentFormatter(chart)
valueTextColor = AndroidColor.WHITE // 扇形上的数值文字保持白色 valueTextColor = textColor
setDrawValues(true) setDrawValues(true)
} }

View File

@ -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
)
}
}
}

View File

@ -22,6 +22,7 @@ fun RecordItem(
members: List<Member> = emptyList() members: List<Member> = emptyList()
) { ) {
var showDeleteDialog by remember { mutableStateOf(false) } var showDeleteDialog by remember { mutableStateOf(false) }
// val dateFormat = remember { SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) }
val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) } val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) }
val member = members.find { it.id == record.memberId } val member = members.find { it.id == record.memberId }
@ -48,14 +49,18 @@ fun RecordItem(
style = MaterialTheme.typography.bodyLarge style = MaterialTheme.typography.bodyLarge
) )
// 第二行:时间 | 成员 | 详情 // 第二行:日期和时间 | 成员 | 详情
Text( Text(
text = buildString { text = buildString {
// append(dateFormat.format(record.date))
// append(" ")
append(timeFormat.format(record.date)) append(timeFormat.format(record.date))
if (member != null && member.name != "自己") { // if (member != null && member.name != "自己") {
append(" | ") append(" | ")
if (member != null) {
append(member.name) append(member.name)
} }
// }
if (record.description.isNotEmpty()) { if (record.description.isNotEmpty()) {
append(" | ") append(" | ")
append(record.description) append(record.description)

View File

@ -3,6 +3,7 @@ package com.yovinchen.bookkeeping.ui.navigation
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home 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.filled.Settings
import androidx.compose.material.icons.outlined.Analytics import androidx.compose.material.icons.outlined.Analytics
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -16,14 +17,19 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.yovinchen.bookkeeping.model.ThemeMode import com.yovinchen.bookkeeping.model.ThemeMode
import com.yovinchen.bookkeeping.ui.screen.AnalysisScreen 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.HomeScreen
import com.yovinchen.bookkeeping.ui.screen.SettingsScreen import com.yovinchen.bookkeeping.ui.screen.SettingsScreen
import java.time.YearMonth
import java.time.format.DateTimeFormatter
sealed class Screen( sealed class Screen(
val route: String, val route: String,
@ -33,6 +39,14 @@ sealed class Screen(
data object Home : Screen("home", Icons.Default.Home, "主页") data object Home : Screen("home", Icons.Default.Home, "主页")
data object Analysis : Screen("analysis", Icons.Outlined.Analytics, "分析") data object Analysis : Screen("analysis", Icons.Outlined.Analytics, "分析")
data object Settings : Screen("settings", Icons.Default.Settings, "设置") 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) @OptIn(ExperimentalMaterial3Api::class)
@ -78,12 +92,37 @@ fun MainNavigation(
modifier = Modifier.padding(innerPadding) modifier = Modifier.padding(innerPadding)
) { ) {
composable(Screen.Home.route) { HomeScreen() } composable(Screen.Home.route) { HomeScreen() }
composable(Screen.Analysis.route) { AnalysisScreen() } composable(Screen.Analysis.route) {
composable(Screen.Settings.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( SettingsScreen(
currentTheme = currentTheme, currentTheme = currentTheme,
onThemeChange = onThemeChange 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() }
)
} }
} }
} }

View File

@ -1,228 +1,109 @@
package com.yovinchen.bookkeeping.ui.screen package com.yovinchen.bookkeeping.ui.screen
import android.annotation.SuppressLint import androidx.compose.foundation.layout.*
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.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.*
import androidx.compose.material.icons.Icons import androidx.compose.runtime.*
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.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel 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.CategoryPieChart
import com.yovinchen.bookkeeping.ui.components.CategoryStatItem
import com.yovinchen.bookkeeping.ui.components.MonthYearPicker import com.yovinchen.bookkeeping.ui.components.MonthYearPicker
import com.yovinchen.bookkeeping.viewmodel.AnalysisType
import com.yovinchen.bookkeeping.viewmodel.AnalysisViewModel 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 @Composable
fun AnalysisScreen( fun AnalysisScreen(
modifier: Modifier = Modifier, onNavigateToCategoryDetail: (String, YearMonth) -> Unit
viewModel: AnalysisViewModel = viewModel()
) { ) {
val viewModel: AnalysisViewModel = viewModel()
val selectedMonth by viewModel.selectedMonth.collectAsState() val selectedMonth by viewModel.selectedMonth.collectAsState()
val selectedType by viewModel.selectedAnalysisType.collectAsState() val selectedAnalysisType by viewModel.selectedAnalysisType.collectAsState()
val categoryStats by viewModel.categoryStats.collectAsState() val categoryStats by viewModel.categoryStats.collectAsState()
var showMonthPicker by remember { mutableStateOf(false) } var showMonthPicker by remember { mutableStateOf(false) }
LazyColumn( Scaffold { padding ->
modifier = modifier Column(
.fillMaxSize() modifier = Modifier
.padding(16.dp) .fillMaxSize()
) { .padding(padding)
// 月份选择器 ) {
item { // 月份选择器和类型切换
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
IconButton(onClick = { // 月份选择按钮
viewModel.setSelectedMonth(selectedMonth.minusMonths(1)) Button(onClick = { showMonthPicker = true }) {
}) { Text(selectedMonth.format(DateTimeFormatter.ofPattern("yyyy年MM月")))
Icon(Icons.AutoMirrored.Filled.KeyboardArrowLeft, "上个月")
} }
Text( // 类型切换
text = "${selectedMonth.year}${selectedMonth.monthValue}", Row {
style = MaterialTheme.typography.titleLarge, AnalysisType.values().forEach { type ->
modifier = Modifier.clickable { showMonthPicker = true } FilterChip(
) selected = selectedAnalysisType == type,
onClick = { viewModel.setAnalysisType(type) },
IconButton(onClick = { label = {
viewModel.setSelectedMonth(selectedMonth.plusMonths(1)) Text(
}) { when (type) {
Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, "下个月") AnalysisType.EXPENSE -> "支出"
} AnalysisType.INCOME -> "收入"
} AnalysisType.TREND -> "趋势"
}
Spacer(modifier = Modifier.height(16.dp)) )
},
// 分析类型选择 modifier = Modifier.padding(horizontal = 4.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)
) )
} }
} }
AnalysisType.TREND -> { }
Text(
text = "收支趋势分析(开发中)", // 饼图
style = MaterialTheme.typography.titleMedium, if (selectedAnalysisType != AnalysisType.TREND) {
modifier = Modifier.padding(vertical = 8.dp) 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()) { if (showMonthPicker) {
items(categoryStats) { stat -> MonthYearPicker(
CategoryStatItem(stat) selectedMonth = selectedMonth,
} onMonthSelected = {
} viewModel.setSelectedMonth(it)
} showMonthPicker = false
},
if (showMonthPicker) { onDismiss = { showMonthPicker = false }
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
) )
} }
} }

View File

@ -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
)
}
}
}
}
}
}
}
}
}
}
}

View File

@ -4,6 +4,8 @@ import android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.yovinchen.bookkeeping.data.BookkeepingDatabase 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 com.yovinchen.bookkeeping.model.TransactionType
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import java.time.LocalDateTime import java.time.LocalDateTime
@ -59,14 +61,3 @@ class AnalysisViewModel(application: Application) : AndroidViewModel(application
_selectedAnalysisType.value = type _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
)

View File

@ -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<List<BookkeepingRecord>>(emptyList())
val records: StateFlow<List<BookkeepingRecord>> = _records
private val _total = MutableStateFlow(0.0)
val total: StateFlow<Double> = _total
private val _members = MutableStateFlow<List<Member>>(emptyList())
val members: StateFlow<List<Member>> = _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
}
}
}
}

View File

@ -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 <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(CategoryDetailViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return CategoryDetailViewModel(database, category, month) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}

View File

@ -20,4 +20,6 @@ kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the # 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, # resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
# Kotlin
org.gradle.java.home=/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home