分类迁移到设置
增加时间分类
This commit is contained in:
parent
b794c8b91e
commit
316c2648ae
@ -7,67 +7,54 @@ 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.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
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.Add
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Settings
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.yovinchen.bookkeeping.model.BookkeepingRecord
|
import com.yovinchen.bookkeeping.model.BookkeepingRecord
|
||||||
import com.yovinchen.bookkeeping.model.TransactionType
|
import com.yovinchen.bookkeeping.model.TransactionType
|
||||||
import com.yovinchen.bookkeeping.ui.dialog.AddRecordDialog
|
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.ui.dialog.RecordEditDialog
|
||||||
import com.yovinchen.bookkeeping.viewmodel.HomeViewModel
|
import com.yovinchen.bookkeeping.viewmodel.HomeViewModel
|
||||||
|
import java.time.YearMonth
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeScreen(
|
fun HomeScreen(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier, viewModel: HomeViewModel = viewModel()
|
||||||
viewModel: HomeViewModel = viewModel()
|
|
||||||
) {
|
) {
|
||||||
val records by viewModel.filteredRecords.collectAsState()
|
val filteredRecords by viewModel.filteredRecords.collectAsState()
|
||||||
val totalIncome by viewModel.totalIncome.collectAsState()
|
val totalIncome by viewModel.totalIncome.collectAsState()
|
||||||
val totalExpense by viewModel.totalExpense.collectAsState()
|
val totalExpense by viewModel.totalExpense.collectAsState()
|
||||||
val categories by viewModel.categories.collectAsState()
|
val categories by viewModel.categories.collectAsState()
|
||||||
val selectedType by viewModel.selectedCategoryType.collectAsState()
|
|
||||||
val selectedRecordType by viewModel.selectedRecordType.collectAsState()
|
val selectedRecordType by viewModel.selectedRecordType.collectAsState()
|
||||||
|
val selectedMonth by viewModel.selectedMonth.collectAsState()
|
||||||
|
|
||||||
var showAddDialog by remember { mutableStateOf(false) }
|
var showAddDialog by remember { mutableStateOf(false) }
|
||||||
var showCategoryDialog by remember { mutableStateOf(false) }
|
|
||||||
var selectedRecord by remember { mutableStateOf<BookkeepingRecord?>(null) }
|
var selectedRecord by remember { mutableStateOf<BookkeepingRecord?>(null) }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(modifier = modifier.fillMaxSize(), floatingActionButton = {
|
||||||
modifier = modifier.fillMaxSize(),
|
FloatingActionButton(onClick = { showAddDialog = true }) {
|
||||||
floatingActionButton = {
|
Icon(Icons.Default.Add, contentDescription = "添加记录")
|
||||||
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 = "类别管理")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
) { padding ->
|
}, floatingActionButtonPosition = FabPosition.End, topBar = {
|
||||||
|
TopAppBar(title = { Text("记账本") })
|
||||||
|
}) { padding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
|
.background(MaterialTheme.colorScheme.background)
|
||||||
) {
|
) {
|
||||||
// 顶部统计信息
|
// 顶部统计信息
|
||||||
MonthlyStatistics(
|
MonthlyStatistics(
|
||||||
@ -76,21 +63,90 @@ fun HomeScreen(
|
|||||||
onIncomeClick = { viewModel.setSelectedRecordType(TransactionType.INCOME) },
|
onIncomeClick = { viewModel.setSelectedRecordType(TransactionType.INCOME) },
|
||||||
onExpenseClick = { viewModel.setSelectedRecordType(TransactionType.EXPENSE) },
|
onExpenseClick = { viewModel.setSelectedRecordType(TransactionType.EXPENSE) },
|
||||||
selectedType = selectedRecordType,
|
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(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
items(records) { record ->
|
filteredRecords.forEach { (date, records) ->
|
||||||
RecordItem(
|
item {
|
||||||
record = record,
|
Surface(
|
||||||
onClick = { selectedRecord = record },
|
modifier = Modifier
|
||||||
onDelete = { viewModel.deleteRecord(record) }
|
.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,9 +154,10 @@ fun HomeScreen(
|
|||||||
// 添加记录对话框
|
// 添加记录对话框
|
||||||
if (showAddDialog) {
|
if (showAddDialog) {
|
||||||
val selectedDateTime by viewModel.selectedDateTime.collectAsState()
|
val selectedDateTime by viewModel.selectedDateTime.collectAsState()
|
||||||
|
val selectedCategoryType by viewModel.selectedCategoryType.collectAsState()
|
||||||
AddRecordDialog(
|
AddRecordDialog(
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
showAddDialog = false
|
showAddDialog = false
|
||||||
viewModel.resetSelectedDateTime()
|
viewModel.resetSelectedDateTime()
|
||||||
},
|
},
|
||||||
onConfirm = { type, amount, category, description ->
|
onConfirm = { type, amount, category, description ->
|
||||||
@ -108,23 +165,10 @@ fun HomeScreen(
|
|||||||
showAddDialog = false
|
showAddDialog = false
|
||||||
},
|
},
|
||||||
categories = categories,
|
categories = categories,
|
||||||
selectedType = selectedType,
|
selectedType = selectedCategoryType,
|
||||||
onTypeChange = { viewModel.setSelectedCategoryType(it) },
|
onTypeChange = viewModel::setSelectedCategoryType,
|
||||||
selectedDateTime = selectedDateTime,
|
selectedDateTime = selectedDateTime,
|
||||||
onDateTimeSelected = { viewModel.setSelectedDateTime(it) }
|
onDateTimeSelected = viewModel::setSelectedDateTime
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 类别管理对话框
|
|
||||||
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) }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,6 +195,9 @@ fun MonthlyStatistics(
|
|||||||
onExpenseClick: () -> Unit,
|
onExpenseClick: () -> Unit,
|
||||||
selectedType: TransactionType?,
|
selectedType: TransactionType?,
|
||||||
onClearFilter: () -> Unit,
|
onClearFilter: () -> Unit,
|
||||||
|
selectedMonth: YearMonth,
|
||||||
|
onPreviousMonth: () -> Unit,
|
||||||
|
onNextMonth: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
@ -164,30 +211,42 @@ fun MonthlyStatistics(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
// 月份选择器
|
||||||
text = "本月统计",
|
Row(
|
||||||
style = MaterialTheme.typography.titleLarge,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
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(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
// 收入统计
|
// 收入统计
|
||||||
Column(
|
Column(modifier = Modifier
|
||||||
modifier = Modifier
|
.weight(1f)
|
||||||
.weight(1f)
|
.clickable { onIncomeClick() }
|
||||||
.clickable { onIncomeClick() }
|
.background(
|
||||||
.background(
|
if (selectedType == TransactionType.INCOME) MaterialTheme.colorScheme.primaryContainer
|
||||||
if (selectedType == TransactionType.INCOME)
|
else Color.Transparent,
|
||||||
MaterialTheme.colorScheme.primaryContainer
|
RoundedCornerShape(8.dp)
|
||||||
else
|
)
|
||||||
Color.Transparent,
|
.padding(8.dp)) {
|
||||||
RoundedCornerShape(8.dp)
|
|
||||||
)
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
text = "收入",
|
text = "收入",
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
@ -202,19 +261,15 @@ fun MonthlyStatistics(
|
|||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
// 支出统计
|
// 支出统计
|
||||||
Column(
|
Column(modifier = Modifier
|
||||||
modifier = Modifier
|
.weight(1f)
|
||||||
.weight(1f)
|
.clickable { onExpenseClick() }
|
||||||
.clickable { onExpenseClick() }
|
.background(
|
||||||
.background(
|
if (selectedType == TransactionType.EXPENSE) MaterialTheme.colorScheme.primaryContainer
|
||||||
if (selectedType == TransactionType.EXPENSE)
|
else Color.Transparent,
|
||||||
MaterialTheme.colorScheme.primaryContainer
|
RoundedCornerShape(8.dp)
|
||||||
else
|
)
|
||||||
Color.Transparent,
|
.padding(8.dp)) {
|
||||||
RoundedCornerShape(8.dp)
|
|
||||||
)
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
text = "支出",
|
text = "支出",
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
@ -261,8 +316,7 @@ fun RecordItem(
|
|||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = record.category,
|
text = record.category, style = MaterialTheme.typography.titleMedium
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
)
|
||||||
if (record.description.isNotEmpty()) {
|
if (record.description.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
@ -272,8 +326,9 @@ fun RecordItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault())
|
text = SimpleDateFormat(
|
||||||
.format(record.date),
|
"yyyy-MM-dd HH:mm", Locale.getDefault()
|
||||||
|
).format(record.date),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
@ -284,18 +339,14 @@ fun RecordItem(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = if (record.type == TransactionType.EXPENSE) "-" else "+",
|
text = if (record.type == TransactionType.EXPENSE) "-" else "+",
|
||||||
color = if (record.type == TransactionType.EXPENSE)
|
color = if (record.type == TransactionType.EXPENSE) MaterialTheme.colorScheme.error
|
||||||
MaterialTheme.colorScheme.error
|
else MaterialTheme.colorScheme.primary,
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.primary,
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = String.format("%.2f", record.amount),
|
text = String.format("%.2f", record.amount),
|
||||||
color = if (record.type == TransactionType.EXPENSE)
|
color = if (record.type == TransactionType.EXPENSE) MaterialTheme.colorScheme.error
|
||||||
MaterialTheme.colorScheme.error
|
else MaterialTheme.colorScheme.primary,
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.primary,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
modifier = Modifier.padding(end = 8.dp)
|
modifier = Modifier.padding(end = 8.dp)
|
||||||
)
|
)
|
||||||
|
@ -7,19 +7,38 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
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.ThemeMode
|
||||||
|
import com.yovinchen.bookkeeping.model.TransactionType
|
||||||
import com.yovinchen.bookkeeping.ui.components.ColorPicker
|
import com.yovinchen.bookkeeping.ui.components.ColorPicker
|
||||||
import com.yovinchen.bookkeeping.ui.components.predefinedColors
|
import com.yovinchen.bookkeeping.ui.components.predefinedColors
|
||||||
|
import com.yovinchen.bookkeeping.ui.dialog.CategoryManagementDialog
|
||||||
|
import com.yovinchen.bookkeeping.viewmodel.SettingsViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
currentTheme: ThemeMode,
|
currentTheme: ThemeMode,
|
||||||
onThemeChange: (ThemeMode) -> Unit
|
onThemeChange: (ThemeMode) -> Unit,
|
||||||
|
viewModel: SettingsViewModel = viewModel()
|
||||||
) {
|
) {
|
||||||
var showThemeDialog by remember { mutableStateOf(false) }
|
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()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
// 类别管理设置项
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text("类别管理") },
|
||||||
|
supportingContent = { Text("管理收入和支出类别") },
|
||||||
|
modifier = Modifier.clickable { showCategoryDialog = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
// 主题设置项
|
// 主题设置项
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text("主题设置") },
|
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
|
@Composable
|
||||||
|
@ -8,33 +8,19 @@ import com.yovinchen.bookkeeping.data.BookkeepingDatabase
|
|||||||
import com.yovinchen.bookkeeping.model.BookkeepingRecord
|
import com.yovinchen.bookkeeping.model.BookkeepingRecord
|
||||||
import com.yovinchen.bookkeeping.model.Category
|
import com.yovinchen.bookkeeping.model.Category
|
||||||
import com.yovinchen.bookkeeping.model.TransactionType
|
import com.yovinchen.bookkeeping.model.TransactionType
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
|
import java.time.YearMonth
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
private val TAG = "HomeViewModel"
|
private val TAG = "HomeViewModel"
|
||||||
private val database = BookkeepingDatabase.getDatabase(application)
|
private val dao = BookkeepingDatabase.getDatabase(application).bookkeepingDao()
|
||||||
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 _selectedRecordType = MutableStateFlow<TransactionType?>(null)
|
private val _selectedRecordType = MutableStateFlow<TransactionType?>(null)
|
||||||
val selectedRecordType: StateFlow<TransactionType?> = _selectedRecordType.asStateFlow()
|
val selectedRecordType: StateFlow<TransactionType?> = _selectedRecordType.asStateFlow()
|
||||||
@ -42,6 +28,19 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
private val _selectedDateTime = MutableStateFlow(LocalDateTime.now())
|
private val _selectedDateTime = MutableStateFlow(LocalDateTime.now())
|
||||||
val selectedDateTime: StateFlow<LocalDateTime> = _selectedDateTime.asStateFlow()
|
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
|
val categories: StateFlow<List<Category>> = _selectedCategoryType
|
||||||
.flatMapLatest { type ->
|
.flatMapLatest { type ->
|
||||||
dao.getCategoriesByType(type)
|
dao.getCategoriesByType(type)
|
||||||
@ -52,38 +51,91 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
initialValue = emptyList()
|
initialValue = emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
val filteredRecords = combine(records, selectedRecordType) { records, type ->
|
val filteredRecords = combine(
|
||||||
when (type) {
|
records,
|
||||||
null -> records.sortedByDescending { it.date }
|
_selectedRecordType,
|
||||||
else -> records.filter { it.type == type }.sortedByDescending { it.date }
|
_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(
|
}.stateIn(
|
||||||
scope = viewModelScope,
|
viewModelScope,
|
||||||
started = SharingStarted.WhileSubscribed(5000),
|
SharingStarted.WhileSubscribed(5000),
|
||||||
initialValue = emptyList()
|
emptyMap()
|
||||||
)
|
)
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(UiState())
|
val totalIncome = combine(
|
||||||
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
|
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 {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
records.collect { recordsList ->
|
records.collect {
|
||||||
updateTotals(recordsList)
|
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) {
|
fun addRecord(type: TransactionType, amount: Double, category: String, description: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val record = BookkeepingRecord(
|
val record = BookkeepingRecord(
|
||||||
@ -102,37 +154,31 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
_selectedDateTime.value = dateTime
|
_selectedDateTime.value = dateTime
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSelectedCategoryType(type: TransactionType) {
|
|
||||||
_selectedCategoryType.value = type
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSelectedRecordType(type: TransactionType?) {
|
fun setSelectedRecordType(type: TransactionType?) {
|
||||||
_selectedRecordType.value = type
|
_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() {
|
fun resetSelectedDateTime() {
|
||||||
_selectedDateTime.value = LocalDateTime.now()
|
_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) {
|
fun updateRecord(record: BookkeepingRecord) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
dao.updateRecord(record)
|
dao.updateRecord(record)
|
||||||
@ -167,11 +213,6 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
return dao.getRecordsByDateRange(start, end)
|
return dao.getRecordsByDateRange(start, end)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取指定类别的记录
|
|
||||||
fun getRecordsByCategory(category: String): Flow<List<BookkeepingRecord>> {
|
|
||||||
return dao.getRecordsByCategory(category)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取指定类型的记录
|
// 获取指定类型的记录
|
||||||
fun getRecordsByType(type: TransactionType): Flow<List<BookkeepingRecord>> {
|
fun getRecordsByType(type: TransactionType): Flow<List<BookkeepingRecord>> {
|
||||||
return dao.getRecordsByType(type)
|
return dao.getRecordsByType(type)
|
||||||
|
Loading…
Reference in New Issue
Block a user