docs: 增加注释

This commit is contained in:
yovinchen 2025-07-14 14:31:36 +08:00
parent 8bc3e987aa
commit 4c1aa501e6
11 changed files with 347 additions and 75 deletions

View File

@ -24,40 +24,66 @@ import com.yovinchen.bookkeeping.ui.navigation.MainNavigation
import com.yovinchen.bookkeeping.ui.theme.BookkeepingTheme import com.yovinchen.bookkeeping.ui.theme.BookkeepingTheme
import com.yovinchen.bookkeeping.utils.FilePickerUtil import com.yovinchen.bookkeeping.utils.FilePickerUtil
/**
* 全局文件选择器启动器
* 用于在整个应用程序中共享同一个文件选择器实例
*/
private var filePickerLauncher: ActivityResultLauncher<Array<String>>? = null private var filePickerLauncher: ActivityResultLauncher<Array<String>>? = null
/**
* 获取预先注册的文件选择器启动器的扩展函数
*
* @return 预先注册的文件选择器启动器
* @throws IllegalStateException 如果文件选择器未初始化
*/
fun ComponentActivity.getPreregisteredFilePickerLauncher(): ActivityResultLauncher<Array<String>> { fun ComponentActivity.getPreregisteredFilePickerLauncher(): ActivityResultLauncher<Array<String>> {
return filePickerLauncher ?: throw IllegalStateException("FilePickerLauncher not initialized") return filePickerLauncher ?: throw IllegalStateException("FilePickerLauncher not initialized")
} }
/**
* 应用程序的主活动
* 负责初始化应用界面和必要的系统组件
*/
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// 设置系统窗口装饰,确保内容能够扩展到系统栏区域
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
// 预注册文件选择器 // 预注册文件选择器,用于处理文件选择操作
filePickerLauncher = registerForActivityResult( filePickerLauncher = registerForActivityResult(
ActivityResultContracts.OpenDocument() ActivityResultContracts.OpenDocument()
) { uri: Uri? -> ) { uri: Uri? ->
// 当用户选择文件后,调用工具类处理文件选择结果
FilePickerUtil.handleFileSelection(this, uri) FilePickerUtil.handleFileSelection(this, uri)
} }
// 设置应用的主Compose内容
setContent { setContent {
BookkeepingApp() BookkeepingApp()
} }
} }
} }
/**
* 系统状态栏和导航栏颜色设置
* 根据当前主题模式设置系统UI元素的颜色和外观
*
* @param isDarkTheme 是否为暗色主题
*/
@Composable @Composable
private fun SystemBarColor(isDarkTheme: Boolean) { private fun SystemBarColor(isDarkTheme: Boolean) {
val view = LocalView.current val view = LocalView.current
if (!view.isInEditMode) { if (!view.isInEditMode) {
// 获取当前主题的表面颜色用于系统栏
val surfaceColor = MaterialTheme.colorScheme.surface.toArgb() val surfaceColor = MaterialTheme.colorScheme.surface.toArgb()
val currentWindow = (view.context as? Activity)?.window val currentWindow = (view.context as? Activity)?.window
SideEffect { SideEffect {
currentWindow?.let { window -> currentWindow?.let { window ->
// 设置状态栏和导航栏颜色
window.statusBarColor = surfaceColor window.statusBarColor = surfaceColor
window.navigationBarColor = surfaceColor window.navigationBarColor = surfaceColor
// 设置系统栏图标的亮暗模式,以确保在不同背景下的可见性
WindowCompat.getInsetsController(window, view).apply { WindowCompat.getInsetsController(window, view).apply {
isAppearanceLightStatusBars = !isDarkTheme isAppearanceLightStatusBars = !isDarkTheme
isAppearanceLightNavigationBars = !isDarkTheme isAppearanceLightNavigationBars = !isDarkTheme
@ -67,27 +93,37 @@ private fun SystemBarColor(isDarkTheme: Boolean) {
} }
} }
/**
* 记账应用的主Compose函数
* 处理主题设置并启动主导航组件
*/
@Composable @Composable
fun BookkeepingApp() { fun BookkeepingApp() {
// 跟踪当前应用的主题模式状态
var themeMode by remember { mutableStateOf<ThemeMode>(ThemeMode.FOLLOW_SYSTEM) } var themeMode by remember { mutableStateOf<ThemeMode>(ThemeMode.FOLLOW_SYSTEM) }
// 根据主题模式确定是否使用暗色主题
val isDarkTheme = when (themeMode) { val isDarkTheme = when (themeMode) {
is ThemeMode.FOLLOW_SYSTEM -> isSystemInDarkTheme() is ThemeMode.FOLLOW_SYSTEM -> isSystemInDarkTheme() // 跟随系统设置
is ThemeMode.LIGHT -> false is ThemeMode.LIGHT -> false // 强制使用亮色主题
is ThemeMode.DARK -> true is ThemeMode.DARK -> true // 强制使用暗色主题
is ThemeMode.CUSTOM -> isSystemInDarkTheme() is ThemeMode.CUSTOM -> isSystemInDarkTheme() // 自定义主题下的基础亮暗模式仍跟随系统
} }
// 处理自定义主题颜色方案
val customColorScheme = when (themeMode) { val customColorScheme = when (themeMode) {
is ThemeMode.CUSTOM -> { is ThemeMode.CUSTOM -> {
// 从主题模式中提取自定义主色
val primaryColor = (themeMode as ThemeMode.CUSTOM).primaryColor val primaryColor = (themeMode as ThemeMode.CUSTOM).primaryColor
if (isDarkTheme) { if (isDarkTheme) {
// 暗色模式下的自定义颜色方案
MaterialTheme.colorScheme.copy( MaterialTheme.colorScheme.copy(
primary = primaryColor, primary = primaryColor,
secondary = primaryColor.copy(alpha = 0.7f), secondary = primaryColor.copy(alpha = 0.7f),
tertiary = primaryColor.copy(alpha = 0.5f) tertiary = primaryColor.copy(alpha = 0.5f)
) )
} else { } else {
// 亮色模式下的自定义颜色方案
MaterialTheme.colorScheme.copy( MaterialTheme.colorScheme.copy(
primary = primaryColor, primary = primaryColor,
secondary = primaryColor.copy(alpha = 0.7f), secondary = primaryColor.copy(alpha = 0.7f),
@ -95,27 +131,38 @@ fun BookkeepingApp() {
) )
} }
} }
else -> null else -> null // 非自定义主题模式使用默认颜色方案
} }
// 应用主题到整个应用内容
BookkeepingTheme( BookkeepingTheme(
darkTheme = isDarkTheme, darkTheme = isDarkTheme,
customColorScheme = customColorScheme customColorScheme = customColorScheme
) { ) {
// 设置系统状态栏和导航栏颜色
SystemBarColor(isDarkTheme) SystemBarColor(isDarkTheme)
// 创建填充整个屏幕的基础Surface
Surface( Surface(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.surface color = MaterialTheme.colorScheme.surface
) { ) {
// 启动主导航组件,并传递主题相关参数
MainNavigation( MainNavigation(
currentTheme = themeMode, currentTheme = themeMode,
onThemeChange = { themeMode = it } onThemeChange = { themeMode = it } // 允许导航组件中的屏幕更改主题
) )
} }
} }
} }
/**
* 示例问候函数
* 仅用于开发预览和测试目的
*
* @param name 要显示的名称
* @param modifier 应用于Text组件的修饰符
*/
@Composable @Composable
fun Greeting(name: String, modifier: Modifier = Modifier) { fun Greeting(name: String, modifier: Modifier = Modifier) {
Text( Text(
@ -124,6 +171,9 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
) )
} }
/**
* Greeting组件的预览函数
*/
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
fun GreetingPreview() { fun GreetingPreview() {
@ -132,6 +182,9 @@ fun GreetingPreview() {
} }
} }
/**
* 整个应用的预览函数
*/
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
fun BookkeepingAppPreview() { fun BookkeepingAppPreview() {

View File

@ -1,7 +1,13 @@
package com.yovinchen.bookkeeping.model package com.yovinchen.bookkeeping.model
/**
* 分析类型枚举
* 定义记账应用中不同的数据分析视图类型
*
* 用于在数据分析模块中区分不同的分析维度和图表类型
*/
enum class AnalysisType { enum class AnalysisType {
EXPENSE, EXPENSE, // 支出分析,用于分析用户的支出情况
INCOME, INCOME, // 收入分析,用于分析用户的收入情况
TREND TREND // 趋势分析,用于分析用户收支随时间的变化趋势
} }

View File

@ -9,32 +9,71 @@ import androidx.room.TypeConverters
import com.yovinchen.bookkeeping.model.Member import com.yovinchen.bookkeeping.model.Member
import java.util.Date import java.util.Date
/**
* 交易类型枚举
* 定义记账记录的交易类型
*/
enum class TransactionType { enum class TransactionType {
INCOME, EXPENSE INCOME, // 收入
EXPENSE // 支出
} }
/**
* Room数据库类型转换器
* 用于在数据库中存储和检索复杂类型
*/
class Converters { class Converters {
/**
* 将时间戳转换为Date对象
*
* @param value 时间戳毫秒
* @return 对应的Date对象如果输入为null则返回null
*/
@TypeConverter @TypeConverter
fun fromTimestamp(value: Long?): Date? { fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) } return value?.let { Date(it) }
} }
/**
* 将Date对象转换为时间戳
*
* @param date Date对象
* @return 对应的时间戳毫秒如果输入为null则返回null
*/
@TypeConverter @TypeConverter
fun dateToTimestamp(date: Date?): Long? { fun dateToTimestamp(date: Date?): Long? {
return date?.time return date?.time
} }
/**
* 将字符串转换为TransactionType枚举
*
* @param value 交易类型的字符串表示
* @return 对应的TransactionType枚举值
*/
@TypeConverter @TypeConverter
fun fromTransactionType(value: String): TransactionType { fun fromTransactionType(value: String): TransactionType {
return enumValueOf<TransactionType>(value) return enumValueOf<TransactionType>(value)
} }
/**
* 将TransactionType枚举转换为字符串
*
* @param type TransactionType枚举值
* @return 对应的字符串表示
*/
@TypeConverter @TypeConverter
fun transactionTypeToString(type: TransactionType): String { fun transactionTypeToString(type: TransactionType): String {
return type.name return type.name
} }
} }
/**
* 记账记录实体类
* 用于在Room数据库中存储用户的收支记录
*
* 该实体与Member实体存在外键关系表示每条记录可以关联到一个家庭成员
*/
@Entity( @Entity(
tableName = "bookkeeping_records", tableName = "bookkeeping_records",
foreignKeys = [ foreignKeys = [
@ -42,21 +81,21 @@ class Converters {
entity = Member::class, entity = Member::class,
parentColumns = ["id"], parentColumns = ["id"],
childColumns = ["memberId"], childColumns = ["memberId"],
onDelete = ForeignKey.SET_NULL onDelete = ForeignKey.SET_NULL // 当关联的成员被删除时将此字段设为NULL
) )
], ],
indices = [ indices = [
Index(value = ["memberId"]) Index(value = ["memberId"]) // 在memberId上创建索引以提高查询性能
] ]
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class) // 应用类型转换器
data class BookkeepingRecord( data class BookkeepingRecord(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
val id: Long = 0, val id: Long = 0, // 记录ID自动生成
val amount: Double, val amount: Double, // 金额
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 // 可为空表示未指定成员 val memberId: Int? = null // 关联的成员ID可为空表示未指定成员
) )

View File

@ -3,11 +3,18 @@ package com.yovinchen.bookkeeping.model
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
/**
* 交易分类实体类
* 用于在Room数据库中存储收支分类信息
*
* 在记账应用中每条记账记录都属于某个分类
* "餐饮""交通""工资"便于用户对支出和收入进行分类统计
*/
@Entity(tableName = "categories") @Entity(tableName = "categories")
data class Category( data class Category(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
val id: Long = 0, val id: Long = 0, // 分类ID自动生成
val name: String, val name: String, // 分类名称
val type: TransactionType, val type: TransactionType, // 分类关联的交易类型(收入或支出)
val icon: Int? = null val icon: Int? = null // 分类图标资源ID可选默认为null
) )

View File

@ -1,8 +1,14 @@
package com.yovinchen.bookkeeping.model package com.yovinchen.bookkeeping.model
/**
* 分类统计数据类
* 用于表示某个分类的统计信息通常用于数据分析和图表展示
*
* 该类不是数据库实体而是从数据库查询结果中聚合生成的统计数据
*/
data class CategoryStat( data class CategoryStat(
val category: String, val category: String, // 分类名称
val amount: Double, val amount: Double, // 该分类的总金额
val count: Int = 0, val count: Int = 0, // 该分类下的记录数量
val percentage: Double = 0.0 val percentage: Double = 0.0 // 该分类金额占总金额的百分比0.0-100.0
) )

View File

@ -3,11 +3,18 @@ package com.yovinchen.bookkeeping.model
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
/**
* 家庭成员实体类
* 用于在Room数据库中存储家庭成员信息
*
* 在记账应用中每条记账记录可以关联到特定的家庭成员
* 以便追踪不同成员的收支情况
*/
@Entity(tableName = "members") @Entity(tableName = "members")
data class Member( data class Member(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
val id: Int = 0, val id: Int = 0, // 成员ID自动生成
val name: String, val name: String, // 成员姓名
val description: String = "", // 可选的描述信息 val description: String = "", // 成员描述信息,可选,默认为空字符串
val icon: Int? = null // 新增icon字段可为空 val icon: Int? = null // 成员图标资源ID可选默认为null
) )

View File

@ -2,16 +2,39 @@ package com.yovinchen.bookkeeping.model
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
/**
* 家庭成员统计数据类
* 用于表示某个成员的统计信息通常用于数据分析和图表展示
*
* 该类不是数据库实体而是通过数据库查询直接映射的结果类
* 表示按成员分组的聚合数据
*/
data class MemberStat( data class MemberStat(
/**
* 成员名称
* 映射数据库查询结果中的member列
*/
@ColumnInfo(name = "member") @ColumnInfo(name = "member")
val member: String, val member: String,
/**
* 该成员的总金额
* 映射数据库查询结果中的amount列
*/
@ColumnInfo(name = "amount") @ColumnInfo(name = "amount")
val amount: Double, val amount: Double,
/**
* 该成员下的记录数量
* 映射数据库查询结果中的count列
*/
@ColumnInfo(name = "count") @ColumnInfo(name = "count")
val count: Int, val count: Int,
/**
* 该成员金额占总金额的百分比0.0-100.0
* 映射数据库查询结果中的percentage列
*/
@ColumnInfo(name = "percentage") @ColumnInfo(name = "percentage")
val percentage: Double = 0.0 val percentage: Double = 0.0
) )

View File

@ -2,9 +2,35 @@ package com.yovinchen.bookkeeping.model
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
/**
* 主题模式密封类
* 用于表示应用程序的不同主题设置选项
* 通过密封类实现限制可能的主题模式类型
*/
sealed class ThemeMode { sealed class ThemeMode {
/**
* 跟随系统主题模式
* 应用将根据设备系统的暗色/亮色主题设置自动调整
*/
object FOLLOW_SYSTEM : ThemeMode() object FOLLOW_SYSTEM : ThemeMode()
/**
* 固定亮色主题模式
* 无论设备系统设置如何应用将始终使用亮色主题
*/
object LIGHT : ThemeMode() object LIGHT : ThemeMode()
/**
* 固定暗色主题模式
* 无论设备系统设置如何应用将始终使用暗色主题
*/
object DARK : ThemeMode() object DARK : ThemeMode()
/**
* 自定义主题模式
* 允许用户选择自定义的主题颜色
*
* @property primaryColor 用户选择的主要颜色将影响应用的主色调
*/
data class CUSTOM(val primaryColor: Color) : ThemeMode() data class CUSTOM(val primaryColor: Color) : ThemeMode()
} }

View File

@ -20,7 +20,6 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState

View File

@ -10,40 +10,72 @@ import com.yovinchen.bookkeeping.getPreregisteredFilePickerLauncher
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
/**
* 文件选择器工具类
* 用于处理文件选择权限获取和文件处理的工具类
*
* 主要功能
* 1. 启动系统文件选择器
* 2. 处理选择结果
* 3. 将选择的文件复制到应用缓存目录
* 4. 文件类型验证
*/
object FilePickerUtil { object FilePickerUtil {
/**
* 当前活跃的文件选择回调
* 用于在文件选择完成后调用
*/
private var currentCallback: ((File) -> Unit)? = null private var currentCallback: ((File) -> Unit)? = null
/**
* 启动文件选择器
*
* @param activity 当前活动用于启动文件选择器
* @param onFileSelected 文件选择完成后的回调函数参数为选中的文件
*/
fun startFilePicker(activity: ComponentActivity, onFileSelected: (File) -> Unit) { fun startFilePicker(activity: ComponentActivity, onFileSelected: (File) -> Unit) {
currentCallback = onFileSelected currentCallback = onFileSelected
try { try {
// 设置可选择的文件类型限制为CSV和Excel文件
val mimeTypes = arrayOf( val mimeTypes = arrayOf(
"text/csv", "text/csv",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-excel" "application/vnd.ms-excel"
) )
// 使用预注册的文件选择器启动文件选择流程
activity.getPreregisteredFilePickerLauncher().launch(mimeTypes) activity.getPreregisteredFilePickerLauncher().launch(mimeTypes)
} catch (e: Exception) { } catch (e: Exception) {
// 文件选择器启动失败时显示错误提示
Toast.makeText(activity, "无法启动文件选择器:${e.message}", Toast.LENGTH_SHORT).show() Toast.makeText(activity, "无法启动文件选择器:${e.message}", Toast.LENGTH_SHORT).show()
currentCallback = null currentCallback = null
} }
} }
/**
* 处理文件选择结果
*
* @param context 上下文对象用于访问ContentResolver
* @param uri 选中文件的URI如果用户取消选择则为null
*/
fun handleFileSelection(context: Context, uri: Uri?) { fun handleFileSelection(context: Context, uri: Uri?) {
if (uri == null) { if (uri == null) {
// 用户未选择文件时显示提示
Toast.makeText(context, "未选择文件", Toast.LENGTH_SHORT).show() Toast.makeText(context, "未选择文件", Toast.LENGTH_SHORT).show()
currentCallback = null currentCallback = null
return return
} }
try { try {
// 获取文件MIME类型
val mimeType = context.contentResolver.getType(uri) val mimeType = context.contentResolver.getType(uri)
// 验证文件类型是否合法
if (!isValidFileType(uri.toString(), mimeType)) { if (!isValidFileType(uri.toString(), mimeType)) {
Toast.makeText(context, "请选择CSV或Excel文件", Toast.LENGTH_SHORT).show() Toast.makeText(context, "请选择CSV或Excel文件", Toast.LENGTH_SHORT).show()
return return
} }
// 获取持久性权限 // 获取持久性权限,确保应用在重启后仍能访问该文件
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_WRITE_URI_PERMISSION
context.contentResolver.takePersistableUriPermission(uri, takeFlags) context.contentResolver.takePersistableUriPermission(uri, takeFlags)
@ -51,6 +83,7 @@ object FilePickerUtil {
// 将选中的文件复制到应用私有目录 // 将选中的文件复制到应用私有目录
val tempFile = copyUriToTempFile(context, uri) val tempFile = copyUriToTempFile(context, uri)
if (tempFile != null) { if (tempFile != null) {
// 调用回调函数,传递临时文件
currentCallback?.invoke(tempFile) currentCallback?.invoke(tempFile)
} else { } else {
Toast.makeText(context, "文件处理失败,请重试", Toast.LENGTH_SHORT).show() Toast.makeText(context, "文件处理失败,请重试", Toast.LENGTH_SHORT).show()
@ -59,10 +92,18 @@ object FilePickerUtil {
e.printStackTrace() e.printStackTrace()
Toast.makeText(context, "文件处理出错:${e.message}", Toast.LENGTH_SHORT).show() Toast.makeText(context, "文件处理出错:${e.message}", Toast.LENGTH_SHORT).show()
} finally { } finally {
// 清除回调引用,避免内存泄漏
currentCallback = null currentCallback = null
} }
} }
/**
* 验证文件类型是否合法
*
* @param fileName 文件名用于检查文件扩展名
* @param mimeType 文件MIME类型
* @return 如果文件类型合法则返回true否则返回false
*/
private fun isValidFileType(fileName: String, mimeType: String?): Boolean { private fun isValidFileType(fileName: String, mimeType: String?): Boolean {
val fileExtension = fileName.lowercase() val fileExtension = fileName.lowercase()
return fileExtension.endsWith(".csv") || return fileExtension.endsWith(".csv") ||
@ -73,11 +114,20 @@ object FilePickerUtil {
mimeType == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" mimeType == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
} }
/**
* 将URI指向的文件复制到应用缓存目录
*
* @param context 上下文对象用于访问ContentResolver和缓存目录
* @param uri 要复制的文件URI
* @return 复制后的临时文件如果复制失败则返回null
*/
private fun copyUriToTempFile(context: Context, uri: Uri): File? { private fun copyUriToTempFile(context: Context, uri: Uri): File? {
return try { return try {
// 获取文件名,如果无法获取则使用时间戳作为文件名
val fileName = getFileName(context, uri) ?: "temp_backup_${System.currentTimeMillis()}" val fileName = getFileName(context, uri) ?: "temp_backup_${System.currentTimeMillis()}"
val tempFile = File(context.cacheDir, fileName) val tempFile = File(context.cacheDir, fileName)
// 从URI读取内容并写入临时文件
context.contentResolver.openInputStream(uri)?.use { inputStream -> context.contentResolver.openInputStream(uri)?.use { inputStream ->
FileOutputStream(tempFile).use { outputStream -> FileOutputStream(tempFile).use { outputStream ->
inputStream.copyTo(outputStream) inputStream.copyTo(outputStream)
@ -90,6 +140,13 @@ object FilePickerUtil {
} }
} }
/**
* 从URI中获取文件名
*
* @param context 上下文对象用于访问ContentResolver
* @param uri 文件URI
* @return 文件名如果无法获取则返回null
*/
private fun getFileName(context: Context, uri: Uri): String? { private fun getFileName(context: Context, uri: Uri): String? {
var fileName: String? = null var fileName: String? = null
context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->

View File

@ -6,77 +6,126 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import com.yovinchen.bookkeeping.R import com.yovinchen.bookkeeping.R
/**
* 图标管理器
* 集中管理应用中使用的各类图标资源
*
* 主要功能
* 1. 管理分类图标和成员图标的映射关系
* 2. 提供根据名称获取对应图标的方法
* 3. 提供获取所有可用图标的方法
*/
object IconManager { object IconManager {
// 类别图标映射 /**
* 类别图标映射
* 将分类名称映射到对应的图标资源ID
*/
private val categoryIcons = mapOf( private val categoryIcons = mapOf(
"餐饮" to R.drawable.ic_category_food_24dp, "餐饮" to R.drawable.ic_category_food_24dp, // 餐饮类别对应食物图标
"交通" to R.drawable.ic_category_taxi_24dp, "交通" to R.drawable.ic_category_taxi_24dp, // 交通类别对应出租车图标
"购物" to R.drawable.ic_category_supermarket_24dp, "购物" to R.drawable.ic_category_supermarket_24dp, // 购物类别对应超市图标
"娱乐" to R.drawable.ic_category_bar_24dp, "娱乐" to R.drawable.ic_category_bar_24dp, // 娱乐类别对应酒吧图标
"居住" to R.drawable.ic_category_hotel_24dp, "居住" to R.drawable.ic_category_hotel_24dp, // 居住类别对应酒店图标
"医疗" to R.drawable.ic_category_medicine_24dp, "医疗" to R.drawable.ic_category_medicine_24dp, // 医疗类别对应药品图标
"教育" to R.drawable.ic_category_training_24dp, "教育" to R.drawable.ic_category_training_24dp, // 教育类别对应培训图标
"宠物" to R.drawable.ic_category_pet_24dp, "宠物" to R.drawable.ic_category_pet_24dp, // 宠物类别对应宠物图标
"鲜花" to R.drawable.ic_category_flower_24dp, "鲜花" to R.drawable.ic_category_flower_24dp, // 鲜花类别对应花图标
"外卖" to R.drawable.ic_category_delivery_24dp, "外卖" to R.drawable.ic_category_delivery_24dp, // 外卖类别对应外卖图标
"数码" to R.drawable.ic_category_digital_24dp, "数码" to R.drawable.ic_category_digital_24dp, // 数码类别对应数码产品图标
"化妆品" to R.drawable.ic_category_cosmetics_24dp, "化妆品" to R.drawable.ic_category_cosmetics_24dp, // 化妆品类别对应化妆品图标
"水果" to R.drawable.ic_category_fruit_24dp, "水果" to R.drawable.ic_category_fruit_24dp, // 水果类别对应水果图标
"零食" to R.drawable.ic_category_snack_24dp, "零食" to R.drawable.ic_category_snack_24dp, // 零食类别对应零食图标
"蔬菜" to R.drawable.ic_category_vegetable_24dp, "蔬菜" to R.drawable.ic_category_vegetable_24dp, // 蔬菜类别对应蔬菜图标
"工资" to R.drawable.ic_category_membership_24dp, "工资" to R.drawable.ic_category_membership_24dp, // 工资类别对应会员图标
"礼物" to R.drawable.ic_category_gift_24dp, "礼物" to R.drawable.ic_category_gift_24dp, // 礼物类别对应礼物图标
"其他" to R.drawable.ic_category_more_24dp, "其他" to R.drawable.ic_category_more_24dp, // 其他类别对应更多图标
"工资" to R.drawable.ic_category_membership_24dp, "会员" to R.drawable.ic_category_membership_24dp, // 会员类别对应会员图标
"会员" to R.drawable.ic_category_membership_24dp, "奖金" to R.drawable.ic_category_gift_24dp, // 奖金类别对应礼物图标
"奖金" to R.drawable.ic_category_gift_24dp, "投资" to R.drawable.ic_category_digital_24dp // 投资类别对应数码图标
"投资" to R.drawable.ic_category_digital_24dp,
"其他" to R.drawable.ic_category_more_24dp
) )
// 成员图标映射 /**
* 成员图标映射
* 将成员角色名称映射到对应的图标资源ID
*/
private val memberIcons = mapOf( private val memberIcons = mapOf(
"自己" to R.drawable.ic_member_boy_24dp, "自己" to R.drawable.ic_member_boy_24dp, // 自己对应男孩图标
"老婆" to R.drawable.ic_member_bride_24dp, "老婆" to R.drawable.ic_member_bride_24dp, // 老婆对应新娘图标
"老公" to R.drawable.ic_member_groom_24dp, "老公" to R.drawable.ic_member_groom_24dp, // 老公对应新郎图标
"家庭" to R.drawable.ic_member_family_24dp, "家庭" to R.drawable.ic_member_family_24dp, // 家庭对应家庭图标
"儿子" to R.drawable.ic_member_baby_boy_24dp, "儿子" to R.drawable.ic_member_baby_boy_24dp, // 儿子对应男婴图标
"女儿" to R.drawable.ic_member_baby_girl_24dp, "女儿" to R.drawable.ic_member_baby_girl_24dp, // 女儿对应女婴图标
"爸爸" to R.drawable.ic_member_father_24dp, "爸爸" to R.drawable.ic_member_father_24dp, // 爸爸对应父亲图标
"妈妈" to R.drawable.ic_member_mother_24dp, "妈妈" to R.drawable.ic_member_mother_24dp, // 妈妈对应母亲图标
"爷爷" to R.drawable.ic_member_grandfather_24dp, "爷爷" to R.drawable.ic_member_grandfather_24dp, // 爷爷对应祖父图标
"奶奶" to R.drawable.ic_member_grandmother_24dp, "奶奶" to R.drawable.ic_member_grandmother_24dp, // 奶奶对应祖母图标
"男生" to R.drawable.ic_member_boy_24dp, "男生" to R.drawable.ic_member_boy_24dp, // 男生对应男孩图标
"女生" to R.drawable.ic_member_girl_24dp, "女生" to R.drawable.ic_member_girl_24dp, // 女生对应女孩图标
"外公" to R.drawable.ic_member_grandfather_24dp, "外公" to R.drawable.ic_member_grandfather_24dp, // 外公对应祖父图标
"外婆" to R.drawable.ic_member_grandmother_24dp, "外婆" to R.drawable.ic_member_grandmother_24dp, // 外婆对应祖母图标
"其他" to R.drawable.ic_member_girl_24dp "其他" to R.drawable.ic_member_girl_24dp // 其他成员使用女孩图标作为默认值
) )
/**
* 获取分类对应的图标向量
* 用于在Compose UI中直接使用
*
* @param name 分类名称
* @return 对应的图标向量如果未找到则返回null
*/
@Composable @Composable
fun getCategoryIconVector(name: String): ImageVector? { fun getCategoryIconVector(name: String): ImageVector? {
return categoryIcons[name]?.let { ImageVector.vectorResource(id = it) } return categoryIcons[name]?.let { ImageVector.vectorResource(id = it) }
} }
/**
* 获取成员对应的图标向量
* 用于在Compose UI中直接使用
*
* @param name 成员名称
* @return 对应的图标向量如果未找到则返回null
*/
@Composable @Composable
fun getMemberIconVector(name: String): ImageVector? { fun getMemberIconVector(name: String): ImageVector? {
return memberIcons[name]?.let { ImageVector.vectorResource(id = it) } return memberIcons[name]?.let { ImageVector.vectorResource(id = it) }
} }
/**
* 获取分类对应的图标资源ID
*
* @param name 分类名称
* @return 对应的图标资源ID如果未找到则返回null
*/
@DrawableRes @DrawableRes
fun getCategoryIcon(name: String): Int? { fun getCategoryIcon(name: String): Int? {
return categoryIcons[name] return categoryIcons[name]
} }
/**
* 获取成员对应的图标资源ID
*
* @param name 成员名称
* @return 对应的图标资源ID如果未找到则返回null
*/
@DrawableRes @DrawableRes
fun getMemberIcon(name: String): Int? { fun getMemberIcon(name: String): Int? {
return memberIcons[name] return memberIcons[name]
} }
/**
* 获取所有可用的分类图标资源ID列表
*
* @return 所有分类图标的资源ID列表
*/
fun getAllCategoryIcons(): List<Int> { fun getAllCategoryIcons(): List<Int> {
return categoryIcons.values.toList() return categoryIcons.values.toList()
} }
/**
* 获取所有可用的成员图标资源ID列表
*
* @return 所有成员图标的资源ID列表
*/
fun getAllMemberIcons(): List<Int> { fun getAllMemberIcons(): List<Int> {
return memberIcons.values.toList() return memberIcons.values.toList()
} }