更新
This commit is contained in:
@@ -0,0 +1,352 @@
|
||||
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.handleIntent(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(Color(0xFFF5F5F5))
|
||||
.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))
|
||||
}
|
||||
Reference in New Issue
Block a user