feat: 升级到 v1.3.0 版本

- 完成图标美化计划
- 增加分类和成员图标支持
- 更新 README.md 文档
This commit is contained in:
yovinchen 2024-12-16 23:40:17 +08:00
parent 84d5b6c672
commit 9a0ed2ec7c
53 changed files with 665 additions and 406 deletions

View File

@ -46,6 +46,18 @@
- [x] 成员消费分析 - [x] 成员消费分析
- [x] 自定义统计周期 - [x] 自定义统计周期
### 2.1 图标美化计划 (进行中 🎨)
- [ ] 食品类图标 (餐饮、零食、饮料等)
- [ ] 交通类图标 (公交、打车、加油等)
- [ ] 购物类图标 (超市、数码、服装等)
- [ ] 居住类图标 (房租、水电、物业等)
- [ ] 医疗类图标 (药品、诊疗、保健等)
- [ ] 娱乐类图标 (游戏、电影、旅游等)
- [ ] 学习类图标 (书籍、课程、文具等)
- [ ] 其他类图标 (礼物、捐赠、其他等)
- [ ] 收入类图标 (工资、奖金、理财等)
- [ ] 成员图标 (家人、朋友、同事等)
### 3. 数据管理 (进行中 🚀) ### 3. 数据管理 (进行中 🚀)
- [ ] 导出 CSV/Excel 功能 - [ ] 导出 CSV/Excel 功能
- [ ] 数据迁移工具 - [ ] 数据迁移工具

View File

