分类迁移到设置
增加时间分类
This commit is contained in:
parent
316c2648ae
commit
21b8f020f5
@ -28,21 +28,21 @@ abstract class BookkeepingDatabase : RoomDatabase() {
|
|||||||
private var Instance: BookkeepingDatabase? = null
|
private var Instance: BookkeepingDatabase? = null
|
||||||
|
|
||||||
private val MIGRATION_1_2 = object : Migration(1, 2) {
|
private val MIGRATION_1_2 = object : Migration(1, 2) {
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "Starting migration from version 1 to 2")
|
Log.d(TAG, "Starting migration from version 1 to 2")
|
||||||
|
|
||||||
// 检查表是否存在
|
// 检查表是否存在
|
||||||
val cursor = database.query("SELECT name FROM sqlite_master WHERE type='table' AND name='categories'")
|
val cursor = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='categories'")
|
||||||
val tableExists = cursor.moveToFirst()
|
val tableExists = cursor.moveToFirst()
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
|
||||||
if (tableExists) {
|
if (tableExists) {
|
||||||
// 如果表存在,执行迁移
|
// 如果表存在,执行迁移
|
||||||
Log.d(TAG, "Categories table exists, performing migration")
|
Log.d(TAG, "Categories table exists, performing migration")
|
||||||
database.execSQL("ALTER TABLE categories RENAME TO categories_old")
|
db.execSQL("ALTER TABLE categories RENAME TO categories_old")
|
||||||
|
|
||||||
database.execSQL("""
|
db.execSQL("""
|
||||||
CREATE TABLE categories (
|
CREATE TABLE categories (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
@ -50,16 +50,16 @@ abstract class BookkeepingDatabase : RoomDatabase() {
|
|||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
database.execSQL("""
|
db.execSQL("""
|
||||||
INSERT INTO categories (name, type)
|
INSERT INTO categories (name, type)
|
||||||
SELECT name, type FROM categories_old
|
SELECT name, type FROM categories_old
|
||||||
""")
|
""")
|
||||||
|
|
||||||
database.execSQL("DROP TABLE categories_old")
|
db.execSQL("DROP TABLE categories_old")
|
||||||
} else {
|
} else {
|
||||||
// 如果表不存在,直接创建新表
|
// 如果表不存在,直接创建新表
|
||||||
Log.d(TAG, "Categories table does not exist, creating new table")
|
Log.d(TAG, "Categories table does not exist, creating new table")
|
||||||
database.execSQL("""
|
db.execSQL("""
|
||||||
CREATE TABLE IF NOT EXISTS categories (
|
CREATE TABLE IF NOT EXISTS categories (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
@ -69,7 +69,7 @@ abstract class BookkeepingDatabase : RoomDatabase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 确保 bookkeeping_records 表存在
|
// 确保 bookkeeping_records 表存在
|
||||||
database.execSQL("""
|
db.execSQL("""
|
||||||
CREATE TABLE IF NOT EXISTS bookkeeping_records (
|
CREATE TABLE IF NOT EXISTS bookkeeping_records (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
type TEXT NOT NULL,
|
type TEXT NOT NULL,
|
||||||
|
12
app/src/main/java/com/yovinchen/bookkeeping/model/Member.kt
Normal file
12
app/src/main/java/com/yovinchen/bookkeeping/model/Member.kt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package com.yovinchen.bookkeeping.model
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(tableName = "members")
|
||||||
|
data class Member(
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
val id: Int = 0,
|
||||||
|
val name: String,
|
||||||
|
val description: String = "" // 可选的描述信息
|
||||||
|
)
|
@ -0,0 +1,99 @@
|
|||||||
|
package com.yovinchen.bookkeeping.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Person
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
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 java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun RecordCard(
|
||||||
|
record: BookkeepingRecord,
|
||||||
|
members: List<Member>,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp)
|
||||||
|
.clickable(onClick = onClick),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = record.category,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
if (record.description.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = record.description,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault())
|
||||||
|
.format(record.date),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.End
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${if (record.type == TransactionType.EXPENSE) "-" else "+"}¥${String.format("%.2f", record.amount)}",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = if (record.type == TransactionType.EXPENSE)
|
||||||
|
MaterialTheme.colorScheme.error
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (members.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Person,
|
||||||
|
contentDescription = "Members",
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text = members.joinToString(", ") { it.name },
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,170 @@
|
|||||||
|
package com.yovinchen.bookkeeping.ui.dialog
|
||||||
|
|
||||||
|
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.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.yovinchen.bookkeeping.model.Member
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MemberManagementDialog(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
members: List<Member>,
|
||||||
|
onAddMember: (String, String) -> Unit,
|
||||||
|
onDeleteMember: (Member) -> Unit,
|
||||||
|
onUpdateMember: (Member, String, String) -> Unit
|
||||||
|
) {
|
||||||
|
var showAddDialog by remember { mutableStateOf(false) }
|
||||||
|
var selectedMember by remember { mutableStateOf<Member?>(null) }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text("成员管理") },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
items(members) { member ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
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 {
|
||||||
|
IconButton(onClick = { selectedMember = member }) {
|
||||||
|
Icon(Icons.Default.Edit, "编辑成员")
|
||||||
|
}
|
||||||
|
IconButton(onClick = { onDeleteMember(member) }) {
|
||||||
|
Icon(Icons.Default.Delete, "删除成员")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (members.indexOf(member) < members.size - 1) {
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { showAddDialog = true },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 16.dp)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Add, "添加成员")
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("添加成员")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text("关闭")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 添加成员对话框
|
||||||
|
if (showAddDialog) {
|
||||||
|
MemberEditDialog(
|
||||||
|
onDismiss = { showAddDialog = false },
|
||||||
|
onConfirm = { name, description ->
|
||||||
|
onAddMember(name, description)
|
||||||
|
showAddDialog = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑成员对话框
|
||||||
|
selectedMember?.let { member ->
|
||||||
|
MemberEditDialog(
|
||||||
|
onDismiss = { selectedMember = null },
|
||||||
|
onConfirm = { name, description ->
|
||||||
|
onUpdateMember(member, name, description)
|
||||||
|
selectedMember = null
|
||||||
|
},
|
||||||
|
initialName = member.name,
|
||||||
|
initialDescription = member.description
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MemberEditDialog(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onConfirm: (String, String) -> Unit,
|
||||||
|
initialName: String = "",
|
||||||
|
initialDescription: String = ""
|
||||||
|
) {
|
||||||
|
var name by remember { mutableStateOf(initialName) }
|
||||||
|
var description by remember { mutableStateOf(initialDescription) }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text(if (initialName.isEmpty()) "添加成员" else "编辑成员") },
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = name,
|
||||||
|
onValueChange = { name = it },
|
||||||
|
label = { Text("成员名称") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = description,
|
||||||
|
onValueChange = { description = it },
|
||||||
|
label = { Text("描述(可选)") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
if (name.isNotBlank()) {
|
||||||
|
onConfirm(name.trim(), description.trim())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = name.isNotBlank()
|
||||||
|
) {
|
||||||
|
Text("确定")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text("取消")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
package com.yovinchen.bookkeeping.viewmodel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.yovinchen.bookkeeping.data.BookkeepingDatabase
|
||||||
|
import com.yovinchen.bookkeeping.model.Category
|
||||||
|
import com.yovinchen.bookkeeping.model.TransactionType
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class SettingsViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
private val database = BookkeepingDatabase.getDatabase(application)
|
||||||
|
private val dao = database.bookkeepingDao()
|
||||||
|
|
||||||
|
private val _selectedCategoryType = MutableStateFlow(TransactionType.EXPENSE)
|
||||||
|
val selectedCategoryType: StateFlow<TransactionType> = _selectedCategoryType.asStateFlow()
|
||||||
|
|
||||||
|
val categories: StateFlow<List<Category>> = _selectedCategoryType
|
||||||
|
.flatMapLatest { type ->
|
||||||
|
dao.getCategoriesByType(type)
|
||||||
|
}
|
||||||
|
.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5000),
|
||||||
|
initialValue = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
fun setSelectedCategoryType(type: TransactionType) {
|
||||||
|
_selectedCategoryType.value = type
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addCategory(name: String, type: TransactionType) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val category = Category(name = name, type = type)
|
||||||
|
dao.insertCategory(category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteCategory(category: Category) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
dao.deleteCategory(category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateCategory(category: Category, newName: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val updatedCategory = category.copy(name = newName)
|
||||||
|
dao.updateCategory(updatedCategory)
|
||||||
|
// 更新所有使用该类别的记录
|
||||||
|
dao.updateRecordCategories(category.name, newName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun isCategoryInUse(categoryName: String): Boolean {
|
||||||
|
return dao.isCategoryInUse(categoryName)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user