分类迁移到设置

增加时间分类
This commit is contained in:
yovinchen 2024-11-26 23:49:02 +08:00
parent b794c8b91e
commit 316c2648ae
3 changed files with 290 additions and 166 deletions

View File

@ -7,67 +7,54 @@ 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.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Settings
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.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.yovinchen.bookkeeping.model.BookkeepingRecord
import com.yovinchen.bookkeeping.model.TransactionType
import com.yovinchen.bookkeeping.ui.dialog.AddRecordDialog
import com.yovinchen.bookkeeping.ui.dialog.CategoryManagementDialog
import com.yovinchen.bookkeeping.ui.dialog.RecordEditDialog
import com.yovinchen.bookkeeping.viewmodel.HomeViewModel
import java.time.YearMonth
import java.text.SimpleDateFormat
import java.util.*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
modifier: Modifier = Modifier,
viewModel: HomeViewModel = viewModel()
modifier: Modifier = Modifier, viewModel: HomeViewModel = viewModel()
) {
val records by viewModel.filteredRecords.collectAsState()
val filteredRecords by viewModel.filteredRecords.collectAsState()
val totalIncome by viewModel.totalIncome.collectAsState()
val totalExpense by viewModel.totalExpense.collectAsState()
val categories by viewModel.categories.collectAsState()
val selectedType by viewModel.selectedCategoryType.collectAsState()
val selectedRecordType by viewModel.selectedRecordType.collectAsState()
val selectedMonth by viewModel.selectedMonth.collectAsState()
var showAddDialog by remember { mutableStateOf(false) }
var showCategoryDialog by remember { mutableStateOf(false) }
var selectedRecord by remember { mutableStateOf<BookkeepingRecord?>(null) }
Scaffold(
modifier = modifier.fillMaxSize(),
floatingActionButton = {
FloatingActionButton(
onClick = { showAddDialog = true }
) {
Icon(Icons.Default.Add, contentDescription = "添加记录")
}
},
floatingActionButtonPosition = FabPosition.End,
topBar = {
TopAppBar(
title = { Text("记账本") },
actions = {
IconButton(onClick = { showCategoryDialog = true }) {
Icon(Icons.Default.Settings, contentDescription = "类别管理")
}
}
)
Scaffold(modifier = modifier.fillMaxSize(), floatingActionButton = {
FloatingActionButton(onClick = { showAddDialog = true }) {
Icon(Icons.Default.Add, contentDescription = "添加记录")
}
) { padding ->
}, floatingActionButtonPosition = FabPosition.End, topBar = {
TopAppBar(title = { Text("记账本") })
}) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.background(MaterialTheme.colorScheme.background)
) {
// 顶部统计信息
MonthlyStatistics(
@ -76,21 +63,90 @@ fun HomeScreen(
onIncomeClick = { viewModel.setSelectedRecordType(TransactionType.INCOME) },
onExpenseClick = { viewModel.setSelectedRecordType(TransactionType.EXPENSE) },
selectedType = selectedRecordType,
onClearFilter = { viewModel.setSelectedRecordType(null) }
onClearFilter = { viewModel.setSelectedRecordType(null) },
selectedMonth = selectedMonth,
onPreviousMonth = { viewModel.setSelectedMonth(selectedMonth.minusMonths(1)) },
onNextMonth = { viewModel.setSelectedMonth(selectedMonth.plusMonths(1)) }
)
// 记录列表
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(records) { record ->
RecordItem(
record = record,
onClick = { selectedRecord = record },
onDelete = { viewModel.deleteRecord(record) }
)
filteredRecords.forEach { (date, records) ->
item {
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f),
shape = RoundedCornerShape(12.dp),
tonalElevation = 2.dp
) {
Column(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
) {
// 日期标签
Text(
text = SimpleDateFormat(
"yyyy年MM月dd日 E", Locale.CHINESE
).format(date),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(12.dp))
// 当天的记录
records.forEachIndexed { index, record ->
RecordItem(record = record,
onClick = { selectedRecord = record },
onDelete = { viewModel.deleteRecord(record) })
if (index < records.size - 1) {
HorizontalDivider(
modifier = Modifier.padding(vertical = 8.dp),
color = MaterialTheme.colorScheme.surfaceVariant,
thickness = 0.5.dp
)
}
}
Spacer(modifier = Modifier.height(8.dp))
// 当天统计
HorizontalDivider(
color = MaterialTheme.colorScheme.surfaceVariant,
thickness = 0.5.dp
)
val dayIncome = records.filter { it.type == TransactionType.INCOME }
.sumOf { it.amount }
val dayExpense =
records.filter { it.type == TransactionType.EXPENSE }
.sumOf { it.amount }
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "收入: ¥%.2f".format(dayIncome),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary
)
Text(
text = "支出: ¥%.2f".format(dayExpense),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error
)
}
}
}
}
}
}
}
@ -98,6 +154,7 @@ fun HomeScreen(
// 添加记录对话框
if (showAddDialog) {
val selectedDateTime by viewModel.selectedDateTime.collectAsState()
val selectedCategoryType by viewModel.selectedCategoryType.collectAsState()
AddRecordDialog(
onDismiss = {
showAddDialog = false
@ -108,23 +165,10 @@ fun HomeScreen(
showAddDialog = false
},
categories = categories,
selectedType = selectedType,
onTypeChange = { viewModel.setSelectedCategoryType(it) },
selectedType = selectedCategoryType,
onTypeChange = viewModel::setSelectedCategoryType,
selectedDateTime = selectedDateTime,
onDateTimeSelected = { viewModel.setSelectedDateTime(it) }
)
}
// 类别管理对话框
if (showCategoryDialog) {
CategoryManagementDialog(
onDismiss = { showCategoryDialog = false },
categories = categories,
onAddCategory = { name, type -> viewModel.addCategory(name, type) },
onDeleteCategory = { category -> viewModel.deleteCategory(category) },
onUpdateCategory = { category, newName -> viewModel.updateCategory(category, newName) },
selectedType = selectedType,
onTypeChange = { viewModel.setSelectedCategoryType(it) }
onDateTimeSelected = viewModel::setSelectedDateTime
)
}
@ -151,6 +195,9 @@ fun MonthlyStatistics(
onExpenseClick: () -> Unit,
selectedType: TransactionType?,
onClearFilter: () -> Unit,
selectedMonth: YearMonth,
onPreviousMonth: () -> Unit,
onNextMonth: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
@ -164,30 +211,42 @@ fun MonthlyStatistics(
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "本月统计",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(bottom = 8.dp)
)
// 月份选择器
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = onPreviousMonth) {
Icon(Icons.AutoMirrored.Filled.KeyboardArrowLeft, "上个月")
}
Text(
text = "${selectedMonth.year}${selectedMonth.monthValue}",
style = MaterialTheme.typography.titleLarge
)
IconButton(onClick = onNextMonth) {
Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, "下个月")
}
}
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
// 收入统计
Column(
modifier = Modifier
.weight(1f)
.clickable { onIncomeClick() }
.background(
if (selectedType == TransactionType.INCOME)
MaterialTheme.colorScheme.primaryContainer
else
Color.Transparent,
RoundedCornerShape(8.dp)
)
.padding(8.dp)
) {
Column(modifier = Modifier
.weight(1f)
.clickable { onIncomeClick() }
.background(
if (selectedType == TransactionType.INCOME) MaterialTheme.colorScheme.primaryContainer
else Color.Transparent,
RoundedCornerShape(8.dp)
)
.padding(8.dp)) {
Text(
text = "收入",
style = MaterialTheme.typography.titleMedium
@ -202,19 +261,15 @@ fun MonthlyStatistics(
Spacer(modifier = Modifier.width(16.dp))
// 支出统计
Column(
modifier = Modifier
.weight(1f)
.clickable { onExpenseClick() }
.background(
if (selectedType == TransactionType.EXPENSE)
MaterialTheme.colorScheme.primaryContainer
else
Color.Transparent,
RoundedCornerShape(8.dp)
)
.padding(8.dp)
) {
Column(modifier = Modifier
.weight(1f)
.clickable { onExpenseClick() }
.background(
if (selectedType == TransactionType.EXPENSE) MaterialTheme.colorScheme.primaryContainer
else Color.Transparent,
RoundedCornerShape(8.dp)
)
.padding(8.dp)) {
Text(
text = "支出",
style = MaterialTheme.typography.titleMedium
@ -261,8 +316,7 @@ fun RecordItem(
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = record.category,
style = MaterialTheme.typography.titleMedium
text = record.category, style = MaterialTheme.typography.titleMedium
)
if (record.description.isNotEmpty()) {
Text(
@ -272,8 +326,9 @@ fun RecordItem(
)
}
Text(
text = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault())
.format(record.date),
text = SimpleDateFormat(
"yyyy-MM-dd HH:mm", Locale.getDefault()
).format(record.date),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@ -284,18 +339,14 @@ fun RecordItem(
) {
Text(
text = if (record.type == TransactionType.EXPENSE) "-" else "+",
color = if (record.type == TransactionType.EXPENSE)
MaterialTheme.colorScheme.error
else
MaterialTheme.colorScheme.primary,
color = if (record.type == TransactionType.EXPENSE) MaterialTheme.colorScheme.error
else MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.titleMedium
)
Text(
text = String.format("%.2f", record.amount),
color = if (record.type == TransactionType.EXPENSE)
MaterialTheme.colorScheme.error
else
MaterialTheme.colorScheme.primary,
color = if (record.type == TransactionType.EXPENSE) MaterialTheme.colorScheme.error
else MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(end = 8.dp)
)

View File

@ -7,19 +7,38 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.yovinchen.bookkeeping.model.Category
import com.yovinchen.bookkeeping.model.ThemeMode
import com.yovinchen.bookkeeping.model.TransactionType
import com.yovinchen.bookkeeping.ui.components.ColorPicker
import com.yovinchen.bookkeeping.ui.components.predefinedColors
import com.yovinchen.bookkeeping.ui.dialog.CategoryManagementDialog
import com.yovinchen.bookkeeping.viewmodel.SettingsViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
currentTheme: ThemeMode,
onThemeChange: (ThemeMode) -> Unit
onThemeChange: (ThemeMode) -> Unit,
viewModel: SettingsViewModel = viewModel()
) {
var showThemeDialog by remember { mutableStateOf(false) }
var showCategoryDialog by remember { mutableStateOf(false) }
val categories by viewModel.categories.collectAsState()
val selectedType by viewModel.selectedCategoryType.collectAsState()
Column(modifier = Modifier.fillMaxSize()) {
// 类别管理设置项
ListItem(
headlineContent = { Text("类别管理") },
supportingContent = { Text("管理收入和支出类别") },
modifier = Modifier.clickable { showCategoryDialog = true }
)
Divider()
// 主题设置项
ListItem(
headlineContent = { Text("主题设置") },
@ -99,6 +118,19 @@ fun SettingsScreen(
)
}
}
// 类别管理对话框
if (showCategoryDialog) {
CategoryManagementDialog(
onDismiss = { showCategoryDialog = false },
categories = categories,
onAddCategory = viewModel::addCategory,
onDeleteCategory = viewModel::deleteCategory,
onUpdateCategory = viewModel::updateCategory,
selectedType = selectedType,
onTypeChange = viewModel::setSelectedCategoryType
)
}
}
@Composable

View File

@ -8,33 +8,19 @@ import com.yovinchen.bookkeeping.data.BookkeepingDatabase
import com.yovinchen.bookkeeping.model.BookkeepingRecord
import com.yovinchen.bookkeeping.model.Category
import com.yovinchen.bookkeeping.model.TransactionType
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.YearMonth
import java.util.Date
import java.util.Calendar
@OptIn(ExperimentalCoroutinesApi::class)
class HomeViewModel(application: Application) : AndroidViewModel(application) {
private val TAG = "HomeViewModel"
private val database = BookkeepingDatabase.getDatabase(application)
private val dao = database.bookkeepingDao()
val records = dao.getAllRecords()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
private val _totalIncome = MutableStateFlow(0.0)
val totalIncome: StateFlow<Double> = _totalIncome.asStateFlow()
private val _totalExpense = MutableStateFlow(0.0)
val totalExpense: StateFlow<Double> = _totalExpense.asStateFlow()
private val _selectedCategoryType = MutableStateFlow(TransactionType.EXPENSE)
val selectedCategoryType: StateFlow<TransactionType> = _selectedCategoryType.asStateFlow()
private val dao = BookkeepingDatabase.getDatabase(application).bookkeepingDao()
private val _selectedRecordType = MutableStateFlow<TransactionType?>(null)
val selectedRecordType: StateFlow<TransactionType?> = _selectedRecordType.asStateFlow()
@ -42,6 +28,19 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
private val _selectedDateTime = MutableStateFlow(LocalDateTime.now())
val selectedDateTime: StateFlow<LocalDateTime> = _selectedDateTime.asStateFlow()
private val _selectedCategoryType = MutableStateFlow(TransactionType.EXPENSE)
val selectedCategoryType: StateFlow<TransactionType> = _selectedCategoryType.asStateFlow()
private val _selectedMonth = MutableStateFlow(YearMonth.now())
val selectedMonth: StateFlow<YearMonth> = _selectedMonth.asStateFlow()
private val records = dao.getAllRecords()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
val categories: StateFlow<List<Category>> = _selectedCategoryType
.flatMapLatest { type ->
dao.getCategoriesByType(type)
@ -52,38 +51,91 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
initialValue = emptyList()
)
val filteredRecords = combine(records, selectedRecordType) { records, type ->
when (type) {
null -> records.sortedByDescending { it.date }
else -> records.filter { it.type == type }.sortedByDescending { it.date }
}
val filteredRecords = combine(
records,
_selectedRecordType,
_selectedMonth
) { records, selectedType, selectedMonth ->
records
.filter { record ->
val recordDate = record.date.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDate()
val recordYearMonth = YearMonth.from(recordDate)
val typeMatches = selectedType?.let { record.type == it } ?: true
val monthMatches = recordYearMonth == selectedMonth
typeMatches && monthMatches
}
.sortedByDescending { it.date }
.groupBy { record ->
val calendar = Calendar.getInstance().apply { time = record.date }
calendar.apply {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.time
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
viewModelScope,
SharingStarted.WhileSubscribed(5000),
emptyMap()
)
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
val totalIncome = combine(
records,
_selectedMonth
) { records, selectedMonth ->
records
.filter { record ->
val recordDate = record.date.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDate()
val recordYearMonth = YearMonth.from(recordDate)
record.type == TransactionType.INCOME && recordYearMonth == selectedMonth
}
.sumOf { it.amount }
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
0.0
)
val totalExpense = combine(
records,
_selectedMonth
) { records, selectedMonth ->
records
.filter { record ->
val recordDate = record.date.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDate()
val recordYearMonth = YearMonth.from(recordDate)
record.type == TransactionType.EXPENSE && recordYearMonth == selectedMonth
}
.sumOf { it.amount }
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
0.0
)
private fun updateTotals() {
// 移除未使用的参数
}
init {
viewModelScope.launch {
records.collect { recordsList ->
updateTotals(recordsList)
records.collect {
updateTotals()
}
}
}
private fun updateTotals(records: List<BookkeepingRecord>) {
_totalIncome.value = records
.filter { it.type == TransactionType.INCOME }
.sumOf { it.amount }
_totalExpense.value = records
.filter { it.type == TransactionType.EXPENSE }
.sumOf { it.amount }
}
fun addRecord(type: TransactionType, amount: Double, category: String, description: String) {
viewModelScope.launch {
val record = BookkeepingRecord(
@ -102,37 +154,31 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
_selectedDateTime.value = dateTime
}
fun setSelectedCategoryType(type: TransactionType) {
_selectedCategoryType.value = type
}
fun setSelectedRecordType(type: TransactionType?) {
_selectedRecordType.value = type
}
fun setSelectedCategoryType(type: TransactionType) {
_selectedCategoryType.value = type
}
fun setSelectedMonth(yearMonth: YearMonth) {
_selectedMonth.value = yearMonth
}
fun moveMonth(forward: Boolean) {
val current = _selectedMonth.value
_selectedMonth.value = if (forward) {
current.plusMonths(1)
} else {
current.minusMonths(1)
}
}
fun resetSelectedDateTime() {
_selectedDateTime.value = LocalDateTime.now()
}
fun addCategory(name: String, type: TransactionType) {
viewModelScope.launch {
val category = Category(name = name, type = type)
dao.insertCategory(category)
}
}
fun updateCategory(category: Category, newName: String) {
viewModelScope.launch {
dao.updateCategory(category.copy(name = newName))
}
}
fun deleteCategory(category: Category) {
viewModelScope.launch {
dao.deleteCategory(category)
}
}
fun updateRecord(record: BookkeepingRecord) {
viewModelScope.launch {
dao.updateRecord(record)
@ -167,11 +213,6 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
return dao.getRecordsByDateRange(start, end)
}
// 获取指定类别的记录
fun getRecordsByCategory(category: String): Flow<List<BookkeepingRecord>> {
return dao.getRecordsByCategory(category)
}
// 获取指定类型的记录
fun getRecordsByType(type: TransactionType): Flow<List<BookkeepingRecord>> {
return dao.getRecordsByType(type)