Compare commits
2 Commits
f59fda3de7
...
d0bd40421a
Author | SHA1 | Date | |
---|---|---|---|
d0bd40421a | |||
ea1dafd0d2 |
@ -4,6 +4,14 @@
|
|||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
<DropdownSelection timestamp="2024-11-27T02:15:16.043756Z">
|
||||||
|
<Target type="DEFAULT_BOOT">
|
||||||
|
<handle>
|
||||||
|
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/yovinchen/.android/avd/Pixel_7a_API_34.avd" />
|
||||||
|
</handle>
|
||||||
|
</Target>
|
||||||
|
</DropdownSelection>
|
||||||
|
<DialogSelection />
|
||||||
</SelectionState>
|
</SelectionState>
|
||||||
</selectionStates>
|
</selectionStates>
|
||||||
</component>
|
</component>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
|
6
.idea/studiobot.xml
Normal file
6
.idea/studiobot.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="StudioBotProjectSettings">
|
||||||
|
<option name="shareContext" value="OptedIn" />
|
||||||
|
</component>
|
||||||
|
</project>
|
128
README.md
128
README.md
@ -1,103 +1,59 @@
|
|||||||
# Bookkeeping App
|
# 轻记账 (Lightweight Bookkeeping)
|
||||||
|
|
||||||
一个基于 Jetpack Compose 开发的现代化记账应用。
|
一个轻量级的个人记账应用,专注于隐私和离线使用。
|
||||||
|
|
||||||
## 项目概述
|
## 🌟 特点
|
||||||
|
|
||||||
本项目是一个使用 Kotlin 和 Jetpack Compose 开发的 Android 记账应用,采用 MVVM 架构,提供简洁直观的用户界面和丰富的记账功能。
|
- 🔒 完全离线运行,无需网络连接
|
||||||
|
- 📱 极简权限要求,仅使用必要的系统权限
|
||||||
|
- 💰 支持收入和支出记录
|
||||||
|
- 👥 支持多人记账
|
||||||
|
- 📊 按日期和类别统计
|
||||||
|
- 🎨 Material You 设计风格
|
||||||
|
|
||||||
## 主要特性
|
## 🛠 技术栈
|
||||||
|
|
||||||
- 💰 收入/支出记录管理
|
- 语言:Kotlin
|
||||||
- 👥 成员管理系统
|
- UI框架:Jetpack Compose
|
||||||
- 📊 分类管理系统
|
- 数据库:Room
|
||||||
- 📅 自定义日期选择器
|
- 架构:MVVM
|
||||||
- 📈 月度统计视图
|
|
||||||
- 🎨 Material 3 设计风格
|
|
||||||
|
|
||||||
## 技术栈
|
## 📱 功能
|
||||||
|
|
||||||
- 开发语言:Kotlin
|
### 记账管理
|
||||||
- UI 框架:Jetpack Compose
|
- 收入和支出记录
|
||||||
- 架构模式:MVVM
|
- 自定义分类管理
|
||||||
- 数据存储:Room Database
|
- 日期和时间选择
|
||||||
- 依赖注入:Hilt
|
- 备注说明
|
||||||
- 异步处理:Kotlin Coroutines
|
|
||||||
|
|
||||||
## 开发计划
|
### 成员管理
|
||||||
|
- 多人记账支持
|
||||||
|
- 成员关联记录
|
||||||
|
- 按成员筛选统计
|
||||||
|
|
||||||
### 0. 基础功能 (已完成)
|
### 数据统计
|
||||||
- [x] 收入/支出记录管理
|
- 月度收支统计
|
||||||
- [x] 分类管理系统
|
- 分类统计
|
||||||
- [x] 默认分类
|
- 每日收支明细
|
||||||
- [x] 自定义分类
|
|
||||||
- [x] 分类编辑/删除
|
|
||||||
- [x] 自定义日期选择器
|
|
||||||
- [x] Material 3 设计界面
|
|
||||||
- [x] 深色/浅色主题切换
|
|
||||||
- [x] 主题色自定义
|
|
||||||
|
|
||||||
### 1. 成员管理功能 (feature/member)
|
## 🔒 隐私保护
|
||||||
- [ ] 成员添加/编辑/删除
|
|
||||||
- [ ] 记账时选择相关成员
|
|
||||||
- [ ] 成员消费统计
|
|
||||||
- [ ] 成员间账单分摊
|
|
||||||
|
|
||||||
### 2. 数据统计与可视化 (feature/statistics)
|
- 完全离线运行,数据存储在本地
|
||||||
- [ ] 支出/收入趋势图表
|
- 无需任何网络权限
|
||||||
- [ ] 分类占比饼图
|
- 最小化系统权限要求
|
||||||
- [ ] 月度/年度报表
|
|
||||||
|
|
||||||
### 3. 数据导出与备份 (feature/backup)
|
## 📝 系统要求
|
||||||
- [ ] 导出 CSV/Excel 功能
|
|
||||||
- [ ] 云端备份支持
|
|
||||||
- [ ] 数据迁移工具
|
|
||||||
|
|
||||||
### 4. 预算管理 (feature/budget)
|
- Android 5.0 (API 21) 或更高版本
|
||||||
- [ ] 月度预算设置
|
- 存储权限(用于数据备份,可选)
|
||||||
- [ ] 预算超支提醒
|
|
||||||
- [ ] 分类预算管理
|
|
||||||
|
|
||||||
### 5. 用户体验优化 (feature/ux-enhancement)
|
## 🔜 未来计划
|
||||||
- [x] 深色模式支持
|
|
||||||
- [ ] 手势操作优化
|
|
||||||
- [ ] 快速记账小组件
|
|
||||||
- [ ] 多语言支持
|
|
||||||
|
|
||||||
### 6. 性能优化 (feature/performance)
|
- [ ] 数据导出和备份
|
||||||
- [ ] 大数据量处理优化
|
- [ ] 预算管理
|
||||||
- [ ] 启动速度优化
|
- [ ] 更多统计图表
|
||||||
- [ ] 内存使用优化
|
- [ ] 自定义主题
|
||||||
|
|
||||||
## 分支管理
|
## 📄 许可证
|
||||||
|
|
||||||
- `master`: 稳定主分支
|
[MIT License](LICENSE)
|
||||||
- `develop`: 主开发分支
|
|
||||||
- `feature/*`: 功能开发分支
|
|
||||||
- `release/*`: 版本发布分支
|
|
||||||
|
|
||||||
## 版本历史
|
|
||||||
|
|
||||||
### v1.0.0
|
|
||||||
- ✨ 基础记账功能
|
|
||||||
- 收入/支出记录
|
|
||||||
- 金额、日期、分类、备注管理
|
|
||||||
- 🎨 Material 3 设计界面
|
|
||||||
- 深色/浅色主题切换
|
|
||||||
- 主题色自定义
|
|
||||||
- 📊 分类管理
|
|
||||||
- 默认分类预设
|
|
||||||
- 自定义分类支持
|
|
||||||
- 分类编辑与删除
|
|
||||||
- 📅 月度统计
|
|
||||||
- 月度收支总览
|
|
||||||
- 月份快速切换
|
|
||||||
- 🗓️ 自定义日期选择器
|
|
||||||
|
|
||||||
## 贡献指南
|
|
||||||
|
|
||||||
欢迎提交 Issue 和 Pull Request 来帮助改进项目。
|
|
||||||
|
|
||||||
## 许可证
|
|
||||||
|
|
||||||
本项目采用 MIT 许可证。
|
|
||||||
|
@ -12,50 +12,42 @@ interface BookkeepingDao {
|
|||||||
@Query("SELECT * FROM bookkeeping_records ORDER BY date DESC")
|
@Query("SELECT * FROM bookkeeping_records ORDER BY date DESC")
|
||||||
fun getAllRecords(): Flow<List<BookkeepingRecord>>
|
fun getAllRecords(): Flow<List<BookkeepingRecord>>
|
||||||
|
|
||||||
@Insert
|
@Query("SELECT * FROM bookkeeping_records WHERE memberId = :memberId OR memberId IS NULL ORDER BY date DESC")
|
||||||
suspend fun insertRecord(record: BookkeepingRecord)
|
fun getRecordsByMember(memberId: Int): Flow<List<BookkeepingRecord>>
|
||||||
|
|
||||||
@Delete
|
@Query("SELECT * FROM bookkeeping_records WHERE date BETWEEN :startDate AND :endDate ORDER BY date DESC")
|
||||||
suspend fun deleteRecord(record: BookkeepingRecord)
|
fun getRecordsByDateRange(startDate: Date, endDate: Date): Flow<List<BookkeepingRecord>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM bookkeeping_records WHERE (memberId = :memberId OR memberId IS NULL) AND date BETWEEN :startDate AND :endDate ORDER BY date DESC")
|
||||||
|
fun getRecordsByMemberAndDateRange(memberId: Int, startDate: Date, endDate: Date): Flow<List<BookkeepingRecord>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM bookkeeping_records WHERE type = :type ORDER BY date DESC")
|
||||||
|
fun getRecordsByType(type: TransactionType): Flow<List<BookkeepingRecord>>
|
||||||
|
|
||||||
|
@Query("SELECT SUM(amount) FROM bookkeeping_records WHERE type = :type AND (memberId = :memberId OR memberId IS NULL)")
|
||||||
|
fun getTotalAmountByType(type: TransactionType, memberId: Int? = null): Flow<Double?>
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
suspend fun insertRecord(record: BookkeepingRecord): Long
|
||||||
|
|
||||||
@Update
|
@Update
|
||||||
suspend fun updateRecord(record: BookkeepingRecord)
|
suspend fun updateRecord(record: BookkeepingRecord)
|
||||||
|
|
||||||
@Query("SELECT * FROM bookkeeping_records WHERE type = 'INCOME'")
|
@Delete
|
||||||
fun getAllIncome(): Flow<List<BookkeepingRecord>>
|
suspend fun deleteRecord(record: BookkeepingRecord)
|
||||||
|
|
||||||
@Query("SELECT * FROM bookkeeping_records WHERE type = 'EXPENSE'")
|
|
||||||
fun getAllExpense(): Flow<List<BookkeepingRecord>>
|
|
||||||
|
|
||||||
// 按日期查询
|
|
||||||
@Query("SELECT * FROM bookkeeping_records WHERE date >= :startOfDay AND date < :endOfDay ORDER BY date DESC")
|
|
||||||
fun getRecordsByDate(startOfDay: Date, endOfDay: Date): Flow<List<BookkeepingRecord>>
|
|
||||||
|
|
||||||
// 按日期范围查询
|
|
||||||
@Query("SELECT * FROM bookkeeping_records WHERE date BETWEEN :startDate AND :endDate ORDER BY date DESC")
|
|
||||||
fun getRecordsByDateRange(startDate: Date, endDate: Date): Flow<List<BookkeepingRecord>>
|
|
||||||
|
|
||||||
// 按类别查询
|
|
||||||
@Query("SELECT * FROM bookkeeping_records WHERE category = :category ORDER BY date DESC")
|
|
||||||
fun getRecordsByCategory(category: String): Flow<List<BookkeepingRecord>>
|
|
||||||
|
|
||||||
// 按类型查询
|
|
||||||
@Query("SELECT * FROM bookkeeping_records WHERE type = :type ORDER BY date DESC")
|
|
||||||
fun getRecordsByType(type: TransactionType): Flow<List<BookkeepingRecord>>
|
|
||||||
|
|
||||||
// Category related queries
|
|
||||||
@Query("SELECT * FROM categories WHERE type = :type ORDER BY name ASC")
|
@Query("SELECT * FROM categories WHERE type = :type ORDER BY name ASC")
|
||||||
fun getCategoriesByType(type: TransactionType): Flow<List<Category>>
|
fun getCategoriesByType(type: TransactionType): Flow<List<Category>>
|
||||||
|
|
||||||
@Insert
|
@Insert
|
||||||
suspend fun insertCategory(category: Category)
|
suspend fun insertCategory(category: Category): Long
|
||||||
|
|
||||||
@Delete
|
|
||||||
suspend fun deleteCategory(category: Category)
|
|
||||||
|
|
||||||
@Update
|
@Update
|
||||||
suspend fun updateCategory(category: Category)
|
suspend fun updateCategory(category: Category)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun deleteCategory(category: Category)
|
||||||
|
|
||||||
@Query("SELECT EXISTS(SELECT 1 FROM bookkeeping_records WHERE category = :categoryName LIMIT 1)")
|
@Query("SELECT EXISTS(SELECT 1 FROM bookkeeping_records WHERE category = :categoryName LIMIT 1)")
|
||||||
suspend fun isCategoryInUse(categoryName: String): Boolean
|
suspend fun isCategoryInUse(categoryName: String): Boolean
|
||||||
|
|
||||||
|
@ -11,159 +11,164 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
|||||||
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
|
||||||
|
import com.yovinchen.bookkeeping.model.Member
|
||||||
import com.yovinchen.bookkeeping.model.TransactionType
|
import com.yovinchen.bookkeeping.model.TransactionType
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Database(entities = [BookkeepingRecord::class, Category::class], version = 2, exportSchema = false)
|
@Database(
|
||||||
|
entities = [BookkeepingRecord::class, Category::class, Member::class],
|
||||||
|
version = 3,
|
||||||
|
exportSchema = false
|
||||||
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
abstract class BookkeepingDatabase : RoomDatabase() {
|
abstract class BookkeepingDatabase : RoomDatabase() {
|
||||||
abstract fun bookkeepingDao(): BookkeepingDao
|
abstract fun bookkeepingDao(): BookkeepingDao
|
||||||
|
abstract fun categoryDao(): CategoryDao
|
||||||
|
abstract fun memberDao(): MemberDao
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "BookkeepingDatabase"
|
private const val TAG = "BookkeepingDatabase"
|
||||||
|
|
||||||
@Volatile
|
|
||||||
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(db: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
try {
|
// 创建成员表
|
||||||
Log.d(TAG, "Starting migration from version 1 to 2")
|
database.execSQL("""
|
||||||
|
CREATE TABLE IF NOT EXISTS members (
|
||||||
// 检查表是否存在
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
val cursor = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='categories'")
|
name TEXT NOT NULL,
|
||||||
val tableExists = cursor.moveToFirst()
|
description TEXT NOT NULL DEFAULT ''
|
||||||
cursor.close()
|
)
|
||||||
|
""")
|
||||||
if (tableExists) {
|
|
||||||
// 如果表存在,执行迁移
|
// 插入默认成员
|
||||||
Log.d(TAG, "Categories table exists, performing migration")
|
database.execSQL("""
|
||||||
db.execSQL("ALTER TABLE categories RENAME TO categories_old")
|
INSERT INTO members (name, description)
|
||||||
|
VALUES ('自己', '默认成员')
|
||||||
db.execSQL("""
|
""")
|
||||||
CREATE TABLE categories (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
// 修改记账记录表,添加成员ID字段
|
||||||
name TEXT NOT NULL,
|
database.execSQL("""
|
||||||
type TEXT NOT NULL
|
ALTER TABLE bookkeeping_records
|
||||||
)
|
ADD COLUMN memberId INTEGER DEFAULT NULL
|
||||||
""")
|
REFERENCES members(id) ON DELETE SET NULL
|
||||||
|
""")
|
||||||
db.execSQL("""
|
|
||||||
INSERT INTO categories (name, type)
|
// 更新现有记录,将其关联到默认成员
|
||||||
SELECT name, type FROM categories_old
|
database.execSQL("""
|
||||||
""")
|
UPDATE bookkeeping_records
|
||||||
|
SET memberId = (SELECT id FROM members WHERE name = '我自己')
|
||||||
db.execSQL("DROP TABLE categories_old")
|
""")
|
||||||
} else {
|
|
||||||
// 如果表不存在,直接创建新表
|
|
||||||
Log.d(TAG, "Categories table does not exist, creating new table")
|
|
||||||
db.execSQL("""
|
|
||||||
CREATE TABLE IF NOT EXISTS categories (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
type TEXT NOT NULL
|
|
||||||
)
|
|
||||||
""")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保 bookkeeping_records 表存在
|
|
||||||
db.execSQL("""
|
|
||||||
CREATE TABLE IF NOT EXISTS bookkeeping_records (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
||||||
type TEXT NOT NULL,
|
|
||||||
amount REAL NOT NULL,
|
|
||||||
category TEXT NOT NULL,
|
|
||||||
description TEXT NOT NULL,
|
|
||||||
date INTEGER NOT NULL
|
|
||||||
)
|
|
||||||
""")
|
|
||||||
|
|
||||||
Log.d(TAG, "Migration completed successfully")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Error during migration", e)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun populateDefaultCategories(dao: BookkeepingDao) {
|
private val MIGRATION_2_3 = object : Migration(2, 3) {
|
||||||
try {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
Log.d(TAG, "Starting to populate default categories")
|
// 重新创建记账记录表
|
||||||
// 支出类别
|
database.execSQL("""
|
||||||
listOf(
|
CREATE TABLE IF NOT EXISTS bookkeeping_records_new (
|
||||||
"餐饮",
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
"交通",
|
amount REAL NOT NULL,
|
||||||
"购物",
|
type TEXT NOT NULL,
|
||||||
"娱乐",
|
category TEXT NOT NULL,
|
||||||
"医疗",
|
description TEXT NOT NULL,
|
||||||
"住房",
|
date INTEGER NOT NULL,
|
||||||
"其他支出"
|
memberId INTEGER,
|
||||||
).forEach { name ->
|
FOREIGN KEY(memberId) REFERENCES members(id) ON DELETE SET NULL
|
||||||
try {
|
)
|
||||||
dao.insertCategory(Category(name = name, type = TransactionType.EXPENSE))
|
""")
|
||||||
Log.d(TAG, "Added expense category: $name")
|
|
||||||
} catch (e: Exception) {
|
// 复制数据
|
||||||
Log.e(TAG, "Error adding expense category: $name", e)
|
database.execSQL("""
|
||||||
}
|
INSERT INTO bookkeeping_records_new (id, amount, type, category, description, date, memberId)
|
||||||
}
|
SELECT id, amount, type, category, description, date, memberId FROM bookkeeping_records
|
||||||
|
""")
|
||||||
// 收入类别
|
|
||||||
listOf(
|
// 删除旧表
|
||||||
"工资",
|
database.execSQL("DROP TABLE bookkeeping_records")
|
||||||
"奖金",
|
|
||||||
"投资",
|
// 重命名新表
|
||||||
"其他收入"
|
database.execSQL("ALTER TABLE bookkeeping_records_new RENAME TO bookkeeping_records")
|
||||||
).forEach { name ->
|
|
||||||
try {
|
// 重新创建分类表
|
||||||
dao.insertCategory(Category(name = name, type = TransactionType.INCOME))
|
database.execSQL("""
|
||||||
Log.d(TAG, "Added income category: $name")
|
CREATE TABLE IF NOT EXISTS categories_new (
|
||||||
} catch (e: Exception) {
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
Log.e(TAG, "Error adding income category: $name", e)
|
name TEXT NOT NULL,
|
||||||
}
|
type TEXT NOT NULL
|
||||||
}
|
)
|
||||||
Log.d(TAG, "Finished populating default categories")
|
""")
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Error during category population", e)
|
// 复制分类数据
|
||||||
|
database.execSQL("""
|
||||||
|
INSERT INTO categories_new (id, name, type)
|
||||||
|
SELECT id, name, type FROM categories
|
||||||
|
""")
|
||||||
|
|
||||||
|
// 删除旧表
|
||||||
|
database.execSQL("DROP TABLE categories")
|
||||||
|
|
||||||
|
// 重命名新表
|
||||||
|
database.execSQL("ALTER TABLE categories_new RENAME TO categories")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var INSTANCE: BookkeepingDatabase? = null
|
||||||
|
|
||||||
fun getDatabase(context: Context): BookkeepingDatabase {
|
fun getDatabase(context: Context): BookkeepingDatabase {
|
||||||
return Instance ?: synchronized(this) {
|
return INSTANCE ?: synchronized(this) {
|
||||||
try {
|
val instance = Room.databaseBuilder(
|
||||||
Log.d(TAG, "Creating new database instance")
|
context.applicationContext,
|
||||||
val instance = Room.databaseBuilder(
|
BookkeepingDatabase::class.java,
|
||||||
context.applicationContext,
|
"bookkeeping_database"
|
||||||
BookkeepingDatabase::class.java,
|
)
|
||||||
"bookkeeping_database"
|
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
|
||||||
)
|
|
||||||
.addCallback(object : Callback() {
|
.addCallback(object : Callback() {
|
||||||
override fun onCreate(db: SupportSQLiteDatabase) {
|
override fun onCreate(db: SupportSQLiteDatabase) {
|
||||||
super.onCreate(db)
|
super.onCreate(db)
|
||||||
Log.d(TAG, "Database created, initializing default categories")
|
Log.d(TAG, "Database created, initializing default data")
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
Instance?.let { database ->
|
val database = getDatabase(context)
|
||||||
populateDefaultCategories(database.bookkeepingDao())
|
|
||||||
|
// 初始化默认成员
|
||||||
|
database.memberDao().apply {
|
||||||
|
if (getMemberCount() == 0) {
|
||||||
|
insertMember(Member(name = "自己", description = "默认成员"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化默认分类
|
||||||
|
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.INCOME))
|
||||||
|
insertCategory(Category(name = "奖金", type = TransactionType.INCOME))
|
||||||
|
insertCategory(Category(name = "投资", type = TransactionType.INCOME))
|
||||||
|
insertCategory(Category(name = "其他收入", type = TransactionType.INCOME))
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Default data initialized successfully")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error in onCreate callback", e)
|
Log.e(TAG, "Error initializing default data", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.addMigrations(MIGRATION_1_2)
|
|
||||||
.fallbackToDestructiveMigration() // 如果迁移失败,允许重建数据库
|
|
||||||
.build()
|
.build()
|
||||||
|
INSTANCE = instance
|
||||||
Instance = instance
|
instance
|
||||||
Log.d(TAG, "Database instance created successfully")
|
|
||||||
instance
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Error creating database", e)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
package com.yovinchen.bookkeeping.data
|
||||||
|
|
||||||
|
import androidx.room.*
|
||||||
|
import com.yovinchen.bookkeeping.model.Category
|
||||||
|
import com.yovinchen.bookkeeping.model.TransactionType
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface CategoryDao {
|
||||||
|
@Query("SELECT * FROM categories WHERE type = :type ORDER BY name ASC")
|
||||||
|
fun getCategoriesByType(type: TransactionType): Flow<List<Category>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM categories ORDER BY type ASC, name ASC")
|
||||||
|
fun getAllCategories(): Flow<List<Category>>
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
suspend fun insertCategory(category: Category): Long
|
||||||
|
|
||||||
|
@Update
|
||||||
|
suspend fun updateCategory(category: Category)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun deleteCategory(category: Category)
|
||||||
|
|
||||||
|
@Query("SELECT EXISTS(SELECT 1 FROM bookkeeping_records WHERE category = :categoryName LIMIT 1)")
|
||||||
|
suspend fun isCategoryInUse(categoryName: String): Boolean
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM categories WHERE type = :type")
|
||||||
|
suspend fun getCategoryCountByType(type: TransactionType): Int
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package com.yovinchen.bookkeeping.data
|
||||||
|
|
||||||
|
import androidx.room.*
|
||||||
|
import com.yovinchen.bookkeeping.model.Member
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface MemberDao {
|
||||||
|
@Query("SELECT * FROM members ORDER BY name ASC")
|
||||||
|
fun getAllMembers(): Flow<List<Member>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM members WHERE id = :memberId")
|
||||||
|
suspend fun getMemberById(memberId: Int): Member?
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
suspend fun insertMember(member: Member): Long
|
||||||
|
|
||||||
|
@Update
|
||||||
|
suspend fun updateMember(member: Member)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun deleteMember(member: Member)
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM members")
|
||||||
|
suspend fun getMemberCount(): Int
|
||||||
|
}
|
@ -1,9 +1,11 @@
|
|||||||
package com.yovinchen.bookkeeping.model
|
package com.yovinchen.bookkeeping.model
|
||||||
|
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
|
import com.yovinchen.bookkeeping.model.Member
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
enum class TransactionType {
|
enum class TransactionType {
|
||||||
@ -32,7 +34,17 @@ class Converters {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity(tableName = "bookkeeping_records")
|
@Entity(
|
||||||
|
tableName = "bookkeeping_records",
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = Member::class,
|
||||||
|
parentColumns = ["id"],
|
||||||
|
childColumns = ["memberId"],
|
||||||
|
onDelete = ForeignKey.SET_NULL
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
data class BookkeepingRecord(
|
data class BookkeepingRecord(
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey(autoGenerate = true)
|
||||||
@ -41,5 +53,6 @@ data class BookkeepingRecord(
|
|||||||
val type: TransactionType,
|
val type: TransactionType,
|
||||||
val category: String,
|
val category: String,
|
||||||
val description: String,
|
val description: String,
|
||||||
val date: Date
|
val date: Date,
|
||||||
|
val memberId: Int? = null // 可为空,表示未指定成员
|
||||||
)
|
)
|
||||||
|
@ -0,0 +1,250 @@
|
|||||||
|
package com.yovinchen.bookkeeping.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
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.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import com.yovinchen.bookkeeping.model.TransactionType
|
||||||
|
import java.time.YearMonth
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MonthYearPickerDialog(
|
||||||
|
selectedMonth: YearMonth,
|
||||||
|
onMonthSelected: (YearMonth) -> Unit,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
var currentYearMonth by remember { mutableStateOf(selectedMonth) }
|
||||||
|
|
||||||
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight(),
|
||||||
|
shape = MaterialTheme.shapes.extraLarge,
|
||||||
|
tonalElevation = 6.dp
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "选择年月",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 年份选择
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
IconButton(onClick = {
|
||||||
|
currentYearMonth = currentYearMonth.minusYears(1)
|
||||||
|
}) {
|
||||||
|
Text("<")
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = "${currentYearMonth.year}年",
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
IconButton(onClick = {
|
||||||
|
currentYearMonth = currentYearMonth.plusYears(1)
|
||||||
|
}) {
|
||||||
|
Text(">")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// 月份网格
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(3),
|
||||||
|
modifier = Modifier.height(200.dp)
|
||||||
|
) {
|
||||||
|
items(12) { index ->
|
||||||
|
val month = index + 1
|
||||||
|
val isSelected = month == currentYearMonth.monthValue
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(4.dp)
|
||||||
|
.aspectRatio(1.5f)
|
||||||
|
.clickable {
|
||||||
|
currentYearMonth = YearMonth.of(currentYearMonth.year, month)
|
||||||
|
},
|
||||||
|
shape = MaterialTheme.shapes.small,
|
||||||
|
color = if (isSelected) MaterialTheme.colorScheme.primary
|
||||||
|
else MaterialTheme.colorScheme.surface
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${month}月",
|
||||||
|
color = if (isSelected) MaterialTheme.colorScheme.onPrimary
|
||||||
|
else MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按钮行
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text("取消")
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Button(onClick = {
|
||||||
|
onMonthSelected(currentYearMonth)
|
||||||
|
onDismiss()
|
||||||
|
}) {
|
||||||
|
Text("确定")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MonthlyStatistics(
|
||||||
|
totalIncome: Double,
|
||||||
|
totalExpense: Double,
|
||||||
|
onIncomeClick: () -> Unit,
|
||||||
|
onExpenseClick: () -> Unit,
|
||||||
|
selectedType: TransactionType?,
|
||||||
|
onClearFilter: () -> Unit,
|
||||||
|
selectedMonth: YearMonth,
|
||||||
|
onPreviousMonth: () -> Unit,
|
||||||
|
onNextMonth: () -> Unit,
|
||||||
|
onMonthSelected: (YearMonth) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
var showMonthPicker by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
// 月份选择器
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
IconButton(onClick = onPreviousMonth) {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.KeyboardArrowLeft, "上个月")
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "${selectedMonth.year}年${selectedMonth.monthValue}月",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
modifier = Modifier.clickable { showMonthPicker = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
IconButton(onClick = onNextMonth) {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, "下个月")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
// 收入统计
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.clickable { onIncomeClick() }
|
||||||
|
.background(
|
||||||
|
if (selectedType == TransactionType.INCOME) MaterialTheme.colorScheme.primaryContainer
|
||||||
|
else Color.Transparent,
|
||||||
|
RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "收入",
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "¥${String.format("%.2f", totalIncome)}",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
|
// 支出统计
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.clickable { onExpenseClick() }
|
||||||
|
.background(
|
||||||
|
if (selectedType == TransactionType.EXPENSE) MaterialTheme.colorScheme.primaryContainer
|
||||||
|
else Color.Transparent,
|
||||||
|
RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "支出",
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "¥${String.format("%.2f", totalExpense)}",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedType != null) {
|
||||||
|
TextButton(
|
||||||
|
onClick = onClearFilter,
|
||||||
|
modifier = Modifier.align(Alignment.End)
|
||||||
|
) {
|
||||||
|
Text("清除筛选")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showMonthPicker) {
|
||||||
|
MonthYearPickerDialog(
|
||||||
|
selectedMonth = selectedMonth,
|
||||||
|
onMonthSelected = onMonthSelected,
|
||||||
|
onDismiss = { showMonthPicker = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
package com.yovinchen.bookkeeping.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
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.BookkeepingRecord
|
||||||
|
import com.yovinchen.bookkeeping.model.Member
|
||||||
|
import com.yovinchen.bookkeeping.model.TransactionType
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RecordItem(
|
||||||
|
record: BookkeepingRecord,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
onDelete: () -> Unit = {},
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
members: List<Member> = emptyList()
|
||||||
|
) {
|
||||||
|
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||||
|
val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) }
|
||||||
|
val member = members.find { it.id == record.memberId }
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||||
|
.clickable(onClick = onClick),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
// 第一行:分类
|
||||||
|
Text(
|
||||||
|
text = record.category,
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
|
||||||
|
// 第二行:时间 | 成员 | 详情
|
||||||
|
Text(
|
||||||
|
text = buildString {
|
||||||
|
append(timeFormat.format(record.date))
|
||||||
|
if (member != null && member.name != "自己") {
|
||||||
|
append(" | ")
|
||||||
|
append(member.name)
|
||||||
|
}
|
||||||
|
if (record.description.isNotEmpty()) {
|
||||||
|
append(" | ")
|
||||||
|
append(record.description)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 金额显示
|
||||||
|
Text(
|
||||||
|
text = String.format("%.2f", record.amount),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = if (record.type == TransactionType.EXPENSE)
|
||||||
|
MaterialTheme.colorScheme.error
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDeleteDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showDeleteDialog = false },
|
||||||
|
title = { Text("确认删除") },
|
||||||
|
text = { Text("确定要删除这条记录吗?") },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onDelete()
|
||||||
|
showDeleteDialog = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("删除")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showDeleteDialog = false }) {
|
||||||
|
Text("取消")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,36 +1,57 @@
|
|||||||
|
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
||||||
package com.yovinchen.bookkeeping.ui.dialog
|
package com.yovinchen.bookkeeping.ui.dialog
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import com.yovinchen.bookkeeping.model.Category
|
import com.yovinchen.bookkeeping.model.Category
|
||||||
|
import com.yovinchen.bookkeeping.model.Member
|
||||||
import com.yovinchen.bookkeeping.model.TransactionType
|
import com.yovinchen.bookkeeping.model.TransactionType
|
||||||
import com.yovinchen.bookkeeping.ui.components.DateTimePicker
|
import com.yovinchen.bookkeeping.ui.components.DateTimePicker
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AddRecordDialog(
|
fun AddRecordDialog(
|
||||||
onDismiss: () -> Unit,
|
|
||||||
onConfirm: (TransactionType, Double, String, String) -> Unit,
|
|
||||||
categories: List<Category>,
|
categories: List<Category>,
|
||||||
selectedType: TransactionType,
|
members: List<Member>,
|
||||||
onTypeChange: (TransactionType) -> Unit,
|
onDismiss: () -> Unit,
|
||||||
selectedDateTime: LocalDateTime,
|
onConfirm: (amount: Double, category: String, description: String, date: Date, type: TransactionType, memberId: Int?) -> Unit
|
||||||
onDateTimeSelected: (LocalDateTime) -> Unit
|
|
||||||
) {
|
) {
|
||||||
var amount by remember { mutableStateOf("") }
|
var amount by remember { mutableStateOf("") }
|
||||||
var selectedCategory by remember { mutableStateOf<Category?>(null) }
|
|
||||||
var description by remember { mutableStateOf("") }
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
var memberExpanded by remember { mutableStateOf(false) }
|
||||||
|
var description by remember { mutableStateOf("") }
|
||||||
|
var selectedType by remember { mutableStateOf(TransactionType.EXPENSE) }
|
||||||
|
|
||||||
|
// 找到默认成员("自己")
|
||||||
|
val defaultMember = remember(members) {
|
||||||
|
members.find { it.name == "自己" }
|
||||||
|
}
|
||||||
|
var currentSelectedMember by remember(defaultMember) {
|
||||||
|
mutableStateOf(defaultMember)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置默认分类为"餐饮"
|
||||||
|
var selectedCategory by remember {
|
||||||
|
mutableStateOf(categories.find { it.type == selectedType && it.name == "餐饮" }?.name ?: categories.firstOrNull { it.type == selectedType }?.name ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedDateTime by remember {
|
||||||
|
mutableStateOf(LocalDateTime.now())
|
||||||
|
}
|
||||||
|
|
||||||
// 根据当前选择的类型过滤类别
|
// 当类型改变时更新分类
|
||||||
val filteredCategories = categories.filter { it.type == selectedType }
|
LaunchedEffect(selectedType) {
|
||||||
|
selectedCategory = categories.find { it.type == selectedType && it.name == "餐饮" }?.name
|
||||||
|
?: categories.firstOrNull { it.type == selectedType }?.name
|
||||||
|
?: ""
|
||||||
|
}
|
||||||
|
|
||||||
Dialog(onDismissRequest = onDismiss) {
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
Card(
|
Card(
|
||||||
@ -51,74 +72,59 @@ fun AddRecordDialog(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// 类型选择
|
// 收入/支出选择
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
) {
|
) {
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = selectedType == TransactionType.EXPENSE,
|
selected = selectedType == TransactionType.EXPENSE,
|
||||||
onClick = {
|
onClick = { selectedType = TransactionType.EXPENSE },
|
||||||
onTypeChange(TransactionType.EXPENSE)
|
|
||||||
selectedCategory = null
|
|
||||||
},
|
|
||||||
label = { Text("支出") }
|
label = { Text("支出") }
|
||||||
)
|
)
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = selectedType == TransactionType.INCOME,
|
selected = selectedType == TransactionType.INCOME,
|
||||||
onClick = {
|
onClick = { selectedType = TransactionType.INCOME },
|
||||||
onTypeChange(TransactionType.INCOME)
|
|
||||||
selectedCategory = null
|
|
||||||
},
|
|
||||||
label = { Text("收入") }
|
label = { Text("收入") }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// 日期时间选择
|
|
||||||
DateTimePicker(
|
|
||||||
selectedDateTime = selectedDateTime,
|
|
||||||
onDateTimeSelected = onDateTimeSelected,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
// 金额输入
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = amount,
|
value = amount,
|
||||||
onValueChange = { amount = it },
|
onValueChange = { amount = it },
|
||||||
label = { Text("金额") },
|
label = { Text("金额") },
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
modifier = Modifier.fillMaxWidth()
|
singleLine = true
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// 类别选择
|
|
||||||
ExposedDropdownMenuBox(
|
ExposedDropdownMenuBox(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onExpandedChange = { expanded = it }
|
onExpandedChange = { expanded = it }
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = selectedCategory?.name ?: "",
|
value = selectedCategory,
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
label = { Text("类别") },
|
label = { Text("类别") },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.menuAnchor()
|
.menuAnchor()
|
||||||
)
|
)
|
||||||
|
|
||||||
ExposedDropdownMenu(
|
ExposedDropdownMenu(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onDismissRequest = { expanded = false }
|
onDismissRequest = { expanded = false }
|
||||||
) {
|
) {
|
||||||
filteredCategories.forEach { category ->
|
categories.filter { it.type == selectedType }.forEach { category ->
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(category.name) },
|
text = { Text(category.name) },
|
||||||
onClick = {
|
onClick = {
|
||||||
selectedCategory = category
|
selectedCategory = category.name
|
||||||
expanded = false
|
expanded = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -126,19 +132,59 @@ fun AddRecordDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// 描述输入
|
ExposedDropdownMenuBox(
|
||||||
OutlinedTextField(
|
expanded = memberExpanded,
|
||||||
value = description,
|
onExpandedChange = { memberExpanded = it }
|
||||||
onValueChange = { description = it },
|
) {
|
||||||
label = { Text("描述") },
|
OutlinedTextField(
|
||||||
|
value = currentSelectedMember?.name ?: "选择成员",
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text("成员") },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = memberExpanded) },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.menuAnchor()
|
||||||
|
)
|
||||||
|
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = memberExpanded,
|
||||||
|
onDismissRequest = { memberExpanded = false }
|
||||||
|
) {
|
||||||
|
members.forEach { member ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(member.name) },
|
||||||
|
onClick = {
|
||||||
|
currentSelectedMember = member
|
||||||
|
memberExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
DateTimePicker(
|
||||||
|
selectedDateTime = selectedDateTime,
|
||||||
|
onDateTimeSelected = { selectedDateTime = it },
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// 按钮
|
OutlinedTextField(
|
||||||
|
value = description,
|
||||||
|
onValueChange = { description = it },
|
||||||
|
label = { Text("备注") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.End
|
horizontalArrangement = Arrangement.End
|
||||||
@ -149,13 +195,21 @@ fun AddRecordDialog(
|
|||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
val amountValue = amount.toDoubleOrNull() ?: 0.0
|
val amountValue = amount.toDoubleOrNull()
|
||||||
selectedCategory?.let { category ->
|
if (amountValue != null) {
|
||||||
onConfirm(selectedType, amountValue, category.name, description)
|
onConfirm(
|
||||||
onDismiss()
|
amountValue,
|
||||||
|
selectedCategory,
|
||||||
|
description,
|
||||||
|
Date.from(
|
||||||
|
selectedDateTime.atZone(ZoneId.systemDefault()).toInstant()
|
||||||
|
),
|
||||||
|
selectedType,
|
||||||
|
currentSelectedMember?.id
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled = amount.isNotEmpty() && selectedCategory != null
|
enabled = amount.isNotEmpty() && selectedCategory.isNotEmpty()
|
||||||
) {
|
) {
|
||||||
Text("确定")
|
Text("确定")
|
||||||
}
|
}
|
||||||
|
@ -8,25 +8,33 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
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.Member
|
||||||
import com.yovinchen.bookkeeping.ui.components.DateTimePicker
|
import com.yovinchen.bookkeeping.ui.components.DateTimePicker
|
||||||
|
import com.yovinchen.bookkeeping.viewmodel.HomeViewModel
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RecordEditDialog(
|
fun RecordEditDialog(
|
||||||
record: BookkeepingRecord,
|
record: BookkeepingRecord,
|
||||||
categories: List<Category>,
|
categories: List<Category>,
|
||||||
|
members: List<Member>,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onConfirm: (BookkeepingRecord) -> Unit
|
onConfirm: (BookkeepingRecord) -> Unit,
|
||||||
|
viewModel: HomeViewModel = viewModel()
|
||||||
) {
|
) {
|
||||||
var amount by remember { mutableStateOf(record.amount.toString()) }
|
var amount by remember { mutableStateOf(record.amount.toString()) }
|
||||||
var selectedCategory by remember { mutableStateOf(record.category) }
|
var selectedCategory by remember { mutableStateOf(record.category) }
|
||||||
var description by remember { mutableStateOf(record.description) }
|
var description by remember { mutableStateOf(record.description) }
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
var memberExpanded by remember { mutableStateOf(false) }
|
||||||
|
var currentSelectedMember by remember { mutableStateOf<Member?>(null) }
|
||||||
var selectedDateTime by remember {
|
var selectedDateTime by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
LocalDateTime.ofInstant(
|
LocalDateTime.ofInstant(
|
||||||
@ -36,6 +44,16 @@ fun RecordEditDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载原关联成员
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
LaunchedEffect(record.memberId) {
|
||||||
|
if (record.memberId != null) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
currentSelectedMember = viewModel.getMemberById(record.memberId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Dialog(onDismissRequest = onDismiss) {
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -55,24 +73,16 @@ fun RecordEditDialog(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// 日期时间选择
|
|
||||||
DateTimePicker(
|
|
||||||
selectedDateTime = selectedDateTime,
|
|
||||||
onDateTimeSelected = { selectedDateTime = it },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
// 金额输入
|
// 金额输入
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = amount,
|
value = amount,
|
||||||
onValueChange = { amount = it },
|
onValueChange = { amount = it },
|
||||||
label = { Text("金额") },
|
label = { Text("金额") },
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// 类别选择
|
// 类别选择
|
||||||
ExposedDropdownMenuBox(
|
ExposedDropdownMenuBox(
|
||||||
@ -84,10 +94,12 @@ fun RecordEditDialog(
|
|||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
label = { Text("类别") },
|
label = { Text("类别") },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.menuAnchor()
|
.menuAnchor()
|
||||||
)
|
)
|
||||||
|
|
||||||
ExposedDropdownMenu(
|
ExposedDropdownMenu(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onDismissRequest = { expanded = false }
|
onDismissRequest = { expanded = false }
|
||||||
@ -104,19 +116,72 @@ fun RecordEditDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// 描述输入
|
// 成员选择
|
||||||
OutlinedTextField(
|
ExposedDropdownMenuBox(
|
||||||
value = description,
|
expanded = memberExpanded,
|
||||||
onValueChange = { description = it },
|
onExpandedChange = { memberExpanded = it }
|
||||||
label = { Text("描述") },
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = currentSelectedMember?.name ?: "选择成员",
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text("成员") },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = memberExpanded) },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.menuAnchor()
|
||||||
|
)
|
||||||
|
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = memberExpanded,
|
||||||
|
onDismissRequest = { memberExpanded = false }
|
||||||
|
) {
|
||||||
|
// 添加一个"清除选择"选项
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("清除选择") },
|
||||||
|
onClick = {
|
||||||
|
currentSelectedMember = null
|
||||||
|
memberExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
members.forEach { member ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(member.name) },
|
||||||
|
onClick = {
|
||||||
|
currentSelectedMember = member
|
||||||
|
memberExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// 日期时间选择
|
||||||
|
DateTimePicker(
|
||||||
|
selectedDateTime = selectedDateTime,
|
||||||
|
onDateTimeSelected = { selectedDateTime = it },
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// 按钮
|
// 备注输入
|
||||||
|
OutlinedTextField(
|
||||||
|
value = description,
|
||||||
|
onValueChange = { description = it },
|
||||||
|
label = { Text("备注") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// 按钮行
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.End
|
horizontalArrangement = Arrangement.End
|
||||||
@ -127,15 +192,22 @@ fun RecordEditDialog(
|
|||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
val updatedRecord = record.copy(
|
val amountValue = amount.toDoubleOrNull()
|
||||||
amount = amount.toDoubleOrNull() ?: record.amount,
|
if (amountValue != null) {
|
||||||
category = selectedCategory,
|
onConfirm(
|
||||||
description = description,
|
record.copy(
|
||||||
date = Date.from(selectedDateTime.atZone(ZoneId.systemDefault()).toInstant())
|
amount = amountValue,
|
||||||
)
|
category = selectedCategory,
|
||||||
onConfirm(updatedRecord)
|
description = description,
|
||||||
onDismiss()
|
date = Date.from(
|
||||||
}
|
selectedDateTime.atZone(ZoneId.systemDefault()).toInstant()
|
||||||
|
),
|
||||||
|
memberId = currentSelectedMember?.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = amount.isNotEmpty()
|
||||||
) {
|
) {
|
||||||
Text("确定")
|
Text("确定")
|
||||||
}
|
}
|
||||||
|
@ -1,60 +1,73 @@
|
|||||||
package com.yovinchen.bookkeeping.ui.screen
|
package com.yovinchen.bookkeeping.ui.screen
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
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.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.yovinchen.bookkeeping.model.BookkeepingRecord
|
import com.yovinchen.bookkeeping.model.BookkeepingRecord
|
||||||
import com.yovinchen.bookkeeping.model.TransactionType
|
import com.yovinchen.bookkeeping.model.TransactionType
|
||||||
|
import com.yovinchen.bookkeeping.ui.components.MonthlyStatistics
|
||||||
|
import com.yovinchen.bookkeeping.ui.components.RecordItem
|
||||||
import com.yovinchen.bookkeeping.ui.dialog.AddRecordDialog
|
import com.yovinchen.bookkeeping.ui.dialog.AddRecordDialog
|
||||||
import com.yovinchen.bookkeeping.ui.dialog.RecordEditDialog
|
import com.yovinchen.bookkeeping.ui.dialog.RecordEditDialog
|
||||||
import com.yovinchen.bookkeeping.viewmodel.HomeViewModel
|
import com.yovinchen.bookkeeping.viewmodel.HomeViewModel
|
||||||
import java.time.YearMonth
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeScreen(
|
fun HomeScreen(
|
||||||
modifier: Modifier = Modifier, viewModel: HomeViewModel = viewModel()
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: HomeViewModel = viewModel()
|
||||||
) {
|
) {
|
||||||
val filteredRecords by viewModel.filteredRecords.collectAsState()
|
|
||||||
val totalIncome by viewModel.totalIncome.collectAsState()
|
|
||||||
val totalExpense by viewModel.totalExpense.collectAsState()
|
|
||||||
val categories by viewModel.categories.collectAsState()
|
|
||||||
val selectedRecordType by viewModel.selectedRecordType.collectAsState()
|
|
||||||
val selectedMonth by viewModel.selectedMonth.collectAsState()
|
|
||||||
|
|
||||||
var showAddDialog by remember { mutableStateOf(false) }
|
var showAddDialog by remember { mutableStateOf(false) }
|
||||||
var selectedRecord by remember { mutableStateOf<BookkeepingRecord?>(null) }
|
var selectedRecord by remember { mutableStateOf<BookkeepingRecord?>(null) }
|
||||||
|
|
||||||
|
val selectedMonth by viewModel.selectedMonth.collectAsState()
|
||||||
|
val filteredRecords by viewModel.filteredRecords.collectAsState()
|
||||||
|
val categories by viewModel.categories.collectAsState(initial = emptyList())
|
||||||
|
val members by viewModel.members.collectAsState(initial = emptyList())
|
||||||
|
val selectedMember by viewModel.selectedMember.collectAsState()
|
||||||
|
val totalIncome by viewModel.totalIncome.collectAsState()
|
||||||
|
val totalExpense by viewModel.totalExpense.collectAsState()
|
||||||
|
|
||||||
Scaffold(modifier = modifier.fillMaxSize(), floatingActionButton = {
|
Scaffold(
|
||||||
FloatingActionButton(onClick = { showAddDialog = true }) {
|
modifier = modifier.fillMaxSize(),
|
||||||
Icon(Icons.Default.Add, contentDescription = "添加记录")
|
floatingActionButton = {
|
||||||
|
ExtendedFloatingActionButton(
|
||||||
|
onClick = { showAddDialog = true },
|
||||||
|
icon = { Icon(Icons.Default.Add, contentDescription = null) },
|
||||||
|
text = { Text("记一笔") }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}, floatingActionButtonPosition = FabPosition.End, topBar = {
|
) { padding ->
|
||||||
TopAppBar(title = { Text("记账本") })
|
|
||||||
}) { padding ->
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@ -62,408 +75,103 @@ fun HomeScreen(
|
|||||||
.background(MaterialTheme.colorScheme.background)
|
.background(MaterialTheme.colorScheme.background)
|
||||||
) {
|
) {
|
||||||
// 顶部统计信息
|
// 顶部统计信息
|
||||||
MonthlyStatistics(totalIncome = totalIncome,
|
MonthlyStatistics(
|
||||||
|
totalIncome = totalIncome,
|
||||||
totalExpense = totalExpense,
|
totalExpense = totalExpense,
|
||||||
|
selectedType = null,
|
||||||
onIncomeClick = { viewModel.setSelectedRecordType(TransactionType.INCOME) },
|
onIncomeClick = { viewModel.setSelectedRecordType(TransactionType.INCOME) },
|
||||||
onExpenseClick = { viewModel.setSelectedRecordType(TransactionType.EXPENSE) },
|
onExpenseClick = { viewModel.setSelectedRecordType(TransactionType.EXPENSE) },
|
||||||
selectedType = selectedRecordType,
|
|
||||||
onClearFilter = { viewModel.setSelectedRecordType(null) },
|
onClearFilter = { viewModel.setSelectedRecordType(null) },
|
||||||
selectedMonth = selectedMonth,
|
selectedMonth = selectedMonth,
|
||||||
onPreviousMonth = { viewModel.setSelectedMonth(selectedMonth.minusMonths(1)) },
|
onPreviousMonth = { viewModel.moveMonth(false) },
|
||||||
onNextMonth = { viewModel.setSelectedMonth(selectedMonth.plusMonths(1)) },
|
onNextMonth = { viewModel.moveMonth(true) },
|
||||||
onMonthSelected = { viewModel.setSelectedMonth(it) })
|
onMonthSelected = { viewModel.setSelectedMonth(it) }
|
||||||
|
)
|
||||||
|
|
||||||
// 记录列表
|
// 记录列表
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
filteredRecords.forEach { (date, records) ->
|
items(filteredRecords.size) { index ->
|
||||||
item {
|
val (date, dayRecords) = filteredRecords.toList()[index]
|
||||||
Surface(
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 4.dp),
|
.padding(16.dp)
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f),
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
tonalElevation = 2.dp
|
|
||||||
) {
|
) {
|
||||||
|
// 日期标签
|
||||||
|
Text(
|
||||||
|
text = SimpleDateFormat(
|
||||||
|
"yyyy年MM月dd日 E",
|
||||||
|
Locale.CHINESE
|
||||||
|
).format(date),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// 当天的记录
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
// 日期标签
|
dayRecords.forEachIndexed { recordIndex, record ->
|
||||||
Text(
|
RecordItem(
|
||||||
text = SimpleDateFormat(
|
record = record,
|
||||||
"yyyy年MM月dd日 E", Locale.CHINESE
|
|
||||||
).format(date),
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
// 当天的记录
|
|
||||||
records.forEachIndexed { index, record ->
|
|
||||||
RecordItem(record = record,
|
|
||||||
onClick = { selectedRecord = record },
|
onClick = { selectedRecord = record },
|
||||||
onDelete = { viewModel.deleteRecord(record) })
|
onDelete = { viewModel.deleteRecord(record) },
|
||||||
|
members = members
|
||||||
|
)
|
||||||
|
|
||||||
if (index < records.size - 1) {
|
if (recordIndex < dayRecords.size - 1) {
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
modifier = Modifier.padding(vertical = 8.dp),
|
modifier = Modifier.padding(vertical = 4.dp),
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
thickness = 0.5.dp
|
thickness = 0.5.dp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
// 当天统计
|
|
||||||
HorizontalDivider(
|
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
|
||||||
thickness = 0.5.dp
|
|
||||||
)
|
|
||||||
|
|
||||||
val dayIncome = records.filter { it.type == TransactionType.INCOME }
|
|
||||||
.sumOf { it.amount }
|
|
||||||
val dayExpense =
|
|
||||||
records.filter { it.type == TransactionType.EXPENSE }
|
|
||||||
.sumOf { it.amount }
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 12.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "收入: ¥%.2f".format(dayIncome),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "支出: ¥%.2f".format(dayExpense),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 添加记录对话框
|
// 添加记录对话框
|
||||||
if (showAddDialog) {
|
if (showAddDialog) {
|
||||||
val selectedDateTime by viewModel.selectedDateTime.collectAsState()
|
AddRecordDialog(
|
||||||
val selectedCategoryType by viewModel.selectedCategoryType.collectAsState()
|
categories = categories,
|
||||||
AddRecordDialog(onDismiss = {
|
members = members,
|
||||||
|
onDismiss = { showAddDialog = false },
|
||||||
|
onConfirm = { amount, category, description, date, type, memberId ->
|
||||||
|
viewModel.addRecord(amount, category, description, date, type, memberId)
|
||||||
showAddDialog = false
|
showAddDialog = false
|
||||||
viewModel.resetSelectedDateTime()
|
}
|
||||||
},
|
)
|
||||||
onConfirm = { type, amount, category, description ->
|
}
|
||||||
viewModel.addRecord(type, amount, category, description)
|
|
||||||
showAddDialog = false
|
|
||||||
},
|
|
||||||
categories = categories,
|
|
||||||
selectedType = selectedCategoryType,
|
|
||||||
onTypeChange = viewModel::setSelectedCategoryType,
|
|
||||||
selectedDateTime = selectedDateTime,
|
|
||||||
onDateTimeSelected = viewModel::setSelectedDateTime
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 编辑记录对话框
|
// 编辑记录对话框
|
||||||
selectedRecord?.let { record ->
|
selectedRecord?.let { record ->
|
||||||
RecordEditDialog(record = record,
|
RecordEditDialog(
|
||||||
categories = categories,
|
record = record,
|
||||||
onDismiss = { selectedRecord = null },
|
categories = categories,
|
||||||
onConfirm = { updatedRecord ->
|
members = members,
|
||||||
viewModel.updateRecord(updatedRecord)
|
onDismiss = { selectedRecord = null },
|
||||||
selectedRecord = null
|
onConfirm = { updatedRecord ->
|
||||||
})
|
viewModel.updateRecord(updatedRecord)
|
||||||
}
|
selectedRecord = null
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MonthYearPickerDialog(
|
|
||||||
selectedMonth: YearMonth, onMonthSelected: (YearMonth) -> Unit, onDismiss: () -> Unit
|
|
||||||
) {
|
|
||||||
var currentYearMonth by remember { mutableStateOf(selectedMonth) }
|
|
||||||
|
|
||||||
Dialog(onDismissRequest = onDismiss) {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.wrapContentHeight(),
|
|
||||||
shape = MaterialTheme.shapes.extraLarge,
|
|
||||||
tonalElevation = 6.dp
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "选择年月",
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
modifier = Modifier.padding(bottom = 16.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
// 年份选择
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
IconButton(onClick = {
|
|
||||||
currentYearMonth = currentYearMonth.minusYears(1)
|
|
||||||
}) {
|
|
||||||
Text("<")
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
text = "${currentYearMonth.year}年",
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
IconButton(onClick = {
|
|
||||||
currentYearMonth = currentYearMonth.plusYears(1)
|
|
||||||
}) {
|
|
||||||
Text(">")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
// 月份网格
|
|
||||||
LazyVerticalGrid(
|
|
||||||
columns = GridCells.Fixed(3), modifier = Modifier.height(200.dp)
|
|
||||||
) {
|
|
||||||
items(12) { index ->
|
|
||||||
val month = index + 1
|
|
||||||
val isSelected = month == currentYearMonth.monthValue
|
|
||||||
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(4.dp)
|
|
||||||
.aspectRatio(1.5f)
|
|
||||||
.clickable {
|
|
||||||
currentYearMonth = YearMonth.of(currentYearMonth.year, month)
|
|
||||||
},
|
|
||||||
shape = MaterialTheme.shapes.small,
|
|
||||||
color = if (isSelected) MaterialTheme.colorScheme.primary
|
|
||||||
else MaterialTheme.colorScheme.surface
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "${month}月",
|
|
||||||
color = if (isSelected) MaterialTheme.colorScheme.onPrimary
|
|
||||||
else MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按钮行
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 16.dp),
|
|
||||||
horizontalArrangement = Arrangement.End
|
|
||||||
) {
|
|
||||||
TextButton(onClick = onDismiss) {
|
|
||||||
Text("取消")
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Button(onClick = {
|
|
||||||
onMonthSelected(currentYearMonth)
|
|
||||||
onDismiss()
|
|
||||||
}) {
|
|
||||||
Text("确定")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MonthlyStatistics(
|
|
||||||
totalIncome: Double,
|
|
||||||
totalExpense: Double,
|
|
||||||
onIncomeClick: () -> Unit,
|
|
||||||
onExpenseClick: () -> Unit,
|
|
||||||
selectedType: TransactionType?,
|
|
||||||
onClearFilter: () -> Unit,
|
|
||||||
selectedMonth: YearMonth,
|
|
||||||
onPreviousMonth: () -> Unit,
|
|
||||||
onNextMonth: () -> Unit,
|
|
||||||
onMonthSelected: (YearMonth) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
var showMonthPicker by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
Card(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
// 月份选择器
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
IconButton(onClick = onPreviousMonth) {
|
|
||||||
Icon(Icons.AutoMirrored.Filled.KeyboardArrowLeft, "上个月")
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(text = "${selectedMonth.year}年${selectedMonth.monthValue}月",
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
modifier = Modifier.clickable { showMonthPicker = true })
|
|
||||||
|
|
||||||
IconButton(onClick = onNextMonth) {
|
|
||||||
Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, "下个月")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
// 收入统计
|
|
||||||
Column(modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.clickable { onIncomeClick() }
|
|
||||||
.background(
|
|
||||||
if (selectedType == TransactionType.INCOME) MaterialTheme.colorScheme.primaryContainer
|
|
||||||
else Color.Transparent, RoundedCornerShape(8.dp)
|
|
||||||
)
|
|
||||||
.padding(8.dp)) {
|
|
||||||
Text(
|
|
||||||
text = "收入", style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "¥${String.format("%.2f", totalIncome)}",
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
|
|
||||||
// 支出统计
|
|
||||||
Column(modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.clickable { onExpenseClick() }
|
|
||||||
.background(
|
|
||||||
if (selectedType == TransactionType.EXPENSE) MaterialTheme.colorScheme.primaryContainer
|
|
||||||
else Color.Transparent, RoundedCornerShape(8.dp)
|
|
||||||
)
|
|
||||||
.padding(8.dp)) {
|
|
||||||
Text(
|
|
||||||
text = "支出", style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "¥${String.format("%.2f", totalExpense)}",
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
color = MaterialTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedType != null) {
|
|
||||||
TextButton(
|
|
||||||
onClick = onClearFilter, modifier = Modifier.align(Alignment.End)
|
|
||||||
) {
|
|
||||||
Text("清除筛选")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showMonthPicker) {
|
|
||||||
MonthYearPickerDialog(selectedMonth = selectedMonth,
|
|
||||||
onMonthSelected = onMonthSelected,
|
|
||||||
onDismiss = { showMonthPicker = false })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun RecordItem(
|
|
||||||
record: BookkeepingRecord,
|
|
||||||
onClick: () -> Unit = {},
|
|
||||||
onDelete: () -> Unit = {},
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable(onClick = onClick),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = if (record.type == TransactionType.EXPENSE) "-" else "+",
|
|
||||||
color = if (record.type == TransactionType.EXPENSE) MaterialTheme.colorScheme.error
|
|
||||||
else MaterialTheme.colorScheme.primary,
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = String.format("%.2f", record.amount),
|
|
||||||
color = if (record.type == TransactionType.EXPENSE) MaterialTheme.colorScheme.error
|
|
||||||
else MaterialTheme.colorScheme.primary,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
modifier = Modifier.padding(end = 8.dp)
|
|
||||||
)
|
|
||||||
IconButton(onClick = onDelete) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Delete,
|
|
||||||
contentDescription = "删除",
|
|
||||||
tint = MaterialTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,8 @@ import com.yovinchen.bookkeeping.model.ThemeMode
|
|||||||
import com.yovinchen.bookkeeping.ui.components.ColorPicker
|
import com.yovinchen.bookkeeping.ui.components.ColorPicker
|
||||||
import com.yovinchen.bookkeeping.ui.components.predefinedColors
|
import com.yovinchen.bookkeeping.ui.components.predefinedColors
|
||||||
import com.yovinchen.bookkeeping.ui.dialog.CategoryManagementDialog
|
import com.yovinchen.bookkeeping.ui.dialog.CategoryManagementDialog
|
||||||
|
import com.yovinchen.bookkeeping.ui.dialog.MemberManagementDialog
|
||||||
|
import com.yovinchen.bookkeeping.viewmodel.MemberViewModel
|
||||||
import com.yovinchen.bookkeeping.viewmodel.SettingsViewModel
|
import com.yovinchen.bookkeeping.viewmodel.SettingsViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@ -35,15 +37,27 @@ import com.yovinchen.bookkeeping.viewmodel.SettingsViewModel
|
|||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
currentTheme: ThemeMode,
|
currentTheme: ThemeMode,
|
||||||
onThemeChange: (ThemeMode) -> Unit,
|
onThemeChange: (ThemeMode) -> Unit,
|
||||||
viewModel: SettingsViewModel = viewModel()
|
viewModel: SettingsViewModel = viewModel(),
|
||||||
|
memberViewModel: MemberViewModel = viewModel()
|
||||||
) {
|
) {
|
||||||
var showThemeDialog by remember { mutableStateOf(false) }
|
var showThemeDialog by remember { mutableStateOf(false) }
|
||||||
var showCategoryDialog by remember { mutableStateOf(false) }
|
var showCategoryDialog by remember { mutableStateOf(false) }
|
||||||
|
var showMemberDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val categories by viewModel.categories.collectAsState()
|
val categories by viewModel.categories.collectAsState()
|
||||||
val selectedType by viewModel.selectedCategoryType.collectAsState()
|
val selectedType by viewModel.selectedCategoryType.collectAsState()
|
||||||
|
val members by memberViewModel.allMembers.collectAsState(initial = emptyList())
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
// 成员管理设置项
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text("成员管理") },
|
||||||
|
supportingContent = { Text("管理账本成员") },
|
||||||
|
modifier = Modifier.clickable { showMemberDialog = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
// 类别管理设置项
|
// 类别管理设置项
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text("类别管理") },
|
headlineContent = { Text("类别管理") },
|
||||||
@ -145,6 +159,19 @@ fun SettingsScreen(
|
|||||||
onTypeChange = viewModel::setSelectedCategoryType
|
onTypeChange = viewModel::setSelectedCategoryType
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 成员管理对话框
|
||||||
|
if (showMemberDialog) {
|
||||||
|
MemberManagementDialog(
|
||||||
|
onDismiss = { showMemberDialog = false },
|
||||||
|
members = members,
|
||||||
|
onAddMember = memberViewModel::addMember,
|
||||||
|
onDeleteMember = memberViewModel::deleteMember,
|
||||||
|
onUpdateMember = { member, name, description ->
|
||||||
|
memberViewModel.updateMember(member.copy(name = name, description = description))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import com.yovinchen.bookkeeping.data.BookkeepingDatabase
|
import com.yovinchen.bookkeeping.data.BookkeepingDatabase
|
||||||
import com.yovinchen.bookkeeping.model.BookkeepingRecord
|
import com.yovinchen.bookkeeping.model.BookkeepingRecord
|
||||||
import com.yovinchen.bookkeeping.model.Category
|
import com.yovinchen.bookkeeping.model.Category
|
||||||
|
import com.yovinchen.bookkeeping.model.Member
|
||||||
import com.yovinchen.bookkeeping.model.TransactionType
|
import com.yovinchen.bookkeeping.model.TransactionType
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
@ -14,37 +15,39 @@ import kotlinx.coroutines.launch
|
|||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.YearMonth
|
import java.time.YearMonth
|
||||||
import java.util.Date
|
import java.util.*
|
||||||
import java.util.Calendar
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
private val TAG = "HomeViewModel"
|
private val TAG = "HomeViewModel"
|
||||||
private val dao = BookkeepingDatabase.getDatabase(application).bookkeepingDao()
|
private val bookkeepingDao = BookkeepingDatabase.getDatabase(application).bookkeepingDao()
|
||||||
|
private val memberDao = BookkeepingDatabase.getDatabase(application).memberDao()
|
||||||
|
private val categoryDao = BookkeepingDatabase.getDatabase(application).categoryDao()
|
||||||
|
|
||||||
private val _selectedRecordType = MutableStateFlow<TransactionType?>(null)
|
private val _selectedRecordType = MutableStateFlow<TransactionType?>(null)
|
||||||
val selectedRecordType: StateFlow<TransactionType?> = _selectedRecordType.asStateFlow()
|
val selectedRecordType: StateFlow<TransactionType?> = _selectedRecordType.asStateFlow()
|
||||||
|
|
||||||
private val _selectedDateTime = MutableStateFlow(LocalDateTime.now())
|
|
||||||
val selectedDateTime: StateFlow<LocalDateTime> = _selectedDateTime.asStateFlow()
|
|
||||||
|
|
||||||
private val _selectedCategoryType = MutableStateFlow(TransactionType.EXPENSE)
|
|
||||||
val selectedCategoryType: StateFlow<TransactionType> = _selectedCategoryType.asStateFlow()
|
|
||||||
|
|
||||||
private val _selectedMonth = MutableStateFlow(YearMonth.now())
|
private val _selectedMonth = MutableStateFlow(YearMonth.now())
|
||||||
val selectedMonth: StateFlow<YearMonth> = _selectedMonth.asStateFlow()
|
val selectedMonth: StateFlow<YearMonth> = _selectedMonth.asStateFlow()
|
||||||
|
|
||||||
private val records = dao.getAllRecords()
|
private val _selectedMember = MutableStateFlow<Member?>(null)
|
||||||
|
val selectedMember: StateFlow<Member?> = _selectedMember.asStateFlow()
|
||||||
|
|
||||||
|
val members = memberDao.getAllMembers()
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
started = SharingStarted.WhileSubscribed(5000),
|
started = SharingStarted.WhileSubscribed(5000),
|
||||||
initialValue = emptyList()
|
initialValue = emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
val categories: StateFlow<List<Category>> = _selectedCategoryType
|
val categories = categoryDao.getAllCategories()
|
||||||
.flatMapLatest { type ->
|
.stateIn(
|
||||||
dao.getCategoriesByType(type)
|
scope = viewModelScope,
|
||||||
}
|
started = SharingStarted.WhileSubscribed(5000),
|
||||||
|
initialValue = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
private val allRecords = bookkeepingDao.getAllRecords()
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
started = SharingStarted.WhileSubscribed(5000),
|
started = SharingStarted.WhileSubscribed(5000),
|
||||||
@ -52,26 +55,28 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val filteredRecords = combine(
|
val filteredRecords = combine(
|
||||||
records,
|
allRecords,
|
||||||
_selectedRecordType,
|
_selectedRecordType,
|
||||||
_selectedMonth
|
_selectedMonth,
|
||||||
) { records, selectedType, selectedMonth ->
|
_selectedMember
|
||||||
|
) { records, selectedType, selectedMonth, selectedMember ->
|
||||||
records
|
records
|
||||||
.filter { record ->
|
.filter { record ->
|
||||||
val recordDate = record.date.toInstant()
|
val recordDate = record.date.toInstant()
|
||||||
.atZone(ZoneId.systemDefault())
|
.atZone(ZoneId.systemDefault())
|
||||||
.toLocalDate()
|
.toLocalDate()
|
||||||
val recordYearMonth = YearMonth.from(recordDate)
|
val recordYearMonth = YearMonth.from(recordDate)
|
||||||
|
|
||||||
val typeMatches = selectedType?.let { record.type == it } ?: true
|
val typeMatches = selectedType?.let { record.type == it } ?: true
|
||||||
val monthMatches = recordYearMonth == selectedMonth
|
val monthMatches = recordYearMonth == selectedMonth
|
||||||
|
val memberMatches = selectedMember?.let { record.memberId == it.id } ?: true
|
||||||
typeMatches && monthMatches
|
|
||||||
|
monthMatches && memberMatches && typeMatches
|
||||||
}
|
}
|
||||||
.sortedByDescending { it.date }
|
.sortedByDescending { it.date }
|
||||||
.groupBy { record ->
|
.groupBy { record ->
|
||||||
val calendar = Calendar.getInstance().apply { time = record.date }
|
Calendar.getInstance().apply {
|
||||||
calendar.apply {
|
time = record.date
|
||||||
set(Calendar.HOUR_OF_DAY, 0)
|
set(Calendar.HOUR_OF_DAY, 0)
|
||||||
set(Calendar.MINUTE, 0)
|
set(Calendar.MINUTE, 0)
|
||||||
set(Calendar.SECOND, 0)
|
set(Calendar.SECOND, 0)
|
||||||
@ -79,15 +84,16 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}.time
|
}.time
|
||||||
}
|
}
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
viewModelScope,
|
scope = viewModelScope,
|
||||||
SharingStarted.WhileSubscribed(5000),
|
started = SharingStarted.WhileSubscribed(5000),
|
||||||
emptyMap()
|
initialValue = emptyMap()
|
||||||
)
|
)
|
||||||
|
|
||||||
val totalIncome = combine(
|
val totalIncome = combine(
|
||||||
records,
|
allRecords,
|
||||||
_selectedMonth
|
_selectedMonth,
|
||||||
) { records, selectedMonth ->
|
_selectedMember
|
||||||
|
) { records, selectedMonth, selectedMember ->
|
||||||
records
|
records
|
||||||
.filter { record ->
|
.filter { record ->
|
||||||
val recordDate = record.date.toInstant()
|
val recordDate = record.date.toInstant()
|
||||||
@ -95,19 +101,24 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
.toLocalDate()
|
.toLocalDate()
|
||||||
val recordYearMonth = YearMonth.from(recordDate)
|
val recordYearMonth = YearMonth.from(recordDate)
|
||||||
|
|
||||||
record.type == TransactionType.INCOME && recordYearMonth == selectedMonth
|
val monthMatches = recordYearMonth == selectedMonth
|
||||||
|
val memberMatches = selectedMember?.let { record.memberId == it.id } ?: true
|
||||||
|
val typeMatches = record.type == TransactionType.INCOME
|
||||||
|
|
||||||
|
monthMatches && memberMatches && typeMatches
|
||||||
}
|
}
|
||||||
.sumOf { it.amount }
|
.sumOf { it.amount }
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
viewModelScope,
|
scope = viewModelScope,
|
||||||
SharingStarted.WhileSubscribed(5000),
|
started = SharingStarted.WhileSubscribed(5000),
|
||||||
0.0
|
initialValue = 0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
val totalExpense = combine(
|
val totalExpense = combine(
|
||||||
records,
|
allRecords,
|
||||||
_selectedMonth
|
_selectedMonth,
|
||||||
) { records, selectedMonth ->
|
_selectedMember
|
||||||
|
) { records, selectedMonth, selectedMember ->
|
||||||
records
|
records
|
||||||
.filter { record ->
|
.filter { record ->
|
||||||
val recordDate = record.date.toInstant()
|
val recordDate = record.date.toInstant()
|
||||||
@ -115,111 +126,73 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
.toLocalDate()
|
.toLocalDate()
|
||||||
val recordYearMonth = YearMonth.from(recordDate)
|
val recordYearMonth = YearMonth.from(recordDate)
|
||||||
|
|
||||||
record.type == TransactionType.EXPENSE && recordYearMonth == selectedMonth
|
val monthMatches = recordYearMonth == selectedMonth
|
||||||
|
val memberMatches = selectedMember?.let { record.memberId == it.id } ?: true
|
||||||
|
val typeMatches = record.type == TransactionType.EXPENSE
|
||||||
|
|
||||||
|
monthMatches && memberMatches && typeMatches
|
||||||
}
|
}
|
||||||
.sumOf { it.amount }
|
.sumOf { it.amount }
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
viewModelScope,
|
scope = viewModelScope,
|
||||||
SharingStarted.WhileSubscribed(5000),
|
started = SharingStarted.WhileSubscribed(5000),
|
||||||
0.0
|
initialValue = 0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun updateTotals() {
|
|
||||||
// 移除未使用的参数
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
viewModelScope.launch {
|
|
||||||
records.collect {
|
|
||||||
updateTotals()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addRecord(type: TransactionType, amount: Double, category: String, description: String) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
val record = BookkeepingRecord(
|
|
||||||
amount = amount,
|
|
||||||
type = type,
|
|
||||||
category = category,
|
|
||||||
description = description,
|
|
||||||
date = Date.from(_selectedDateTime.value.atZone(ZoneId.systemDefault()).toInstant())
|
|
||||||
)
|
|
||||||
dao.insertRecord(record)
|
|
||||||
resetSelectedDateTime()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSelectedDateTime(dateTime: LocalDateTime) {
|
|
||||||
_selectedDateTime.value = dateTime
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSelectedRecordType(type: TransactionType?) {
|
|
||||||
_selectedRecordType.value = type
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSelectedCategoryType(type: TransactionType) {
|
|
||||||
_selectedCategoryType.value = type
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSelectedMonth(yearMonth: YearMonth) {
|
fun setSelectedMonth(yearMonth: YearMonth) {
|
||||||
_selectedMonth.value = yearMonth
|
_selectedMonth.value = yearMonth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setSelectedMember(member: Member?) {
|
||||||
|
_selectedMember.value = member
|
||||||
|
}
|
||||||
|
|
||||||
fun moveMonth(forward: Boolean) {
|
fun moveMonth(forward: Boolean) {
|
||||||
val current = _selectedMonth.value
|
|
||||||
_selectedMonth.value = if (forward) {
|
_selectedMonth.value = if (forward) {
|
||||||
current.plusMonths(1)
|
_selectedMonth.value.plusMonths(1)
|
||||||
} else {
|
} else {
|
||||||
current.minusMonths(1)
|
_selectedMonth.value.minusMonths(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetSelectedDateTime() {
|
suspend fun getMemberById(memberId: Int): Member? {
|
||||||
_selectedDateTime.value = LocalDateTime.now()
|
return memberDao.getMemberById(memberId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addRecord(
|
||||||
|
amount: Double,
|
||||||
|
category: String,
|
||||||
|
description: String,
|
||||||
|
date: Date,
|
||||||
|
type: TransactionType,
|
||||||
|
memberId: Int?
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val record = BookkeepingRecord(
|
||||||
|
type = type,
|
||||||
|
amount = amount,
|
||||||
|
category = category,
|
||||||
|
description = description,
|
||||||
|
date = date,
|
||||||
|
memberId = memberId
|
||||||
|
)
|
||||||
|
bookkeepingDao.insertRecord(record)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateRecord(record: BookkeepingRecord) {
|
fun updateRecord(record: BookkeepingRecord) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
dao.updateRecord(record)
|
bookkeepingDao.updateRecord(record)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteRecord(record: BookkeepingRecord) {
|
fun deleteRecord(record: BookkeepingRecord) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
dao.deleteRecord(record)
|
bookkeepingDao.deleteRecord(record)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取指定日期的记录
|
fun setSelectedRecordType(type: TransactionType?) {
|
||||||
fun getRecordsByDate(date: LocalDateTime): Flow<List<BookkeepingRecord>> {
|
_selectedRecordType.value = type
|
||||||
val calendar = Calendar.getInstance().apply {
|
|
||||||
time = Date.from(date.atZone(ZoneId.systemDefault()).toInstant())
|
|
||||||
set(Calendar.HOUR_OF_DAY, 0)
|
|
||||||
set(Calendar.MINUTE, 0)
|
|
||||||
set(Calendar.SECOND, 0)
|
|
||||||
set(Calendar.MILLISECOND, 0)
|
|
||||||
}
|
|
||||||
val startOfDay = calendar.time
|
|
||||||
calendar.add(Calendar.DAY_OF_MONTH, 1)
|
|
||||||
val endOfDay = calendar.time
|
|
||||||
return dao.getRecordsByDateRange(startOfDay, endOfDay)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取指定日期范围的记录
|
|
||||||
fun getRecordsByDateRange(startDate: LocalDateTime, endDate: LocalDateTime): Flow<List<BookkeepingRecord>> {
|
|
||||||
val start = Date.from(startDate.atZone(ZoneId.systemDefault()).toInstant())
|
|
||||||
val end = Date.from(endDate.atZone(ZoneId.systemDefault()).toInstant())
|
|
||||||
return dao.getRecordsByDateRange(start, end)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取指定类型的记录
|
|
||||||
fun getRecordsByType(type: TransactionType): Flow<List<BookkeepingRecord>> {
|
|
||||||
return dao.getRecordsByType(type)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class UiState(
|
|
||||||
val isAddingRecord: Boolean = false,
|
|
||||||
val isManagingCategories: Boolean = false
|
|
||||||
)
|
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
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.Member
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class MemberViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
private val memberDao = BookkeepingDatabase.getDatabase(application).memberDao()
|
||||||
|
|
||||||
|
val allMembers: Flow<List<Member>> = memberDao.getAllMembers()
|
||||||
|
|
||||||
|
fun addMember(name: String, description: String = "") {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val member = Member(name = name, description = description)
|
||||||
|
memberDao.insertMember(member)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateMember(member: Member) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
memberDao.updateMember(member)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteMember(member: Member) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
memberDao.deleteMember(member)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getMemberCount(): Int {
|
||||||
|
return memberDao.getMemberCount()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user