package com.taskttl.presentation.task import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource 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.aspectRatio 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.lazy.itemsIndexed import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.outlined.Circle import androidx.compose.material.icons.rounded.Delete import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Checkbox import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FilledIconButton import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect 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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import com.taskttl.core.routes.Routes import com.taskttl.core.ui.ActionButtonListItem import com.taskttl.core.ui.ErrorDialog import com.taskttl.core.ui.LoadingOverlay import com.taskttl.core.utils.ToastUtils import com.taskttl.data.local.model.Task import com.taskttl.data.state.TaskEffect import com.taskttl.data.state.TaskIntent import com.taskttl.data.viewmodel.TaskViewModel import com.taskttl.ui.components.AppHeader import com.taskttl.ui.components.CategoryFilter import com.taskttl.ui.components.SearchBar 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.desc_completed import taskttl.composeapp.generated.resources.desc_incomplete import taskttl.composeapp.generated.resources.label_show_completed import taskttl.composeapp.generated.resources.label_task_list import taskttl.composeapp.generated.resources.text_add_task_hint import taskttl.composeapp.generated.resources.text_no_tasks import taskttl.composeapp.generated.resources.title_add_task import taskttl.composeapp.generated.resources.title_task @Composable @Preview fun TaskScreen( navController: NavHostController, viewModel: TaskViewModel = koinViewModel() ) { val state by viewModel.state.collectAsState() var isOpenIndex by remember { mutableStateOf(null) } fun closeExpandedItem() { isOpenIndex = null } LaunchedEffect(Unit) { viewModel.effects.collect { effect -> when (effect) { is TaskEffect.ShowMessage -> { ToastUtils.show(effect.message) } is TaskEffect.NavigateToTaskDetail -> { navController.navigate(Routes.Main.Task.TaskDetail(effect.taskId)) } else -> {} } } } state.error?.let { error -> ErrorDialog( errorMessage = state.error, onDismiss = { viewModel.handleIntent(TaskIntent.ClearError) } ) } Box( modifier = Modifier .fillMaxSize() .background(Color.White) ) { Column( modifier = Modifier.fillMaxSize() ) { AppHeader( title = Res.string.title_task, trailingIcon = Icons.Default.Search, onTrailingClick = { viewModel.handleIntent(TaskIntent.SearchView) } ) Column( modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background) .padding(16.dp) ) { if (state.isSearch) { SearchBar( query = state.searchQuery, onQueryChange = { viewModel.handleIntent(TaskIntent.SearchTasks(it)) }, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(8.dp)) } CategoryFilter( categories = state.categories, selectedCategory = state.selectedCategory, onCategorySelected = { viewModel.handleIntent(TaskIntent.FilterByCategory(it)) }, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(8.dp)) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text( text = "${stringResource(Res.string.label_task_list)} (${state.filteredTasks.size})", style = MaterialTheme.typography.titleMedium ) Row(verticalAlignment = Alignment.CenterVertically) { Checkbox( checked = state.showCompleted, onCheckedChange = { viewModel.handleIntent(TaskIntent.ToggleShowCompleted(it)) } ) Text( stringResource(Res.string.label_show_completed), style = MaterialTheme.typography.bodySmall ) } } Spacer(modifier = Modifier.height(8.dp)) when { state.isLoading -> { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { CircularProgressIndicator() } } state.filteredTasks.isEmpty() -> { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( text = stringResource(Res.string.text_no_tasks), style = MaterialTheme.typography.bodyLarge ) Spacer(modifier = Modifier.height(8.dp)) Text( text = stringResource(Res.string.text_add_task_hint), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) } } } else -> { LazyColumn( modifier = Modifier.fillMaxSize().clickable( indication = null, interactionSource = remember { MutableInteractionSource() } ) { if (isOpenIndex != null) closeExpandedItem() }, verticalArrangement = Arrangement.spacedBy(8.dp) ) { item { Spacer(Modifier) } itemsIndexed( state.filteredTasks, key = { index, item -> item.id }) { index, task -> var isOpen by remember { mutableStateOf(false) } TaskCardItem( task = task, isOpen = isOpenIndex == index && isOpen, onOpenChange = { isOpenIndex = if (it) index else null isOpen = it }, onClick = { navController.navigate(Routes.Main.Task.TaskDetail(task.id)) }, onToggleComplete = { viewModel.handleIntent(TaskIntent.ToggleTaskCompletion(task.id)) }, onDeleteTask = { viewModel.handleIntent(TaskIntent.DeleteTask(task.id)) } ) } } } } } } // 悬浮按钮 FloatingActionButton( onClick = { navController.navigate(Routes.Main.Task.AddTask) }, modifier = Modifier .align(Alignment.BottomEnd) .padding(20.dp), containerColor = Color(0xFF667EEA) ) { Icon( imageVector = Icons.Default.Add, contentDescription = stringResource(Res.string.title_add_task) ) } LoadingOverlay(state.isLoading) } } /** * 任务卡项目 * @param [task] 任务 * @param [isOpen] 是开放 * @param [alignment] 对齐 * @param [onOpenChange] 论开放式变革 * @param [onClick] 单击 * @param [onToggleComplete] 切换完成 * @param [onDeleteTask] 关于删除任务 * @param [modifier] 修饰符 */ @Composable fun TaskCardItem( task: Task, isOpen: Boolean, alignment: Alignment.Horizontal = Alignment.End, onOpenChange: (Boolean) -> Unit, onClick: () -> Unit, onToggleComplete: () -> Unit, onDeleteTask: () -> Unit, modifier: Modifier = Modifier ) { ActionButtonListItem( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(12.dp)), isOpen = isOpen, actionAlignment = alignment, onOpenChange = onOpenChange, onClick = onClick ) { Card( modifier = modifier .fillMaxWidth() .background(Color.Transparent, shape = RoundedCornerShape(12.dp)) .clickable { onClick.invoke() }, colors = CardDefaults.cardColors(containerColor = Color.White), elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) ) { Row( modifier = Modifier .fillMaxWidth() .padding(16.dp), verticalAlignment = Alignment.CenterVertically ) { // 完成状态按钮 IconButton( onClick = onToggleComplete, modifier = Modifier.size(24.dp) ) { val isCompleted = if (task.isCompleted) Res.string.desc_completed else Res.string.desc_incomplete Icon( imageVector = if (task.isCompleted) Icons.Filled.CheckCircle else Icons.Outlined.Circle, contentDescription = stringResource(isCompleted), tint = if (task.isCompleted) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant ) } Spacer(modifier = Modifier.width(12.dp)) // 任务内容 Column( modifier = Modifier.weight(1f) ) { Text( text = task.title, style = MaterialTheme.typography.titleMedium, textDecoration = if (task.isCompleted) TextDecoration.LineThrough else null, color = if (task.isCompleted) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface, maxLines = 1, overflow = TextOverflow.Ellipsis ) if (task.description.isNotBlank()) { Spacer(modifier = Modifier.height(4.dp)) Text( text = task.description, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 2, overflow = TextOverflow.Ellipsis ) } } Spacer(modifier = Modifier.width(12.dp)) Row( verticalAlignment = Alignment.CenterVertically ) { // 优先级标签 Box( modifier = Modifier .background( color = task.priority.color.copy(alpha = 0.1f), shape = RoundedCornerShape(12.dp) ) .padding(horizontal = 8.dp, vertical = 4.dp) ) { Text( text = stringResource(task.priority.displayNameRes), style = MaterialTheme.typography.labelSmall, color = task.priority.color ) } Spacer(modifier = Modifier.width(8.dp)) // 分类标签 Box( modifier = Modifier .background( color = task.category.color.backgroundColor, shape = RoundedCornerShape(12.dp) ) .padding(horizontal = 8.dp, vertical = 4.dp) ) { Text( text = task.category.name, style = MaterialTheme.typography.labelSmall, color = task.category.color.textColor ) } } } } FilledIconButton( onClick = { onDeleteTask.invoke() }, shape = CircleShape, modifier = Modifier .size(32.dp) .aspectRatio(1f), colors = IconButtonDefaults.filledIconButtonColors( containerColor = Color(0xffff1111), contentColor = Color.White ) ) { Icon( imageVector = Icons.Rounded.Delete, contentDescription = Icons.Rounded.Delete.name ) } } }