@ -16,8 +16,8 @@ android {
applicationId = "com.yovinchen.bookkeeping" applicationId = "com.yovinchen.bookkeeping"
minSdk = 26 minSdk = 26
targetSdk = 34 targetSdk = 34
versionCode = 5 versionCode = 6
versionName = "1.2.4" versionName = "1.3.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {

View File

@ -8,6 +8,7 @@ import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
import androidx.room.migration.Migration import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import com.yovinchen.bookkeeping.R
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.Converters import com.yovinchen.bookkeeping.model.Converters
@ -19,7 +20,7 @@ import kotlinx.coroutines.launch
@Database( @Database(
entities = [BookkeepingRecord::class, Category::class, Member::class], entities = [BookkeepingRecord::class, Category::class, Member::class],
version = 3, version = 4,
exportSchema = false exportSchema = false
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
@ -38,14 +39,15 @@ abstract class BookkeepingDatabase : RoomDatabase() {
CREATE TABLE IF NOT EXISTS members ( CREATE TABLE IF NOT EXISTS members (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '' description TEXT NOT NULL DEFAULT '',
icon INTEGER NOT NULL DEFAULT 0
) )
""") """)
// 插入默认成员 // 插入默认成员
db.execSQL(""" db.execSQL("""
INSERT INTO members (name, description) INSERT INTO members (name, description, icon)
VALUES ('自己', '默认成员') VALUES ('自己', '默认成员', ${R.drawable.ic_member_boy_24dp})
""") """)
// 修改记账记录表添加成员ID字段 // 修改记账记录表添加成员ID字段
@ -96,14 +98,15 @@ abstract class BookkeepingDatabase : RoomDatabase() {
CREATE TABLE IF NOT EXISTS categories_new ( CREATE TABLE IF NOT EXISTS categories_new (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
type TEXT NOT NULL type TEXT NOT NULL,
icon INTEGER NOT NULL DEFAULT 0
) )
""") """)
// 复制分类数据 // 复制分类数据
db.execSQL(""" db.execSQL("""
INSERT INTO categories_new (id, name, type) INSERT INTO categories_new (id, name, type, icon)
SELECT id, name, type FROM categories SELECT id, name, type, 0 FROM categories
""") """)
// 删除旧表 // 删除旧表
@ -114,6 +117,13 @@ abstract class BookkeepingDatabase : RoomDatabase() {
} }
} }
private val MIGRATION_3_4 = object : Migration(3, 4) {
override fun migrate(db: SupportSQLiteDatabase) {
// 如果需要,在这里添加数据库迁移逻辑
// 由于这次更改可能只是schema hash的变化我们不需要实际的数据库更改
}
}
@Volatile @Volatile
private var INSTANCE: BookkeepingDatabase? = null private var INSTANCE: BookkeepingDatabase? = null
@ -124,7 +134,7 @@ abstract class BookkeepingDatabase : RoomDatabase() {
BookkeepingDatabase::class.java, BookkeepingDatabase::class.java,
"bookkeeping_database" "bookkeeping_database"
) )
.addMigrations(MIGRATION_1_2, MIGRATION_2_3) .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
.addCallback(object : Callback() { .addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) { override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db) super.onCreate(db)
@ -136,41 +146,53 @@ abstract class BookkeepingDatabase : RoomDatabase() {
// 初始化默认成员 // 初始化默认成员
database.memberDao().apply { database.memberDao().apply {
if (getMemberCount() == 0) { if (getMemberCount() == 0) {
insertMember(Member(name = "自己", description = "默认成员")) insertMember(Member(name = "自己", description = "默认成员", icon = R.drawable.ic_member_boy_24dp))
insertMember(Member(name = "老婆", description = "默认成员")) insertMember(Member(name = "老婆", description = "默认成员", icon = R.drawable.ic_member_girl_24dp))
insertMember(Member(name = "老公", description = "默认成员")) insertMember(Member(name = "老公", description = "默认成员", icon = R.drawable.ic_member_boy_24dp))
insertMember(Member(name = "家庭", description = "默认成员")) insertMember(Member(name = "家庭", description = "默认成员", icon = R.drawable.ic_member_family_24dp))
insertMember(Member(name = "儿子", description = "默认成员")) insertMember(Member(name = "儿子", description = "默认成员", icon = R.drawable.ic_member_baby_boy_24dp))
insertMember(Member(name = "女儿", description = "默认成员")) insertMember(Member(name = "女儿", description = "默认成员", icon = R.drawable.ic_member_baby_girl_24dp))
insertMember(Member(name = "爸爸", description = "默认成员")) insertMember(Member(name = "爸爸", description = "默认成员", icon = R.drawable.ic_member_father_24dp))
insertMember(Member(name = "妈妈", description = "默认成员")) insertMember(Member(name = "妈妈", description = "默认成员", icon = R.drawable.ic_member_mother_24dp))
insertMember(Member(name = "爷爷", description = "默认成员")) insertMember(Member(name = "爷爷", description = "默认成员", icon = R.drawable.ic_member_grandfather_24dp))
insertMember(Member(name = "奶奶", description = "默认成员")) insertMember(Member(name = "奶奶", description = "默认成员", icon = R.drawable.ic_member_grandmother_24dp))
insertMember(Member(name = "外公", description = "默认成员")) insertMember(Member(name = "外公", description = "默认成员", icon = R.drawable.ic_member_grandfather_24dp))
insertMember(Member(name = "外婆", description = "默认成员")) insertMember(Member(name = "外婆", description = "默认成员", icon = R.drawable.ic_member_grandmother_24dp))
insertMember(Member(name = "其他人", description = "默认成员")) insertMember(Member(name = "其他人", description = "默认成员", icon = R.drawable.ic_member_boy_24dp))
} }
} }
// 初始化默认分类 // 初始化默认分类
database.categoryDao().apply { database.categoryDao().apply {
// 支出分类 // 支出分类
insertCategory(Category(name = "餐饮", type = TransactionType.EXPENSE)) insertCategory(Category(name = "餐饮", type = TransactionType.EXPENSE, icon = R.drawable.ic_category_food_24dp))
insertCategory(Category(name = "交通", type = TransactionType.EXPENSE)) insertCategory(Category(name = "交通", type = TransactionType.EXPENSE, icon = R.drawable.ic_category_taxi_24dp))
insertCategory(Category(name = "购物", type = TransactionType.EXPENSE)) insertCategory(Category(name = "购物", type = TransactionType.EXPENSE, icon = R.drawable.ic_category_supermarket_24dp))
insertCategory(Category(name = "娱乐", type = TransactionType.EXPENSE)) insertCategory(Category(name = "娱乐", type = TransactionType.EXPENSE, icon = R.drawable.ic_category_bar_24dp))
insertCategory(Category(name = "居住", type = TransactionType.EXPENSE)) insertCategory(Category(name = "居住", type = TransactionType.EXPENSE, icon = R.drawable.ic_category_hotel_24dp))
insertCategory(Category(name = "医疗", type = TransactionType.EXPENSE)) insertCategory(Category(name = "医疗", type = TransactionType.EXPENSE, icon = R.drawable.ic_category_medicine_24dp))
insertCategory(Category(name = "教育", type = TransactionType.EXPENSE)) insertCategory(Category(name = "教育", type = TransactionType.EXPENSE, icon = R.drawable.ic_category_training_24dp))
insertCategory(Category(name = "其他支出", type = TransactionType.EXPENSE)) insertCategory(Category(name = "宠物", type = TransactionType.EXPENSE, icon = R.drawable.ic_category_pet_24dp))
insertCategory(Category(name = "花卉", type = TransactionType.EXPENSE, icon = R.drawable.ic_category_flower_24dp))
insertCategory(Category(name = "酒吧", type = TransactionType.EXPENSE, icon = R.drawable.ic_category_bar_24dp))
insertCategory(Category(name = "快递", type = TransactionType.EXPENSE, icon = R.drawable.ic_category_delivery_24dp))
insertCategory(Category(name = "数码", type = TransactionType.EXPENSE, icon = R.drawable.ic_category_digital_24dp))
insertCategory(Category(name = "化妆品", type = TransactionType.EXPENSE, icon = R.drawable.ic_category_cosmetics_24dp))
insertCategory(Category(name = "水果", type = TransactionType.EXPENSE, icon = R.drawable.ic_category_fruit_24dp))
insertCategory(Category(name = "零食", type = TransactionType.EXPENSE, icon = R.drawable.ic_category_snack_24dp))
insertCategory(Category(name = "蔬菜", type = TransactionType.EXPENSE, icon = R.drawable.ic_category_vegetable_24dp))
insertCategory(Category(name = "其他支出", type = TransactionType.EXPENSE, icon = R.drawable.ic_category_more_24dp))
// 收入分类 // 收入分类
insertCategory(Category(name = "工资", type = TransactionType.INCOME)) insertCategory(Category(name = "工资", type = TransactionType.INCOME, icon = R.drawable.ic_category_membership_24dp))
insertCategory(Category(name = "奖金", type = TransactionType.INCOME)) insertCategory(Category(name = "奖金", type = TransactionType.INCOME, icon = R.drawable.ic_category_gift_24dp))
insertCategory(Category(name = "投资", type = TransactionType.INCOME)) insertCategory(Category(name = "投资", type = TransactionType.INCOME, icon = R.drawable.ic_category_digital_24dp))
insertCategory(Category(name = "其他收入", type = TransactionType.INCOME)) insertCategory(Category(name = "礼物", type = TransactionType.INCOME, icon = R.drawable.ic_category_gift_24dp))
insertCategory(Category(name = "会员费", type = TransactionType.INCOME, icon = R.drawable.ic_category_membership_24dp))
insertCategory(Category(name = "其他收入", type = TransactionType.INCOME, icon = R.drawable.ic_category_more_24dp))
} }
Log.d(TAG, "Default data initialized successfully") Log.d(TAG, "Default data initialized successfully")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error initializing default data", e) Log.e(TAG, "Error initializing default data", e)

View File

@ -8,5 +8,6 @@ data class Category(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
val id: Long = 0, val id: Long = 0,
val name: String, val name: String,
val type: TransactionType val type: TransactionType,
val icon: Int? = null
) )

View File

@ -8,5 +8,6 @@ data class Member(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
val id: Int = 0, val id: Int = 0,
val name: String, val name: String,
val description: String = "" // 可选的描述信息 val description: String = "", // 可选的描述信息
val icon: Int? = null // 新增icon字段可为空
) )

