Compare commits

..

No commits in common. "a0d47864d872d7418c4565820986b1e1e69616c7" and "94fc7b2a7e1a1bf9b4c52473a633eb951b04e1ac" have entirely different histories.

7 changed files with 58 additions and 210 deletions

View File

@ -16,8 +16,8 @@ android {
applicationId = "com.yovinchen.bookkeeping"
minSdk = 26
targetSdk = 34
versionCode = 4
versionName = "1.2.2"
versionCode = 1
versionName = "1.0.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@ -108,49 +108,17 @@ interface BookkeepingDao {
@Query("UPDATE bookkeeping_records SET category = :newName WHERE category = :oldName")
suspend fun updateRecordCategories(oldName: String, newName: String)
@Query("""
SELECT * FROM bookkeeping_records
WHERE memberId IN (SELECT id FROM members WHERE name = :memberName)
AND date BETWEEN :startDate AND :endDate
AND (
:transactionType IS NULL
OR type = (
CASE :transactionType
WHEN 'INCOME' THEN 'INCOME'
WHEN 'EXPENSE' THEN 'EXPENSE'
END
)
)
ORDER BY date DESC
""")
suspend fun getRecordsByMember(
memberName: String,
startDate: Date,
endDate: Date,
transactionType: TransactionType?
): List<BookkeepingRecord>
@Query("""
SELECT * FROM bookkeeping_records
WHERE memberId IN (SELECT id FROM members WHERE name = :memberName)
AND category = :category
AND category = :category
AND date BETWEEN :startDate AND :endDate
AND (
:transactionType IS NULL
OR type = (
CASE :transactionType
WHEN 'INCOME' THEN 'INCOME'
WHEN 'EXPENSE' THEN 'EXPENSE'
END
)
)
ORDER BY date DESC
""")
suspend fun getRecordsByMemberAndCategory(
memberName: String,
category: String,
startDate: Date,
endDate: Date,
transactionType: TransactionType?
endDate: Date
): List<BookkeepingRecord>
}

View File

@ -22,7 +22,6 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.yovinchen.bookkeeping.model.AnalysisType
import com.yovinchen.bookkeeping.model.ThemeMode
import com.yovinchen.bookkeeping.ui.screen.*
import java.time.YearMonth
@ -41,9 +40,9 @@ sealed class Screen(
return "category_detail/$category/${yearMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"))}"
}
}
object MemberDetail : Screen("member_detail/{memberName}/{category}/{yearMonth}?type={type}", "成员详情") {
fun createRoute(memberName: String, category: String, yearMonth: YearMonth, type: AnalysisType): String {
return "member_detail/$memberName/$category/${yearMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"))}?type=${type.name}"
object MemberDetail : Screen("member_detail/{memberName}/{category}/{yearMonth}", "成员详情") {
fun createRoute(memberName: String, category: String, yearMonth: YearMonth): String {
return "member_detail/$memberName/$category/${yearMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"))}"
}
}
@ -97,8 +96,10 @@ fun MainNavigation(
onNavigateToCategoryDetail = { category, yearMonth ->
navController.navigate(Screen.CategoryDetail.createRoute(category, yearMonth))
},
onNavigateToMemberDetail = { memberName, yearMonth, analysisType ->
navController.navigate(Screen.MemberDetail.createRoute(memberName, "", yearMonth, analysisType))
onNavigateToMemberDetail = { memberName, yearMonth ->
// 在这里我们暂时使用一个默认分类,你需要根据实际情况修改这里的逻辑
val defaultCategory = "默认"
navController.navigate(Screen.MemberDetail.createRoute(memberName, defaultCategory, yearMonth))
}
)
}
@ -126,7 +127,7 @@ fun MainNavigation(
yearMonth = yearMonth,
onNavigateBack = { navController.popBackStack() },
onNavigateToMemberDetail = { memberName ->
navController.navigate(Screen.MemberDetail.createRoute(memberName, category, yearMonth, AnalysisType.EXPENSE))
navController.navigate(Screen.MemberDetail.createRoute(memberName, category, yearMonth))
}
)
}
@ -136,30 +137,18 @@ fun MainNavigation(
arguments = listOf(
navArgument("memberName") { type = NavType.StringType },
navArgument("category") { type = NavType.StringType },
navArgument("yearMonth") { type = NavType.StringType },
navArgument("type") {
type = NavType.StringType
defaultValue = AnalysisType.EXPENSE.name
}
navArgument("yearMonth") { type = NavType.StringType }
)
) { backStackEntry ->
val memberName = backStackEntry.arguments?.getString("memberName") ?: return@composable
val category = backStackEntry.arguments?.getString("category") ?: return@composable
val yearMonthStr = backStackEntry.arguments?.getString("yearMonth") ?: return@composable
val yearMonth = YearMonth.parse(yearMonthStr)
val type = backStackEntry.arguments?.getString("type")?.let {
try {
AnalysisType.valueOf(it)
} catch (e: IllegalArgumentException) {
AnalysisType.EXPENSE
}
} ?: AnalysisType.EXPENSE
MemberDetailScreen(
memberName = memberName,
yearMonth = yearMonth,
category = category,
analysisType = type,
yearMonth = yearMonth,
onNavigateBack = { navController.popBackStack() }
)
}

