This commit is contained in:
2025-10-08 18:08:15 +08:00
parent dc71bb19a9
commit 989e5be041
109 changed files with 10815 additions and 170 deletions

View File

@@ -0,0 +1,232 @@
package com.taskttl.presentation.task
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.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Label
import androidx.compose.material.icons.filled.CalendarToday
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Timer
import androidx.compose.material.icons.outlined.Circle
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
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 org.jetbrains.compose.resources.stringResource
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_created_at
import taskttl.composeapp.generated.resources.label_description
import taskttl.composeapp.generated.resources.label_due_date
import taskttl.composeapp.generated.resources.label_none
import taskttl.composeapp.generated.resources.text_task_not_found
import taskttl.composeapp.generated.resources.title_task_info
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TaskDetailScreen(
taskId: String,
onNavigateBack: () -> Unit,
onNavigateToEdit: () -> Unit,
viewModel: TaskViewModel = koinViewModel()
) {
val state by viewModel.state.collectAsState()
val task = state.tasks.find { it.id == taskId }
LaunchedEffect(Unit) {
viewModel.effects.collect { effect ->
when (effect) {
is TaskEffect.NavigateBack -> {
onNavigateBack.invoke()
}
is TaskEffect.NavigateToEditTask -> {
onNavigateToEdit.invoke()
}
else -> {}
}
}
}
if (task == null) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(stringResource(Res.string.text_task_not_found))
}
return
}
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
) {
Column(
modifier = Modifier.fillMaxSize()
) {
AppHeader(
title = Res.string.title_task_info,
showBack = true,
onBackClick = { viewModel.handleIntent(TaskIntent.NavigateBack) },
trailingIcon = Icons.Default.Edit,
onTrailingClick = { viewModel.handleIntent(TaskIntent.NavigateToEditTask) }
)
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp),
verticalArrangement = Arrangement.Top
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
// Checkbox 圆点
IconButton(
onClick = { viewModel.handleIntent(TaskIntent.ToggleTaskCompletion(taskId)) },
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))
Text(
text = task.title,
modifier = Modifier.weight(1f),
fontSize = 20.sp,
color = Color(0xFF333333),
fontWeight = FontWeight.SemiBold
)
// 优先级指示器
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.height(16.dp))
// 类型
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
.background(Color(0xFFF8F9FA), RoundedCornerShape(12.dp))
.padding(12.dp)
) {
Icon(Icons.AutoMirrored.Filled.Label, contentDescription = null)
Spacer(modifier = Modifier.width(12.dp))
Text(text = task.category.name, color = task.category.color.textColor)
}
// 截止日期
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
.background(Color(0xFFF8F9FA), RoundedCornerShape(12.dp))
.padding(12.dp)
) {
Icon(Icons.Default.CalendarToday, contentDescription = null)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "${stringResource(Res.string.label_due_date)}${
task.dueDate ?: stringResource(Res.string.label_none)
}"
)
}
// 创建日期
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
.background(Color(0xFFF8F9FA), RoundedCornerShape(12.dp))
.padding(12.dp)
) {
Icon(Icons.Default.Timer, contentDescription = null)
Spacer(modifier = Modifier.width(12.dp))
Text(text = "${stringResource(Res.string.label_created_at)}${task.createdAt}")
}
Spacer(modifier = Modifier.height(20.dp))
Text(
text = stringResource(Res.string.label_description),
fontWeight = FontWeight.SemiBold,
color = Color(0xFF333333)
)
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
.background(Color(0xFFF8F9FA), RoundedCornerShape(8.dp))
.padding(15.dp)
) {
Text(text = task.description, color = Color(0xFF666666), lineHeight = 20.sp)
}
}
}
}
}

View File