View File

@ -6,10 +6,14 @@ 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.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.yovinchen.bookkeeping.model.BookkeepingRecord import com.yovinchen.bookkeeping.model.BookkeepingRecord
import com.yovinchen.bookkeeping.model.Member import com.yovinchen.bookkeeping.model.Member
import com.yovinchen.bookkeeping.model.TransactionType import com.yovinchen.bookkeeping.model.TransactionType
import com.yovinchen.bookkeeping.utils.IconManager
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -24,6 +28,7 @@ fun RecordItem(
var showDeleteDialog by remember { mutableStateOf(false) } var showDeleteDialog by remember { mutableStateOf(false) }
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 }
val categoryIcon = IconManager.getCategoryIconVector(record.category)
Card( Card(
modifier = modifier modifier = modifier
@ -36,9 +41,20 @@ fun RecordItem(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// 左侧分类图标
if (categoryIcon != null) {
Icon(
imageVector = categoryIcon,
contentDescription = record.category,
modifier = Modifier.size(24.dp),
tint = Color.Unspecified
)
}
// 中间内容区域
Column( Column(
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {
@ -64,7 +80,7 @@ fun RecordItem(
) )
} }
// 金额显示 // 右侧金额显示
Text( Text(
text = String.format("%.2f", record.amount), text = String.format("%.2f", record.amount),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,

View File

@ -1,38 +1,23 @@
package com.yovinchen.bookkeeping.ui.dialog package com.yovinchen.bookkeeping.ui.dialog
import android.util.Log
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.material.icons.Icons import androidx.compose.material.icons.Icons
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.material3.AlertDialog import androidx.compose.material3.*
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.*
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
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.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
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 com.yovinchen.bookkeeping.utils.IconManager
private const val TAG = "CategoryManagementDialog" private const val TAG = "CategoryManagementDialog"
@ -41,233 +26,217 @@ private const val TAG = "CategoryManagementDialog"
fun CategoryManagementDialog( fun CategoryManagementDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
categories: List<Category>, categories: List<Category>,
onAddCategory: (String, TransactionType) -> Unit, onAddCategory: (String, TransactionType, Int?) -> Unit,
onDeleteCategory: (Category) -> Unit, onDeleteCategory: (Category) -> Unit,
onUpdateCategory: (Category, String) -> Unit, onUpdateCategory: (Category, String, Int?) -> Unit,
selectedType: TransactionType, selectedType: TransactionType,
onTypeChange: (TransactionType) -> Unit onTypeChange: (TransactionType) -> Unit
) { ) {
var newCategoryName by remember { mutableStateOf("") } var showAddDialog by remember { mutableStateOf(false) }
var showDialog by remember { mutableStateOf(true) } var editingCategory by remember { mutableStateOf<Category?>(null) }
var showDeleteDialog by remember { mutableStateOf(false) }
var showEditDialog by remember { mutableStateOf(false) }
var selectedCategory: Category? by remember { mutableStateOf(null) }
var editingCategoryName by remember { mutableStateOf("") }
val filteredCategories = categories.filter { it.type == selectedType }
Log.d(TAG, "Dialog state - showDialog: $showDialog, showDeleteDialog: $showDeleteDialog") AlertDialog(
Log.d(TAG, "Selected category: ${selectedCategory?.name}") onDismissRequest = onDismiss,
title = { Text("类别管理") },
if (showDialog) { text = {
AlertDialog( Column {
onDismissRequest = { // 类型选择器
Log.d(TAG, "Main dialog dismiss requested") Row(
showDialog = false
onDismiss()
},
title = { Text("类别管理") },
text = {
Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 8.dp) .padding(bottom = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
// 类型选择 TransactionType.values().forEach { type ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
FilterChip( FilterChip(
selected = selectedType == TransactionType.EXPENSE, selected = type == selectedType,
onClick = { onClick = { onTypeChange(type) },
Log.d(TAG, "Switching to EXPENSE type") label = {
onTypeChange(TransactionType.EXPENSE) Text(when (type) {
}, TransactionType.EXPENSE -> "支出"
label = { Text("支出") } TransactionType.INCOME -> "收入"
) })
FilterChip(
selected = selectedType == TransactionType.INCOME,
onClick = {
Log.d(TAG, "Switching to INCOME type")
onTypeChange(TransactionType.INCOME)
},
label = { Text("收入") }
)
}
Spacer(modifier = Modifier.height(16.dp))
// 添加新类别
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
value = newCategoryName,
onValueChange = { newCategoryName = it },
label = { Text("新类别名称") },
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
IconButton(
onClick = {
if (newCategoryName.isNotBlank()) {
Log.d(TAG, "Adding new category: $newCategoryName")
onAddCategory(newCategoryName, selectedType)
newCategoryName = ""
}
} }
) { )
Icon(Icons.Default.Add, contentDescription = "添加类别")
}
} }
}
Spacer(modifier = Modifier.height(16.dp)) // 类别列表
LazyColumn(
// 类别列表 modifier = Modifier
LazyColumn( .fillMaxWidth()
modifier = Modifier.fillMaxWidth(), .weight(1f)
verticalArrangement = Arrangement.spacedBy(8.dp) ) {
) { items(categories.filter { it.type == selectedType }) { category ->
items(filteredCategories) { category -> Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
.clickable { editingCategory = category },
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row( Row(
modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.weight(1f)
verticalAlignment = Alignment.CenterVertically
) { ) {
Text( // 显示类别图标
text = category.name, if (category.icon != null) {
modifier = Modifier Icon(
.weight(1f) imageVector = ImageVector.vectorResource(id = category.icon),
.clickable { contentDescription = null,
selectedCategory = category modifier = Modifier.size(24.dp),
editingCategoryName = category.name tint = Color.Unspecified
showEditDialog = true )
} } else {
) IconManager.getCategoryIconVector(category.name)?.let { icon ->
IconButton( Icon(
onClick = { imageVector = icon,
Log.d(TAG, "Selected category for deletion: ${category.name}") contentDescription = null,
selectedCategory = category modifier = Modifier.size(24.dp),
showDeleteDialog = true tint = Color.Unspecified
)
} }
) {
Icon(Icons.Default.Delete, contentDescription = "删除类别")
} }
Spacer(modifier = Modifier.width(8.dp))
Text(category.name)
}
IconButton(
onClick = { onDeleteCategory(category) },
enabled = categories.size > 1
) {
Icon(
Icons.Default.Delete,
contentDescription = "删除",
tint = if (categories.size > 1)
MaterialTheme.colorScheme.error
else
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
)
} }
} }
} }
} }
},
confirmButton = { // 添加类别按钮
TextButton( Button(
onClick = { onClick = { showAddDialog = true },
Log.d(TAG, "Main dialog confirmed") modifier = Modifier
showDialog = false .fillMaxWidth()
onDismiss() .padding(top = 8.dp)
}
) { ) {
Text("完成") Icon(Icons.Default.Add, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("添加类别")
} }
} }
) },
} confirmButton = {
TextButton(onClick = onDismiss) {
Text("完成")
}
}
)
// 删除确认对话框 // 添加类别对话框
if (showDeleteDialog && selectedCategory != null) { if (showAddDialog) {
AlertDialog( CategoryEditDialog(
onDismissRequest = { onDismiss = { showAddDialog = false },
Log.d(TAG, "Delete dialog dismissed") onConfirm = { name, iconResId ->
showDeleteDialog = false onAddCategory(name, selectedType, iconResId)
selectedCategory = null showAddDialog = false
},
title = { Text("确认删除") },
text = {
Text(
text = buildString {
append("确定要删除类别 ")
append(selectedCategory?.name ?: "")
append(" 吗?")
}
)
},
confirmButton = {
TextButton(
onClick = {
try {
selectedCategory?.let { category ->
Log.d(TAG, "Confirming deletion of category: ${category.name}")
onDeleteCategory(category)
}
} catch (e: Exception) {
Log.e(TAG, "Error during category deletion callback", e)
e.printStackTrace()
} finally {
showDeleteDialog = false
selectedCategory = null
}
}
) {
Text("确定")
}
},
dismissButton = {
TextButton(
onClick = {
Log.d(TAG, "Canceling deletion")
showDeleteDialog = false
selectedCategory = null
}
) {
Text("取消")
}
} }
) )
} }
// 编辑类别对话框 // 编辑类别对话框
if (showEditDialog && selectedCategory != null) { editingCategory?.let { category ->
AlertDialog( CategoryEditDialog(
onDismissRequest = { onDismiss = { editingCategory = null },
showEditDialog = false onConfirm = { name, iconResId ->
selectedCategory = null onUpdateCategory(category, name, iconResId)
editingCategoryName = "" editingCategory = null
}, },
title = { Text("编辑类别") }, initialName = category.name,
text = { initialIcon = category.icon
OutlinedTextField( )
value = editingCategoryName, }
onValueChange = { editingCategoryName = it }, }
label = { Text("类别名称") }
) @Composable
}, private fun CategoryEditDialog(
confirmButton = { onDismiss: () -> Unit,
TextButton( onConfirm: (String, Int?) -> Unit,
onClick = { initialName: String = "",
if (editingCategoryName.isNotBlank()) { initialIcon: Int? = null
selectedCategory?.let { category -> ) {
onUpdateCategory(category, editingCategoryName) var name by remember { mutableStateOf(initialName) }
} var selectedIcon by remember { mutableStateOf(initialIcon) }
} var showIconPicker by remember { mutableStateOf(false) }
showEditDialog = false
selectedCategory = null AlertDialog(
editingCategoryName = "" onDismissRequest = onDismiss,
} title = { Text(if (initialName.isEmpty()) "添加类别" else "编辑类别") },
) { text = {
Text("确定") Column(
} modifier = Modifier.fillMaxWidth(),
}, verticalArrangement = Arrangement.spacedBy(8.dp)
dismissButton = { ) {
TextButton( OutlinedTextField(
onClick = { value = name,
showEditDialog = false onValueChange = { name = it },
selectedCategory = null label = { Text("名称") },
editingCategoryName = "" modifier = Modifier.fillMaxWidth()
} )
) {
Text("取消") // 图标选择按钮
} Button(
} onClick = { showIconPicker = true },
modifier = Modifier.fillMaxWidth()
) {
selectedIcon?.let { iconResId ->
Icon(
imageVector = ImageVector.vectorResource(id = iconResId),
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = Color.Unspecified
)
Spacer(modifier = Modifier.width(8.dp))
}
Text(if (selectedIcon == null) "选择图标" else "更改图标")
}
}
},
confirmButton = {
TextButton(
onClick = {
if (name.isNotBlank()) {
onConfirm(name, selectedIcon)
}
}
) {
Text("确定")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("取消")
}
}
)
if (showIconPicker) {
IconPickerDialog(
onDismiss = { showIconPicker = false },
onIconSelected = {
selectedIcon = it
showIconPicker = false
},
selectedIcon = selectedIcon,
isMemberIcon = false,
title = "选择类别图标"
) )
} }
} }

