Files
TaskTTL/composeApp/src/commonMain/kotlin/com/taskttl/presentation/statistics/StatisticsScreen.kt
2025-10-15 15:03:16 +08:00

354 lines
13 KiB
Kotlin

package com.taskttl.presentation.statistics
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Assignment
import androidx.compose.material.icons.automirrored.filled.TrendingUp
import androidx.compose.material.icons.filled.CalendarToday
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Schedule
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
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.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.taskttl.core.routes.Routes
import com.taskttl.core.ui.Chip
import com.taskttl.data.local.model.Category
import com.taskttl.data.state.CountdownIntent
import com.taskttl.data.state.TaskIntent
import com.taskttl.data.viewmodel.CountdownViewModel
import com.taskttl.data.viewmodel.TaskViewModel
import com.taskttl.ui.components.AppHeader
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.koin.compose.viewmodel.koinViewModel
import taskttl.composeapp.generated.resources.Res
import taskttl.composeapp.generated.resources.category_countdown
import taskttl.composeapp.generated.resources.category_statistics
import taskttl.composeapp.generated.resources.category_task
import taskttl.composeapp.generated.resources.completed
import taskttl.composeapp.generated.resources.completion_rate
import taskttl.composeapp.generated.resources.overview
import taskttl.composeapp.generated.resources.setting_category_management
import taskttl.composeapp.generated.resources.title_statistics
import taskttl.composeapp.generated.resources.total_tasks
@Composable
@Preview
fun StatisticsScreen(
navController: NavHostController,
taskViewModel: TaskViewModel = koinViewModel(),
countdownViewModel: CountdownViewModel = koinViewModel(),
) {
val taskState by taskViewModel.state.collectAsState()
val countdownState by countdownViewModel.state.collectAsState()
LaunchedEffect(Unit) {
taskViewModel.processIntent(TaskIntent.LoadTasks)
countdownViewModel.handleIntent(CountdownIntent.LoadCountdowns)
}
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
) {
AppHeader(
title = Res.string.title_statistics,
trailingIcon = Icons.Default.CalendarToday
)
LazyColumn(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// 总览统计
item {
Text(
text = stringResource(Res.string.overview),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
}
item {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
StatisticCard(
titleRes = Res.string.total_tasks,
value = taskState.tasks.size.toString(),
icon = Icons.AutoMirrored.Filled.Assignment,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.weight(1f)
)
StatisticCard(
titleRes = Res.string.completed,
value = taskState.tasks.count { it.isCompleted }.toString(),
icon = Icons.Default.CheckCircle,
color = Color(0xFF4CAF50),
modifier = Modifier.weight(1f)
)
}
}
item {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
StatisticCard(
titleRes = Res.string.category_countdown,
value = countdownState.countdowns.size.toString(),
icon = Icons.Default.Schedule,
color = Color(0xFFFF9800),
modifier = Modifier.weight(1f)
)
val completionRate = if (taskState.tasks.isNotEmpty()) {
(taskState.tasks.count { it.isCompleted }
.toFloat() / taskState.tasks.size * 100).toInt()
} else 0
StatisticCard(
titleRes = Res.string.completion_rate,
value = "$completionRate%",
icon = Icons.AutoMirrored.Filled.TrendingUp,
color = Color(0xFF9C27B0),
modifier = Modifier.weight(1f)
)
}
}
// 分类统计
item {
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(Res.string.category_statistics),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
TextButton(onClick = {
navController.navigate(Routes.Main.Settings.CategoryManagement)
}) {
Text(stringResource(Res.string.setting_category_management))
}
}
}
item {
taskState.categories.let {
taskState.categories.forEach { category ->
val categoryTasks = taskState.tasks.filter { it.category == category }
val completedTasks = categoryTasks.count { it.isCompleted }
CategoryStatisticItem(
category = category,
totalCount = categoryTasks.size,
completedCount = completedTasks,
typeRes = Res.string.category_task
)
}
}
countdownState.categories.let {
countdownState.categories.forEach { category ->
val categoryCountdowns =
countdownState.countdowns.filter { it.category == category }
val activeCountdowns = categoryCountdowns.count { it.isActive }
CategoryStatisticItem(
category = category,
totalCount = categoryCountdowns.size,
completedCount = activeCountdowns,
typeRes = Res.string.category_countdown
)
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun StatisticCard(
titleRes: StringResource,
value: String,
icon: ImageVector,
color: Color,
modifier: Modifier = Modifier,
) {
Card(
modifier = modifier,
colors = CardDefaults.cardColors(containerColor = color.copy(alpha = 0.1f))
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = icon,
contentDescription = stringResource(titleRes),
tint = color,
modifier = Modifier.size(32.dp)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = value,
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = color
)
Text(
text = stringResource(titleRes),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CategoryStatisticItem(
category: Category,
totalCount: Int,
completedCount: Int,
typeRes: StringResource,
) {
if (totalCount == 0) return
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color.White),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 分类颜色指示器
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(category.color.backgroundColor),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = category.icon.icon,
contentDescription = stringResource(category.icon.displayNameRes),
tint = category.color.iconColor,
modifier = Modifier.size(24.dp)
)
}
Spacer(modifier = Modifier.width(16.dp))
// 分类信息
Column(
modifier = Modifier.weight(1f)
) {
Row() {
Text(
text = category.name,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
color = category.color.textColor
)
Spacer(modifier = Modifier.width(6.dp))
Chip(
textRes = category.type.displayNameRes,
)
}
Text(
text = "${stringResource(typeRes)}: $completedCount/$totalCount",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
// 进度条
Column(
horizontalAlignment = Alignment.End
) {
val progress = if (totalCount > 0) completedCount.toFloat() / totalCount else 0f
Text(
text = "${(progress * 100).toInt()}%",
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Medium,
color = category.color.textColor
)
Spacer(modifier = Modifier.height(4.dp))
LinearProgressIndicator(
progress = { progress },
modifier = Modifier
.width(80.dp)
.height(6.dp)
.clip(RoundedCornerShape(3.dp)),
color = category.color.textColor,
trackColor = ProgressIndicatorDefaults.linearTrackColor,
strokeCap = ProgressIndicatorDefaults.LinearStrokeCap,
)
}
}
}
Spacer(modifier = Modifier.height(10.dp))
}