feat: 升级到 v1.3.0 版本
- 完成图标美化计划 - 增加分类和成员图标支持 - 更新 README.md 文档
This commit is contained in:
parent
84d5b6c672
commit
9a0ed2ec7c
12
README.md
12
README.md
@ -46,6 +46,18 @@
|
||||
- [x] 成员消费分析
|
||||
- [x] 自定义统计周期
|
||||
|
||||
### 2.1 图标美化计划 (进行中 🎨)
|
||||
- [ ] 食品类图标 (餐饮、零食、饮料等)
|
||||
- [ ] 交通类图标 (公交、打车、加油等)
|
||||
- [ ] 购物类图标 (超市、数码、服装等)
|
||||
- [ ] 居住类图标 (房租、水电、物业等)
|
||||
- [ ] 医疗类图标 (药品、诊疗、保健等)
|
||||
- [ ] 娱乐类图标 (游戏、电影、旅游等)
|
||||
- [ ] 学习类图标 (书籍、课程、文具等)
|
||||
- [ ] 其他类图标 (礼物、捐赠、其他等)
|
||||
- [ ] 收入类图标 (工资、奖金、理财等)
|
||||
- [ ] 成员图标 (家人、朋友、同事等)
|
||||
|
||||
### 3. 数据管理 (进行中 🚀)
|
||||
- [ ] 导出 CSV/Excel 功能
|
||||
- [ ] 数据迁移工具
|
||||
|
@ -16,8 +16,8 @@ android {
|
||||
applicationId = "com.yovinchen.bookkeeping"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 5
|
||||
versionName = "1.2.4"
|
||||
versionCode = 6
|
||||
versionName = "1.3.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
@ -8,6 +8,7 @@ import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.yovinchen.bookkeeping.R
|
||||
import com.yovinchen.bookkeeping.model.BookkeepingRecord
|
||||
import com.yovinchen.bookkeeping.model.Category
|
||||
import com.yovinchen.bookkeeping.model.Converters
|
||||
@ -19,7 +20,7 @@ import kotlinx.coroutines.launch
|
||||
|
||||
@Database(
|
||||
entities = [BookkeepingRecord::class, Category::class, Member::class],
|
||||
version = 3,
|
||||
version = 4,
|
||||
exportSchema = false
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
@ -38,14 +39,15 @@ abstract class BookkeepingDatabase : RoomDatabase() {
|
||||
CREATE TABLE IF NOT EXISTS members (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT ''
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
icon INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
""")
|
||||
|
||||
// 插入默认成员
|
||||
db.execSQL("""
|
||||
INSERT INTO members (name, description)
|
||||
VALUES ('自己', '默认成员')
|
||||
INSERT INTO members (name, description, icon)
|
||||
VALUES ('自己', '默认成员', ${R.drawable.ic_member_boy_24dp})
|
||||
""")
|
||||
|
||||
// 修改记账记录表,添加成员ID字段
|
||||
@ -96,14 +98,15 @@ abstract class BookkeepingDatabase : RoomDatabase() {
|
||||
CREATE TABLE IF NOT EXISTS categories_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL
|
||||
type TEXT NOT NULL,
|
||||
icon INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
""")
|
||||
|
||||
// 复制分类数据
|
||||
db.execSQL("""
|
||||
INSERT INTO categories_new (id, name, type)
|
||||
SELECT id, name, type FROM categories
|
||||
INSERT INTO categories_new (id, name, type, icon)
|
||||
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
|
||||
private var INSTANCE: BookkeepingDatabase? = null
|
||||
|
||||
@ -124,7 +134,7 @@ abstract class BookkeepingDatabase : RoomDatabase() {
|
||||
BookkeepingDatabase::class.java,
|
||||
"bookkeeping_database"
|
||||
)
|
||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
|
||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
|
||||
.addCallback(object : Callback() {
|
||||
override fun onCreate(db: SupportSQLiteDatabase) {
|
||||
super.onCreate(db)
|
||||
@ -136,41 +146,53 @@ abstract class BookkeepingDatabase : RoomDatabase() {
|
||||
// 初始化默认成员
|
||||
database.memberDao().apply {
|
||||
if (getMemberCount() == 0) {
|
||||
insertMember(Member(name = "自己", description = "默认成员"))
|
||||
insertMember(Member(name = "老婆", description = "默认成员"))
|
||||
insertMember(Member(name = "老公", description = "默认成员"))
|
||||
insertMember(Member(name = "家庭", description = "默认成员"))
|
||||
insertMember(Member(name = "儿子", description = "默认成员"))
|
||||
insertMember(Member(name = "女儿", description = "默认成员"))
|
||||
insertMember(Member(name = "爸爸", description = "默认成员"))
|
||||
insertMember(Member(name = "妈妈", description = "默认成员"))
|
||||
insertMember(Member(name = "爷爷", description = "默认成员"))
|
||||
insertMember(Member(name = "奶奶", description = "默认成员"))
|
||||
insertMember(Member(name = "外公", description = "默认成员"))
|
||||
insertMember(Member(name = "外婆", description = "默认成员"))
|
||||
insertMember(Member(name = "其他人", description = "默认成员"))
|
||||
insertMember(Member(name = "自己", description = "默认成员", icon = R.drawable.ic_member_boy_24dp))
|
||||
insertMember(Member(name = "老婆", description = "默认成员", icon = R.drawable.ic_member_girl_24dp))
|
||||
insertMember(Member(name = "老公", description = "默认成员", icon = R.drawable.ic_member_boy_24dp))
|
||||
insertMember(Member(name = "家庭", description = "默认成员", icon = R.drawable.ic_member_family_24dp))
|
||||
insertMember(Member(name = "儿子", description = "默认成员", icon = R.drawable.ic_member_baby_boy_24dp))
|
||||
insertMember(Member(name = "女儿", description = "默认成员", icon = R.drawable.ic_member_baby_girl_24dp))
|
||||
insertMember(Member(name = "爸爸", description = "默认成员", icon = R.drawable.ic_member_father_24dp))
|
||||
insertMember(Member(name = "妈妈", description = "默认成员", icon = R.drawable.ic_member_mother_24dp))
|
||||
insertMember(Member(name = "爷爷", description = "默认成员", icon = R.drawable.ic_member_grandfather_24dp))
|
||||
insertMember(Member(name = "奶奶", description = "默认成员", icon = R.drawable.ic_member_grandmother_24dp))
|
||||
insertMember(Member(name = "外公", description = "默认成员", icon = R.drawable.ic_member_grandfather_24dp))
|
||||
insertMember(Member(name = "外婆", description = "默认成员", icon = R.drawable.ic_member_grandmother_24dp))
|
||||
insertMember(Member(name = "其他人", description = "默认成员", icon = R.drawable.ic_member_boy_24dp))
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化默认分类
|
||||
// 初始化默认分类
|
||||
database.categoryDao().apply {
|
||||
// 支出分类
|
||||
insertCategory(Category(name = "餐饮", type = TransactionType.EXPENSE))
|
||||
insertCategory(Category(name = "交通", type = TransactionType.EXPENSE))
|
||||
insertCategory(Category(name = "购物", type = TransactionType.EXPENSE))
|
||||
insertCategory(Category(name = "娱乐", type = TransactionType.EXPENSE))
|
||||
insertCategory(Category(name = "居住", type = TransactionType.EXPENSE))
|
||||
insertCategory(Category(name = "医疗", type = TransactionType.EXPENSE))
|
||||
insertCategory(Category(name = "教育", type = TransactionType.EXPENSE))
|
||||
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, icon = R.drawable.ic_category_taxi_24dp))
|
||||
insertCategory(Category(name = "购物", type = TransactionType.EXPENSE, icon = R.drawable.ic_category_supermarket_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_hotel_24dp))
|
||||
insertCategory(Category(name = "医疗", type = TransactionType.EXPENSE, icon = R.drawable.ic_category_medicine_24dp))
|
||||
insertCategory(Category(name = "教育", type = TransactionType.EXPENSE, icon = R.drawable.ic_category_training_24dp))
|
||||
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))
|
||||
insertCategory(Category(name = "投资", type = TransactionType.INCOME))
|
||||
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, icon = R.drawable.ic_category_gift_24dp))
|
||||
insertCategory(Category(name = "投资", type = TransactionType.INCOME, icon = R.drawable.ic_category_digital_24dp))
|
||||
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")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error initializing default data", e)
|
||||
|
@ -8,5 +8,6 @@ data class Category(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Long = 0,
|
||||
val name: String,
|
||||
val type: TransactionType
|
||||
val type: TransactionType,
|
||||
val icon: Int? = null
|
||||
)
|
||||
|
@ -8,5 +8,6 @@ data class Member(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Int = 0,
|
||||
val name: String,
|
||||
val description: String = "" // 可选的描述信息
|
||||
val description: String = "", // 可选的描述信息
|
||||
val icon: Int? = null // 新增icon字段,可为空
|
||||
)
|
||||
|
@ -6,10 +6,14 @@ 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.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.yovinchen.bookkeeping.model.BookkeepingRecord
|
||||
import com.yovinchen.bookkeeping.model.Member
|
||||
import com.yovinchen.bookkeeping.model.TransactionType
|
||||
import com.yovinchen.bookkeeping.utils.IconManager
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@ -24,6 +28,7 @@ fun RecordItem(
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) }
|
||||
val member = members.find { it.id == record.memberId }
|
||||
val categoryIcon = IconManager.getCategoryIconVector(record.category)
|
||||
|
||||
Card(
|
||||
modifier = modifier
|
||||
@ -36,9 +41,20 @@ fun RecordItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 左侧分类图标
|
||||
if (categoryIcon != null) {
|
||||
Icon(
|
||||
imageVector = categoryIcon,
|
||||
contentDescription = record.category,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
}
|
||||
|
||||
// 中间内容区域
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
@ -64,7 +80,7 @@ fun RecordItem(
|
||||
)
|
||||
}
|
||||
|
||||
// 金额显示
|
||||
// 右侧金额显示
|
||||
Text(
|
||||
text = String.format("%.2f", record.amount),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
|
@ -1,38 +1,23 @@
|
||||
package com.yovinchen.bookkeeping.ui.dialog
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
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.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.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.yovinchen.bookkeeping.model.Category
|
||||
import com.yovinchen.bookkeeping.model.TransactionType
|
||||
import com.yovinchen.bookkeeping.utils.IconManager
|
||||
|
||||
private const val TAG = "CategoryManagementDialog"
|
||||
|
||||
@ -41,233 +26,217 @@ private const val TAG = "CategoryManagementDialog"
|
||||
fun CategoryManagementDialog(
|
||||
onDismiss: () -> Unit,
|
||||
categories: List<Category>,
|
||||
onAddCategory: (String, TransactionType) -> Unit,
|
||||
onAddCategory: (String, TransactionType, Int?) -> Unit,
|
||||
onDeleteCategory: (Category) -> Unit,
|
||||
onUpdateCategory: (Category, String) -> Unit,
|
||||
onUpdateCategory: (Category, String, Int?) -> Unit,
|
||||
selectedType: TransactionType,
|
||||
onTypeChange: (TransactionType) -> Unit
|
||||
) {
|
||||
var newCategoryName by remember { mutableStateOf("") }
|
||||
var showDialog by remember { mutableStateOf(true) }
|
||||
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 }
|
||||
var showAddDialog by remember { mutableStateOf(false) }
|
||||
var editingCategory by remember { mutableStateOf<Category?>(null) }
|
||||
|
||||
Log.d(TAG, "Dialog state - showDialog: $showDialog, showDeleteDialog: $showDeleteDialog")
|
||||
Log.d(TAG, "Selected category: ${selectedCategory?.name}")
|
||||
|
||||
if (showDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
Log.d(TAG, "Main dialog dismiss requested")
|
||||
showDialog = false
|
||||
onDismiss()
|
||||
},
|
||||
title = { Text("类别管理") },
|
||||
text = {
|
||||
Column(
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("类别管理") },
|
||||
text = {
|
||||
Column {
|
||||
// 类型选择器
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
.padding(bottom = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// 类型选择
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
TransactionType.values().forEach { type ->
|
||||
FilterChip(
|
||||
selected = selectedType == TransactionType.EXPENSE,
|
||||
onClick = {
|
||||
Log.d(TAG, "Switching to EXPENSE type")
|
||||
onTypeChange(TransactionType.EXPENSE)
|
||||
},
|
||||
label = { Text("支出") }
|
||||
)
|
||||
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 = ""
|
||||
}
|
||||
selected = type == selectedType,
|
||||
onClick = { onTypeChange(type) },
|
||||
label = {
|
||||
Text(when (type) {
|
||||
TransactionType.EXPENSE -> "支出"
|
||||
TransactionType.INCOME -> "收入"
|
||||
})
|
||||
}
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = "添加类别")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 类别列表
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(filteredCategories) { category ->
|
||||
// 类别列表
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) {
|
||||
items(categories.filter { it.type == selectedType }) { category ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
.clickable { editingCategory = category },
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = category.name,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clickable {
|
||||
selectedCategory = category
|
||||
editingCategoryName = category.name
|
||||
showEditDialog = true
|
||||
}
|
||||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
Log.d(TAG, "Selected category for deletion: ${category.name}")
|
||||
selectedCategory = category
|
||||
showDeleteDialog = true
|
||||
// 显示类别图标
|
||||
if (category.icon != null) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = category.icon),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
} else {
|
||||
IconManager.getCategoryIconVector(category.name)?.let { icon ->
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
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(
|
||||
onClick = {
|
||||
Log.d(TAG, "Main dialog confirmed")
|
||||
showDialog = false
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
// 添加类别按钮
|
||||
Button(
|
||||
onClick = { showAddDialog = true },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.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) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
Log.d(TAG, "Delete dialog dismissed")
|
||||
showDeleteDialog = false
|
||||
selectedCategory = null
|
||||
},
|
||||
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 (showAddDialog) {
|
||||
CategoryEditDialog(
|
||||
onDismiss = { showAddDialog = false },
|
||||
onConfirm = { name, iconResId ->
|
||||
onAddCategory(name, selectedType, iconResId)
|
||||
showAddDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 编辑类别对话框
|
||||
if (showEditDialog && selectedCategory != null) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
showEditDialog = false
|
||||
selectedCategory = null
|
||||
editingCategoryName = ""
|
||||
editingCategory?.let { category ->
|
||||
CategoryEditDialog(
|
||||
onDismiss = { editingCategory = null },
|
||||
onConfirm = { name, iconResId ->
|
||||
onUpdateCategory(category, name, iconResId)
|
||||
editingCategory = null
|
||||
},
|
||||
title = { Text("编辑类别") },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = editingCategoryName,
|
||||
onValueChange = { editingCategoryName = it },
|
||||
label = { Text("类别名称") }
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (editingCategoryName.isNotBlank()) {
|
||||
selectedCategory?.let { category ->
|
||||
onUpdateCategory(category, editingCategoryName)
|
||||
}
|
||||
}
|
||||
showEditDialog = false
|
||||
selectedCategory = null
|
||||
editingCategoryName = ""
|
||||
}
|
||||
) {
|
||||
Text("确定")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showEditDialog = false
|
||||
selectedCategory = null
|
||||
editingCategoryName = ""
|
||||
}
|
||||
) {
|
||||
Text("取消")
|
||||
}
|
||||
}
|
||||
initialName = category.name,
|
||||
initialIcon = category.icon
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CategoryEditDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (String, Int?) -> Unit,
|
||||
initialName: String = "",
|
||||
initialIcon: Int? = null
|
||||
) {
|
||||
var name by remember { mutableStateOf(initialName) }
|
||||
var selectedIcon by remember { mutableStateOf(initialIcon) }
|
||||
var showIconPicker by remember { mutableStateOf(false) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(if (initialName.isEmpty()) "添加类别" else "编辑类别") },
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text("名称") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
// 图标选择按钮
|
||||
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 = "选择类别图标"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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("取消")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
@ -7,20 +7,25 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
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.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.yovinchen.bookkeeping.model.Member
|
||||
import com.yovinchen.bookkeeping.utils.IconManager
|
||||
|
||||
@Composable
|
||||
fun MemberManagementDialog(
|
||||
onDismiss: () -> Unit,
|
||||
members: List<Member>,
|
||||
onAddMember: (String, String) -> Unit,
|
||||
onAddMember: (String, String, Int?) -> Unit,
|
||||
onDeleteMember: (Member) -> Unit,
|
||||
onUpdateMember: (Member, String, String) -> Unit
|
||||
onUpdateMember: (Member, String, String, Int?) -> Unit
|
||||
) {
|
||||
var showAddDialog by remember { mutableStateOf(false) }
|
||||
var selectedMember by remember { mutableStateOf<Member?>(null) }
|
||||
@ -43,31 +48,55 @@ fun MemberManagementDialog(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
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(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
// 显示成员图标
|
||||
if (member.icon != null) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(member.icon),
|
||||
contentDescription = null,
|
||||
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 {
|
||||
IconButton(onClick = { selectedMember = member }) {
|
||||
Icon(Icons.Default.Edit, "编辑成员")
|
||||
Icon(Icons.Default.Edit, contentDescription = "编辑")
|
||||
}
|
||||
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 },
|
||||
modifier = Modifier
|
||||
.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))
|
||||
Text("添加成员")
|
||||
}
|
||||
@ -85,32 +114,31 @@ fun MemberManagementDialog(
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("关闭")
|
||||
Text("完成")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 添加成员对话框
|
||||
if (showAddDialog) {
|
||||
MemberEditDialog(
|
||||
onDismiss = { showAddDialog = false },
|
||||
onConfirm = { name, description ->
|
||||
onAddMember(name, description)
|
||||
onConfirm = { name, description, iconResId ->
|
||||
onAddMember(name, description, iconResId)
|
||||
showAddDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 编辑成员对话框
|
||||
selectedMember?.let { member ->
|
||||
MemberEditDialog(
|
||||
onDismiss = { selectedMember = null },
|
||||
onConfirm = { name, description ->
|
||||
onUpdateMember(member, name, description)
|
||||
onConfirm = { name, description, iconResId ->
|
||||
onUpdateMember(member, name, description, iconResId)
|
||||
selectedMember = null
|
||||
},
|
||||
initialName = member.name,
|
||||
initialDescription = member.description
|
||||
initialDescription = member.description,
|
||||
initialIcon = member.icon
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -118,45 +146,63 @@ fun MemberManagementDialog(
|
||||
@Composable
|
||||
private fun MemberEditDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (String, String) -> Unit,
|
||||
onConfirm: (String, String, Int?) -> Unit,
|
||||
initialName: String = "",
|
||||
initialDescription: String = ""
|
||||
initialDescription: String = "",
|
||||
initialIcon: Int? = null
|
||||
) {
|
||||
var name by remember { mutableStateOf(initialName) }
|
||||
var description by remember { mutableStateOf(initialDescription) }
|
||||
var selectedIcon by remember { mutableStateOf(initialIcon) }
|
||||
var showIconPicker by remember { mutableStateOf(false) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(if (initialName.isEmpty()) "添加成员" else "编辑成员") },
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text("成员名称") },
|
||||
label = { Text("名称") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = description,
|
||||
onValueChange = { description = it },
|
||||
label = { Text("描述(可选)") },
|
||||
label = { Text("描述") },
|
||||
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 = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (name.isNotBlank()) {
|
||||
onConfirm(name.trim(), description.trim())
|
||||
onConfirm(name, description, selectedIcon)
|
||||
}
|
||||
},
|
||||
enabled = name.isNotBlank()
|
||||
}
|
||||
) {
|
||||
Text("确定")
|
||||
}
|
||||
@ -167,4 +213,17 @@ private fun MemberEditDialog(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (showIconPicker) {
|
||||
IconPickerDialog(
|
||||
onDismiss = { showIconPicker = false },
|
||||
onIconSelected = {
|
||||
selectedIcon = it
|
||||
showIconPicker = false
|
||||
},
|
||||
selectedIcon = selectedIcon,
|
||||
isMemberIcon = true,
|
||||
title = "选择成员图标"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,13 @@
|
||||
package com.yovinchen.bookkeeping.ui.navigation
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
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.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
@ -22,20 +15,38 @@ import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.yovinchen.bookkeeping.R
|
||||
import com.yovinchen.bookkeeping.model.AnalysisType
|
||||
import com.yovinchen.bookkeeping.model.ThemeMode
|
||||
import com.yovinchen.bookkeeping.ui.screen.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
import java.time.YearMonth
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
sealed class Screen(
|
||||
val route: String,
|
||||
val title: String,
|
||||
val icon: ImageVector? = null
|
||||
val iconResId: Int? = null
|
||||
) {
|
||||
object Home : Screen("home", "记账", Icons.AutoMirrored.Filled.List)
|
||||
object Analysis : Screen("analysis", "分析", Icons.Default.Analytics)
|
||||
object Settings : Screen("settings", "设置", Icons.Default.Settings)
|
||||
@Composable
|
||||
fun icon(): ImageVector? = iconResId?.let { ImageVector.vectorResource(it) }
|
||||
|
||||
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(
|
||||
"category_detail/{category}/{startMonth}/{endMonth}",
|
||||
"分类详情"
|
||||
@ -75,6 +86,11 @@ fun MainNavigation(
|
||||
onThemeChange: (ThemeMode) -> Unit
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
val items = listOf(
|
||||
Screen.Home,
|
||||
Screen.Analysis,
|
||||
Screen.Settings
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
@ -82,11 +98,21 @@ fun MainNavigation(
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentRoute = navBackStackEntry?.destination?.route
|
||||
|
||||
Screen.bottomNavigationItems().forEach { screen ->
|
||||
items.forEach { screen ->
|
||||
val selected = currentRoute == screen.route
|
||||
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) },
|
||||
selected = currentRoute == screen.route,
|
||||
selected = selected,
|
||||
onClick = {
|
||||
navController.navigate(screen.route) {
|
||||
popUpTo(navController.graph.findStartDestination().id) {
|
||||
|
@ -152,9 +152,13 @@ fun SettingsScreen(
|
||||
CategoryManagementDialog(
|
||||
onDismiss = { showCategoryDialog = false },
|
||||
categories = categories,
|
||||
onAddCategory = viewModel::addCategory,
|
||||
onAddCategory = { name, type, iconResId ->
|
||||
viewModel.addCategory(name, type, iconResId)
|
||||
},
|
||||
onDeleteCategory = viewModel::deleteCategory,
|
||||
onUpdateCategory = viewModel::updateCategory,
|
||||
onUpdateCategory = { category, newName, iconResId ->
|
||||
viewModel.updateCategory(category, newName, iconResId)
|
||||
},
|
||||
selectedType = selectedType,
|
||||
onTypeChange = viewModel::setSelectedCategoryType
|
||||
)
|
||||
@ -165,10 +169,16 @@ fun SettingsScreen(
|
||||
MemberManagementDialog(
|
||||
onDismiss = { showMemberDialog = false },
|
||||
members = members,
|
||||
onAddMember = memberViewModel::addMember,
|
||||
onAddMember = { name, description, iconResId ->
|
||||
memberViewModel.addMember(name, description, iconResId)
|
||||
},
|
||||
onDeleteMember = memberViewModel::deleteMember,
|
||||
onUpdateMember = { member, name, description ->
|
||||
memberViewModel.updateMember(member.copy(name = name, description = description))
|
||||
onUpdateMember = { member, name, description, iconResId ->
|
||||
memberViewModel.updateMember(member.copy(
|
||||
name = name,
|
||||
description = description,
|
||||
icon = iconResId
|
||||
))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package com.yovinchen.bookkeeping.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.yovinchen.bookkeeping.data.BookkeepingDatabase
|
||||
@ -19,7 +18,6 @@ import java.util.*
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private val TAG = "HomeViewModel"
|
||||
private val bookkeepingDao = BookkeepingDatabase.getDatabase(application).bookkeepingDao()
|
||||
private val memberDao = BookkeepingDatabase.getDatabase(application).memberDao()
|
||||
private val categoryDao = BookkeepingDatabase.getDatabase(application).categoryDao()
|
||||
|
@ -13,9 +13,9 @@ class MemberViewModel(application: Application) : AndroidViewModel(application)
|
||||
|
||||
val allMembers: Flow<List<Member>> = memberDao.getAllMembers()
|
||||
|
||||
fun addMember(name: String, description: String = "") {
|
||||
fun addMember(name: String, description: String = "", iconResId: Int? = null) {
|
||||
viewModelScope.launch {
|
||||
val member = Member(name = name, description = description)
|
||||
val member = Member(name = name, description = description, icon = iconResId)
|
||||
memberDao.insertMember(member)
|
||||
}
|
||||
}
|
||||
|
@ -32,9 +32,9 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
||||
_selectedCategoryType.value = type
|
||||
}
|
||||
|
||||
fun addCategory(name: String, type: TransactionType) {
|
||||
fun addCategory(name: String, type: TransactionType, iconResId: Int?) {
|
||||
viewModelScope.launch {
|
||||
val category = Category(name = name, type = type)
|
||||
val category = Category(name = name, type = type, icon = iconResId)
|
||||
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 {
|
||||
val updatedCategory = category.copy(name = newName)
|
||||
val updatedCategory = category.copy(name = newName, icon = iconResId)
|
||||
dao.updateCategory(updatedCategory)
|
||||
// 更新所有使用该类别的记录
|
||||
dao.updateRecordCategories(category.name, newName)
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
@ -1,6 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
|
Loading…
Reference in New Issue
Block a user