View File

@ -0,0 +1,61 @@
package com.yovinchen.bookkeeping.ui.dialog
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import com.yovinchen.bookkeeping.utils.IconManager
@Composable
fun IconPickerDialog(
onDismiss: () -> Unit,
onIconSelected: (Int) -> Unit,
selectedIcon: Int? = null,
isMemberIcon: Boolean = false,
title: String = "选择图标"
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(title) },
text = {
LazyVerticalGrid(
columns = GridCells.Fixed(4),
modifier = Modifier
.fillMaxWidth()
.height(300.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
val icons = if (isMemberIcon) {
IconManager.getAllMemberIcons()
} else {
IconManager.getAllCategoryIcons()
}
items(icons) { iconResId ->
Icon(
imageVector = ImageVector.vectorResource(id = iconResId),
contentDescription = null,
modifier = Modifier
.size(40.dp)
.clickable { onIconSelected(iconResId) },
tint = if (selectedIcon == iconResId) MaterialTheme.colorScheme.primary else Color.Unspecified
)
}
}
},
confirmButton = {
TextButton(onClick = onDismiss) {
Text("取消")
}
}
)
}

View File

@ -7,20 +7,25 @@ import androidx.compose.material.icons.Icons
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.Edit import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Person
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.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.yovinchen.bookkeeping.model.Member import com.yovinchen.bookkeeping.model.Member
import com.yovinchen.bookkeeping.utils.IconManager
@Composable @Composable
fun MemberManagementDialog( fun MemberManagementDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
members: List<Member>, members: List<Member>,
onAddMember: (String, String) -> Unit, onAddMember: (String, String, Int?) -> Unit,
onDeleteMember: (Member) -> Unit, onDeleteMember: (Member) -> Unit,
onUpdateMember: (Member, String, String) -> Unit onUpdateMember: (Member, String, String, Int?) -> Unit
) { ) {
var showAddDialog by remember { mutableStateOf(false) } var showAddDialog by remember { mutableStateOf(false) }
var selectedMember by remember { mutableStateOf<Member?>(null) } var selectedMember by remember { mutableStateOf<Member?>(null) }
@ -43,31 +48,55 @@ fun MemberManagementDialog(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Column(modifier = Modifier.weight(1f)) { Row(
Text( verticalAlignment = Alignment.CenterVertically,
text = member.name, modifier = Modifier.weight(1f)
style = MaterialTheme.typography.titleMedium ) {
) // 显示成员图标
if (member.description.isNotEmpty()) { if (member.icon != null) {
Text( Icon(
text = member.description, imageVector = ImageVector.vectorResource(member.icon),
style = MaterialTheme.typography.bodyMedium, contentDescription = null,
color = MaterialTheme.colorScheme.onSurfaceVariant modifier = Modifier.size(24.dp),
tint = Color.Unspecified
) )
} else {
IconManager.getMemberIconVector(member.name)?.let { icon ->
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = Color.Unspecified
)
}
}
Spacer(modifier = Modifier.width(8.dp))
Column {
Text(
text = member.name,
style = MaterialTheme.typography.titleMedium
)
if (member.description.isNotEmpty()) {
Text(
text = member.description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} }
} }
Row { Row {
IconButton(onClick = { selectedMember = member }) { IconButton(onClick = { selectedMember = member }) {
Icon(Icons.Default.Edit, "编辑成员") Icon(Icons.Default.Edit, contentDescription = "编辑")
} }
IconButton(onClick = { onDeleteMember(member) }) { IconButton(onClick = { onDeleteMember(member) }) {
Icon(Icons.Default.Delete, "删除成员") Icon(Icons.Default.Delete, contentDescription = "删除")
} }
} }
} }
if (members.indexOf(member) < members.size - 1) {
HorizontalDivider()
}
} }
} }
@ -75,9 +104,9 @@ fun MemberManagementDialog(
onClick = { showAddDialog = true }, onClick = { showAddDialog = true },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 16.dp) .padding(top = 8.dp)
) { ) {
Icon(Icons.Default.Add, "添加成员") Icon(Icons.Default.Add, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text("添加成员") Text("添加成员")
} }
@ -85,32 +114,31 @@ fun MemberManagementDialog(
}, },
confirmButton = { confirmButton = {
TextButton(onClick = onDismiss) { TextButton(onClick = onDismiss) {
Text("关闭") Text("完成")
} }
} }
) )
// 添加成员对话框
if (showAddDialog) { if (showAddDialog) {
MemberEditDialog( MemberEditDialog(
onDismiss = { showAddDialog = false }, onDismiss = { showAddDialog = false },
onConfirm = { name, description -> onConfirm = { name, description, iconResId ->
onAddMember(name, description) onAddMember(name, description, iconResId)
showAddDialog = false showAddDialog = false
} }
) )
} }
// 编辑成员对话框
selectedMember?.let { member -> selectedMember?.let { member ->
MemberEditDialog( MemberEditDialog(
onDismiss = { selectedMember = null }, onDismiss = { selectedMember = null },
onConfirm = { name, description -> onConfirm = { name, description, iconResId ->
onUpdateMember(member, name, description) onUpdateMember(member, name, description, iconResId)
selectedMember = null selectedMember = null
}, },
initialName = member.name, initialName = member.name,
initialDescription = member.description initialDescription = member.description,
initialIcon = member.icon
) )
} }
} }
@ -118,45 +146,63 @@ fun MemberManagementDialog(
@Composable @Composable
private fun MemberEditDialog( private fun MemberEditDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
onConfirm: (String, String) -> Unit, onConfirm: (String, String, Int?) -> Unit,
initialName: String = "", initialName: String = "",
initialDescription: String = "" initialDescription: String = "",
initialIcon: Int? = null
) { ) {
var name by remember { mutableStateOf(initialName) } var name by remember { mutableStateOf(initialName) }
var description by remember { mutableStateOf(initialDescription) } var description by remember { mutableStateOf(initialDescription) }
var selectedIcon by remember { mutableStateOf(initialIcon) }
var showIconPicker by remember { mutableStateOf(false) }
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text(if (initialName.isEmpty()) "添加成员" else "编辑成员") }, title = { Text(if (initialName.isEmpty()) "添加成员" else "编辑成员") },
text = { text = {
Column( Column(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth() verticalArrangement = Arrangement.spacedBy(8.dp)
.padding(vertical = 8.dp)
) { ) {
OutlinedTextField( OutlinedTextField(
value = name, value = name,
onValueChange = { name = it }, onValueChange = { name = it },
label = { Text("成员名称") }, label = { Text("名称") },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField( OutlinedTextField(
value = description, value = description,
onValueChange = { description = it }, onValueChange = { description = it },
label = { Text("描述(可选)") }, label = { Text("描述") },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
// 图标选择按钮
Button(
onClick = { showIconPicker = true },
modifier = Modifier.fillMaxWidth()
) {
selectedIcon?.let { iconResId ->
Icon(
imageVector = ImageVector.vectorResource(iconResId),
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = Color.Unspecified
)
Spacer(modifier = Modifier.width(8.dp))
}
Text(if (selectedIcon == null) "选择图标" else "更改图标")
}
} }
}, },
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
if (name.isNotBlank()) { if (name.isNotBlank()) {
onConfirm(name.trim(), description.trim()) onConfirm(name, description, selectedIcon)
} }
}, }
enabled = name.isNotBlank()
) { ) {
Text("确定") Text("确定")
} }
@ -167,4 +213,17 @@ private fun MemberEditDialog(
} }
} }
) )
if (showIconPicker) {
IconPickerDialog(
onDismiss = { showIconPicker = false },
onIconSelected = {
selectedIcon = it
showIconPicker = false
},
selectedIcon = selectedIcon,
isMemberIcon = true,
title = "选择成员图标"
)
}
} }