View File

@ -29,7 +29,7 @@ enum class ViewMode {
@Composable
fun AnalysisScreen(
onNavigateToCategoryDetail: (String, YearMonth) -> Unit,
onNavigateToMemberDetail: (String, YearMonth, AnalysisType) -> Unit
onNavigateToMemberDetail: (String, YearMonth) -> Unit
) {
val viewModel: AnalysisViewModel = viewModel()
val selectedMonth by viewModel.selectedMonth.collectAsState()
@ -138,7 +138,7 @@ fun AnalysisScreen(
if (currentViewMode == ViewMode.CATEGORY) {
onNavigateToCategoryDetail(category, selectedMonth)
} else {
onNavigateToMemberDetail(category, selectedMonth, selectedAnalysisType)
onNavigateToMemberDetail(category, selectedMonth)
}
}
)
@ -149,11 +149,11 @@ fun AnalysisScreen(
items(if (currentViewMode == ViewMode.CATEGORY) categoryStats else memberStats) { stat ->
CategoryStatItem(
stat = stat,
onClick = {
onClick = {
if (currentViewMode == ViewMode.CATEGORY) {
onNavigateToCategoryDetail(stat.category, selectedMonth)
} else {
onNavigateToMemberDetail(stat.category, selectedMonth, selectedAnalysisType)
onNavigateToMemberDetail(stat.category, selectedMonth)
}
}
)

View File

@ -1,30 +1,12 @@
package com.yovinchen.bookkeeping.ui.screen
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.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@ -33,7 +15,6 @@ 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.model.TransactionType
import com.yovinchen.bookkeeping.ui.components.CategoryPieChart
import com.yovinchen.bookkeeping.ui.components.RecordItem
import com.yovinchen.bookkeeping.viewmodel.CategoryDetailViewModel
@ -41,7 +22,8 @@ import com.yovinchen.bookkeeping.viewmodel.CategoryDetailViewModelFactory
import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.time.YearMonth
import java.util.Locale
import java.time.format.DateTimeFormatter
import java.util.*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -80,78 +62,27 @@ fun CategoryDetailScreen(
.padding(padding),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 第一部分:总支出
item {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = if (records.isNotEmpty() && records.first().type == TransactionType.INCOME) "总收入" else "总支出",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = NumberFormat.getCurrencyInstance(Locale.CHINA).format(total),
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
}
}
}
// 第二部分:扇形图
item {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "成员分布",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 16.dp)
)
CategoryPieChart(
categoryData = memberStats.map { Pair(it.category, it.percentage.toFloat()) },
memberData = emptyList(),
currentViewMode = false,
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
onCategoryClick = { memberName ->
if (records.isNotEmpty() && records.first().type == TransactionType.EXPENSE) {
onNavigateToMemberDetail(memberName)
}
}
)
}
}
}
// 第三部分:详细信息
item {
Text(
text = "详细记录",
style = MaterialTheme.typography.titleMedium,
text = NumberFormat.getCurrencyInstance(Locale.CHINA).format(total),
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(16.dp)
)
}
// 按日期分组的记录列表
item {
CategoryPieChart(
categoryData = memberStats.map { Pair(it.category, it.percentage.toFloat()) },
memberData = emptyList(),
currentViewMode = false,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
onCategoryClick = { memberName -> onNavigateToMemberDetail(memberName) }
)
}
// 按日期分组记录
val groupedRecords = records.groupBy { record ->
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(record.date)
}.toSortedMap(compareByDescending { it })