@@ -0,0 +1,281 @@
package com.taskttl.presentation.task
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.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
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.Modifier
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.taskttl.data.local.model.Category
import com.taskttl.data.local.model.Task
import com.taskttl.data.local.model.TaskPriority
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.CategoryCard
import com.taskttl.ui.components.CompactDatePickerDialog
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
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_select_date
import taskttl.composeapp.generated.resources.hint_tags
import taskttl.composeapp.generated.resources.title_add_task
import taskttl.composeapp.generated.resources.title_due_date
import taskttl.composeapp.generated.resources.title_edit_task
import taskttl.composeapp.generated.resources.title_priority
import taskttl.composeapp.generated.resources.title_select_category
import taskttl.composeapp.generated.resources.title_tags
import taskttl.composeapp.generated.resources.title_task_description
import taskttl.composeapp.generated.resources.title_task_title
import kotlin.time.Clock
import kotlin.time.ExperimentalTime
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@OptIn(ExperimentalMaterial3Api::class, ExperimentalTime::class, ExperimentalUuidApi::class)
@Composable
@Preview
fun TaskEditorScreen(
taskId: String? = null,
onNavigateBack: () -> Unit,
viewModel: TaskViewModel = koinViewModel()
) {
LaunchedEffect(taskId) {
taskId?.let { viewModel.handleIntent(TaskIntent.GetTaskById(it)) }
}
val state by viewModel.state.collectAsState()
val existingTask = state.editingTask
var showDatePicker by remember { mutableStateOf(false) }
var title by remember { mutableStateOf("") }
var description by remember { mutableStateOf("") }
var selectedCategory by remember { mutableStateOf<Category?>(null) }
var selectedPriority by remember { mutableStateOf(TaskPriority.MEDIUM) }
var dueDate by remember { mutableStateOf(existingTask?.dueDate) }
var tags by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
viewModel.effects.collect { effect ->
when (effect) {
is TaskEffect.NavigateBack -> {
onNavigateBack.invoke()
}
else -> {}
}
}
}
LaunchedEffect(existingTask) {
existingTask?.let {
title = it.title
description = it.description
selectedCategory = it.category
selectedPriority = it.priority
dueDate = it.dueDate
tags = it.tags.joinToString(",")
}
}
Box(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier.fillMaxSize()
) {
AppHeader(
title = if (taskId == null) Res.string.title_add_task else Res.string.title_edit_task,
showBack = true,
onBackClick = { viewModel.handleIntent(TaskIntent.NavigateBack) },
trailingIcon = if (taskId == null) Icons.Default.Add else Icons.Default.Edit,
onTrailingClick = {
if (title.isNotBlank() && selectedCategory != null) {
val now = Clock.System.now()
.toLocalDateTime(TimeZone.currentSystemDefault())
val task = Task(
id = existingTask?.id ?: Uuid.random().toString(),
title = title.trim(),
description = description.trim(),
category = selectedCategory!!,
priority = selectedPriority,
createdAt = now,
updatedAt = now,
dueDate = dueDate,
tags = tags.split(",").map { it.trim() }.filter { it.isNotEmpty() }
)
if (taskId == null) {
viewModel.handleIntent(TaskIntent.AddTask(task))
} else {
existingTask?.let { task.isCompleted = existingTask.isCompleted }
viewModel.handleIntent(TaskIntent.UpdateTask(task))
}
}
}
)
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp),
) {
// 任务标题
OutlinedTextField(
value = title,
onValueChange = { title = it },
label = { Text(stringResource(Res.string.title_task_title)) },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
Spacer(modifier = Modifier.height(16.dp))
// 任务描述
OutlinedTextField(
value = description,
onValueChange = { description = it },
label = { Text(stringResource(Res.string.title_task_description)) },
modifier = Modifier.fillMaxWidth(),
minLines = 3,
maxLines = 5
)
Spacer(modifier = Modifier.height(24.dp))
// 分类选择
Text(
text = stringResource(Res.string.title_select_category),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(12.dp))
LazyVerticalGrid(
columns = GridCells.Fixed(4),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.height(80.dp)
) {
items(state.categories) { category ->
CategoryCard(
category = category,
isSelected = selectedCategory == category,
onClick = { selectedCategory = category }
)
}
}
Spacer(modifier = Modifier.height(24.dp))
// 优先级选择
Text(
text = stringResource(Res.string.title_priority),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
TaskPriority.entries.forEach { priority ->
FilterChip(
selected = selectedPriority == priority,
onClick = { selectedPriority = priority },
label = {
Text(
text = stringResource(priority.displayNameRes),
textAlign = TextAlign.Center,
fontSize = 10.sp,
modifier = Modifier.fillMaxWidth()
)
},
modifier = Modifier.weight(1f),
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = priority.color.copy(alpha = 0.2f),
selectedLabelColor = priority.color
),
)
}
}
Spacer(modifier = Modifier.height(24.dp))
// 截止日期
OutlinedTextField(
value = dueDate?.toString() ?: "",
onValueChange = { },
label = { Text(stringResource(Res.string.title_due_date)) },
modifier = Modifier.fillMaxWidth(),
readOnly = true,
trailingIcon = {
IconButton(onClick = { showDatePicker = true }) {
Icon(
Icons.Default.DateRange,
contentDescription = stringResource(Res.string.desc_select_date)
)
}
}
)
CompactDatePickerDialog(
show = showDatePicker,
initialSelected = dueDate,
onConfirm = { selected -> dueDate = selected },
onDismiss = { showDatePicker = false }
)
Spacer(modifier = Modifier.height(16.dp))
// 标签
OutlinedTextField(
value = tags,
onValueChange = { tags = it },
label = { Text(stringResource(Res.string.title_tags)) },
modifier = Modifier.fillMaxWidth(),
placeholder = { Text(stringResource(Res.string.hint_tags)) }
)
}
}
}
}

View File

@@ -0,0 +1,396 @@
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.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<Int?>(null) }
fun closeExpandedItem() {
isOpenIndex = null
}
LaunchedEffect(Unit) {
viewModel.effects.collect { effect ->
when (effect) {
is TaskEffect.NavigateToTaskDetail -> {
navController.navigate(Routes.Main.Task.TaskDetail(effect.taskId))
}
else -> {}
}
}
}
state.error?.let { error ->
LaunchedEffect(error) { 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(Color(0xFFF5F5F5))
.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.title }) { 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)
)
}
}
}
/**
* 任务卡项目
* @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
)
}
}
}