View File

@ -1,20 +1,13 @@
package com.yovinchen.bookkeeping.ui.navigation 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.foundation.layout.size
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.filled.Analytics
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
@ -22,20 +15,38 @@ 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 androidx.navigation.navArgument
import com.yovinchen.bookkeeping.R
import com.yovinchen.bookkeeping.model.AnalysisType import com.yovinchen.bookkeeping.model.AnalysisType
import com.yovinchen.bookkeeping.model.ThemeMode import com.yovinchen.bookkeeping.model.ThemeMode
import com.yovinchen.bookkeeping.ui.screen.* import com.yovinchen.bookkeeping.ui.screen.*
import androidx.compose.material3.*
import androidx.compose.ui.unit.dp
import java.time.YearMonth import java.time.YearMonth
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
sealed class Screen( sealed class Screen(
val route: String, val route: String,
val title: String, val title: String,
val icon: ImageVector? = null val iconResId: Int? = null
) { ) {
object Home : Screen("home", "记账", Icons.AutoMirrored.Filled.List) @Composable
object Analysis : Screen("analysis", "分析", Icons.Default.Analytics) fun icon(): ImageVector? = iconResId?.let { ImageVector.vectorResource(it) }
object Settings : Screen("settings", "设置", Icons.Default.Settings)
object Home : Screen(
"home",
"记账",
iconResId = R.drawable.account
)
object Analysis : Screen(
"analysis",
"分析",
iconResId = R.drawable.piechart
)
object Settings : Screen(
"settings",
"设置",
iconResId = R.drawable.setting
)
object CategoryDetail : Screen( object CategoryDetail : Screen(
"category_detail/{category}/{startMonth}/{endMonth}", "category_detail/{category}/{startMonth}/{endMonth}",
"分类详情" "分类详情"
@ -75,6 +86,11 @@ fun MainNavigation(
onThemeChange: (ThemeMode) -> Unit onThemeChange: (ThemeMode) -> Unit
) { ) {
val navController = rememberNavController() val navController = rememberNavController()
val items = listOf(
Screen.Home,
Screen.Analysis,
Screen.Settings
)
Scaffold( Scaffold(
bottomBar = { bottomBar = {
@ -82,11 +98,21 @@ fun MainNavigation(
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route val currentRoute = navBackStackEntry?.destination?.route
Screen.bottomNavigationItems().forEach { screen -> items.forEach { screen ->
val selected = currentRoute == screen.route
NavigationBarItem( NavigationBarItem(
icon = { Icon(screen.icon!!, contentDescription = screen.title) }, icon = {
screen.icon()?.let { icon ->
Icon(
imageVector = icon,
contentDescription = screen.title,
modifier = Modifier.size(24.dp),
tint = Color.Unspecified
)
}
},
label = { Text(screen.title) }, label = { Text(screen.title) },
selected = currentRoute == screen.route, selected = selected,
onClick = { onClick = {
navController.navigate(screen.route) { navController.navigate(screen.route) {
popUpTo(navController.graph.findStartDestination().id) { popUpTo(navController.graph.findStartDestination().id) {

View File

@ -152,9 +152,13 @@ fun SettingsScreen(
CategoryManagementDialog( CategoryManagementDialog(
onDismiss = { showCategoryDialog = false }, onDismiss = { showCategoryDialog = false },
categories = categories, categories = categories,
onAddCategory = viewModel::addCategory, onAddCategory = { name, type, iconResId ->
viewModel.addCategory(name, type, iconResId)
},
onDeleteCategory = viewModel::deleteCategory, onDeleteCategory = viewModel::deleteCategory,
onUpdateCategory = viewModel::updateCategory, onUpdateCategory = { category, newName, iconResId ->
viewModel.updateCategory(category, newName, iconResId)
},
selectedType = selectedType, selectedType = selectedType,
onTypeChange = viewModel::setSelectedCategoryType onTypeChange = viewModel::setSelectedCategoryType
) )
@ -165,10 +169,16 @@ fun SettingsScreen(
MemberManagementDialog( MemberManagementDialog(
onDismiss = { showMemberDialog = false }, onDismiss = { showMemberDialog = false },
members = members, members = members,
onAddMember = memberViewModel::addMember, onAddMember = { name, description, iconResId ->
memberViewModel.addMember(name, description, iconResId)
},
onDeleteMember = memberViewModel::deleteMember, onDeleteMember = memberViewModel::deleteMember,
onUpdateMember = { member, name, description -> onUpdateMember = { member, name, description, iconResId ->
memberViewModel.updateMember(member.copy(name = name, description = description)) memberViewModel.updateMember(member.copy(
name = name,
description = description,
icon = iconResId
))
} }
) )
} }

View File

@ -0,0 +1,84 @@
package com.yovinchen.bookkeeping.utils
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import com.yovinchen.bookkeeping.R
object IconManager {
// 类别图标映射
private val categoryIcons = mapOf(
"食品" to R.drawable.ic_category_food_24dp,
"交通" to R.drawable.ic_category_taxi_24dp,
"娱乐" to R.drawable.ic_category_bar_24dp,
"购物" to R.drawable.ic_category_supermarket_24dp,
"工资" to R.drawable.ic_category_membership_24dp,
"服装" to R.drawable.ic_category_clothes_24dp,
"数码" to R.drawable.ic_category_digital_24dp,
"饮料" to R.drawable.ic_category_drink_24dp,
"医疗" to R.drawable.ic_category_medicine_24dp,
"旅行" to R.drawable.ic_category_travel_24dp,
"便利店" to R.drawable.ic_category_convenience_24dp,
"化妆品" to R.drawable.ic_category_cosmetics_24dp,
"外卖" to R.drawable.ic_category_delivery_24dp,
"鲜花" to R.drawable.ic_category_flower_24dp,
"水果" to R.drawable.ic_category_fruit_24dp,
"礼物" to R.drawable.ic_category_gift_24dp,
"住宿" to R.drawable.ic_category_hotel_24dp,
"宠物" to R.drawable.ic_category_pet_24dp,
"景点" to R.drawable.ic_category_scenic_24dp,
"零食" to R.drawable.ic_category_snack_24dp,
"培训" to R.drawable.ic_category_training_24dp,
"蔬菜" to R.drawable.ic_category_vegetable_24dp,
"婴儿" to R.drawable.ic_category_baby_24dp,
"餐饮" to R.drawable.ic_category_food_24dp, // 添加餐饮分类
"居住" to R.drawable.ic_category_hotel_24dp, // 添加居住分类
"其他" to R.drawable.ic_category_more_24dp
)
// 成员图标映射
private val memberIcons = mapOf(
"自己" to R.drawable.ic_member_boy_24dp,
"家庭" to R.drawable.ic_member_family_24dp,
"父亲" to R.drawable.ic_member_father_24dp,
"母亲" to R.drawable.ic_member_mother_24dp,
"男宝" to R.drawable.ic_member_baby_boy_24dp,
"女宝" to R.drawable.ic_member_baby_girl_24dp,
"新娘" to R.drawable.ic_member_bride_24dp,
"新郎" to R.drawable.ic_member_groom_24dp,
"爷爷" to R.drawable.ic_member_grandfather_24dp,
"奶奶" to R.drawable.ic_member_grandmother_24dp,
"男生" to R.drawable.ic_member_boy_24dp,
"女生" to R.drawable.ic_member_girl_24dp,
"其他" to R.drawable.ic_member_girl_24dp
)
@Composable
fun getCategoryIconVector(name: String): ImageVector? {
return categoryIcons[name]?.let { ImageVector.vectorResource(id = it) }
}
@Composable
fun getMemberIconVector(name: String): ImageVector? {
return memberIcons[name]?.let { ImageVector.vectorResource(id = it) }
}
@DrawableRes
fun getCategoryIcon(name: String): Int? {
return categoryIcons[name]
}
@DrawableRes
fun getMemberIcon(name: String): Int? {
return memberIcons[name]
}
fun getAllCategoryIcons(): List<Int> {
return categoryIcons.values.toList()
}
fun getAllMemberIcons(): List<Int> {
return memberIcons.values.toList()
}
}

View File

@ -1,7 +1,6 @@
package com.yovinchen.bookkeeping.viewmodel package com.yovinchen.bookkeeping.viewmodel
import android.app.Application import android.app.Application
import android.util.Log
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
@ -19,7 +18,6 @@ import java.util.*
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class HomeViewModel(application: Application) : AndroidViewModel(application) { class HomeViewModel(application: Application) : AndroidViewModel(application) {
private val TAG = "HomeViewModel"
private val bookkeepingDao = BookkeepingDatabase.getDatabase(application).bookkeepingDao() private val bookkeepingDao = BookkeepingDatabase.getDatabase(application).bookkeepingDao()
private val memberDao = BookkeepingDatabase.getDatabase(application).memberDao() private val memberDao = BookkeepingDatabase.getDatabase(application).memberDao()
private val categoryDao = BookkeepingDatabase.getDatabase(application).categoryDao() private val categoryDao = BookkeepingDatabase.getDatabase(application).categoryDao()

View File

@ -13,9 +13,9 @@ class MemberViewModel(application: Application) : AndroidViewModel(application)
val allMembers: Flow<List<Member>> = memberDao.getAllMembers() val allMembers: Flow<List<Member>> = memberDao.getAllMembers()
fun addMember(name: String, description: String = "") { fun addMember(name: String, description: String = "", iconResId: Int? = null) {
viewModelScope.launch { viewModelScope.launch {
val member = Member(name = name, description = description) val member = Member(name = name, description = description, icon = iconResId)
memberDao.insertMember(member) memberDao.insertMember(member)
} }
} }

View File

@ -32,9 +32,9 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
_selectedCategoryType.value = type _selectedCategoryType.value = type
} }
fun addCategory(name: String, type: TransactionType) { fun addCategory(name: String, type: TransactionType, iconResId: Int?) {
viewModelScope.launch { viewModelScope.launch {
val category = Category(name = name, type = type) val category = Category(name = name, type = type, icon = iconResId)
dao.insertCategory(category) dao.insertCategory(category)
} }
} }
@ -45,9 +45,9 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
} }
} }
fun updateCategory(category: Category, newName: String) { fun updateCategory(category: Category, newName: String, iconResId: Int?) {
viewModelScope.launch { viewModelScope.launch {
val updatedCategory = category.copy(name = newName) val updatedCategory = category.copy(name = newName, icon = iconResId)
dao.updateCategory(updatedCategory) dao.updateCategory(updatedCategory)
// 更新所有使用该类别的记录 // 更新所有使用该类别的记录
dao.updateRecordCategories(category.name, newName) dao.updateRecordCategories(category.name, newName)

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp" android:width="24dp"
android:height="200dp" android:height="24dp"
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path