feat: 优化月度/年度报表和数据分析页面
- 创建 MonthlyYearlyReport 组件,显示收支对比、盈余状况、储蓄率和日均消费 - 创建 DetailedAnalysisReport 组件,提供详细的分类统计分析 - 支出/收入分类明细与占比 - TOP5分类排行榜(金银铜奖牌设计) - 可视化进度条和百分比显示 - 在 AnalysisScreen 中新增"报表"视图模式 - 支持分类、成员、报表三种视图切换 - 集成月度/年度报表和详细分析报表 - 更新 README:标记月度/年度报表功能为已完成 - 更新 v1.5 版本历史,记录数据分析优化内容 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
e651086e6d
commit
74cc6f36a9
@ -4,7 +4,8 @@
|
|||||||
"Bash(./gradlew:*)",
|
"Bash(./gradlew:*)",
|
||||||
"Bash(git push:*)",
|
"Bash(git push:*)",
|
||||||
"Bash(git branch:*)",
|
"Bash(git branch:*)",
|
||||||
"Bash(git add:*)"
|
"Bash(git add:*)",
|
||||||
|
"Bash(git commit:*)"
|
||||||
],
|
],
|
||||||
"deny": []
|
"deny": []
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@
|
|||||||
### 2. 图表分析 (已完成 🎉)
|
### 2. 图表分析 (已完成 🎉)
|
||||||
- [x] 支出/收入趋势图表
|
- [x] 支出/收入趋势图表
|
||||||
- [x] 分类占比饼图
|
- [x] 分类占比饼图
|
||||||
- [ ] 月度/年度报表
|
- [x] 月度/年度报表
|
||||||
- [x] 成员消费分析
|
- [x] 成员消费分析
|
||||||
- [x] 自定义统计周期
|
- [x] 自定义统计周期
|
||||||
|
|
||||||
@ -82,7 +82,6 @@
|
|||||||
### 6. 体验优化 (持续进行 🔄)
|
### 6. 体验优化 (持续进行 🔄)
|
||||||
- [x] 深色模式支持
|
- [x] 深色模式支持
|
||||||
- [ ] 手势操作优化
|
- [ ] 手势操作优化
|
||||||
- [ ] 快速记账小组件
|
|
||||||
- [ ] 多语言支持
|
- [ ] 多语言支持
|
||||||
- [ ] 自定义主题
|
- [ ] 自定义主题
|
||||||
|
|
||||||
@ -131,6 +130,12 @@
|
|||||||
- 预算编辑对话框
|
- 预算编辑对话框
|
||||||
- 预算状态可视化(进度条、超支提醒)
|
- 预算状态可视化(进度条、超支提醒)
|
||||||
- 预算导航集成
|
- 预算导航集成
|
||||||
|
- 数据分析优化
|
||||||
|
- 月度/年度报表组件
|
||||||
|
- 详细分析报表(分类统计明细)
|
||||||
|
- 收支对比、储蓄率、日均消费分析
|
||||||
|
- TOP分类排行榜
|
||||||
|
- 报表视图集成到分析页面
|
||||||
|
|
||||||
### v1.4
|
### v1.4
|
||||||
- 数据安全功能
|
- 数据安全功能
|
||||||
|
@ -0,0 +1,310 @@
|
|||||||
|
package com.yovinchen.bookkeeping.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.yovinchen.bookkeeping.model.BookkeepingRecord
|
||||||
|
import com.yovinchen.bookkeeping.model.TransactionType
|
||||||
|
import java.text.NumberFormat
|
||||||
|
import java.time.YearMonth
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 详细分析报表组件
|
||||||
|
* 显示按分类统计的详细信息
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun DetailedAnalysisReport(
|
||||||
|
records: List<BookkeepingRecord>,
|
||||||
|
startMonth: YearMonth,
|
||||||
|
endMonth: YearMonth,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val currencyFormatter = NumberFormat.getCurrencyInstance(Locale.CHINA)
|
||||||
|
|
||||||
|
// 按类型分组
|
||||||
|
val incomeRecords = records.filter { it.type == TransactionType.INCOME }
|
||||||
|
val expenseRecords = records.filter { it.type == TransactionType.EXPENSE }
|
||||||
|
|
||||||
|
// 按分类统计
|
||||||
|
val incomeByCategory = incomeRecords.groupBy { it.category }
|
||||||
|
.mapValues { it.value.sumOf { record -> record.amount } }
|
||||||
|
.toList()
|
||||||
|
.sortedByDescending { it.second }
|
||||||
|
|
||||||
|
val expenseByCategory = expenseRecords.groupBy { it.category }
|
||||||
|
.mapValues { it.value.sumOf { record -> record.amount } }
|
||||||
|
.toList()
|
||||||
|
.sortedByDescending { it.second }
|
||||||
|
|
||||||
|
// 总收入和总支出
|
||||||
|
val totalIncome = incomeRecords.sumOf { it.amount }
|
||||||
|
val totalExpense = expenseRecords.sumOf { it.amount }
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
// 时间范围标题
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (startMonth == endMonth) {
|
||||||
|
"统计期间:${startMonth.format(DateTimeFormatter.ofPattern("yyyy年MM月"))}"
|
||||||
|
} else {
|
||||||
|
"统计期间:${startMonth.format(DateTimeFormatter.ofPattern("yyyy年MM月"))} 至 ${endMonth.format(DateTimeFormatter.ofPattern("yyyy年MM月"))}"
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支出分类详情
|
||||||
|
if (expenseByCategory.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
CategoryDetailCard(
|
||||||
|
title = "支出分类明细",
|
||||||
|
categoryData = expenseByCategory,
|
||||||
|
total = totalExpense,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
currencyFormatter = currencyFormatter
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收入分类详情
|
||||||
|
if (incomeByCategory.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
CategoryDetailCard(
|
||||||
|
title = "收入分类明细",
|
||||||
|
categoryData = incomeByCategory,
|
||||||
|
total = totalIncome,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
currencyFormatter = currencyFormatter
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分类占比前5名
|
||||||
|
if (expenseByCategory.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
TopCategoriesCard(
|
||||||
|
title = "支出TOP5",
|
||||||
|
categoryData = expenseByCategory.take(5),
|
||||||
|
total = totalExpense,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
currencyFormatter = currencyFormatter
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类详情卡片
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun CategoryDetailCard(
|
||||||
|
title: String,
|
||||||
|
categoryData: List<Pair<String, Double>>,
|
||||||
|
total: Double,
|
||||||
|
color: Color,
|
||||||
|
currencyFormatter: NumberFormat
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = color,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 总计
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 12.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "总计",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = currencyFormatter.format(total),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = color
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
||||||
|
|
||||||
|
// 分类列表
|
||||||
|
categoryData.forEach { (category, amount) ->
|
||||||
|
val percentage = if (total > 0) (amount / total * 100) else 0.0
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = category,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${String.format("%.1f", percentage)}%",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = currencyFormatter.format(amount),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进度条
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = (percentage / 100).toFloat(),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(4.dp)
|
||||||
|
.clip(RoundedCornerShape(2.dp)),
|
||||||
|
color = color.copy(alpha = 0.8f),
|
||||||
|
trackColor = color.copy(alpha = 0.2f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TOP分类卡片
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun TopCategoriesCard(
|
||||||
|
title: String,
|
||||||
|
categoryData: List<Pair<String, Double>>,
|
||||||
|
total: Double,
|
||||||
|
color: Color,
|
||||||
|
currencyFormatter: NumberFormat
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(bottom = 12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
categoryData.forEachIndexed { index, (category, amount) ->
|
||||||
|
val percentage = if (total > 0) (amount / total * 100) else 0.0
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// 排名
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(
|
||||||
|
when (index) {
|
||||||
|
0 -> Color(0xFFFFD700) // 金色
|
||||||
|
1 -> Color(0xFFC0C0C0) // 银色
|
||||||
|
2 -> Color(0xFFCD7F32) // 铜色
|
||||||
|
else -> MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
}
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${index + 1}",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = if (index < 3) Color.White else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = category,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = if (index == 0) FontWeight.Bold else FontWeight.Normal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.End
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = currencyFormatter.format(amount),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${String.format("%.1f", percentage)}%",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,258 @@
|
|||||||
|
package com.yovinchen.bookkeeping.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.yovinchen.bookkeeping.model.BookkeepingRecord
|
||||||
|
import com.yovinchen.bookkeeping.model.TransactionType
|
||||||
|
import java.text.NumberFormat
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.YearMonth
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 月度/年度报表组件
|
||||||
|
* 显示收支对比、盈余情况等统计信息
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun MonthlyYearlyReport(
|
||||||
|
records: List<BookkeepingRecord>,
|
||||||
|
period: String, // "月度" 或 "年度"
|
||||||
|
startMonth: YearMonth,
|
||||||
|
endMonth: YearMonth,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val totalIncome = records
|
||||||
|
.filter { it.type == TransactionType.INCOME }
|
||||||
|
.sumOf { it.amount }
|
||||||
|
|
||||||
|
val totalExpense = records
|
||||||
|
.filter { it.type == TransactionType.EXPENSE }
|
||||||
|
.sumOf { it.amount }
|
||||||
|
|
||||||
|
val balance = totalIncome - totalExpense
|
||||||
|
val savingsRate = if (totalIncome > 0) {
|
||||||
|
((totalIncome - totalExpense) / totalIncome * 100).coerceAtLeast(0.0)
|
||||||
|
} else 0.0
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
// 标题
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "$period 报表",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = if (startMonth == endMonth) {
|
||||||
|
startMonth.format(DateTimeFormatter.ofPattern("yyyy年MM月"))
|
||||||
|
} else {
|
||||||
|
"${startMonth.format(DateTimeFormatter.ofPattern("yyyy年MM月"))} - ${endMonth.format(DateTimeFormatter.ofPattern("yyyy年MM月"))}"
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider()
|
||||||
|
|
||||||
|
// 收支对比
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
|
// 收入
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "总收入",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = formatCurrency(totalIncome),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分隔线
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(1.dp)
|
||||||
|
.height(40.dp)
|
||||||
|
.background(MaterialTheme.colorScheme.outlineVariant)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 支出
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "总支出",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = formatCurrency(totalExpense),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 盈余情况
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = if (balance >= 0) {
|
||||||
|
MaterialTheme.colorScheme.primaryContainer
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.errorContainer
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (balance >= 0) "盈余" else "亏损",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = if (balance >= 0) {
|
||||||
|
MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onErrorContainer
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = formatCurrency(kotlin.math.abs(balance)),
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = if (balance >= 0) {
|
||||||
|
MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onErrorContainer
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 储蓄率
|
||||||
|
if (totalIncome > 0) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "储蓄率",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.weight(2f)
|
||||||
|
) {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = (savingsRate / 100).toFloat().coerceIn(0f, 1f),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(8.dp)
|
||||||
|
.clip(RoundedCornerShape(4.dp)),
|
||||||
|
color = when {
|
||||||
|
savingsRate >= 30 -> MaterialTheme.colorScheme.primary
|
||||||
|
savingsRate >= 10 -> MaterialTheme.colorScheme.tertiary
|
||||||
|
else -> MaterialTheme.colorScheme.error
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "${String.format("%.1f", savingsRate)}%",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
|
color = when {
|
||||||
|
savingsRate >= 30 -> MaterialTheme.colorScheme.primary
|
||||||
|
savingsRate >= 10 -> MaterialTheme.colorScheme.tertiary
|
||||||
|
else -> MaterialTheme.colorScheme.error
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日均消费
|
||||||
|
val dayCount = if (period == "月度") {
|
||||||
|
// 计算月度天数
|
||||||
|
java.time.temporal.ChronoUnit.DAYS.between(
|
||||||
|
startMonth.atDay(1),
|
||||||
|
endMonth.atEndOfMonth()
|
||||||
|
) + 1
|
||||||
|
} else {
|
||||||
|
// 计算年度天数
|
||||||
|
java.time.temporal.ChronoUnit.DAYS.between(
|
||||||
|
startMonth.atDay(1),
|
||||||
|
endMonth.atEndOfMonth()
|
||||||
|
) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
val dailyAverage = if (dayCount > 0) totalExpense / dayCount else 0.0
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "日均消费",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = formatCurrency(dailyAverage),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化货币
|
||||||
|
*/
|
||||||
|
private fun formatCurrency(amount: Double): String {
|
||||||
|
val format = NumberFormat.getCurrencyInstance(Locale.CHINA)
|
||||||
|
return format.format(amount)
|
||||||
|
}
|
@ -38,12 +38,14 @@ import com.yovinchen.bookkeeping.model.MemberStat
|
|||||||
import com.yovinchen.bookkeeping.ui.components.CategoryPieChart
|
import com.yovinchen.bookkeeping.ui.components.CategoryPieChart
|
||||||
import com.yovinchen.bookkeeping.ui.components.CategoryStatItem
|
import com.yovinchen.bookkeeping.ui.components.CategoryStatItem
|
||||||
import com.yovinchen.bookkeeping.ui.components.DateRangePicker
|
import com.yovinchen.bookkeeping.ui.components.DateRangePicker
|
||||||
|
import com.yovinchen.bookkeeping.ui.components.DetailedAnalysisReport
|
||||||
|
import com.yovinchen.bookkeeping.ui.components.MonthlyYearlyReport
|
||||||
import com.yovinchen.bookkeeping.ui.components.TrendLineChart
|
import com.yovinchen.bookkeeping.ui.components.TrendLineChart
|
||||||
import com.yovinchen.bookkeeping.viewmodel.AnalysisViewModel
|
import com.yovinchen.bookkeeping.viewmodel.AnalysisViewModel
|
||||||
import java.time.YearMonth
|
import java.time.YearMonth
|
||||||
|
|
||||||
enum class ViewMode {
|
enum class ViewMode {
|
||||||
CATEGORY, MEMBER
|
CATEGORY, MEMBER, REPORT
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@ -93,7 +95,13 @@ fun AnalysisScreen(
|
|||||||
Button(
|
Button(
|
||||||
onClick = { showViewModeMenu = true }
|
onClick = { showViewModeMenu = true }
|
||||||
) {
|
) {
|
||||||
Text(if (currentViewMode == ViewMode.CATEGORY) "分类" else "成员")
|
Text(
|
||||||
|
when {
|
||||||
|
currentViewMode == ViewMode.CATEGORY -> "分类"
|
||||||
|
currentViewMode == ViewMode.MEMBER -> "成员"
|
||||||
|
else -> "报表"
|
||||||
|
}
|
||||||
|
)
|
||||||
Icon(Icons.Default.ArrowDropDown, "切换视图")
|
Icon(Icons.Default.ArrowDropDown, "切换视图")
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
@ -114,6 +122,13 @@ fun AnalysisScreen(
|
|||||||
showViewModeMenu = false
|
showViewModeMenu = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("报表") },
|
||||||
|
onClick = {
|
||||||
|
currentViewMode = ViewMode.REPORT
|
||||||
|
showViewModeMenu = false
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,41 +174,68 @@ fun AnalysisScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// 饼图视图
|
if (currentViewMode == ViewMode.REPORT) {
|
||||||
item {
|
// 报表视图
|
||||||
CategoryPieChart(
|
item {
|
||||||
categoryData = categoryStats.map { Pair(it.category, it.percentage.toFloat()) },
|
MonthlyYearlyReport(
|
||||||
memberData = memberStats.map { Pair(it.member, it.percentage.toFloat()) },
|
records = records,
|
||||||
currentViewMode = currentViewMode == ViewMode.MEMBER,
|
period = if (startMonth == endMonth) "月度" else "年度",
|
||||||
modifier = Modifier
|
startMonth = startMonth,
|
||||||
.fillMaxWidth()
|
endMonth = endMonth,
|
||||||
.height(200.dp)
|
modifier = Modifier
|
||||||
.padding(bottom = 16.dp),
|
.fillMaxWidth()
|
||||||
onCategoryClick = { category ->
|
.padding(vertical = 8.dp)
|
||||||
if (currentViewMode == ViewMode.CATEGORY) {
|
)
|
||||||
onNavigateToCategoryDetail(category, startMonth, endMonth)
|
}
|
||||||
} else {
|
|
||||||
onNavigateToMemberDetail(category, startMonth, endMonth, selectedAnalysisType)
|
// 详细分析报表
|
||||||
|
item {
|
||||||
|
DetailedAnalysisReport(
|
||||||
|
records = records,
|
||||||
|
startMonth = startMonth,
|
||||||
|
endMonth = endMonth,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 饼图视图
|
||||||
|
item {
|
||||||
|
CategoryPieChart(
|
||||||
|
categoryData = categoryStats.map { Pair(it.category, it.percentage.toFloat()) },
|
||||||
|
memberData = memberStats.map { Pair(it.member, it.percentage.toFloat()) },
|
||||||
|
currentViewMode = currentViewMode == ViewMode.MEMBER,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(200.dp)
|
||||||
|
.padding(bottom = 16.dp),
|
||||||
|
onCategoryClick = { category ->
|
||||||
|
if (currentViewMode == ViewMode.CATEGORY) {
|
||||||
|
onNavigateToCategoryDetail(category, startMonth, endMonth)
|
||||||
|
} else {
|
||||||
|
onNavigateToMemberDetail(category, startMonth, endMonth, selectedAnalysisType)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 统计列表
|
// 统计列表
|
||||||
items(if (currentViewMode == ViewMode.CATEGORY) categoryStats else memberStats) { stat ->
|
items(if (currentViewMode == ViewMode.CATEGORY) categoryStats else memberStats) { stat ->
|
||||||
val category = if (stat is CategoryStat) stat.category else null
|
val category = if (stat is CategoryStat) stat.category else null
|
||||||
val member = if (stat is MemberStat) stat.member else null
|
val member = if (stat is MemberStat) stat.member else null
|
||||||
|
|
||||||
CategoryStatItem(
|
CategoryStatItem(
|
||||||
stat = stat,
|
stat = stat,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (currentViewMode == ViewMode.CATEGORY && category != null) {
|
if (currentViewMode == ViewMode.CATEGORY && category != null) {
|
||||||
onNavigateToCategoryDetail(category, startMonth, endMonth)
|
onNavigateToCategoryDetail(category, startMonth, endMonth)
|
||||||
} else if (currentViewMode == ViewMode.MEMBER && member != null) {
|
} else if (currentViewMode == ViewMode.MEMBER && member != null) {
|
||||||
onNavigateToMemberDetail(member, startMonth, endMonth, selectedAnalysisType)
|
onNavigateToMemberDetail(member, startMonth, endMonth, selectedAnalysisType)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user