View File

@ -1,61 +1,39 @@
package com.yovinchen.bookkeeping.ui.screen
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.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.yovinchen.bookkeeping.data.Record
import com.yovinchen.bookkeeping.model.TransactionType
import com.yovinchen.bookkeeping.model.AnalysisType
import com.yovinchen.bookkeeping.ui.components.RecordItem
import com.yovinchen.bookkeeping.viewmodel.MemberDetailViewModel
import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.time.YearMonth
import java.util.Locale
import java.time.format.DateTimeFormatter
import java.util.*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MemberDetailScreen(
memberName: String,
category: String,
yearMonth: YearMonth,
category: String = "",
analysisType: AnalysisType = AnalysisType.EXPENSE,
onNavigateBack: () -> Unit,
viewModel: MemberDetailViewModel = viewModel()
) {
val records by viewModel.memberRecords.collectAsState(initial = emptyList())
val totalAmount by viewModel.totalAmount.collectAsState(initial = 0.0)
LaunchedEffect(memberName, category, yearMonth, analysisType) {
viewModel.loadMemberRecords(memberName, category, yearMonth, analysisType)
LaunchedEffect(memberName, category, yearMonth) {
viewModel.loadMemberRecords(memberName, category, yearMonth)
}
val groupedRecords = remember(records) {
@ -68,11 +46,11 @@ fun MemberDetailScreen(
topBar = {
TopAppBar(
title = {
Text(memberName)
Text("$category - $memberName")
},
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, "返回")
Icon(Icons.Default.ArrowBack, "返回")
}
}
)
@ -97,7 +75,7 @@ fun MemberDetailScreen(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = if (records.isNotEmpty() && records.first().type == TransactionType.INCOME) "总收入" else "总支出",
text = "当前分类总支出",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(8.dp))

View File

@ -5,8 +5,6 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.yovinchen.bookkeeping.data.BookkeepingDatabase
import com.yovinchen.bookkeeping.model.BookkeepingRecord
import com.yovinchen.bookkeeping.model.AnalysisType
import com.yovinchen.bookkeeping.model.TransactionType
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
@ -24,7 +22,7 @@ class MemberDetailViewModel(application: Application) : AndroidViewModel(applica
private val _totalAmount = MutableStateFlow(0.0)
val totalAmount: StateFlow<Double> = _totalAmount
fun loadMemberRecords(memberName: String, category: String, yearMonth: YearMonth, analysisType: AnalysisType) {
fun loadMemberRecords(memberName: String, category: String, yearMonth: YearMonth) {
viewModelScope.launch {
val startDate = yearMonth.atDay(1).atStartOfDay()
.atZone(ZoneId.systemDefault())
@ -36,28 +34,12 @@ class MemberDetailViewModel(application: Application) : AndroidViewModel(applica
.toInstant()
.let { Date.from(it) }
val transactionType = when (analysisType) {
AnalysisType.INCOME -> TransactionType.INCOME
AnalysisType.EXPENSE -> TransactionType.EXPENSE
else -> null
}
val records = if (category.isEmpty()) {
recordDao.getRecordsByMember(
memberName = memberName,
startDate = startDate,
endDate = endDate,
transactionType = transactionType
)
} else {
recordDao.getRecordsByMemberAndCategory(
memberName = memberName,
category = category,
startDate = startDate,
endDate = endDate,
transactionType = transactionType
)
}
val records = recordDao.getRecordsByMemberAndCategory(
memberName = memberName,
category = category,
startDate = startDate,
endDate = endDate
)
_memberRecords.value = records
_totalAmount.value = records.sumOf { it.amount }
}